import { createMockMteamApi, type MockMteamApi } from "@umt/testkit"; import { afterEach, describe, expect, it } from "vitest"; import { createMteamAdapter, createSiteRateLimiter, getDownloadVolumeFactor, mteamDefinition, type SiteAdapter, SiteError, type SiteRateLimiter, staticSecretResolver, } from "../index.ts"; let api: MockMteamApi | undefined; afterEach(async () => { await api?.close(); api = undefined; }); function adapterFor(m: MockMteamApi, key: string | null = m.apiKey, limiter?: SiteRateLimiter): SiteAdapter { return createMteamAdapter(mteamDefinition, { baseUrl: m.baseUrl, secrets: staticSecretResolver(key ? { mteam: { apiKey: key } } : {}), limiter: limiter ?? createSiteRateLimiter({ minIntervalMs: 0 }), }); } async function expectSiteError(p: Promise, kind: string): Promise { try { await p; } catch (e) { expect(e).toBeInstanceOf(SiteError); expect((e as SiteError).kind).toBe(kind); return e as SiteError; } throw new Error(`expected SiteError ${kind}`); } describe("M-Team API 适配器(mock)", () => { it("列表:x-api-key 头 + 搜索体,映射 MV1 字段,结果不含 API Key", async () => { api = await createMockMteamApi(); const page = await adapterFor(api).list(1); const req = api.requests[0]!; expect(req.path).toBe("/api/torrent/search"); expect(req.headers["x-api-key"]).toBe(api.apiKey); expect(api.lastSearchBody).toEqual({ keyword: null, categories: [], pageNumber: 1, pageSize: 100, visible: 1 }); expect(page).toMatchObject({ page: 1, totalPages: 1, totalPagesEstimated: false }); const shogun = page.items.find((i) => i.siteReleaseId === "912233")!; expect(shogun).toMatchObject({ siteId: "mteam", title: "Shogun.2024.S01.2160p.DSNP.WEB-DL.DDP5.1.Atmos.DV.HDR.H.265-FLUX", subtitle: "幕府将军 第一季 全10集 杜比视界", category: { id: "402", name: "剧集" }, raw: expect.objectContaining({ category: "电视剧", category_id: "402" }), sizeBytes: 61234567890, seeders: 532, leechers: 4, snatched: 2011, promotion: { type: "50%", downloadFactor: 0.5, uploadFactor: 1, expiresAt: "2026-09-28T00:15:00.000Z" }, hr: "unknown", uploadedAt: "2026-09-20T00:15:00.000Z", detailUrl: "https://tp.m-team.cc/detail/912233", imdbId: "tt2788316", tags: ["中字", "DV"], }); expect(shogun.downloadUrl).toBeUndefined(); expect(JSON.stringify(page)).not.toContain(api.apiKey); }); it("促销优先级:商城单种免费 > 全站促销 > 基础折扣;2x 免费;标签位图", async () => { api = await createMockMteamApi(); const { items } = await adapterFor(api).list(1); const by = Object.fromEntries(items.map((i) => [i.siteReleaseId, i])); expect(by["912234"]!.promotion).toEqual({ type: "free", downloadFactor: 0, uploadFactor: 1, expiresAt: "2026-09-27T12:00:00.000Z", }); expect(by["912234"]!.tags).toEqual(["国配", "中字"]); expect(by["912235"]!.promotion).toMatchObject({ type: "free", downloadFactor: 0, expiresAt: "2026-10-07T15:59:59.000Z", }); expect(by["912236"]!.promotion).toMatchObject({ type: "2xfree", downloadFactor: 0, uploadFactor: 2 }); expect(by["912237"]!.promotion).toEqual({ type: "none", downloadFactor: 1, uploadFactor: 1, expiresAt: null }); // MP __get_downloadvolumefactor:未知折扣代码按 1 处理 expect(getDownloadVolumeFactor("SOMETHING_NEW")).toBe(1); }); it("分类原始 ID 保留(404 纪录 / 405 动画 / 407 体育)", async () => { api = await createMockMteamApi(); const { items } = await adapterFor(api).list(1); const cats = Object.fromEntries(items.map((i) => [i.siteReleaseId, i.category])); expect(cats["912235"]).toEqual({ id: "405", name: "动画" }); expect(cats["912236"]).toEqual({ id: "404", name: "纪录" }); expect(cats["912237"]).toEqual({ id: "407", name: "体育" }); }); it("搜索:IMDb 关键词改写为完整 URL;分类与免费筛选下推", async () => { api = await createMockMteamApi(); const a = adapterFor(api); const r = await a.search("tt15398776"); expect(api.lastSearchBody!.keyword).toBe("https://www.imdb.com/title/tt15398776"); expect(r.items.map((i) => i.siteReleaseId)).toEqual(["912234"]); const free = await a.search("", { freeOnly: true, categoryIds: ["404", "405"] }); expect(api.lastSearchBody).toMatchObject({ keyword: null, categories: ["404", "405"] }); const tv = await a.search("Shogun", { mtype: "tv" }); expect(api.lastSearchBody!.categories).toEqual(["403", "402", "435", "438", "404", "405"]); expect(tv.items.map((i) => i.siteReleaseId)).toEqual(["912233"]); // 912235 基础 NORMAL 但全站促销 FREE → 下载系数 0 expect(free.items.map((i) => i.siteReleaseId)).toEqual(["912235", "912236"]); const kw = await a.search("芙莉莲"); expect(kw.items.map((i) => i.siteReleaseId)).toEqual(["912235"]); }); it("详情:表单 id,带简介;不存在 → not_found", async () => { api = await createMockMteamApi(); const a = adapterFor(api); const d = await a.detail("912233"); expect(api.requests[0]!.body).toBe("id=912233"); expect(d.descriptionText).toContain("幕府将军"); expect(d.imdbId).toBe("tt2788316"); const byUrl = await a.detail("https://tp.m-team.cc/detail/912233"); expect(byUrl.siteReleaseId).toBe("912233"); await expectSiteError(a.detail("1"), "not_found"); }); it("下载令牌:现场 genDlToken → 一次性直链 → .torrent,infohash 校验", async () => { api = await createMockMteamApi(); const a = adapterFor(api); const t = await a.downloadTarget("912233"); expect(api.issuedTokens).toBe(1); expect(api.requests.map((r) => r.path.split("?")[0])).toEqual(["/api/torrent/genDlToken", "/api/rss/dlv2"]); // MP:genDlToken 的 id 走 query 参数;直链下载不带 x-api-key expect(api.requests[0]!.path).toBe("/api/torrent/genDlToken?id=912233"); expect(api.requests[1]!.headers["x-api-key"]).toBeUndefined(); expect(t.kind).toBe("torrent"); if (t.kind === "torrent") expect(t.infoHash).toBe(api.infoHashOf("912233")); // 每次下载都重新签发令牌 await a.downloadTarget("912233"); expect(api.issuedTokens).toBe(2); api.setFailMode("no_dl_token"); await expectSiteError(a.downloadTarget("912233"), "rate_limited"); }); it("Key 失效/停用 → auth_invalid(停止重试),健康检查报告;缺 Key → auth_missing", async () => { api = await createMockMteamApi(); const wrong = await expectSiteError(adapterFor(api, "wrong-key").list(1), "auth_invalid"); expect(wrong.isAuthError).toBe(true); api.setFailMode("banned_key"); const banned = await expectSiteError(adapterFor(api).search("x"), "auth_invalid"); expect(banned.message).not.toContain(api.apiKey); expect(await adapterFor(api).healthCheck()).toMatchObject({ ok: false, status: "auth_invalid" }); api.setFailMode(null); expect((await adapterFor(api).healthCheck()).status).toBe("ok"); const before = api.requests.length; await expectSiteError(adapterFor(api, null).list(1), "auth_missing"); expect(api.requests.length).toBe(before); }); it("非 JSON 维护页 → site_maintenance;业务限流/HTTP 429 → rate_limited 并顺延限流器", async () => { api = await createMockMteamApi(); const limiter = createSiteRateLimiter({ minIntervalMs: 0 }); const a = adapterFor(api, api.apiKey, limiter); api.setFailMode("non_json"); await expectSiteError(a.list(1), "site_maintenance"); api.setFailMode("rate_limit"); await expectSiteError(a.list(1), "rate_limited"); api.setFailMode("http_429"); const e = await expectSiteError(a.list(1), "rate_limited"); expect(e.retryAfterMs).toBe(5000); expect(limiter.tryAcquire()).toBe(false); }); it("站点级限流 1s:列表/搜索/详情/下载共享", async () => { api = await createMockMteamApi(); let clock = 0; const sleeps: number[] = []; const limiter = createSiteRateLimiter({ minIntervalMs: mteamDefinition.umt.rateLimit.minIntervalMs, now: () => clock, sleep: async (ms) => { sleeps.push(ms); clock += ms; }, }); const a = adapterFor(api, api.apiKey, limiter); await a.list(1); await a.search("Shogun"); await a.detail("912233"); await a.downloadTarget("912233"); // genDlToken + GET 直链 = 2 次 expect(sleeps).toEqual([1000, 1000, 1000, 1000]); }); });