89 lines
3.4 KiB
TypeScript
89 lines
3.4 KiB
TypeScript
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import LoginPage from './page';
|
|
import { AuthProvider } from '../_lib/auth-context';
|
|
import { SuperAdminProvider } from '../_lib/super-admin-context';
|
|
|
|
const pushMock = jest.fn();
|
|
const refreshMock = jest.fn();
|
|
|
|
jest.mock('next/navigation', () => ({
|
|
useRouter: () => ({ push: pushMock, refresh: refreshMock, replace: jest.fn() }),
|
|
useSearchParams: () => new URLSearchParams(),
|
|
}));
|
|
|
|
function authResponse(body: unknown, status = 200) {
|
|
return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
|
}
|
|
|
|
const UNAUTHORIZED = () => Promise.resolve(new Response('Unauthorized', { status: 401 }));
|
|
|
|
function mockFetch(responses: Record<string, (url: string, init?: RequestInit) => Response | Promise<Response>>) {
|
|
fetchSpy.mockImplementation((url: string, init?: RequestInit) => {
|
|
const handler = responses[url];
|
|
if (handler) return handler(url, init);
|
|
return responses['default']?.(url, init) ?? UNAUTHORIZED();
|
|
});
|
|
}
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
fetchSpy = jest.spyOn(global, 'fetch');
|
|
mockFetch({ default: UNAUTHORIZED });
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
function renderWithProvider(ui: React.ReactElement) {
|
|
return render(
|
|
<AuthProvider>
|
|
<SuperAdminProvider>{ui}</SuperAdminProvider>
|
|
</AuthProvider>,
|
|
);
|
|
}
|
|
|
|
describe('LoginPage', () => {
|
|
it('renders email and password fields and a submit button', async () => {
|
|
renderWithProvider(<LoginPage />);
|
|
expect(await screen.findByLabelText(/email/i)).toBeInTheDocument();
|
|
expect(await screen.findByLabelText(/password/i)).toBeInTheDocument();
|
|
expect(await screen.findByRole('button', { name: /sign in/i })).toBeInTheDocument();
|
|
});
|
|
|
|
it('posts credentials to /api/auth/login and redirects on success', async () => {
|
|
mockFetch({
|
|
'/api/auth/login': () => authResponse({ admin: { id: 'a-1', email: 'me@x.com' } }),
|
|
'/api/auth/me': () => authResponse({ admin: { id: 'a-1', email: 'me@x.com', role: 'OWNER', tenantId: 't1', tenantSlug: 'default', tenantName: 'Default' } }),
|
|
});
|
|
renderWithProvider(<LoginPage />);
|
|
await screen.findByLabelText(/email/i);
|
|
fireEvent.change(screen.getByLabelText(/email/i), { target: { value: 'me@x.com' } });
|
|
fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'secret' } });
|
|
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
|
|
await waitFor(() =>
|
|
expect(fetchSpy).toHaveBeenCalledWith(
|
|
'/api/auth/login',
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
body: JSON.stringify({ email: 'me@x.com', password: 'secret' }),
|
|
}),
|
|
),
|
|
);
|
|
expect(pushMock).toHaveBeenCalledWith('/');
|
|
});
|
|
|
|
it('shows an error message on 401', async () => {
|
|
mockFetch({
|
|
'/api/auth/login': () => authResponse({ message: 'Invalid email or password' }, 401),
|
|
});
|
|
renderWithProvider(<LoginPage />);
|
|
await screen.findByLabelText(/email/i);
|
|
fireEvent.change(screen.getByLabelText(/email/i), { target: { value: 'me@x.com' } });
|
|
fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'wrong' } });
|
|
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
|
|
expect(await screen.findByRole('alert')).toHaveTextContent(/invalid email or password/i);
|
|
expect(pushMock).not.toHaveBeenCalled();
|
|
});
|
|
});
|