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
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
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 new database service
|
||||
// Mock the database service used by authService
|
||||
jest.mock('../../database', () => ({
|
||||
databaseService: {
|
||||
findUserByEmail: jest.fn(),
|
||||
@@ -14,7 +16,7 @@ jest.mock('../../database', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the emailVerification service
|
||||
// Mock the email verification service
|
||||
jest.mock('../emailVerification.service', () => ({
|
||||
EmailVerificationService: jest.fn().mockImplementation(() => ({
|
||||
generateVerificationToken: jest.fn().mockResolvedValue({
|
||||
@@ -38,31 +40,36 @@ describe('Authentication Integration Tests', () => {
|
||||
|
||||
let mockUser: AuthenticatedUser;
|
||||
let mockDatabaseService: any;
|
||||
let hashedPassword: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
localStorage.clear();
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Get the mocked database service
|
||||
const { databaseService } = await import('../../database');
|
||||
mockDatabaseService = databaseService;
|
||||
|
||||
// Setup default mock user
|
||||
hashedPassword = await bcrypt.hash(testCredentials.password, 10);
|
||||
|
||||
mockUser = {
|
||||
_id: 'user1',
|
||||
_rev: 'mock-rev-1',
|
||||
email: testCredentials.email,
|
||||
username: testCredentials.username,
|
||||
password: testCredentials.password,
|
||||
password: hashedPassword,
|
||||
emailVerified: false,
|
||||
status: AccountStatus.PENDING,
|
||||
};
|
||||
|
||||
mockDatabaseService.createUserWithPassword.mockResolvedValue(mockUser);
|
||||
mockDatabaseService.updateUser.mockImplementation(
|
||||
async (user: any) => user
|
||||
);
|
||||
});
|
||||
|
||||
describe('User Registration', () => {
|
||||
test('should create a pending account for new user', async () => {
|
||||
test('should hash password before persisting new user', async () => {
|
||||
mockDatabaseService.findUserByEmail.mockResolvedValue(null);
|
||||
mockDatabaseService.createUserWithPassword.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await authService.register(
|
||||
testCredentials.email,
|
||||
@@ -72,10 +79,7 @@ describe('Authentication Integration Tests', () => {
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.user.username).toBe(testCredentials.username);
|
||||
expect(result.user.email).toBe(testCredentials.email);
|
||||
expect(result.user.status).toBe(AccountStatus.PENDING);
|
||||
expect(result.user.emailVerified).toBe(false);
|
||||
expect(result.verificationToken).toBeDefined();
|
||||
expect(result.verificationToken.token).toBe('mock-verification-token');
|
||||
|
||||
expect(mockDatabaseService.findUserByEmail).toHaveBeenCalledWith(
|
||||
@@ -83,9 +87,14 @@ describe('Authentication Integration Tests', () => {
|
||||
);
|
||||
expect(mockDatabaseService.createUserWithPassword).toHaveBeenCalledWith(
|
||||
testCredentials.email,
|
||||
testCredentials.password,
|
||||
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 () => {
|
||||
@@ -115,7 +124,7 @@ describe('Authentication Integration Tests', () => {
|
||||
).rejects.toThrow('Email verification required');
|
||||
});
|
||||
|
||||
test('should succeed after email verification', async () => {
|
||||
test('should succeed with correct bcrypt hashed password', async () => {
|
||||
const verifiedUser = {
|
||||
...mockUser,
|
||||
emailVerified: true,
|
||||
@@ -128,10 +137,8 @@ describe('Authentication Integration Tests', () => {
|
||||
password: testCredentials.password,
|
||||
});
|
||||
|
||||
expect(tokens).toBeDefined();
|
||||
expect(tokens.accessToken).toBeTruthy();
|
||||
expect(tokens.refreshToken).toBeTruthy();
|
||||
expect(tokens.user).toBeDefined();
|
||||
expect(tokens.user.status).toBe(AccountStatus.ACTIVE);
|
||||
});
|
||||
|
||||
@@ -151,6 +158,23 @@ describe('Authentication Integration Tests', () => {
|
||||
).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);
|
||||
|
||||
@@ -185,19 +209,9 @@ describe('Authentication Integration Tests', () => {
|
||||
|
||||
const result = await authService.loginWithOAuth('google', oauthUserData);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.user.email).toBe(oauthUserData.email);
|
||||
expect(result.user.username).toBe(oauthUserData.username);
|
||||
expect(result.user.status).toBe(AccountStatus.ACTIVE);
|
||||
expect(result.user.emailVerified).toBe(true);
|
||||
expect(result.accessToken).toBeTruthy();
|
||||
expect(result.refreshToken).toBeTruthy();
|
||||
|
||||
expect(mockDatabaseService.createUserFromOAuth).toHaveBeenCalledWith(
|
||||
oauthUserData.email,
|
||||
oauthUserData.username,
|
||||
'google'
|
||||
);
|
||||
});
|
||||
|
||||
test('should login existing OAuth user', async () => {
|
||||
@@ -215,74 +229,70 @@ describe('Authentication Integration Tests', () => {
|
||||
|
||||
const result = await authService.loginWithOAuth('google', oauthUserData);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.user.email).toBe(oauthUserData.email);
|
||||
expect(result.user._id).toBe('existing-user1');
|
||||
expect(result.accessToken).toBeTruthy();
|
||||
expect(result.refreshToken).toBeTruthy();
|
||||
|
||||
expect(mockDatabaseService.createUserFromOAuth).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should handle OAuth login errors gracefully', async () => {
|
||||
mockDatabaseService.findUserByEmail.mockRejectedValue(
|
||||
new Error('Database error')
|
||||
);
|
||||
|
||||
await expect(
|
||||
authService.loginWithOAuth('google', oauthUserData)
|
||||
).rejects.toThrow('OAuth login failed: Database error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Password Management', () => {
|
||||
test('should change password with valid current password', async () => {
|
||||
const userId = 'user1';
|
||||
const currentPassword = 'currentPassword';
|
||||
const newPassword = 'newPassword123';
|
||||
const userWithPassword = {
|
||||
const activeUser = {
|
||||
...mockUser,
|
||||
password: currentPassword,
|
||||
};
|
||||
const updatedUser = {
|
||||
...userWithPassword,
|
||||
password: newPassword,
|
||||
emailVerified: true,
|
||||
status: AccountStatus.ACTIVE,
|
||||
};
|
||||
|
||||
mockDatabaseService.getUserById.mockResolvedValue(userWithPassword);
|
||||
mockDatabaseService.updateUser.mockResolvedValue(updatedUser);
|
||||
mockDatabaseService.getUserById.mockResolvedValue(activeUser);
|
||||
|
||||
const result = await authService.changePassword(
|
||||
userId,
|
||||
currentPassword,
|
||||
testCredentials.password,
|
||||
newPassword
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.user.password).toBe(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');
|
||||
expect(mockDatabaseService.updateUser).toHaveBeenCalledWith({
|
||||
...userWithPassword,
|
||||
password: newPassword,
|
||||
});
|
||||
});
|
||||
|
||||
test('should fail password change with incorrect current password', async () => {
|
||||
const userId = 'user1';
|
||||
const currentPassword = 'wrongPassword';
|
||||
const newPassword = 'newPassword123';
|
||||
const userWithPassword = {
|
||||
const hashed = await bcrypt.hash('correctPassword', 10);
|
||||
const activeUser = {
|
||||
...mockUser,
|
||||
password: 'correctPassword',
|
||||
emailVerified: true,
|
||||
status: AccountStatus.ACTIVE,
|
||||
password: hashed,
|
||||
};
|
||||
|
||||
mockDatabaseService.getUserById.mockResolvedValue(userWithPassword);
|
||||
mockDatabaseService.getUserById.mockResolvedValue(activeUser);
|
||||
|
||||
await expect(
|
||||
authService.changePassword(userId, currentPassword, newPassword)
|
||||
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 = {
|
||||
@@ -302,7 +312,8 @@ describe('Authentication Integration Tests', () => {
|
||||
test('should request password reset for existing user', async () => {
|
||||
const userWithPassword = {
|
||||
...mockUser,
|
||||
password: 'hasPassword',
|
||||
emailVerified: true,
|
||||
status: AccountStatus.ACTIVE,
|
||||
};
|
||||
mockDatabaseService.findUserByEmail.mockResolvedValue(userWithPassword);
|
||||
|
||||
@@ -310,7 +321,6 @@ describe('Authentication Integration Tests', () => {
|
||||
testCredentials.email
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.message).toContain('password reset link has been sent');
|
||||
});
|
||||
|
||||
@@ -321,7 +331,6 @@ describe('Authentication Integration Tests', () => {
|
||||
'nonexistent@example.com'
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.message).toContain('password reset link has been sent');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user