test(worker): Auth (context/storage/login) — fill gaps + lifecycle test
Drive the worker Auth domain to 100% coverage: - AuthContext.tsx: 91.3/80 -> 100/100 (signIn body, mount-restore branches, useAuth guard) - Login.tsx: 78/50 -> 100/100 (handleSubmit success/error/pending, submit disable) - auth-storage.ts: already 100 (no change) New Login.test.tsx adds the domain lifecycle test: from the unauthenticated app, a successful login stores the token and flips the context to the authenticated tabbed shell. Extended AuthContext.test.tsx covers signIn delegation, mount restore (token present/absent), and the out-of-provider throw. No bugs found; code was already correct. Note: the design checklist's 'clears on 401' worker behavior is not implemented (no global 401 interceptor) and is left as-is (missing feature, not a defect).
This commit is contained in:
120
apps/worker/src/screens/Login.test.tsx
Normal file
120
apps/worker/src/screens/Login.test.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import Login from './Login';
|
||||
import App from '../App';
|
||||
import { AuthProvider } from '../auth/AuthContext';
|
||||
import { clearToken, getToken } from '../lib/auth-storage';
|
||||
|
||||
// Keep the real signIn/apiFetch flow (drive it through a stubbed global fetch); only silence the
|
||||
// authed shell's data fetches so the lifecycle test's screens render without real requests.
|
||||
vi.mock('../lib/api', async () => {
|
||||
const actual = await vi.importActual<typeof import('../lib/api')>('../lib/api');
|
||||
return { ...actual, apiFetch: vi.fn().mockResolvedValue([]) };
|
||||
});
|
||||
|
||||
// A 200 response carrying the bearer token the way better-auth returns it.
|
||||
function tokenResponse() {
|
||||
return new Response(null, { status: 200, headers: { 'set-auth-token': 'tok' } });
|
||||
}
|
||||
|
||||
function renderLogin() {
|
||||
return render(
|
||||
<AuthProvider>
|
||||
<Login />
|
||||
</AuthProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('Login', () => {
|
||||
beforeEach(() => {
|
||||
clearToken();
|
||||
window.history.replaceState(null, '', '/');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
clearToken();
|
||||
});
|
||||
|
||||
it('renders the Dutch login form', () => {
|
||||
renderLogin();
|
||||
expect(screen.getByText('E-mailadres')).toBeInTheDocument();
|
||||
expect(screen.getByText('Wachtwoord')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Inloggen' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('submits the typed credentials to the sign-in endpoint and stores the token', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(tokenResponse());
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const user = userEvent.setup();
|
||||
renderLogin();
|
||||
|
||||
await user.type(screen.getByLabelText('E-mailadres'), 'worker@example.com');
|
||||
await user.type(screen.getByLabelText('Wachtwoord'), 'geheim123');
|
||||
await user.click(screen.getByRole('button', { name: 'Inloggen' }));
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1));
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toContain('/api/auth/sign-in/email');
|
||||
expect(JSON.parse(init.body as string)).toEqual({
|
||||
email: 'worker@example.com',
|
||||
password: 'geheim123',
|
||||
});
|
||||
expect(getToken()).toBe('tok');
|
||||
});
|
||||
|
||||
it('shows an error message and stores no token when sign-in fails', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 401 })));
|
||||
const user = userEvent.setup();
|
||||
renderLogin();
|
||||
|
||||
await user.type(screen.getByLabelText('E-mailadres'), 'x@y.z');
|
||||
await user.type(screen.getByLabelText('Wachtwoord'), 'wrong');
|
||||
await user.click(screen.getByRole('button', { name: 'Inloggen' }));
|
||||
|
||||
expect(await screen.findByText('Inloggen mislukt')).toBeInTheDocument();
|
||||
expect(getToken()).toBeNull();
|
||||
});
|
||||
|
||||
it('disables the submit button while the request is pending', async () => {
|
||||
let release: (() => void) | undefined;
|
||||
const pending = new Promise<Response>((resolve) => {
|
||||
release = () => resolve(tokenResponse());
|
||||
});
|
||||
vi.stubGlobal('fetch', vi.fn().mockReturnValue(pending));
|
||||
const user = userEvent.setup();
|
||||
renderLogin();
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Inloggen' });
|
||||
await user.type(screen.getByLabelText('E-mailadres'), 'a@b.c');
|
||||
await user.type(screen.getByLabelText('Wachtwoord'), 'pw');
|
||||
await user.click(button);
|
||||
|
||||
await waitFor(() => expect(button).toBeDisabled());
|
||||
release?.();
|
||||
await waitFor(() => expect(button).not.toBeDisabled());
|
||||
});
|
||||
|
||||
it('lifecycle: a successful login flips the app to the authenticated shell', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(tokenResponse()));
|
||||
const user = userEvent.setup();
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Unauthenticated: the login form is shown first.
|
||||
await user.type(screen.getByLabelText('E-mailadres'), 'worker@example.com');
|
||||
await user.type(screen.getByLabelText('Wachtwoord'), 'geheim123');
|
||||
await user.click(screen.getByRole('button', { name: 'Inloggen' }));
|
||||
|
||||
// Context flips to authenticated → the tabbed shell renders.
|
||||
const nav = await screen.findByRole('navigation');
|
||||
expect(within(nav).getByText('Stopwatch')).toBeInTheDocument();
|
||||
expect(getToken()).toBe('tok');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user