fix: 分离日常发布与首次初始化(09-09-existing-deploy-seed-separation)
这个提交包含在:
@@ -0,0 +1,108 @@
|
||||
const { spawnSync } = require("node:child_process");
|
||||
const { createHash, randomBytes } = require("node:crypto");
|
||||
const path = require("node:path");
|
||||
|
||||
const testUrl = process.env.DEPLOY_INIT_TEST_DATABASE_URL;
|
||||
const configurationModels = [
|
||||
"expenseType", "bankInstitution", "approvalRule", "documentSequence",
|
||||
"documentCategory", "workflowDefinition", "leaveType", "leavePolicy",
|
||||
"featureFlag", "holidayCalendar", "holidayCalendarDay", "attendancePolicy",
|
||||
"budgetControlRule", "department", "employee", "employeeRole", "workspaceSetting",
|
||||
];
|
||||
|
||||
describe.skipIf(!testUrl)("real init against an explicitly supplied empty synthetic database", () => {
|
||||
let prisma;
|
||||
let pool;
|
||||
let environment;
|
||||
const originalUrl = process.env.DATABASE_URL;
|
||||
|
||||
beforeAll(async () => {
|
||||
const parsed = new URL(testUrl);
|
||||
if (parsed.hostname !== "127.0.0.1" || !/^\/hyb_seed_test_[a-z0-9]+$/.test(parsed.pathname)) {
|
||||
throw new Error("synthetic_loopback_database_required");
|
||||
}
|
||||
process.env.DATABASE_URL = testUrl;
|
||||
({ prisma, pool } = require("../../../src/prismaClient").createPrismaClient());
|
||||
const tables = await prisma.$queryRaw`SELECT count(*)::int AS total FROM information_schema.tables WHERE table_schema = 'public'`;
|
||||
expect(tables[0].total).toBe(0);
|
||||
environment = {
|
||||
PATH: process.env.PATH, NODE_ENV: "test", DATABASE_URL: testUrl,
|
||||
JWT_SECRET: randomBytes(32).toString("hex"),
|
||||
TRIALRUN_ADMIN_EMAIL: "[email protected]",
|
||||
TRIALRUN_ADMIN_PHONE: "19900000000",
|
||||
TRIALRUN_ADMIN_PASSWORD: randomBytes(24).toString("hex") + "Aa1!",
|
||||
TRIALRUN_ADMIN_NAME: "Synthetic Admin",
|
||||
TRIALRUN_ADMIN_EMPLOYEE_NO: "SEEDTEST",
|
||||
TRIALRUN_EXTRA_FORCE_ON_FLAGS: "",
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
try {
|
||||
if (prisma) await prisma.$disconnect();
|
||||
} finally {
|
||||
if (pool) await pool.end();
|
||||
if (originalUrl === undefined) delete process.env.DATABASE_URL;
|
||||
else process.env.DATABASE_URL = originalUrl;
|
||||
}
|
||||
});
|
||||
|
||||
function run(mode) {
|
||||
return spawnSync(process.execPath, ["src/scripts/deploy-init.js"], {
|
||||
cwd: path.resolve(__dirname, "../../.."),
|
||||
env: { ...environment, DEPLOY_DATA_MODE: mode },
|
||||
encoding: "utf8", timeout: 120000, maxBuffer: 4 * 1024 * 1024,
|
||||
});
|
||||
}
|
||||
|
||||
async function snapshot() {
|
||||
const hashes = {};
|
||||
for (const model of configurationModels) {
|
||||
const rows = await prisma[model].findMany();
|
||||
const serialized = rows.map((row) => JSON.stringify(row)).sort();
|
||||
hashes[model] = createHash("sha256").update(JSON.stringify(serialized)).digest("hex");
|
||||
}
|
||||
return hashes;
|
||||
}
|
||||
|
||||
it("initializes an empty database and preserves customized configuration through repeated existing runs and missing-config failures", async () => {
|
||||
const initialized = run("initialize");
|
||||
expect(initialized.status).toBe(0);
|
||||
expect(initialized.stdout).toContain('"status":"INIT_PASS"');
|
||||
const flag = await prisma.featureFlag.findFirstOrThrow();
|
||||
await prisma.featureFlag.update({
|
||||
where: { id: flag.id },
|
||||
data: { override_mode: "force_off", rollout_type: "department_allowlist", department_ids: [], version: 77, note: "synthetic customization" },
|
||||
});
|
||||
const workflow = await prisma.workflowDefinition.findFirstOrThrow({ where: { entity_type: "reimbursement" } });
|
||||
await prisma.workflowDefinition.update({ where: { id: workflow.id }, data: { version: 42, name: "Synthetic custom workflow" } });
|
||||
const holiday = await prisma.holidayCalendarDay.findFirstOrThrow();
|
||||
await prisma.holidayCalendarDay.update({ where: { id: holiday.id }, data: { name: "Synthetic custom holiday" } });
|
||||
const expected = await snapshot();
|
||||
const migrations = await prisma.$queryRaw`SELECT migration_name FROM "_prisma_migrations" WHERE finished_at IS NOT NULL ORDER BY migration_name`;
|
||||
expect(migrations.length).toBeGreaterThan(0);
|
||||
for (const attempt of [1, 2]) {
|
||||
const existing = run("existing");
|
||||
expect(existing.status, "existing attempt " + attempt).toBe(0);
|
||||
expect(existing.stdout).toContain('"status":"INIT_PASS"');
|
||||
expect(existing.stdout).not.toContain('"stage":"seed:');
|
||||
expect(await snapshot()).toEqual(expected);
|
||||
}
|
||||
for (const [model, field, where] of [
|
||||
["expenseType", "enabled", {}],
|
||||
["approvalRule", "enabled", {}],
|
||||
["workflowDefinition", "is_active", { entity_type: "reimbursement" }],
|
||||
]) {
|
||||
const enabled = await prisma[model].findMany({ where: { ...where, [field]: true }, select: { id: true } });
|
||||
await prisma[model].updateMany({ where, data: { [field]: false } });
|
||||
const beforeFailure = await snapshot();
|
||||
const failed = run("existing");
|
||||
expect(failed.status).toBe(1);
|
||||
expect(failed.stdout).toContain("REQUIRED_CONFIGURATION_MISSING");
|
||||
expect(failed.stdout).not.toContain('"status":"INIT_PASS"');
|
||||
expect(await snapshot()).toEqual(beforeFailure);
|
||||
await prisma[model].updateMany({ where: { id: { in: enabled.map((row) => row.id) } }, data: { [field]: true } });
|
||||
}
|
||||
expect(await prisma.$queryRaw`SELECT migration_name FROM "_prisma_migrations" WHERE finished_at IS NOT NULL ORDER BY migration_name`).toEqual(migrations);
|
||||
}, 240000);
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户