/** * Tests for TotpSettings (M4-T08). * * Strategy: vi.mock the auth/totp module so we can control API responses * without a real server. We also mock the api/client module for ApiError. * * Coverage: * 1. Shows loading spinner while fetching TOTP status. * 2. Shows error when TOTP status fetch fails. * 3. Shows "Disabled" badge and enable button when TOTP is disabled. * 4. Enable flow: clicking setup → shows QR code + recovery codes + confirm form. * 5. Confirm with correct code → TOTP enabled (status refetched). * 6. Confirm with wrong code → shows error, stays on confirm form. * 7. Shows "Enabled" badge and disable form when TOTP is enabled. * 8. Disable with password → TOTP disabled. * 9. Disable with TOTP code → TOTP disabled. * 10. Disable without providing password or code → shows validation error. * 11. Setup error shows error message. */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { screen, waitFor, fireEvent } from '@testing-library/react' import { renderWithProviders } from '../test-utils' import { TotpSettings } from './TotpSettings' // --------------------------------------------------------------------------- // Mock auth/totp // --------------------------------------------------------------------------- const mockGetTotpStatus = vi.fn() const mockSetupTotp = vi.fn() const mockEnableTotp = vi.fn() const mockDisableTotp = vi.fn() vi.mock('../auth/totp', () => ({ getTotpStatus: (...args: unknown[]) => mockGetTotpStatus(...args), setupTotp: (...args: unknown[]) => mockSetupTotp(...args), enableTotp: (...args: unknown[]) => mockEnableTotp(...args), disableTotp: (...args: unknown[]) => mockDisableTotp(...args), })) // --------------------------------------------------------------------------- // Mock api/client (for ApiError) // --------------------------------------------------------------------------- vi.mock('../api/client', () => ({ default: { GET: vi.fn(), POST: vi.fn(), PUT: vi.fn(), }, ApiError: class ApiError extends Error { status: number body: unknown constructor(status: number, body: unknown) { super(`API error ${status}`) this.name = 'ApiError' this.status = status this.body = body } }, registerLoginRedirect: vi.fn(), })) // --------------------------------------------------------------------------- // Fixture // --------------------------------------------------------------------------- const MOCK_SETUP_DATA = { secret: 'JBSWY3DPEHPK3PXP', otpauth_uri: 'otpauth://totp/TestApp:admin?secret=JBSWY3DPEHPK3PXP&issuer=TestApp', recovery_codes: ['abcd-1234', 'efgh-5678', 'ijkl-9012'], } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function renderTotp() { return renderWithProviders(, { initialPath: '/config' }) } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('TotpSettings', () => { beforeEach(() => { vi.clearAllMocks() }) // ------------------------------------------------------------------------- // 1. Loading state // ------------------------------------------------------------------------- it('shows loading spinner while fetching TOTP status', async () => { // Never resolves during this test mockGetTotpStatus.mockReturnValue(new Promise(() => {})) renderTotp() await waitFor(() => { expect(screen.getByTestId('totp-status-loading')).toBeInTheDocument() }) }) // ------------------------------------------------------------------------- // 2. Error state // ------------------------------------------------------------------------- it('shows error when TOTP status fetch fails', async () => { mockGetTotpStatus.mockRejectedValue(new Error('Network error')) renderTotp() await waitFor(() => { expect(screen.getByTestId('totp-status-error')).toBeInTheDocument() }) }) // ------------------------------------------------------------------------- // 3. Disabled state // ------------------------------------------------------------------------- it('shows Disabled badge and enable button when TOTP is disabled', async () => { mockGetTotpStatus.mockResolvedValue({ enabled: false }) renderTotp() await waitFor(() => { expect(screen.getByTestId('totp-status-badge')).toBeInTheDocument() }) expect(screen.getByTestId('totp-status-badge')).toHaveTextContent(/disabled/i) expect(screen.getByTestId('totp-setup-button')).toBeInTheDocument() }) // ------------------------------------------------------------------------- // 4. Enable flow: setup shows QR + recovery codes + confirm form // ------------------------------------------------------------------------- it('shows QR code, secret, and recovery codes after clicking setup', async () => { mockGetTotpStatus.mockResolvedValue({ enabled: false }) mockSetupTotp.mockResolvedValue(MOCK_SETUP_DATA) renderTotp() await waitFor(() => { expect(screen.getByTestId('totp-setup-button')).toBeInTheDocument() }) fireEvent.click(screen.getByTestId('totp-setup-button')) await waitFor(() => { expect(screen.getByTestId('totp-qr-code')).toBeInTheDocument() }) expect(screen.getByTestId('totp-secret')).toHaveTextContent('JBSWY3DPEHPK3PXP') expect(screen.getByTestId('totp-recovery-codes')).toBeInTheDocument() expect(screen.getByTestId('totp-recovery-codes')).toHaveTextContent('abcd-1234') expect(screen.getByTestId('totp-confirm-form')).toBeInTheDocument() expect(screen.getByTestId('totp-confirm-input')).toBeInTheDocument() }) // ------------------------------------------------------------------------- // 5. Confirm with correct code → enabled // ------------------------------------------------------------------------- it('enables TOTP after confirming with correct code', async () => { mockGetTotpStatus .mockResolvedValueOnce({ enabled: false }) .mockResolvedValueOnce({ enabled: true }) mockSetupTotp.mockResolvedValue(MOCK_SETUP_DATA) mockEnableTotp.mockResolvedValue(undefined) renderTotp() await waitFor(() => { expect(screen.getByTestId('totp-setup-button')).toBeInTheDocument() }) fireEvent.click(screen.getByTestId('totp-setup-button')) await waitFor(() => { expect(screen.getByTestId('totp-confirm-form')).toBeInTheDocument() }) fireEvent.change(screen.getByTestId('totp-confirm-input'), { target: { value: '123456' } }) fireEvent.submit(screen.getByTestId('totp-confirm-form')) await waitFor(() => { expect(mockEnableTotp).toHaveBeenCalledWith('123456') }) // After enable the status refetches → shows enabled await waitFor(() => { expect(screen.getByTestId('totp-status-badge')).toHaveTextContent(/enabled/i) }) }) // ------------------------------------------------------------------------- // 6. Confirm with wrong code → error // ------------------------------------------------------------------------- it('shows error on wrong TOTP code during enable', async () => { mockGetTotpStatus.mockResolvedValue({ enabled: false }) mockSetupTotp.mockResolvedValue(MOCK_SETUP_DATA) const { ApiError } = await import('../api/client') mockEnableTotp.mockRejectedValue(new ApiError(400, { detail: 'invalid code' })) renderTotp() await waitFor(() => { expect(screen.getByTestId('totp-setup-button')).toBeInTheDocument() }) fireEvent.click(screen.getByTestId('totp-setup-button')) await waitFor(() => { expect(screen.getByTestId('totp-confirm-form')).toBeInTheDocument() }) fireEvent.change(screen.getByTestId('totp-confirm-input'), { target: { value: '000000' } }) fireEvent.submit(screen.getByTestId('totp-confirm-form')) await waitFor(() => { expect(screen.getByTestId('totp-confirm-error')).toBeInTheDocument() }) // Should still show confirm form (not enabled) expect(screen.getByTestId('totp-confirm-form')).toBeInTheDocument() }) // ------------------------------------------------------------------------- // 7. Enabled state // ------------------------------------------------------------------------- it('shows Enabled badge and disable form when TOTP is enabled', async () => { mockGetTotpStatus.mockResolvedValue({ enabled: true }) renderTotp() await waitFor(() => { expect(screen.getByTestId('totp-status-badge')).toBeInTheDocument() }) expect(screen.getByTestId('totp-status-badge')).toHaveTextContent(/enabled/i) expect(screen.getByTestId('totp-disable-form')).toBeInTheDocument() expect(screen.getByTestId('totp-disable-button')).toBeInTheDocument() }) // ------------------------------------------------------------------------- // 8. Disable with password // ------------------------------------------------------------------------- it('disables TOTP when correct password is provided', async () => { mockGetTotpStatus .mockResolvedValueOnce({ enabled: true }) .mockResolvedValueOnce({ enabled: false }) mockDisableTotp.mockResolvedValue(undefined) renderTotp() await waitFor(() => { expect(screen.getByTestId('totp-disable-form')).toBeInTheDocument() }) fireEvent.change(screen.getByTestId('totp-disable-password'), { target: { value: 'my-password' }, }) fireEvent.submit(screen.getByTestId('totp-disable-form')) await waitFor(() => { expect(mockDisableTotp).toHaveBeenCalledWith({ password: 'my-password', code: null }) }) await waitFor(() => { expect(screen.getByTestId('totp-status-badge')).toHaveTextContent(/disabled/i) }) }) // ------------------------------------------------------------------------- // 9. Disable with TOTP code // ------------------------------------------------------------------------- it('disables TOTP when correct TOTP code is provided', async () => { mockGetTotpStatus .mockResolvedValueOnce({ enabled: true }) .mockResolvedValueOnce({ enabled: false }) mockDisableTotp.mockResolvedValue(undefined) renderTotp() await waitFor(() => { expect(screen.getByTestId('totp-disable-form')).toBeInTheDocument() }) fireEvent.change(screen.getByTestId('totp-disable-code'), { target: { value: '654321' } }) fireEvent.submit(screen.getByTestId('totp-disable-form')) await waitFor(() => { expect(mockDisableTotp).toHaveBeenCalledWith({ password: null, code: '654321' }) }) }) // ------------------------------------------------------------------------- // 10. Disable without credentials → validation error // ------------------------------------------------------------------------- it('shows validation error when disable submitted without password or code', async () => { mockGetTotpStatus.mockResolvedValue({ enabled: true }) renderTotp() await waitFor(() => { expect(screen.getByTestId('totp-disable-form')).toBeInTheDocument() }) // Submit without filling in anything fireEvent.submit(screen.getByTestId('totp-disable-form')) await waitFor(() => { expect(screen.getByTestId('totp-disable-error')).toBeInTheDocument() }) expect(mockDisableTotp).not.toHaveBeenCalled() }) // ------------------------------------------------------------------------- // 11. Setup error // ------------------------------------------------------------------------- it('shows error when TOTP setup request fails', async () => { mockGetTotpStatus.mockResolvedValue({ enabled: false }) mockSetupTotp.mockRejectedValue(new Error('Network error')) renderTotp() await waitFor(() => { expect(screen.getByTestId('totp-setup-button')).toBeInTheDocument() }) fireEvent.click(screen.getByTestId('totp-setup-button')) await waitFor(() => { expect(screen.getByTestId('totp-enable-error')).toBeInTheDocument() }) }) })