Files
rxminder/services/auth/__tests__/auth.integration.test.ts
William Valentin 6a6b48cbc5 test: update auth and database tests for password hashing
- Refactor AvatarDropdown tests to use helper function pattern
- Add ResetPasswordPage test coverage for form validation and submission
- Update auth integration tests to verify bcrypt password handling
- Fix database service tests to expect hashed passwords
- Add proper mock setup for password verification scenarios
2025-10-16 13:16:00 -07:00

350 lines
11 KiB
TypeScript

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');
});
});
});