- 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 配置收敛。
148 lines
5.7 KiB
JavaScript
148 lines
5.7 KiB
JavaScript
/**
|
||
* 集成测试(真库)安全边界工具 —— 单一真源。
|
||
*
|
||
* 背景:验收报告 ACCEPTANCE_REVIEW_2026-09-21.md A-8 指出 jest.setup.js:12-50 全局 mock 了
|
||
* `@/generated/prisma/client` 与 `@/lib/cms/data-server`,因此整个单测套件从未触碰真实数据层。
|
||
* 本目录(config/test/itest/)提供一套独立的 Jest project,跑在**真实但一次性**的 SQLite 文件上。
|
||
*
|
||
* 硬安全约束(共享开发机):本文件的存在意义就是保证
|
||
* prisma/dev.db / dev.db / data.db / 任何 .env* 永远不可能被集成测试打开或写入。
|
||
* 实现方式:只允许 `file:<绝对路径>` 且绝对路径必须位于 realpath(os.tmpdir()) 之下。
|
||
* 任何不满足条件的 URL 立刻 throw —— globalSetup / 每个 worker 的 setupFiles / teardown 三处分别断言。
|
||
*/
|
||
// @ts-nocheck
|
||
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const os = require('os');
|
||
|
||
/** 仓库根(config/test/itest/env.js → 上三级) */
|
||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
|
||
|
||
/**
|
||
* 临时目录选择:优先 /tmp(任务书要求的落点;macOS 上 realpath 后为 /private/tmp,
|
||
* 与 os.tmpdir() 的 per-user var/folders 相比更可预测),不可写时回落 os.tmpdir()。
|
||
* 可用 ITEST_TMP_DIR 显式覆盖(CI 里指向专用挂载点)。
|
||
* 无论选哪个,后续所有断言都基于 realpath 后的结果。
|
||
*/
|
||
function pickTmpDir() {
|
||
const { constants } = fs;
|
||
const candidates = [process.env.ITEST_TMP_DIR, '/tmp', os.tmpdir()].filter(Boolean);
|
||
for (const dir of candidates) {
|
||
try {
|
||
fs.accessSync(dir, constants.W_OK | constants.R_OK);
|
||
return fs.realpathSync(dir);
|
||
} catch {
|
||
/* 下一个候选 */
|
||
}
|
||
}
|
||
throw new Error('[itest-guard] 找不到可写的临时目录');
|
||
}
|
||
|
||
const TMP_REALPATH = pickTmpDir();
|
||
|
||
/** 一次性集成库的固定路径(任务书建议的 /tmp/novalon-itest.db) */
|
||
const DB_PATH = path.join(TMP_REALPATH, 'novalon-itest.db');
|
||
|
||
/** globalSetup 写入、teardown 读取的运行期清单(含 dev.db 基线指纹) */
|
||
const MANIFEST_PATH = path.join(TMP_REALPATH, 'novalon-itest-manifest.json');
|
||
|
||
/** migrate diff 输出的 DDL 脚本(只写 /tmp,不写仓库) */
|
||
const DDL_PATH = path.join(TMP_REALPATH, 'novalon-itest-schema.sql');
|
||
|
||
/** 项目自身的数据源候选 —— 任何一个被集成测试打开都属安全事故 */
|
||
const FORBIDDEN_DB_BASENAMES = ['dev.db', 'data.db', 'test.db', 'prod.db', 'novalon.db'];
|
||
const FORBIDDEN_DB_PATHS = [
|
||
path.join(REPO_ROOT, 'prisma', 'dev.db'),
|
||
path.join(REPO_ROOT, 'dev.db'),
|
||
path.join(REPO_ROOT, 'data.db'),
|
||
];
|
||
|
||
/** 记录一个库文件的指纹(不存在时返回 null,同样可作为「未被创建」的证据) */
|
||
function fingerprint(file) {
|
||
try {
|
||
const st = fs.statSync(file);
|
||
return { path: file, ino: st.ino, size: st.size, mtimeMs: st.mtimeMs, exists: true };
|
||
} catch {
|
||
return { path: file, ino: null, size: null, mtimeMs: null, exists: false };
|
||
}
|
||
}
|
||
|
||
function devDbBaselines() {
|
||
return {
|
||
prismaDevDb: fingerprint(path.join(REPO_ROOT, 'prisma', 'dev.db')),
|
||
rootDevDb: fingerprint(path.join(REPO_ROOT, 'dev.db')),
|
||
rootDataDb: fingerprint(path.join(REPO_ROOT, 'data.db')),
|
||
};
|
||
}
|
||
|
||
function sameFingerprint(a, b) {
|
||
if (!a || !b) return false;
|
||
if (a.exists !== b.exists) return false;
|
||
if (!a.exists) return true;
|
||
return a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs;
|
||
}
|
||
|
||
/**
|
||
* 唯一允许的 URL 形态:`file:` + 绝对路径 + 位于真实临时目录之下。
|
||
* 注意 `file:./dev.db` 这类相对写法在 Prisma 里按 schema 目录解析,会命中 prisma/dev.db,
|
||
* 因此这里要求绝对路径,不做任何相对解析。
|
||
*/
|
||
function assertSafeDatasourceUrl(url, where) {
|
||
const label = `[itest-guard:${where}]`;
|
||
if (typeof url !== 'string' || url.length === 0) {
|
||
throw new Error(`${label} DATABASE_URL 未设置:集成测试拒绝在无显式数据源的情况下运行`);
|
||
}
|
||
if (!url.startsWith('file:')) {
|
||
throw new Error(`${label} 集成测试只允许 file: 协议,收到 ${url}`);
|
||
}
|
||
const raw = url.slice('file:'.length).split('?')[0];
|
||
if (!path.isAbsolute(raw)) {
|
||
throw new Error(`${label} file: URL 必须是绝对路径(相对路径会被 Prisma 按 prisma/ 目录解析,可能命中 dev.db),收到 ${url}`);
|
||
}
|
||
const resolved = fs.existsSync(raw) ? fs.realpathSync(raw) : path.resolve(raw);
|
||
if (!resolved.startsWith(TMP_REALPATH + path.sep)) {
|
||
throw new Error(`${label} 集成库必须位于临时目录 ${TMP_REALPATH} 之下,解析结果为 ${resolved}`);
|
||
}
|
||
if (FORBIDDEN_DB_BASENAMES.includes(path.basename(resolved))) {
|
||
throw new Error(`${label} 命中禁用库名 ${path.basename(resolved)}`);
|
||
}
|
||
if (FORBIDDEN_DB_PATHS.includes(resolved)) {
|
||
throw new Error(`${label} 命中项目数据源 ${resolved}`);
|
||
}
|
||
// 仓库内的任何路径都不允许作为集成库(防止误把 DATABASE_URL 指回 prisma/dev.db)
|
||
if (resolved.startsWith(REPO_ROOT + path.sep)) {
|
||
throw new Error(`${label} 集成库不得位于仓库内,解析结果为 ${resolved}`);
|
||
}
|
||
return resolved;
|
||
}
|
||
|
||
const DATASOURCE_URL = `file:${DB_PATH}`;
|
||
|
||
/** 把 migrate diff 的 --script 输出切成可逐条执行的语句 */
|
||
function splitStatements(sql) {
|
||
return sql
|
||
.split('\n')
|
||
.filter((line) => !line.startsWith('--') && line.trim().length > 0)
|
||
.join('\n')
|
||
.split(';')
|
||
.map((s) => s.trim())
|
||
.filter((s) => s.length > 0);
|
||
}
|
||
|
||
module.exports = {
|
||
REPO_ROOT,
|
||
TMP_REALPATH,
|
||
DB_PATH,
|
||
DDL_PATH,
|
||
MANIFEST_PATH,
|
||
DATASOURCE_URL,
|
||
FORBIDDEN_DB_PATHS,
|
||
assertSafeDatasourceUrl,
|
||
fingerprint,
|
||
devDbBaselines,
|
||
sameFingerprint,
|
||
splitStatements,
|
||
};
|