300 lines
9.0 KiB
JavaScript
300 lines
9.0 KiB
JavaScript
// Mock AWS SDK before requiring modules
|
|
jest.mock("@aws-sdk/client-ses", () => ({
|
|
SESClient: jest.fn().mockImplementation(() => ({
|
|
send: jest.fn(),
|
|
})),
|
|
SendEmailCommand: jest.fn(),
|
|
}));
|
|
|
|
jest.mock("../../../../config/aws", () => ({
|
|
getAWSConfig: jest.fn(() => ({ region: "us-east-1" })),
|
|
}));
|
|
|
|
jest.mock("../../../../services/email/core/emailUtils", () => ({
|
|
htmlToPlainText: jest.fn((html) => html.replace(/<[^>]*>/g, "")),
|
|
}));
|
|
|
|
// Clear singleton between tests
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
// Reset the singleton instance
|
|
const EmailClient = require("../../../../services/email/core/EmailClient");
|
|
EmailClient.instance = null;
|
|
});
|
|
|
|
describe("EmailClient", () => {
|
|
const { SESClient, SendEmailCommand } = require("@aws-sdk/client-ses");
|
|
const { getAWSConfig } = require("../../../../config/aws");
|
|
|
|
describe("constructor", () => {
|
|
it("should create a new instance", () => {
|
|
const EmailClient = require("../../../../services/email/core/EmailClient");
|
|
EmailClient.instance = null;
|
|
const client = new EmailClient();
|
|
expect(client).toBeDefined();
|
|
expect(client.sesClient).toBeNull();
|
|
expect(client.initialized).toBe(false);
|
|
});
|
|
|
|
it("should return existing instance (singleton pattern)", () => {
|
|
const EmailClient = require("../../../../services/email/core/EmailClient");
|
|
EmailClient.instance = null;
|
|
const client1 = new EmailClient();
|
|
const client2 = new EmailClient();
|
|
expect(client1).toBe(client2);
|
|
});
|
|
});
|
|
|
|
describe("initialize", () => {
|
|
it("should initialize SES client with AWS config", async () => {
|
|
const EmailClient = require("../../../../services/email/core/EmailClient");
|
|
EmailClient.instance = null;
|
|
const client = new EmailClient();
|
|
|
|
await client.initialize();
|
|
|
|
expect(getAWSConfig).toHaveBeenCalled();
|
|
expect(SESClient).toHaveBeenCalledWith({ region: "us-east-1" });
|
|
expect(client.initialized).toBe(true);
|
|
});
|
|
|
|
it("should not re-initialize if already initialized", async () => {
|
|
const EmailClient = require("../../../../services/email/core/EmailClient");
|
|
EmailClient.instance = null;
|
|
const client = new EmailClient();
|
|
|
|
await client.initialize();
|
|
await client.initialize();
|
|
|
|
expect(SESClient).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("should wait for existing initialization if in progress", async () => {
|
|
const EmailClient = require("../../../../services/email/core/EmailClient");
|
|
EmailClient.instance = null;
|
|
const client = new EmailClient();
|
|
|
|
// Start two initializations concurrently
|
|
const [result1, result2] = await Promise.all([
|
|
client.initialize(),
|
|
client.initialize(),
|
|
]);
|
|
|
|
expect(SESClient).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("should throw error if AWS config fails", async () => {
|
|
getAWSConfig.mockImplementationOnce(() => {
|
|
throw new Error("AWS config error");
|
|
});
|
|
|
|
const EmailClient = require("../../../../services/email/core/EmailClient");
|
|
EmailClient.instance = null;
|
|
const client = new EmailClient();
|
|
|
|
await expect(client.initialize()).rejects.toThrow("AWS config error");
|
|
});
|
|
});
|
|
|
|
describe("sendEmail", () => {
|
|
const originalEnv = process.env;
|
|
|
|
beforeEach(() => {
|
|
process.env = {
|
|
...originalEnv,
|
|
EMAIL_ENABLED: "true",
|
|
SES_FROM_EMAIL: "noreply@email.com",
|
|
SES_FROM_NAME: "Village Share",
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = originalEnv;
|
|
});
|
|
|
|
it("should return early if EMAIL_ENABLED is not true", async () => {
|
|
process.env.EMAIL_ENABLED = "false";
|
|
|
|
const EmailClient = require("../../../../services/email/core/EmailClient");
|
|
EmailClient.instance = null;
|
|
const client = new EmailClient();
|
|
|
|
const result = await client.sendEmail(
|
|
"test@example.com",
|
|
"Test Subject",
|
|
"<p>Hello</p>",
|
|
);
|
|
|
|
expect(result).toEqual({ success: true, messageId: "disabled" });
|
|
});
|
|
|
|
it("should return early if EMAIL_ENABLED is not set", async () => {
|
|
delete process.env.EMAIL_ENABLED;
|
|
|
|
const EmailClient = require("../../../../services/email/core/EmailClient");
|
|
EmailClient.instance = null;
|
|
const client = new EmailClient();
|
|
|
|
const result = await client.sendEmail(
|
|
"test@example.com",
|
|
"Test Subject",
|
|
"<p>Hello</p>",
|
|
);
|
|
|
|
expect(result).toEqual({ success: true, messageId: "disabled" });
|
|
});
|
|
|
|
it("should send email with correct parameters", async () => {
|
|
const mockSend = jest.fn().mockResolvedValue({ MessageId: "msg-123" });
|
|
SESClient.mockImplementation(() => ({ send: mockSend }));
|
|
|
|
const EmailClient = require("../../../../services/email/core/EmailClient");
|
|
EmailClient.instance = null;
|
|
const client = new EmailClient();
|
|
|
|
const result = await client.sendEmail(
|
|
"test@example.com",
|
|
"Test Subject",
|
|
"<p>Hello World</p>",
|
|
);
|
|
|
|
expect(SendEmailCommand).toHaveBeenCalledWith({
|
|
Source: "Village Share <noreply@villageshare.app>",
|
|
Destination: {
|
|
ToAddresses: ["test@example.com"],
|
|
},
|
|
Message: {
|
|
Subject: {
|
|
Data: "Test Subject",
|
|
Charset: "UTF-8",
|
|
},
|
|
Body: {
|
|
Html: {
|
|
Data: "<p>Hello World</p>",
|
|
Charset: "UTF-8",
|
|
},
|
|
Text: {
|
|
Data: expect.any(String),
|
|
Charset: "UTF-8",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result).toEqual({ success: true, messageId: "msg-123" });
|
|
});
|
|
|
|
it("should send to multiple recipients", async () => {
|
|
const mockSend = jest.fn().mockResolvedValue({ MessageId: "msg-456" });
|
|
SESClient.mockImplementation(() => ({ send: mockSend }));
|
|
|
|
const EmailClient = require("../../../../services/email/core/EmailClient");
|
|
EmailClient.instance = null;
|
|
const client = new EmailClient();
|
|
|
|
await client.sendEmail(
|
|
["user1@example.com", "user2@example.com"],
|
|
"Test Subject",
|
|
"<p>Hello</p>",
|
|
);
|
|
|
|
expect(SendEmailCommand).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
Destination: {
|
|
ToAddresses: ["user1@example.com", "user2@example.com"],
|
|
},
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("should use provided text content", async () => {
|
|
const mockSend = jest.fn().mockResolvedValue({ MessageId: "msg-789" });
|
|
SESClient.mockImplementation(() => ({ send: mockSend }));
|
|
|
|
const EmailClient = require("../../../../services/email/core/EmailClient");
|
|
EmailClient.instance = null;
|
|
const client = new EmailClient();
|
|
|
|
await client.sendEmail(
|
|
"test@example.com",
|
|
"Test Subject",
|
|
"<p>Hello</p>",
|
|
"Custom plain text",
|
|
);
|
|
|
|
expect(SendEmailCommand).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
Message: expect.objectContaining({
|
|
Body: expect.objectContaining({
|
|
Text: {
|
|
Data: "Custom plain text",
|
|
Charset: "UTF-8",
|
|
},
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("should add reply-to address if configured", async () => {
|
|
process.env.SES_REPLY_TO_EMAIL = "support@email.com";
|
|
const mockSend = jest.fn().mockResolvedValue({ MessageId: "msg-000" });
|
|
SESClient.mockImplementation(() => ({ send: mockSend }));
|
|
|
|
const EmailClient = require("../../../../services/email/core/EmailClient");
|
|
EmailClient.instance = null;
|
|
const client = new EmailClient();
|
|
|
|
await client.sendEmail(
|
|
"test@example.com",
|
|
"Test Subject",
|
|
"<p>Hello</p>",
|
|
);
|
|
|
|
expect(SendEmailCommand).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
ReplyToAddresses: ["support@villageshare.app"],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("should return error if send fails", async () => {
|
|
const mockSend = jest
|
|
.fn()
|
|
.mockRejectedValue(new Error("SES send failed"));
|
|
SESClient.mockImplementation(() => ({ send: mockSend }));
|
|
|
|
const EmailClient = require("../../../../services/email/core/EmailClient");
|
|
EmailClient.instance = null;
|
|
const client = new EmailClient();
|
|
|
|
const result = await client.sendEmail(
|
|
"test@example.com",
|
|
"Test Subject",
|
|
"<p>Hello</p>",
|
|
);
|
|
|
|
expect(result).toEqual({ success: false, error: "SES send failed" });
|
|
});
|
|
|
|
it("should auto-initialize if not initialized", async () => {
|
|
const mockSend = jest.fn().mockResolvedValue({ MessageId: "msg-auto" });
|
|
SESClient.mockImplementation(() => ({ send: mockSend }));
|
|
|
|
const EmailClient = require("../../../../services/email/core/EmailClient");
|
|
EmailClient.instance = null;
|
|
const client = new EmailClient();
|
|
|
|
expect(client.initialized).toBe(false);
|
|
|
|
await client.sendEmail(
|
|
"test@example.com",
|
|
"Test Subject",
|
|
"<p>Hello</p>",
|
|
);
|
|
|
|
expect(client.initialized).toBe(true);
|
|
});
|
|
});
|
|
});
|