统一 Node 22 与后端安全运行基线(07-15-production-trust-closeout-cleanup)

这个提交包含在:
2026-07-16 05:17:18 +08:00
父节点 dcf34d7ac6
当前提交 abb6a22f5c
共修改 28 个文件,包含 2250 行新增和 3012 行删除
@@ -0,0 +1,141 @@
const { validateDevSeedSafety } = require("../../../src/scripts/dev-seed-safety");
const { ensureEmployee } = require("../../../src/scripts/seed-dev");
function safeEnv(overrides = {}) {
return {
NODE_ENV: "development",
SEED_DEV_ALLOW: "1",
SEED_DEV_DATABASE_CONFIRM: "hyb_oa_test",
DATABASE_URL: "postgresql://local-user:[email protected]:5432/hyb_oa_test?schema=public",
...overrides,
};
}
function employeePayload() {
return {
employeeNo: "DEV001",
name: "开发员工",
phone: "00000000000",
email: "[email protected]",
password: "OneTimePassword1234",
departmentId: "department-1",
legalEntityId: "entity-1",
title: "测试",
directManagerId: null,
roles: ["employee"],
hireDate: new Date("2020-01-01T00:00:00.000Z"),
};
}
function buildPrisma(existing = { id: "employee-1" }) {
return {
employee: {
findFirst: vi.fn(async () => existing),
update: vi.fn(async () => ({ id: existing?.id ?? "employee-1" })),
create: vi.fn(async () => ({ id: "employee-created" })),
},
employeeRole: {
deleteMany: vi.fn(async () => ({ count: 0 })),
createMany: vi.fn(async () => ({ count: 1 })),
},
};
}
describe("dev seed safety guard", () => {
it("rejects production even when all explicit switches are present", () => {
expect(() => validateDevSeedSafety(safeEnv({ NODE_ENV: "production" }))).toThrowError(
expect.objectContaining({ code: "production_forbidden" }),
);
});
it("rejects execution without the explicit allow switch", () => {
expect(() => validateDevSeedSafety(safeEnv({ SEED_DEV_ALLOW: "" }))).toThrowError(
expect.objectContaining({ code: "explicit_allow_required" }),
);
});
it("rejects non-loopback database targets", () => {
expect(() => validateDevSeedSafety(safeEnv({
DATABASE_URL: "postgresql://local-user:[email protected]:5432/hyb_oa_test",
}))).toThrowError(expect.objectContaining({ code: "database_host_not_local" }));
});
it("rejects a query-string host override that would bypass the loopback authority", () => {
expect(() => validateDevSeedSafety(safeEnv({
DATABASE_URL: "postgresql://local-user:[email protected]:5432/hyb_oa_test?host=database.internal",
}))).toThrowError(expect.objectContaining({ code: "database_host_not_local" }));
});
it("rejects a missing or mismatched database confirmation", () => {
expect(() => validateDevSeedSafety(safeEnv({ SEED_DEV_DATABASE_CONFIRM: "" }))).toThrowError(
expect.objectContaining({ code: "database_confirmation_mismatch" }),
);
expect(() => validateDevSeedSafety(safeEnv({ SEED_DEV_DATABASE_CONFIRM: "another_database" }))).toThrowError(
expect.objectContaining({ code: "database_confirmation_mismatch" }),
);
});
it.each(["localhost", "127.0.0.1", "[::1]"])("accepts the local host %s with exact confirmation", (host) => {
expect(validateDevSeedSafety(safeEnv({
DATABASE_URL: `postgresql://local-user:local-password@${host}:5432/hyb_oa_test`,
}))).toEqual({ databaseName: "hyb_oa_test", resetPasswords: false });
});
it("enables one-time password rotation only through the separate reset switch", () => {
expect(validateDevSeedSafety(safeEnv({ SEED_DEV_RESET_PASSWORDS: "1" }))).toEqual({
databaseName: "hyb_oa_test",
resetPasswords: true,
});
});
it("does not include database credentials or target details in refusal messages", () => {
const env = safeEnv({
DATABASE_URL: "postgresql://sensitive-user:[email protected]:5432/sensitive_database",
SEED_DEV_DATABASE_CONFIRM: "sensitive_database",
});
try {
validateDevSeedSafety(env);
throw new Error("expected guard refusal");
} catch (error) {
expect(error.message).not.toMatch(/super-secret|sensitive-user|database\.internal|sensitive_database/);
}
});
});
describe("dev seed employee password behavior", () => {
it("preserves the password fields of an existing account by default", async () => {
const prisma = buildPrisma();
const result = await ensureEmployee(prisma, employeePayload());
const updateData = prisma.employee.update.mock.calls[0][0].data;
expect(updateData).not.toHaveProperty("password_hash");
expect(updateData).not.toHaveProperty("must_change_password");
expect(result.seedPasswordAction).toBe("preserved");
});
it("rotates an existing password only when requested and marks it one-time", async () => {
const prisma = buildPrisma();
const result = await ensureEmployee(prisma, employeePayload(), { resetPasswords: true });
const updateData = prisma.employee.update.mock.calls[0][0].data;
expect(updateData.password_hash).toEqual(expect.any(String));
expect(updateData.password_hash).not.toContain("OneTimePassword1234");
expect(updateData.must_change_password).toBe(true);
expect(result.seedPasswordAction).toBe("reset");
});
it("creates a missing development account without printing its credential", async () => {
const prisma = buildPrisma(null);
const result = await ensureEmployee(prisma, employeePayload());
const createData = prisma.employee.create.mock.calls[0][0].data;
expect(createData.password_hash).toEqual(expect.any(String));
expect(createData.password_hash).not.toContain("OneTimePassword1234");
expect(createData.must_change_password).toBe(true);
expect(result.seedPasswordAction).toBe("created");
});
});