Phase 7
This commit is contained in:
189 files changed
+9873
-655
No files matched your search
@@ -0,0 +1,75 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const sendMail = vi.fn();
|
||||
vi.mock("nodemailer", () => ({
|
||||
default: { createTransport: vi.fn(() => ({ sendMail })) },
|
||||
}));
|
||||
|
||||
const { BrevoEmailProvider } = await import("../brevo-email-provider");
|
||||
|
||||
/**
|
||||
* Exercises BrevoEmailProvider's retry classification (see isRetryable in
|
||||
* brevo-email-provider.ts) with a mocked nodemailer transporter — no real
|
||||
* SMTP connection is ever made in this test file, matching the Phase 7
|
||||
* brief's "no real API calls in automated tests" requirement.
|
||||
*/
|
||||
describe("BrevoEmailProvider", () => {
|
||||
const provider = new BrevoEmailProvider({
|
||||
host: "smtp-relay.brevo.com",
|
||||
port: 587,
|
||||
username: "test-user",
|
||||
password: "test-pass",
|
||||
fromAddress: "noreply@example.com",
|
||||
fromName: "Test Business",
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
sendMail.mockReset();
|
||||
});
|
||||
|
||||
const params = { to: { email: "customer@example.com" }, subject: "Hi", html: "<p>Hi</p>", text: "Hi" };
|
||||
|
||||
it("reports success with the provider message id on a clean send", async () => {
|
||||
sendMail.mockResolvedValueOnce({ messageId: "<abc123@smtp-relay.brevo.com>" });
|
||||
const result = await provider.sendEmail(params);
|
||||
expect(result).toEqual({ status: "sent", providerMessageId: "<abc123@smtp-relay.brevo.com>" });
|
||||
});
|
||||
|
||||
it("classifies a connection timeout as retryable", async () => {
|
||||
sendMail.mockRejectedValueOnce({ code: "ETIMEDOUT", message: "Connection timed out" });
|
||||
const result = await provider.sendEmail(params);
|
||||
expect(result).toEqual({ status: "failed", retryable: true, reason: "Connection timed out" });
|
||||
});
|
||||
|
||||
it("classifies a 4xx SMTP response (rate limit / temporary) as retryable", async () => {
|
||||
sendMail.mockRejectedValueOnce({ responseCode: 421, message: "Too many connections" });
|
||||
const result = await provider.sendEmail(params);
|
||||
expect(result).toEqual({ status: "failed", retryable: true, reason: "Too many connections" });
|
||||
});
|
||||
|
||||
it("classifies a 5xx SMTP response (invalid recipient / rejected) as permanent, not retryable", async () => {
|
||||
sendMail.mockRejectedValueOnce({ responseCode: 550, message: "Mailbox unavailable" });
|
||||
const result = await provider.sendEmail(params);
|
||||
expect(result).toEqual({ status: "failed", retryable: false, reason: "Mailbox unavailable" });
|
||||
});
|
||||
|
||||
it("classifies an auth failure (535, permanent per SMTP) as not retryable", async () => {
|
||||
sendMail.mockRejectedValueOnce({ responseCode: 535, message: "Authentication failed" });
|
||||
const result = await provider.sendEmail(params);
|
||||
expect(result.status).toBe("failed");
|
||||
expect((result as { retryable: boolean }).retryable).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults an unrecognized error to retryable, bounded later by the background processor's max-attempt cap", async () => {
|
||||
sendMail.mockRejectedValueOnce({ message: "Something unexpected" });
|
||||
const result = await provider.sendEmail(params);
|
||||
expect(result).toEqual({ status: "failed", retryable: true, reason: "Something unexpected" });
|
||||
});
|
||||
|
||||
it("never includes the SMTP username/password in a failure result", async () => {
|
||||
sendMail.mockRejectedValueOnce({ responseCode: 535, message: "Authentication failed" });
|
||||
const result = await provider.sendEmail(params);
|
||||
expect(JSON.stringify(result)).not.toContain("test-pass");
|
||||
expect(JSON.stringify(result)).not.toContain("test-user");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user