const crypto = require('crypto'); // Mock crypto module jest.mock('crypto'); // Mock the logger to prevent winston-daily-rotate-file issues jest.mock('../../../utils/logger', () => ({ error: jest.fn(), info: jest.fn(), warn: jest.fn(), debug: jest.fn(), withRequestId: jest.fn(() => ({ error: jest.fn(), info: jest.fn(), warn: jest.fn(), debug: jest.fn(), })), })); // Mock TwoFactorService to prevent otplib loading jest.mock('../../../services/TwoFactorService', () => ({ generateSecret: jest.fn(), verifyToken: jest.fn(), generateQRCode: jest.fn(), })); // Mock the entire models module jest.mock('../../../models', () => { const mockUser = { update: jest.fn(), verificationToken: null, verificationTokenExpiry: null, isVerified: false, verifiedAt: null }; return { User: mockUser, sequelize: { models: { User: mockUser } } }; }); // Import User model methods - we'll test them directly const User = require('../../../models/User'); describe('User Model - Email Verification', () => { let mockUser; beforeEach(() => { jest.clearAllMocks(); // Create a fresh mock user for each test mockUser = { id: 1, email: 'test@example.com', firstName: 'Test', lastName: 'User', verificationToken: null, verificationTokenExpiry: null, verificationAttempts: 0, isVerified: false, verifiedAt: null, update: jest.fn().mockImplementation(function(updates) { Object.assign(this, updates); return Promise.resolve(this); }) }; // Add the prototype methods to mockUser Object.setPrototypeOf(mockUser, User.prototype); }); describe('generateVerificationToken', () => { it('should generate a 6-digit code and set 24-hour expiry', async () => { const mockCode = 123456; crypto.randomInt.mockReturnValue(mockCode); await User.prototype.generateVerificationToken.call(mockUser); expect(crypto.randomInt).toHaveBeenCalledWith(100000, 999999); expect(mockUser.update).toHaveBeenCalledWith( expect.objectContaining({ verificationToken: '123456', verificationAttempts: 0, }) ); // Check that expiry is approximately 24 hours from now const updateCall = mockUser.update.mock.calls[0][0]; const expiryTime = updateCall.verificationTokenExpiry.getTime(); const expectedExpiry = Date.now() + 24 * 60 * 60 * 1000; expect(expiryTime).toBeGreaterThan(expectedExpiry - 1000); expect(expiryTime).toBeLessThan(expectedExpiry + 1000); }); it('should update the user with code and expiry', async () => { const mockCode = 654321; crypto.randomInt.mockReturnValue(mockCode); const result = await User.prototype.generateVerificationToken.call(mockUser); expect(mockUser.update).toHaveBeenCalledTimes(1); expect(result.verificationToken).toBe('654321'); expect(result.verificationTokenExpiry).toBeInstanceOf(Date); }); it('should generate unique codes on multiple calls', async () => { crypto.randomInt .mockReturnValueOnce(111111) .mockReturnValueOnce(222222); await User.prototype.generateVerificationToken.call(mockUser); const firstCode = mockUser.update.mock.calls[0][0].verificationToken; await User.prototype.generateVerificationToken.call(mockUser); const secondCode = mockUser.update.mock.calls[1][0].verificationToken; expect(firstCode).not.toBe(secondCode); }); }); describe('isVerificationTokenValid', () => { beforeEach(() => { // Mock timingSafeEqual to do a simple comparison crypto.timingSafeEqual = jest.fn((a, b) => a.equals(b)); }); it('should return true for valid token and non-expired time', () => { const validToken = '123456'; const futureExpiry = new Date(Date.now() + 60 * 60 * 1000); // 1 hour from now mockUser.verificationToken = validToken; mockUser.verificationTokenExpiry = futureExpiry; const result = User.prototype.isVerificationTokenValid.call(mockUser, validToken); expect(result).toBe(true); }); it('should return false for missing token', () => { mockUser.verificationToken = null; mockUser.verificationTokenExpiry = new Date(Date.now() + 60 * 60 * 1000); const result = User.prototype.isVerificationTokenValid.call(mockUser, 'any-token'); expect(result).toBe(false); }); it('should return false for missing expiry', () => { mockUser.verificationToken = '123456'; mockUser.verificationTokenExpiry = null; const result = User.prototype.isVerificationTokenValid.call(mockUser, '123456'); expect(result).toBe(false); }); it('should return false for mismatched token', () => { mockUser.verificationToken = '123456'; mockUser.verificationTokenExpiry = new Date(Date.now() + 60 * 60 * 1000); const result = User.prototype.isVerificationTokenValid.call(mockUser, '654321'); expect(result).toBe(false); }); it('should return false for expired token', () => { const validToken = '123456'; const pastExpiry = new Date(Date.now() - 60 * 60 * 1000); // 1 hour ago mockUser.verificationToken = validToken; mockUser.verificationTokenExpiry = pastExpiry; const result = User.prototype.isVerificationTokenValid.call(mockUser, validToken); expect(result).toBe(false); }); it('should return false for token expiring in the past by 1 second', () => { const validToken = '123456'; const pastExpiry = new Date(Date.now() - 1000); // 1 second ago mockUser.verificationToken = validToken; mockUser.verificationTokenExpiry = pastExpiry; const result = User.prototype.isVerificationTokenValid.call(mockUser, validToken); expect(result).toBe(false); }); it('should handle edge case of token expiring exactly now', () => { const validToken = '123456'; // Set expiry 1ms in the future to handle timing precision const nowExpiry = new Date(Date.now() + 1); mockUser.verificationToken = validToken; mockUser.verificationTokenExpiry = nowExpiry; // This should be true because expiry is slightly in the future const result = User.prototype.isVerificationTokenValid.call(mockUser, validToken); expect(result).toBe(true); }); it('should handle string dates correctly', () => { const validToken = '123456'; const futureExpiry = new Date(Date.now() + 60 * 60 * 1000).toISOString(); // String date mockUser.verificationToken = validToken; mockUser.verificationTokenExpiry = futureExpiry; const result = User.prototype.isVerificationTokenValid.call(mockUser, validToken); expect(result).toBe(true); }); }); describe('verifyEmail', () => { it('should mark user as verified and clear token fields', async () => { mockUser.verificationToken = '123456'; mockUser.verificationTokenExpiry = new Date(); await User.prototype.verifyEmail.call(mockUser); expect(mockUser.update).toHaveBeenCalledWith( expect.objectContaining({ isVerified: true, verificationToken: null, verificationTokenExpiry: null }) ); }); it('should set verifiedAt timestamp', async () => { const beforeTime = Date.now(); await User.prototype.verifyEmail.call(mockUser); const updateCall = mockUser.update.mock.calls[0][0]; const verifiedAtTime = updateCall.verifiedAt.getTime(); const afterTime = Date.now(); expect(verifiedAtTime).toBeGreaterThanOrEqual(beforeTime); expect(verifiedAtTime).toBeLessThanOrEqual(afterTime); }); it('should return updated user object', async () => { const result = await User.prototype.verifyEmail.call(mockUser); expect(result.isVerified).toBe(true); expect(result.verificationToken).toBe(null); expect(result.verificationTokenExpiry).toBe(null); expect(result.verifiedAt).toBeInstanceOf(Date); }); it('should call update only once', async () => { await User.prototype.verifyEmail.call(mockUser); expect(mockUser.update).toHaveBeenCalledTimes(1); }); }); describe('Complete verification flow', () => { beforeEach(() => { crypto.timingSafeEqual = jest.fn((a, b) => a.equals(b)); }); it('should complete full verification flow successfully', async () => { // Step 1: Generate verification code const mockCode = 999888; crypto.randomInt.mockReturnValue(mockCode); await User.prototype.generateVerificationToken.call(mockUser); expect(mockUser.verificationToken).toBe('999888'); expect(mockUser.verificationTokenExpiry).toBeInstanceOf(Date); // Step 2: Validate code const isValid = User.prototype.isVerificationTokenValid.call(mockUser, '999888'); expect(isValid).toBe(true); // Step 3: Verify email await User.prototype.verifyEmail.call(mockUser); expect(mockUser.isVerified).toBe(true); expect(mockUser.verificationToken).toBe(null); expect(mockUser.verificationTokenExpiry).toBe(null); expect(mockUser.verifiedAt).toBeInstanceOf(Date); }); it('should fail verification with wrong token', async () => { // Generate code crypto.randomInt.mockReturnValue(123456); await User.prototype.generateVerificationToken.call(mockUser); // Try to validate with wrong code const isValid = User.prototype.isVerificationTokenValid.call(mockUser, '654321'); expect(isValid).toBe(false); }); it('should fail verification with expired token', async () => { // Manually set an expired token mockUser.verificationToken = '123456'; mockUser.verificationTokenExpiry = new Date(Date.now() - 25 * 60 * 60 * 1000); // 25 hours ago const isValid = User.prototype.isVerificationTokenValid.call(mockUser, '123456'); expect(isValid).toBe(false); }); }); });