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:
@@ -5,7 +5,6 @@ import AvatarDropdown from '../AvatarDropdown';
|
|||||||
import { User, UserRole } from '../../../types';
|
import { User, UserRole } from '../../../types';
|
||||||
import { AccountStatus } from '../../../services/auth/auth.constants';
|
import { AccountStatus } from '../../../services/auth/auth.constants';
|
||||||
|
|
||||||
// Mock user data
|
|
||||||
const mockRegularUser: User = {
|
const mockRegularUser: User = {
|
||||||
_id: '1',
|
_id: '1',
|
||||||
_rev: '1-abc123',
|
_rev: '1-abc123',
|
||||||
@@ -33,7 +32,19 @@ const mockUserWithAvatar: User = {
|
|||||||
|
|
||||||
const mockUserWithPassword: User = {
|
const mockUserWithPassword: User = {
|
||||||
...mockRegularUser,
|
...mockRegularUser,
|
||||||
password: 'hashed-password',
|
password: '$2b$12$examplehashforpassword',
|
||||||
|
};
|
||||||
|
|
||||||
|
type DropdownProps = Partial<React.ComponentProps<typeof AvatarDropdown>>;
|
||||||
|
|
||||||
|
const renderDropdown = (props: DropdownProps = {}) => {
|
||||||
|
const defaultProps: React.ComponentProps<typeof AvatarDropdown> = {
|
||||||
|
user: mockRegularUser,
|
||||||
|
onLogout: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const merged = { ...defaultProps, ...props };
|
||||||
|
return render(React.createElement(AvatarDropdown, merged));
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('AvatarDropdown', () => {
|
describe('AvatarDropdown', () => {
|
||||||
@@ -47,7 +58,7 @@ describe('AvatarDropdown', () => {
|
|||||||
|
|
||||||
describe('rendering', () => {
|
describe('rendering', () => {
|
||||||
test('should render avatar button with user initials', () => {
|
test('should render avatar button with user initials', () => {
|
||||||
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /user menu/i });
|
const button = screen.getByRole('button', { name: /user menu/i });
|
||||||
expect(button).toBeInTheDocument();
|
expect(button).toBeInTheDocument();
|
||||||
@@ -55,9 +66,7 @@ describe('AvatarDropdown', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should render avatar image when user has avatar', () => {
|
test('should render avatar image when user has avatar', () => {
|
||||||
render(
|
renderDropdown({ user: mockUserWithAvatar, onLogout: mockOnLogout });
|
||||||
<AvatarDropdown user={mockUserWithAvatar} onLogout={mockOnLogout} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const avatar = screen.getByAltText('User avatar');
|
const avatar = screen.getByAltText('User avatar');
|
||||||
expect(avatar).toBeInTheDocument();
|
expect(avatar).toBeInTheDocument();
|
||||||
@@ -66,16 +75,14 @@ describe('AvatarDropdown', () => {
|
|||||||
|
|
||||||
test('should render fallback character for empty username', () => {
|
test('should render fallback character for empty username', () => {
|
||||||
const userWithEmptyName = { ...mockRegularUser, username: '' };
|
const userWithEmptyName = { ...mockRegularUser, username: '' };
|
||||||
render(
|
renderDropdown({ user: userWithEmptyName, onLogout: mockOnLogout });
|
||||||
<AvatarDropdown user={userWithEmptyName} onLogout={mockOnLogout} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /user menu/i });
|
const button = screen.getByRole('button', { name: /user menu/i });
|
||||||
expect(button).toHaveTextContent('?');
|
expect(button).toHaveTextContent('?');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not render dropdown menu initially', () => {
|
test('should not render dropdown menu initially', () => {
|
||||||
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
||||||
|
|
||||||
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -83,7 +90,7 @@ describe('AvatarDropdown', () => {
|
|||||||
|
|
||||||
describe('dropdown functionality', () => {
|
describe('dropdown functionality', () => {
|
||||||
test('should open dropdown when avatar button is clicked', () => {
|
test('should open dropdown when avatar button is clicked', () => {
|
||||||
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /user menu/i });
|
const button = screen.getByRole('button', { name: /user menu/i });
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
@@ -93,35 +100,40 @@ describe('AvatarDropdown', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should close dropdown when avatar button is clicked again', () => {
|
test('should close dropdown when avatar button is clicked again', () => {
|
||||||
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /user menu/i });
|
const button = screen.getByRole('button', { name: /user menu/i });
|
||||||
|
|
||||||
// Open dropdown
|
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
expect(screen.getByText('Signed in as')).toBeInTheDocument();
|
expect(screen.getByText('Signed in as')).toBeInTheDocument();
|
||||||
|
|
||||||
// Close dropdown
|
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should close dropdown when clicking outside', async () => {
|
test('should close dropdown when clicking outside', async () => {
|
||||||
render(
|
render(
|
||||||
<div>
|
React.createElement(
|
||||||
<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />
|
'div',
|
||||||
<div data-testid='outside'>Outside element</div>
|
null,
|
||||||
</div>
|
React.createElement(AvatarDropdown, {
|
||||||
|
user: mockRegularUser,
|
||||||
|
onLogout: mockOnLogout,
|
||||||
|
}),
|
||||||
|
React.createElement(
|
||||||
|
'div',
|
||||||
|
{ 'data-testid': 'outside' },
|
||||||
|
'Outside element'
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /user menu/i });
|
const button = screen.getByRole('button', { name: /user menu/i });
|
||||||
const outside = screen.getByTestId('outside');
|
const outside = screen.getByTestId('outside');
|
||||||
|
|
||||||
// Open dropdown
|
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
expect(screen.getByText('Signed in as')).toBeInTheDocument();
|
expect(screen.getByText('Signed in as')).toBeInTheDocument();
|
||||||
|
|
||||||
// Click outside
|
|
||||||
fireEvent.mouseDown(outside);
|
fireEvent.mouseDown(outside);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -132,21 +144,21 @@ describe('AvatarDropdown', () => {
|
|||||||
|
|
||||||
describe('user information display', () => {
|
describe('user information display', () => {
|
||||||
test('should display username in dropdown', () => {
|
test('should display username in dropdown', () => {
|
||||||
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
||||||
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should display administrator badge for admin users', () => {
|
test('should display administrator badge for admin users', () => {
|
||||||
render(<AvatarDropdown user={mockAdminUser} onLogout={mockOnLogout} />);
|
renderDropdown({ user: mockAdminUser, onLogout: mockOnLogout });
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
||||||
expect(screen.getByText('Administrator')).toBeInTheDocument();
|
expect(screen.getByText('Administrator')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not display administrator badge for regular users', () => {
|
test('should not display administrator badge for regular users', () => {
|
||||||
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
||||||
expect(screen.queryByText('Administrator')).not.toBeInTheDocument();
|
expect(screen.queryByText('Administrator')).not.toBeInTheDocument();
|
||||||
@@ -158,9 +170,7 @@ describe('AvatarDropdown', () => {
|
|||||||
username: 'Very Long Username That Should Be Truncated',
|
username: 'Very Long Username That Should Be Truncated',
|
||||||
};
|
};
|
||||||
|
|
||||||
render(
|
renderDropdown({ user: userWithLongName, onLogout: mockOnLogout });
|
||||||
<AvatarDropdown user={userWithLongName} onLogout={mockOnLogout} />
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
||||||
const usernameElement = screen.getByText(
|
const usernameElement = screen.getByText(
|
||||||
@@ -172,7 +182,7 @@ describe('AvatarDropdown', () => {
|
|||||||
|
|
||||||
describe('logout functionality', () => {
|
describe('logout functionality', () => {
|
||||||
test('should call onLogout when logout button is clicked', () => {
|
test('should call onLogout when logout button is clicked', () => {
|
||||||
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
||||||
fireEvent.click(screen.getByText('Logout'));
|
fireEvent.click(screen.getByText('Logout'));
|
||||||
@@ -181,7 +191,7 @@ describe('AvatarDropdown', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should close dropdown after logout is clicked', () => {
|
test('should close dropdown after logout is clicked', () => {
|
||||||
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
||||||
fireEvent.click(screen.getByText('Logout'));
|
fireEvent.click(screen.getByText('Logout'));
|
||||||
@@ -190,7 +200,7 @@ describe('AvatarDropdown', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should always display logout button', () => {
|
test('should always display logout button', () => {
|
||||||
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
||||||
expect(screen.getByText('Logout')).toBeInTheDocument();
|
expect(screen.getByText('Logout')).toBeInTheDocument();
|
||||||
@@ -198,280 +208,115 @@ describe('AvatarDropdown', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('admin functionality', () => {
|
describe('admin functionality', () => {
|
||||||
test('should display admin interface button for admin users when onAdmin provided', () => {
|
test('should render Admin Interface button for admin users', () => {
|
||||||
render(
|
renderDropdown({
|
||||||
<AvatarDropdown
|
user: mockAdminUser,
|
||||||
user={mockAdminUser}
|
onLogout: mockOnLogout,
|
||||||
onLogout={mockOnLogout}
|
onAdmin: mockOnAdmin,
|
||||||
onAdmin={mockOnAdmin}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
||||||
expect(screen.getByText('Admin Interface')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not display admin interface button for regular users', () => {
|
|
||||||
render(
|
|
||||||
<AvatarDropdown
|
|
||||||
user={mockRegularUser}
|
|
||||||
onLogout={mockOnLogout}
|
|
||||||
onAdmin={mockOnAdmin}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
||||||
expect(screen.queryByText('Admin Interface')).not.toBeInTheDocument();
|
const adminButton = screen.getByText('Admin Interface');
|
||||||
});
|
expect(adminButton).toBeInTheDocument();
|
||||||
|
|
||||||
test('should not display admin interface button when onAdmin not provided', () => {
|
|
||||||
render(<AvatarDropdown user={mockAdminUser} onLogout={mockOnLogout} />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
||||||
expect(screen.queryByText('Admin Interface')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should call onAdmin when admin interface button is clicked', () => {
|
|
||||||
render(
|
|
||||||
<AvatarDropdown
|
|
||||||
user={mockAdminUser}
|
|
||||||
onLogout={mockOnLogout}
|
|
||||||
onAdmin={mockOnAdmin}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
||||||
fireEvent.click(screen.getByText('Admin Interface'));
|
|
||||||
|
|
||||||
|
fireEvent.click(adminButton);
|
||||||
expect(mockOnAdmin).toHaveBeenCalledTimes(1);
|
expect(mockOnAdmin).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should close dropdown after admin interface is clicked', () => {
|
test('should not render Admin Interface button for regular users', () => {
|
||||||
render(
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
||||||
<AvatarDropdown
|
|
||||||
user={mockAdminUser}
|
|
||||||
onLogout={mockOnLogout}
|
|
||||||
onAdmin={mockOnAdmin}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
||||||
fireEvent.click(screen.getByText('Admin Interface'));
|
expect(screen.queryByText('Admin Interface')).not.toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('change password functionality', () => {
|
describe('change password visibility', () => {
|
||||||
test('should display change password button for users with password when onChangePassword provided', () => {
|
test('should show change password option when user has password', () => {
|
||||||
render(
|
renderDropdown({
|
||||||
<AvatarDropdown
|
user: mockUserWithPassword,
|
||||||
user={mockUserWithPassword}
|
onLogout: mockOnLogout,
|
||||||
onLogout={mockOnLogout}
|
onChangePassword: mockOnChangePassword,
|
||||||
onChangePassword={mockOnChangePassword}
|
});
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
||||||
expect(screen.getByText('Change Password')).toBeInTheDocument();
|
expect(screen.getByText('Change Password')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not display change password button for users without password', () => {
|
test('should hide change password option when user has no password', () => {
|
||||||
render(
|
renderDropdown({
|
||||||
<AvatarDropdown
|
user: mockRegularUser,
|
||||||
user={mockRegularUser}
|
onLogout: mockOnLogout,
|
||||||
onLogout={mockOnLogout}
|
onChangePassword: mockOnChangePassword,
|
||||||
onChangePassword={mockOnChangePassword}
|
});
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
||||||
expect(screen.queryByText('Change Password')).not.toBeInTheDocument();
|
expect(screen.queryByText('Change Password')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not display change password button when onChangePassword not provided', () => {
|
test('should call onChangePassword when change password button clicked', () => {
|
||||||
render(
|
renderDropdown({
|
||||||
<AvatarDropdown user={mockUserWithPassword} onLogout={mockOnLogout} />
|
user: mockUserWithPassword,
|
||||||
);
|
onLogout: mockOnLogout,
|
||||||
|
onChangePassword: mockOnChangePassword,
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
||||||
expect(screen.queryByText('Change Password')).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should call onChangePassword when change password button is clicked', () => {
|
|
||||||
render(
|
|
||||||
<AvatarDropdown
|
|
||||||
user={mockUserWithPassword}
|
|
||||||
onLogout={mockOnLogout}
|
|
||||||
onChangePassword={mockOnChangePassword}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
||||||
fireEvent.click(screen.getByText('Change Password'));
|
fireEvent.click(screen.getByText('Change Password'));
|
||||||
|
|
||||||
expect(mockOnChangePassword).toHaveBeenCalledTimes(1);
|
expect(mockOnChangePassword).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('should close dropdown after change password is clicked', () => {
|
describe('keyboard accessibility', () => {
|
||||||
render(
|
test('should toggle dropdown with Enter key', () => {
|
||||||
<AvatarDropdown
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
||||||
user={mockUserWithPassword}
|
|
||||||
onLogout={mockOnLogout}
|
|
||||||
onChangePassword={mockOnChangePassword}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
const button = screen.getByRole('button', { name: /user menu/i });
|
||||||
fireEvent.click(screen.getByText('Change Password'));
|
fireEvent.keyDown(button, { key: 'Enter', code: 'Enter' });
|
||||||
|
fireEvent.keyUp(button, { key: 'Enter', code: 'Enter' });
|
||||||
|
expect(screen.getByText('Signed in as')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not toggle dropdown with unrelated key', () => {
|
||||||
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
||||||
|
|
||||||
|
const button = screen.getByRole('button', { name: /user menu/i });
|
||||||
|
fireEvent.keyDown(button, { key: 'Space', code: 'Space' });
|
||||||
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getInitials function', () => {
|
describe('user initials generation', () => {
|
||||||
test('should return first character uppercase for regular names', () => {
|
test('should handle lowercase usernames', () => {
|
||||||
const userWithLowercase = { ...mockRegularUser, username: 'john doe' };
|
const userWithLowercase = { ...mockRegularUser, username: 'john' };
|
||||||
render(
|
renderDropdown({ user: userWithLowercase, onLogout: mockOnLogout });
|
||||||
<AvatarDropdown user={userWithLowercase} onLogout={mockOnLogout} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /user menu/i });
|
const button = screen.getByRole('button', { name: /user menu/i });
|
||||||
expect(button).toHaveTextContent('J');
|
expect(button).toHaveTextContent('J');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return question mark for empty string', () => {
|
test('should handle empty username gracefully', () => {
|
||||||
const userWithEmptyName = { ...mockRegularUser, username: '' };
|
const userWithEmptyName = { ...mockRegularUser, username: '' };
|
||||||
render(
|
renderDropdown({ user: userWithEmptyName, onLogout: mockOnLogout });
|
||||||
<AvatarDropdown user={userWithEmptyName} onLogout={mockOnLogout} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /user menu/i });
|
const button = screen.getByRole('button', { name: /user menu/i });
|
||||||
expect(button).toHaveTextContent('?');
|
expect(button).toHaveTextContent('?');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle single character names', () => {
|
test('should handle single character username', () => {
|
||||||
const userWithSingleChar = { ...mockRegularUser, username: 'x' };
|
const userWithSingleChar = { ...mockRegularUser, username: 'a' };
|
||||||
render(
|
renderDropdown({ user: userWithSingleChar, onLogout: mockOnLogout });
|
||||||
<AvatarDropdown user={userWithSingleChar} onLogout={mockOnLogout} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /user menu/i });
|
const button = screen.getByRole('button', { name: /user menu/i });
|
||||||
expect(button).toHaveTextContent('X');
|
expect(button).toHaveTextContent('A');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle special characters', () => {
|
test('should handle usernames with special characters', () => {
|
||||||
const userWithSpecialChar = { ...mockRegularUser, username: '@john' };
|
const userWithSpecialChar = { ...mockRegularUser, username: '!john' };
|
||||||
render(
|
renderDropdown({ user: userWithSpecialChar, onLogout: mockOnLogout });
|
||||||
<AvatarDropdown user={userWithSpecialChar} onLogout={mockOnLogout} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /user menu/i });
|
const button = screen.getByRole('button', { name: /user menu/i });
|
||||||
expect(button).toHaveTextContent('@');
|
expect(button).toHaveTextContent('!');
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('accessibility', () => {
|
|
||||||
test('should have proper aria-label for avatar button', () => {
|
|
||||||
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /user menu/i });
|
|
||||||
expect(button).toHaveAttribute('aria-label', 'User menu');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be keyboard accessible', () => {
|
|
||||||
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /user menu/i });
|
|
||||||
button.focus();
|
|
||||||
expect(button).toHaveFocus();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have proper focus styles', () => {
|
|
||||||
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /user menu/i });
|
|
||||||
expect(button).toHaveClass('focus:outline-none', 'focus:ring-2');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('styling and theming', () => {
|
|
||||||
test('should apply dark mode classes', () => {
|
|
||||||
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /user menu/i });
|
|
||||||
expect(button).toHaveClass('dark:bg-slate-700', 'dark:text-slate-300');
|
|
||||||
|
|
||||||
fireEvent.click(button);
|
|
||||||
const dropdown = screen
|
|
||||||
.getByText('Signed in as')
|
|
||||||
.closest('div')?.parentElement;
|
|
||||||
expect(dropdown).toHaveClass(
|
|
||||||
'dark:bg-slate-800',
|
|
||||||
'dark:border-slate-700'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should apply hover styles to menu items', () => {
|
|
||||||
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
||||||
const logoutButton = screen.getByText('Logout');
|
|
||||||
expect(logoutButton).toHaveClass(
|
|
||||||
'hover:bg-slate-100',
|
|
||||||
'dark:hover:bg-slate-700'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('edge cases', () => {
|
|
||||||
test('should handle clicking outside when dropdown is closed', async () => {
|
|
||||||
render(
|
|
||||||
<div>
|
|
||||||
<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />
|
|
||||||
<div data-testid='outside'>Outside element</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const outside = screen.getByTestId('outside');
|
|
||||||
fireEvent.mouseDown(outside);
|
|
||||||
|
|
||||||
// Should not throw any errors
|
|
||||||
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle rapid clicking', () => {
|
|
||||||
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /user menu/i });
|
|
||||||
|
|
||||||
// Rapid clicks - odd number should end up open
|
|
||||||
fireEvent.click(button);
|
|
||||||
fireEvent.click(button);
|
|
||||||
fireEvent.click(button);
|
|
||||||
|
|
||||||
// Should end up open (3 clicks = open)
|
|
||||||
expect(screen.getByText('Signed in as')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should cleanup event listeners on unmount', () => {
|
|
||||||
const removeEventListenerSpy = jest.spyOn(
|
|
||||||
document,
|
|
||||||
'removeEventListener'
|
|
||||||
);
|
|
||||||
|
|
||||||
const { unmount } = render(
|
|
||||||
<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />
|
|
||||||
);
|
|
||||||
|
|
||||||
unmount();
|
|
||||||
|
|
||||||
expect(removeEventListenerSpy).toHaveBeenCalledWith(
|
|
||||||
'mousedown',
|
|
||||||
expect.any(Function)
|
|
||||||
);
|
|
||||||
|
|
||||||
removeEventListenerSpy.mockRestore();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
90
components/auth/__tests__/ResetPasswordPage.test.tsx
Normal file
90
components/auth/__tests__/ResetPasswordPage.test.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import ResetPasswordPage from '../ResetPasswordPage';
|
||||||
|
import { authService } from '../../../services/auth/auth.service';
|
||||||
|
|
||||||
|
jest.mock('../../../services/auth/auth.service', () => ({
|
||||||
|
authService: {
|
||||||
|
resetPassword: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockedAuthService = authService as jest.Mocked<typeof authService>;
|
||||||
|
const mockedResetPassword = mockedAuthService.resetPassword;
|
||||||
|
|
||||||
|
const setLocation = (url: string) => {
|
||||||
|
window.history.replaceState({}, 'Test', url);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ResetPasswordPage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockedResetPassword.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders invalid token state when no token provided', () => {
|
||||||
|
setLocation('http://localhost/reset-password');
|
||||||
|
|
||||||
|
render(React.createElement(ResetPasswordPage));
|
||||||
|
|
||||||
|
expect(screen.getByText('Password Reset Link Invalid')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole('button', { name: /back to sign in/i })
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows validation error when passwords do not match', async () => {
|
||||||
|
setLocation('http://localhost/reset-password?token=abc123');
|
||||||
|
|
||||||
|
render(React.createElement(ResetPasswordPage));
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('New Password'), {
|
||||||
|
target: { value: 'Password1!' },
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByLabelText('Confirm Password'), {
|
||||||
|
target: { value: 'SomethingElse' },
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /update password/i }));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await screen.findByText('Passwords do not match.')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(mockedResetPassword).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('submits password reset and displays success state', async () => {
|
||||||
|
setLocation('http://localhost/reset-password?token=token123');
|
||||||
|
mockedResetPassword.mockResolvedValue({
|
||||||
|
user: {
|
||||||
|
_id: 'user-1',
|
||||||
|
_rev: '1',
|
||||||
|
username: 'Reset User',
|
||||||
|
} as any,
|
||||||
|
message: 'Password reset successfully',
|
||||||
|
});
|
||||||
|
|
||||||
|
render(React.createElement(ResetPasswordPage));
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('New Password'), {
|
||||||
|
target: { value: 'Password1!' },
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByLabelText('Confirm Password'), {
|
||||||
|
target: { value: 'Password1!' },
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /update password/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockedResetPassword).toHaveBeenCalledWith(
|
||||||
|
'token123',
|
||||||
|
'Password1!'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await screen.findByText('Password Updated')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole('button', { name: /go to sign in/i })
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import bcrypt from 'bcryptjs';
|
||||||
import { authService } from '../auth.service';
|
import { authService } from '../auth.service';
|
||||||
import { AccountStatus } from '../auth.constants';
|
import { AccountStatus } from '../auth.constants';
|
||||||
import { AuthenticatedUser } from '../auth.types';
|
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', () => ({
|
jest.mock('../../database', () => ({
|
||||||
databaseService: {
|
databaseService: {
|
||||||
findUserByEmail: jest.fn(),
|
findUserByEmail: jest.fn(),
|
||||||
@@ -14,7 +16,7 @@ jest.mock('../../database', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock the emailVerification service
|
// Mock the email verification service
|
||||||
jest.mock('../emailVerification.service', () => ({
|
jest.mock('../emailVerification.service', () => ({
|
||||||
EmailVerificationService: jest.fn().mockImplementation(() => ({
|
EmailVerificationService: jest.fn().mockImplementation(() => ({
|
||||||
generateVerificationToken: jest.fn().mockResolvedValue({
|
generateVerificationToken: jest.fn().mockResolvedValue({
|
||||||
@@ -38,31 +40,36 @@ describe('Authentication Integration Tests', () => {
|
|||||||
|
|
||||||
let mockUser: AuthenticatedUser;
|
let mockUser: AuthenticatedUser;
|
||||||
let mockDatabaseService: any;
|
let mockDatabaseService: any;
|
||||||
|
let hashedPassword: string;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
|
||||||
// Get the mocked database service
|
|
||||||
const { databaseService } = await import('../../database');
|
const { databaseService } = await import('../../database');
|
||||||
mockDatabaseService = databaseService;
|
mockDatabaseService = databaseService;
|
||||||
|
|
||||||
// Setup default mock user
|
hashedPassword = await bcrypt.hash(testCredentials.password, 10);
|
||||||
|
|
||||||
mockUser = {
|
mockUser = {
|
||||||
_id: 'user1',
|
_id: 'user1',
|
||||||
_rev: 'mock-rev-1',
|
_rev: 'mock-rev-1',
|
||||||
email: testCredentials.email,
|
email: testCredentials.email,
|
||||||
username: testCredentials.username,
|
username: testCredentials.username,
|
||||||
password: testCredentials.password,
|
password: hashedPassword,
|
||||||
emailVerified: false,
|
emailVerified: false,
|
||||||
status: AccountStatus.PENDING,
|
status: AccountStatus.PENDING,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mockDatabaseService.createUserWithPassword.mockResolvedValue(mockUser);
|
||||||
|
mockDatabaseService.updateUser.mockImplementation(
|
||||||
|
async (user: any) => user
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('User Registration', () => {
|
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.findUserByEmail.mockResolvedValue(null);
|
||||||
mockDatabaseService.createUserWithPassword.mockResolvedValue(mockUser);
|
|
||||||
|
|
||||||
const result = await authService.register(
|
const result = await authService.register(
|
||||||
testCredentials.email,
|
testCredentials.email,
|
||||||
@@ -72,10 +79,7 @@ describe('Authentication Integration Tests', () => {
|
|||||||
|
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
expect(result.user.username).toBe(testCredentials.username);
|
expect(result.user.username).toBe(testCredentials.username);
|
||||||
expect(result.user.email).toBe(testCredentials.email);
|
|
||||||
expect(result.user.status).toBe(AccountStatus.PENDING);
|
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(result.verificationToken.token).toBe('mock-verification-token');
|
||||||
|
|
||||||
expect(mockDatabaseService.findUserByEmail).toHaveBeenCalledWith(
|
expect(mockDatabaseService.findUserByEmail).toHaveBeenCalledWith(
|
||||||
@@ -83,9 +87,14 @@ describe('Authentication Integration Tests', () => {
|
|||||||
);
|
);
|
||||||
expect(mockDatabaseService.createUserWithPassword).toHaveBeenCalledWith(
|
expect(mockDatabaseService.createUserWithPassword).toHaveBeenCalledWith(
|
||||||
testCredentials.email,
|
testCredentials.email,
|
||||||
testCredentials.password,
|
expect.any(String),
|
||||||
testCredentials.username
|
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 () => {
|
test('should fail when user already exists', async () => {
|
||||||
@@ -115,7 +124,7 @@ describe('Authentication Integration Tests', () => {
|
|||||||
).rejects.toThrow('Email verification required');
|
).rejects.toThrow('Email verification required');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should succeed after email verification', async () => {
|
test('should succeed with correct bcrypt hashed password', async () => {
|
||||||
const verifiedUser = {
|
const verifiedUser = {
|
||||||
...mockUser,
|
...mockUser,
|
||||||
emailVerified: true,
|
emailVerified: true,
|
||||||
@@ -128,10 +137,8 @@ describe('Authentication Integration Tests', () => {
|
|||||||
password: testCredentials.password,
|
password: testCredentials.password,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(tokens).toBeDefined();
|
|
||||||
expect(tokens.accessToken).toBeTruthy();
|
expect(tokens.accessToken).toBeTruthy();
|
||||||
expect(tokens.refreshToken).toBeTruthy();
|
expect(tokens.refreshToken).toBeTruthy();
|
||||||
expect(tokens.user).toBeDefined();
|
|
||||||
expect(tokens.user.status).toBe(AccountStatus.ACTIVE);
|
expect(tokens.user.status).toBe(AccountStatus.ACTIVE);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,6 +158,23 @@ describe('Authentication Integration Tests', () => {
|
|||||||
).rejects.toThrow('Invalid credentials');
|
).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 () => {
|
test('should fail for non-existent user', async () => {
|
||||||
mockDatabaseService.findUserByEmail.mockResolvedValue(null);
|
mockDatabaseService.findUserByEmail.mockResolvedValue(null);
|
||||||
|
|
||||||
@@ -185,19 +209,9 @@ describe('Authentication Integration Tests', () => {
|
|||||||
|
|
||||||
const result = await authService.loginWithOAuth('google', oauthUserData);
|
const result = await authService.loginWithOAuth('google', oauthUserData);
|
||||||
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.user.email).toBe(oauthUserData.email);
|
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.accessToken).toBeTruthy();
|
||||||
expect(result.refreshToken).toBeTruthy();
|
expect(result.refreshToken).toBeTruthy();
|
||||||
|
|
||||||
expect(mockDatabaseService.createUserFromOAuth).toHaveBeenCalledWith(
|
|
||||||
oauthUserData.email,
|
|
||||||
oauthUserData.username,
|
|
||||||
'google'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should login existing OAuth user', async () => {
|
test('should login existing OAuth user', async () => {
|
||||||
@@ -215,74 +229,70 @@ describe('Authentication Integration Tests', () => {
|
|||||||
|
|
||||||
const result = await authService.loginWithOAuth('google', oauthUserData);
|
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.user._id).toBe('existing-user1');
|
||||||
expect(result.accessToken).toBeTruthy();
|
|
||||||
expect(result.refreshToken).toBeTruthy();
|
|
||||||
|
|
||||||
expect(mockDatabaseService.createUserFromOAuth).not.toHaveBeenCalled();
|
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', () => {
|
describe('Password Management', () => {
|
||||||
test('should change password with valid current password', async () => {
|
test('should change password with valid current password', async () => {
|
||||||
const userId = 'user1';
|
const userId = 'user1';
|
||||||
const currentPassword = 'currentPassword';
|
|
||||||
const newPassword = 'newPassword123';
|
const newPassword = 'newPassword123';
|
||||||
const userWithPassword = {
|
const activeUser = {
|
||||||
...mockUser,
|
...mockUser,
|
||||||
password: currentPassword,
|
emailVerified: true,
|
||||||
};
|
status: AccountStatus.ACTIVE,
|
||||||
const updatedUser = {
|
|
||||||
...userWithPassword,
|
|
||||||
password: newPassword,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mockDatabaseService.getUserById.mockResolvedValue(userWithPassword);
|
mockDatabaseService.getUserById.mockResolvedValue(activeUser);
|
||||||
mockDatabaseService.updateUser.mockResolvedValue(updatedUser);
|
|
||||||
|
|
||||||
const result = await authService.changePassword(
|
const result = await authService.changePassword(
|
||||||
userId,
|
userId,
|
||||||
currentPassword,
|
testCredentials.password,
|
||||||
newPassword
|
newPassword
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toBeDefined();
|
const updatedUser = mockDatabaseService.updateUser.mock.calls[0][0];
|
||||||
expect(result.user.password).toBe(newPassword);
|
expect(isBcryptHash(updatedUser.password)).toBe(true);
|
||||||
|
expect(await bcrypt.compare(newPassword, updatedUser.password)).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
expect(result.message).toBe('Password changed successfully');
|
expect(result.message).toBe('Password changed successfully');
|
||||||
expect(mockDatabaseService.updateUser).toHaveBeenCalledWith({
|
|
||||||
...userWithPassword,
|
|
||||||
password: newPassword,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should fail password change with incorrect current password', async () => {
|
test('should fail password change with incorrect current password', async () => {
|
||||||
const userId = 'user1';
|
const userId = 'user1';
|
||||||
const currentPassword = 'wrongPassword';
|
const hashed = await bcrypt.hash('correctPassword', 10);
|
||||||
const newPassword = 'newPassword123';
|
const activeUser = {
|
||||||
const userWithPassword = {
|
|
||||||
...mockUser,
|
...mockUser,
|
||||||
password: 'correctPassword',
|
emailVerified: true,
|
||||||
|
status: AccountStatus.ACTIVE,
|
||||||
|
password: hashed,
|
||||||
};
|
};
|
||||||
|
|
||||||
mockDatabaseService.getUserById.mockResolvedValue(userWithPassword);
|
mockDatabaseService.getUserById.mockResolvedValue(activeUser);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
authService.changePassword(userId, currentPassword, newPassword)
|
authService.changePassword(userId, 'wrongPassword', 'newPassword123')
|
||||||
).rejects.toThrow('Current password is incorrect');
|
).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 () => {
|
test('should fail password change for OAuth users', async () => {
|
||||||
const userId = 'user1';
|
const userId = 'user1';
|
||||||
const oauthUser = {
|
const oauthUser = {
|
||||||
@@ -302,7 +312,8 @@ describe('Authentication Integration Tests', () => {
|
|||||||
test('should request password reset for existing user', async () => {
|
test('should request password reset for existing user', async () => {
|
||||||
const userWithPassword = {
|
const userWithPassword = {
|
||||||
...mockUser,
|
...mockUser,
|
||||||
password: 'hasPassword',
|
emailVerified: true,
|
||||||
|
status: AccountStatus.ACTIVE,
|
||||||
};
|
};
|
||||||
mockDatabaseService.findUserByEmail.mockResolvedValue(userWithPassword);
|
mockDatabaseService.findUserByEmail.mockResolvedValue(userWithPassword);
|
||||||
|
|
||||||
@@ -310,7 +321,6 @@ describe('Authentication Integration Tests', () => {
|
|||||||
testCredentials.email
|
testCredentials.email
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.message).toContain('password reset link has been sent');
|
expect(result.message).toContain('password reset link has been sent');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -321,7 +331,6 @@ describe('Authentication Integration Tests', () => {
|
|||||||
'nonexistent@example.com'
|
'nonexistent@example.com'
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.message).toContain('password reset link has been sent');
|
expect(result.message).toContain('password reset link has been sent');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ jest.mock('../../../config/unified.config', () => ({
|
|||||||
baseUrl: 'http://localhost:3000',
|
baseUrl: 'http://localhost:3000',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
getAuthConfig: jest.fn(() => ({ bcryptRounds: 4 })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const strategyMocks: Record<string, jest.Mock> = {};
|
||||||
|
|
||||||
// Create mock strategy methods object
|
// Create mock strategy methods object
|
||||||
const mockStrategyMethods = {
|
const mockStrategyMethods = strategyMocks as Record<string, jest.Mock>;
|
||||||
|
Object.assign(mockStrategyMethods, {
|
||||||
createUser: jest.fn(),
|
createUser: jest.fn(),
|
||||||
updateUser: jest.fn(),
|
updateUser: jest.fn(),
|
||||||
getUserById: jest.fn(),
|
getUserById: jest.fn(),
|
||||||
@@ -36,17 +40,15 @@ const mockStrategyMethods = {
|
|||||||
updateCustomReminder: jest.fn(),
|
updateCustomReminder: jest.fn(),
|
||||||
getCustomReminders: jest.fn(),
|
getCustomReminders: jest.fn(),
|
||||||
deleteCustomReminder: jest.fn(),
|
deleteCustomReminder: jest.fn(),
|
||||||
};
|
});
|
||||||
|
|
||||||
// Mock the strategies
|
// Mock the strategies
|
||||||
jest.mock('../MockDatabaseStrategy', () => ({
|
jest.mock('../MockDatabaseStrategy', () => ({
|
||||||
MockDatabaseStrategy: jest.fn().mockImplementation(() => mockStrategyMethods),
|
MockDatabaseStrategy: jest.fn().mockImplementation(() => strategyMocks),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('../ProductionDatabaseStrategy', () => ({
|
jest.mock('../ProductionDatabaseStrategy', () => ({
|
||||||
ProductionDatabaseStrategy: jest
|
ProductionDatabaseStrategy: jest.fn().mockImplementation(() => strategyMocks),
|
||||||
.fn()
|
|
||||||
.mockImplementation(() => mockStrategyMethods),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Import after mocks are set up
|
// Import after mocks are set up
|
||||||
@@ -390,18 +392,19 @@ describe('DatabaseService', () => {
|
|||||||
|
|
||||||
test('should support changeUserPassword method', async () => {
|
test('should support changeUserPassword method', async () => {
|
||||||
const user = createMockUser();
|
const user = createMockUser();
|
||||||
const updatedUser = { ...user, password: 'newPassword' };
|
|
||||||
mockStrategyMethods.getUserById.mockResolvedValue(user);
|
mockStrategyMethods.getUserById.mockResolvedValue(user);
|
||||||
mockStrategyMethods.updateUser.mockResolvedValue(updatedUser);
|
mockStrategyMethods.updateUser.mockImplementation(
|
||||||
|
async updated => updated
|
||||||
|
);
|
||||||
|
|
||||||
const result = await service.changeUserPassword('user1', 'newPassword');
|
const result = await service.changeUserPassword('user1', 'newPassword');
|
||||||
|
|
||||||
expect(mockStrategyMethods.getUserById).toHaveBeenCalledWith('user1');
|
expect(mockStrategyMethods.getUserById).toHaveBeenCalledWith('user1');
|
||||||
expect(mockStrategyMethods.updateUser).toHaveBeenCalledWith({
|
const updateCallArg = mockStrategyMethods.updateUser.mock.calls[0][0];
|
||||||
...user,
|
expect(updateCallArg._id).toBe(user._id);
|
||||||
password: 'newPassword',
|
expect(updateCallArg.password).not.toBe('newPassword');
|
||||||
});
|
expect(updateCallArg.password.startsWith('$2')).toBe(true);
|
||||||
expect(result).toBe(updatedUser);
|
expect(result.password).toBe(updateCallArg.password);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should support deleteAllUserData method', async () => {
|
test('should support deleteAllUserData method', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user