From 2956b79f3466ea52e0f8d9c19b9b50be01bb0b0a Mon Sep 17 00:00:00 2001 From: jackiettran <41605212+jackiettran@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:28:35 -0500 Subject: [PATCH] updated test --- .../unit/models/User.passwordReset.test.js | 127 ++++++++++++------ 1 file changed, 88 insertions(+), 39 deletions(-) diff --git a/backend/tests/unit/models/User.passwordReset.test.js b/backend/tests/unit/models/User.passwordReset.test.js index 0067388..c1ff0f6 100644 --- a/backend/tests/unit/models/User.passwordReset.test.js +++ b/backend/tests/unit/models/User.passwordReset.test.js @@ -1,7 +1,22 @@ const crypto = require('crypto'); // Mock crypto module -jest.mock('crypto'); +jest.mock('crypto', () => ({ + randomBytes: jest.fn(), + createHash: jest.fn(() => ({ + update: jest.fn().mockReturnThis(), + digest: jest.fn((encoding) => { + if (encoding === 'hex') { + return 'a'.repeat(64); // Deterministic hash for testing + } + return Buffer.from('a'.repeat(32)); + }) + })), + timingSafeEqual: jest.fn((a, b) => { + if (a.length !== b.length) return false; + return a.equals(b); + }) +})); // Mock the entire models module jest.mock('../../../models', () => { @@ -39,6 +54,7 @@ describe('User Model - Password Reset', () => { password: 'hashedPassword123', passwordResetToken: null, passwordResetTokenExpiry: null, + jwtVersion: 0, update: jest.fn().mockImplementation(function(updates) { Object.assign(this, updates); return Promise.resolve(this); @@ -52,7 +68,7 @@ describe('User Model - Password Reset', () => { describe('generatePasswordResetToken', () => { it('should generate a random token and set 1-hour expiry', async () => { const mockRandomBytes = Buffer.from('a'.repeat(32)); - const mockToken = mockRandomBytes.toString('hex'); + const hashedToken = 'a'.repeat(64); // Hashed version stored in DB crypto.randomBytes.mockReturnValue(mockRandomBytes); @@ -61,7 +77,7 @@ describe('User Model - Password Reset', () => { expect(crypto.randomBytes).toHaveBeenCalledWith(32); expect(mockUser.update).toHaveBeenCalledWith( expect.objectContaining({ - passwordResetToken: mockToken + passwordResetToken: hashedToken // Expect hashed token in DB }) ); @@ -76,44 +92,48 @@ describe('User Model - Password Reset', () => { it('should update the user with token and expiry', async () => { const mockRandomBytes = Buffer.from('b'.repeat(32)); - const mockToken = mockRandomBytes.toString('hex'); + const plainToken = mockRandomBytes.toString('hex'); crypto.randomBytes.mockReturnValue(mockRandomBytes); const result = await User.prototype.generatePasswordResetToken.call(mockUser); expect(mockUser.update).toHaveBeenCalledTimes(1); - expect(result.passwordResetToken).toBe(mockToken); - expect(result.passwordResetTokenExpiry).toBeInstanceOf(Date); + expect(result).toBe(plainToken); // Method returns plain token + expect(mockUser.passwordResetToken).toBe('a'.repeat(64)); // DB has hashed token + expect(mockUser.passwordResetTokenExpiry).toBeInstanceOf(Date); }); it('should generate unique tokens on multiple calls', async () => { const mockRandomBytes1 = Buffer.from('a'.repeat(32)); const mockRandomBytes2 = Buffer.from('b'.repeat(32)); + const plainToken1 = mockRandomBytes1.toString('hex'); + const plainToken2 = mockRandomBytes2.toString('hex'); crypto.randomBytes .mockReturnValueOnce(mockRandomBytes1) .mockReturnValueOnce(mockRandomBytes2); - await User.prototype.generatePasswordResetToken.call(mockUser); - const firstToken = mockUser.update.mock.calls[0][0].passwordResetToken; + const result1 = await User.prototype.generatePasswordResetToken.call(mockUser); + const result2 = await User.prototype.generatePasswordResetToken.call(mockUser); - await User.prototype.generatePasswordResetToken.call(mockUser); - const secondToken = mockUser.update.mock.calls[1][0].passwordResetToken; - - expect(firstToken).not.toBe(secondToken); + // Plain tokens returned should be different + expect(result1).toBe(plainToken1); + expect(result2).toBe(plainToken2); + expect(plainToken1).not.toBe(plainToken2); }); }); describe('isPasswordResetTokenValid', () => { it('should return true for valid token and non-expired time', () => { - const validToken = 'valid-token-123'; + const plainToken = 'valid-token-123'; + const hashedToken = 'a'.repeat(64); // Mocked hash result const futureExpiry = new Date(Date.now() + 30 * 60 * 1000); // 30 minutes from now - mockUser.passwordResetToken = validToken; + mockUser.passwordResetToken = hashedToken; // Store hashed token mockUser.passwordResetTokenExpiry = futureExpiry; - const result = User.prototype.isPasswordResetTokenValid.call(mockUser, validToken); + const result = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken); expect(result).toBe(true); }); @@ -128,7 +148,8 @@ describe('User Model - Password Reset', () => { }); it('should return false for missing expiry', () => { - mockUser.passwordResetToken = 'valid-token'; + const hashedToken = 'a'.repeat(64); + mockUser.passwordResetToken = hashedToken; mockUser.passwordResetTokenExpiry = null; const result = User.prototype.isPasswordResetTokenValid.call(mockUser, 'valid-token'); @@ -137,60 +158,71 @@ describe('User Model - Password Reset', () => { }); it('should return false for mismatched token', () => { - mockUser.passwordResetToken = 'correct-token'; + const hashedToken = 'a'.repeat(64); + mockUser.passwordResetToken = hashedToken; mockUser.passwordResetTokenExpiry = new Date(Date.now() + 30 * 60 * 1000); + // Mock createHash to return different hash for wrong token + crypto.createHash.mockReturnValueOnce({ + update: jest.fn().mockReturnThis(), + digest: jest.fn(() => 'b'.repeat(64)) // Different hash + }); + const result = User.prototype.isPasswordResetTokenValid.call(mockUser, 'wrong-token'); expect(result).toBe(false); }); it('should return false for expired token', () => { - const validToken = 'valid-token-123'; + const plainToken = 'valid-token-123'; + const hashedToken = 'a'.repeat(64); const pastExpiry = new Date(Date.now() - 10 * 60 * 1000); // 10 minutes ago - mockUser.passwordResetToken = validToken; + mockUser.passwordResetToken = hashedToken; mockUser.passwordResetTokenExpiry = pastExpiry; - const result = User.prototype.isPasswordResetTokenValid.call(mockUser, validToken); + const result = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken); expect(result).toBe(false); }); it('should return false for token expiring in the past by 1 second', () => { - const validToken = 'valid-token-123'; + const plainToken = 'valid-token-123'; + const hashedToken = 'a'.repeat(64); const pastExpiry = new Date(Date.now() - 1000); // 1 second ago - mockUser.passwordResetToken = validToken; + mockUser.passwordResetToken = hashedToken; mockUser.passwordResetTokenExpiry = pastExpiry; - const result = User.prototype.isPasswordResetTokenValid.call(mockUser, validToken); + const result = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken); expect(result).toBe(false); }); it('should handle edge case of token expiring exactly now', () => { - const validToken = 'valid-token-123'; + const plainToken = 'valid-token-123'; + const hashedToken = 'a'.repeat(64); // Set expiry 1ms in the future to handle timing precision const nowExpiry = new Date(Date.now() + 1); - mockUser.passwordResetToken = validToken; + mockUser.passwordResetToken = hashedToken; mockUser.passwordResetTokenExpiry = nowExpiry; // This should be true because expiry is slightly in the future - const result = User.prototype.isPasswordResetTokenValid.call(mockUser, validToken); + const result = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken); expect(result).toBe(true); }); it('should handle string dates correctly', () => { - const validToken = 'valid-token-123'; + const plainToken = 'valid-token-123'; + const hashedToken = 'a'.repeat(64); const futureExpiry = new Date(Date.now() + 30 * 60 * 1000).toISOString(); // String date - mockUser.passwordResetToken = validToken; + mockUser.passwordResetToken = hashedToken; mockUser.passwordResetTokenExpiry = futureExpiry; - const result = User.prototype.isPasswordResetTokenValid.call(mockUser, validToken); + const result = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken); expect(result).toBe(true); }); @@ -200,22 +232,26 @@ describe('User Model - Password Reset', () => { it('should update password and clear token fields', async () => { mockUser.passwordResetToken = 'some-token'; mockUser.passwordResetTokenExpiry = new Date(); + mockUser.jwtVersion = 0; await User.prototype.resetPassword.call(mockUser, 'newSecurePassword123!'); expect(mockUser.update).toHaveBeenCalledWith({ password: 'newSecurePassword123!', passwordResetToken: null, - passwordResetTokenExpiry: null + passwordResetTokenExpiry: null, + jwtVersion: 1 }); }); it('should return updated user object', async () => { + mockUser.jwtVersion = 0; const result = await User.prototype.resetPassword.call(mockUser, 'newPassword123!'); expect(result.password).toBe('newPassword123!'); expect(result.passwordResetToken).toBe(null); expect(result.passwordResetTokenExpiry).toBe(null); + expect(result.jwtVersion).toBe(1); }); it('should call update only once', async () => { @@ -229,34 +265,45 @@ describe('User Model - Password Reset', () => { it('should complete full password reset flow successfully', async () => { // Step 1: Generate password reset token const mockRandomBytes = Buffer.from('c'.repeat(32)); - const mockToken = mockRandomBytes.toString('hex'); + const plainToken = mockRandomBytes.toString('hex'); + const hashedToken = 'a'.repeat(64); crypto.randomBytes.mockReturnValue(mockRandomBytes); - await User.prototype.generatePasswordResetToken.call(mockUser); + const returnedToken = await User.prototype.generatePasswordResetToken.call(mockUser); - expect(mockUser.passwordResetToken).toBe(mockToken); + expect(returnedToken).toBe(plainToken); // Returns plain token + expect(mockUser.passwordResetToken).toBe(hashedToken); // Stores hashed token expect(mockUser.passwordResetTokenExpiry).toBeInstanceOf(Date); - // Step 2: Validate token - const isValid = User.prototype.isPasswordResetTokenValid.call(mockUser, mockToken); + // Step 2: Validate token (pass plain token, compares with hashed) + const isValid = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken); expect(isValid).toBe(true); // Step 3: Reset password + mockUser.jwtVersion = 0; await User.prototype.resetPassword.call(mockUser, 'newPassword123!'); expect(mockUser.password).toBe('newPassword123!'); expect(mockUser.passwordResetToken).toBe(null); expect(mockUser.passwordResetTokenExpiry).toBe(null); + expect(mockUser.jwtVersion).toBe(1); }); it('should fail password reset with wrong token', async () => { // Generate token - const mockToken = 'd'.repeat(64); const mockRandomBytes = Buffer.from('d'.repeat(32)); + const plainToken = mockRandomBytes.toString('hex'); + const hashedToken = 'a'.repeat(64); crypto.randomBytes.mockReturnValue(mockRandomBytes); await User.prototype.generatePasswordResetToken.call(mockUser); + // Mock createHash to return different hash for wrong token + crypto.createHash.mockReturnValueOnce({ + update: jest.fn().mockReturnThis(), + digest: jest.fn(() => 'b'.repeat(64)) // Different hash + }); + // Try to validate with wrong token const isValid = User.prototype.isPasswordResetTokenValid.call(mockUser, 'wrong-token'); @@ -265,7 +312,8 @@ describe('User Model - Password Reset', () => { it('should fail password reset with expired token', async () => { // Manually set an expired token - mockUser.passwordResetToken = 'expired-token'; + const hashedToken = 'a'.repeat(64); + mockUser.passwordResetToken = hashedToken; mockUser.passwordResetTokenExpiry = new Date(Date.now() - 2 * 60 * 60 * 1000); // 2 hours ago const isValid = User.prototype.isPasswordResetTokenValid.call(mockUser, 'expired-token'); @@ -276,16 +324,17 @@ describe('User Model - Password Reset', () => { it('should not allow password reset after token has been used', async () => { // Generate token const mockRandomBytes = Buffer.from('e'.repeat(32)); - const mockToken = mockRandomBytes.toString('hex'); + const plainToken = mockRandomBytes.toString('hex'); crypto.randomBytes.mockReturnValue(mockRandomBytes); await User.prototype.generatePasswordResetToken.call(mockUser); // Reset password (clears token) + mockUser.jwtVersion = 0; await User.prototype.resetPassword.call(mockUser, 'newPassword123!'); // Try to validate same token again (should fail because it's cleared) - const isValid = User.prototype.isPasswordResetTokenValid.call(mockUser, mockToken); + const isValid = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken); expect(isValid).toBe(false); });