- e2e:修死选择器与蜜罐误命中、补断言、GA4/security-headers 用例诚实标注 skip 边界;新增 assert-helpers/fixtures/hydrated/primary-nav/touch-targets 等复用桩。 - 集成层:config/test/jest.integration.config.js + tests-integration/ 真库 一次性 SQLite 用例,teardown 守卫开发库指纹。 - 门禁工具链:jest.setup 归位到根、scripts/accessibility 全站 axe 节点计数、 check-brand-text-token/check-motion-constraints 机械守卫、stryker/lighthouse 配置收敛。
93 lines
3.8 KiB
TypeScript
93 lines
3.8 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from '@jest/globals';
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { prisma, resetDb, disconnect, unique, seedModel } from './helpers/harness';
|
||
|
||
const DEV_DB = path.resolve(__dirname, '..', 'prisma', 'dev.db');
|
||
|
||
interface DbFingerprint {
|
||
exists: boolean;
|
||
ino: number | null;
|
||
size: number | null;
|
||
mtimeMs: number | null;
|
||
}
|
||
|
||
interface Manifest {
|
||
datasourceUrl: string;
|
||
baseline: Record<string, DbFingerprint>;
|
||
}
|
||
|
||
/** globalSetup 落盘的运行期清单(与临时库同目录,见 config/test/itest/env.js) */
|
||
function readManifest(): Manifest {
|
||
const dbFile = process.env.NOVALON_ITEST_DB;
|
||
if (!dbFile) throw new Error('NOVALON_ITEST_DB 未设置:worker 未经过 config/test/itest/setup-files.js');
|
||
const file = path.join(path.dirname(dbFile), 'novalon-itest-manifest.json');
|
||
return JSON.parse(fs.readFileSync(file, 'utf8')) as Manifest;
|
||
}
|
||
|
||
/**
|
||
* 安全边界规格:这个文件不测业务,它测的是「其余所有 itest 到底连在哪个库上」。
|
||
* 它是 A-8 修复的前置条件 —— 集成层一旦误连 prisma/dev.db,这里必须变红。
|
||
*/
|
||
describe('集成层数据源边界', () => {
|
||
beforeAll(async () => {
|
||
await resetDb();
|
||
});
|
||
|
||
afterAll(async () => {
|
||
await disconnect();
|
||
});
|
||
|
||
it('打开的是临时目录里的一次性 SQLite 文件,项目数据源指纹未变', async () => {
|
||
// 1) 引擎自报打开的文件 —— 不是「我们以为的路径」,而是 SQLite 实际 attach 的路径
|
||
const attached = await prisma.$queryRawUnsafe<Array<{ file: string }>>('PRAGMA database_list');
|
||
const file = attached[0]?.file ?? '';
|
||
expect(file).toMatch(/novalon-itest\.db$/);
|
||
expect(file).not.toMatch(/(^|[/\\])(dev|data|test|prod)\.db$/);
|
||
|
||
// 2) 真的在跟 SQLite 说话:引擎版本号只有真实引擎给得出,任何 jest mock 都造不出来
|
||
const version = await prisma.$queryRawUnsafe<Array<{ v: string }>>('SELECT sqlite_version() AS v');
|
||
expect(version[0]?.v).toMatch(/^\d+\.\d+\.\d+$/);
|
||
|
||
// 3) worker 的 DATABASE_URL 与 globalSetup 落盘的 manifest 一致,且是绝对 file: 路径
|
||
const envUrl = process.env.DATABASE_URL ?? '';
|
||
const manifest = readManifest();
|
||
expect(envUrl.startsWith('file:/')).toBe(true);
|
||
expect(manifest.datasourceUrl).toBe(envUrl);
|
||
expect(envUrl).toBe(`file:${process.env.NOVALON_ITEST_DB}`);
|
||
|
||
// 4) 项目数据源在集成测试运行前后是同一个文件且完全未写(inode + size + mtime 三重比对)
|
||
const now = fs.statSync(DEV_DB);
|
||
const before = manifest.baseline.prismaDevDb;
|
||
if (!before) throw new Error('manifest 缺少 prismaDevDb 基线,守卫无法比对');
|
||
expect([now.ino, now.size, Math.round(now.mtimeMs)]).toEqual([
|
||
before.ino,
|
||
before.size,
|
||
before.mtimeMs === null ? null : Math.round(before.mtimeMs),
|
||
]);
|
||
});
|
||
|
||
it('应用的 schema 带真实约束(唯一索引与外键都生效,不是空表壳)', async () => {
|
||
const code = unique('it-model');
|
||
const { id } = await seedModel(code, []);
|
||
|
||
await prisma.contentItem.create({
|
||
data: { modelId: id, modelCode: code, title: '第一版', slug: 'dup', data: '{}' },
|
||
});
|
||
|
||
// 同一 (modelCode, slug, locale) 的第二次写入必须被真实唯一索引拒绝
|
||
await expect(
|
||
prisma.contentItem.create({
|
||
data: { modelId: id, modelCode: code, title: '第二版', slug: 'dup', data: '{}' },
|
||
}),
|
||
).rejects.toMatchObject({ code: 'P2002' });
|
||
|
||
// modelId 指向不存在的模型必须被真实外键(ContentItem_modelId_fkey)拒绝
|
||
await expect(
|
||
prisma.contentItem.create({
|
||
data: { modelId: 'no-such-model', modelCode: code, title: '孤儿', slug: unique('orphan'), data: '{}' },
|
||
}),
|
||
).rejects.toThrow();
|
||
});
|
||
});
|