// 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@villageshare.app', 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', '
Hello
' ); 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', 'Hello
' ); 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', 'Hello World
' ); expect(SendEmailCommand).toHaveBeenCalledWith({ Source: 'Village ShareHello World
', 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', 'Hello
' ); 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', 'Hello
', '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@villageshare.app'; 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', 'Hello
' ); 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', 'Hello
' ); 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', 'Hello
' ); expect(client.initialized).toBe(true); }); }); });