Backend Complete: Interface Refactoring & Service Container Enhancements
Repository Interface Optimization: - Created IBaseRepository.ts and IPaginatedRepository.ts - Refactored all 7 repository interfaces to extend base interfaces - Eliminated ~200 lines of redundant code (70% reduction) - Improved type safety and maintainability Dependency Injection Improvements: - Added EmailService and GameTokenService to DIContainer - Updated CreateUserCommandHandler constructor for DI - Updated RequestPasswordResetCommandHandler constructor for DI - Enhanced testability and service consistency Environment Configuration: - Created comprehensive .env.example with 40+ variables - Organized into 12 logical sections (Database, Security, Email, etc.) - Added security guidelines and best practices - Documented all backend environment requirements Documentation: - Added comprehensive codebase review - Created refactoring summary report - Added frontend implementation guide Impact: Improved code quality, reduced maintenance overhead, enhanced developer experience
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
import { EmailService, EmailOptions } from '../../../src/Application/Services/EmailService';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Mock nodemailer
|
||||
jest.mock('nodemailer');
|
||||
jest.mock('fs');
|
||||
|
||||
// Mock logger
|
||||
jest.mock('../../../src/Application/Services/Logger', () => ({
|
||||
logError: jest.fn(),
|
||||
logAuth: jest.fn(),
|
||||
logStartup: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('EmailService', () => {
|
||||
let emailService: EmailService;
|
||||
let mockTransporter: jest.Mocked<nodemailer.Transporter>;
|
||||
let mockCreateTransporter: jest.MockedFunction<typeof nodemailer.createTransport>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Mock nodemailer.createTransporter
|
||||
mockTransporter = {
|
||||
sendMail: jest.fn(),
|
||||
} as any;
|
||||
|
||||
mockCreateTransporter = nodemailer.createTransport as jest.MockedFunction<typeof nodemailer.createTransport>;
|
||||
mockCreateTransporter.mockReturnValue(mockTransporter);
|
||||
|
||||
// Mock fs
|
||||
(fs.readFileSync as jest.Mock).mockImplementation((filePath: string) => {
|
||||
if (filePath.includes('html')) {
|
||||
return 'HTML template: {{name}}';
|
||||
}
|
||||
return 'Text template: {{name}}';
|
||||
});
|
||||
|
||||
(fs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
|
||||
emailService = new EmailService();
|
||||
});
|
||||
|
||||
describe('sendEmail', () => {
|
||||
it('should send email successfully', async () => {
|
||||
// Arrange
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: 'Test Subject',
|
||||
html: '<p>Test HTML</p>',
|
||||
text: 'Test Text',
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' });
|
||||
|
||||
// Act
|
||||
const result = await emailService.sendEmail(emailOptions);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockTransporter.sendMail).toHaveBeenCalledWith({
|
||||
from: process.env.EMAIL_FROM || 'noreply@serpentrace.com',
|
||||
to: emailOptions.to,
|
||||
subject: emailOptions.subject,
|
||||
html: emailOptions.html,
|
||||
text: emailOptions.text,
|
||||
});
|
||||
});
|
||||
|
||||
it('should send email with template', async () => {
|
||||
// Arrange
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: 'Test Subject',
|
||||
template: 'verification',
|
||||
templateData: { name: 'John', token: 'abc123' },
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' });
|
||||
|
||||
// Act
|
||||
const result = await emailService.sendEmail(emailOptions);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockTransporter.sendMail).toHaveBeenCalledWith({
|
||||
from: process.env.EMAIL_FROM || 'noreply@serpentrace.com',
|
||||
to: emailOptions.to,
|
||||
subject: emailOptions.subject,
|
||||
html: expect.stringContaining('John'),
|
||||
text: expect.stringContaining('John'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle email send failure', async () => {
|
||||
// Arrange
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: 'Test Subject',
|
||||
text: 'Test Text',
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockRejectedValue(new Error('SMTP Error'));
|
||||
|
||||
// Act
|
||||
const result = await emailService.sendEmail(emailOptions);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle missing template files', async () => {
|
||||
// Arrange
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: 'Test Subject',
|
||||
template: 'nonexistent',
|
||||
templateData: { name: 'John' },
|
||||
};
|
||||
|
||||
(fs.existsSync as jest.Mock).mockReturnValue(false);
|
||||
|
||||
// Act
|
||||
const result = await emailService.sendEmail(emailOptions);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle template processing errors', async () => {
|
||||
// Arrange
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: 'Test Subject',
|
||||
template: 'verification',
|
||||
templateData: { name: 'John' },
|
||||
};
|
||||
|
||||
(fs.readFileSync as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('File read error');
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await emailService.sendEmail(emailOptions);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should use fallback content when template data is missing', async () => {
|
||||
// Arrange
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: 'Test Subject',
|
||||
template: 'verification',
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' });
|
||||
|
||||
// Act
|
||||
const result = await emailService.sendEmail(emailOptions);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with environment variables', () => {
|
||||
// Arrange
|
||||
const originalEnv = process.env;
|
||||
process.env = {
|
||||
...originalEnv,
|
||||
EMAIL_HOST: 'test-smtp.com',
|
||||
EMAIL_PORT: '465',
|
||||
EMAIL_SECURE: 'true',
|
||||
EMAIL_USER: 'test@example.com',
|
||||
EMAIL_PASS: 'testpass',
|
||||
EMAIL_FROM: 'sender@example.com',
|
||||
};
|
||||
|
||||
// Act
|
||||
const service = new EmailService();
|
||||
|
||||
// Assert
|
||||
expect(mockCreateTransporter).toHaveBeenCalledWith({
|
||||
host: 'test-smtp.com',
|
||||
port: 465,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: 'test@example.com',
|
||||
pass: 'testpass',
|
||||
},
|
||||
});
|
||||
|
||||
// Restore environment
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it('should use default values when environment variables are missing', () => {
|
||||
// Arrange
|
||||
const originalEnv = process.env;
|
||||
process.env = {};
|
||||
|
||||
// Act
|
||||
const service = new EmailService();
|
||||
|
||||
// Assert
|
||||
expect(mockCreateTransporter).toHaveBeenCalledWith({
|
||||
host: 'smtp.gmail.com',
|
||||
port: 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: '',
|
||||
pass: '',
|
||||
},
|
||||
});
|
||||
|
||||
// Restore environment
|
||||
process.env = originalEnv;
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user