Files
rxminder/services/__tests__/mailgun.service.test.ts
William Valentin a7e5df4b2e test: update mocks to use unified config structure
- Update mailgun service test mock to use unifiedConfig.app.baseUrl
- Update database service test mock to use unifiedConfig.app.baseUrl
- Ensures test mocks reflect actual unified configuration structure
- Maintains test compatibility after config system consolidation
2025-09-08 20:40:29 -07:00

411 lines
12 KiB
TypeScript

// 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<typeof fetch>;
let mockFormData: jest.MockedFunction<any>;
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<typeof fetch>;
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: '<p>Test HTML</p>',
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: '<p>Test HTML</p>',
};
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: '<p>Test HTML</p>',
};
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: '<p>Test HTML</p>',
text: 'Test Text',
};
await service.sendEmail('test@example.com', template);
expect(mockFormDataInstance.append).toHaveBeenCalledWith(
'from',
'Test App <test@example.com>'
);
expect(mockFormDataInstance.append).toHaveBeenCalledWith(
'to',
'test@example.com'
);
expect(mockFormDataInstance.append).toHaveBeenCalledWith(
'subject',
'Test Subject'
);
expect(mockFormDataInstance.append).toHaveBeenCalledWith(
'html',
'<p>Test HTML</p>'
);
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: '<p>Test HTML</p>',
};
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: '',
});
});
});
});