feat: complete release readiness work
- consolidate API ownership and remove dead vendor code - add Stripe billing, learning paths, and public CV hardening - add migration, recovery, security, audit, and browser gates
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
@@ -20,9 +20,9 @@ const mockedApi = api as jest.Mocked<typeof api>;
|
||||
|
||||
let consoleErrorSpy: jest.SpyInstance;
|
||||
|
||||
function renderLoginPage(initialMode: "login" | "register" = "login") {
|
||||
function renderLoginPage(initialMode: "login" | "register" = "login", from?: string) {
|
||||
return render(
|
||||
<MemoryRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||
<MemoryRouter initialEntries={[{ pathname: "/login", state: from ? { from } : undefined }]} future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||
<I18nProvider>
|
||||
<ToastProvider>
|
||||
<LoginPage initialMode={initialMode} />
|
||||
@@ -32,6 +32,10 @@ function renderLoginPage(initialMode: "login" | "register" = "login") {
|
||||
);
|
||||
}
|
||||
|
||||
async function click(element: Element) {
|
||||
await act(async () => { userEvent.click(element); });
|
||||
}
|
||||
|
||||
describe('LoginPage', () => {
|
||||
it('renders a dedicated registration screen and explains when registration is disabled', async () => {
|
||||
mockedApi.get.mockResolvedValueOnce({
|
||||
@@ -75,8 +79,8 @@ describe('LoginPage', () => {
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Email'), 'person@example.com');
|
||||
await userEvent.type(screen.getByLabelText('Current password'), 'hunter2');
|
||||
await userEvent.click(screen.getByLabelText('Remember me'));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Sign in' }));
|
||||
await click(screen.getByLabelText('Remember me'));
|
||||
await click(screen.getByRole('button', { name: 'Sign in' }));
|
||||
|
||||
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/auth/login', { email: 'person@example.com', password: 'hunter2', rememberMe: false }));
|
||||
await waitFor(() => expect(mockedApi.get).toHaveBeenCalledWith('/auth/me'));
|
||||
@@ -86,12 +90,24 @@ describe('LoginPage', () => {
|
||||
expect(window.localStorage.getItem('authTokenPersistence')).toBe('session');
|
||||
});
|
||||
|
||||
it('rejects an unsafe post-login redirect path', async () => {
|
||||
mockedApi.post.mockResolvedValueOnce({ data: { authenticated: true, provider: 'local' } } as any);
|
||||
mockedApi.get.mockResolvedValueOnce({ data: { roles: [], email: 'person@example.com', userName: 'person' } } as any);
|
||||
|
||||
renderLoginPage("login", "/\\evil.example");
|
||||
await userEvent.type(await screen.findByLabelText('Email'), 'person@example.com');
|
||||
await userEvent.type(screen.getByLabelText('Current password'), 'hunter2');
|
||||
await click(screen.getByRole('button', { name: 'Sign in' }));
|
||||
|
||||
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/dashboard', { replace: true }));
|
||||
});
|
||||
|
||||
it('opens the separate forgot-password page with the typed email prefilled', async () => {
|
||||
renderLoginPage();
|
||||
await screen.findByLabelText('Email');
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Email'), 'person@example.com');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Forgot password?' }));
|
||||
await click(screen.getByRole('button', { name: 'Forgot password?' }));
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/forgot-password?email=person%40example.com');
|
||||
});
|
||||
@@ -114,13 +130,13 @@ describe('LoginPage', () => {
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Email'), 'person@example.com');
|
||||
await userEvent.type(screen.getByLabelText('Current password'), 'hunter2');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Sign in' }));
|
||||
await click(screen.getByRole('button', { name: 'Sign in' }));
|
||||
|
||||
await screen.findByText('Two-factor verification');
|
||||
expect(screen.queryByLabelText('Email')).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Code'), '123456');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Verify' }));
|
||||
await click(screen.getByRole('button', { name: 'Verify' }));
|
||||
|
||||
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/auth/2fa/challenge', { pendingToken: 'pending-abc', code: '123456', trustDevice: false }));
|
||||
await waitFor(() => expect(mockedApi.get).toHaveBeenCalledWith('/auth/me'));
|
||||
@@ -142,11 +158,11 @@ describe('LoginPage', () => {
|
||||
await screen.findByLabelText('Email');
|
||||
await userEvent.type(screen.getByLabelText('Email'), 'person@example.com');
|
||||
await userEvent.type(screen.getByLabelText('Current password'), 'hunter2');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Sign in' }));
|
||||
await click(screen.getByRole('button', { name: 'Sign in' }));
|
||||
|
||||
await screen.findByText('Two-factor verification');
|
||||
await userEvent.type(screen.getByLabelText('Code'), '123456');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Verify' }));
|
||||
await click(screen.getByRole('button', { name: 'Verify' }));
|
||||
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('Too many attempts. Please wait a few minutes and try again.');
|
||||
});
|
||||
@@ -170,11 +186,11 @@ describe('LoginPage', () => {
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Email'), 'unverified@example.com');
|
||||
await userEvent.type(screen.getByLabelText('Current password'), 'hunter2');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Sign in' }));
|
||||
await click(screen.getByRole('button', { name: 'Sign in' }));
|
||||
|
||||
expect(await screen.findByText('Please verify your email address before signing in.')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Resend verification email' }));
|
||||
await click(screen.getByRole('button', { name: 'Resend verification email' }));
|
||||
|
||||
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/auth/resend-verification-email', { email: 'unverified@example.com' }));
|
||||
await screen.findByText('Verification email sent. Check your inbox.');
|
||||
|
||||
Reference in New Issue
Block a user