// Mock the mailgun config before any imports const mockGetMailgunConfig = jest.fn().mockReturnValue({ apiKey: 'test-api-key', domain: 'test.mailgun.org', baseUrl: 'https://api.mailgun.net/v3', fromName: 'Test App', fromEmail: 'test@example.com', }); jest.mock('../mailgun.config', () => ({ getMailgunConfig: mockGetMailgunConfig, })); // Mock the app config jest.mock('../../config/unified.config', () => ({ unifiedConfig: { app: { baseUrl: 'http://localhost:3000', }, }, })); // Mock global fetch and related APIs global.fetch = jest.fn(); const MockFormData = jest.fn().mockImplementation(() => ({ append: jest.fn(), })); (global as any).FormData = MockFormData; global.btoa = jest .fn() .mockImplementation(str => Buffer.from(str).toString('base64')); // Import the service after mocks are set up import { MailgunService } from '../mailgun.service'; describe('MailgunService', () => { let mockFetch: jest.MockedFunction; let mockFormData: jest.MockedFunction; const mockConfig = { apiKey: 'test-api-key', domain: 'test.mailgun.org', baseUrl: 'https://api.mailgun.net/v3', fromName: 'Test App', fromEmail: 'test@example.com', }; beforeEach(() => { jest.clearAllMocks(); console.warn = jest.fn(); console.error = jest.fn(); mockFetch = fetch as jest.MockedFunction; mockFormData = MockFormData; }); describe('constructor', () => { test('should initialize with development mode warning when not configured', () => { const unconfiguredConfig = { apiKey: undefined, domain: undefined, baseUrl: 'https://api.mailgun.net/v3', fromName: 'Test App', fromEmail: undefined, }; mockGetMailgunConfig.mockReturnValue(unconfiguredConfig); new MailgunService(); expect(console.warn).toHaveBeenCalledWith( '📧 Mailgun Service: Running in development mode (emails will be logged only)' ); expect(console.warn).toHaveBeenCalledWith( '💡 To enable real emails, configure Mailgun credentials in .env.local' ); }); test('should initialize with production mode message when configured', () => { mockGetMailgunConfig.mockReturnValue(mockConfig); new MailgunService(); expect(console.warn).toHaveBeenCalledWith( '📧 Mailgun Service: Configured for production with domain:', 'test.mailgun.org' ); }); }); describe('sendEmail', () => { let service: MailgunService; beforeEach(() => { service = new MailgunService(); }); test('should send email successfully', async () => { const mockResponse = { ok: true, json: jest.fn().mockResolvedValue({ id: 'test-message-id' }), }; mockFetch.mockResolvedValue(mockResponse as any); const template = { subject: 'Test Subject', html: '

Test HTML

', text: 'Test Text', }; const result = await service.sendEmail('test@example.com', template); expect(result).toBe(true); expect(mockFetch).toHaveBeenCalledWith( 'https://api.mailgun.net/v3/test.mailgun.org/messages', expect.objectContaining({ method: 'POST', headers: { Authorization: 'Basic YXBpOnRlc3QtYXBpLWtleQ==', // base64 of "api:test-api-key" }, body: expect.objectContaining({ append: expect.any(Function), }), }) ); expect(console.warn).toHaveBeenCalledWith( '📧 Email sent successfully via Mailgun:', { to: 'test@example.com', subject: 'Test Subject', messageId: 'test-message-id', } ); }); test('should handle email sending failure', async () => { const mockResponse = { ok: false, status: 400, text: jest.fn().mockResolvedValue('Bad Request'), }; mockFetch.mockResolvedValue(mockResponse as any); const template = { subject: 'Test Subject', html: '

Test HTML

', }; const result = await service.sendEmail('test@example.com', template); expect(result).toBe(false); expect(console.error).toHaveBeenCalledWith( 'Email sending failed:', expect.any(Error) ); }); test('should handle network errors', async () => { mockFetch.mockRejectedValue(new Error('Network error')); const template = { subject: 'Test Subject', html: '

Test HTML

', }; const result = await service.sendEmail('test@example.com', template); expect(result).toBe(false); expect(console.error).toHaveBeenCalledWith( 'Email sending failed:', expect.any(Error) ); }); test('should properly format FormData', async () => { const mockFormDataInstance = { append: jest.fn(), }; mockFormData.mockReturnValue(mockFormDataInstance as any); const mockResponse = { ok: true, json: jest.fn().mockResolvedValue({ id: 'test-id' }), }; mockFetch.mockResolvedValue(mockResponse as any); const template = { subject: 'Test Subject', html: '

Test HTML

', text: 'Test Text', }; await service.sendEmail('test@example.com', template); expect(mockFormDataInstance.append).toHaveBeenCalledWith( 'from', 'Test App ' ); expect(mockFormDataInstance.append).toHaveBeenCalledWith( 'to', 'test@example.com' ); expect(mockFormDataInstance.append).toHaveBeenCalledWith( 'subject', 'Test Subject' ); expect(mockFormDataInstance.append).toHaveBeenCalledWith( 'html', '

Test HTML

' ); expect(mockFormDataInstance.append).toHaveBeenCalledWith( 'text', 'Test Text' ); }); test('should work without text field in template', async () => { const mockFormDataInstance = { append: jest.fn(), }; mockFormData.mockReturnValue(mockFormDataInstance as any); const mockResponse = { ok: true, json: jest.fn().mockResolvedValue({ id: 'test-id' }), }; mockFetch.mockResolvedValue(mockResponse as any); const template = { subject: 'Test Subject', html: '

Test HTML

', }; await service.sendEmail('test@example.com', template); expect(mockFormDataInstance.append).not.toHaveBeenCalledWith( 'text', expect.anything() ); }); }); describe('sendVerificationEmail', () => { let service: MailgunService; beforeEach(() => { service = new MailgunService(); service.sendEmail = jest.fn().mockResolvedValue(true); }); test('should send verification email with correct URL and template', async () => { const result = await service.sendVerificationEmail( 'user@example.com', 'verification-token' ); expect(result).toBe(true); expect(service.sendEmail).toHaveBeenCalledWith( 'user@example.com', expect.objectContaining({ subject: 'Verify Your Email - Medication Reminder', html: expect.stringContaining( 'http://localhost:3000/verify-email?token=verification-token' ), text: expect.stringContaining( 'http://localhost:3000/verify-email?token=verification-token' ), }) ); }); test('should include proper HTML structure in verification email', async () => { await service.sendVerificationEmail('user@example.com', 'test-token'); const mockCall = (service.sendEmail as jest.Mock).mock.calls[0]; const template = mockCall[1]; expect(template.html).toContain('Verify Your Email Address'); expect(template.html).toContain('Verify Email Address'); expect(template.html).toContain('This link will expire in 24 hours'); expect(template.html).toContain('color: #4f46e5'); }); test('should include text version in verification email', async () => { await service.sendVerificationEmail('user@example.com', 'test-token'); const mockCall = (service.sendEmail as jest.Mock).mock.calls[0]; const template = mockCall[1]; expect(template.text).toContain( 'Verify Your Email - Medication Reminder' ); expect(template.text).toContain('This link will expire in 24 hours'); expect(template.text).not.toContain('<'); }); }); describe('sendPasswordResetEmail', () => { let service: MailgunService; beforeEach(() => { service = new MailgunService(); service.sendEmail = jest.fn().mockResolvedValue(true); }); test('should send password reset email with correct URL and template', async () => { const result = await service.sendPasswordResetEmail( 'user@example.com', 'reset-token' ); expect(result).toBe(true); expect(service.sendEmail).toHaveBeenCalledWith( 'user@example.com', expect.objectContaining({ subject: 'Reset Your Password - Medication Reminder', html: expect.stringContaining( 'http://localhost:3000/reset-password?token=reset-token' ), text: expect.stringContaining( 'http://localhost:3000/reset-password?token=reset-token' ), }) ); }); test('should include proper HTML structure in password reset email', async () => { await service.sendPasswordResetEmail('user@example.com', 'test-token'); const mockCall = (service.sendEmail as jest.Mock).mock.calls[0]; const template = mockCall[1]; expect(template.html).toContain('Reset Your Password'); expect(template.html).toContain('Reset Password'); expect(template.html).toContain('This link will expire in 1 hour'); expect(template.html).toContain("If you didn't request this"); }); test('should include text version in password reset email', async () => { await service.sendPasswordResetEmail('user@example.com', 'test-token'); const mockCall = (service.sendEmail as jest.Mock).mock.calls[0]; const template = mockCall[1]; expect(template.text).toContain( 'Reset Your Password - Medication Reminder' ); expect(template.text).toContain('This link will expire in 1 hour'); expect(template.text).toContain("If you didn't request this"); expect(template.text).not.toContain('<'); }); }); describe('getConfigurationStatus', () => { test('should return configured status when all fields are present', () => { const service = new MailgunService(); const status = service.getConfigurationStatus(); expect(status).toEqual({ configured: true, mode: 'production', domain: 'test.mailgun.org', fromEmail: 'test@example.com', }); }); test('should return unconfigured status when fields are missing', () => { const unconfiguredConfig = { apiKey: undefined, domain: undefined, baseUrl: 'https://api.mailgun.net/v3', fromName: 'Test App', fromEmail: undefined, }; mockGetMailgunConfig.mockReturnValue(unconfiguredConfig); const service = new MailgunService(); const status = service.getConfigurationStatus(); expect(status).toEqual({ configured: false, mode: 'development', domain: undefined, fromEmail: undefined, }); }); test('should return unconfigured status when fields are empty strings', () => { const emptyConfig = { apiKey: '', domain: '', baseUrl: '', fromName: '', fromEmail: '', }; mockGetMailgunConfig.mockReturnValue(emptyConfig); const service = new MailgunService(); const status = service.getConfigurationStatus(); expect(status).toEqual({ configured: false, mode: 'development', domain: '', fromEmail: '', }); }); }); });