// Mock dependencies BEFORE requiring modules jest.mock('@aws-sdk/client-ses'); jest.mock('../../../config/aws', () => ({ getAWSConfig: jest.fn(() => ({ region: 'us-east-1', credentials: { accessKeyId: 'test-key', secretAccessKey: 'test-secret' } })) })); jest.mock('../../../models', () => ({ User: { findByPk: jest.fn() } })); const emailService = require('../../../services/emailService'); const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses'); const { getAWSConfig } = require('../../../config/aws'); describe('EmailService', () => { let mockSESClient; let mockSend; beforeEach(() => { mockSend = jest.fn(); mockSESClient = { send: mockSend }; SESClient.mockImplementation(() => mockSESClient); // Reset environment variables process.env.EMAIL_ENABLED = 'true'; process.env.AWS_REGION = 'us-east-1'; process.env.AWS_ACCESS_KEY_ID = 'test-key'; process.env.AWS_SECRET_ACCESS_KEY = 'test-secret'; process.env.SES_FROM_EMAIL = 'test@example.com'; process.env.SES_REPLY_TO_EMAIL = 'reply@example.com'; // Reset the service instance emailService.initialized = false; emailService.sesClient = null; emailService.templates.clear(); }); afterEach(() => { jest.clearAllMocks(); }); describe('initialization', () => { it('should initialize SES client using AWS config', async () => { await emailService.initialize(); expect(getAWSConfig).toHaveBeenCalled(); expect(SESClient).toHaveBeenCalledWith({ region: 'us-east-1', credentials: { accessKeyId: 'test-key', secretAccessKey: 'test-secret' } }); expect(emailService.initialized).toBe(true); }); it('should handle initialization errors', async () => { SESClient.mockImplementationOnce(() => { throw new Error('AWS credentials not found'); }); // Reset initialized state emailService.initialized = false; await expect(emailService.initialize()).rejects.toThrow('AWS credentials not found'); }); }); describe('sendEmail', () => { beforeEach(async () => { mockSend.mockResolvedValue({ MessageId: 'test-message-id' }); await emailService.initialize(); }); it('should send email successfully', async () => { const result = await emailService.sendEmail( 'recipient@example.com', 'Test Subject', '
Content
'); expect(result.success).toBe(true); expect(mockSend).toHaveBeenCalledWith(expect.any(SendEmailCommand)); }); it('should handle array of email addresses', async () => { const result = await emailService.sendEmail( ['first@example.com', 'second@example.com'], 'Subject', 'Content
' ); expect(result.success).toBe(true); expect(mockSend).toHaveBeenCalledWith(expect.any(SendEmailCommand)); }); it('should include reply-to address when configured', async () => { const result = await emailService.sendEmail('test@example.com', 'Subject', 'Content
'); expect(result.success).toBe(true); expect(mockSend).toHaveBeenCalledWith(expect.any(SendEmailCommand)); }); it('should handle SES errors', async () => { mockSend.mockRejectedValue(new Error('SES Error')); const result = await emailService.sendEmail('test@example.com', 'Subject', 'Content
'); expect(result.success).toBe(false); expect(result.error).toBe('SES Error'); }); it('should skip sending when email is disabled', async () => { process.env.EMAIL_ENABLED = 'false'; const result = await emailService.sendEmail('test@example.com', 'Subject', 'Content
'); expect(result.success).toBe(true); expect(result.messageId).toBe('disabled'); expect(mockSend).not.toHaveBeenCalled(); }); }); describe('template rendering', () => { it('should render template with variables', () => { const template = 'Your order {{orderId}} is ready.
'; emailService.templates.set('test', template); const rendered = emailService.renderTemplate('test', { name: 'John Doe', orderId: '12345' }); expect(rendered).toBe('Your order 12345 is ready.
'); }); it('should handle missing variables by replacing with empty string', () => { const template = 'Your order {{orderId}} is ready.
'; emailService.templates.set('test', template); const rendered = emailService.renderTemplate('test', { name: 'John Doe', orderId: '' // Explicitly provide empty string }); expect(rendered).toContain('Hello John Doe'); expect(rendered).toContain('Your order'); }); it('should use fallback template when template not found', () => { const rendered = emailService.renderTemplate('nonexistent', { title: 'Test Title', content: 'Test Content', message: 'Test message' }); expect(rendered).toContain('Test Title'); expect(rendered).toContain('Test message'); expect(rendered).toContain('RentAll'); }); }); describe('notification-specific senders', () => { beforeEach(async () => { mockSend.mockResolvedValue({ MessageId: 'test-message-id' }); await emailService.initialize(); }); it('should send condition check reminder', async () => { const notification = { title: 'Condition Check Required', message: 'Please take photos of the item', metadata: { deadline: '2024-01-15' } }; const rental = { item: { name: 'Test Item' } }; const result = await emailService.sendConditionCheckReminder( 'test@example.com', notification, rental ); expect(result.success).toBe(true); expect(mockSend).toHaveBeenCalled(); }); it('should send rental confirmation', async () => { const notification = { title: 'Rental Confirmed', message: 'Your rental has been confirmed' }; const rental = { item: { name: 'Test Item' }, startDateTime: '2024-01-15T10:00:00Z', endDateTime: '2024-01-17T10:00:00Z' }; const result = await emailService.sendRentalConfirmation( 'test@example.com', notification, rental ); expect(result.success).toBe(true); expect(mockSend).toHaveBeenCalled(); }); }); describe('error handling', () => { beforeEach(async () => { await emailService.initialize(); }); it('should handle missing rental data gracefully', async () => { mockSend.mockResolvedValue({ MessageId: 'test-message-id' }); const notification = { title: 'Test', message: 'Test message', metadata: {} }; const result = await emailService.sendConditionCheckReminder( 'test@example.com', notification, null ); expect(result.success).toBe(true); }); }); });