- server 的 src/modules/*/__tests__ 挪到 test/<模块>/;e2e 去掉 eN- 前缀, w5a2 / v2 / search2 / *-dh1 / *-md1 / *-md2、web 的 agui3 / search2、domain 的 id2 / id3 改为按行为命名 - vitest 分三组:unit、pg(真实 PG + 假引擎,文件并行,最多 6 个)、serial(真实引擎 / worker / e2e,串行); pg 组用例超时放宽到 60s(迁移类用例并行时排队等 schema 锁)。server 测试 615s → 约 350s - CLAUDE.md 补测试约定;待办补 harness 去重与 continuous-series 偶发超时(疑似 Slot 评估竞争) - ADR-004 补充:维持 Deno,记录实际只用到 --allow-run 白名单 Co-Authored-By: Claude Opus 5.5 <[email protected]>
204 lines
9.1 KiB
TypeScript
204 lines
9.1 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import { readAccountCookie } from "../../src/modules/sources-identity/service/credentials.ts";
|
|
import { type BrowserHarness, createBrowserHarness, type Loose, ORIGIN, pgAvailable } from "./harness.ts";
|
|
|
|
// BR-2 凭据统一(对 testkit mock camofox + mock NexusPHP 登录态判定):
|
|
// 打开会话注入站点账号 Cookie → 首页 / 受保护页是登录态;Cookie 过期 → 需要人工登录;
|
|
// 人工在画面里登录后交还 → 读回站点域名 Cookie 写进账号(来源 browser);关闭时也回写;没有插件时降级说明。
|
|
|
|
const up = await pgAvailable();
|
|
const GOOD = "good-session-value-7f3a";
|
|
const STALE = "stale-session-value-11";
|
|
const FRESH = "fresh-browser-login-9c2";
|
|
|
|
describe.skipIf(!up)("BR-2 站点账号 Cookie ⇄ 浏览器 profile", () => {
|
|
let h: BrowserHarness;
|
|
let cookieAcc: string;
|
|
|
|
const setCookieAccount = async (cookie: string) => {
|
|
const ref = await h.secrets.create({ purpose: "site_cookie", plaintext: cookie });
|
|
await h.db
|
|
.updateTable("site_account")
|
|
.set({ auth_kind: "cookie", secret_ref: ref, session_secret_ref: null, status: "active", user_info: "{}" })
|
|
.where("id", "=", cookieAcc)
|
|
.execute();
|
|
};
|
|
const open = async (path = "/torrents.php") =>
|
|
(await h.req("/browser/sessions", { body: { siteId: h.siteId, accountId: cookieAcc, url: `${ORIGIN}${path}` } }))
|
|
.body.data as Loose;
|
|
const closeAll = async () => {
|
|
for (const s of (await h.req("/browser/sessions")).body.data.items as Loose[])
|
|
await h.req(`/browser/sessions/${s.id}`, { method: "DELETE" });
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
h = await createBrowserHarness("cred", {
|
|
sessionCookie: { name: "c_secure_pass", valid: [GOOD], fresh: FRESH },
|
|
});
|
|
cookieAcc = h.accountId;
|
|
}, 120_000);
|
|
afterAll(async () => {
|
|
await h?.close();
|
|
});
|
|
|
|
it("注入:Cookie 账号打开会话前写进该 profile,受保护页直接是登录态;响应里没有 Cookie 值", async () => {
|
|
await setCookieAccount(`c_secure_uid=1001; c_secure_pass=${GOOD}`);
|
|
const s = await open();
|
|
expect(s.controller).toBe("automation");
|
|
expect(s.currentUrl).toBe(`${ORIGIN}/torrents.php`);
|
|
expect(s.credential).toMatchObject({ status: "injected", source: "manual", count: 2 });
|
|
expect(JSON.stringify(s)).not.toContain(GOOD);
|
|
// 注入走 umt-cookies 插件,早于打开标签页;Cookie 按站点主机 host-only、https → secure
|
|
const calls = h.mock.calls.map((c) => `${c.method} ${c.path}`);
|
|
const inj = calls.indexOf(`POST /umt/sessions/${encodeURIComponent(s.profileKey)}/cookies`);
|
|
expect(inj).toBeGreaterThanOrEqual(0);
|
|
expect(inj).toBeLessThan(calls.lastIndexOf("POST /tabs"));
|
|
const jar = h.mock.cookieJars.get(s.profileKey)!;
|
|
expect(jar.find((c) => c.name === "c_secure_pass")).toMatchObject({
|
|
value: GOOD,
|
|
domain: "audiences.example",
|
|
secure: true,
|
|
});
|
|
await closeAll();
|
|
});
|
|
|
|
it("过期:注入后仍落在登录页 → 需要人工登录;人工登录后交还 → 回写账号 Cookie(来源浏览器),下次抓取用新值", async () => {
|
|
h.mock.loggedIn.clear();
|
|
h.mock.cookieJars.clear();
|
|
await setCookieAccount(`c_secure_uid=1001; c_secure_pass=${STALE}`);
|
|
const s = await open();
|
|
expect(s.controller).toBe("waiting");
|
|
expect(s.waitingReason).toBe("login_required");
|
|
expect(s.credential).toMatchObject({ status: "expired" });
|
|
expect(s.credential.message).toContain("登录一次");
|
|
const d = await h.db.selectFrom("decision").selectAll().where("subject_id", "=", s.id).executeTakeFirstOrThrow();
|
|
expect(d.summary).toContain("交还后全站生效");
|
|
|
|
// 人工接管,在画面里登录(桌面模式里的导航不经 API:直接改 mock 的登录态与当前地址)
|
|
expect((await h.req(`/browser/sessions/${s.id}/control/acquire`, { body: { reason: "登录" } })).status).toBe(200);
|
|
const tab = [...h.mock.tabs.values()].find((t) => t.userId === s.profileKey)!;
|
|
h.mock.loggedIn.add(s.profileKey);
|
|
h.mock.cookieJars.get(s.profileKey)!.push(
|
|
{
|
|
name: "c_secure_pass",
|
|
value: FRESH,
|
|
domain: "audiences.example",
|
|
path: "/",
|
|
expires: -1,
|
|
httpOnly: true,
|
|
secure: true,
|
|
},
|
|
// 别的域名的 Cookie 不回写
|
|
{
|
|
name: "_ga",
|
|
value: "tracker",
|
|
domain: ".analytics.example",
|
|
path: "/",
|
|
expires: -1,
|
|
httpOnly: false,
|
|
secure: true,
|
|
},
|
|
);
|
|
tab.url = `${ORIGIN}/torrents.php`;
|
|
const r = await h.req(`/browser/sessions/${s.id}/control/release`, { body: {} });
|
|
expect(r.status).toBe(200);
|
|
|
|
const acc = await readAccountCookie(h.ctx, cookieAcc);
|
|
expect(acc).toMatchObject({ source: "browser", applicable: true });
|
|
expect(acc!.header).toContain(`c_secure_pass=${FRESH}`);
|
|
expect(acc!.header).toContain("c_secure_uid=1001");
|
|
expect(acc!.header).not.toContain("_ga");
|
|
const row = await h.db.selectFrom("site_account").selectAll().where("id", "=", cookieAcc).executeTakeFirstOrThrow();
|
|
expect((row.user_info as Loose).cookieOrigin).toMatchObject({ source: "browser", by: "browser" });
|
|
const view = (await h.req(`/browser/sessions/${s.id}`)).body.data;
|
|
expect(view.credential).toMatchObject({ status: "injected", source: "browser" });
|
|
expect(view.credential.syncedAt).toBeTruthy();
|
|
expect(JSON.stringify(view)).not.toContain(FRESH);
|
|
|
|
await closeAll();
|
|
});
|
|
|
|
it("关闭会话时回写;停在登录页不回写(未登录的 Cookie 不覆盖存储)", async () => {
|
|
await setCookieAccount(`c_secure_uid=1001; c_secure_pass=${GOOD}`);
|
|
h.mock.cookieJars.clear();
|
|
const s = await open();
|
|
expect(s.credential.status).toBe("injected");
|
|
// 站点在浏览器里轮换了会话 Cookie
|
|
const jar = h.mock.cookieJars.get(s.profileKey)!;
|
|
jar.find((c) => c.name === "c_secure_pass")!.value = "rotated-by-site-44";
|
|
await h.req(`/browser/sessions/${s.id}`, { method: "DELETE" });
|
|
expect((await readAccountCookie(h.ctx, cookieAcc))!.header).toContain("c_secure_pass=rotated-by-site-44");
|
|
|
|
// 登录页:不回写
|
|
await setCookieAccount(`c_secure_uid=1001; c_secure_pass=${GOOD}`);
|
|
const s2 = await open("/login.php");
|
|
h.mock.cookieJars.get(s2.profileKey)!.find((c) => c.name === "c_secure_pass")!.value = "anonymous-junk";
|
|
await h.req(`/browser/sessions/${s2.id}`, { method: "DELETE" });
|
|
expect((await readAccountCookie(h.ctx, cookieAcc))!.header).toContain(`c_secure_pass=${GOOD}`);
|
|
});
|
|
|
|
it("有活动时每 10 分钟读回一次", async () => {
|
|
await setCookieAccount(`c_secure_uid=1001; c_secure_pass=${GOOD}`);
|
|
h.mock.cookieJars.clear();
|
|
const s = await open();
|
|
h.mock.cookieJars.get(s.profileKey)!.find((c) => c.name === "c_secure_pass")!.value = "periodic-rotated-5";
|
|
// 刚打开:不读回
|
|
await h.req(`/browser/sessions/${s.id}/snapshot`);
|
|
expect((await readAccountCookie(h.ctx, cookieAcc))!.header).toContain(`c_secure_pass=${GOOD}`);
|
|
// 会话创建时间拨到 11 分钟前:下一次活动触发后台读回
|
|
await h.db
|
|
.updateTable("browser_session")
|
|
.set({ created_at: new Date(Date.now() - 11 * 60_000) })
|
|
.where("id", "=", s.id)
|
|
.execute();
|
|
await h.req(`/browser/sessions/${s.id}/snapshot`);
|
|
await expect
|
|
.poll(async () => (await readAccountCookie(h.ctx, cookieAcc))!.header, { timeout: 3000 })
|
|
.toContain("c_secure_pass=periodic-rotated-5");
|
|
await closeAll();
|
|
});
|
|
|
|
it("匿名会话与 API Key 账号不注入", async () => {
|
|
const r = await h.req("/browser/sessions", { body: { siteId: h.siteId, accountId: null, url: `${ORIGIN}/` } });
|
|
expect(r.body.data.credential).toMatchObject({ status: "not_applicable" });
|
|
await closeAll();
|
|
const ref = await h.secrets.create({ purpose: "site_api_key", plaintext: "api-key-xyz" });
|
|
await h.db
|
|
.updateTable("site_account")
|
|
.set({ auth_kind: "api_key", secret_ref: ref })
|
|
.where("id", "=", cookieAcc)
|
|
.execute();
|
|
const before = h.mock.callsTo(/\/cookies$/).length;
|
|
const s = await open("/");
|
|
expect(s.credential).toMatchObject({ status: "not_applicable" });
|
|
expect(h.mock.callsTo(/\/cookies$/).length).toBe(before);
|
|
await closeAll();
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!up)("BR-2 浏览器服务没有 umt-cookies 插件", () => {
|
|
let h: BrowserHarness;
|
|
beforeAll(async () => {
|
|
h = await createBrowserHarness("cred_noplugin", {
|
|
sessionCookie: { name: "c_secure_pass", valid: [GOOD], fresh: FRESH },
|
|
cookieApi: "none",
|
|
});
|
|
}, 120_000);
|
|
afterAll(async () => {
|
|
await h?.close();
|
|
});
|
|
|
|
it("注入报不支持,会话照常打开并说明原因", async () => {
|
|
const ref = await h.secrets.create({ purpose: "site_cookie", plaintext: `c_secure_pass=${GOOD}` });
|
|
await h.db
|
|
.updateTable("site_account")
|
|
.set({ auth_kind: "cookie", secret_ref: ref })
|
|
.where("id", "=", h.accountId)
|
|
.execute();
|
|
const r = await h.req("/browser/sessions", { body: { siteId: h.siteId, url: `${ORIGIN}/` } });
|
|
expect(r.status).toBe(200);
|
|
expect(r.body.data.credential).toMatchObject({ status: "unsupported", count: 0 });
|
|
expect(r.body.data.credential.message).toContain("umt-cookies");
|
|
});
|
|
});
|