建立生产密钥恢复合同(08-01-production-secret-recovery-env-externalization)
这个提交包含在:
@@ -0,0 +1,106 @@
|
||||
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("环境恢复分类键数: 164");
|
||||
});
|
||||
|
||||
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"),
|
||||
});
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户