@jmruthers/pace-core 0.5.87 → 0.5.89

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 (243) hide show
  1. package/dist/{AuthService-Df3IozMG.d.ts → AuthService-DcTI5Ov4.d.ts} +9 -0
  2. package/dist/{DataTable-FA6EUX5M.js → DataTable-PWBMKMOG.js} +7 -7
  3. package/dist/{PublicLoadingSpinner-DecuJBX0.d.ts → PublicLoadingSpinner-BQXD1fbO.d.ts} +160 -130
  4. package/dist/{UnifiedAuthProvider-K2IZAY5F.js → UnifiedAuthProvider-5D3HEQND.js} +4 -4
  5. package/dist/{UnifiedAuthProvider-B391Aqum.d.ts → UnifiedAuthProvider-BVKmQd9u.d.ts} +4 -0
  6. package/dist/auth-DReDSLq9.d.ts +16 -0
  7. package/dist/{chunk-CBSD3BZ3.js → chunk-3RZBKQ5Y.js} +2 -6
  8. package/dist/{chunk-CBSD3BZ3.js.map → chunk-3RZBKQ5Y.js.map} +1 -1
  9. package/dist/{chunk-NTW3KGS4.js → chunk-6UHXQH7P.js} +5 -5
  10. package/dist/{chunk-ZFLOV3OM.js → chunk-7VJDS5QD.js} +401 -16
  11. package/dist/chunk-7VJDS5QD.js.map +1 -0
  12. package/dist/{chunk-YVUZWLQG.js → chunk-AQGF5OG7.js} +3 -3
  13. package/dist/{chunk-CVMVPYAL.js → chunk-BDZUMRBD.js} +3 -5
  14. package/dist/chunk-BDZUMRBD.js.map +1 -0
  15. package/dist/{chunk-KAY3K5TP.js → chunk-BNXBJOGL.js} +4 -4
  16. package/dist/{chunk-S3JKDMD5.js → chunk-CXKMRKRF.js} +4 -4
  17. package/dist/{chunk-5BN3YGNK.js → chunk-DP5X5ORK.js} +217 -27
  18. package/dist/chunk-DP5X5ORK.js.map +1 -0
  19. package/dist/{chunk-RIXPZJUB.js → chunk-KTPG5VCH.js} +2 -2
  20. package/dist/{chunk-2FQEQUJT.js → chunk-KWICIQVK.js} +4 -4
  21. package/dist/{chunk-WUXCWRL6.js → chunk-XJ2HZOBU.js} +6 -1
  22. package/dist/chunk-XJ2HZOBU.js.map +1 -0
  23. package/dist/{chunk-I7O3RSMN.js → chunk-YWAFPVJA.js} +1298 -769
  24. package/dist/chunk-YWAFPVJA.js.map +1 -0
  25. package/dist/{chunk-I2VVV5PQ.js → chunk-YY4YYM3E.js} +2 -2
  26. package/dist/components.d.ts +6 -55
  27. package/dist/components.js +24 -205
  28. package/dist/components.js.map +1 -1
  29. package/dist/{file-reference-9xUOnwyt.d.ts → file-reference-C9isKNPn.d.ts} +67 -2
  30. package/dist/hooks.js +9 -8
  31. package/dist/hooks.js.map +1 -1
  32. package/dist/index.d.ts +152 -26
  33. package/dist/index.js +64 -194
  34. package/dist/index.js.map +1 -1
  35. package/dist/providers.d.ts +5 -3
  36. package/dist/providers.js +3 -3
  37. package/dist/rbac/index.js +8 -8
  38. package/dist/types.d.ts +2 -1
  39. package/dist/types.js +3 -3
  40. package/dist/utils.js +2 -2
  41. package/docs/DOCUMENTATION_AUDIT.md +6 -6
  42. package/docs/DOCUMENTATION_STANDARD.md +137 -0
  43. package/docs/README.md +1 -1
  44. package/docs/api/classes/ColumnFactory.md +1 -1
  45. package/docs/api/classes/ErrorBoundary.md +1 -1
  46. package/docs/api/classes/InvalidScopeError.md +1 -1
  47. package/docs/api/classes/MissingUserContextError.md +1 -1
  48. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  49. package/docs/api/classes/PermissionDeniedError.md +1 -1
  50. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  51. package/docs/api/classes/RBACAuditManager.md +1 -1
  52. package/docs/api/classes/RBACCache.md +1 -1
  53. package/docs/api/classes/RBACEngine.md +1 -1
  54. package/docs/api/classes/RBACError.md +1 -1
  55. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  56. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  57. package/docs/api/classes/StorageUtils.md +83 -40
  58. package/docs/api/enums/FileCategory.md +56 -1
  59. package/docs/api/interfaces/AggregateConfig.md +1 -1
  60. package/docs/api/interfaces/ButtonProps.md +1 -1
  61. package/docs/api/interfaces/CardProps.md +1 -1
  62. package/docs/api/interfaces/ColorPalette.md +1 -1
  63. package/docs/api/interfaces/ColorShade.md +1 -1
  64. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  65. package/docs/api/interfaces/DataRecord.md +1 -1
  66. package/docs/api/interfaces/DataTableAction.md +1 -1
  67. package/docs/api/interfaces/DataTableColumn.md +1 -1
  68. package/docs/api/interfaces/DataTableProps.md +1 -1
  69. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  70. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  71. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  72. package/docs/api/interfaces/EventLogoProps.md +11 -11
  73. package/docs/api/interfaces/FileDisplayProps.md +10 -10
  74. package/docs/api/interfaces/FileMetadata.md +1 -1
  75. package/docs/api/interfaces/FileReference.md +1 -1
  76. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  77. package/docs/api/interfaces/FileUploadOptions.md +8 -8
  78. package/docs/api/interfaces/FileUploadProps.md +137 -42
  79. package/docs/api/interfaces/FooterProps.md +1 -1
  80. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  81. package/docs/api/interfaces/InputProps.md +1 -1
  82. package/docs/api/interfaces/LabelProps.md +1 -1
  83. package/docs/api/interfaces/LoginFormProps.md +1 -1
  84. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  85. package/docs/api/interfaces/NavigationContextType.md +1 -1
  86. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  87. package/docs/api/interfaces/NavigationItem.md +1 -1
  88. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  89. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  90. package/docs/api/interfaces/Organisation.md +1 -1
  91. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  92. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  93. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  94. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  95. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  96. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  97. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  98. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  99. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  100. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  101. package/docs/api/interfaces/PaletteData.md +1 -1
  102. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  103. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  104. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  105. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  106. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  107. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  108. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  109. package/docs/api/interfaces/RBACConfig.md +1 -1
  110. package/docs/api/interfaces/RBACLogger.md +1 -1
  111. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  112. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  113. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  114. package/docs/api/interfaces/RouteConfig.md +1 -1
  115. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  116. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  117. package/docs/api/interfaces/StorageConfig.md +1 -1
  118. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  119. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  120. package/docs/api/interfaces/StorageListOptions.md +1 -1
  121. package/docs/api/interfaces/StorageListResult.md +1 -1
  122. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  123. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  124. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  125. package/docs/api/interfaces/StyleImport.md +1 -1
  126. package/docs/api/interfaces/SwitchProps.md +1 -1
  127. package/docs/api/interfaces/ToastActionElement.md +1 -1
  128. package/docs/api/interfaces/ToastProps.md +1 -1
  129. package/docs/api/interfaces/UnifiedAuthContextType.md +83 -50
  130. package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
  131. package/docs/api/interfaces/UseEventLogoOptions.md +74 -0
  132. package/docs/api/interfaces/UseEventLogoReturn.md +81 -0
  133. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  134. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  135. package/docs/api/interfaces/UsePublicEventLogoOptions.md +6 -6
  136. package/docs/api/interfaces/UsePublicEventLogoReturn.md +6 -6
  137. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  138. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  139. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  140. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  141. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  142. package/docs/api/interfaces/UserEventAccess.md +11 -11
  143. package/docs/api/interfaces/UserMenuProps.md +1 -1
  144. package/docs/api/interfaces/UserProfile.md +1 -1
  145. package/docs/api/modules.md +290 -95
  146. package/docs/api-reference/components.md +1 -18
  147. package/docs/api-reference/hooks.md +1 -4
  148. package/docs/best-practices/testing.md +2 -0
  149. package/docs/documentation-index.md +1 -1
  150. package/docs/getting-started/faq.md +1 -1
  151. package/docs/implementation-guides/file-reference-system.md +592 -58
  152. package/docs/implementation-guides/file-upload-storage.md +137 -73
  153. package/docs/implementation-guides/public-pages-advanced.md +10 -0
  154. package/docs/rbac/super-admin-guide.md +18 -70
  155. package/docs/testing/README.md +2 -0
  156. package/package.json +1 -1
  157. package/src/__tests__/TEST_STANDARD.md +674 -0
  158. package/src/__tests__/helpers/test-utils.tsx +3 -2
  159. package/src/components/DataTable/__tests__/{DataTable.comprehensive.test.tsx.skip → DataTable.comprehensive.test.tsx} +17 -18
  160. package/src/components/DataTable/__tests__/{DataTable.test.tsx.skip → DataTable.test.tsx} +14 -22
  161. package/src/components/DataTable/__tests__/{ssr.strict-mode.test.tsx.skip → ssr.strict-mode.test.tsx} +42 -47
  162. package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +1 -1
  163. package/src/components/DataTable/examples/__tests__/PerformanceExample.test.tsx +13 -4
  164. package/src/components/DataTable/utils/__tests__/COVERAGE_NOTE.md +1 -1
  165. package/src/components/DataTable/utils/__tests__/performanceUtils.test.ts +10 -6
  166. package/src/components/FileDisplay/FileDisplay.test.tsx +257 -0
  167. package/src/components/{FileDisplay.tsx → FileDisplay/FileDisplay.tsx} +111 -10
  168. package/src/components/FileDisplay/index.tsx +4 -0
  169. package/src/components/FileUpload/FileUpload.test.tsx +171 -621
  170. package/src/components/FileUpload/FileUpload.tsx +512 -168
  171. package/src/components/FileUpload/index.tsx +4 -0
  172. package/src/components/Progress/Progress.test.tsx +38 -0
  173. package/src/components/PublicLayout/EventLogo.tsx +6 -4
  174. package/src/components/Select/Select.test.tsx +1 -1
  175. package/src/components/SessionRestorationLoader.tsx +48 -0
  176. package/src/components/Toast/Toast.tsx +13 -8
  177. package/src/components/index.ts +16 -16
  178. package/src/hooks/__tests__/ServiceHooks.test.tsx +615 -0
  179. package/src/hooks/public/usePublicEventLogo.ts +16 -20
  180. package/src/hooks/useEventLogo.ts +316 -0
  181. package/src/hooks/useEvents.ts +0 -5
  182. package/src/hooks/useFileReference.test.ts +659 -0
  183. package/src/hooks/useFileReference.ts +207 -3
  184. package/src/hooks/useSessionRestoration.ts +64 -0
  185. package/src/index.ts +17 -5
  186. package/src/providers/{UnifiedAuthProvider.test.simple.tsx → UnifiedAuthProvider.smoke.test.tsx} +81 -60
  187. package/src/providers/services/AuthServiceProvider.tsx +27 -3
  188. package/src/providers/services/UnifiedAuthProvider.tsx +34 -5
  189. package/src/rbac/{engine.test.simple.ts → RBACEngine.smoke.test.ts} +17 -12
  190. package/src/services/AuthService.ts +142 -20
  191. package/src/services/EventService.ts +0 -4
  192. package/src/types/auth.ts +15 -0
  193. package/src/types/file-reference.ts +73 -1
  194. package/src/types/index.ts +1 -0
  195. package/src/utils/__tests__/organisationContext.unit.test.ts +2 -4
  196. package/src/utils/appNameResolver.simple.test.ts +99 -29
  197. package/src/utils/file-reference.test.ts +535 -0
  198. package/src/utils/file-reference.ts +200 -30
  199. package/src/utils/organisationContext.test.ts +5 -19
  200. package/src/utils/organisationContext.ts +3 -5
  201. package/src/utils/storage/README.md +269 -262
  202. package/src/utils/storage/config.ts +9 -0
  203. package/src/utils/storage/helpers.test.ts +735 -0
  204. package/src/utils/storage/helpers.ts +189 -16
  205. package/src/utils/storage/index.ts +3 -0
  206. package/src/validation/__tests__/sanitization.unit.test.ts +1 -1
  207. package/src/validation/__tests__/schemaUtils.unit.test.ts +1 -1
  208. package/src/validation/__tests__/user.unit.test.ts +1 -1
  209. package/dist/chunk-5BN3YGNK.js.map +0 -1
  210. package/dist/chunk-CVMVPYAL.js.map +0 -1
  211. package/dist/chunk-I7O3RSMN.js.map +0 -1
  212. package/dist/chunk-WUXCWRL6.js.map +0 -1
  213. package/dist/chunk-ZFLOV3OM.js.map +0 -1
  214. package/docs/CONTENT_AUDIT_REPORT.md +0 -253
  215. package/docs/STYLE_GUIDE.md +0 -37
  216. package/examples/RBAC/__tests__/PermissionExample.test.tsx +0 -150
  217. package/examples/public-pages/__tests__/PublicPageUsageExample.test.tsx +0 -159
  218. package/src/__tests__/TEST_GUIDE_CURSOR.md +0 -1605
  219. package/src/__tests__/TEST_GUIDE_HUMAN.md +0 -103
  220. package/src/components/FileUpload/FileUpload.example.tsx +0 -218
  221. package/src/components/FileUpload/index.ts +0 -6
  222. package/src/components/FileUpload.tsx +0 -176
  223. package/src/components/Progress/index.ts +0 -3
  224. package/src/components/PublicLayout/__tests__/EventLogo.test.tsx +0 -666
  225. package/src/components/SuperAdminGuard.tsx +0 -116
  226. package/src/components/__tests__/FileDisplay.test.tsx +0 -575
  227. package/src/components/__tests__/FileUpload.test.tsx +0 -446
  228. package/src/components/__tests__/SuperAdminGuard.test.tsx +0 -627
  229. package/src/components/examples/PermissionExample.tsx +0 -173
  230. package/src/hooks/__tests__/usePublicEvent.unit.test.ts +0 -583
  231. package/src/hooks/__tests__/usePublicEventLogo.unit.test.ts +0 -640
  232. package/src/types/__tests__/file-reference.test.ts +0 -447
  233. package/src/utils/__tests__/file-reference.test.ts +0 -383
  234. /package/dist/{DataTable-FA6EUX5M.js.map → DataTable-PWBMKMOG.js.map} +0 -0
  235. /package/dist/{UnifiedAuthProvider-K2IZAY5F.js.map → UnifiedAuthProvider-5D3HEQND.js.map} +0 -0
  236. /package/dist/{chunk-NTW3KGS4.js.map → chunk-6UHXQH7P.js.map} +0 -0
  237. /package/dist/{chunk-YVUZWLQG.js.map → chunk-AQGF5OG7.js.map} +0 -0
  238. /package/dist/{chunk-KAY3K5TP.js.map → chunk-BNXBJOGL.js.map} +0 -0
  239. /package/dist/{chunk-S3JKDMD5.js.map → chunk-CXKMRKRF.js.map} +0 -0
  240. /package/dist/{chunk-RIXPZJUB.js.map → chunk-KTPG5VCH.js.map} +0 -0
  241. /package/dist/{chunk-2FQEQUJT.js.map → chunk-KWICIQVK.js.map} +0 -0
  242. /package/dist/{chunk-I2VVV5PQ.js.map → chunk-YY4YYM3E.js.map} +0 -0
  243. /package/src/providers/{OrganisationProvider.test.simple.tsx → OrganisationProvider.context.test.tsx} +0 -0
@@ -0,0 +1,735 @@
1
+ /**
2
+ * @file Storage Helpers Tests
3
+ * @description Comprehensive tests for storage utility functions following TEST_STANDARD.md
4
+ */
5
+
6
+ import { vi } from 'vitest';
7
+ import {
8
+ uploadFile,
9
+ deleteFile,
10
+ downloadFile,
11
+ getPublicUrl,
12
+ getSignedUrl,
13
+ generateFilePath
14
+ } from './helpers';
15
+ import { getBucketName } from './config';
16
+ import { FileCategory } from '../../types/file-reference';
17
+ import { createMockSupabaseClient } from '../../__tests__/helpers/test-utils';
18
+
19
+ // Setup mocks
20
+ beforeEach(() => {
21
+ vi.clearAllMocks();
22
+ });
23
+
24
+ afterEach(() => {
25
+ vi.restoreAllMocks();
26
+ });
27
+
28
+ // Test data
29
+ const mockSupabase = createMockSupabaseClient();
30
+ const createTestFile = (name = 'test.pdf', type = 'application/pdf', size = 1024) => {
31
+ return new File(['test content'], name, { type, size });
32
+ };
33
+
34
+ describe('[utility] Storage Helpers', () => {
35
+ describe('getBucketName', () => {
36
+ it('returns public-files bucket for public files', () => {
37
+ expect(getBucketName(true)).toBe('public-files');
38
+ });
39
+
40
+ it('returns files bucket for private files', () => {
41
+ expect(getBucketName(false)).toBe('files');
42
+ });
43
+ });
44
+
45
+ describe('generateFilePath', () => {
46
+ it('generates correct file path with org and customPath', () => {
47
+ const result = generateFilePath(
48
+ { orgId: 'test-org-123', isPublic: false, customPath: 'general_documents' },
49
+ 'test-document.pdf'
50
+ );
51
+ expect(result).toBe('test-org-123/general_documents/test-document.pdf');
52
+ });
53
+
54
+ it('generates unique paths for same file name', () => {
55
+ const path1 = generateFilePath({ orgId: 'org-123', customPath: 'images' }, 'test.jpg');
56
+ const path2 = generateFilePath({ orgId: 'org-123', customPath: 'images' }, 'test.jpg');
57
+
58
+ expect(path1).toBe(path2);
59
+ });
60
+
61
+ it('validates required orgId', () => {
62
+ expect(() => generateFilePath({ orgId: '' } as any, 'test.jpg'))
63
+ .toThrow('orgId is required for file path generation');
64
+ });
65
+
66
+ it('handles special characters in file names', () => {
67
+ const result = generateFilePath(
68
+ { orgId: 'org-123', customPath: 'images' },
69
+ 'test file with spaces & symbols.jpg'
70
+ );
71
+
72
+ expect(result).toBe('org-123/images/test file with spaces & symbols.jpg');
73
+ });
74
+
75
+ it('preserves file extension', () => {
76
+ const testCases = [
77
+ { fileName: 'test.pdf', expectedExt: '.pdf' },
78
+ { fileName: 'image.jpeg', expectedExt: '.jpeg' },
79
+ { fileName: 'document.docx', expectedExt: '.docx' },
80
+ { fileName: 'noextension', expectedExt: '' }
81
+ ];
82
+
83
+ testCases.forEach(({ fileName, expectedExt }) => {
84
+ const result = generateFilePath({ orgId: 'org-123', customPath: 'general_documents' }, fileName);
85
+ expect(result).toMatch(new RegExp(`${expectedExt.replace('.', '\\.')}$`));
86
+ });
87
+ });
88
+ });
89
+
90
+ describe('uploadFile', () => {
91
+ it('uploads file to correct bucket successfully', async () => {
92
+ const testFile = createTestFile();
93
+ const options = {
94
+ orgId: 'test-org-123',
95
+ category: FileCategory.GENERAL_DOCUMENTS,
96
+ isPublic: false
97
+ };
98
+
99
+ mockSupabase.storage = {
100
+ from: vi.fn(() => ({
101
+ upload: vi.fn().mockResolvedValue({
102
+ data: { path: 'test-path' },
103
+ error: null
104
+ })
105
+ }))
106
+ };
107
+
108
+ const result = await uploadFile(mockSupabase, testFile, options);
109
+
110
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('files');
111
+ expect(result.success).toBe(true);
112
+ expect(result.path).toMatch(/^test-org-123\//);
113
+ });
114
+
115
+ it('uploads to public bucket when isPublic is true', async () => {
116
+ const testFile = createTestFile();
117
+ const options = {
118
+ orgId: 'test-org-123',
119
+ category: FileCategory.EVENT_LOGOS,
120
+ isPublic: true
121
+ };
122
+
123
+ mockSupabase.storage = {
124
+ from: vi.fn(() => ({
125
+ upload: vi.fn().mockResolvedValue({
126
+ data: { path: 'test-path' },
127
+ error: null
128
+ })
129
+ }))
130
+ };
131
+
132
+ await uploadFile(mockSupabase, testFile, options);
133
+
134
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
135
+ });
136
+
137
+ it('handles upload errors gracefully', async () => {
138
+ const testFile = createTestFile();
139
+ const options = {
140
+ orgId: 'test-org-123',
141
+ category: FileCategory.GENERAL_DOCUMENTS,
142
+ isPublic: false
143
+ };
144
+
145
+ mockSupabase.storage = {
146
+ from: vi.fn(() => ({
147
+ upload: vi.fn().mockResolvedValue({
148
+ data: null,
149
+ error: { message: 'Upload failed' }
150
+ })
151
+ }))
152
+ };
153
+
154
+ const result = await uploadFile(mockSupabase, testFile, options);
155
+
156
+ expect(result.success).toBe(false);
157
+ expect(result.error).toBe('Upload failed: Upload failed');
158
+ });
159
+
160
+ it('validates required parameters', async () => {
161
+ const testFile = createTestFile();
162
+
163
+ const noClient = await uploadFile(null as any, testFile, {} as any);
164
+ expect(noClient.success).toBe(false);
165
+
166
+ const noFile = await uploadFile(mockSupabase, null as any, {} as any);
167
+ expect(noFile.success).toBe(false);
168
+
169
+ const noOrg = await uploadFile(mockSupabase, testFile, { orgId: '' } as any);
170
+ expect(noOrg.success).toBe(false);
171
+ });
172
+
173
+ it('includes file metadata in result', async () => {
174
+ const testFile = createTestFile('test.jpg', 'image/jpeg', 2048);
175
+ const options = {
176
+ orgId: 'test-org-123',
177
+ category: FileCategory.IMAGES,
178
+ isPublic: false
179
+ };
180
+
181
+ mockSupabase.storage = {
182
+ from: vi.fn(() => ({
183
+ upload: vi.fn().mockResolvedValue({
184
+ data: { path: 'test-path' },
185
+ error: null
186
+ })
187
+ }))
188
+ };
189
+
190
+ const result = await uploadFile(mockSupabase, testFile, options);
191
+
192
+ expect(result.metadata.mimeType).toBe('image/jpeg');
193
+ expect(result.metadata.size).toBe(testFile.size);
194
+ expect(result.metadata.orgId).toBe('test-org-123');
195
+ });
196
+
197
+ it('uses custom path when provided', async () => {
198
+ const testFile = createTestFile();
199
+ const options = {
200
+ orgId: 'test-org-123',
201
+ category: FileCategory.GENERAL_DOCUMENTS,
202
+ isPublic: false,
203
+ customPath: 'custom/path/file.pdf'
204
+ };
205
+
206
+ const mockUpload = vi.fn().mockResolvedValue({
207
+ data: { path: 'custom/path/file.pdf' },
208
+ error: null
209
+ });
210
+
211
+ mockSupabase.storage = {
212
+ from: vi.fn(() => ({ upload: mockUpload }))
213
+ };
214
+
215
+ await uploadFile(mockSupabase, testFile, options);
216
+
217
+ expect(mockUpload).toHaveBeenCalled();
218
+ });
219
+ });
220
+
221
+ describe('deleteFile', () => {
222
+ it('deletes file from correct bucket successfully', async () => {
223
+ const filePath = 'org-123/documents/test.pdf';
224
+
225
+ const mockRemove = vi.fn().mockResolvedValue({
226
+ data: null,
227
+ error: null
228
+ });
229
+
230
+ mockSupabase.storage = {
231
+ from: vi.fn(() => ({ remove: mockRemove }))
232
+ };
233
+
234
+ const result = await deleteFile(mockSupabase, filePath, false);
235
+
236
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('files');
237
+ expect(mockRemove).toHaveBeenCalledWith([filePath]);
238
+ expect(result.success).toBe(true);
239
+ });
240
+
241
+ it('deletes from public bucket when isPublic is true', async () => {
242
+ const filePath = 'org-123/logos/logo.png';
243
+
244
+ const mockRemove = vi.fn().mockResolvedValue({
245
+ data: null,
246
+ error: null
247
+ });
248
+
249
+ mockSupabase.storage = {
250
+ from: vi.fn(() => ({ remove: mockRemove }))
251
+ };
252
+
253
+ await deleteFile(mockSupabase, filePath, true);
254
+
255
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
256
+ });
257
+
258
+ it('handles delete errors gracefully', async () => {
259
+ const filePath = 'org-123/documents/test.pdf';
260
+
261
+ const mockRemove = vi.fn().mockResolvedValue({
262
+ data: null,
263
+ error: { message: 'File not found' }
264
+ });
265
+
266
+ mockSupabase.storage = {
267
+ from: vi.fn(() => ({ remove: mockRemove }))
268
+ };
269
+
270
+ const result = await deleteFile(mockSupabase, filePath, false);
271
+
272
+ expect(result.success).toBe(false);
273
+ expect(result.error).toBe('Delete failed: File not found');
274
+ });
275
+
276
+ it('validates required parameters', async () => {
277
+ const delNoClient = await deleteFile(null as any, 'path', false);
278
+ expect(delNoClient.success).toBe(false);
279
+
280
+ const delNoPath = await deleteFile(mockSupabase, '', false);
281
+ expect(delNoPath.success).toBe(false);
282
+ });
283
+ });
284
+
285
+ describe('downloadFile', () => {
286
+ it('downloads file from correct bucket successfully', async () => {
287
+ const filePath = 'org-123/documents/test.pdf';
288
+ const mockBlob = new Blob(['file content'], { type: 'application/pdf' });
289
+
290
+ const mockDownload = vi.fn().mockResolvedValue({
291
+ data: mockBlob,
292
+ error: null
293
+ });
294
+
295
+ const mockList = vi.fn().mockResolvedValue({
296
+ data: [{ metadata: { size: mockBlob.size, mimetype: mockBlob.type } }],
297
+ error: null
298
+ });
299
+ mockSupabase.storage = {
300
+ from: vi.fn(() => ({ download: mockDownload, list: mockList }))
301
+ };
302
+
303
+ const result = await downloadFile(mockSupabase, filePath, false);
304
+
305
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('files');
306
+ expect(mockDownload).toHaveBeenCalledWith(filePath);
307
+ expect(result?.blob).toBe(mockBlob);
308
+ });
309
+
310
+ it('downloads from public bucket when isPublic is true', async () => {
311
+ const filePath = 'org-123/logos/logo.png';
312
+ const mockBlob = new Blob(['image content'], { type: 'image/png' });
313
+
314
+ const mockDownload = vi.fn().mockResolvedValue({
315
+ data: mockBlob,
316
+ error: null
317
+ });
318
+
319
+ mockSupabase.storage = {
320
+ from: vi.fn(() => ({ download: mockDownload }))
321
+ };
322
+
323
+ await downloadFile(mockSupabase, filePath, true);
324
+
325
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
326
+ });
327
+
328
+ it('returns null when download fails', async () => {
329
+ const filePath = 'org-123/documents/test.pdf';
330
+
331
+ const mockDownload = vi.fn().mockResolvedValue({
332
+ data: null,
333
+ error: { message: 'File not found' }
334
+ });
335
+
336
+ mockSupabase.storage = {
337
+ from: vi.fn(() => ({ download: mockDownload }))
338
+ };
339
+
340
+ const result = await downloadFile(mockSupabase, filePath, false);
341
+
342
+ expect(result).toBe(null);
343
+ });
344
+
345
+ it('validates required parameters', async () => {
346
+ const dlNoClient = await downloadFile(null as any, 'path', false);
347
+ expect(dlNoClient).toBe(null);
348
+
349
+ const dlNoPath = await downloadFile(mockSupabase, '', false);
350
+ expect(dlNoPath).toBe(null);
351
+ });
352
+ });
353
+
354
+ describe('getPublicUrl', () => {
355
+ it('generates public URL for public bucket', () => {
356
+ const filePath = 'org-123/logos/logo.png';
357
+
358
+ const mockGetPublicUrl = vi.fn().mockReturnValue({
359
+ data: { publicUrl: 'https://example.com/public/logo.png' }
360
+ });
361
+
362
+ mockSupabase.storage = {
363
+ from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
364
+ };
365
+
366
+ const result = getPublicUrl(mockSupabase, filePath, true);
367
+
368
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
369
+ expect(mockGetPublicUrl).toHaveBeenCalledWith(filePath);
370
+ expect(result).toBe('https://example.com/public/logo.png');
371
+ });
372
+
373
+ it('returns direct URLs without calling storage', () => {
374
+ const directUrl = 'https://cdn.example.com/logo.png';
375
+
376
+ const localSupabase = createMockSupabaseClient();
377
+ localSupabase.storage = {
378
+ from: vi.fn()
379
+ };
380
+
381
+ const result = getPublicUrl(localSupabase as any, directUrl, true);
382
+
383
+ expect(localSupabase.storage.from).not.toHaveBeenCalled();
384
+ expect(result).toBe(directUrl);
385
+ });
386
+
387
+ it('generates public URL for private bucket', () => {
388
+ const filePath = 'org-123/documents/test.pdf';
389
+
390
+ const mockGetPublicUrl = vi.fn().mockReturnValue({
391
+ data: { publicUrl: 'https://example.com/files/test.pdf' }
392
+ });
393
+
394
+ mockSupabase.storage = {
395
+ from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
396
+ };
397
+
398
+ const result = getPublicUrl(mockSupabase, filePath, false);
399
+
400
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('files');
401
+ expect(result).toBe('https://example.com/files/test.pdf');
402
+ });
403
+
404
+ it('uses explicit bucket prefix when provided', () => {
405
+ const filePath = 'public-files/org-123/logo.png';
406
+
407
+ const mockGetPublicUrl = vi.fn().mockReturnValue({
408
+ data: { publicUrl: 'https://example.com/public/logo.png' }
409
+ });
410
+
411
+ mockSupabase.storage = {
412
+ from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
413
+ };
414
+
415
+ const result = getPublicUrl(mockSupabase, filePath, true);
416
+
417
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
418
+ expect(mockGetPublicUrl).toHaveBeenCalledWith('org-123/logo.png');
419
+ expect(result).toBe('https://example.com/public/logo.png');
420
+ });
421
+
422
+ it('supports custom bucket hints without hyphen characters', () => {
423
+ const filePath = 'branding/org-123/logo.png';
424
+
425
+ const mockGetPublicUrl = vi.fn().mockReturnValue({
426
+ data: { publicUrl: 'https://example.com/branding/logo.png' }
427
+ });
428
+
429
+ mockSupabase.storage = {
430
+ from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
431
+ };
432
+
433
+ const result = getPublicUrl(mockSupabase, filePath, true);
434
+
435
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('branding');
436
+ expect(mockGetPublicUrl).toHaveBeenCalledWith('org-123/logo.png');
437
+ expect(result).toBe('https://example.com/branding/logo.png');
438
+ });
439
+
440
+ it('supports double colon bucket hints to avoid path ambiguity', () => {
441
+ const filePath = 'brand-assets::org-123/logo.png';
442
+
443
+ const mockGetPublicUrl = vi.fn().mockReturnValue({
444
+ data: { publicUrl: 'https://example.com/brand/logo.png' }
445
+ });
446
+
447
+ mockSupabase.storage = {
448
+ from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
449
+ };
450
+
451
+ const result = getPublicUrl(mockSupabase, filePath, true);
452
+
453
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('brand-assets');
454
+ expect(mockGetPublicUrl).toHaveBeenCalledWith('org-123/logo.png');
455
+ expect(result).toBe('https://example.com/brand/logo.png');
456
+ });
457
+
458
+ it('treats numeric leading path segments as directories rather than buckets', () => {
459
+ const filePath = '2024/05/logo.png';
460
+
461
+ const mockGetPublicUrl = vi.fn().mockReturnValue({
462
+ data: { publicUrl: 'https://example.com/public/logo.png' }
463
+ });
464
+
465
+ mockSupabase.storage = {
466
+ from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
467
+ };
468
+
469
+ const result = getPublicUrl(mockSupabase, filePath, true);
470
+
471
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
472
+ expect(mockGetPublicUrl).toHaveBeenCalledWith(filePath);
473
+ expect(result).toBe('https://example.com/public/logo.png');
474
+ });
475
+
476
+ it('ignores bucket prefix when path starts with UUID', () => {
477
+ const filePath = '123e4567-e89b-12d3-a456-426614174000/event_logos/logo.png';
478
+
479
+ const mockGetPublicUrl = vi.fn().mockReturnValue({
480
+ data: { publicUrl: 'https://example.com/public/logo.png' }
481
+ });
482
+
483
+ mockSupabase.storage = {
484
+ from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
485
+ };
486
+
487
+ const result = getPublicUrl(mockSupabase, filePath, true);
488
+
489
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
490
+ expect(mockGetPublicUrl).toHaveBeenCalledWith(filePath);
491
+ expect(result).toBe('https://example.com/public/logo.png');
492
+ });
493
+
494
+ it('validates required parameters', () => {
495
+ expect(() => getPublicUrl(null as any, 'path', false))
496
+ .toThrow();
497
+
498
+ expect(() => getPublicUrl(mockSupabase, '', false))
499
+ .toThrow();
500
+ });
501
+ });
502
+
503
+ describe('getSignedUrl', () => {
504
+ it('generates signed URL with default expiration', async () => {
505
+ const filePath = 'org-123/documents/test.pdf';
506
+ const options = {
507
+ appName: 'test-app',
508
+ orgId: 'org-123'
509
+ };
510
+
511
+ const mockCreateSignedUrl = vi.fn().mockResolvedValue({
512
+ data: { signedUrl: 'https://example.com/signed/test.pdf?token=abc123' },
513
+ error: null
514
+ });
515
+
516
+ mockSupabase.storage = {
517
+ from: vi.fn(() => ({ createSignedUrl: mockCreateSignedUrl }))
518
+ };
519
+
520
+ const result = await getSignedUrl(mockSupabase, filePath, options);
521
+
522
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('files');
523
+ expect(mockCreateSignedUrl).toHaveBeenCalledWith(filePath, 3600);
524
+ expect(result?.url).toBe('https://example.com/signed/test.pdf?token=abc123');
525
+ });
526
+
527
+ it('generates signed URL with custom expiration', async () => {
528
+ const filePath = 'org-123/documents/test.pdf';
529
+ const options = {
530
+ appName: 'test-app',
531
+ orgId: 'org-123',
532
+ expiresIn: 7200
533
+ };
534
+
535
+ const mockCreateSignedUrl = vi.fn().mockResolvedValue({
536
+ data: { signedUrl: 'https://example.com/signed/test.pdf?token=def456' },
537
+ error: null
538
+ });
539
+
540
+ mockSupabase.storage = {
541
+ from: vi.fn(() => ({ createSignedUrl: mockCreateSignedUrl }))
542
+ };
543
+
544
+ await getSignedUrl(mockSupabase, filePath, options);
545
+
546
+ expect(mockCreateSignedUrl).toHaveBeenCalledWith(filePath, 7200);
547
+ });
548
+
549
+ it('returns null when signed URL generation fails', async () => {
550
+ const filePath = 'org-123/documents/test.pdf';
551
+ const options = {
552
+ appName: 'test-app',
553
+ orgId: 'org-123'
554
+ };
555
+
556
+ const mockCreateSignedUrl = vi.fn().mockResolvedValue({
557
+ data: null,
558
+ error: { message: 'Permission denied' }
559
+ });
560
+
561
+ mockSupabase.storage = {
562
+ from: vi.fn(() => ({ createSignedUrl: mockCreateSignedUrl }))
563
+ };
564
+
565
+ const result = await getSignedUrl(mockSupabase, filePath, options);
566
+
567
+ expect(result).toBe(null);
568
+ });
569
+
570
+ it('validates required parameters', async () => {
571
+ const options = { appName: 'test-app', orgId: 'org-123' };
572
+
573
+ const gsNoClient = await getSignedUrl(null as any, 'path', options);
574
+ expect(gsNoClient).toBe(null);
575
+
576
+ const gsNoPath = await getSignedUrl(mockSupabase, '', options);
577
+ expect(gsNoPath).toBe(null);
578
+
579
+ const gsNoOpts = await getSignedUrl(mockSupabase, 'path', {} as any);
580
+ expect(gsNoOpts).toBe(null);
581
+
582
+ const gsNoOrg = await getSignedUrl(mockSupabase, 'path', { appName: 'test' } as any);
583
+ expect(gsNoOrg).toBe(null);
584
+ });
585
+
586
+ it('includes expiration metadata in result', async () => {
587
+ const filePath = 'org-123/documents/test.pdf';
588
+ const options = {
589
+ appName: 'test-app',
590
+ orgId: 'org-123',
591
+ expiresIn: 3600
592
+ };
593
+
594
+ const mockCreateSignedUrl = vi.fn().mockResolvedValue({
595
+ data: { signedUrl: 'https://example.com/signed/test.pdf?token=abc123' },
596
+ error: null
597
+ });
598
+
599
+ mockSupabase.storage = {
600
+ from: vi.fn(() => ({ createSignedUrl: mockCreateSignedUrl }))
601
+ };
602
+
603
+ const result = await getSignedUrl(mockSupabase, filePath, options);
604
+
605
+ expect(result?.url).toContain('signed');
606
+ expect(typeof result?.expiresAt).toBe('string');
607
+ });
608
+ });
609
+
610
+ describe('Error Handling', () => {
611
+ it('handles storage client errors gracefully', async () => {
612
+ const testFile = createTestFile();
613
+ const options = {
614
+ orgId: 'test-org-123',
615
+ category: FileCategory.GENERAL_DOCUMENTS,
616
+ isPublic: false
617
+ };
618
+
619
+ mockSupabase.storage = {
620
+ from: vi.fn(() => {
621
+ throw new Error('Storage client error');
622
+ })
623
+ };
624
+
625
+ const res = await uploadFile(mockSupabase, testFile, options);
626
+ expect(res.success).toBe(false);
627
+ });
628
+
629
+ it('handles network timeouts gracefully', async () => {
630
+ const filePath = 'org-123/documents/test.pdf';
631
+
632
+ const mockDownload = vi.fn().mockRejectedValue(new Error('Network timeout'));
633
+
634
+ mockSupabase.storage = {
635
+ from: vi.fn(() => ({ download: mockDownload }))
636
+ };
637
+
638
+ const dl = await downloadFile(mockSupabase, filePath, false);
639
+ expect(dl).toBe(null);
640
+ });
641
+ });
642
+
643
+ describe('Integration Scenarios', () => {
644
+ it('handles complete upload-download cycle', async () => {
645
+ const testFile = createTestFile('integration-test.pdf', 'application/pdf', 2048);
646
+ const uploadOptions = {
647
+ orgId: 'test-org-123',
648
+ category: FileCategory.GENERAL_DOCUMENTS,
649
+ isPublic: false
650
+ };
651
+
652
+ // Mock upload
653
+ const mockUpload = vi.fn().mockResolvedValue({
654
+ data: { path: 'org-123/documents/integration-test.pdf' },
655
+ error: null
656
+ });
657
+
658
+ // Mock download
659
+ const mockDownload = vi.fn().mockResolvedValue({
660
+ data: new Blob(['file content'], { type: 'application/pdf' }),
661
+ error: null
662
+ });
663
+
664
+ const mockList = vi.fn().mockResolvedValue({ data: [{ metadata: { size: 2048, mimetype: 'application/pdf' } }], error: null });
665
+ mockSupabase.storage = {
666
+ from: vi.fn(() => ({
667
+ upload: mockUpload,
668
+ download: mockDownload,
669
+ list: mockList
670
+ }))
671
+ };
672
+
673
+ // Upload file
674
+ const uploadResult = await uploadFile(mockSupabase, testFile, uploadOptions);
675
+ expect(uploadResult.success).toBe(true);
676
+
677
+ // Download file
678
+ const downloadResult = await downloadFile(mockSupabase, uploadResult.path!, false);
679
+ expect(downloadResult?.blob).toBeInstanceOf(Blob);
680
+ expect(downloadResult?.metadata.type).toBe('application/pdf');
681
+ });
682
+
683
+ it('handles public vs private file workflows', async () => {
684
+ const publicFile = createTestFile('public.jpg', 'image/jpeg');
685
+ const privateFile = createTestFile('private.pdf', 'application/pdf');
686
+
687
+ const mockUpload = vi.fn().mockResolvedValue({
688
+ data: { path: 'test-path' },
689
+ error: null
690
+ });
691
+
692
+ const mockGetPublicUrl = vi.fn().mockReturnValue({
693
+ data: { publicUrl: 'https://example.com/public.jpg' }
694
+ });
695
+
696
+ const mockCreateSignedUrl = vi.fn().mockResolvedValue({
697
+ data: { signedUrl: 'https://example.com/signed/private.pdf?token=abc' },
698
+ error: null
699
+ });
700
+
701
+ mockSupabase.storage = {
702
+ from: vi.fn(() => ({
703
+ upload: mockUpload,
704
+ getPublicUrl: mockGetPublicUrl,
705
+ createSignedUrl: mockCreateSignedUrl
706
+ }))
707
+ };
708
+
709
+ // Upload public file
710
+ await uploadFile(mockSupabase, publicFile, {
711
+ orgId: 'org-123',
712
+ category: FileCategory.IMAGES,
713
+ isPublic: true
714
+ });
715
+
716
+ // Upload private file
717
+ await uploadFile(mockSupabase, privateFile, {
718
+ orgId: 'org-123',
719
+ category: FileCategory.GENERAL_DOCUMENTS,
720
+ isPublic: false
721
+ });
722
+
723
+ // Get public URL
724
+ const publicUrl = getPublicUrl(mockSupabase, 'org-123/images/public.jpg', true);
725
+ expect(publicUrl).toBe('https://example.com/public.jpg');
726
+
727
+ // Get signed URL
728
+ const signedResult = await getSignedUrl(mockSupabase, 'org-123/documents/private.pdf', {
729
+ appName: 'test-app',
730
+ orgId: 'org-123'
731
+ });
732
+ expect(signedResult?.url).toBe('https://example.com/signed/private.pdf?token=abc');
733
+ });
734
+ });
735
+ });