@jmruthers/pace-core 0.5.114 → 0.5.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/dist/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
  2. package/dist/{DataTable-3JRLZXER.js → DataTable-ZOAKQ3SU.js} +10 -9
  3. package/dist/{UnifiedAuthProvider-KZZUO27W.js → UnifiedAuthProvider-YFN7YGVN.js} +4 -3
  4. package/dist/{api-PKU4PUBO.js → api-TNIBJWLM.js} +3 -3
  5. package/dist/{audit-H4YJJF7R.js → audit-T36HM7IM.js} +2 -2
  6. package/dist/{chunk-4OX5PXHX.js → chunk-2GJ5GL77.js} +4 -5
  7. package/dist/chunk-2GJ5GL77.js.map +1 -0
  8. package/dist/{chunk-5YIZFEUQ.js → chunk-2LM4QQGH.js} +31 -35
  9. package/dist/chunk-2LM4QQGH.js.map +1 -0
  10. package/dist/{chunk-3OGQLOJM.js → chunk-3DBFLLLU.js} +30 -1
  11. package/dist/chunk-3DBFLLLU.js.map +1 -0
  12. package/dist/{chunk-KTHLNIMA.js → chunk-ECOVPXYS.js} +13 -62
  13. package/dist/chunk-ECOVPXYS.js.map +1 -0
  14. package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
  15. package/dist/chunk-KA3PSVNV.js.map +1 -0
  16. package/dist/{chunk-HKWQN44G.js → chunk-KMPWND3F.js} +15 -15
  17. package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
  18. package/dist/{chunk-NEONKMTU.js → chunk-LZYHAL7Y.js} +9 -4
  19. package/dist/{chunk-NEONKMTU.js.map → chunk-LZYHAL7Y.js.map} +1 -1
  20. package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
  21. package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
  22. package/dist/chunk-P3PUOL6B.js.map +1 -0
  23. package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
  24. package/dist/chunk-PHDAXDHB.js.map +1 -0
  25. package/dist/chunk-UJI6WSMD.js +201 -0
  26. package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
  27. package/dist/{chunk-JHWQNJP3.js → chunk-UKZWNQMB.js} +65 -19
  28. package/dist/{chunk-JHWQNJP3.js.map → chunk-UKZWNQMB.js.map} +1 -1
  29. package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
  30. package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
  31. package/dist/components.d.ts +1 -1
  32. package/dist/components.js +12 -11
  33. package/dist/components.js.map +1 -1
  34. package/dist/hooks.d.ts +1 -1
  35. package/dist/hooks.js +10 -9
  36. package/dist/hooks.js.map +1 -1
  37. package/dist/index.d.ts +4 -4
  38. package/dist/index.js +19 -16
  39. package/dist/index.js.map +1 -1
  40. package/dist/providers.d.ts +2 -2
  41. package/dist/providers.js +3 -2
  42. package/dist/rbac/index.d.ts +82 -1
  43. package/dist/rbac/index.js +13 -10
  44. package/dist/{useToast-DRah6K-g.d.ts → useToast-Cs_g32bg.d.ts} +8 -6
  45. package/dist/utils.js +6 -4
  46. package/dist/utils.js.map +1 -1
  47. package/dist/validation.js +3 -1
  48. package/dist/validation.js.map +1 -1
  49. package/docs/README.md +4 -0
  50. package/docs/api/classes/ColumnFactory.md +1 -1
  51. package/docs/api/classes/ErrorBoundary.md +1 -1
  52. package/docs/api/classes/InvalidScopeError.md +1 -1
  53. package/docs/api/classes/MissingUserContextError.md +1 -1
  54. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  55. package/docs/api/classes/PermissionDeniedError.md +1 -1
  56. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  57. package/docs/api/classes/RBACAuditManager.md +35 -12
  58. package/docs/api/classes/RBACCache.md +1 -1
  59. package/docs/api/classes/RBACEngine.md +1 -1
  60. package/docs/api/classes/RBACError.md +1 -1
  61. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  62. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  63. package/docs/api/classes/StorageUtils.md +1 -1
  64. package/docs/api/enums/FileCategory.md +1 -1
  65. package/docs/api/interfaces/AggregateConfig.md +1 -1
  66. package/docs/api/interfaces/ButtonProps.md +1 -1
  67. package/docs/api/interfaces/CardProps.md +1 -1
  68. package/docs/api/interfaces/ColorPalette.md +1 -1
  69. package/docs/api/interfaces/ColorShade.md +1 -1
  70. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  71. package/docs/api/interfaces/DataRecord.md +1 -1
  72. package/docs/api/interfaces/DataTableAction.md +1 -1
  73. package/docs/api/interfaces/DataTableColumn.md +1 -1
  74. package/docs/api/interfaces/DataTableProps.md +1 -1
  75. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  76. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  77. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  78. package/docs/api/interfaces/EventAppRoleData.md +71 -0
  79. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  80. package/docs/api/interfaces/FileMetadata.md +1 -1
  81. package/docs/api/interfaces/FileReference.md +1 -1
  82. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  83. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  84. package/docs/api/interfaces/FileUploadProps.md +1 -1
  85. package/docs/api/interfaces/FooterProps.md +1 -1
  86. package/docs/api/interfaces/GrantEventAppRoleParams.md +122 -0
  87. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  88. package/docs/api/interfaces/InputProps.md +1 -1
  89. package/docs/api/interfaces/LabelProps.md +1 -1
  90. package/docs/api/interfaces/LoginFormProps.md +1 -1
  91. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  92. package/docs/api/interfaces/NavigationContextType.md +1 -1
  93. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  94. package/docs/api/interfaces/NavigationItem.md +1 -1
  95. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  97. package/docs/api/interfaces/Organisation.md +1 -1
  98. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  99. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  100. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  101. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  102. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  103. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  104. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  105. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  106. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  107. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  108. package/docs/api/interfaces/PaletteData.md +1 -1
  109. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  110. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  111. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  112. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  113. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  114. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  115. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  116. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  117. package/docs/api/interfaces/RBACConfig.md +1 -1
  118. package/docs/api/interfaces/RBACLogger.md +1 -1
  119. package/docs/api/interfaces/RevokeEventAppRoleParams.md +100 -0
  120. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  121. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  122. package/docs/api/interfaces/RoleManagementResult.md +52 -0
  123. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  124. package/docs/api/interfaces/RouteConfig.md +1 -1
  125. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  126. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  127. package/docs/api/interfaces/StorageConfig.md +1 -1
  128. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  129. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  130. package/docs/api/interfaces/StorageListOptions.md +1 -1
  131. package/docs/api/interfaces/StorageListResult.md +1 -1
  132. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  133. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  134. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  135. package/docs/api/interfaces/StyleImport.md +1 -1
  136. package/docs/api/interfaces/SwitchProps.md +1 -1
  137. package/docs/api/interfaces/ToastActionElement.md +1 -1
  138. package/docs/api/interfaces/ToastProps.md +1 -1
  139. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  140. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  141. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  142. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  143. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  144. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  145. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  146. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  147. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  148. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  149. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  150. package/docs/api/interfaces/UserEventAccess.md +1 -1
  151. package/docs/api/interfaces/UserMenuProps.md +1 -1
  152. package/docs/api/interfaces/UserProfile.md +1 -1
  153. package/docs/api/modules.md +43 -16
  154. package/docs/architecture/rpc-function-standards.md +193 -0
  155. package/package.json +1 -1
  156. package/src/__tests__/TEST_STANDARD.md +244 -2
  157. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +46 -16
  158. package/src/components/DataTable/__tests__/keyboard.test.tsx +276 -217
  159. package/src/components/DataTable/components/DataTableCore.tsx +32 -17
  160. package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
  161. package/src/components/DataTable/components/EditableRow.tsx +18 -1
  162. package/src/components/DataTable/components/ImportModal.tsx +25 -2
  163. package/src/components/DataTable/components/ViewRowModal.tsx +1 -1
  164. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
  165. package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
  166. package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
  167. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
  168. package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
  169. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
  170. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
  171. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
  172. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
  173. package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
  174. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
  175. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
  176. package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
  177. package/src/components/EventSelector/EventSelector.tsx +5 -25
  178. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
  179. package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
  180. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
  181. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
  182. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
  183. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
  184. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
  185. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
  186. package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
  187. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
  188. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
  189. package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
  190. package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
  191. package/src/components/Select/Select.tsx +8 -0
  192. package/src/components/Toast/Toast.test.tsx +8 -7
  193. package/src/components/Toast/Toast.tsx +4 -4
  194. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
  195. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
  196. package/src/hooks/useEventTheme.ts +49 -18
  197. package/src/hooks/usePermissionCache.ts +5 -3
  198. package/src/hooks/useSecureDataAccess.ts +11 -1
  199. package/src/hooks/useToast.ts +11 -12
  200. package/src/providers/services/EventServiceProvider.tsx +15 -8
  201. package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
  202. package/src/rbac/audit.test.ts +206 -0
  203. package/src/rbac/audit.ts +37 -2
  204. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
  205. package/src/rbac/errors.test.ts +340 -0
  206. package/src/rbac/hooks/index.ts +9 -0
  207. package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
  208. package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
  209. package/src/rbac/hooks/useRoleManagement.ts +255 -0
  210. package/src/services/AuthService.ts +10 -0
  211. package/src/services/EventService.ts +111 -50
  212. package/src/services/__tests__/AuthService.test.ts +1 -1
  213. package/src/services/__tests__/EventService.test.ts +60 -45
  214. package/src/services/interfaces/IEventService.ts +1 -1
  215. package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
  216. package/src/utils/__tests__/logger.unit.test.ts +398 -0
  217. package/src/utils/__tests__/validation.unit.test.ts +225 -1
  218. package/src/utils/file-reference.test.ts +214 -0
  219. package/dist/chunk-3OGQLOJM.js.map +0 -1
  220. package/dist/chunk-4OX5PXHX.js.map +0 -1
  221. package/dist/chunk-5CDJCTOO.js +0 -190
  222. package/dist/chunk-5YIZFEUQ.js.map +0 -1
  223. package/dist/chunk-F6QB26OS.js.map +0 -1
  224. package/dist/chunk-KTHLNIMA.js.map +0 -1
  225. package/dist/chunk-OO3V7W4H.js.map +0 -1
  226. package/dist/chunk-ZPXWJA4H.js.map +0 -1
  227. package/src/rbac/audit-enhanced.ts +0 -351
  228. /package/dist/{DataTable-3JRLZXER.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
  229. /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
  230. /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
  231. /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
  232. /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
  233. /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
  234. /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
  235. /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
  236. /package/dist/{chunk-QKIVSZ2O.js.map → chunk-WP5I5GLN.js.map} +0 -0
@@ -0,0 +1,916 @@
1
+ /**
2
+ * @file usePublicFileDisplay Hook Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Hooks/Public
5
+ * @since 1.0.0
6
+ *
7
+ * Comprehensive tests for the usePublicFileDisplay hook following TEST_STANDARD.md.
8
+ * Tests focus on behavior: file fetching, caching, error handling, and loading states.
9
+ */
10
+
11
+ import { renderHook, waitFor } from '@testing-library/react';
12
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
13
+ import {
14
+ usePublicFileDisplay,
15
+ clearPublicFileDisplayCache,
16
+ getPublicFileDisplayCacheStats
17
+ } from '../public/usePublicFileDisplay';
18
+ import { createMockSupabaseClient } from '../../__tests__/helpers/supabaseMock';
19
+ import type { SupabaseClient } from '@supabase/supabase-js';
20
+ import type { Database } from '../../types/database';
21
+ import type { FileCategory } from '../../types/file-reference';
22
+ import { FileCategory as FileCategoryEnum } from '../../types/file-reference';
23
+
24
+ // Mock getPublicUrl
25
+ vi.mock('../../utils/storage/helpers', () => ({
26
+ getPublicUrl: vi.fn((supabase: any, path: string) => `https://example.com/${path}`)
27
+ }));
28
+
29
+ import { getPublicUrl } from '../../utils/storage/helpers';
30
+
31
+ describe('usePublicFileDisplay Hook', () => {
32
+ let mockSupabase: SupabaseClient<Database>;
33
+
34
+ const mockFileReference = {
35
+ id: 'file-123',
36
+ table_name: 'event',
37
+ record_id: 'event-123',
38
+ file_path: 'org-123/logos/logo.png',
39
+ file_metadata: {
40
+ category: FileCategoryEnum.EVENT_LOGOS,
41
+ app_id: 'app-123'
42
+ },
43
+ organisation_id: 'org-123',
44
+ app_id: 'app-123',
45
+ is_public: true,
46
+ created_at: '2024-01-01T00:00:00Z',
47
+ updated_at: '2024-01-01T00:00:00Z'
48
+ };
49
+
50
+ beforeEach(() => {
51
+ vi.clearAllMocks();
52
+ clearPublicFileDisplayCache();
53
+ mockSupabase = createMockSupabaseClient() as any;
54
+ });
55
+
56
+ afterEach(() => {
57
+ vi.clearAllMocks();
58
+ clearPublicFileDisplayCache();
59
+ });
60
+
61
+ describe('Initialization', () => {
62
+ it('initializes with correct default state when parameters are missing', () => {
63
+ const { result } = renderHook(() =>
64
+ usePublicFileDisplay(undefined, undefined, undefined, undefined, {
65
+ supabase: mockSupabase
66
+ })
67
+ );
68
+
69
+ expect(result.current.fileUrl).toBe(null);
70
+ expect(result.current.fileReference).toBe(null);
71
+ expect(result.current.fileReferences).toEqual([]);
72
+ expect(result.current.fileUrls).toEqual(new Map());
73
+ expect(result.current.fileCount).toBe(0);
74
+ expect(result.current.isLoading).toBe(false);
75
+ expect(result.current.error).toBe(null);
76
+ });
77
+
78
+ it('initializes with loading state when parameters are provided', () => {
79
+ (mockSupabase.rpc as any).mockImplementation(() => new Promise(() => {})); // Never resolves
80
+
81
+ const { result } = renderHook(() =>
82
+ usePublicFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
83
+ supabase: mockSupabase
84
+ })
85
+ );
86
+
87
+ expect(result.current.isLoading).toBe(true);
88
+ expect(result.current.fileUrl).toBe(null);
89
+ expect(result.current.error).toBe(null);
90
+ });
91
+ });
92
+
93
+ describe('Single File Mode (with category)', () => {
94
+ it('fetches single file by category successfully', async () => {
95
+ (mockSupabase.rpc as any).mockResolvedValue({
96
+ data: [
97
+ {
98
+ id: 'file-123',
99
+ file_path: 'org-123/logos/logo.png',
100
+ file_metadata: { category: FileCategoryEnum.EVENT_LOGOS },
101
+ is_public: true,
102
+ created_at: '2024-01-01T00:00:00Z'
103
+ }
104
+ ],
105
+ error: null
106
+ });
107
+
108
+ const { result } = renderHook(() =>
109
+ usePublicFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
110
+ supabase: mockSupabase
111
+ })
112
+ );
113
+
114
+ await waitFor(
115
+ () => {
116
+ expect(result.current.isLoading).toBe(false);
117
+ },
118
+ { timeout: 2000 }
119
+ );
120
+
121
+ expect(result.current.fileReference).not.toBeNull();
122
+ expect(result.current.fileReference?.id).toBe('file-123');
123
+ expect(result.current.fileUrl).toBe('https://example.com/org-123/logos/logo.png');
124
+ expect(result.current.fileCount).toBe(1);
125
+ expect(result.current.error).toBe(null);
126
+ expect(mockSupabase.rpc).toHaveBeenCalledWith('data_file_reference_by_category_list', {
127
+ p_table_name: 'event',
128
+ p_record_id: 'event-123',
129
+ p_category: FileCategoryEnum.EVENT_LOGOS,
130
+ p_organisation_id: 'org-123'
131
+ });
132
+ });
133
+
134
+ it('handles no files found for category', async () => {
135
+ (mockSupabase.rpc as any).mockResolvedValue({
136
+ data: [],
137
+ error: null
138
+ });
139
+
140
+ const { result } = renderHook(() =>
141
+ usePublicFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
142
+ supabase: mockSupabase
143
+ })
144
+ );
145
+
146
+ await waitFor(
147
+ () => {
148
+ expect(result.current.isLoading).toBe(false);
149
+ },
150
+ { timeout: 2000 }
151
+ );
152
+
153
+ expect(result.current.fileUrl).toBe(null);
154
+ expect(result.current.fileReference).toBe(null);
155
+ expect(result.current.fileCount).toBe(0);
156
+ expect(result.current.error).toBe(null);
157
+ });
158
+
159
+ it('filters out non-public files in single file mode', async () => {
160
+ (mockSupabase.rpc as any).mockResolvedValue({
161
+ data: [
162
+ {
163
+ id: 'file-123',
164
+ file_path: 'org-123/logos/logo.png',
165
+ file_metadata: { category: FileCategoryEnum.EVENT_LOGOS },
166
+ is_public: false, // Non-public file
167
+ created_at: '2024-01-01T00:00:00Z'
168
+ }
169
+ ],
170
+ error: null
171
+ });
172
+
173
+ const { result } = renderHook(() =>
174
+ usePublicFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
175
+ supabase: mockSupabase
176
+ })
177
+ );
178
+
179
+ await waitFor(
180
+ () => {
181
+ expect(result.current.isLoading).toBe(false);
182
+ },
183
+ { timeout: 2000 }
184
+ );
185
+
186
+ // Should filter out non-public files
187
+ expect(result.current.fileUrl).toBe(null);
188
+ expect(result.current.fileReference).toBe(null);
189
+ expect(result.current.fileCount).toBe(0);
190
+ });
191
+
192
+ it('handles RPC errors in single file mode', async () => {
193
+ const rpcError = { message: 'Database error', code: 'PGRST116' };
194
+ (mockSupabase.rpc as any).mockResolvedValue({
195
+ data: null,
196
+ error: rpcError
197
+ });
198
+
199
+ const { result } = renderHook(() =>
200
+ usePublicFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
201
+ supabase: mockSupabase
202
+ })
203
+ );
204
+
205
+ await waitFor(
206
+ () => {
207
+ expect(result.current.isLoading).toBe(false);
208
+ },
209
+ { timeout: 2000 }
210
+ );
211
+
212
+ expect(result.current.error).toBeInstanceOf(Error);
213
+ expect(result.current.error?.message).toBe('Database error');
214
+ expect(result.current.fileUrl).toBe(null);
215
+ expect(result.current.fileReference).toBe(null);
216
+ });
217
+ });
218
+
219
+ describe('Multiple Files Mode (without category)', () => {
220
+ it('fetches multiple files successfully', async () => {
221
+ const fileIds = ['file-123', 'file-456'];
222
+ (mockSupabase.rpc as any)
223
+ .mockResolvedValueOnce({
224
+ data: fileIds.map(id => ({ id })),
225
+ error: null
226
+ });
227
+
228
+ (mockSupabase.from as any).mockReturnValue({
229
+ select: vi.fn().mockReturnThis(),
230
+ in: vi.fn().mockReturnThis(),
231
+ eq: vi.fn().mockResolvedValue({
232
+ data: [
233
+ { ...mockFileReference, id: 'file-123' },
234
+ { ...mockFileReference, id: 'file-456', file_path: 'org-123/logos/logo2.png' }
235
+ ],
236
+ error: null
237
+ })
238
+ });
239
+
240
+ const { result } = renderHook(() =>
241
+ usePublicFileDisplay('event', 'event-123', 'org-123', undefined, {
242
+ supabase: mockSupabase
243
+ })
244
+ );
245
+
246
+ await waitFor(
247
+ () => {
248
+ expect(result.current.isLoading).toBe(false);
249
+ },
250
+ { timeout: 2000 }
251
+ );
252
+
253
+ expect(result.current.fileCount).toBe(2);
254
+ expect(result.current.fileReferences.length).toBe(2);
255
+ expect(result.current.fileUrls.size).toBe(2);
256
+ expect(result.current.fileUrls.get('file-123')).toBe('https://example.com/org-123/logos/logo.png');
257
+ expect(result.current.fileUrls.get('file-456')).toBe('https://example.com/org-123/logos/logo2.png');
258
+ expect(result.current.fileUrl).toBe(null); // No single file URL in multiple mode
259
+ expect(result.current.fileReference).toBe(null); // No single file reference in multiple mode
260
+ });
261
+
262
+ it('handles no files found in multiple file mode', async () => {
263
+ (mockSupabase.rpc as any).mockResolvedValue({
264
+ data: [],
265
+ error: null
266
+ });
267
+
268
+ const { result } = renderHook(() =>
269
+ usePublicFileDisplay('event', 'event-123', 'org-123', undefined, {
270
+ supabase: mockSupabase
271
+ })
272
+ );
273
+
274
+ await waitFor(
275
+ () => {
276
+ expect(result.current.isLoading).toBe(false);
277
+ },
278
+ { timeout: 2000 }
279
+ );
280
+
281
+ expect(result.current.fileCount).toBe(0);
282
+ expect(result.current.fileReferences).toEqual([]);
283
+ expect(result.current.fileUrls.size).toBe(0);
284
+ expect(result.current.error).toBe(null);
285
+ });
286
+
287
+ it('filters out non-public files in multiple file mode', async () => {
288
+ const fileIds = ['file-123', 'file-456'];
289
+ (mockSupabase.rpc as any)
290
+ .mockResolvedValueOnce({
291
+ data: fileIds.map(id => ({ id })),
292
+ error: null
293
+ });
294
+
295
+ (mockSupabase.from as any).mockReturnValue({
296
+ select: vi.fn().mockReturnThis(),
297
+ in: vi.fn().mockReturnThis(),
298
+ eq: vi.fn().mockResolvedValue({
299
+ data: [
300
+ { ...mockFileReference, id: 'file-123', is_public: true },
301
+ { ...mockFileReference, id: 'file-456', is_public: false } // Non-public
302
+ ],
303
+ error: null
304
+ })
305
+ });
306
+
307
+ const { result } = renderHook(() =>
308
+ usePublicFileDisplay('event', 'event-123', 'org-123', undefined, {
309
+ supabase: mockSupabase
310
+ })
311
+ );
312
+
313
+ await waitFor(
314
+ () => {
315
+ expect(result.current.isLoading).toBe(false);
316
+ },
317
+ { timeout: 2000 }
318
+ );
319
+
320
+ // Should only include public files
321
+ expect(result.current.fileCount).toBe(1);
322
+ expect(result.current.fileReferences.length).toBe(1);
323
+ expect(result.current.fileReferences[0].id).toBe('file-123');
324
+ });
325
+
326
+ it('handles RPC errors in multiple file mode', async () => {
327
+ const rpcError = { message: 'Failed to fetch file references', code: 'PGRST116' };
328
+ (mockSupabase.rpc as any).mockResolvedValue({
329
+ data: null,
330
+ error: rpcError
331
+ });
332
+
333
+ const { result } = renderHook(() =>
334
+ usePublicFileDisplay('event', 'event-123', 'org-123', undefined, {
335
+ supabase: mockSupabase
336
+ })
337
+ );
338
+
339
+ await waitFor(
340
+ () => {
341
+ expect(result.current.isLoading).toBe(false);
342
+ },
343
+ { timeout: 2000 }
344
+ );
345
+
346
+ expect(result.current.error).toBeInstanceOf(Error);
347
+ expect(result.current.error?.message).toBe('Failed to fetch file references');
348
+ expect(result.current.fileCount).toBe(0);
349
+ });
350
+ });
351
+
352
+ describe('Caching', () => {
353
+ it('caches file data and returns from cache on subsequent calls', async () => {
354
+ (mockSupabase.rpc as any).mockResolvedValue({
355
+ data: [
356
+ {
357
+ id: 'file-123',
358
+ file_path: 'org-123/logos/logo.png',
359
+ file_metadata: { category: FileCategoryEnum.EVENT_LOGOS },
360
+ is_public: true,
361
+ created_at: '2024-01-01T00:00:00Z'
362
+ }
363
+ ],
364
+ error: null
365
+ });
366
+
367
+ const { result, rerender } = renderHook(() =>
368
+ usePublicFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
369
+ supabase: mockSupabase,
370
+ enableCache: true,
371
+ cacheTtl: 30 * 60 * 1000 // 30 minutes
372
+ })
373
+ );
374
+
375
+ await waitFor(
376
+ () => {
377
+ expect(result.current.isLoading).toBe(false);
378
+ },
379
+ { timeout: 2000 }
380
+ );
381
+
382
+ const firstCallCount = (mockSupabase.rpc as any).mock.calls.length;
383
+
384
+ // Rerender with same parameters - should use cache
385
+ rerender();
386
+
387
+ await waitFor(
388
+ () => {
389
+ expect(result.current.fileUrl).toBe('https://example.com/org-123/logos/logo.png');
390
+ },
391
+ { timeout: 1000 }
392
+ );
393
+
394
+ // Should not make another RPC call
395
+ expect((mockSupabase.rpc as any).mock.calls.length).toBe(firstCallCount);
396
+ });
397
+
398
+ it('respects cache TTL option', async () => {
399
+ (mockSupabase.rpc as any).mockResolvedValue({
400
+ data: [
401
+ {
402
+ id: 'file-123',
403
+ file_path: 'org-123/logos/logo.png',
404
+ file_metadata: { category: FileCategoryEnum.EVENT_LOGOS },
405
+ is_public: true,
406
+ created_at: '2024-01-01T00:00:00Z'
407
+ }
408
+ ],
409
+ error: null
410
+ });
411
+
412
+ const { result } = renderHook(() =>
413
+ usePublicFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
414
+ supabase: mockSupabase,
415
+ enableCache: true,
416
+ cacheTtl: 1000 // 1 second
417
+ })
418
+ );
419
+
420
+ await waitFor(
421
+ () => {
422
+ expect(result.current.isLoading).toBe(false);
423
+ },
424
+ { timeout: 2000 }
425
+ );
426
+
427
+ expect(result.current.fileUrl).toBe('https://example.com/org-123/logos/logo.png');
428
+ });
429
+
430
+ it('disables caching when enableCache is false', async () => {
431
+ (mockSupabase.rpc as any).mockResolvedValue({
432
+ data: [
433
+ {
434
+ id: 'file-123',
435
+ file_path: 'org-123/logos/logo.png',
436
+ file_metadata: { category: FileCategoryEnum.EVENT_LOGOS },
437
+ is_public: true,
438
+ created_at: '2024-01-01T00:00:00Z'
439
+ }
440
+ ],
441
+ error: null
442
+ });
443
+
444
+ const { result, rerender } = renderHook(() =>
445
+ usePublicFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
446
+ supabase: mockSupabase,
447
+ enableCache: false
448
+ })
449
+ );
450
+
451
+ await waitFor(
452
+ () => {
453
+ expect(result.current.isLoading).toBe(false);
454
+ },
455
+ { timeout: 2000 }
456
+ );
457
+
458
+ const firstCallCount = (mockSupabase.rpc as any).mock.calls.length;
459
+
460
+ // Rerender - should make another call since caching is disabled
461
+ rerender();
462
+
463
+ // The hook should refetch when dependencies change, but with caching disabled
464
+ // it will make a new call on each render
465
+ await waitFor(
466
+ () => {
467
+ expect(result.current.fileUrl).toBe('https://example.com/org-123/logos/logo.png');
468
+ },
469
+ { timeout: 2000 }
470
+ );
471
+ });
472
+ });
473
+
474
+ describe('Refetch Functionality', () => {
475
+ it('refetches data when refetch is called', async () => {
476
+ (mockSupabase.rpc as any).mockResolvedValue({
477
+ data: [
478
+ {
479
+ id: 'file-123',
480
+ file_path: 'org-123/logos/logo.png',
481
+ file_metadata: { category: FileCategoryEnum.EVENT_LOGOS },
482
+ is_public: true,
483
+ created_at: '2024-01-01T00:00:00Z'
484
+ }
485
+ ],
486
+ error: null
487
+ });
488
+
489
+ const { result } = renderHook(() =>
490
+ usePublicFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
491
+ supabase: mockSupabase,
492
+ enableCache: true
493
+ })
494
+ );
495
+
496
+ await waitFor(
497
+ () => {
498
+ expect(result.current.isLoading).toBe(false);
499
+ },
500
+ { timeout: 2000 }
501
+ );
502
+
503
+ const firstCallCount = (mockSupabase.rpc as any).mock.calls.length;
504
+
505
+ // Update mock to return different data
506
+ (mockSupabase.rpc as any).mockResolvedValue({
507
+ data: [
508
+ {
509
+ id: 'file-456',
510
+ file_path: 'org-123/logos/new-logo.png',
511
+ file_metadata: { category: FileCategoryEnum.EVENT_LOGOS },
512
+ is_public: true,
513
+ created_at: '2024-01-01T00:00:00Z'
514
+ }
515
+ ],
516
+ error: null
517
+ });
518
+
519
+ await result.current.refetch();
520
+
521
+ await waitFor(
522
+ () => {
523
+ expect(result.current.fileReference?.id).toBe('file-456');
524
+ },
525
+ { timeout: 2000 }
526
+ );
527
+
528
+ // Should have made another call
529
+ expect((mockSupabase.rpc as any).mock.calls.length).toBeGreaterThan(firstCallCount);
530
+ });
531
+
532
+ it('clears cache before refetching', async () => {
533
+ (mockSupabase.rpc as any).mockResolvedValue({
534
+ data: [
535
+ {
536
+ id: 'file-123',
537
+ file_path: 'org-123/logos/logo.png',
538
+ file_metadata: { category: FileCategoryEnum.EVENT_LOGOS },
539
+ is_public: true,
540
+ created_at: '2024-01-01T00:00:00Z'
541
+ }
542
+ ],
543
+ error: null
544
+ });
545
+
546
+ const { result } = renderHook(() =>
547
+ usePublicFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
548
+ supabase: mockSupabase,
549
+ enableCache: true
550
+ })
551
+ );
552
+
553
+ await waitFor(
554
+ () => {
555
+ expect(result.current.isLoading).toBe(false);
556
+ },
557
+ { timeout: 2000 }
558
+ );
559
+
560
+ // Verify cache exists
561
+ const statsBefore = getPublicFileDisplayCacheStats();
562
+ expect(statsBefore.size).toBeGreaterThan(0);
563
+
564
+ await result.current.refetch();
565
+
566
+ // Cache should be cleared and then repopulated
567
+ await waitFor(
568
+ () => {
569
+ expect(result.current.fileUrl).toBeDefined();
570
+ },
571
+ { timeout: 2000 }
572
+ );
573
+ });
574
+ });
575
+
576
+ describe('Error Handling', () => {
577
+ it('handles exceptions during file fetch', async () => {
578
+ const error = new Error('Network timeout');
579
+ (mockSupabase.rpc as any).mockRejectedValue(error);
580
+
581
+ const { result } = renderHook(() =>
582
+ usePublicFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
583
+ supabase: mockSupabase
584
+ })
585
+ );
586
+
587
+ await waitFor(
588
+ () => {
589
+ expect(result.current.isLoading).toBe(false);
590
+ },
591
+ { timeout: 2000 }
592
+ );
593
+
594
+ expect(result.current.error).toBeInstanceOf(Error);
595
+ expect(result.current.error?.message).toBe('Network timeout');
596
+ expect(result.current.fileUrl).toBe(null);
597
+ expect(result.current.fileReference).toBe(null);
598
+ });
599
+
600
+ it('handles non-Error exceptions', async () => {
601
+ (mockSupabase.rpc as any).mockRejectedValue('String error');
602
+
603
+ const { result } = renderHook(() =>
604
+ usePublicFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
605
+ supabase: mockSupabase
606
+ })
607
+ );
608
+
609
+ await waitFor(
610
+ () => {
611
+ expect(result.current.isLoading).toBe(false);
612
+ },
613
+ { timeout: 2000 }
614
+ );
615
+
616
+ expect(result.current.error).toBeInstanceOf(Error);
617
+ expect(result.current.error?.message).toBe('Unknown error occurred');
618
+ });
619
+
620
+ it('validates UUID format for organisation_id', async () => {
621
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
622
+
623
+ (mockSupabase.rpc as any).mockResolvedValue({
624
+ data: [],
625
+ error: null
626
+ });
627
+
628
+ const { result } = renderHook(() =>
629
+ usePublicFileDisplay('event', 'event-123', 'invalid-uuid', FileCategoryEnum.EVENT_LOGOS, {
630
+ supabase: mockSupabase
631
+ })
632
+ );
633
+
634
+ await waitFor(
635
+ () => {
636
+ expect(result.current.isLoading).toBe(false);
637
+ },
638
+ { timeout: 2000 }
639
+ );
640
+
641
+ expect(consoleSpy).toHaveBeenCalledWith(
642
+ '[usePublicFileDisplay] Invalid organisationId format (not a valid UUID):',
643
+ 'invalid-uuid'
644
+ );
645
+
646
+ consoleSpy.mockRestore();
647
+ });
648
+ });
649
+
650
+ describe('Parameter Changes', () => {
651
+ it('refetches when table_name changes', async () => {
652
+ (mockSupabase.rpc as any).mockResolvedValue({
653
+ data: [],
654
+ error: null
655
+ });
656
+
657
+ const { result, rerender } = renderHook(
658
+ ({ tableName }) =>
659
+ usePublicFileDisplay(tableName, 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
660
+ supabase: mockSupabase
661
+ }),
662
+ {
663
+ initialProps: { tableName: 'event' }
664
+ }
665
+ );
666
+
667
+ await waitFor(
668
+ () => {
669
+ expect(result.current.isLoading).toBe(false);
670
+ },
671
+ { timeout: 2000 }
672
+ );
673
+
674
+ const firstCallCount = (mockSupabase.rpc as any).mock.calls.length;
675
+
676
+ rerender({ tableName: 'organisation' });
677
+
678
+ await waitFor(
679
+ () => {
680
+ expect(result.current.isLoading).toBe(false);
681
+ },
682
+ { timeout: 2000 }
683
+ );
684
+
685
+ // Should make another call with new table name
686
+ expect((mockSupabase.rpc as any).mock.calls.length).toBeGreaterThan(firstCallCount);
687
+ expect((mockSupabase.rpc as any).mock.calls[firstCallCount][0]).toBe('data_file_reference_by_category_list');
688
+ expect((mockSupabase.rpc as any).mock.calls[firstCallCount][1].p_table_name).toBe('organisation');
689
+ });
690
+
691
+ it('refetches when record_id changes', async () => {
692
+ (mockSupabase.rpc as any).mockResolvedValue({
693
+ data: [],
694
+ error: null
695
+ });
696
+
697
+ const { result, rerender } = renderHook(
698
+ ({ recordId }) =>
699
+ usePublicFileDisplay('event', recordId, 'org-123', FileCategoryEnum.EVENT_LOGOS, {
700
+ supabase: mockSupabase
701
+ }),
702
+ {
703
+ initialProps: { recordId: 'event-123' }
704
+ }
705
+ );
706
+
707
+ await waitFor(
708
+ () => {
709
+ expect(result.current.isLoading).toBe(false);
710
+ },
711
+ { timeout: 2000 }
712
+ );
713
+
714
+ rerender({ recordId: 'event-456' });
715
+
716
+ await waitFor(
717
+ () => {
718
+ expect(result.current.isLoading).toBe(false);
719
+ },
720
+ { timeout: 2000 }
721
+ );
722
+
723
+ // Should have refetched with new record ID
724
+ expect((mockSupabase.rpc as any).mock.calls.length).toBeGreaterThan(1);
725
+ });
726
+
727
+ it('refetches when category changes', async () => {
728
+ (mockSupabase.rpc as any).mockResolvedValue({
729
+ data: [],
730
+ error: null
731
+ });
732
+
733
+ const { result, rerender } = renderHook(
734
+ ({ category }) =>
735
+ usePublicFileDisplay('event', 'event-123', 'org-123', category, {
736
+ supabase: mockSupabase
737
+ }),
738
+ {
739
+ initialProps: { category: FileCategoryEnum.EVENT_LOGOS }
740
+ }
741
+ );
742
+
743
+ await waitFor(
744
+ () => {
745
+ expect(result.current.isLoading).toBe(false);
746
+ },
747
+ { timeout: 2000 }
748
+ );
749
+
750
+ rerender({ category: FileCategoryEnum.EVENT_DOCUMENTS });
751
+
752
+ await waitFor(
753
+ () => {
754
+ expect(result.current.isLoading).toBe(false);
755
+ },
756
+ { timeout: 2000 }
757
+ );
758
+
759
+ // Should have refetched with new category
760
+ expect((mockSupabase.rpc as any).mock.calls.length).toBeGreaterThan(1);
761
+ });
762
+ });
763
+
764
+ describe('Cache Management Utilities', () => {
765
+ it('clears public file display cache', () => {
766
+ // Add some data to cache (by fetching files)
767
+ (mockSupabase.rpc as any).mockResolvedValue({
768
+ data: [
769
+ {
770
+ id: 'file-123',
771
+ file_path: 'org-123/logos/logo.png',
772
+ file_metadata: { category: FileCategoryEnum.EVENT_LOGOS },
773
+ is_public: true,
774
+ created_at: '2024-01-01T00:00:00Z'
775
+ }
776
+ ],
777
+ error: null
778
+ });
779
+
780
+ const { result } = renderHook(() =>
781
+ usePublicFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
782
+ supabase: mockSupabase,
783
+ enableCache: true
784
+ })
785
+ );
786
+
787
+ // Wait for cache to be populated
788
+ waitFor(
789
+ () => {
790
+ expect(result.current.fileUrl).not.toBeNull();
791
+ },
792
+ { timeout: 2000 }
793
+ );
794
+
795
+ // Clear cache
796
+ clearPublicFileDisplayCache();
797
+
798
+ const stats = getPublicFileDisplayCacheStats();
799
+ expect(stats.size).toBe(0);
800
+ expect(stats.keys).toEqual([]);
801
+ });
802
+
803
+ it('gets cache statistics', () => {
804
+ const stats = getPublicFileDisplayCacheStats();
805
+ expect(stats).toEqual({
806
+ size: 0,
807
+ keys: []
808
+ });
809
+ });
810
+ });
811
+
812
+ describe('Edge Cases', () => {
813
+ it('handles missing file metadata gracefully', async () => {
814
+ (mockSupabase.rpc as any).mockResolvedValue({
815
+ data: [
816
+ {
817
+ id: 'file-123',
818
+ file_path: 'org-123/logos/logo.png',
819
+ file_metadata: null, // Missing metadata
820
+ is_public: true,
821
+ created_at: '2024-01-01T00:00:00Z'
822
+ }
823
+ ],
824
+ error: null
825
+ });
826
+
827
+ const { result } = renderHook(() =>
828
+ usePublicFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
829
+ supabase: mockSupabase
830
+ })
831
+ );
832
+
833
+ await waitFor(
834
+ () => {
835
+ expect(result.current.isLoading).toBe(false);
836
+ },
837
+ { timeout: 2000 }
838
+ );
839
+
840
+ // The implementation filters out files without file_metadata when category is provided
841
+ // because it checks for file_metadata in the filter
842
+ // So we expect no file reference when metadata is missing
843
+ expect(result.current.fileReference).toBe(null);
844
+ expect(result.current.fileCount).toBe(0);
845
+ });
846
+
847
+ it('handles files without file_path', async () => {
848
+ (mockSupabase.rpc as any).mockResolvedValue({
849
+ data: [
850
+ {
851
+ id: 'file-123',
852
+ file_path: null, // Missing file path
853
+ file_metadata: { category: FileCategoryEnum.EVENT_LOGOS },
854
+ is_public: true,
855
+ created_at: '2024-01-01T00:00:00Z'
856
+ }
857
+ ],
858
+ error: null
859
+ });
860
+
861
+ const { result } = renderHook(() =>
862
+ usePublicFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
863
+ supabase: mockSupabase
864
+ })
865
+ );
866
+
867
+ await waitFor(
868
+ () => {
869
+ expect(result.current.isLoading).toBe(false);
870
+ },
871
+ { timeout: 2000 }
872
+ );
873
+
874
+ // Should filter out files without file_path
875
+ expect(result.current.fileReference).toBe(null);
876
+ expect(result.current.fileUrl).toBe(null);
877
+ });
878
+
879
+ it('handles empty string parameters', () => {
880
+ const { result } = renderHook(() =>
881
+ usePublicFileDisplay('', '', '', undefined, {
882
+ supabase: mockSupabase
883
+ })
884
+ );
885
+
886
+ expect(result.current.isLoading).toBe(false);
887
+ expect(result.current.fileUrl).toBe(null);
888
+ expect(result.current.fileReference).toBe(null);
889
+ expect(result.current.fileCount).toBe(0);
890
+ });
891
+
892
+ it('handles null category parameter for multiple file mode', async () => {
893
+ (mockSupabase.rpc as any).mockResolvedValue({
894
+ data: [],
895
+ error: null
896
+ });
897
+
898
+ const { result } = renderHook(() =>
899
+ usePublicFileDisplay('event', 'event-123', 'org-123', null, {
900
+ supabase: mockSupabase
901
+ })
902
+ );
903
+
904
+ await waitFor(
905
+ () => {
906
+ expect(result.current.isLoading).toBe(false);
907
+ },
908
+ { timeout: 2000 }
909
+ );
910
+
911
+ // Should use multiple file mode
912
+ expect(mockSupabase.rpc).toHaveBeenCalledWith('data_file_reference_list', expect.any(Object));
913
+ });
914
+ });
915
+ });
916
+