Files
cms-website/tests/integration/db.routes.test.ts
T

133 lines
7.5 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.
// 路由域集成测试:route_registry 唯一索引 route_exact_uq (site_id, host, exact_route_key,
// query_key) NULLS NOT DISTINCT(0005 迁移手写 SQL——drizzle-kit 0.31 不生成该子句)。
// 断言组(V2 设计 ADR-007 双键语义):
// 1. exact_route_key 区分大小写——/About 与 /about 是两条路由,均可插入(设计语义,不是 bug);
// 2. 纯路径唯一:同 (site, host, exact_route_key) 且 query_key 均 NULL 的第二条插入拒绝
// ——这正是 NULLS NOT DISTINCT 的意义(默认 NULLS DISTINCT 下两行 NULL 不冲突,挡不住);
// 3. query_key 参与唯一:同 exact key 下 query_key='p' 与 NULL 共存;同 'p' 第二条拒绝;
// 4. route_kind enum 六值(html/asset/redirect/gone/dynamic/unresolved)往返 + 第七值拒绝;
// 5. query_policy enum 四值(ignore/exact/preserve/dynamic)往返 + 第五值拒绝。
// 生命周期纪律:随机后缀建 site + finally 反序清理(route_registry → sites);本表无不可变触发器。
import { describe, it, expect } from 'vitest'
import { getDb } from '../../apps/api/src/db/client.js'
type Db = ReturnType<typeof getDb>
type Fixture = { siteId: string; rand: string }
async function setupRoutesFixture(db: Db): Promise<Fixture> {
const rand = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const site = (await db.execute(`insert into sites (id, key, name, default_locale, production_origin)
values (gen_random_uuid(), 'routes-${rand}', 'RoutesFixture', 'en', 'https://routes.example') returning id`)) as any
return { siteId: site[0].id, rand }
}
// 反序清理:route_registry(无不可变触发器,直接删)→ sites(级联挡在 site 前,先删子表)。
async function cleanupRoutesFixture(db: Db, f: Fixture) {
await db.execute(`delete from route_registry where site_id = '${f.siteId}'`)
await db.execute(`delete from sites where id = '${f.siteId}'`)
}
async function withRoutesFixture(fn: (db: Db, f: Fixture) => Promise<void>) {
const db = getDb()
const f = await setupRoutesFixture(db)
try {
await fn(db, f)
} finally {
await cleanupRoutesFixture(db, f)
await (db as any).$client.end()
}
}
// drizzle 会把 PostgresError 包成 DrizzleQueryError(message 只有 "Failed query: ..."),
// 服务端原始错误文本在 cause 里——断言约束/枚举身份时必须看 cause。
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}`)
}
// 探测插入:除显式指定的列外全部换新值(随机 suffix 的 id/path/collision_key),保证被测
// 唯一索引 (site_id, host, exact_route_key, query_key) 是唯一可能的冲突源;默认 query_key=NULL
// (纯路径路由)。其余 NOT NULL 列填合法基线值。
function routeProbe(f: Fixture, overrides: {
host?: string
exactRouteKey?: string
queryKey?: string | null
routeKind?: string
queryPolicy?: string
} = {}) {
const suffix = Math.random().toString(36).slice(2, 8)
const key = overrides.exactRouteKey ?? `/probe-${suffix}`
const host = overrides.host ?? 'example.com'
const queryKeySql = overrides.queryKey == null ? 'null' : `'${overrides.queryKey}'`
return `insert into route_registry (id, site_id, host, source_path_raw, route_path, exact_route_key, collision_key, query_policy, query_key, source_status, target_status, route_kind, indexable, priority, verification_status)
values (gen_random_uuid(), '${f.siteId}', '${host}', '/raw-${suffix}', '${key}', '${key}', 'collision-${suffix}', '${overrides.queryPolicy ?? 'ignore'}', ${queryKeySql}, '200', '200', '${overrides.routeKind ?? 'html'}', true, 100, 'pending') returning id`
}
describe('路由域约束', () => {
it('exact_route_key 区分大小写(ADR-007 语义):同站同 host 下 /About 与 /about 两条均可插入', async () => {
await withRoutesFixture(async (db, f) => {
await db.execute(routeProbe(f, { exactRouteKey: '/About' }))
await db.execute(routeProbe(f, { exactRouteKey: '/about' }))
// 两条都真实入库,各自独立成行(exact key 不折叠大小写——这是请求匹配键的设计语义);
// 排序在 JS 侧做,不依赖库端 collation(默认排序 /about 在 /About 前)
const rows = (await db.execute(`select exact_route_key from route_registry
where site_id = '${f.siteId}' and exact_route_key in ('/About', '/about')`)) as any
expect(rows.map((r: any) => r.exact_route_key).sort()).toEqual(['/About', '/about'])
})
})
it('纯路径唯一(NULLS NOT DISTINCT):同 (site, host, exact_route_key) 且 query_key 均 NULL 的第二条拒绝', async () => {
await withRoutesFixture(async (db, f) => {
await db.execute(routeProbe(f, { exactRouteKey: '/dup-null' }))
// 若索引是默认 NULLS DISTINCT,两行 query_key=NULL 不冲突、会静默放行——本断言专守这一点
expect(await rejectionMessage(db, routeProbe(f, { exactRouteKey: '/dup-null' })))
.toMatch(/route_exact_uq/)
})
})
it('query_key 参与唯一:同 exact key 下 NULL 与 p 共存;同 query_key 第二条拒绝', async () => {
await withRoutesFixture(async (db, f) => {
// 纯路径路由(NULL)与查询路由('p')是不同路由,合法共存
await db.execute(routeProbe(f, { exactRouteKey: '/query', queryKey: null }))
await db.execute(routeProbe(f, { exactRouteKey: '/query', queryKey: 'p' }))
// 同 exact key 同 query_key 的第二条 → 唯一索引拒绝(身份断言匹配索引名)
expect(await rejectionMessage(db, routeProbe(f, { exactRouteKey: '/query', queryKey: 'p' })))
.toMatch(/route_exact_uq/)
})
})
it('route_kind enum 六值(html/asset/redirect/gone/dynamic/unresolved)可写往返,第七值拒绝', async () => {
await withRoutesFixture(async (db, f) => {
const inserted = (await db.execute(routeProbe(f))) as any
const routeId = inserted[0].id
for (const kind of ['asset', 'redirect', 'gone', 'dynamic', 'unresolved', 'html']) {
await db.execute(`update route_registry set route_kind = '${kind}' where id = '${routeId}'`)
const row = (await db.execute(`select route_kind from route_registry where id = '${routeId}'`)) as any
expect(row[0].route_kind).toBe(kind)
}
// enum 在库:类型化列拒绝六值之外的任意字符串
expect(await rejectionMessage(db, `update route_registry set route_kind = 'bogus' where id = '${routeId}'`))
.toMatch(/route_kind/)
})
})
it('query_policy enum 四值(ignore/exact/preserve/dynamic)可写往返,第五值拒绝', async () => {
await withRoutesFixture(async (db, f) => {
const inserted = (await db.execute(routeProbe(f))) as any
const routeId = inserted[0].id
for (const policy of ['exact', 'preserve', 'dynamic', 'ignore']) {
await db.execute(`update route_registry set query_policy = '${policy}' where id = '${routeId}'`)
const row = (await db.execute(`select query_policy from route_registry where id = '${routeId}'`)) as any
expect(row[0].query_policy).toBe(policy)
}
// enum 在库:类型化列拒绝四值之外的任意字符串
expect(await rejectionMessage(db, `update route_registry set query_policy = 'bogus' where id = '${routeId}'`))
.toMatch(/route_query_policy/)
})
})
})