96 lines
4.0 KiB
TypeScript
96 lines
4.0 KiB
TypeScript
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 { ToastProvider } from './toast';
|
|
import { I18nProvider } from './i18n/I18nProvider';
|
|
import TwoFactorSettingsCard from './components/TwoFactorSettingsCard';
|
|
import { api } from './api';
|
|
|
|
const mockedApi = api as jest.Mocked<typeof api>;
|
|
|
|
const writeTextMock = jest.fn(() => Promise.resolve());
|
|
Object.assign(navigator, { clipboard: { writeText: writeTextMock } });
|
|
Object.defineProperty(window.URL, 'createObjectURL', { writable: true, value: jest.fn(() => 'blob:mock') });
|
|
Object.defineProperty(window.URL, 'revokeObjectURL', { writable: true, value: jest.fn() });
|
|
|
|
function renderCard() {
|
|
return render(
|
|
<ToastProvider>
|
|
<I18nProvider>
|
|
<TwoFactorSettingsCard />
|
|
</I18nProvider>
|
|
</ToastProvider>,
|
|
);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
mockedApi.get.mockImplementation((url: string) => {
|
|
if (url === '/auth/2fa/status') {
|
|
return Promise.resolve({ data: { enabled: false, enabledAtUtc: null } } as any);
|
|
}
|
|
return Promise.resolve({ data: {} } as any);
|
|
});
|
|
});
|
|
|
|
test('shows not-enabled status and walks through the full enable flow to recovery codes', async () => {
|
|
mockedApi.post.mockImplementation((url: string) => {
|
|
if (url === '/auth/2fa/setup') {
|
|
return Promise.resolve({ data: { manualEntryKey: 'ABCD1234', qrCodeDataUrl: 'data:image/png;base64,abc' } } as any);
|
|
}
|
|
if (url === '/auth/2fa/verify-setup') {
|
|
return Promise.resolve({ data: { enabled: true, recoveryCodes: ['aaaaa-11111', 'bbbbb-22222'] } } as any);
|
|
}
|
|
return Promise.resolve({ data: {} } as any);
|
|
});
|
|
|
|
renderCard();
|
|
expect(await screen.findByText('Not enabled')).toBeInTheDocument();
|
|
|
|
await userEvent.click(screen.getByRole('button', { name: 'Enable 2FA' }));
|
|
await userEvent.type(await screen.findByLabelText('Current password'), 'hunter2');
|
|
await userEvent.click(screen.getByRole('button', { name: 'Continue' }));
|
|
|
|
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/auth/2fa/setup', { currentPassword: 'hunter2' }));
|
|
expect(await screen.findByAltText('Scan this QR code')).toHaveAttribute('src', 'data:image/png;base64,abc');
|
|
expect(screen.getByDisplayValue('ABCD1234')).toBeInTheDocument();
|
|
|
|
await userEvent.type(screen.getByLabelText('6-digit code'), '654321');
|
|
await userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
|
|
|
|
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/auth/2fa/verify-setup', { code: '654321' }));
|
|
expect(await screen.findByText('Save your recovery codes')).toBeInTheDocument();
|
|
expect(screen.getByText('aaaaa-11111')).toBeInTheDocument();
|
|
expect(screen.getByText('bbbbb-22222')).toBeInTheDocument();
|
|
|
|
const doneButton = screen.getByRole('button', { name: 'Done' });
|
|
expect(doneButton).toBeDisabled();
|
|
|
|
await userEvent.click(screen.getByLabelText("I've saved my recovery codes"));
|
|
expect(doneButton).toBeEnabled();
|
|
await userEvent.click(doneButton);
|
|
|
|
await waitFor(() => expect(screen.queryByText('Save your recovery codes')).not.toBeInTheDocument());
|
|
});
|
|
|
|
test('shows wrong-password error on disable and lets the user retry', async () => {
|
|
mockedApi.get.mockImplementation((url: string) => {
|
|
if (url === '/auth/2fa/status') {
|
|
return Promise.resolve({ data: { enabled: true, enabledAtUtc: '2026-01-01T00:00:00Z' } } as any);
|
|
}
|
|
return Promise.resolve({ data: {} } as any);
|
|
});
|
|
mockedApi.post.mockRejectedValueOnce({ response: { status: 401 } });
|
|
|
|
renderCard();
|
|
expect(await screen.findByText(/enabled since/i)).toBeInTheDocument();
|
|
|
|
await userEvent.click(screen.getByRole('button', { name: 'Disable 2FA' }));
|
|
await userEvent.type(await screen.findByLabelText('Current password'), 'wrong');
|
|
await userEvent.click(screen.getByRole('button', { name: 'Continue' }));
|
|
|
|
expect(await screen.findByText('Incorrect password.')).toBeInTheDocument();
|
|
expect(screen.getByLabelText('Current password')).toBeInTheDocument();
|
|
});
|