Files
cms-website/tests/integration/helpers.ts
T
浅念 f1c181de02 test(db): 空库一键迁移验收 + globalSetup 临时库 + 公共夹具收敛(阶段1验收)
- vitest globalSetup:每次运行 CREATE sf_test_<ts> 临时库 → drizzle-orm 程序化迁移器
  全量迁移(apps/api/src/db/migrate.ts,drizzle-kit 无法指向运行时建库)→ 设
  DATABASE_URL_TEST 让 getDb() 自动回退到临时库;teardown DROP(WITH FORCE)。
  有 default 导出时 vitest 不捕获具名 teardown,故用双具名导出。
- db.full-migrate.test.ts:21 表齐 + 四张不可变触发器在库 + sf_test_* 护栏断言
  (current_database 不符即红,杜绝静默连开发库)。
- helpers.ts 收敛:rand/rejectionMessage/createSiteFixture/withImmutableCleanup/
  withTestDb,Task 6–12 七文件内联版替换为 import(断言、对照组、清场顺序不变)。
- scripts/migrate-test.sh:docker compose up --wait + pnpm test 一键验收。
- 实证:75 测试全绿(72+3);开发库 21 表行数总和 0(零写入);无 sf_test_* 残留。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 20:43:13 +08:00

70 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 集成测试公共夹具(Task 13 收敛):Task 6–12 七个域测试文件的内联重复件抽此共享。
// 收敛纪律:只抽"每个文件逐字重复"的实现——随机后缀、建 site、不可变表清场开关、
// 拒绝断言解包、连接生命周期;各文件的断言、对照组、清场范围与顺序保持不变。
import { getDb } from '../../apps/api/src/db/client.js'
export type Db = ReturnType<typeof getDb>
/** 随机后缀:毫秒时间戳 + 6 位随机,同毫秒并发行也可区分;所有 fixture 的唯一性来源。 */
export function rand(): string {
return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
}
// drizzle 会把 PostgresError 包成 DrizzleQueryError(message 只有 "Failed query: ..."),
// 服务端原始错误文本在 cause 里——断言触发器/约束/枚举身份时必须看 cause。
export async function rejectionMessage(db: Db, query: string): Promise<string> {
try {
await db.execute(query)
} catch (e: any) {
return String(e?.cause?.message ?? e?.message ?? e)
}
throw new Error(`expected query to be rejected: ${query}`)
}
/**
* 站点夹具:随机 key 建 site,返回 id + 清理句柄。
* 清理句柄只删 site 行——调用方必须先删完该站点的子表行再调用(FK 依赖顺序由各域自查)。
* name/origin 是不参与任何断言的装饰列,统一按 prefix 派生。
*/
export type SiteFixture = { siteId: string; key: string; rand: string; cleanup: () => Promise<void> }
export async function createSiteFixture(db: Db, prefix: string): Promise<SiteFixture> {
const r = rand()
const key = `${prefix}-${r}`
const site = (await db.execute(`insert into sites (id, key, name, default_locale, production_origin)
values (gen_random_uuid(), '${key}', '${prefix} fixture', 'en', 'https://${prefix}.example') returning id`)) as any
const siteId = site[0].id
return {
siteId,
key,
rand: r,
cleanup: async () => {
await db.execute(`delete from sites where id = '${siteId}'`)
},
}
}
/**
* 不可变表清场封装:DISABLE 触发器 → 删行 → ENABLE(嵌套 finally 保证触发器必然恢复)。
* 不可变表的生产语义是永不删;测试清场是唯一例外,删除范围由调用方谓词圈定。
* @param where 完整 WHERE 谓词(域测试按 site_id 圈;audit_logs 组织级行按 command_id 圈)
*/
export async function withImmutableCleanup(db: Db, table: string, trigger: string, where: string): Promise<void> {
await db.execute(`alter table ${table} disable trigger ${trigger}`)
try {
await db.execute(`delete from ${table} where ${where}`)
} finally {
await db.execute(`alter table ${table} enable trigger ${trigger}`)
}
}
/** 连接生命周期封装:每个测试持有独立 getDb 池,结束必关(清场先于关池执行)。 */
export async function withTestDb(fn: (db: Db) => Promise<void>): Promise<void> {
const db = getDb()
try {
await fn(db)
} finally {
await (db as any).$client.end()
}
}