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[] = []; 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({ 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; }>`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); }); });