Files
novalon-website/config/test/itest/env.js
T
zhangxiang 6bb7c557ee test(qa): e2e 装配修正 + 集成/门禁测试与工具链重构
- 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
  配置收敛。
2026-09-28 10:48:08 +08:00

148 lines
5.7 KiB
JavaScript
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.
/**
* 集成测试(真库)安全边界工具 —— 单一真源。
*
* 背景:验收报告 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,
};