import { describe, expect, it } from "vitest"; import { challengeRuleOfHtml, classifyChallengePage, detectChallenge } from "./challenge.ts"; import { siteFetch } from "./http.ts"; import { createSiteRateLimiter } from "./rate-limit.ts"; import { SiteError } from "./types.ts"; // CF-2:Cloudflare 挑战识别。HTTP 端挑战不是凭据错误(403 不再算 auth_invalid、503 不再算维护); // 浏览器端按页面分:自动 / 可点(Turnstile)/ 真验证码。 const CF_PAGE = `Just a moment...
`; const CF_JS_PAGE = `Just a moment... `; const NORMAL = `种子 - 观众ok`; function ctx(fetchImpl: typeof fetch) { return { siteId: "s1", fetch: fetchImpl, limiter: createSiteRateLimiter({ minIntervalMs: 0, burst: 10 }), timeoutMs: 5_000, now: () => new Date(), }; } const reply = (status: number, body: string, headers: Record = {}) => (async () => new Response(body, { status, headers: { "content-type": "text/html", ...headers } })) as typeof fetch; describe("challengeRuleOfHtml", () => { it("挑战页标题 / 脚本特征命中;正常页面里注入的 challenge-platform 脚本不算", () => { expect(challengeRuleOfHtml(CF_PAGE)).toBe("title=Just a moment..."); expect(challengeRuleOfHtml('
')).toContain( "challenge-form", ); expect(challengeRuleOfHtml(NORMAL)).toBeNull(); expect(challengeRuleOfHtml("")).toBeNull(); }); }); describe("detectChallenge", () => { it("cf-mitigated: challenge 不读正文;403 / 503 读正文判定并重新包装响应", async () => { const a = await detectChallenge(new Response("x", { status: 200, headers: { "cf-mitigated": "challenge" } })); expect(a.rule).toBe("header cf-mitigated=challenge"); const b = await detectChallenge(new Response(CF_PAGE, { status: 403 })); expect(b.rule).toContain("Just a moment"); expect(await b.res.text()).toBe(CF_PAGE); const c = await detectChallenge(new Response("Forbidden", { status: 403 })); expect(c.rule).toBeNull(); expect(await c.res.text()).toBe("Forbidden"); const d = await detectChallenge(new Response(NORMAL, { status: 200 })); expect(d.rule).toBeNull(); }); }); describe("siteFetch 挑战分类", () => { const url = "https://audiences.example/torrents.php"; const kind = async (f: typeof fetch) => { try { await siteFetch(ctx(f), url); return "ok"; } catch (e) { return e instanceof SiteError ? e.kind : "other"; } }; it("403 + cf-mitigated → challenge(不是 auth_invalid),不算凭据错误", async () => { let err: unknown; try { await siteFetch(ctx(reply(403, CF_PAGE, { "cf-mitigated": "challenge" })), url); } catch (e) { err = e; } expect(err).toBeInstanceOf(SiteError); const se = err as SiteError; expect(se.kind).toBe("challenge"); expect(se.isAuthError).toBe(false); expect(se.detail).toMatchObject({ reason: "challenge", rule: "header cf-mitigated=challenge", httpStatus: 403 }); }); it("503 挑战页 → challenge(不是维护);403 挑战页(没有响应头)也认", async () => { expect(await kind(reply(503, CF_PAGE))).toBe("challenge"); expect(await kind(reply(403, CF_PAGE))).toBe("challenge"); expect(await kind(reply(429, CF_JS_PAGE))).toBe("challenge"); }); it("普通 503 仍是维护,普通 403 交给适配器(响应体可读)", async () => { expect(await kind(reply(503, "maintenance"))).toBe("site_maintenance"); const res = await siteFetch(ctx(reply(403, "Forbidden")), url); expect(res.status).toBe(403); expect(await res.text()).toBe("Forbidden"); }); }); describe("classifyChallengePage", () => { it("不是挑战页 → none", () => { expect(classifyChallengePage({ title: "种子", html: NORMAL, text: "种子列表" }).kind).toBe("none"); }); it("JS 检查 → waiting;托管 / Turnstile → interactive", () => { expect(classifyChallengePage({ html: CF_JS_PAGE, text: "Checking your browser" }).kind).toBe("waiting"); expect(classifyChallengePage({ html: CF_PAGE, text: "" }).kind).toBe("interactive"); expect( classifyChallengePage({ title: "请稍候…", html: "", text: "请完成以下操作,验证您是真人。", }).kind, ).toBe("interactive"); }); it("选图 / 输码 → captcha(不自动处理)", () => { expect(classifyChallengePage({ html: `${CF_PAGE}
`, text: "Just a moment" }).kind).toBe( "captcha", ); expect(classifyChallengePage({ title: "Just a moment...", html: "", text: "请点击图中所有的红绿灯" }).kind).toBe( "captcha", ); }); });