mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-07-09 15:05:59 +00:00
test(client): expand frontend test suite to 69.1% coverage
Add and extend tests across 32 files (+10 595 lines) covering Admin panels (AuditLog, Backup, DevNotifications, GitHub), Collab (Chat, Notes, Panel, Polls), Planner (DayDetailPanel, DayPlanSidebar), Settings (DisplaySettings, Integrations, MapSettings), Files (FileManager, FilesPage), Map, Layout (DemoBanner, InAppNotificationBell), shared pickers (CustomDateTimePicker, CustomTimePicker), Vacay holidays, pages (Dashboard, Login), unit stores (authStore, inAppNotificationStore), API (authUrl, client integration), and i18n. Also updates sonar-project.properties and MSW trip handlers to support the new cases.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { render, screen, waitFor } from '../../tests/helpers/render';
|
||||
import { render, screen, waitFor, fireEvent } from '../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { server } from '../../tests/helpers/msw/server';
|
||||
@@ -243,4 +243,348 @@ describe('LoginPage', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-LOGIN-011: Password change step appears when must_change_password', () => {
|
||||
it('transitions to change password form when login returns must_change_password=true', async () => {
|
||||
server.use(
|
||||
http.post('/api/auth/login', () => {
|
||||
return HttpResponse.json({
|
||||
user: { id: 1, username: 'test', email: 'test@example.com', role: 'user', must_change_password: true },
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com');
|
||||
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123');
|
||||
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('New password')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByPlaceholderText('Confirm new password')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-LOGIN-012: Password change form validates length', () => {
|
||||
it('shows error when new password is shorter than 8 characters', async () => {
|
||||
server.use(
|
||||
http.post('/api/auth/login', () => {
|
||||
return HttpResponse.json({
|
||||
user: { id: 1, username: 'test', email: 'test@example.com', role: 'user', must_change_password: true },
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com');
|
||||
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123');
|
||||
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('New password')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.type(screen.getByPlaceholderText('New password'), 'short');
|
||||
await user.type(screen.getByPlaceholderText('Confirm new password'), 'short');
|
||||
await user.click(screen.getByRole('button', { name: /update password/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/at least 8/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-LOGIN-013: Password change form validates mismatch', () => {
|
||||
it('shows error when new passwords do not match', async () => {
|
||||
server.use(
|
||||
http.post('/api/auth/login', () => {
|
||||
return HttpResponse.json({
|
||||
user: { id: 1, username: 'test', email: 'test@example.com', role: 'user', must_change_password: true },
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com');
|
||||
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123');
|
||||
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('New password')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.type(screen.getByPlaceholderText('New password'), 'newpassword123');
|
||||
await user.type(screen.getByPlaceholderText('Confirm new password'), 'differentpassword123');
|
||||
await user.click(screen.getByRole('button', { name: /update password/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/do not match/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-LOGIN-014: Password change success navigates', () => {
|
||||
it('shows takeoff overlay after successful password change', async () => {
|
||||
server.use(
|
||||
http.post('/api/auth/login', () => {
|
||||
return HttpResponse.json({
|
||||
user: { id: 1, username: 'test', email: 'test@example.com', role: 'user', must_change_password: true },
|
||||
});
|
||||
}),
|
||||
http.put('/api/auth/me/password', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com');
|
||||
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123');
|
||||
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('New password')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.type(screen.getByPlaceholderText('New password'), 'newpassword123');
|
||||
await user.type(screen.getByPlaceholderText('Confirm new password'), 'newpassword123');
|
||||
await user.click(screen.getByRole('button', { name: /update password/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.takeoff-overlay')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-LOGIN-015: First-setup mode switches to register when has_users=false', () => {
|
||||
it('shows register form automatically when has_users is false', async () => {
|
||||
server.use(
|
||||
http.get('/api/auth/app-config', () => {
|
||||
return HttpResponse.json({
|
||||
has_users: false,
|
||||
allow_registration: true,
|
||||
demo_mode: false,
|
||||
oidc_configured: false,
|
||||
oidc_only_mode: false,
|
||||
setup_complete: true,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('admin')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-LOGIN-016: Registration disabled hides register option', () => {
|
||||
it('does not show register button when allow_registration is false', async () => {
|
||||
server.use(
|
||||
http.get('/api/auth/app-config', () => {
|
||||
return HttpResponse.json({
|
||||
has_users: true,
|
||||
allow_registration: false,
|
||||
demo_mode: false,
|
||||
oidc_configured: false,
|
||||
oidc_only_mode: false,
|
||||
setup_complete: true,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByRole('button', { name: /^register$/i })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-LOGIN-017: OIDC-only mode hides standard login form', () => {
|
||||
it('does not render email/password inputs in oidc_only_mode', async () => {
|
||||
server.use(
|
||||
http.get('/api/auth/app-config', () => {
|
||||
return HttpResponse.json({
|
||||
has_users: true,
|
||||
allow_registration: false,
|
||||
demo_mode: false,
|
||||
oidc_configured: true,
|
||||
oidc_only_mode: true,
|
||||
setup_complete: true,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Pass noRedirect via location.state to prevent window.location.href redirect
|
||||
render(<LoginPage />, {
|
||||
initialEntries: [{ pathname: '/login', state: { noRedirect: true } }],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByPlaceholderText(EMAIL_PLACEHOLDER)).toBeNull();
|
||||
expect(screen.queryByPlaceholderText(PASSWORD_PLACEHOLDER)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-LOGIN-018: MFA code submission completes login', () => {
|
||||
it('shows takeoff overlay after successful MFA verification', async () => {
|
||||
server.use(
|
||||
http.post('/api/auth/login', () => {
|
||||
return HttpResponse.json({
|
||||
mfa_required: true,
|
||||
mfa_token: 'test-mfa-token-abc',
|
||||
});
|
||||
}),
|
||||
http.post('/api/auth/mfa/verify-login', () => {
|
||||
return HttpResponse.json({
|
||||
user: { id: 1, username: 'test', email: 'test@example.com', role: 'user' },
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com');
|
||||
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123');
|
||||
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('000000 or XXXX-XXXX')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.type(screen.getByPlaceholderText('000000 or XXXX-XXXX'), '123456');
|
||||
await user.click(screen.getByRole('button', { name: /verify/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.takeoff-overlay')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-LOGIN-019: Empty MFA code shows error', () => {
|
||||
it('shows error when MFA code is empty and does not show takeoff overlay', async () => {
|
||||
server.use(
|
||||
http.post('/api/auth/login', () => {
|
||||
return HttpResponse.json({
|
||||
mfa_required: true,
|
||||
mfa_token: 'test-mfa-token-abc',
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com');
|
||||
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123');
|
||||
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('000000 or XXXX-XXXX')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Submit the form directly (bypasses browser constraint validation on required field)
|
||||
const form = document.querySelector('form')!;
|
||||
fireEvent.submit(form);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/enter the code from your authenticator/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(document.querySelector('.takeoff-overlay')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-LOGIN-020: Register form validates password length', () => {
|
||||
it('shows error when registration password is shorter than 8 characters', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /^register$/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /^register$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('admin')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.type(screen.getByPlaceholderText('admin'), 'newuser');
|
||||
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'new@example.com');
|
||||
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'short');
|
||||
await user.click(screen.getByRole('button', { name: /create account/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/at least 8/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-LOGIN-021: Invite token pre-fills register mode', () => {
|
||||
it('renders register form when invite query param is present', async () => {
|
||||
server.use(
|
||||
http.get('/api/auth/invite/:token', () => {
|
||||
return HttpResponse.json({ valid: true });
|
||||
}),
|
||||
);
|
||||
|
||||
// Simulate ?invite=abc123 by replacing window.location.search
|
||||
const originalSearch = window.location.search;
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: { ...window.location, search: '?invite=abc123' },
|
||||
});
|
||||
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('admin')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: { ...window.location, search: originalSearch },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user