import { type DB, sql } from "@umt/db"; import { createMockQb, createTempTree, type MockQb, makeTorrent, type TempTree } from "@umt/testkit"; import type { Kysely } from "kysely"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { evaluateAcquisition, subscriptionSavePath, } from "../../src/modules/acquisition-organize/services/acquisition.ts"; import { submitDownload } from "../../src/modules/acquisition-organize/services/downloads.ts"; import { planImport } from "../../src/modules/acquisition-organize/services/organize.ts"; import { allOfAny, chainHard, mergeHard } from "../../src/modules/acquisition-organize/services/rules.ts"; import { calendarLabel, releaseDateOf } from "../../src/modules/acquisition-organize/services/subscription-calendar.ts"; import { parseMpEffect, parseMpResolution, parseMpSource, presetConditions, } from "../../src/modules/acquisition-organize/services/subscriptions-params.ts"; import { createHarness, fakeDisk, type Harness, pgAvailable, seedBase, seedEntity, seedRelease, } from "../acquisition-organize/harness.ts"; // C1-A 订阅遗留收尾:包含 / 排除 → 订阅私有规则组(评估链纳入、随订阅删除)、MP 分辨率 / 来源 / 特效 → 收录版本、 // 保存路径经路径映射传给 qB、库根用于首次整理、下载器实例、重置进度、日历。 const hasPg = await pgAvailable(); const SIZE = 300_000; describe("C1-A 纯函数", () => { it("MP 质量字段:标签 / 正则值 / 枚举都能识别", () => { expect(parseMpResolution("4K|2160p|x2160")).toBe("4k"); expect(parseMpResolution("1080[pi]|x1080")).toBe("1080p"); expect(parseMpResolution("720P")).toBe("720p"); expect(parseMpResolution("480p")).toBeNull(); expect(parseMpSource("Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|MiniBD")).toBe("bluray_disc"); expect(parseMpSource("Remux")).toBe("remux"); expect(parseMpSource("Blu-?Ray")).toBe("bluray"); expect(parseMpSource("UHD|UltraHD")).toBe("uhd"); expect(parseMpSource("WEB-?DL|WEB-?RIP")).toBe("web"); expect(parseMpSource("[Hx].?265|HEVC")).toBe("h265"); expect(parseMpEffect("Dolby[\\s.]+Vision|DOVI|[\\s.]+DV[\\s.]+")).toBe("dv"); expect(parseMpEffect("Dolby[\\s.]*\\+?Atmos|Atmos")).toBe("atmos"); expect(parseMpEffect("[\\s.]+HDR[\\s.]+|HDR10|HDR10\\+")).toBe("hdr"); expect(parseMpEffect("SDR")).toBe("sdr"); }); it("质量预设 → 硬条件 / 软偏好;UHD / Atmos 只能走包含", () => { expect(presetConditions({ resolution: "4k", source: "web", effect: "dv" })).toEqual({ hard: { resolutions: ["2160p"], sources: ["WEB-DL", "WEBRip"], hdr: { anyOf: ["DV"] } }, soft: { sources: ["WEB-DL", "WEBRip"], hdr: ["DV", "HDR10+", "HDR10"] }, include: [], }); expect(presetConditions({ source: "uhd", effect: "atmos" }).include).toEqual(["UHD|Ultra-?HD", "Atmos"]); expect(presetConditions({ effect: "sdr" }).hard).toEqual({ hdr: { requireSdr: true } }); }); it("包含取 AND:多组合成前瞻正则;排除取并集", () => { expect(allOfAny([["a", "b"]])).toEqual(["a", "b"]); const [re] = allOfAny([["中字", "CHS"], ["UHD"]]); const rx = new RegExp(re!, "i"); expect(rx.test("Show 2160p UHD 中字")).toBe(true); expect(rx.test("Show 2160p 中字")).toBe(false); expect(chainHard([{ hard: { include: ["x"] } }, { hard: { exclude: ["y"] } }, { hard: {} }])).toEqual({ include: ["x"], exclude: ["y"], }); expect(mergeHard({ exclude: ["a"] }, { exclude: ["b"] })).toEqual({ exclude: ["a", "b"] }); expect(mergeHard({ include: ["a"] }, {})).toEqual({ include: ["a"] }); }); it("保存路径:本机路径经映射换成下载器路径;下载器侧路径原样;都不匹配为 null;无映射同路径", () => { const t = { inst: { id: "i1" } as never, mappings: [{ instanceId: "i1", downloaderPath: "/downloads", hostPath: "/mnt/cache" }], }; expect(subscriptionSavePath(t, "/mnt/cache/tv/")).toBe("/downloads/tv"); expect(subscriptionSavePath(t, "/downloads/tv")).toBe("/downloads/tv"); expect(subscriptionSavePath(t, "/elsewhere")).toBeNull(); expect(subscriptionSavePath({ inst: t.inst, mappings: [] }, "/x/y")).toBe("/x/y"); }); it("日历标签与上映日期", () => { expect(calendarLabel(1, [3])).toBe("S01E03"); expect(calendarLabel(1, [4, 3])).toBe("S01E03–E04"); expect(calendarLabel(2, [1, 3])).toBe("S02E01、E03"); expect(calendarLabel(null, [])).toBe("当期"); expect(releaseDateOf({ releaseDate: { value: "2026-10-02", source: "tmdb" } })).toBe("2026-10-02"); expect(releaseDateOf({})).toBeNull(); }); }); describe.skipIf(!hasPg)("C1-A 订阅遗留收尾(真实 PG + mock qB)", () => { let h: Harness; let qb: MockQb; let lib: TempTree; let lib2: TempTree; let cache: TempTree; const disk = fakeDisk(10_000_000_000); const torrents = new Map(); let base: Awaited>; const db = (): Kysely => h.db; beforeAll(async () => { qb = await createMockQb({ addVisibleAfterPolls: 0 }); lib = await createTempTree({}, { sentinel: true, prefix: "umt-c1a-lib-" }); lib2 = await createTempTree({}, { sentinel: true, prefix: "umt-c1a-lib2-" }); cache = await createTempTree({ "Root.Pick.2021.1080p.BluRay.x264-GRP.mkv": SIZE }, { prefix: "umt-c1a-cache-" }); disk.state.watch = [cache.root]; disk.setPct(10); h = await createHarness("subsc1a", { diskUsage: disk.fn, torrentSource: async ({ release }) => ({ kind: "file", bytes: torrents.get(release.id)! }), }); base = await seedBase(h.db, { libraryPath: lib.root, cachePath: cache.root, qbUrl: qb.url }); }, 60_000); afterAll(async () => { await h?.close(); await qb?.close(); for (const t of [lib, lib2, cache]) await t?.cleanup(); }); async function release(entityId: string, title: string) { const t = makeTorrent({ name: `${title}.mkv`, length: SIZE, pieceSeed: Math.floor(Math.random() * 1e6) }); const id = await seedRelease(db(), { siteId: base.siteId, entityId, title, size: SIZE, hash: t.hash }); torrents.set(id, t.bytes); return id; } const privateGroups = (subId: string) => db() .selectFrom("rule_group") .selectAll() .where(sql`scope ->> 'subscriptionId'`, "=", subId) .orderBy("created_at") .execute(); const slotsOf = (subId: string) => db().selectFrom("acquisition_slot").selectAll().where("subscription_id", "=", subId).orderBy("position").execute(); it("包含 / 排除 → 私有规则组:规则页不列出;修改 revision+1;清空软删;删除订阅软删,重新订阅重建", async () => { const e = await seedEntity(db(), { kind: "series", title: "私有组剧", year: 2025 }); const c = await h.api("POST", "/subscriptions", { mediaEntityId: e, mediaKind: "series", params: { include: "中字|CHS", exclude: ["HDTV"] }, }); expect(c.status, JSON.stringify(c.body)).toBe(201); const id = c.body.data.id as string; let gs = await privateGroups(id); expect(gs).toHaveLength(1); expect(gs[0]).toMatchObject({ kind: "acquisition", deleted_at: null, revision: 1, ladder_required: false }); expect(gs[0]!.hard).toEqual({ include: ["中字|CHS"], exclude: ["HDTV"] }); const list = await h.api("GET", "/rules?limit=200"); expect(list.body.data.items.some((g: { id: string }) => g.id === gs[0]!.id)).toBe(false); const p = await h.api("PATCH", `/subscriptions/${id}`, { params: { include: ["中字"], exclude: ["HDTV", "CAM"] }, }); expect(p.status).toBe(200); gs = await privateGroups(id); expect(gs[0]).toMatchObject({ revision: 2, hard: { include: ["中字"], exclude: ["HDTV", "CAM"] } }); await h.api("PATCH", `/subscriptions/${id}`, { params: {} }); expect((await privateGroups(id))[0]!.deleted_at).not.toBeNull(); await h.api("PATCH", `/subscriptions/${id}`, { params: { exclude: ["CAM"] } }); const live = (await privateGroups(id)).filter((g) => !g.deleted_at); expect(live).toHaveLength(1); expect(live[0]!.hard).toEqual({ exclude: ["CAM"] }); const del = await h.api("POST", "/subscriptions/batch", { ids: [id], action: "delete" }); expect(del.body.data.results[0].ok).toBe(true); expect((await privateGroups(id)).filter((g) => !g.deleted_at)).toHaveLength(0); const re = await h.api("POST", `/subscriptions/${id}/resubscribe`); expect(re.status, JSON.stringify(re.body)).toBe(200); const again = (await privateGroups(id)).filter((g) => !g.deleted_at); expect(again).toHaveLength(1); expect(again[0]!.hard).toEqual({ exclude: ["CAM"] }); }); it("评估链纳入私有组:包含没命中 / 排除命中都拒绝;命中包含建计划;与收录版本自己的包含取 AND", async () => { const e = await seedEntity(db(), { kind: "movie", title: "Filter Film", year: 2024 }); const c = await h.api("POST", "/subscriptions", { mediaEntityId: e, mediaKind: "movie", params: { include: ["CHS"], exclude: ["HDTV"], resolution: "1080P" }, }); expect(c.status, JSON.stringify(c.body)).toBe(201); const [slot] = await slotsOf(c.body.data.id); expect(slot).toBeTruthy(); const noInc = await release(e, "Filter.Film.2024.1080p.WEB-DL.x264-GRP"); const r1 = await evaluateAcquisition(h.d, { sourceReleaseId: noInc }); expect(r1.plans).toHaveLength(0); const cand = await db() .selectFrom("artifact") .select("content") .where("title", "=", "candidate_evaluation") .where(sql`content->>'sourceReleaseId' = ${noInc}`) .executeTakeFirstOrThrow(); expect(JSON.stringify(cand.content)).toContain("include_missed"); const excl = await release(e, "Filter.Film.2024.1080p.HDTV.CHS.x264-GRP"); expect((await evaluateAcquisition(h.d, { sourceReleaseId: excl })).plans).toHaveLength(0); const ok = await release(e, "Filter.Film.2024.1080p.WEB-DL.CHS.x264-GRP"); const r3 = await evaluateAcquisition(h.d, { sourceReleaseId: ok }); expect(r3.plans, JSON.stringify(r3)).toHaveLength(1); // 收录版本自己也要求 "GRP2":与私有组的 CHS 取 AND await db().updateTable("acquisition_plan").set({ status: "cancelled" }).where("id", "=", r3.plans[0]!).execute(); await db() .updateTable("acquisition_slot") .set({ status: "empty", hard_conditions: JSON.stringify({ resolutions: ["1080p", "1080i"], include: ["GRP2"] }), }) .where("id", "=", slot!.id) .execute(); const onlyGrp = await release(e, "Filter.Film.2024.1080p.WEB-DL.x264-GRP2"); expect((await evaluateAcquisition(h.d, { sourceReleaseId: onlyGrp })).plans).toHaveLength(0); const both = await release(e, "Filter.Film.2024.1080p.WEB-DL.CHS.x264-GRP2"); expect((await evaluateAcquisition(h.d, { sourceReleaseId: both })).plans).toHaveLength(1); }); it("MP 分辨率 / 来源 / 特效:新建时建默认收录版本承载预设;改预设覆盖同一字段,手改过的字段保留;无法识别 400", async () => { const e = await seedEntity(db(), { kind: "series", title: "预设剧", year: 2025 }); const bad = await h.api("POST", "/subscriptions", { mediaEntityId: e, mediaKind: "series", params: { resolution: "480p" }, }); expect(bad.status).toBe(400); const c = await h.api("POST", "/subscriptions", { mediaEntityId: e, mediaKind: "series", params: { resolution: "4K|2160p|x2160", quality: "WEB-?DL|WEB-?RIP", effect: "杜比视界" }, }); expect(c.status, JSON.stringify(c.body)).toBe(201); const id = c.body.data.id as string; expect(c.body.data.params).toMatchObject({ resolution: "4k", source: "web", effect: "dv" }); expect(c.body.data.params.quality).toBeUndefined(); let [slot] = await slotsOf(id); expect(slot).toMatchObject({ name: "默认版本" }); expect(slot!.hard_conditions).toEqual({ resolutions: ["2160p"], sources: ["WEB-DL", "WEBRip"], hdr: { anyOf: ["DV"] }, }); expect(slot!.soft_preferences).toEqual({ sources: ["WEB-DL", "WEBRip"], hdr: ["DV", "HDR10+", "HDR10"] }); // 手改来源 await db() .updateTable("acquisition_slot") .set({ hard_conditions: JSON.stringify({ ...(slot!.hard_conditions as object), sources: ["WEB-DL"] }) }) .where("id", "=", slot!.id) .execute(); const p = await h.api("PATCH", `/subscriptions/${id}`, { params: { resolution: "1080p" } }); expect(p.status).toBe(200); [slot] = await slotsOf(id); // 分辨率换成新预设;特效预设去掉 → 删;来源手改过 → 保留 expect(slot!.hard_conditions).toEqual({ resolutions: ["1080p", "1080i"], sources: ["WEB-DL"] }); expect(slot!.soft_preferences).toEqual({}); // 预设 UHD / Atmos 进私有组包含,与表单包含取 AND await h.api("PATCH", `/subscriptions/${id}`, { params: { source: "uhd", include: ["中字"] } }); const g = (await privateGroups(id)).find((x) => !x.deleted_at)!; expect((g.hard as { include: string[] }).include).toHaveLength(1); expect(new RegExp((g.hard as { include: string[] }).include[0]!, "i").test("X UHD 中字")).toBe(true); expect(new RegExp((g.hard as { include: string[] }).include[0]!, "i").test("X 中字")).toBe(false); }); it("下载提交:订阅指定下载器与保存路径(本机路径经映射)→ 计划评估与 qB savepath;映射不到 → 不建计划", async () => { const inst2 = await db() .insertInto("downloader_instance") .values({ name: "qb-c1a-2", base_url: qb.url, username: "admin", settings: JSON.stringify({ savePath: "/dl" }) }) .returning("id") .executeTakeFirstOrThrow(); await db() .insertInto("path_mapping") .values({ view_kind: "downloader", downloader_instance_id: inst2.id, remote_prefix: "/dl", local_prefix: cache.root, }) .execute(); const e = await seedEntity(db(), { kind: "movie", title: "Save Path Film", year: 2023 }); const c = await h.api("POST", "/subscriptions", { mediaEntityId: e, mediaKind: "movie", params: { downloaderInstanceId: inst2.id, downloadSavePath: `${cache.root}/movies`, resolution: "1080p" }, }); expect(c.status, JSON.stringify(c.body)).toBe(201); const rel = await release(e, "Save.Path.Film.2023.1080p.WEB-DL.x264-GRP"); const ev = await evaluateAcquisition(h.d, { sourceReleaseId: rel }); expect(ev.plans, JSON.stringify(ev)).toHaveLength(1); const plan = await db() .selectFrom("acquisition_plan") .selectAll() .where("id", "=", ev.plans[0]!) .executeTakeFirstOrThrow(); expect(plan.evaluation).toMatchObject({ instanceId: inst2.id, savePath: "/dl/movies" }); const s = await submitDownload(h.d, plan.id); expect(s.status, JSON.stringify(s)).toBe("submitted"); const dt = await db() .selectFrom("download_task") .selectAll() .where("id", "=", s.downloadTaskId!) .executeTakeFirstOrThrow(); expect(dt.instance_id).toBe(inst2.id); expect(dt.save_path).toBe("/dl/movies"); // 映射不到的保存路径:不建计划,理由可见 const e2 = await seedEntity(db(), { kind: "movie", title: "Unmapped Film", year: 2023 }); await h.api("POST", "/subscriptions", { mediaEntityId: e2, mediaKind: "movie", params: { downloaderInstanceId: inst2.id, downloadSavePath: "/nowhere", resolution: "1080p" }, }); const rel2 = await release(e2, "Unmapped.Film.2023.1080p.WEB-DL.x264-GRP"); const ev2 = await evaluateAcquisition(h.d, { sourceReleaseId: rel2 }); expect(ev2.plans).toHaveLength(0); expect(ev2.skipped).toContainEqual({ sourceReleaseId: rel2, reason: "download_save_path_unmapped" }); }); it("库根:作品还没有路径绑定时,整理按订阅指定的库根;已有绑定仍按绑定", async () => { const root2 = await db() .insertInto("library_root") .values({ name: "c1a-root2", media_kind: "series", local_path: lib2.root, management_mode: "managed" }) .returning("id") .executeTakeFirstOrThrow(); const e = await seedEntity(db(), { kind: "movie", title: "Root Pick", year: 2021 }); const dry = () => planImport(h.d, { sourcePaths: [cache.path("Root.Pick.2021.1080p.BluRay.x264-GRP.mkv")], mediaEntityId: e, action: "copy", trigger: "manual", actorType: "user", dryRun: true, }); const before = await dry(); expect(before.plan?.libraryRootId, JSON.stringify(before)).toBe(base.rootId); const c = await h.api("POST", "/subscriptions", { mediaEntityId: e, mediaKind: "movie", params: { libraryRootId: root2.id }, }); expect(c.status).toBe(201); const after = await dry(); expect(after.plan?.libraryRootId, JSON.stringify(after)).toBe(root2.id); }); it("重置:取消未提交的计划与下载已不在的计划,保留仍在下载的;清延后;排立即重评;已完成 409", async () => { const e = await seedEntity(db(), { kind: "series", title: "Reset Show", year: 2024 }); const c = await h.api("POST", "/subscriptions", { mediaEntityId: e, mediaKind: "series", params: { resolution: "1080p" }, }); const id = c.body.data.id as string; const [slot] = await slotsOf(id); const r1 = await release(e, "Reset.Show.S01E01.1080p.WEB-DL.x264-GRP"); const r2 = await release(e, "Reset.Show.S01E02.1080p.WEB-DL.x264-GRP"); const r3 = await release(e, "Reset.Show.S01E03.1080p.WEB-DL.x264-GRP"); const [p1] = (await evaluateAcquisition(h.d, { sourceReleaseId: r1 })).plans; const [p2] = (await evaluateAcquisition(h.d, { sourceReleaseId: r2 })).plans; const [p3] = (await evaluateAcquisition(h.d, { sourceReleaseId: r3 })).plans; expect([p1, p2, p3].every(Boolean)).toBe(true); // p1 在下载;p2 已提交但下载任务被移除;p3 只预留 expect((await submitDownload(h.d, p1!)).status).toBe("submitted"); const s2 = await submitDownload(h.d, p2!); await db() .updateTable("download_task") .set({ removed_at: new Date() }) .where("id", "=", s2.downloadTaskId!) .execute(); const r2b = await release(e, "Reset.Show.S01E02.1080p.WEB-DL.x265-OTHER"); await evaluateAcquisition(h.d, { sourceReleaseId: r2b }); expect(await db().selectFrom("slot_deferral").select("id").where("slot_id", "=", slot!.id).execute()).toHaveLength( 1, ); h.engine.enqueued.length = 0; const res = await h.api("POST", `/subscriptions/${id}/reset`); expect(res.status, JSON.stringify(res.body)).toBe(200); expect(res.body.data).toMatchObject({ subscriptionId: id, keptPlans: [p1], clearedDeferrals: 1, reevaluated: true, }); expect([...res.body.data.releasedPlans].sort()).toEqual([p2, p3].sort()); const statuses = await db() .selectFrom("acquisition_plan") .select(["id", "status", "status_reason"]) .where("id", "in", [p1!, p2!, p3!]) .execute(); expect(statuses.find((x) => x.id === p1)?.status).toBe("submitted"); expect(statuses.filter((x) => x.status === "cancelled").map((x) => x.status_reason)).toEqual([ "subscription_reset", "subscription_reset", ]); expect(await db().selectFrom("slot_deferral").select("id").where("slot_id", "=", slot!.id).execute()).toHaveLength( 0, ); expect((await slotsOf(id))[0]!.next_evaluate_at).not.toBeNull(); expect(h.engine.enqueued.some((q) => q.task === "evaluateAcquisition")).toBe(true); const audit = await db() .selectFrom("audit_event") .select("action") .where("target_id", "=", id) .where("action", "=", "subscription.reset") .execute(); expect(audit).toHaveLength(1); await db().updateTable("subscription").set({ status: "completed" }).where("id", "=", id).execute(); expect((await h.api("POST", `/subscriptions/${id}/reset`)).status).toBe(409); }); it("日历:集播出日期同日同季聚合,状态取主版本;没有播出日期列 unknown;电影取上映日期;跨度超限 400", async () => { const today = new Date().toISOString().slice(0, 10); const addDays = (n: number) => new Date(Date.now() + n * 86_400_000).toISOString().slice(0, 10); const e = await seedEntity(db(), { kind: "series", title: "日历剧", year: 2026 }); const season = await db() .insertInto("content_unit") .values({ media_entity_id: e, kind: "season", season_number: 1, status: "confirmed", episode_total: 4, episode_total_source: "tmdb", }) .returning("id") .executeTakeFirstOrThrow(); const ep = async (n: number, air: string | null) => ( await db() .insertInto("content_unit") .values({ media_entity_id: e, parent_unit_id: season.id, kind: "episode", season_number: 1, episode_number: n, status: "confirmed", air_date: air, }) .returning("id") .executeTakeFirstOrThrow() ).id; const e1 = await ep(1, addDays(-3)); await ep(2, addDays(-3)); await ep(3, addDays(5)); await ep(4, null); const c = await h.api("POST", "/subscriptions", { mediaEntityId: e, mediaKind: "series", seasonUnitId: season.id, params: { resolution: "1080p" }, }); expect(c.status, JSON.stringify(c.body)).toBe(201); const subId = c.body.data.id as string; // E01 已入库(单收录版本:作品下资产都算) const a = await db() .insertInto("media_asset") .values({ media_entity_id: e, kind: "video", size_bytes: SIZE, media_info: JSON.stringify({ _umt: { quality: { resolution: "1080p", source: "WEB-DL" } } }), }) .returning("id") .executeTakeFirstOrThrow(); await db().insertInto("asset_unit_coverage").values({ media_asset_id: a.id, content_unit_id: e1 }).execute(); const m = await seedEntity(db(), { kind: "movie", title: "日历片", year: 2026 }); await db() .updateTable("media_entity") .set({ metadata: JSON.stringify({ releaseDate: { value: addDays(10), source: "tmdb" } }) }) .where("id", "=", m) .execute(); await h.api("POST", "/subscriptions", { mediaEntityId: m, mediaKind: "movie" }); const m2 = await seedEntity(db(), { kind: "movie", title: "无日期片", year: 2026 }); await h.api("POST", "/subscriptions", { mediaEntityId: m2, mediaKind: "movie" }); const cal = await h.api("GET", `/subscriptions/calendar?from=${addDays(-10)}&to=${addDays(20)}`); expect(cal.status, JSON.stringify(cal.body)).toBe(200); const mine = cal.body.data.entries.filter((x: { subscriptionId: string }) => x.subscriptionId === subId); expect(mine).toEqual([ expect.objectContaining({ date: addDays(-3), seasonNumber: 1, episodes: [1, 2], label: "S01E01–E02", state: "partial", have: 1, total: 2, }), expect.objectContaining({ date: addDays(5), episodes: [3], state: "upcoming", have: 0, total: 1 }), ]); expect(cal.body.data.unknown).toContainEqual( expect.objectContaining({ subscriptionId: subId, seasonNumber: 1, episodes: [4] }), ); expect(cal.body.data.entries).toContainEqual( expect.objectContaining({ title: "日历片", date: addDays(10), label: "上映", state: "upcoming" }), ); expect(cal.body.data.unknown).toContainEqual(expect.objectContaining({ title: "无日期片", episodes: [] })); const movies = await h.api("GET", `/subscriptions/calendar?from=${addDays(-10)}&to=${addDays(20)}&mediaKind=movie`); expect(movies.body.data.entries.every((x: { mediaKind: string }) => x.mediaKind === "movie")).toBe(true); expect((await h.api("GET", `/subscriptions/calendar?from=${today}&to=${addDays(90)}`)).status).toBe(400); expect((await h.api("GET", `/subscriptions/calendar?from=${today}&to=${addDays(-1)}`)).status).toBe(400); }); });