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:
William Valentin
2025-10-16 13:16:00 -07:00
parent 7317616032
commit 6a6b48cbc5
4 changed files with 277 additions and 330 deletions

View File

@@ -5,7 +5,6 @@ import AvatarDropdown from '../AvatarDropdown';
import { User, UserRole } from '../../../types';
import { AccountStatus } from '../../../services/auth/auth.constants';
// Mock user data
const mockRegularUser: User = {
_id: '1',
_rev: '1-abc123',
@@ -33,7 +32,19 @@ const mockUserWithAvatar: User = {
const mockUserWithPassword: User = {
...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', () => {
@@ -47,7 +58,7 @@ describe('AvatarDropdown', () => {
describe('rendering', () => {
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 });
expect(button).toBeInTheDocument();
@@ -55,9 +66,7 @@ describe('AvatarDropdown', () => {
});
test('should render avatar image when user has avatar', () => {
render(
<AvatarDropdown user={mockUserWithAvatar} onLogout={mockOnLogout} />
);
renderDropdown({ user: mockUserWithAvatar, onLogout: mockOnLogout });
const avatar = screen.getByAltText('User avatar');
expect(avatar).toBeInTheDocument();
@@ -66,16 +75,14 @@ describe('AvatarDropdown', () => {
test('should render fallback character for empty username', () => {
const userWithEmptyName = { ...mockRegularUser, username: '' };
render(
<AvatarDropdown user={userWithEmptyName} onLogout={mockOnLogout} />
);
renderDropdown({ user: userWithEmptyName, onLogout: mockOnLogout });
const button = screen.getByRole('button', { name: /user menu/i });
expect(button).toHaveTextContent('?');
});
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();
});
@@ -83,7 +90,7 @@ describe('AvatarDropdown', () => {
describe('dropdown functionality', () => {
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 });
fireEvent.click(button);
@@ -93,35 +100,40 @@ describe('AvatarDropdown', () => {
});
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 });
// Open dropdown
fireEvent.click(button);
expect(screen.getByText('Signed in as')).toBeInTheDocument();
// Close dropdown
fireEvent.click(button);
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
});
test('should close dropdown when clicking outside', async () => {
render(
<div>
<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />
<div data-testid='outside'>Outside element</div>
</div>
React.createElement(
'div',
null,
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 outside = screen.getByTestId('outside');
// Open dropdown
fireEvent.click(button);
expect(screen.getByText('Signed in as')).toBeInTheDocument();
// Click outside
fireEvent.mouseDown(outside);
await waitFor(() => {
@@ -132,21 +144,21 @@ describe('AvatarDropdown', () => {
describe('user information display', () => {
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 }));
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
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 }));
expect(screen.getByText('Administrator')).toBeInTheDocument();
});
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 }));
expect(screen.queryByText('Administrator')).not.toBeInTheDocument();
@@ -158,9 +170,7 @@ describe('AvatarDropdown', () => {
username: 'Very Long Username That Should Be Truncated',
};
render(
<AvatarDropdown user={userWithLongName} onLogout={mockOnLogout} />
);
renderDropdown({ user: userWithLongName, onLogout: mockOnLogout });
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
const usernameElement = screen.getByText(
@@ -172,7 +182,7 @@ describe('AvatarDropdown', () => {
describe('logout functionality', () => {
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.getByText('Logout'));
@@ -181,7 +191,7 @@ describe('AvatarDropdown', () => {
});
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.getByText('Logout'));
@@ -190,7 +200,7 @@ describe('AvatarDropdown', () => {
});
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 }));
expect(screen.getByText('Logout')).toBeInTheDocument();
@@ -198,280 +208,115 @@ describe('AvatarDropdown', () => {
});
describe('admin functionality', () => {
test('should display admin interface button for admin users when onAdmin provided', () => {
render(
<AvatarDropdown
user={mockAdminUser}
onLogout={mockOnLogout}
onAdmin={mockOnAdmin}
/>
);
test('should render Admin Interface button for admin users', () => {
renderDropdown({
user: mockAdminUser,
onLogout: mockOnLogout,
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 }));
expect(screen.queryByText('Admin Interface')).not.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'));
const adminButton = screen.getByText('Admin Interface');
expect(adminButton).toBeInTheDocument();
fireEvent.click(adminButton);
expect(mockOnAdmin).toHaveBeenCalledTimes(1);
});
test('should close dropdown after admin interface is clicked', () => {
render(
<AvatarDropdown
user={mockAdminUser}
onLogout={mockOnLogout}
onAdmin={mockOnAdmin}
/>
);
test('should not render Admin Interface button for regular users', () => {
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
fireEvent.click(screen.getByText('Admin Interface'));
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
expect(screen.queryByText('Admin Interface')).not.toBeInTheDocument();
});
});
describe('change password functionality', () => {
test('should display change password button for users with password when onChangePassword provided', () => {
render(
<AvatarDropdown
user={mockUserWithPassword}
onLogout={mockOnLogout}
onChangePassword={mockOnChangePassword}
/>
);
describe('change password visibility', () => {
test('should show change password option when user has password', () => {
renderDropdown({
user: mockUserWithPassword,
onLogout: mockOnLogout,
onChangePassword: mockOnChangePassword,
});
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
expect(screen.getByText('Change Password')).toBeInTheDocument();
});
test('should not display change password button for users without password', () => {
render(
<AvatarDropdown
user={mockRegularUser}
onLogout={mockOnLogout}
onChangePassword={mockOnChangePassword}
/>
);
test('should hide change password option when user has no password', () => {
renderDropdown({
user: mockRegularUser,
onLogout: mockOnLogout,
onChangePassword: mockOnChangePassword,
});
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
expect(screen.queryByText('Change Password')).not.toBeInTheDocument();
});
test('should not display change password button when onChangePassword not provided', () => {
render(
<AvatarDropdown user={mockUserWithPassword} onLogout={mockOnLogout} />
);
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}
/>
);
test('should call onChangePassword when change password button clicked', () => {
renderDropdown({
user: mockUserWithPassword,
onLogout: mockOnLogout,
onChangePassword: mockOnChangePassword,
});
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
fireEvent.click(screen.getByText('Change Password'));
expect(mockOnChangePassword).toHaveBeenCalledTimes(1);
});
});
test('should close dropdown after change password is clicked', () => {
render(
<AvatarDropdown
user={mockUserWithPassword}
onLogout={mockOnLogout}
onChangePassword={mockOnChangePassword}
/>
);
describe('keyboard accessibility', () => {
test('should toggle dropdown with Enter key', () => {
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
fireEvent.click(screen.getByText('Change Password'));
const button = screen.getByRole('button', { name: /user menu/i });
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();
});
});
describe('getInitials function', () => {
test('should return first character uppercase for regular names', () => {
const userWithLowercase = { ...mockRegularUser, username: 'john doe' };
render(
<AvatarDropdown user={userWithLowercase} onLogout={mockOnLogout} />
);
describe('user initials generation', () => {
test('should handle lowercase usernames', () => {
const userWithLowercase = { ...mockRegularUser, username: 'john' };
renderDropdown({ user: userWithLowercase, onLogout: mockOnLogout });
const button = screen.getByRole('button', { name: /user menu/i });
expect(button).toHaveTextContent('J');
});
test('should return question mark for empty string', () => {
test('should handle empty username gracefully', () => {
const userWithEmptyName = { ...mockRegularUser, username: '' };
render(
<AvatarDropdown user={userWithEmptyName} onLogout={mockOnLogout} />
);
renderDropdown({ user: userWithEmptyName, onLogout: mockOnLogout });
const button = screen.getByRole('button', { name: /user menu/i });
expect(button).toHaveTextContent('?');
});
test('should handle single character names', () => {
const userWithSingleChar = { ...mockRegularUser, username: 'x' };
render(
<AvatarDropdown user={userWithSingleChar} onLogout={mockOnLogout} />
);
test('should handle single character username', () => {
const userWithSingleChar = { ...mockRegularUser, username: 'a' };
renderDropdown({ user: userWithSingleChar, onLogout: mockOnLogout });
const button = screen.getByRole('button', { name: /user menu/i });
expect(button).toHaveTextContent('X');
expect(button).toHaveTextContent('A');
});
test('should handle special characters', () => {
const userWithSpecialChar = { ...mockRegularUser, username: '@john' };
render(
<AvatarDropdown user={userWithSpecialChar} onLogout={mockOnLogout} />
);
test('should handle usernames with special characters', () => {
const userWithSpecialChar = { ...mockRegularUser, username: '!john' };
renderDropdown({ user: userWithSpecialChar, onLogout: mockOnLogout });
const button = screen.getByRole('button', { name: /user menu/i });
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();
expect(button).toHaveTextContent('!');
});
});
});

View 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();
});
});