// 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); }); }); describe('sendRentalConfirmationEmails', () => { const { User } = require('../../../models'); beforeEach(async () => { mockSend.mockResolvedValue({ MessageId: 'test-message-id' }); await emailService.initialize(); }); it('should send emails to both owner and renter successfully', async () => { const mockOwner = { email: 'owner@example.com' }; const mockRenter = { email: 'renter@example.com' }; User.findByPk .mockResolvedValueOnce(mockOwner) // First call for owner .mockResolvedValueOnce(mockRenter); // Second call for renter const rental = { id: 1, ownerId: 10, renterId: 20, item: { name: 'Test Item' }, startDateTime: '2024-01-15T10:00:00Z', endDateTime: '2024-01-17T10:00:00Z' }; const results = await emailService.sendRentalConfirmationEmails(rental); expect(results.ownerEmailSent).toBe(true); expect(results.renterEmailSent).toBe(true); expect(mockSend).toHaveBeenCalledTimes(2); }); it('should send renter email even if owner email fails', async () => { const mockOwner = { email: 'owner@example.com' }; const mockRenter = { email: 'renter@example.com' }; User.findByPk .mockResolvedValueOnce(mockOwner) .mockResolvedValueOnce(mockRenter); // First call (owner) fails, second call (renter) succeeds mockSend .mockRejectedValueOnce(new Error('SES Error for owner')) .mockResolvedValueOnce({ MessageId: 'renter-message-id' }); const rental = { id: 1, ownerId: 10, renterId: 20, item: { name: 'Test Item' }, startDateTime: '2024-01-15T10:00:00Z', endDateTime: '2024-01-17T10:00:00Z' }; const results = await emailService.sendRentalConfirmationEmails(rental); expect(results.ownerEmailSent).toBe(false); expect(results.renterEmailSent).toBe(true); expect(mockSend).toHaveBeenCalledTimes(2); }); it('should send owner email even if renter email fails', async () => { const mockOwner = { email: 'owner@example.com' }; const mockRenter = { email: 'renter@example.com' }; User.findByPk .mockResolvedValueOnce(mockOwner) .mockResolvedValueOnce(mockRenter); // First call (owner) succeeds, second call (renter) fails mockSend .mockResolvedValueOnce({ MessageId: 'owner-message-id' }) .mockRejectedValueOnce(new Error('SES Error for renter')); const rental = { id: 1, ownerId: 10, renterId: 20, item: { name: 'Test Item' }, startDateTime: '2024-01-15T10:00:00Z', endDateTime: '2024-01-17T10:00:00Z' }; const results = await emailService.sendRentalConfirmationEmails(rental); expect(results.ownerEmailSent).toBe(true); expect(results.renterEmailSent).toBe(false); expect(mockSend).toHaveBeenCalledTimes(2); }); it('should handle both emails failing gracefully', async () => { const mockOwner = { email: 'owner@example.com' }; const mockRenter = { email: 'renter@example.com' }; User.findByPk .mockResolvedValueOnce(mockOwner) .mockResolvedValueOnce(mockRenter); // Both calls fail mockSend .mockRejectedValueOnce(new Error('SES Error for owner')) .mockRejectedValueOnce(new Error('SES Error for renter')); const rental = { id: 1, ownerId: 10, renterId: 20, item: { name: 'Test Item' }, startDateTime: '2024-01-15T10:00:00Z', endDateTime: '2024-01-17T10:00:00Z' }; const results = await emailService.sendRentalConfirmationEmails(rental); expect(results.ownerEmailSent).toBe(false); expect(results.renterEmailSent).toBe(false); expect(mockSend).toHaveBeenCalledTimes(2); }); it('should handle missing owner email', async () => { const mockOwner = { email: null }; const mockRenter = { email: 'renter@example.com' }; User.findByPk .mockResolvedValueOnce(mockOwner) .mockResolvedValueOnce(mockRenter); const rental = { id: 1, ownerId: 10, renterId: 20, item: { name: 'Test Item' }, startDateTime: '2024-01-15T10:00:00Z', endDateTime: '2024-01-17T10:00:00Z' }; const results = await emailService.sendRentalConfirmationEmails(rental); expect(results.ownerEmailSent).toBe(false); expect(results.renterEmailSent).toBe(true); expect(mockSend).toHaveBeenCalledTimes(1); }); it('should handle missing renter email', async () => { const mockOwner = { email: 'owner@example.com' }; const mockRenter = { email: null }; User.findByPk .mockResolvedValueOnce(mockOwner) .mockResolvedValueOnce(mockRenter); const rental = { id: 1, ownerId: 10, renterId: 20, item: { name: 'Test Item' }, startDateTime: '2024-01-15T10:00:00Z', endDateTime: '2024-01-17T10:00:00Z' }; const results = await emailService.sendRentalConfirmationEmails(rental); expect(results.ownerEmailSent).toBe(true); expect(results.renterEmailSent).toBe(false); expect(mockSend).toHaveBeenCalledTimes(1); }); }); describe('sendRentalRequestEmail', () => { const { User } = require('../../../models'); beforeEach(async () => { mockSend.mockResolvedValue({ MessageId: 'test-message-id' }); await emailService.initialize(); }); it('should send rental request email to owner', async () => { const mockOwner = { email: 'owner@example.com', firstName: 'John', lastName: 'Smith' }; const mockRenter = { firstName: 'Jane', lastName: 'Doe' }; User.findByPk .mockResolvedValueOnce(mockOwner) // First call for owner .mockResolvedValueOnce(mockRenter); // Second call for renter const rental = { id: 1, ownerId: 10, renterId: 20, startDateTime: new Date('2024-12-01T10:00:00Z'), endDateTime: new Date('2024-12-03T10:00:00Z'), totalAmount: 150.00, payoutAmount: 135.00, deliveryMethod: 'pickup', item: { name: 'Power Drill' } }; const result = await emailService.sendRentalRequestEmail(rental); expect(result.success).toBe(true); expect(result.messageId).toBe('test-message-id'); expect(mockSend).toHaveBeenCalledWith(expect.any(SendEmailCommand)); }); it('should handle missing owner gracefully', async () => { User.findByPk.mockResolvedValue(null); const rental = { id: 1, ownerId: 1, renterId: 2, item: { name: 'Power Drill' } }; const result = await emailService.sendRentalRequestEmail(rental); expect(result.success).toBe(false); expect(result.error).toBe('User not found'); }); it('should handle missing renter gracefully', async () => { const mockOwner = { email: 'owner@example.com', firstName: 'John' }; User.findByPk .mockResolvedValueOnce(mockOwner) .mockResolvedValueOnce(null); // Renter not found const rental = { id: 1, ownerId: 1, renterId: 2, item: { name: 'Power Drill' } }; const result = await emailService.sendRentalRequestEmail(rental); expect(result.success).toBe(false); expect(result.error).toBe('User not found'); }); it('should handle free rentals (amount = 0)', async () => { const mockOwner = { email: 'owner@example.com', firstName: 'John' }; const mockRenter = { firstName: 'Jane', lastName: 'Doe' }; User.findByPk .mockResolvedValueOnce(mockOwner) .mockResolvedValueOnce(mockRenter); const rental = { id: 1, ownerId: 10, renterId: 20, startDateTime: new Date('2024-12-01T10:00:00Z'), endDateTime: new Date('2024-12-03T10:00:00Z'), totalAmount: 0, payoutAmount: 0, deliveryMethod: 'pickup', item: { name: 'Free Item' } }; const result = await emailService.sendRentalRequestEmail(rental); expect(result.success).toBe(true); }); it('should generate correct approval URL', async () => { const mockOwner = { email: 'owner@example.com', firstName: 'John' }; const mockRenter = { firstName: 'Jane', lastName: 'Doe' }; User.findByPk .mockResolvedValueOnce(mockOwner) .mockResolvedValueOnce(mockRenter); process.env.FRONTEND_URL = 'https://rentall.com'; const rental = { id: 123, ownerId: 10, renterId: 20, startDateTime: new Date('2024-12-01T10:00:00Z'), endDateTime: new Date('2024-12-03T10:00:00Z'), totalAmount: 100, payoutAmount: 90, deliveryMethod: 'pickup', item: { name: 'Test Item' } }; const result = await emailService.sendRentalRequestEmail(rental); expect(result.success).toBe(true); // The URL should be constructed correctly // We can't directly test the content, but we know it was called expect(mockSend).toHaveBeenCalled(); }); }); });