- 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
411 lines
12 KiB
TypeScript
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: '',
|
|
});
|
|
});
|
|
});
|
|
});
|