import { createMockCamofox, type MockCamofox } from "@umt/testkit"; import { afterEach, describe, expect, it } from "vitest"; import { BrowserError, type BrowserLogger, cookieDomainMatches, cookiesFromHeader, createCamofoxBrowserProvider, headerFromCookies, mergeCookieHeaders, parseCookieHeader, profileKeyFor, sameCookieHeader, staticBrowserSecrets, } from "./index.ts"; // BR-2:Cookie 头串 ⇄ 浏览器 Cookie 互转,与 camofox Cookie 接口(umt-cookies 插件 / 上游导入端点 / 都没有)。 const ACCESS_KEY = "camofox-access-key-br2"; const PROVIDER = "11111111-1111-4111-8111-111111111111"; const SITE = "22222222-2222-4222-8222-222222222222"; const ACCOUNT = "33333333-3333-4333-8333-333333333333"; const SECRET = "c0ffee-session-value-br2"; describe("Cookie 头串互转", () => { it("解析 / 合并(后者优先)/ 等价比较", () => { expect([...parseCookieHeader(" a=1; b = 2 ;;bad; c=x=y ")]).toEqual([ ["a", "1"], ["b", "2"], ["c", "x=y"], ]); expect(mergeCookieHeaders("a=1; b=2", "b=3; c=4")).toBe("a=1; b=3; c=4"); expect(mergeCookieHeaders(null, "")).toBeNull(); expect(sameCookieHeader("a=1; b=2", "b=2;a=1")).toBe(true); expect(sameCookieHeader("a=1", "a=2")).toBe(false); }); it("注入对象:站点主机 host-only、https → secure、30 天期限", () => { const list = cookiesFromHeader("uid=1; pass=x", "https://hhanclub.net/", 1000); expect(list).toEqual([ expect.objectContaining({ name: "uid", domain: "hhanclub.net", path: "/", secure: true, expires: 1000 + 30 * 86400, }), expect.objectContaining({ name: "pass", value: "x" }), ]); }); it("读回只取站点域名、未过期的 Cookie;同名取更具体的", () => { expect(cookieDomainMatches(".hhanclub.net", "www.hhanclub.net")).toBe(true); expect(cookieDomainMatches("hhanclub.net", "evilhhanclub.net")).toBe(false); const h = headerFromCookies( [ { name: "pass", value: "root", domain: ".hhanclub.net", path: "/", expires: -1, httpOnly: true, secure: true }, { name: "pass", value: "host", domain: "hhanclub.net", path: "/", expires: -1, httpOnly: true, secure: true }, { name: "old", value: "1", domain: "hhanclub.net", path: "/", expires: 10, httpOnly: false, secure: false }, { name: "_ga", value: "t", domain: ".google.com", path: "/", expires: -1, httpOnly: false, secure: false }, ], "https://hhanclub.net/", 100, ); expect(h).toBe("pass=host"); expect(headerFromCookies([], "https://hhanclub.net/")).toBeNull(); }); }); describe("camofox Cookie 接口", () => { let mock: MockCamofox | undefined; afterEach(async () => { await mock?.close(); mock = undefined; }); const provider = (m: MockCamofox, logs?: Parameters[0][]) => createCamofoxBrowserProvider({ providerId: PROVIDER, baseUrl: m.url, secrets: staticBrowserSecrets({ [`provider:${PROVIDER}`]: ACCESS_KEY }), ...(logs ? { logger: (e) => void logs.push(e) } : {}), }); const key = profileKeyFor(SITE, ACCOUNT); const cookies = cookiesFromHeader(`c_secure_uid=1001; c_secure_pass=${SECRET}`, "https://audiences.example/"); it("umt-cookies 插件:写入(带访问凭据)与读回;日志不含 Cookie 值", async () => { mock = await createMockCamofox({ accessKey: ACCESS_KEY }); const logs: Parameters[0][] = []; const p = provider(mock, logs); expect(await p.setCookies(key, cookies)).toEqual({ count: 2 }); const call = mock.callsTo("/umt/sessions/")[0]!; expect(call.headers.authorization).toBe(`Bearer ${ACCESS_KEY}`); const back = await p.getCookies(key, ["https://audiences.example/"]); expect(headerFromCookies(back, "https://audiences.example/")).toBe(`c_secure_pass=${SECRET}; c_secure_uid=1001`); expect(await p.getCookies(key, ["https://other.example/"])).toEqual([]); expect(JSON.stringify(logs)).not.toContain(SECRET); }); it("openSession 带 cookies:先注入再开标签页", async () => { mock = await createMockCamofox({ accessKey: ACCESS_KEY }); const p = provider(mock); const r = await p.openSession({ providerId: PROVIDER, siteId: SITE, accountId: ACCOUNT, initialUrl: "https://audiences.example/", budget: { domainAllowlist: ["audiences.example"], timeoutMs: 500, maxRetries: 0, maxBytes: 1_000_000 }, cookies, }); expect(r.cookies).toEqual({ status: "injected", count: 2 }); const paths = mock.calls.map((c) => c.path); expect(paths.findIndex((x) => x.startsWith("/umt/"))).toBeLessThan(paths.indexOf("/tabs")); }); it("没有插件:回退上游导入端点(CAMOFOX_API_KEY 与访问凭据相同时);读回报不支持", async () => { mock = await createMockCamofox({ accessKey: ACCESS_KEY, cookieApi: "upstream", cookieImportKey: ACCESS_KEY }); const p = provider(mock); expect(await p.setCookies(key, cookies)).toEqual({ count: 2 }); expect(mock.cookieJars.get(key)).toHaveLength(2); await expect(p.getCookies(key, ["https://audiences.example/"])).rejects.toMatchObject({ kind: "not_implemented" }); }); it("插件与上游导入都不可用:not_implemented;openSession 照常打开并标 unsupported", async () => { mock = await createMockCamofox({ accessKey: ACCESS_KEY, cookieApi: "upstream" }); const p = provider(mock); const e = await p.setCookies(key, cookies).catch((x: unknown) => x); expect(e).toBeInstanceOf(BrowserError); expect((e as BrowserError).kind).toBe("not_implemented"); expect((e as BrowserError).message).not.toContain(SECRET); const r = await p.openSession({ providerId: PROVIDER, siteId: SITE, accountId: ACCOUNT, budget: { domainAllowlist: ["audiences.example"], timeoutMs: 500, maxRetries: 0, maxBytes: 1_000_000 }, cookies, }); expect(r.cookies).toEqual({ status: "unsupported", count: 0 }); }); });