import React from 'react'; import '@testing-library/jest-dom'; import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import BackupCard from './components/BackupCard'; import { I18nProvider } from './i18n/I18nProvider'; import { ToastProvider } from './toast'; import { api } from './api'; import { PromptProvider } from './prompt'; jest.mock('./api', () => ({ api: { get: jest.fn(), post: jest.fn(), put: jest.fn(), patch: jest.fn(), delete: jest.fn(), interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } }, }, getApiErrorMessage: (error: any, fallback?: string) => error?.response?.data?.detail || fallback || 'Request failed.', })); const mockedApi = api as jest.Mocked; beforeEach(() => { mockedApi.get.mockReset(); mockedApi.post.mockReset(); mockedApi.get.mockResolvedValue({ data: { deletionEnabled: false, deletionStatus: 'active', requiredConfirmation: 'DELETE owner@example.test', request: null } } as any); Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: jest.fn(() => 'blob:account-export') }); Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: jest.fn() }); jest.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined); }); afterEach(() => jest.restoreAllMocks()); test('downloads the readable account ZIP from the protected endpoint', async () => { mockedApi.post.mockResolvedValue({ data: new Blob(['zip']), headers: { 'content-disposition': 'attachment; filename="jobjakt-account-export.zip"' }, } as any); render(); fireEvent.click(screen.getByRole('button', { name: /download readable account export/i })); await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/export/account', null, { responseType: 'blob' })); expect(HTMLAnchorElement.prototype.click).toHaveBeenCalled(); expect(await screen.findByText(/readable account export downloaded/i)).toBeInTheDocument(); }); test('explains the recent-sign-in requirement without weakening it', async () => { mockedApi.post.mockRejectedValue({ response: { status: 403, data: { detail: 'Sign in again before downloading a complete account export.' } } }); render(); expect(screen.getByText(/requires a sign-in from the last 15 minutes/i)).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /download readable account export/i })); expect(await screen.findByText(/sign in again before downloading/i)).toBeInTheDocument(); }); test('keeps account deletion hidden behind the server flag', async () => { render(); expect(await screen.findByText(/account deletion is not enabled yet/i)).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /delete my account/i })).not.toBeInTheDocument(); }); test('requires the exact server-provided deletion phrase', async () => { mockedApi.get.mockResolvedValue({ data: { deletionEnabled: true, deletionStatus: 'active', requiredConfirmation: 'DELETE owner@example.test', request: null } } as any); mockedApi.post.mockRejectedValue({ response: { status: 503, data: { detail: 'Safeguard rehearsal is incomplete.' } } }); render(); fireEvent.click(await screen.findByRole('button', { name: /delete my account/i })); let dialog = await screen.findByRole('dialog'); fireEvent.change(within(dialog).getByRole('textbox'), { target: { value: 'DELETE somebody-else@example.test' } }); fireEvent.click(within(dialog).getByRole('button', { name: /delete my account/i })); expect(await screen.findByText(/confirmation did not match/i)).toBeInTheDocument(); expect(mockedApi.post).not.toHaveBeenCalledWith('/account-lifecycle/delete', expect.anything()); await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()); fireEvent.click(screen.getByRole('button', { name: /delete my account/i })); dialog = await screen.findByRole('dialog'); fireEvent.change(within(dialog).getByRole('textbox'), { target: { value: 'DELETE owner@example.test' } }); fireEvent.click(within(dialog).getByRole('button', { name: /delete my account/i })); await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/account-lifecycle/delete', { confirmation: 'DELETE owner@example.test' })); });