import { sameCookieHeader } from "@umt/adapters"; import { createMockNexusPhpSite, type MockNexusPhpSite } from "@umt/testkit"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { readAccountCookie, registerBrowserCookieReader, writeBrowserCookie, } from "../../src/modules/sources-identity/service/credentials.ts"; import { createHarness, type Harness, pgAvailable } from "./harness.ts"; // BR-2 凭据单一入口(mock NexusPHP 登录态判定):来源判定(手填 / 自动登录 / 浏览器回写)、回写只在有变化时写, // refresh-login 发现 Cookie 失效时从浏览器 profile 读回试一次,有效才回写。 const up = await pgAvailable(); // biome-ignore lint/suspicious/noExplicitAny: 响应体断言 const json = async (r: Response): Promise => { const body = await r.json(); if (!r.ok) throw new Error(`${r.status} ${JSON.stringify(body)}`); return body.data; }; describe.skipIf(!up)("BR-2 站点账号 Cookie 单一入口", () => { let h: Harness; let mock: MockNexusPhpSite; let siteId: string; let accountId: string; let browserCookie: string | null = null; beforeAll(async () => { mock = await createMockNexusPhpSite({ username: "alice", password: "s3cret-pw" }); h = await createHarness("br2cred"); // 测试用 secret 替身不记 rotated_at:补上(来源判定依赖它) const replace = h.secrets.replace.bind(h.secrets); h.secrets.replace = async (ref, plaintext) => { await replace(ref, plaintext); await h.db.updateTable("secret").set({ rotated_at: new Date() }).where("id", "=", ref).execute(); }; const site = await json( await h.req("/sites", { body: { key: "audiences", name: "观众", adapter: "nexusphp", baseUrl: `${mock.baseUrl}/`, rateLimit: { minIntervalMs: 0, concurrency: 10 }, }, }), ); siteId = site.id; registerBrowserCookieReader(h.ctx, async () => browserCookie); }); afterAll(async () => { await h?.close(); await mock?.close(); }); const refresh = async () => (await json(await h.req(`/sites/${siteId}/refresh-login`, { body: { wait: true, accountId } }))).result; it("手填 Cookie → 来源 manual;浏览器回写 → browser;再手填 → manual", async () => { const r = await json( await h.req(`/sites/${siteId}/credentials`, { method: "PUT", body: { kind: "cookie", cookie: "c_secure_uid=1; c_secure_pass=stale" }, }), ); accountId = r.siteAccountId; expect(await readAccountCookie(h.ctx, accountId)).toMatchObject({ source: "manual", applicable: true }); expect(await writeBrowserCookie(h.ctx, accountId, "c_secure_pass=stale; c_secure_uid=1")).toMatchObject({ changed: false, }); const w = await writeBrowserCookie(h.ctx, accountId, "c_secure_uid=1; c_secure_pass=from-browser"); expect(w).toMatchObject({ changed: true, count: 2 }); expect(await readAccountCookie(h.ctx, accountId)).toMatchObject({ source: "browser", header: "c_secure_pass=from-browser; c_secure_uid=1", }); await new Promise((r) => setTimeout(r, 10)); await json( await h.req(`/sites/${siteId}/credentials`, { method: "PUT", body: { kind: "cookie", cookie: "c_secure_uid=1; c_secure_pass=stale" }, }), ); // cookieOrigin 记的时间比 secret 最近写入早 5 秒以上才判手填;测试里直接把回写时间拨早 await h.db .updateTable("site_account") .set({ user_info: JSON.stringify({ cookieOrigin: { source: "browser", by: "browser", at: "2020-01-01T00:00:00Z" } }), }) .where("id", "=", accountId) .execute(); expect(await readAccountCookie(h.ctx, accountId)).toMatchObject({ source: "manual" }); }); it("refresh-login:存的 Cookie 失效,浏览器里有有效登录态 → 验证通过后回写,账号 active", async () => { browserCookie = "c_secure_uid=1; c_secure_pass=also-stale"; const bad = await refresh(); expect(bad).toMatchObject({ ok: false, status: "expired" }); browserCookie = mock.cookie; const ok = await refresh(); expect(ok).toMatchObject({ ok: true, status: "active" }); const acc = await readAccountCookie(h.ctx, accountId); expect(acc).toMatchObject({ source: "browser" }); expect(acc!.header).toContain("mock-session-secret"); // 之后 HTTP 抓取取凭证走同一存储 const used = await h.rt["siteSecrets"](siteId).resolve({ siteId, kind: "cookie" }); expect(sameCookieHeader(used, acc!.header)).toBe(true); }); it("API Key 账号不适用", async () => { const r = await json( await h.req(`/sites/${siteId}/credentials`, { method: "PUT", body: { kind: "api_key", label: "k", apiKey: "k-123" }, }), ); expect(await readAccountCookie(h.ctx, r.siteAccountId)).toMatchObject({ applicable: false, header: null }); expect(await writeBrowserCookie(h.ctx, r.siteAccountId, "a=1")).toMatchObject({ changed: false }); }); });