Files
UHD_Media_Tool/packages/adapters/src/sites/challenge.test.ts
T
rootandClaude Opus 5.5 a11ef16e40 fix(sites): Cloudflare 挑战不算凭据错误(CF-2)
- http.ts:cf-mitigated: challenge 或 403/503/429 挑战页 → SiteError(challenge),503 不再算维护、403 不再落到 auth_invalid
- 挑战页分类 classifyChallengePage:自动 / 可点(Turnstile)/ 真验证码
- 刷新登录 / 用户数据遇 Cloudflare 挑战只记 last_error,账号状态不变、不开登录失效事项

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 21:26:53 +08:00

117 lines
5.0 KiB
TypeScript

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 = `<!DOCTYPE html><html><head><title>Just a moment...</title></head><body>
<div id="challenge-spinner"></div><script>window._cf_chl_opt={cType: 'managed'}</script></body></html>`;
const CF_JS_PAGE = `<html><head><title>Just a moment...</title></head><body>
<script>window._cf_chl_opt={cType: 'non-interactive'}</script></body></html>`;
const NORMAL = `<html><head><title>种子 - 观众</title><script src="/cdn-cgi/challenge-platform/scripts/jsd/main.js"></script></head><body>ok</body></html>`;
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<string, string> = {}) =>
(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('<html><body><form id="challenge-form"></form></body></html>')).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: "<html><body></body></html>",
text: "请完成以下操作,验证您是真人。",
}).kind,
).toBe("interactive");
});
it("选图 / 输码 → captcha(不自动处理)", () => {
expect(classifyChallengePage({ html: `${CF_PAGE}<div class="h-captcha"></div>`, text: "Just a moment" }).kind).toBe(
"captcha",
);
expect(classifyChallengePage({ title: "Just a moment...", html: "", text: "请点击图中所有的红绿灯" }).kind).toBe(
"captcha",
);
});
});