@jmruthers/pace-core 0.5.86 → 0.5.88

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 (248) hide show
  1. package/dist/{AuthService-Df3IozMG.d.ts → AuthService-DcTI5Ov4.d.ts} +9 -0
  2. package/dist/{DataTable-DKGTBLWT.js → DataTable-PWBMKMOG.js} +8 -8
  3. package/dist/{PublicLoadingSpinner-CnUaz0vG.d.ts → PublicLoadingSpinner-BQXD1fbO.d.ts} +161 -131
  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-YVUZWLQG.js → chunk-AQGF5OG7.js} +3 -3
  11. package/dist/{chunk-CVMVPYAL.js → chunk-BDZUMRBD.js} +3 -5
  12. package/dist/chunk-BDZUMRBD.js.map +1 -0
  13. package/dist/{chunk-QCCJ3P4W.js → chunk-BNXBJOGL.js} +5 -5
  14. package/dist/{chunk-IBMPGOCN.js → chunk-CJIZS3UE.js} +1430 -783
  15. package/dist/chunk-CJIZS3UE.js.map +1 -0
  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-KUYWZVR2.js → chunk-H3P2RGKZ.js} +353 -9
  20. package/dist/chunk-H3P2RGKZ.js.map +1 -0
  21. package/dist/{chunk-RIXPZJUB.js → chunk-KTPG5VCH.js} +2 -2
  22. package/dist/{chunk-YCKPEMJA.js → chunk-QPCAGLUS.js} +2 -3
  23. package/dist/chunk-QPCAGLUS.js.map +1 -0
  24. package/dist/{chunk-WUXCWRL6.js → chunk-XJ2HZOBU.js} +6 -1
  25. package/dist/chunk-XJ2HZOBU.js.map +1 -0
  26. package/dist/{chunk-V5SWX6KL.js → chunk-XXVM53P4.js} +4 -4
  27. package/dist/{chunk-I2VVV5PQ.js → chunk-YY4YYM3E.js} +2 -2
  28. package/dist/components.d.ts +6 -55
  29. package/dist/components.js +25 -206
  30. package/dist/components.js.map +1 -1
  31. package/dist/{file-reference-9xUOnwyt.d.ts → file-reference-C9isKNPn.d.ts} +67 -2
  32. package/dist/hooks.js +10 -9
  33. package/dist/hooks.js.map +1 -1
  34. package/dist/index.d.ts +152 -26
  35. package/dist/index.js +65 -195
  36. package/dist/index.js.map +1 -1
  37. package/dist/providers.d.ts +5 -3
  38. package/dist/providers.js +3 -3
  39. package/dist/rbac/index.js +8 -8
  40. package/dist/types.d.ts +2 -1
  41. package/dist/types.js +3 -3
  42. package/dist/utils.js +2 -2
  43. package/docs/DOCUMENTATION_AUDIT.md +6 -6
  44. package/docs/DOCUMENTATION_STANDARD.md +137 -0
  45. package/docs/README.md +1 -1
  46. package/docs/api/classes/ColumnFactory.md +1 -1
  47. package/docs/api/classes/ErrorBoundary.md +1 -1
  48. package/docs/api/classes/InvalidScopeError.md +1 -1
  49. package/docs/api/classes/MissingUserContextError.md +1 -1
  50. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  51. package/docs/api/classes/PermissionDeniedError.md +1 -1
  52. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  53. package/docs/api/classes/RBACAuditManager.md +1 -1
  54. package/docs/api/classes/RBACCache.md +1 -1
  55. package/docs/api/classes/RBACEngine.md +1 -1
  56. package/docs/api/classes/RBACError.md +1 -1
  57. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  58. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  59. package/docs/api/classes/StorageUtils.md +83 -40
  60. package/docs/api/enums/FileCategory.md +56 -1
  61. package/docs/api/interfaces/AggregateConfig.md +1 -1
  62. package/docs/api/interfaces/ButtonProps.md +1 -1
  63. package/docs/api/interfaces/CardProps.md +1 -1
  64. package/docs/api/interfaces/ColorPalette.md +1 -1
  65. package/docs/api/interfaces/ColorShade.md +1 -1
  66. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  67. package/docs/api/interfaces/DataRecord.md +1 -1
  68. package/docs/api/interfaces/DataTableAction.md +1 -1
  69. package/docs/api/interfaces/DataTableColumn.md +1 -1
  70. package/docs/api/interfaces/DataTableProps.md +1 -1
  71. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  72. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  73. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  74. package/docs/api/interfaces/EventLogoProps.md +11 -11
  75. package/docs/api/interfaces/FileDisplayProps.md +10 -10
  76. package/docs/api/interfaces/FileMetadata.md +1 -1
  77. package/docs/api/interfaces/FileReference.md +1 -1
  78. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  79. package/docs/api/interfaces/FileUploadOptions.md +8 -8
  80. package/docs/api/interfaces/FileUploadProps.md +137 -42
  81. package/docs/api/interfaces/FooterProps.md +1 -1
  82. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  83. package/docs/api/interfaces/InputProps.md +1 -1
  84. package/docs/api/interfaces/LabelProps.md +1 -1
  85. package/docs/api/interfaces/LoginFormProps.md +1 -1
  86. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  87. package/docs/api/interfaces/NavigationContextType.md +1 -1
  88. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  89. package/docs/api/interfaces/NavigationItem.md +1 -1
  90. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  91. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  92. package/docs/api/interfaces/Organisation.md +1 -1
  93. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  94. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  95. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  96. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  97. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  98. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  99. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  100. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  101. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  102. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  103. package/docs/api/interfaces/PaletteData.md +1 -1
  104. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  105. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  106. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  107. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  108. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  109. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  110. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  111. package/docs/api/interfaces/RBACConfig.md +1 -1
  112. package/docs/api/interfaces/RBACLogger.md +1 -1
  113. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  114. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  115. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  116. package/docs/api/interfaces/RouteConfig.md +1 -1
  117. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  118. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  119. package/docs/api/interfaces/StorageConfig.md +1 -1
  120. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  121. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  122. package/docs/api/interfaces/StorageListOptions.md +1 -1
  123. package/docs/api/interfaces/StorageListResult.md +1 -1
  124. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  125. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  126. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  127. package/docs/api/interfaces/StyleImport.md +1 -1
  128. package/docs/api/interfaces/SwitchProps.md +1 -1
  129. package/docs/api/interfaces/ToastActionElement.md +1 -1
  130. package/docs/api/interfaces/ToastProps.md +1 -1
  131. package/docs/api/interfaces/UnifiedAuthContextType.md +83 -50
  132. package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
  133. package/docs/api/interfaces/UseEventLogoOptions.md +74 -0
  134. package/docs/api/interfaces/UseEventLogoReturn.md +81 -0
  135. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  136. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  137. package/docs/api/interfaces/UsePublicEventLogoOptions.md +6 -6
  138. package/docs/api/interfaces/UsePublicEventLogoReturn.md +6 -6
  139. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  140. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  141. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  142. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  143. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  144. package/docs/api/interfaces/UserEventAccess.md +11 -11
  145. package/docs/api/interfaces/UserMenuProps.md +1 -1
  146. package/docs/api/interfaces/UserProfile.md +1 -1
  147. package/docs/api/modules.md +292 -97
  148. package/docs/api-reference/components.md +1 -18
  149. package/docs/api-reference/hooks.md +1 -4
  150. package/docs/best-practices/testing.md +2 -0
  151. package/docs/documentation-index.md +1 -1
  152. package/docs/getting-started/faq.md +1 -1
  153. package/docs/implementation-guides/file-reference-system.md +592 -58
  154. package/docs/implementation-guides/file-upload-storage.md +137 -73
  155. package/docs/rbac/super-admin-guide.md +18 -70
  156. package/docs/testing/README.md +2 -0
  157. package/package.json +1 -1
  158. package/src/__tests__/TEST_STANDARD.md +674 -0
  159. package/src/__tests__/helpers/test-utils.tsx +3 -2
  160. package/src/components/DataTable/__tests__/{DataTable.comprehensive.test.tsx.skip → DataTable.comprehensive.test.tsx} +17 -18
  161. package/src/components/DataTable/__tests__/{DataTable.test.tsx.skip → DataTable.test.tsx} +14 -22
  162. package/src/components/DataTable/__tests__/{ssr.strict-mode.test.tsx.skip → ssr.strict-mode.test.tsx} +42 -47
  163. package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +1 -1
  164. package/src/components/DataTable/examples/__tests__/PerformanceExample.test.tsx +13 -4
  165. package/src/components/DataTable/utils/__tests__/COVERAGE_NOTE.md +1 -1
  166. package/src/components/DataTable/utils/__tests__/performanceUtils.test.ts +10 -6
  167. package/src/components/DataTable/utils/performanceUtils.ts +12 -3
  168. package/src/components/FileDisplay/FileDisplay.test.tsx +257 -0
  169. package/src/components/{FileDisplay.tsx → FileDisplay/FileDisplay.tsx} +111 -10
  170. package/src/components/FileDisplay/index.tsx +4 -0
  171. package/src/components/FileUpload/FileUpload.test.tsx +171 -621
  172. package/src/components/FileUpload/FileUpload.tsx +512 -168
  173. package/src/components/FileUpload/index.tsx +4 -0
  174. package/src/components/Progress/Progress.test.tsx +38 -0
  175. package/src/components/PublicLayout/EventLogo.tsx +220 -39
  176. package/src/components/PublicLayout/PublicPageProvider.tsx +1 -1
  177. package/src/components/Select/Select.test.tsx +1 -1
  178. package/src/components/SessionRestorationLoader.tsx +48 -0
  179. package/src/components/Toast/Toast.tsx +13 -8
  180. package/src/components/index.ts +16 -16
  181. package/src/hooks/__tests__/ServiceHooks.test.tsx +615 -0
  182. package/src/hooks/public/usePublicEventLogo.ts +17 -7
  183. package/src/hooks/useDataTablePerformance.ts +4 -0
  184. package/src/hooks/useEventLogo.ts +316 -0
  185. package/src/hooks/useEvents.ts +0 -5
  186. package/src/hooks/useFileReference.test.ts +659 -0
  187. package/src/hooks/useFileReference.ts +207 -3
  188. package/src/hooks/useSessionRestoration.ts +64 -0
  189. package/src/index.ts +17 -5
  190. package/src/providers/{UnifiedAuthProvider.test.simple.tsx → UnifiedAuthProvider.smoke.test.tsx} +81 -60
  191. package/src/providers/services/AuthServiceProvider.tsx +27 -3
  192. package/src/providers/services/UnifiedAuthProvider.tsx +34 -5
  193. package/src/rbac/{engine.test.simple.ts → RBACEngine.smoke.test.ts} +17 -12
  194. package/src/services/AuthService.ts +142 -20
  195. package/src/services/EventService.ts +0 -4
  196. package/src/types/auth.ts +15 -0
  197. package/src/types/file-reference.ts +73 -1
  198. package/src/types/index.ts +1 -0
  199. package/src/utils/__tests__/organisationContext.unit.test.ts +2 -4
  200. package/src/utils/appNameResolver.simple.test.ts +99 -29
  201. package/src/utils/file-reference.test.ts +535 -0
  202. package/src/utils/file-reference.ts +200 -30
  203. package/src/utils/organisationContext.test.ts +5 -19
  204. package/src/utils/organisationContext.ts +3 -5
  205. package/src/utils/storage/README.md +269 -262
  206. package/src/utils/storage/config.ts +9 -0
  207. package/src/utils/storage/helpers.test.ts +631 -0
  208. package/src/utils/storage/helpers.ts +112 -14
  209. package/src/utils/storage/index.ts +3 -0
  210. package/src/validation/__tests__/sanitization.unit.test.ts +1 -1
  211. package/src/validation/__tests__/schemaUtils.unit.test.ts +1 -1
  212. package/src/validation/__tests__/user.unit.test.ts +1 -1
  213. package/dist/chunk-5BN3YGNK.js.map +0 -1
  214. package/dist/chunk-CVMVPYAL.js.map +0 -1
  215. package/dist/chunk-IBMPGOCN.js.map +0 -1
  216. package/dist/chunk-KUYWZVR2.js.map +0 -1
  217. package/dist/chunk-WUXCWRL6.js.map +0 -1
  218. package/dist/chunk-YCKPEMJA.js.map +0 -1
  219. package/docs/CONTENT_AUDIT_REPORT.md +0 -253
  220. package/docs/STYLE_GUIDE.md +0 -37
  221. package/examples/RBAC/__tests__/PermissionExample.test.tsx +0 -150
  222. package/examples/public-pages/__tests__/PublicPageUsageExample.test.tsx +0 -159
  223. package/src/__tests__/TEST_GUIDE_CURSOR.md +0 -1605
  224. package/src/__tests__/TEST_GUIDE_HUMAN.md +0 -103
  225. package/src/components/FileUpload/FileUpload.example.tsx +0 -218
  226. package/src/components/FileUpload/index.ts +0 -6
  227. package/src/components/FileUpload.tsx +0 -176
  228. package/src/components/Progress/index.ts +0 -3
  229. package/src/components/PublicLayout/__tests__/EventLogo.test.tsx +0 -666
  230. package/src/components/SuperAdminGuard.tsx +0 -116
  231. package/src/components/__tests__/FileDisplay.test.tsx +0 -575
  232. package/src/components/__tests__/FileUpload.test.tsx +0 -446
  233. package/src/components/__tests__/SuperAdminGuard.test.tsx +0 -627
  234. package/src/components/examples/PermissionExample.tsx +0 -173
  235. package/src/hooks/__tests__/usePublicEvent.unit.test.ts +0 -583
  236. package/src/hooks/__tests__/usePublicEventLogo.unit.test.ts +0 -640
  237. package/src/types/__tests__/file-reference.test.ts +0 -447
  238. package/src/utils/__tests__/file-reference.test.ts +0 -383
  239. /package/dist/{DataTable-DKGTBLWT.js.map → DataTable-PWBMKMOG.js.map} +0 -0
  240. /package/dist/{UnifiedAuthProvider-K2IZAY5F.js.map → UnifiedAuthProvider-5D3HEQND.js.map} +0 -0
  241. /package/dist/{chunk-NTW3KGS4.js.map → chunk-6UHXQH7P.js.map} +0 -0
  242. /package/dist/{chunk-YVUZWLQG.js.map → chunk-AQGF5OG7.js.map} +0 -0
  243. /package/dist/{chunk-QCCJ3P4W.js.map → chunk-BNXBJOGL.js.map} +0 -0
  244. /package/dist/{chunk-S3JKDMD5.js.map → chunk-CXKMRKRF.js.map} +0 -0
  245. /package/dist/{chunk-RIXPZJUB.js.map → chunk-KTPG5VCH.js.map} +0 -0
  246. /package/dist/{chunk-V5SWX6KL.js.map → chunk-XXVM53P4.js.map} +0 -0
  247. /package/dist/{chunk-I2VVV5PQ.js.map → chunk-YY4YYM3E.js.map} +0 -0
  248. /package/src/providers/{OrganisationProvider.test.simple.tsx → OrganisationProvider.context.test.tsx} +0 -0
@@ -0,0 +1,631 @@
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('generates public URL for private bucket', () => {
374
+ const filePath = 'org-123/documents/test.pdf';
375
+
376
+ const mockGetPublicUrl = vi.fn().mockReturnValue({
377
+ data: { publicUrl: 'https://example.com/files/test.pdf' }
378
+ });
379
+
380
+ mockSupabase.storage = {
381
+ from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
382
+ };
383
+
384
+ const result = getPublicUrl(mockSupabase, filePath, false);
385
+
386
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('files');
387
+ expect(result).toBe('https://example.com/files/test.pdf');
388
+ });
389
+
390
+ it('validates required parameters', () => {
391
+ expect(() => getPublicUrl(null as any, 'path', false))
392
+ .toThrow();
393
+
394
+ expect(() => getPublicUrl(mockSupabase, '', false))
395
+ .toThrow();
396
+ });
397
+ });
398
+
399
+ describe('getSignedUrl', () => {
400
+ it('generates signed URL with default expiration', async () => {
401
+ const filePath = 'org-123/documents/test.pdf';
402
+ const options = {
403
+ appName: 'test-app',
404
+ orgId: 'org-123'
405
+ };
406
+
407
+ const mockCreateSignedUrl = vi.fn().mockResolvedValue({
408
+ data: { signedUrl: 'https://example.com/signed/test.pdf?token=abc123' },
409
+ error: null
410
+ });
411
+
412
+ mockSupabase.storage = {
413
+ from: vi.fn(() => ({ createSignedUrl: mockCreateSignedUrl }))
414
+ };
415
+
416
+ const result = await getSignedUrl(mockSupabase, filePath, options);
417
+
418
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('files');
419
+ expect(mockCreateSignedUrl).toHaveBeenCalledWith(filePath, 3600);
420
+ expect(result?.url).toBe('https://example.com/signed/test.pdf?token=abc123');
421
+ });
422
+
423
+ it('generates signed URL with custom expiration', async () => {
424
+ const filePath = 'org-123/documents/test.pdf';
425
+ const options = {
426
+ appName: 'test-app',
427
+ orgId: 'org-123',
428
+ expiresIn: 7200
429
+ };
430
+
431
+ const mockCreateSignedUrl = vi.fn().mockResolvedValue({
432
+ data: { signedUrl: 'https://example.com/signed/test.pdf?token=def456' },
433
+ error: null
434
+ });
435
+
436
+ mockSupabase.storage = {
437
+ from: vi.fn(() => ({ createSignedUrl: mockCreateSignedUrl }))
438
+ };
439
+
440
+ await getSignedUrl(mockSupabase, filePath, options);
441
+
442
+ expect(mockCreateSignedUrl).toHaveBeenCalledWith(filePath, 7200);
443
+ });
444
+
445
+ it('returns null when signed URL generation fails', async () => {
446
+ const filePath = 'org-123/documents/test.pdf';
447
+ const options = {
448
+ appName: 'test-app',
449
+ orgId: 'org-123'
450
+ };
451
+
452
+ const mockCreateSignedUrl = vi.fn().mockResolvedValue({
453
+ data: null,
454
+ error: { message: 'Permission denied' }
455
+ });
456
+
457
+ mockSupabase.storage = {
458
+ from: vi.fn(() => ({ createSignedUrl: mockCreateSignedUrl }))
459
+ };
460
+
461
+ const result = await getSignedUrl(mockSupabase, filePath, options);
462
+
463
+ expect(result).toBe(null);
464
+ });
465
+
466
+ it('validates required parameters', async () => {
467
+ const options = { appName: 'test-app', orgId: 'org-123' };
468
+
469
+ const gsNoClient = await getSignedUrl(null as any, 'path', options);
470
+ expect(gsNoClient).toBe(null);
471
+
472
+ const gsNoPath = await getSignedUrl(mockSupabase, '', options);
473
+ expect(gsNoPath).toBe(null);
474
+
475
+ const gsNoOpts = await getSignedUrl(mockSupabase, 'path', {} as any);
476
+ expect(gsNoOpts).toBe(null);
477
+
478
+ const gsNoOrg = await getSignedUrl(mockSupabase, 'path', { appName: 'test' } as any);
479
+ expect(gsNoOrg).toBe(null);
480
+ });
481
+
482
+ it('includes expiration metadata in result', async () => {
483
+ const filePath = 'org-123/documents/test.pdf';
484
+ const options = {
485
+ appName: 'test-app',
486
+ orgId: 'org-123',
487
+ expiresIn: 3600
488
+ };
489
+
490
+ const mockCreateSignedUrl = vi.fn().mockResolvedValue({
491
+ data: { signedUrl: 'https://example.com/signed/test.pdf?token=abc123' },
492
+ error: null
493
+ });
494
+
495
+ mockSupabase.storage = {
496
+ from: vi.fn(() => ({ createSignedUrl: mockCreateSignedUrl }))
497
+ };
498
+
499
+ const result = await getSignedUrl(mockSupabase, filePath, options);
500
+
501
+ expect(result?.url).toContain('signed');
502
+ expect(typeof result?.expiresAt).toBe('string');
503
+ });
504
+ });
505
+
506
+ describe('Error Handling', () => {
507
+ it('handles storage client errors gracefully', async () => {
508
+ const testFile = createTestFile();
509
+ const options = {
510
+ orgId: 'test-org-123',
511
+ category: FileCategory.GENERAL_DOCUMENTS,
512
+ isPublic: false
513
+ };
514
+
515
+ mockSupabase.storage = {
516
+ from: vi.fn(() => {
517
+ throw new Error('Storage client error');
518
+ })
519
+ };
520
+
521
+ const res = await uploadFile(mockSupabase, testFile, options);
522
+ expect(res.success).toBe(false);
523
+ });
524
+
525
+ it('handles network timeouts gracefully', async () => {
526
+ const filePath = 'org-123/documents/test.pdf';
527
+
528
+ const mockDownload = vi.fn().mockRejectedValue(new Error('Network timeout'));
529
+
530
+ mockSupabase.storage = {
531
+ from: vi.fn(() => ({ download: mockDownload }))
532
+ };
533
+
534
+ const dl = await downloadFile(mockSupabase, filePath, false);
535
+ expect(dl).toBe(null);
536
+ });
537
+ });
538
+
539
+ describe('Integration Scenarios', () => {
540
+ it('handles complete upload-download cycle', async () => {
541
+ const testFile = createTestFile('integration-test.pdf', 'application/pdf', 2048);
542
+ const uploadOptions = {
543
+ orgId: 'test-org-123',
544
+ category: FileCategory.GENERAL_DOCUMENTS,
545
+ isPublic: false
546
+ };
547
+
548
+ // Mock upload
549
+ const mockUpload = vi.fn().mockResolvedValue({
550
+ data: { path: 'org-123/documents/integration-test.pdf' },
551
+ error: null
552
+ });
553
+
554
+ // Mock download
555
+ const mockDownload = vi.fn().mockResolvedValue({
556
+ data: new Blob(['file content'], { type: 'application/pdf' }),
557
+ error: null
558
+ });
559
+
560
+ const mockList = vi.fn().mockResolvedValue({ data: [{ metadata: { size: 2048, mimetype: 'application/pdf' } }], error: null });
561
+ mockSupabase.storage = {
562
+ from: vi.fn(() => ({
563
+ upload: mockUpload,
564
+ download: mockDownload,
565
+ list: mockList
566
+ }))
567
+ };
568
+
569
+ // Upload file
570
+ const uploadResult = await uploadFile(mockSupabase, testFile, uploadOptions);
571
+ expect(uploadResult.success).toBe(true);
572
+
573
+ // Download file
574
+ const downloadResult = await downloadFile(mockSupabase, uploadResult.path!, false);
575
+ expect(downloadResult?.blob).toBeInstanceOf(Blob);
576
+ expect(downloadResult?.metadata.type).toBe('application/pdf');
577
+ });
578
+
579
+ it('handles public vs private file workflows', async () => {
580
+ const publicFile = createTestFile('public.jpg', 'image/jpeg');
581
+ const privateFile = createTestFile('private.pdf', 'application/pdf');
582
+
583
+ const mockUpload = vi.fn().mockResolvedValue({
584
+ data: { path: 'test-path' },
585
+ error: null
586
+ });
587
+
588
+ const mockGetPublicUrl = vi.fn().mockReturnValue({
589
+ data: { publicUrl: 'https://example.com/public.jpg' }
590
+ });
591
+
592
+ const mockCreateSignedUrl = vi.fn().mockResolvedValue({
593
+ data: { signedUrl: 'https://example.com/signed/private.pdf?token=abc' },
594
+ error: null
595
+ });
596
+
597
+ mockSupabase.storage = {
598
+ from: vi.fn(() => ({
599
+ upload: mockUpload,
600
+ getPublicUrl: mockGetPublicUrl,
601
+ createSignedUrl: mockCreateSignedUrl
602
+ }))
603
+ };
604
+
605
+ // Upload public file
606
+ await uploadFile(mockSupabase, publicFile, {
607
+ orgId: 'org-123',
608
+ category: FileCategory.IMAGES,
609
+ isPublic: true
610
+ });
611
+
612
+ // Upload private file
613
+ await uploadFile(mockSupabase, privateFile, {
614
+ orgId: 'org-123',
615
+ category: FileCategory.GENERAL_DOCUMENTS,
616
+ isPublic: false
617
+ });
618
+
619
+ // Get public URL
620
+ const publicUrl = getPublicUrl(mockSupabase, 'org-123/images/public.jpg', true);
621
+ expect(publicUrl).toBe('https://example.com/public.jpg');
622
+
623
+ // Get signed URL
624
+ const signedResult = await getSignedUrl(mockSupabase, 'org-123/documents/private.pdf', {
625
+ appName: 'test-app',
626
+ orgId: 'org-123'
627
+ });
628
+ expect(signedResult?.url).toBe('https://example.com/signed/private.pdf?token=abc');
629
+ });
630
+ });
631
+ });