// Mock fs before requiring modules jest.mock('fs', () => ({ promises: { readFile: jest.fn(), readdir: jest.fn(), }, // Include sync methods needed by logger existsSync: jest.fn().mockReturnValue(true), mkdirSync: jest.fn(), statSync: jest.fn().mockReturnValue({ isDirectory: () => true }), readdirSync: jest.fn().mockReturnValue([]), unlinkSync: jest.fn(), })); // Mock the logger to avoid file system issues in tests jest.mock('../../../../utils/logger', () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn(), })); // Clear singleton between tests beforeEach(() => { jest.clearAllMocks(); const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; }); describe('TemplateManager', () => { const fs = require('fs').promises; // Helper to set up common mocks const setupMocks = (templateFiles = []) => { fs.readdir.mockResolvedValue(templateFiles.map((t) => `${t}.html`)); fs.readFile.mockResolvedValue('{{content}}'); }; describe('constructor', () => { it('should create a new instance', () => { const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); expect(manager).toBeDefined(); expect(manager.templates).toBeInstanceOf(Map); expect(manager.templateNames).toBeInstanceOf(Set); expect(manager.initialized).toBe(false); }); it('should return existing instance (singleton pattern)', () => { const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager1 = new TemplateManager(); const manager2 = new TemplateManager(); expect(manager1).toBe(manager2); }); }); describe('discoverTemplates', () => { it('should discover all HTML templates in directory', async () => { const templateFiles = [ 'emailVerificationToUser.html', 'passwordResetToUser.html', 'rentalConfirmationToUser.html', 'README.md', // Should be ignored ]; fs.readdir.mockResolvedValue(templateFiles); const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); await manager.discoverTemplates(); expect(manager.templateNames.size).toBe(3); // Only .html files expect(manager.templateNames.has('emailVerificationToUser')).toBe(true); expect(manager.templateNames.has('passwordResetToUser')).toBe(true); expect(manager.templateNames.has('rentalConfirmationToUser')).toBe(true); expect(manager.templateNames.has('README')).toBe(false); }); it('should throw error if directory read fails', async () => { fs.readdir.mockRejectedValue(new Error('Directory not found')); const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); await expect(manager.discoverTemplates()).rejects.toThrow('Directory not found'); }); }); describe('loadTemplate', () => { it('should load template from disk on first access', async () => { fs.readFile.mockResolvedValue('Template Content'); const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); const content = await manager.loadTemplate('testTemplate'); expect(content).toBe('Template Content'); expect(fs.readFile).toHaveBeenCalledTimes(1); }); it('should return cached template on subsequent access', async () => { fs.readFile.mockResolvedValue('Template Content'); const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); await manager.loadTemplate('testTemplate'); await manager.loadTemplate('testTemplate'); await manager.loadTemplate('testTemplate'); expect(fs.readFile).toHaveBeenCalledTimes(1); // Only read once }); it('should throw error if template file not found', async () => { fs.readFile.mockRejectedValue(new Error('ENOENT: no such file')); const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); await expect(manager.loadTemplate('nonExistent')).rejects.toThrow('ENOENT'); }); }); describe('initialize', () => { const criticalTemplates = [ 'emailVerificationToUser', 'passwordResetToUser', 'passwordChangedToUser', 'personalInfoChangedToUser', ]; it('should discover templates and preload critical ones', async () => { setupMocks(criticalTemplates); const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); await manager.initialize(); expect(manager.initialized).toBe(true); expect(fs.readdir).toHaveBeenCalledTimes(1); // Should have loaded all 4 critical templates expect(fs.readFile).toHaveBeenCalledTimes(4); }); it('should not re-initialize if already initialized', async () => { setupMocks(criticalTemplates); const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); await manager.initialize(); const readCallCount = fs.readFile.mock.calls.length; await manager.initialize(); expect(fs.readFile.mock.calls.length).toBe(readCallCount); }); it('should wait for existing initialization if in progress', async () => { setupMocks(criticalTemplates); const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); // Start two initializations concurrently await Promise.all([manager.initialize(), manager.initialize()]); // Should only discover once expect(fs.readdir).toHaveBeenCalledTimes(1); }); it('should throw error if critical templates are not found', async () => { // Only discover non-critical templates setupMocks(['rentalConfirmationToUser', 'feedbackToUser']); const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); await expect(manager.initialize()).rejects.toThrow('Critical email templates not found'); }); it('should succeed if critical templates exist even if non-critical are missing', async () => { // Discover critical templates plus some others setupMocks([...criticalTemplates, 'rentalConfirmationToUser']); const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); await expect(manager.initialize()).resolves.not.toThrow(); expect(manager.initialized).toBe(true); expect(manager.templateNames.size).toBe(5); }); }); describe('renderTemplate', () => { const criticalTemplates = [ 'emailVerificationToUser', 'passwordResetToUser', 'passwordChangedToUser', 'personalInfoChangedToUser', ]; beforeEach(() => { fs.readdir.mockResolvedValue([...criticalTemplates, 'testTemplate'].map((t) => `${t}.html`)); fs.readFile.mockResolvedValue('Hello {{name}}, your email is {{email}}'); }); it('should render template with variables', async () => { const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); await manager.initialize(); const result = await manager.renderTemplate('testTemplate', { name: 'John', email: 'john@example.com', }); expect(result).toBe('Hello John, your email is john@example.com'); }); it('should lazy load non-critical templates on first render', async () => { const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); await manager.initialize(); // At this point, only critical templates are loaded const initialLoadCount = fs.readFile.mock.calls.length; expect(initialLoadCount).toBe(4); // Only critical templates // Render a non-critical template await manager.renderTemplate('testTemplate', { name: 'John', email: 'test@example.com' }); // Should have loaded one more template expect(fs.readFile.mock.calls.length).toBe(5); }); it('should replace all occurrences of a variable', async () => { fs.readFile.mockResolvedValue('{{name}} {{name}} {{name}}'); const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); await manager.initialize(); const result = await manager.renderTemplate('testTemplate', { name: 'John', }); expect(result).toBe('John John John'); }); it('should not replace unspecified variables', async () => { fs.readFile.mockResolvedValue('Hello {{name}}, {{missing}}'); const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); await manager.initialize(); const result = await manager.renderTemplate('testTemplate', { name: 'John', }); expect(result).toBe('Hello John, {{missing}}'); }); it('should use fallback template when template not discovered', async () => { const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); await manager.initialize(); const result = await manager.renderTemplate('nonExistentTemplate', { title: 'Test Title', message: 'Test Message', recipientName: 'John', }); // Should return fallback template content expect(result).toContain('Test Title'); expect(result).toContain('Test Message'); expect(result).toContain('John'); expect(result).toContain('Village Share'); }); it('should auto-initialize if not initialized', async () => { const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); expect(manager.initialized).toBe(false); await manager.renderTemplate('testTemplate', {}); expect(manager.initialized).toBe(true); }); it('should handle empty variables object', async () => { fs.readFile.mockResolvedValue('No variables'); const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); await manager.initialize(); const result = await manager.renderTemplate('testTemplate', {}); expect(result).toBe('No variables'); }); it('should handle null or undefined variable values', async () => { fs.readFile.mockResolvedValue('Hello {{name}}'); const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); await manager.initialize(); const result = await manager.renderTemplate('testTemplate', { name: null, }); expect(result).toBe('Hello '); }); it('should escape HTML in variables to prevent XSS', async () => { fs.readFile.mockResolvedValue('Hello {{name}}'); const TemplateManager = require('../../../../services/email/core/TemplateManager'); TemplateManager.instance = null; const manager = new TemplateManager(); await manager.initialize(); const result = await manager.renderTemplate('testTemplate', { name: '', }); expect(result).toContain('<script>'); expect(result).toContain('</script>'); expect(result).not.toContain('