FinanceDeletedUser.test.tsx 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. /**
  2. * What a deleted user leaves in the finance admin view: ledger entries without
  3. * a user (kept, so a shared cost centre stays spent) and private cost centres
  4. * without an owner. Neither reads as "User #null" or "Personal", which look
  5. * like the viewing admin's own. An entry without a user says "No user": with
  6. * auth off a print never had one, so "Deleted user" would be wrong there. A
  7. * private centre is always created with an owner, so none means deleted.
  8. */
  9. import { describe, it, expect } from 'vitest';
  10. import { screen, waitFor, within } from '@testing-library/react';
  11. import userEvent from '@testing-library/user-event';
  12. import { http, HttpResponse } from 'msw';
  13. import { render } from '../utils';
  14. import { FinancePage } from '../../pages/FinancePage';
  15. import { server } from '../mocks/server';
  16. const orphanedCentre = {
  17. id: 9,
  18. name: 'Alice private',
  19. is_private: true,
  20. owner_user_id: null,
  21. is_active: true,
  22. total_balance: 0,
  23. total_budget: null,
  24. monthly_budget: null,
  25. budget_mode: 'none',
  26. budget_limit: null,
  27. budget_used: null,
  28. budget_available: null,
  29. can_print: true,
  30. };
  31. const orphanedEntry = {
  32. id: 1,
  33. user_id: null,
  34. cost_center_id: null,
  35. transaction_type: 'deposit',
  36. amount: 42,
  37. balance_after: 42,
  38. description: 'Left by a deleted user',
  39. created_by_user_id: null,
  40. print_run_id: null,
  41. print_archive_id: null,
  42. print_queue_id: null,
  43. created_at: '2026-10-11T10:00:00',
  44. };
  45. describe('FinancePage after a user was deleted', () => {
  46. it('labels their ledger entries as having no user and their private cost centre as a deleted user', async () => {
  47. server.use(
  48. http.get('*/api/v1/settings/ui-flags', () => HttpResponse.json({ billing_enabled: true, currency: 'USD' })),
  49. http.get('*/api/v1/finance/me/balance', () =>
  50. HttpResponse.json({ user_id: 1, balance: 0, currency: 'USD', updated_at: null }),
  51. ),
  52. http.get('*/api/v1/finance/me/transactions', () => HttpResponse.json({ items: [], total: 0 })),
  53. http.get('*/api/v1/finance/transactions', () =>
  54. HttpResponse.json({ items: [orphanedEntry], total: 1, limit: 50, offset: 0 }),
  55. ),
  56. http.get('*/api/v1/finance/cost-centers/mine', () => HttpResponse.json([])),
  57. http.get('*/api/v1/finance/cost-centers', () => HttpResponse.json([orphanedCentre])),
  58. http.get('*/api/v1/users/slim', () => HttpResponse.json([])),
  59. );
  60. render(<FinancePage />);
  61. await userEvent.click(await screen.findByText('Admin view'));
  62. const entry = (await screen.findByText('Left by a deleted user')).closest('tr') as HTMLElement;
  63. expect(within(entry).getByText('No user')).toBeInTheDocument();
  64. expect(within(entry).queryByText('Deleted user')).not.toBeInTheDocument();
  65. await waitFor(() => {
  66. const rows = screen
  67. .getAllByText('Alice private')
  68. .map((cell) => cell.closest('tr'))
  69. .filter((row): row is HTMLTableRowElement => row !== null);
  70. expect(rows.some((row) => within(row).queryByText('Deleted user'))).toBe(true);
  71. });
  72. expect(screen.queryByText(/User #null/)).not.toBeInTheDocument();
  73. });
  74. });