86211923db
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
225 lines
5.9 KiB
TypeScript
225 lines
5.9 KiB
TypeScript
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;
|
|
});
|
|
});
|
|
});
|