feat(account): add deletion lifecycle
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
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: {
|
||||
@@ -22,7 +23,9 @@ jest.mock('./api', () => ({
|
||||
const mockedApi = api as jest.Mocked<typeof api>;
|
||||
|
||||
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);
|
||||
@@ -35,7 +38,7 @@ test('downloads the readable account ZIP from the protected endpoint', async ()
|
||||
data: new Blob(['zip']),
|
||||
headers: { 'content-disposition': 'attachment; filename="jobjakt-account-export.zip"' },
|
||||
} as any);
|
||||
render(<ToastProvider><I18nProvider><BackupCard /></I18nProvider></ToastProvider>);
|
||||
render(<ToastProvider><I18nProvider><PromptProvider><BackupCard /></PromptProvider></I18nProvider></ToastProvider>);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /download readable account export/i }));
|
||||
|
||||
@@ -46,9 +49,36 @@ test('downloads the readable account ZIP from the protected endpoint', async ()
|
||||
|
||||
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(<ToastProvider><I18nProvider><BackupCard /></I18nProvider></ToastProvider>);
|
||||
render(<ToastProvider><I18nProvider><PromptProvider><BackupCard /></PromptProvider></I18nProvider></ToastProvider>);
|
||||
|
||||
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(<ToastProvider><I18nProvider><PromptProvider><BackupCard /></PromptProvider></I18nProvider></ToastProvider>);
|
||||
|
||||
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(<ToastProvider><I18nProvider><PromptProvider><BackupCard /></PromptProvider></I18nProvider></ToastProvider>);
|
||||
|
||||
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' }));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user