FileManagerFolderDelete.test.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. /**
  2. * Tests for folder deletion permission gating in the File Manager tree (#1781).
  3. *
  4. * Users with only library:delete_own may delete empty, unlinked, non-external
  5. * folders; everything else stays behind library:delete_all.
  6. */
  7. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  8. import { screen, waitFor, within } from '@testing-library/react';
  9. import userEvent from '@testing-library/user-event';
  10. import { http, HttpResponse } from 'msw';
  11. import { render } from '../utils';
  12. import { server } from '../mocks/server';
  13. import { FileManagerPage } from '../../pages/FileManagerPage';
  14. import { setAuthToken } from '../../api/client';
  15. const mockFolders = [
  16. {
  17. id: 1,
  18. name: 'EmptyOne',
  19. parent_id: null,
  20. file_count: 0,
  21. project_id: null,
  22. archive_id: null,
  23. project_name: null,
  24. archive_name: null,
  25. is_external: false,
  26. children: [],
  27. },
  28. {
  29. id: 2,
  30. name: 'HasFiles',
  31. parent_id: null,
  32. file_count: 3,
  33. project_id: null,
  34. archive_id: null,
  35. project_name: null,
  36. archive_name: null,
  37. is_external: false,
  38. children: [],
  39. },
  40. {
  41. id: 3,
  42. name: 'LinkedEmpty',
  43. parent_id: null,
  44. file_count: 0,
  45. project_id: 1,
  46. archive_id: null,
  47. project_name: 'My Project',
  48. archive_name: null,
  49. is_external: false,
  50. children: [],
  51. },
  52. ];
  53. function mockAuthUser(permissions: string[]) {
  54. setAuthToken('test-token', 'session');
  55. server.use(
  56. http.get('*/api/v1/auth/status', () =>
  57. HttpResponse.json({ auth_enabled: true, requires_setup: false }),
  58. ),
  59. http.get('*/api/v1/auth/me', () =>
  60. HttpResponse.json({
  61. id: 7,
  62. username: 'operator1',
  63. is_admin: false,
  64. permissions,
  65. }),
  66. ),
  67. );
  68. }
  69. async function openFolderMenu(user: ReturnType<typeof userEvent.setup>, folderName: string) {
  70. // Walk up to the row itself rather than assuming the name is its direct
  71. // child — the name sits in a wrapper that also holds the optional
  72. // last-activity line (#2680).
  73. const row = screen.getByText(folderName).closest('div.group')!;
  74. const buttons = within(row).getAllByRole('button');
  75. // The kebab (MoreVertical) menu toggle is the last button in the row
  76. await user.click(buttons[buttons.length - 1]);
  77. return row;
  78. }
  79. describe('FileManager folder deletion gating (#1781)', () => {
  80. beforeEach(() => {
  81. localStorage.clear();
  82. server.use(
  83. http.get('/api/v1/library/folders', () => HttpResponse.json(mockFolders)),
  84. http.get('/api/v1/library/files', () => HttpResponse.json([])),
  85. http.get('/api/v1/library/stats', () =>
  86. HttpResponse.json({
  87. total_files: 3,
  88. total_folders: 3,
  89. total_size_bytes: 1024,
  90. disk_free_bytes: 10737418240,
  91. disk_total_bytes: 107374182400,
  92. }),
  93. ),
  94. http.get('/api/v1/projects/', () => HttpResponse.json([{ id: 1, name: 'My Project', color: '#00ae42' }])),
  95. http.get('/api/v1/archives/', () => HttpResponse.json([])),
  96. );
  97. });
  98. afterEach(() => {
  99. setAuthToken(null);
  100. });
  101. it('enables delete on an empty folder for a delete_own user', async () => {
  102. mockAuthUser(['library:read_own', 'library:delete_own']);
  103. render(<FileManagerPage />);
  104. await waitFor(() => expect(screen.getByText('EmptyOne')).toBeInTheDocument());
  105. const user = userEvent.setup();
  106. const row = await openFolderMenu(user, 'EmptyOne');
  107. const deleteButton = within(row).getByRole('button', { name: 'Delete' });
  108. expect(deleteButton).not.toBeDisabled();
  109. });
  110. it('disables delete on a non-empty folder for a delete_own user, with empty-only tooltip', async () => {
  111. mockAuthUser(['library:read_own', 'library:delete_own']);
  112. render(<FileManagerPage />);
  113. await waitFor(() => expect(screen.getByText('HasFiles')).toBeInTheDocument());
  114. const user = userEvent.setup();
  115. const row = await openFolderMenu(user, 'HasFiles');
  116. const deleteButton = within(row).getByRole('button', { name: 'Delete' });
  117. expect(deleteButton).toBeDisabled();
  118. expect(deleteButton).toHaveAttribute('title', 'You can only delete empty folders');
  119. });
  120. it('disables delete on a linked folder for a delete_own user, with no-permission tooltip', async () => {
  121. mockAuthUser(['library:read_own', 'library:delete_own']);
  122. render(<FileManagerPage />);
  123. await waitFor(() => expect(screen.getByText('LinkedEmpty')).toBeInTheDocument());
  124. const user = userEvent.setup();
  125. const row = await openFolderMenu(user, 'LinkedEmpty');
  126. const deleteButton = within(row).getByRole('button', { name: 'Delete' });
  127. expect(deleteButton).toBeDisabled();
  128. expect(deleteButton).toHaveAttribute('title', 'You do not have permission to delete folders');
  129. });
  130. it('disables delete entirely for a user without any delete permission', async () => {
  131. mockAuthUser(['library:read_own']);
  132. render(<FileManagerPage />);
  133. await waitFor(() => expect(screen.getByText('EmptyOne')).toBeInTheDocument());
  134. const user = userEvent.setup();
  135. const row = await openFolderMenu(user, 'EmptyOne');
  136. const deleteButton = within(row).getByRole('button', { name: 'Delete' });
  137. expect(deleteButton).toBeDisabled();
  138. expect(deleteButton).toHaveAttribute('title', 'You do not have permission to delete folders');
  139. });
  140. it('keeps delete enabled on non-empty folders for a delete_all user', async () => {
  141. mockAuthUser(['library:read_all', 'library:delete_all']);
  142. render(<FileManagerPage />);
  143. await waitFor(() => expect(screen.getByText('HasFiles')).toBeInTheDocument());
  144. const user = userEvent.setup();
  145. const row = await openFolderMenu(user, 'HasFiles');
  146. const deleteButton = within(row).getByRole('button', { name: 'Delete' });
  147. expect(deleteButton).not.toBeDisabled();
  148. });
  149. });