文件
hyb-oa/server/tests/unit/scripts/env-recovery-classification.test.js

118 行
4.7 KiB
JavaScript

const { execFile } = require("node:child_process");
const { mkdtemp, readFile, rm, writeFile } = require("node:fs/promises");
const os = require("node:os");
const path = require("node:path");
const { promisify } = require("node:util");
const execFileAsync = promisify(execFile);
const ROOT_DIR = path.resolve(__dirname, "../../../..");
const VALIDATOR = path.join(ROOT_DIR, "deploy/trialrun/validate-env-contract.js");
const CLASSIFICATION = path.join(
ROOT_DIR,
"deploy/trialrun/env-recovery-classification.json",
);
describe("environment recovery classification contract", () => {
let fixtureRoot;
let baseClassification;
beforeAll(async () => {
baseClassification = JSON.parse(await readFile(CLASSIFICATION, "utf8"));
});
beforeEach(async () => {
fixtureRoot = await mkdtemp(path.join(os.tmpdir(), "hyb-oa-env-recovery-"));
});
afterEach(async () => {
await rm(fixtureRoot, { recursive: true, force: true });
});
async function validate(classification = baseClassification) {
const fixturePath = path.join(fixtureRoot, "classification.json");
await writeFile(fixturePath, `${JSON.stringify(classification, null, 2)}\n`);
return execFileAsync("node", [VALIDATOR], {
cwd: ROOT_DIR,
env: {
...process.env,
ENV_CONTRACT_RECOVERY_CLASSIFICATION_PATH: fixturePath,
},
});
}
it("classifies every env contract key exactly once", async () => {
const result = await validate();
expect(result.stdout).toContain("环境恢复分类键数: 166");
});
it("skips tracked paths deleted in the current worktree before reading metadata", async () => {
const source = await readFile(VALIDATOR, "utf8");
const trackedFileLoop = source.indexOf("for (const relativePath of trackedFiles)");
const existsGuard = source.indexOf("if (!fs.existsSync(filePath)) continue;", trackedFileLoop);
const statRead = source.indexOf("fs.statSync(filePath)", trackedFileLoop);
expect(trackedFileLoop).toBeGreaterThanOrEqual(0);
expect(existsGuard).toBeGreaterThan(trackedFileLoop);
expect(statRead).toBeGreaterThan(existsGuard);
});
it("rejects a missing key", async () => {
const classification = structuredClone(baseClassification);
classification.categories.template_rebuild = classification.categories.template_rebuild
.filter((key) => key !== "APP_TIMEZONE");
await expect(validate(classification)).rejects.toMatchObject({
stderr: expect.stringContaining("环境恢复分类缺少 APP_TIMEZONE"),
});
});
it("rejects duplicate classification", async () => {
const classification = structuredClone(baseClassification);
classification.categories.direct_rotate.push("APP_TIMEZONE");
await expect(validate(classification)).rejects.toMatchObject({
stderr: expect.stringContaining("环境恢复键 APP_TIMEZONE 重复分类"),
});
});
it("rejects keys outside the env contract", async () => {
const classification = structuredClone(baseClassification);
classification.categories.template_rebuild.push("SYNTHETIC_EXTRA_KEY");
await expect(validate(classification)).rejects.toMatchObject({
stderr: expect.stringContaining("环境恢复分类包含 .env.example 外键 SYNTHETIC_EXTRA_KEY"),
});
});
it("rejects unknown categories", async () => {
const classification = structuredClone(baseClassification);
classification.categories.unreviewed = [];
await expect(validate(classification)).rejects.toMatchObject({
stderr: expect.stringContaining("环境恢复分类清单存在未知类别 unreviewed"),
});
});
it("rejects illegal key names and suspected values", async () => {
const classification = structuredClone(baseClassification);
classification.categories.template_rebuild.push("JWT_SECRET=not-a-key-name");
await expect(validate(classification)).rejects.toMatchObject({
stderr: expect.stringContaining("含非法键名或疑似值"),
});
});
it("keeps FIELD_ENCRYPTION_KEY in preserve_original", async () => {
const classification = structuredClone(baseClassification);
classification.categories.preserve_original = [];
classification.categories.direct_rotate.push("FIELD_ENCRYPTION_KEY");
await expect(validate(classification)).rejects.toMatchObject({
stderr: expect.stringContaining("FIELD_ENCRYPTION_KEY 必须归入 preserve_original"),
});
});
it("keeps POSTGRES_PASSWORD in downtime_reset", async () => {
const classification = structuredClone(baseClassification);
classification.categories.downtime_reset = [];
classification.categories.direct_rotate.push("POSTGRES_PASSWORD");
await expect(validate(classification)).rejects.toMatchObject({
stderr: expect.stringContaining("POSTGRES_PASSWORD 必须归入 downtime_reset"),
});
});
});