import bcrypt from 'bcryptjs'; import { authService } from '../auth.service'; import { AccountStatus } from '../auth.constants'; import { AuthenticatedUser } from '../auth.types'; import { isBcryptHash } from '../password.service'; // Mock the database service used by authService jest.mock('../../database', () => ({ databaseService: { findUserByEmail: jest.fn(), createUserWithPassword: jest.fn(), createUserFromOAuth: jest.fn(), updateUser: jest.fn(), getUserById: jest.fn(), changeUserPassword: jest.fn(), }, })); // Mock the email verification service jest.mock('../emailVerification.service', () => ({ EmailVerificationService: jest.fn().mockImplementation(() => ({ generateVerificationToken: jest.fn().mockResolvedValue({ token: 'mock-verification-token', userId: 'user1', email: 'testuser@example.com', expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), }), validateVerificationToken: jest.fn(), markEmailVerified: jest.fn(), sendPasswordResetEmail: jest.fn().mockResolvedValue(true), })), })); describe('Authentication Integration Tests', () => { const testCredentials = { username: 'testuser', password: 'Passw0rd!', email: 'testuser@example.com', }; let mockUser: AuthenticatedUser; let mockDatabaseService: any; let hashedPassword: string; beforeEach(async () => { localStorage.clear(); jest.clearAllMocks(); const { databaseService } = await import('../../database'); mockDatabaseService = databaseService; hashedPassword = await bcrypt.hash(testCredentials.password, 10); mockUser = { _id: 'user1', _rev: 'mock-rev-1', email: testCredentials.email, username: testCredentials.username, password: hashedPassword, emailVerified: false, status: AccountStatus.PENDING, }; mockDatabaseService.createUserWithPassword.mockResolvedValue(mockUser); mockDatabaseService.updateUser.mockImplementation( async (user: any) => user ); }); describe('User Registration', () => { test('should hash password before persisting new user', async () => { mockDatabaseService.findUserByEmail.mockResolvedValue(null); const result = await authService.register( testCredentials.email, testCredentials.password, testCredentials.username ); expect(result).toBeDefined(); expect(result.user.username).toBe(testCredentials.username); expect(result.user.status).toBe(AccountStatus.PENDING); expect(result.verificationToken.token).toBe('mock-verification-token'); expect(mockDatabaseService.findUserByEmail).toHaveBeenCalledWith( testCredentials.email ); expect(mockDatabaseService.createUserWithPassword).toHaveBeenCalledWith( testCredentials.email, expect.any(String), testCredentials.username ); const persistedPassword = mockDatabaseService.createUserWithPassword.mock.calls[0][1]; expect(isBcryptHash(persistedPassword)).toBe(true); expect(persistedPassword).not.toBe(testCredentials.password); }); test('should fail when user already exists', async () => { mockDatabaseService.findUserByEmail.mockResolvedValue(mockUser); await expect( authService.register( testCredentials.email, testCredentials.password, testCredentials.username ) ).rejects.toThrow('User already exists'); expect(mockDatabaseService.createUserWithPassword).not.toHaveBeenCalled(); }); }); describe('User Login', () => { test('should fail for unverified (pending) account', async () => { mockDatabaseService.findUserByEmail.mockResolvedValue(mockUser); await expect( authService.login({ email: testCredentials.email, password: testCredentials.password, }) ).rejects.toThrow('Email verification required'); }); test('should succeed with correct bcrypt hashed password', async () => { const verifiedUser = { ...mockUser, emailVerified: true, status: AccountStatus.ACTIVE, }; mockDatabaseService.findUserByEmail.mockResolvedValue(verifiedUser); const tokens = await authService.login({ email: testCredentials.email, password: testCredentials.password, }); expect(tokens.accessToken).toBeTruthy(); expect(tokens.refreshToken).toBeTruthy(); expect(tokens.user.status).toBe(AccountStatus.ACTIVE); }); test('should fail with wrong password', async () => { const verifiedUser = { ...mockUser, emailVerified: true, status: AccountStatus.ACTIVE, }; mockDatabaseService.findUserByEmail.mockResolvedValue(verifiedUser); await expect( authService.login({ email: testCredentials.email, password: 'wrongpassword', }) ).rejects.toThrow('Invalid credentials'); }); test('should reject legacy accounts with plaintext passwords', async () => { const legacyUser = { ...mockUser, emailVerified: true, status: AccountStatus.ACTIVE, password: testCredentials.password, }; mockDatabaseService.findUserByEmail.mockResolvedValue(legacyUser); await expect( authService.login({ email: testCredentials.email, password: testCredentials.password, }) ).rejects.toThrow('Invalid credentials'); }); test('should fail for non-existent user', async () => { mockDatabaseService.findUserByEmail.mockResolvedValue(null); await expect( authService.login({ email: 'nonexistent@example.com', password: testCredentials.password, }) ).rejects.toThrow('User not found'); }); }); describe('OAuth Authentication', () => { const oauthUserData = { email: 'oauthuser@example.com', username: 'OAuth User', }; test('should register new OAuth user', async () => { const oauthUser: AuthenticatedUser = { _id: 'oauth-user1', _rev: 'mock-rev-oauth-1', email: oauthUserData.email, username: oauthUserData.username, password: '', emailVerified: true, status: AccountStatus.ACTIVE, }; mockDatabaseService.findUserByEmail.mockResolvedValue(null); mockDatabaseService.createUserFromOAuth.mockResolvedValue(oauthUser); const result = await authService.loginWithOAuth('google', oauthUserData); expect(result.user.email).toBe(oauthUserData.email); expect(result.accessToken).toBeTruthy(); expect(result.refreshToken).toBeTruthy(); }); test('should login existing OAuth user', async () => { const existingUser: AuthenticatedUser = { _id: 'existing-user1', _rev: 'mock-rev-existing-1', email: oauthUserData.email, username: oauthUserData.username, password: 'existing-password', emailVerified: true, status: AccountStatus.ACTIVE, }; mockDatabaseService.findUserByEmail.mockResolvedValue(existingUser); const result = await authService.loginWithOAuth('google', oauthUserData); expect(result.user._id).toBe('existing-user1'); expect(mockDatabaseService.createUserFromOAuth).not.toHaveBeenCalled(); }); }); describe('Password Management', () => { test('should change password with valid current password', async () => { const userId = 'user1'; const newPassword = 'newPassword123'; const activeUser = { ...mockUser, emailVerified: true, status: AccountStatus.ACTIVE, }; mockDatabaseService.getUserById.mockResolvedValue(activeUser); const result = await authService.changePassword( userId, testCredentials.password, newPassword ); const updatedUser = mockDatabaseService.updateUser.mock.calls[0][0]; expect(isBcryptHash(updatedUser.password)).toBe(true); expect(await bcrypt.compare(newPassword, updatedUser.password)).toBe( true ); expect(result.message).toBe('Password changed successfully'); }); test('should fail password change with incorrect current password', async () => { const userId = 'user1'; const hashed = await bcrypt.hash('correctPassword', 10); const activeUser = { ...mockUser, emailVerified: true, status: AccountStatus.ACTIVE, password: hashed, }; mockDatabaseService.getUserById.mockResolvedValue(activeUser); await expect( authService.changePassword(userId, 'wrongPassword', 'newPassword123') ).rejects.toThrow('Current password is incorrect'); }); test('should fail password change when legacy password is detected', async () => { const userId = 'user1'; const legacyUser = { ...mockUser, emailVerified: true, status: AccountStatus.ACTIVE, password: 'legacyPassword', }; mockDatabaseService.getUserById.mockResolvedValue(legacyUser); await expect( authService.changePassword(userId, 'legacyPassword', 'newPassword123') ).rejects.toThrow('Password needs to be reset before it can be changed'); }); test('should fail password change for OAuth users', async () => { const userId = 'user1'; const oauthUser = { ...mockUser, password: '', }; mockDatabaseService.getUserById.mockResolvedValue(oauthUser); await expect( authService.changePassword(userId, 'any', 'newPassword') ).rejects.toThrow('Cannot change password for OAuth accounts'); }); }); describe('Password Reset', () => { test('should request password reset for existing user', async () => { const userWithPassword = { ...mockUser, emailVerified: true, status: AccountStatus.ACTIVE, }; mockDatabaseService.findUserByEmail.mockResolvedValue(userWithPassword); const result = await authService.requestPasswordReset( testCredentials.email ); expect(result.message).toContain('password reset link has been sent'); }); test('should handle password reset for non-existent user gracefully', async () => { mockDatabaseService.findUserByEmail.mockResolvedValue(null); const result = await authService.requestPasswordReset( 'nonexistent@example.com' ); expect(result.message).toContain('password reset link has been sent'); }); test('should fail password reset for OAuth users', async () => { const oauthUser = { ...mockUser, password: '', }; mockDatabaseService.findUserByEmail.mockResolvedValue(oauthUser); await expect( authService.requestPasswordReset(testCredentials.email) ).rejects.toThrow('Cannot reset password for OAuth accounts'); }); }); });