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

120 lines
6.9 KiB
TypeScript
Raw Normal View History

// 路由域集成测试: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)往返 + 第五值拒绝。
// Task 13 起夹具走公共 helpers(createSiteFixture/withTestDb/rejectionMessage);
// 生命周期纪律不变:随机后缀建 site + finally 反序清理(route_registry → sites);本表无不可变触发器。
import { describe, it, expect } from 'vitest'
import { createSiteFixture, withTestDb, rejectionMessage, type Db } from './helpers.js'
type Fixture = { siteId: string; rand: string }
async function setupRoutesFixture(db: Db): Promise<Fixture> {
const site = await createSiteFixture(db, 'routes')
return { siteId: site.siteId, rand: site.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>) {
await withTestDb(async (db) => {
const f = await setupRoutesFixture(db)
try {
await fn(db, f)
} finally {
await cleanupRoutesFixture(db, f)
}
})
}
// 探测插入:除显式指定的列外全部换新值(随机 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/)
})
})
})