import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { enqueueEmail, flushEmails } from "@/lib/email"; import { resetEnvCache } from "@/lib/env"; import { db, resetDb } from "./support/db"; const sendMail = vi.fn(); // SMTP-Versand simulieren; der Datei-Transport (streamTransport) bleibt echt. vi.mock("nodemailer", async (importOriginal) => { const actual = await importOriginal(); const createTransport = (options: Record) => options.streamTransport ? actual.default.createTransport(options) : { sendMail }; return { ...actual, default: { ...actual.default, createTransport } }; }); function smtpError(message: string, responseCode?: number) { return Object.assign(new Error(message), { code: "EENVELOPE", responseCode }); } async function queueMail(to = "test@test.de") { await enqueueEmail(db, { dedupeKey: `test:${to}`, to, subject: "Buchung bestätigt", text: "Hallo", html: "

Hallo

" }); return db.emailMessage.findFirstOrThrow({ where: { toAddress: to } }); } beforeEach(async () => { await resetDb(); process.env.MAIL_TRANSPORT = "smtp"; process.env.SMTP_HOST = "smtp.example.org"; resetEnvCache(); sendMail.mockReset(); }); afterEach(() => { delete process.env.MAIL_TRANSPORT; delete process.env.SMTP_HOST; resetEnvCache(); vi.restoreAllMocks(); }); describe("Postausgang: Fehler beim SMTP-Versand", () => { test("dauerhafte Ablehnung (5xx) wird sofort FAILED und nie wiederholt", async () => { const mail = await queueMail(); sendMail.mockRejectedValue( smtpError("Can't send mail - all recipients were rejected: 556 5.1.10 : Recipient address rejected: Domain test.de does not accept mail (nullMX)", 556), ); vi.spyOn(console, "error").mockImplementation(() => {}); expect(await flushEmails(db)).toEqual({ sent: 0, failed: 1 }); const after = await db.emailMessage.findUniqueOrThrow({ where: { id: mail.id } }); expect(after.status).toBe("FAILED"); expect(after.attempts).toBe(1); expect(await flushEmails(db)).toEqual({ sent: 0, failed: 0 }); expect(sendMail).toHaveBeenCalledTimes(1); }); test("Log und gespeicherter Fehler enthalten keine E-Mail-Adresse", async () => { await queueMail(); sendMail.mockRejectedValue(smtpError("all recipients were rejected: 556 5.1.10 : Recipient address rejected", 556)); const log = vi.spyOn(console, "error").mockImplementation(() => {}); await flushEmails(db); const logged = log.mock.calls.flat().join(" "); expect(logged).toContain("556 5.1.10"); expect(logged).not.toContain("test@test.de"); expect((await db.emailMessage.findFirstOrThrow()).lastError).not.toContain("test@test.de"); }); test("vorübergehende Fehler (4xx, Verbindung) bleiben PENDING und werden wiederholt", async () => { const greylisted = await queueMail("a@example.org"); const offline = await queueMail("b@example.org"); sendMail .mockRejectedValueOnce(smtpError("421 4.7.0 Try again later", 421)) .mockRejectedValueOnce(Object.assign(new Error("connect ECONNREFUSED"), { code: "ECONNECTION" })); vi.spyOn(console, "error").mockImplementation(() => {}); expect(await flushEmails(db)).toEqual({ sent: 0, failed: 2 }); for (const id of [greylisted.id, offline.id]) { expect(await db.emailMessage.findUniqueOrThrow({ where: { id } })).toMatchObject({ status: "PENDING", attempts: 1 }); } }); });