Files
UHD_Media_Tool/apps/server/test/sources-identity/migration-browser-identity.test.ts
T
rootandClaude Opus 5.5 adc853aa64 feat(sites): site requests follow browser UA, smooth UA switch, CF cookies browser-only (BR-4)
- every outbound site request (adapter list/search/detail/download/health,
  RSS, login, user data) carries the account UA via SourcesRuntime.siteFetch;
  no more Deno default UA
- priority: account manual > pending > global setting > browser identity
  (fallback: same-shape Windows Firefox UA); settings UA default is empty
- migration 0056: existing cookie/password accounts keep their old UA as
  ua_pending until re-login or browser cookie write-back switches cookie+UA
  in one transaction; browser_provider.identity caches the browser identity
- cf_clearance/__cf_bm stripped on browser write-back (names kept only)
- browser sessions get per-site egress (same proxy as HTTP, or direct)
- site account page shows UA source; raw UA in tech info

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 03:17:13 +08:00

101 lines
5.1 KiB
TypeScript

import { createMigrator, type DB, migrateToLatest, sql } from "@umt/db";
import { Kysely, PostgresDialect } from "kysely";
import pg from "pg";
import { afterAll, describe, expect, it } from "vitest";
import { PG_URL, pgAvailable, withSchemaLock } from "../auth-settings/harness.ts";
// 0056:存量 Cookie / 账密账号标"待切换"(沿用旧缺省 UA);手动 UA 与 API Key 账号不动;设置里的旧缺省 UA 清掉 = 跟随浏览器。
const hasPg = await pgAvailable();
const LEGACY =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";
describe.skipIf(!hasPg)("0056 浏览器身份迁移", () => {
const schemas: string[] = [];
const dbs: Kysely<DB>[] = [];
const admin = async (q: string) => {
const c = new pg.Client({ connectionString: PG_URL });
await c.connect();
await c.query(q).finally(() => c.end());
};
afterAll(async () => {
for (const d of dbs) await d.destroy();
await withSchemaLock(async () => {
for (const s of schemas) await admin(`drop schema if exists ${s} cascade`);
});
});
const setup = async (settingsUa: string | null) => {
const schema = `br4_mig_${process.pid.toString(36)}${Math.random().toString(36).slice(2, 6)}`;
schemas.push(schema);
await admin(`create schema ${schema}`);
const db = new Kysely<DB>({
dialect: new PostgresDialect({
pool: new pg.Pool({ connectionString: PG_URL, max: 2, options: `-c search_path=${schema},public` }),
}),
});
dbs.push(db);
await withSchemaLock(async () => {
const r = await createMigrator(db, { migrationTableSchema: schema }).migrateTo("0055_site_kind");
if (r.error) throw r.error;
});
await sql`insert into site (key, name, adapter, base_url) values ('audiences', '观众', 'nexusphp', 'https://a.example/')`.execute(
db,
);
await sql`insert into secret (id, name, purpose, ciphertext, nonce) values
('00000000-0000-4000-8000-000000000001', 's1', 'site_cookie', 'x', 'x'),
('00000000-0000-4000-8000-000000000002', 's2', 'site_password', 'x', 'x'),
('00000000-0000-4000-8000-000000000003', 's3', 'site_api_key', 'x', 'x'),
('00000000-0000-4000-8000-000000000004', 's4', 'site_cookie', 'x', 'x')`.execute(db);
await sql`insert into site_account (site_id, label, auth_kind, secret_ref, user_agent) values
((select id from site), 'cookie', 'cookie', '00000000-0000-4000-8000-000000000001', null),
((select id from site), 'password', 'password', '00000000-0000-4000-8000-000000000002', null),
((select id from site), 'apikey', 'api_key', '00000000-0000-4000-8000-000000000003', null),
((select id from site), 'manual', 'cookie', '00000000-0000-4000-8000-000000000004', 'My-UA/1'),
((select id from site), 'empty', 'cookie', null, null)`.execute(db);
if (settingsUa !== null)
await sql`insert into settings (key, value) values ('sites', ${JSON.stringify({ userAgent: settingsUa, proxyUrl: null })}::jsonb)`.execute(
db,
);
await withSchemaLock(() => migrateToLatest(db, { migrationTableSchema: schema }));
const accounts = await sql<{ label: string; ua_pending: string | null }>`
select label, ua_pending from site_account order by label`.execute(db);
const settings = await sql<{
value: Record<string, unknown>;
}>`select value from settings where key = 'sites'`.execute(db);
return { db, accounts: Object.fromEntries(accounts.rows.map((r) => [r.label, r.ua_pending])), settings };
};
it("设置仍是旧缺省:Cookie / 账密账号待切换到旧 UA,设置里的 UA 清掉(跟随浏览器)", async () => {
const r = await setup(LEGACY);
expect(r.accounts).toEqual({ apikey: null, cookie: LEGACY, empty: null, manual: null, password: LEGACY });
expect(r.settings.rows[0]!.value).not.toHaveProperty("userAgent");
expect(r.settings.rows[0]!.value).toHaveProperty("proxyUrl", null);
const cols = await sql<{ column_name: string }>`select column_name from information_schema.columns
where table_schema = current_schema() and table_name = 'browser_provider' and column_name = 'identity'`.execute(
r.db,
);
expect(cols.rows).toHaveLength(1);
});
it("用户改过全局 UA:保留为全局 UA,存量账号待切换到它;没有设置行时按旧缺省", async () => {
const r = await setup("Custom-UA/9");
expect(r.accounts.cookie).toBe("Custom-UA/9");
expect(r.settings.rows[0]!.value).toHaveProperty("userAgent", "Custom-UA/9");
const r2 = await setup(null);
expect(r2.accounts.password).toBe(LEGACY);
expect(r2.settings.rows).toHaveLength(0);
});
it("回滚只删列", async () => {
const r = await setup(LEGACY);
await withSchemaLock(async () => {
const m = await createMigrator(r.db, { migrationTableSchema: schemas.at(-1)! }).migrateTo("0055_site_kind");
if (m.error) throw m.error;
});
const cols = await sql<{ column_name: string }>`select column_name from information_schema.columns
where table_schema = current_schema() and column_name in ('ua_pending', 'identity')`.execute(r.db);
expect(cols.rows).toHaveLength(0);
});
});