142 行
5.4 KiB
JavaScript
142 行
5.4 KiB
JavaScript
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");
|
|
});
|
|
});
|