- Inject the site account Cookie (manual + auto-login session, session wins)
into the camofox profile before the first tab opens; password accounts
without a session run refresh-login first; API Key sites are skipped.
- Cookie expired after injection -> credential.status=expired; cookie
accounts go to waiting/login_required ("log in once, applies site-wide").
- Write back site-domain cookies from the browser on handover, session close,
every 10 min of activity, and from refresh-login when the stored cookie is
invalid (verified before saving). user_info.cookieOrigin records
source=browser; source shown as manual/login/browser in the origin view.
- New camofox plugin umt-cookies (GET/POST /umt/sessions/:userId/cookies
under the access key); profile dir 0700/files 0600 in the desktop image.
- Single entry: sources-identity/service/credentials.ts.
- Real check: HHClub cookie injected into a fresh profile -> home page
logged in (logout link present), 5 cookies read back.
Requires rebuilding umt-browser-desktop (docker compose build camofox &&
docker compose up -d camofox) to take effect.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
126 lines
5.0 KiB
TypeScript
126 lines
5.0 KiB
TypeScript
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<any> => {
|
|
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 });
|
|
});
|
|
});
|