diff --git a/services/auth/__tests__/emailVerification.test.ts b/services/auth/__tests__/emailVerification.test.ts index 6eaa0ae..5d6b4dc 100644 --- a/services/auth/__tests__/emailVerification.test.ts +++ b/services/auth/__tests__/emailVerification.test.ts @@ -1,58 +1,445 @@ import { EmailVerificationService } from '../emailVerification.service'; +import { AuthenticatedUser } from '../auth.types'; +import { AccountStatus } from '../auth.constants'; -jest.mock('../../couchdb.factory'); -jest.mock('../../email'); +// 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; - beforeEach(() => { + 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; }); - test('should generate and validate verification token', async () => { - const user = { - _id: 'user1', - email: 'test@example.com', - username: 'testuser', - password: 'password', - }; + describe('generateVerificationToken', () => { + test('should generate a valid verification token', async () => { + // Arrange + const user = createMockUser(); + mockMailgunService.sendVerificationEmail.mockResolvedValue(true); - const verificationToken = - await emailVerificationService.generateVerificationToken(user as any); + // Act + const verificationToken = + await emailVerificationService.generateVerificationToken(user); - expect(verificationToken).toBeDefined(); - expect(verificationToken.token).toBeDefined(); - expect(verificationToken.expiresAt).toBeDefined(); + // 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()); - const validatedUser = - await emailVerificationService.validateVerificationToken( + // 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 ); + }); - expect(validatedUser).toBeDefined(); - expect(validatedUser!._id).toBe(user._id); + 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); + }); }); - test('should not validate expired token', async () => { - const user = { - _id: 'user2', - email: 'test2@example.com', - username: 'testuser2', - password: 'password2', - }; + describe('validateVerificationToken', () => { + test('should validate a valid token and return user', async () => { + // Arrange + const user = createMockUser(); + mockDbService.findUserByEmail.mockResolvedValue(user); - const verificationToken = - await emailVerificationService.generateVerificationToken(user as any); + // Generate and store a token first + const verificationToken = + await emailVerificationService.generateVerificationToken(user); - // Set expiresAt to past date - verificationToken.expiresAt = new Date(Date.now() - 1000 * 60 * 60 * 24); + // Act + const validatedUser = + await emailVerificationService.validateVerificationToken( + verificationToken.token + ); - 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') ); - expect(validatedUser).toBeNull(); + // 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(); + }); }); });