Files
ToBePlayed-Frontend/src/components/DeleteAccountCard.test.jsx
T
lobo 13b0d5e7e9 feat(admin): add user role dialog
Admins change a user's role and see the change log. The last admin
gets a clear error when deleting their own account.
2026-10-04 20:09:22 +02:00

125 lines
4.8 KiB
React

// @vitest-environment jsdom
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {cleanup, fireEvent, render, screen, waitFor} from '@testing-library/react';
import {createMemoryRouter, RouterProvider} from 'react-router-dom';
import RequireSession from './RequireSession.jsx';
import {toast} from 'sonner';
import DeleteAccountCard from './DeleteAccountCard.jsx';
import {fetchRoutes, json} from '../test/fetchRoutes.js';
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
const EMAIL = 'player@example.com';
const deleteAnswers = (answer, routes = {}) => fetchRoutes({'DELETE /api/users/me': answer, ...routes});
// A data router like main.jsx, without the providers: their /me load would change the request order.
const renderCard = () => render(<RouterProvider router={createMemoryRouter([
{element: <RequireSession/>, children: [{path: '/settings', element: <DeleteAccountCard email={EMAIL}/>}]},
{path: '/', element: <p>landing</p>},
{path: '/login', element: <p>login</p>},
], {initialEntries: ['/settings']})}/>);
const openAndFill = (email, password) => {
fireEvent.click(screen.getByRole('button', {name: 'SETTINGS.DELETE_ACCOUNT.BUTTON'}));
fireEvent.change(screen.getByLabelText('SETTINGS.DELETE_ACCOUNT.TYPE_EMAIL'), {target: {value: email}});
fireEvent.change(screen.getByLabelText('SETTINGS.DELETE_ACCOUNT.PASSWORD'), {target: {value: password}});
};
const confirmButton = () => screen.getByRole('button', {name: 'SETTINGS.DELETE_ACCOUNT.CONFIRM'});
describe('DeleteAccountCard', () => {
beforeEach(() => {
localStorage.setItem('tbp_session', '1');
deleteAnswers(json(204, {}));
});
afterEach(() => {
cleanup();
localStorage.clear();
vi.clearAllMocks();
});
it('asks for the own email and a password before it deletes', () => {
renderCard();
openAndFill('someone@else.com', 'secret');
expect(confirmButton().disabled).toBe(true);
fireEvent.change(screen.getByLabelText('SETTINGS.DELETE_ACCOUNT.TYPE_EMAIL'), {target: {value: ' Player@Example.com '}});
expect(confirmButton().disabled).toBe(false);
});
it('sends the password, ends the session and goes to the landing page', async () => {
renderCard();
openAndFill(EMAIL, 'secret');
fireEvent.click(confirmButton());
expect(await screen.findByText('landing')).toBeTruthy();
const [url, init] = fetch.mock.calls[0];
expect(url).toMatch(/\/api\/users\/me$/);
expect(init.method).toBe('DELETE');
expect(JSON.parse(init.body)).toEqual({password: 'secret'});
expect(localStorage.getItem('tbp_session')).toBeNull();
expect(toast.success).toHaveBeenCalledWith('SETTINGS.DELETE_ACCOUNT.DONE');
});
it('keeps the dialog open on a wrong password', async () => {
deleteAnswers(json(403, {}));
renderCard();
openAndFill(EMAIL, 'wrong');
fireEvent.click(confirmButton());
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('SETTINGS.DELETE_ACCOUNT.WRONG_PASSWORD'));
expect(confirmButton()).toBeTruthy();
expect(localStorage.getItem('tbp_session')).toBe('1');
});
it('tells the last admin to make another admin first and stays on the page', async () => {
deleteAnswers(json(409, {code: 'LAST_ADMIN'}));
renderCard();
openAndFill(EMAIL, 'secret');
fireEvent.click(confirmButton());
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('SETTINGS.DELETE_ACCOUNT.LAST_ADMIN'));
expect(confirmButton()).toBeTruthy();
expect(localStorage.getItem('tbp_session')).toBe('1');
});
it('shows the lock message after too many wrong passwords', async () => {
deleteAnswers({...json(429, {}), headers: new Headers({'Retry-After': '900'})});
renderCard();
openAndFill(EMAIL, 'wrong');
fireEvent.click(confirmButton());
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('SETTINGS.DELETE_ACCOUNT.TOO_MANY'));
});
it('opens the login page when the session is gone', async () => {
deleteAnswers(json(401, {}), {'POST /api/auth/refresh': json(401, {})});
renderCard();
openAndFill(EMAIL, 'secret');
fireEvent.click(confirmButton());
expect(await screen.findByText('login')).toBeTruthy();
});
it('shows the network error when Auth does not answer', async () => {
deleteAnswers(() => {
throw new TypeError('Failed to fetch');
});
renderCard();
openAndFill(EMAIL, 'secret');
fireEvent.click(confirmButton());
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('INFO.ERROR.NETWORK_FAILURE'));
});
});