const { resolveEmailDeliveryStatus, sendNotificationEmail, } = require("../../../src/services/email"); function buildEmailConfig(overrides = {}) { return { emailEnabled: true, emailFrom: "no-reply@hyinda.test", emailBaseUrl: "https://oa.hyinda.test/", appOrigin: "", smtpHost: "smtp.hyinda.test", smtpPort: 587, smtpSecure: false, smtpUser: "sender", smtpPass: "test-only-password", ...overrides, }; } describe("email delivery status", () => { it("reports ready only when SMTP delivery has the required public settings", () => { expect( resolveEmailDeliveryStatus({ emailEnabled: true, emailFrom: "no-reply@hyinda.test", smtpHost: "smtp.hyinda.test", smtpPort: 587, smtpSecure: false, smtpUser: "", smtpPass: "", }), ).toEqual({ ready: true, reasons: [], }); }); it("reports non-secret missing SMTP reasons", () => { expect( resolveEmailDeliveryStatus({ emailEnabled: true, emailFrom: "", smtpHost: "", smtpPort: 0, smtpSecure: false, smtpUser: "sender", smtpPass: "", }), ).toEqual({ ready: false, reasons: ["email_from_missing", "smtp_host_missing", "smtp_port_invalid", "smtp_auth_incomplete"], }); }); }); describe("notification email delivery", () => { it("skips delivery without creating a transporter when email is disabled", async () => { const createTransport = vi.fn(); const prisma = { employee: { findMany: vi.fn() } }; const result = await sendNotificationEmail({ prisma, config: buildEmailConfig({ emailEnabled: false }), recipientEmployeeIds: ["employee-1"], subject: "待办提醒", body: "请处理待办。", createTransport, }); expect(result).toEqual({ skipped: true, sent: 0, failed: 0 }); expect(createTransport).not.toHaveBeenCalled(); expect(prisma.employee.findMany).not.toHaveBeenCalled(); }); it("creates a Nodemailer 9 transporter and sends text mail with a safe link", async () => { const sendMail = vi.fn(async () => ({ messageId: "message-1" })); const createTransport = vi.fn(() => ({ sendMail })); const prisma = { employee: { findMany: vi.fn(async () => [ { id: "employee-1", email: "employee@hyinda.test", name: "员工一" }, { id: "employee-2", email: null, name: "员工二" }, ]), }, }; const result = await sendNotificationEmail({ prisma, config: buildEmailConfig({ smtpHost: "smtp-success.hyinda.test" }), recipientEmployeeIds: ["employee-1", "employee-2", "employee-1"], subject: "待办提醒", body: "请处理待办。", type: "workflow_task", targetPath: "/workflows/tasks/1", requestId: "request-1", createTransport, }); expect(createTransport).toHaveBeenCalledWith({ host: "smtp-success.hyinda.test", port: 587, secure: false, auth: { user: "sender", pass: "test-only-password" }, }); expect(sendMail).toHaveBeenCalledWith({ from: "no-reply@hyinda.test", to: "employee@hyinda.test", subject: "待办提醒", text: "请处理待办。\n\n查看详情:https://oa.hyinda.test/workflows/tasks/1", headers: { "x-hyb-notification-type": "workflow_task", "x-request-id": "request-1", }, }); expect(result).toEqual({ skipped: false, sent: 1, failed: 1 }); }); it("counts send failures without logging the recipient email address", async () => { const error = new Error("smtp_rejected"); error.code = "EENVELOPE"; error.command = "RCPT TO"; error.responseCode = 550; error.response = "550 rejected private@hyinda.test"; error.rejected = ["private@hyinda.test"]; const createTransport = vi.fn(() => ({ sendMail: vi.fn(async () => { throw error; }) })); const logger = { error: vi.fn() }; const prisma = { employee: { findMany: vi.fn(async () => [ { id: "employee-3", email: "private@hyinda.test", name: "员工三" }, ]), }, }; const result = await sendNotificationEmail({ prisma, config: buildEmailConfig({ smtpHost: "smtp-failure.hyinda.test" }), logger, requestId: "request-2", recipientEmployeeIds: ["employee-3"], subject: "待办提醒", body: "请处理待办。", createTransport, }); expect(result).toEqual({ skipped: false, sent: 0, failed: 1 }); expect(logger.error).toHaveBeenCalledOnce(); const [context] = logger.error.mock.calls[0]; expect(context).toMatchObject({ errorCode: "email_delivery_failed", providerCode: "EENVELOPE", smtpCommand: "RCPT TO", smtpResponseCode: 550, requestId: "request-2", employeeId: "employee-3", }); expect(context).not.toHaveProperty("err"); expect(context).not.toHaveProperty("email"); expect(JSON.stringify(context)).not.toContain("private@hyinda.test"); }); });