feat(auth): add 2FA setup UI and login challenge step
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
@@ -81,4 +82,59 @@ describe('LoginPage', () => {
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/forgot-password?email=person%40example.com');
|
||||
});
|
||||
|
||||
it('shows the 2FA code step when login requires two-factor, then proceeds like a normal login on success', async () => {
|
||||
mockedApi.post.mockImplementation((url: string, payload?: any) => {
|
||||
if (url === '/auth/login') {
|
||||
return Promise.resolve({ data: { requiresTwoFactor: true, pendingToken: 'pending-abc' } } as any);
|
||||
}
|
||||
if (url === '/auth/2fa/challenge') {
|
||||
expect(payload).toEqual({ pendingToken: 'pending-abc', code: '123456' });
|
||||
return Promise.resolve({ data: { authenticated: true, provider: 'local' } } as any);
|
||||
}
|
||||
return Promise.resolve({ data: {} } as any);
|
||||
});
|
||||
mockedApi.get.mockResolvedValueOnce({ data: { roles: [], email: 'person@example.com', userName: 'person' } } as any);
|
||||
|
||||
renderLoginPage();
|
||||
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 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 waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/auth/2fa/challenge', { pendingToken: 'pending-abc', code: '123456' }));
|
||||
await waitFor(() => expect(mockedApi.get).toHaveBeenCalledWith('/auth/me'));
|
||||
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/dashboard', { replace: true }));
|
||||
});
|
||||
|
||||
it('shows a clear message when the 2FA challenge is rate-limited', async () => {
|
||||
mockedApi.post.mockImplementation((url: string) => {
|
||||
if (url === '/auth/login') {
|
||||
return Promise.resolve({ data: { requiresTwoFactor: true, pendingToken: 'pending-abc' } } as any);
|
||||
}
|
||||
if (url === '/auth/2fa/challenge') {
|
||||
return Promise.reject({ response: { status: 429 } });
|
||||
}
|
||||
return Promise.resolve({ data: {} } as any);
|
||||
});
|
||||
|
||||
renderLoginPage();
|
||||
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 screen.findByText('Two-factor verification');
|
||||
await userEvent.type(screen.getByLabelText('Code'), '123456');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Verify' }));
|
||||
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('Too many attempts. Please wait a few minutes and try again.');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user