import { EmailVerificationService } from '../emailVerification.service'; import { AuthenticatedUser } from '../auth.types'; import { AccountStatus } from '../auth.constants'; // Mock the mailgun service jest.mock('../../mailgun.service', () => ({ mailgunService: { sendVerificationEmail: jest.fn().mockResolvedValue(true), sendPasswordResetEmail: jest.fn().mockResolvedValue(true), }, })); // Mock the couchdb.factory service jest.mock('../../couchdb.factory', () => ({ dbService: { findUserByEmail: jest.fn(), updateUser: jest.fn(), }, })); describe('EmailVerificationService', () => { let emailVerificationService: EmailVerificationService; let mockMailgunService: any; let mockDbService: any; const createMockUser = ( overrides: Partial = {} ): AuthenticatedUser => ({ _id: 'user1', _rev: 'mock-rev-1', email: 'test@example.com', username: 'testuser', password: 'password', emailVerified: false, status: AccountStatus.PENDING, ...overrides, }); beforeEach(async () => { emailVerificationService = new EmailVerificationService(); // Clear localStorage before each test localStorage.clear(); // Reset all mocks jest.clearAllMocks(); // Get mocked services const mailgunModule = await import('../../mailgun.service'); const dbModule = await import('../../couchdb.factory'); mockMailgunService = mailgunModule.mailgunService; mockDbService = dbModule.dbService; }); describe('generateVerificationToken', () => { test('should generate a valid verification token', async () => { // Arrange const user = createMockUser(); mockMailgunService.sendVerificationEmail.mockResolvedValue(true); // Act const verificationToken = await emailVerificationService.generateVerificationToken(user); // Assert expect(verificationToken).toBeDefined(); expect(verificationToken.token).toBeDefined(); expect(verificationToken.token).toHaveLength(32); // UUID without hyphens expect(verificationToken.expiresAt).toBeDefined(); expect(verificationToken.userId).toBe(user._id); expect(verificationToken.email).toBe(user.email); expect(verificationToken.expiresAt.getTime()).toBeGreaterThan(Date.now()); // Verify token is stored in localStorage const storedTokens = JSON.parse( localStorage.getItem('verification_tokens') || '[]' ); expect(storedTokens).toHaveLength(1); expect(storedTokens[0].token).toBe(verificationToken.token); expect(storedTokens[0].userId).toBe(user._id); expect(storedTokens[0].email).toBe(user.email); }); test('should send verification email when user has email', async () => { // Arrange const user = createMockUser({ email: 'user@example.com' }); mockMailgunService.sendVerificationEmail.mockResolvedValue(true); // Act const verificationToken = await emailVerificationService.generateVerificationToken(user); // Assert expect(mockMailgunService.sendVerificationEmail).toHaveBeenCalledWith( user.email, verificationToken.token ); }); test('should handle email sending failure gracefully', async () => { // Arrange const user = createMockUser({ email: 'user@example.com' }); mockMailgunService.sendVerificationEmail.mockResolvedValue(false); // Act const verificationToken = await emailVerificationService.generateVerificationToken(user); // Assert expect(verificationToken).toBeDefined(); expect(verificationToken.token).toBeDefined(); expect(mockMailgunService.sendVerificationEmail).toHaveBeenCalledWith( user.email, verificationToken.token ); }); test('should not send email when user has no email', async () => { // Arrange const user = createMockUser({ email: '' }); // Act const verificationToken = await emailVerificationService.generateVerificationToken(user); // Assert expect(verificationToken).toBeDefined(); expect(mockMailgunService.sendVerificationEmail).not.toHaveBeenCalled(); }); test('should generate unique tokens for multiple calls', async () => { // Arrange const user1 = createMockUser({ _id: 'user1', email: 'user1@example.com', }); const user2 = createMockUser({ _id: 'user2', email: 'user2@example.com', }); // Act const token1 = await emailVerificationService.generateVerificationToken(user1); const token2 = await emailVerificationService.generateVerificationToken(user2); // Assert expect(token1.token).not.toBe(token2.token); expect(token1.userId).toBe('user1'); expect(token2.userId).toBe('user2'); // Verify both tokens are stored const storedTokens = JSON.parse( localStorage.getItem('verification_tokens') || '[]' ); expect(storedTokens).toHaveLength(2); }); }); describe('validateVerificationToken', () => { test('should validate a valid token and return user', async () => { // Arrange const user = createMockUser(); mockDbService.findUserByEmail.mockResolvedValue(user); // Generate and store a token first const verificationToken = await emailVerificationService.generateVerificationToken(user); // Act const validatedUser = await emailVerificationService.validateVerificationToken( verificationToken.token ); // Assert expect(validatedUser).toBeDefined(); expect(validatedUser!._id).toBe(user._id); expect(validatedUser!.email).toBe(user.email); expect(mockDbService.findUserByEmail).toHaveBeenCalledWith(user.email); }); test('should return null for non-existent token', async () => { // Act const validatedUser = await emailVerificationService.validateVerificationToken( 'invalid-token' ); // Assert expect(validatedUser).toBeNull(); expect(mockDbService.findUserByEmail).not.toHaveBeenCalled(); }); test('should return null for expired token', async () => { // Arrange const user = createMockUser(); const verificationToken = await emailVerificationService.generateVerificationToken(user); // Manually expire the token in localStorage const tokens = JSON.parse( localStorage.getItem('verification_tokens') || '[]' ); tokens[0].expiresAt = new Date( Date.now() - 1000 * 60 * 60 * 24 ).toISOString(); // 24 hours ago localStorage.setItem('verification_tokens', JSON.stringify(tokens)); // Act const validatedUser = await emailVerificationService.validateVerificationToken( verificationToken.token ); // Assert expect(validatedUser).toBeNull(); expect(mockDbService.findUserByEmail).not.toHaveBeenCalled(); }); test('should return null when user not found in database', async () => { // Arrange const user = createMockUser(); const verificationToken = await emailVerificationService.generateVerificationToken(user); mockDbService.findUserByEmail.mockResolvedValue(null); // Act const validatedUser = await emailVerificationService.validateVerificationToken( verificationToken.token ); // Assert expect(validatedUser).toBeNull(); expect(mockDbService.findUserByEmail).toHaveBeenCalledWith(user.email); }); test('should handle database errors gracefully', async () => { // Arrange const user = createMockUser(); const verificationToken = await emailVerificationService.generateVerificationToken(user); mockDbService.findUserByEmail.mockRejectedValue( new Error('Database error') ); // Act & Assert await expect( emailVerificationService.validateVerificationToken( verificationToken.token ) ).rejects.toThrow('Database error'); }); }); describe('markEmailVerified', () => { test('should update user status to active and mark email verified', async () => { // Arrange const user = createMockUser(); const expectedUpdatedUser = { ...user, emailVerified: true, status: AccountStatus.ACTIVE, }; // Store a verification token first await emailVerificationService.generateVerificationToken(user); mockDbService.updateUser.mockResolvedValue(expectedUpdatedUser); // Act await emailVerificationService.markEmailVerified(user); // Assert expect(mockDbService.updateUser).toHaveBeenCalledWith( expectedUpdatedUser ); }); test('should remove verification token after marking verified', async () => { // Arrange const user = createMockUser(); await emailVerificationService.generateVerificationToken(user); mockDbService.updateUser.mockResolvedValue(user); // Verify token exists before let tokens = JSON.parse( localStorage.getItem('verification_tokens') || '[]' ); expect(tokens).toHaveLength(1); // Act await emailVerificationService.markEmailVerified(user); // Assert - token should be removed tokens = JSON.parse(localStorage.getItem('verification_tokens') || '[]'); expect(tokens).toHaveLength(0); }); test('should only remove tokens for the specific user', async () => { // Arrange const user1 = createMockUser({ _id: 'user1', email: 'user1@example.com', }); const user2 = createMockUser({ _id: 'user2', email: 'user2@example.com', }); await emailVerificationService.generateVerificationToken(user1); await emailVerificationService.generateVerificationToken(user2); mockDbService.updateUser.mockResolvedValue(user1); // Verify both tokens exist let tokens = JSON.parse( localStorage.getItem('verification_tokens') || '[]' ); expect(tokens).toHaveLength(2); // Act - verify only user1 await emailVerificationService.markEmailVerified(user1); // Assert - only user1's token should be removed tokens = JSON.parse(localStorage.getItem('verification_tokens') || '[]'); expect(tokens).toHaveLength(1); expect(tokens[0].userId).toBe('user2'); }); test('should handle database update errors', async () => { // Arrange const user = createMockUser(); mockDbService.updateUser.mockRejectedValue(new Error('Update failed')); // Act & Assert await expect( emailVerificationService.markEmailVerified(user) ).rejects.toThrow('Update failed'); }); }); describe('sendPasswordResetEmail', () => { test('should delegate to mailgun service', async () => { // Arrange const email = 'user@example.com'; const token = 'reset-token'; mockMailgunService.sendPasswordResetEmail.mockResolvedValue(true); // Act const result = await emailVerificationService.sendPasswordResetEmail( email, token ); // Assert expect(result).toBe(true); expect(mockMailgunService.sendPasswordResetEmail).toHaveBeenCalledWith( email, token ); }); test('should return false when email sending fails', async () => { // Arrange const email = 'user@example.com'; const token = 'reset-token'; mockMailgunService.sendPasswordResetEmail.mockResolvedValue(false); // Act const result = await emailVerificationService.sendPasswordResetEmail( email, token ); // Assert expect(result).toBe(false); expect(mockMailgunService.sendPasswordResetEmail).toHaveBeenCalledWith( email, token ); }); }); describe('integration scenarios', () => { test('should complete full verification flow', async () => { // Arrange const user = createMockUser(); mockDbService.findUserByEmail.mockResolvedValue(user); mockDbService.updateUser.mockResolvedValue({ ...user, emailVerified: true, status: AccountStatus.ACTIVE, }); // Act - Generate token const verificationToken = await emailVerificationService.generateVerificationToken(user); // Act - Validate token const validatedUser = await emailVerificationService.validateVerificationToken( verificationToken.token ); // Act - Mark as verified await emailVerificationService.markEmailVerified(validatedUser!); // Assert expect(validatedUser).toBeDefined(); expect(mockDbService.updateUser).toHaveBeenCalledWith({ ...user, emailVerified: true, status: AccountStatus.ACTIVE, }); // Verify token is cleaned up const tokens = JSON.parse( localStorage.getItem('verification_tokens') || '[]' ); expect(tokens).toHaveLength(0); }); test('should handle token expiration during flow', async () => { // Arrange const user = createMockUser(); const verificationToken = await emailVerificationService.generateVerificationToken(user); // Simulate time passing - expire the token const tokens = JSON.parse( localStorage.getItem('verification_tokens') || '[]' ); tokens[0].expiresAt = new Date(Date.now() - 1000).toISOString(); // 1 second ago localStorage.setItem('verification_tokens', JSON.stringify(tokens)); // Act const validatedUser = await emailVerificationService.validateVerificationToken( verificationToken.token ); // Assert expect(validatedUser).toBeNull(); }); }); });