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:local-password@127.0.0.1:5432/hyb_oa_test?schema=public", ...overrides, }; } function employeePayload() { return { employeeNo: "DEV001", name: "开发员工", phone: "00000000000", email: "dev-user@example.test", 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:local-password@database.internal: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:local-password@127.0.0.1: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:super-secret@database.internal: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"); }); });