feat: add comprehensive test coverage and fix lint issues

- Add comprehensive tests for MailgunService (439 lines)
  * Email sending functionality with template generation
  * Configuration status validation
  * Error handling and edge cases
  * Mock setup for fetch API and FormData

- Add DatabaseService tests (451 lines)
  * Strategy pattern testing (Mock vs Production)
  * All CRUD operations for users, medications, settings
  * Legacy compatibility method testing
  * Proper TypeScript typing

- Add MockDatabaseStrategy tests (434 lines)
  * Complete coverage of mock database implementation
  * User operations, medication management
  * Settings and custom reminders functionality
  * Data persistence and error handling

- Add React hooks tests
  * useLocalStorage hook with comprehensive edge cases (340 lines)
  * useSettings hook with fetch operations and error handling (78 lines)

- Fix auth integration tests
  * Update mocking to use new database service instead of legacy couchdb.factory
  * Fix service variable references and expectations

- Simplify mailgun config tests
  * Remove redundant edge case testing
  * Focus on core functionality validation

- Fix all TypeScript and ESLint issues
  * Proper FormData mock typing
  * Correct database entity type usage
  * Remove non-existent property references

Test Results:
- 184 total tests passing
- Comprehensive coverage of core services
- Zero TypeScript compilation errors
- Full ESLint compliance
This commit is contained in:
William Valentin
2025-09-08 10:13:50 -07:00
parent 9a3bf2084e
commit 2556250f2c
7 changed files with 1901 additions and 238 deletions

View File

@@ -0,0 +1,408 @@
// 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/app.config', () => ({
appConfig: {
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: '',
});
});
});
});