Files
UHD_Media_Tool/packages/testkit/src/sites/mock-fetch.ts
T
rootandClaude Opus 5.5 d9f2af2f3c feat(adapters): port MoviePilot open site framework (7 spiders, 20 user parsers) + MovieClaw v0.27.0 site definitions + mp-sync tools (W1-3c)
- sites/spiders: Haidan/HDDolby/RousiPro/SunnyPT/TNode/TorrentLeech/Yema (+mTorrent wrapper), specialized SiteAdapter, indirect download resolved at download time
- sites/user-parsers: SiteParserBase + all 20 MP SiteSchema parsers (lxml XPath via xpath+xmldom), refreshSiteUserData with schema fallback
- sites/definitions: zod SiteDefinition schemas, 23 MovieClaw v0.27.0 (MIT) definitions via scripts/mp-sync/movieclaw-import.mjs, registry getSiteDefinition/listSiteDefinitions, NOTICE
- scripts/mp-sync: ledger.json (upstream sha256), check.mjs, build-ledger.mjs; docs/porting/mp-sync-ledger.md
- license-and-provenance: add copied-mit kind, re-anchor MovieClaw rows to v0.27.0

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 18:39:27 +08:00

98 lines
3.4 KiB
TypeScript

/**
* 路由表式 fetch 替身(站点用户信息解析器 / API spider 测试用)。
* 路由键:`METHOD path?query`(query 可省略,省略时匹配任意 query;METHOD 省略为任意方法);
* 值为响应描述或函数。未命中返回 404。记录全部请求(含头与 body)供断言。
*/
export interface MockRouteResponse {
status?: number;
body?: string | object;
contentType?: string;
headers?: Record<string, string>;
}
export type MockRouteHandler = MockRouteResponse | ((req: MockFetchRequest) => MockRouteResponse);
export interface MockFetchRequest {
method: string;
url: URL;
headers: Record<string, string>;
body: string | null;
}
export interface MockFetch {
fetch: typeof globalThis.fetch;
requests: MockFetchRequest[];
/** 追加/覆盖路由 */
route(key: string, handler: MockRouteHandler): void;
}
function match(key: string, req: MockFetchRequest): boolean {
let method: string | null = null;
let target = key;
const sp = key.indexOf(" ");
if (sp > 0 && /^[A-Z]+$/.test(key.slice(0, sp))) {
method = key.slice(0, sp);
target = key.slice(sp + 1);
}
if (method && method !== req.method) return false;
// 可带主机:"https://api.x.com/path"
let host: string | null = null;
if (/^https?:\/\//.test(target)) {
const u = new URL(target);
host = u.host;
target = u.pathname + u.search;
}
if (host && host !== req.url.host) return false;
const [path, query] = target.split("?");
if (path !== req.url.pathname) return false;
if (query === undefined) return true;
const want = new URLSearchParams(query);
for (const [k, v] of want) if (req.url.searchParams.get(k) !== v) return false;
return true;
}
export function createMockFetch(routes: Record<string, MockRouteHandler> = {}): MockFetch {
const table = new Map<string, MockRouteHandler>(Object.entries(routes));
const requests: MockFetchRequest[] = [];
const fetchImpl = async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
const headers: Record<string, string> = {};
new Headers(init?.headers).forEach((v, k) => {
headers[k] = v;
});
const req: MockFetchRequest = {
method: (init?.method ?? "GET").toUpperCase(),
url,
headers,
body: typeof init?.body === "string" ? init.body : null,
};
requests.push(req);
// 后加入的路由优先,便于测试覆盖默认路由;同时更具体(带 query)的键优先
const keys = [...table.keys()].reverse().sort((a, b) => Number(b.includes("?")) - Number(a.includes("?")));
for (const key of keys) {
if (!match(key, req)) continue;
const h = table.get(key)!;
const r = typeof h === "function" ? h(req) : h;
const isJson = r.body !== undefined && typeof r.body !== "string";
const body = r.body === undefined ? "" : isJson ? JSON.stringify(r.body) : (r.body as string);
return new Response(body, {
status: r.status ?? 200,
headers: {
"content-type": r.contentType ?? (isJson ? "application/json" : "text/html; charset=utf-8"),
...(r.headers ?? {}),
},
});
}
return new Response("not found", { status: 404 });
};
return {
fetch: fetchImpl as typeof globalThis.fetch,
requests,
route(key, handler) {
table.delete(key);
table.set(key, handler);
},
};
}