@jmruthers/pace-core 0.5.75 → 0.5.76

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 (226) hide show
  1. package/dist/{DataTable-HWZQGASI.js → DataTable-4GAVPIEG.js} +48 -30
  2. package/dist/{PublicLoadingSpinner-BKNBT6b6.d.ts → PublicLoadingSpinner-BiNER8F5.d.ts} +28 -17
  3. package/dist/{chunk-33PHABLB.js → chunk-AFGTSUAD.js} +10 -127
  4. package/dist/chunk-AFGTSUAD.js.map +1 -0
  5. package/dist/{chunk-2DFZ432F.js → chunk-K34IM5CT.js} +3 -5
  6. package/dist/{chunk-2DFZ432F.js.map → chunk-K34IM5CT.js.map} +1 -1
  7. package/dist/{chunk-2CHATWBF.js → chunk-KHJS6VIA.js} +199 -35
  8. package/dist/chunk-KHJS6VIA.js.map +1 -0
  9. package/dist/{chunk-ZTT2AXMX.js → chunk-KK73ZB4E.js} +2 -2
  10. package/dist/{chunk-CY3AHGO4.js → chunk-M5IWZRBT.js} +1750 -2815
  11. package/dist/chunk-M5IWZRBT.js.map +1 -0
  12. package/dist/{chunk-DAXLNIDY.js → chunk-Y6TXWPJO.js} +6 -4
  13. package/dist/{chunk-DAXLNIDY.js.map → chunk-Y6TXWPJO.js.map} +1 -1
  14. package/dist/{chunk-YNUBMSMV.js → chunk-YCKPEMJA.js} +186 -263
  15. package/dist/chunk-YCKPEMJA.js.map +1 -0
  16. package/dist/components.d.ts +1 -1
  17. package/dist/components.js +7 -6
  18. package/dist/components.js.map +1 -1
  19. package/dist/hooks.d.ts +17 -40
  20. package/dist/hooks.js +6 -6
  21. package/dist/index.d.ts +3 -3
  22. package/dist/index.js +12 -10
  23. package/dist/index.js.map +1 -1
  24. package/dist/rbac/index.d.ts +54 -1
  25. package/dist/rbac/index.js +5 -4
  26. package/dist/utils.js +1 -1
  27. package/docs/TERMINOLOGY.md +231 -0
  28. package/docs/api/classes/ColumnFactory.md +1 -1
  29. package/docs/api/classes/ErrorBoundary.md +1 -1
  30. package/docs/api/classes/InvalidScopeError.md +1 -1
  31. package/docs/api/classes/MissingUserContextError.md +1 -1
  32. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  33. package/docs/api/classes/PermissionDeniedError.md +1 -1
  34. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  35. package/docs/api/classes/RBACAuditManager.md +1 -1
  36. package/docs/api/classes/RBACCache.md +1 -1
  37. package/docs/api/classes/RBACEngine.md +1 -1
  38. package/docs/api/classes/RBACError.md +1 -1
  39. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  40. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  41. package/docs/api/classes/StorageUtils.md +1 -1
  42. package/docs/api/enums/FileCategory.md +1 -1
  43. package/docs/api/interfaces/AggregateConfig.md +1 -1
  44. package/docs/api/interfaces/ButtonProps.md +1 -1
  45. package/docs/api/interfaces/CardProps.md +1 -1
  46. package/docs/api/interfaces/ColorPalette.md +1 -1
  47. package/docs/api/interfaces/ColorShade.md +1 -1
  48. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  49. package/docs/api/interfaces/DataTableAction.md +1 -1
  50. package/docs/api/interfaces/DataTableColumn.md +1 -1
  51. package/docs/api/interfaces/DataTableProps.md +1 -1
  52. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  53. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  54. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  55. package/docs/api/interfaces/EventLogoProps.md +1 -1
  56. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  57. package/docs/api/interfaces/FileMetadata.md +1 -1
  58. package/docs/api/interfaces/FileReference.md +1 -1
  59. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  60. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  61. package/docs/api/interfaces/FileUploadProps.md +1 -1
  62. package/docs/api/interfaces/FooterProps.md +1 -1
  63. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  64. package/docs/api/interfaces/InputProps.md +1 -1
  65. package/docs/api/interfaces/LabelProps.md +1 -1
  66. package/docs/api/interfaces/LoginFormProps.md +1 -1
  67. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  68. package/docs/api/interfaces/NavigationContextType.md +1 -1
  69. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  70. package/docs/api/interfaces/NavigationItem.md +1 -1
  71. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  72. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  73. package/docs/api/interfaces/Organisation.md +1 -1
  74. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  75. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  76. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  77. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  78. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  79. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  80. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  81. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  82. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  83. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  84. package/docs/api/interfaces/PaletteData.md +1 -1
  85. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  86. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  87. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  88. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  89. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  90. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  91. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  92. package/docs/api/interfaces/RBACConfig.md +1 -1
  93. package/docs/api/interfaces/RBACContextType.md +1 -1
  94. package/docs/api/interfaces/RBACLogger.md +1 -1
  95. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  96. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  97. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  98. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  99. package/docs/api/interfaces/RouteConfig.md +1 -1
  100. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  101. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  102. package/docs/api/interfaces/StorageConfig.md +1 -1
  103. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  104. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  105. package/docs/api/interfaces/StorageListOptions.md +1 -1
  106. package/docs/api/interfaces/StorageListResult.md +1 -1
  107. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  108. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  109. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  110. package/docs/api/interfaces/StyleImport.md +1 -1
  111. package/docs/api/interfaces/SwitchProps.md +1 -1
  112. package/docs/api/interfaces/ToastActionElement.md +1 -1
  113. package/docs/api/interfaces/ToastProps.md +1 -1
  114. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  115. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  116. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  117. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  118. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  119. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  120. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  121. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  122. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  123. package/docs/api/interfaces/UseResolvedScopeOptions.md +47 -0
  124. package/docs/api/interfaces/UseResolvedScopeReturn.md +47 -0
  125. package/docs/api/interfaces/UserEventAccess.md +1 -1
  126. package/docs/api/interfaces/UserMenuProps.md +1 -1
  127. package/docs/api/interfaces/UserProfile.md +1 -1
  128. package/docs/api/modules.md +57 -11
  129. package/docs/api-reference/providers.md +26 -7
  130. package/docs/best-practices/README.md +20 -0
  131. package/docs/best-practices/accessibility.md +566 -0
  132. package/docs/best-practices/performance-expansion.md +473 -0
  133. package/docs/core-concepts/authentication.md +15 -7
  134. package/docs/documentation-index.md +1 -1
  135. package/docs/documentation-templates.md +539 -0
  136. package/docs/getting-started/quick-start.md +16 -66
  137. package/docs/implementation-guides/component-styling.md +410 -0
  138. package/docs/implementation-guides/data-tables.md +1 -1
  139. package/docs/style-guide.md +39 -0
  140. package/package.json +1 -1
  141. package/src/__tests__/TEST_GUIDE_CURSOR.md +290 -0
  142. package/src/__tests__/helpers/supabaseMock.ts +48 -2
  143. package/src/components/DataTable/__tests__/DataTable.default-state.test.tsx +17 -6
  144. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +73 -9
  145. package/src/components/DataTable/components/DataTableCore.tsx +280 -475
  146. package/src/components/DataTable/components/UnifiedTableBody.tsx +120 -153
  147. package/src/components/DataTable/components/index.ts +1 -2
  148. package/src/components/DataTable/context/__tests__/DataTableContext.test.tsx +208 -275
  149. package/src/components/DataTable/core/index.ts +1 -8
  150. package/src/components/DataTable/hooks/__tests__/useColumnOrderPersistence.test.ts +525 -0
  151. package/src/components/DataTable/hooks/__tests__/useColumnReordering.test.ts +570 -0
  152. package/src/components/DataTable/hooks/__tests__/useHierarchicalState.test.ts +214 -0
  153. package/src/components/DataTable/hooks/__tests__/useTableColumns.test.ts +224 -0
  154. package/src/components/DataTable/hooks/index.ts +6 -0
  155. package/src/components/DataTable/hooks/useColumnReordering.ts +1 -0
  156. package/src/components/DataTable/hooks/useDataTablePermissions.ts +149 -0
  157. package/src/components/DataTable/hooks/useDataTableState.ts +12 -6
  158. package/src/components/DataTable/hooks/useHierarchicalState.ts +26 -8
  159. package/src/components/DataTable/hooks/useTableColumns.ts +153 -0
  160. package/src/components/DataTable/index.ts +1 -9
  161. package/src/components/DataTable/utils/__tests__/COVERAGE_NOTE.md +89 -0
  162. package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +3 -6
  163. package/src/components/DataTable/utils/__tests__/flexibleImport.test.ts +462 -0
  164. package/src/components/DataTable/utils/__tests__/hierarchicalSorting.test.ts +247 -0
  165. package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +8 -6
  166. package/src/components/DataTable/utils/__tests__/performanceUtils.test.ts +466 -0
  167. package/src/components/DataTable/utils/__tests__/rowUtils.test.ts +265 -0
  168. package/src/components/DataTable/utils/errorHandling.ts +52 -460
  169. package/src/components/DataTable/utils/exportUtils.ts +46 -15
  170. package/src/components/DataTable/utils/hierarchicalSorting.ts +50 -3
  171. package/src/components/DataTable/utils/hierarchicalUtils.ts +167 -34
  172. package/src/components/DataTable/utils/index.ts +5 -0
  173. package/src/components/DataTable/utils/rowUtils.ts +68 -0
  174. package/src/components/EventSelector/EventSelector.test.tsx +672 -0
  175. package/src/components/Label/__tests__/Label.test.tsx +434 -0
  176. package/src/components/PublicLayout/__tests__/PublicPageContextChecker.test.tsx +190 -0
  177. package/src/components/PublicLayout/__tests__/PublicPageDebugger.test.tsx +185 -0
  178. package/src/components/PublicLayout/__tests__/PublicPageProvider.test.tsx +313 -0
  179. package/src/components/Select/Select.test.tsx +143 -120
  180. package/src/components/Select/Select.tsx +47 -212
  181. package/src/components/Select/hooks.ts +36 -1
  182. package/src/components/Select/index.ts +2 -1
  183. package/src/hooks/services/__tests__/useServiceHooks.test.tsx +137 -0
  184. package/src/hooks/useSecureDataAccess.test.ts +32 -29
  185. package/src/providers/__tests__/ProviderLifecycle.test.tsx +341 -0
  186. package/src/rbac/hooks/__tests__/usePermissions.integration.test.ts +437 -0
  187. package/src/rbac/hooks/index.ts +2 -0
  188. package/src/rbac/hooks/useResolvedScope.ts +232 -0
  189. package/src/services/__tests__/InactivityService.lifecycle.test.ts +411 -0
  190. package/src/services/__tests__/OrganisationService.pagination.test.ts +375 -0
  191. package/src/types/__tests__/README.md +114 -0
  192. package/src/types/__tests__/validation.test.ts +731 -0
  193. package/src/utils/__tests__/file-reference.test.ts +383 -0
  194. package/src/utils/__tests__/performanceBenchmark.test.ts +175 -0
  195. package/src/utils/appNameResolver.test.ts +54 -0
  196. package/src/validation/__tests__/csrf.unit.test.ts +63 -0
  197. package/src/validation/__tests__/passwordSchema.unit.test.ts +105 -0
  198. package/dist/chunk-2CHATWBF.js.map +0 -1
  199. package/dist/chunk-33PHABLB.js.map +0 -1
  200. package/dist/chunk-CY3AHGO4.js.map +0 -1
  201. package/dist/chunk-TYHR5X4W.js +0 -33
  202. package/dist/chunk-TYHR5X4W.js.map +0 -1
  203. package/dist/chunk-YNUBMSMV.js.map +0 -1
  204. package/dist/eventContext-BBA42P6G.js +0 -14
  205. package/dist/eventContext-BBA42P6G.js.map +0 -1
  206. package/docs/documentation-style-checklist.md +0 -294
  207. package/src/components/DataTable/components/DataTableBody.tsx +0 -488
  208. package/src/components/DataTable/components/DraggableColumnHeader.tsx +0 -144
  209. package/src/components/DataTable/components/VirtualizedDataTable.tsx +0 -515
  210. package/src/components/DataTable/core/ActionManager.ts +0 -235
  211. package/src/components/DataTable/core/ColumnManager.ts +0 -215
  212. package/src/components/DataTable/core/DataManager.ts +0 -188
  213. package/src/components/DataTable/core/DataTableContext.tsx +0 -181
  214. package/src/components/DataTable/core/LocalDataAdapter.ts +0 -264
  215. package/src/components/DataTable/core/PluginRegistry.ts +0 -229
  216. package/src/components/DataTable/core/StateManager.ts +0 -311
  217. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +0 -634
  218. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +0 -193
  219. package/src/components/DataTable/core/__tests__/DataManager.test.ts +0 -519
  220. package/src/components/DataTable/core/__tests__/StateManager.test.ts +0 -714
  221. package/src/components/DataTable/core/interfaces.ts +0 -338
  222. package/src/components/DataTable/utils/debugTools.ts +0 -583
  223. package/src/components/Select/Select.bug-test.tsx +0 -69
  224. package/src/components/Select/Select.refactored.tsx +0 -497
  225. /package/dist/{DataTable-HWZQGASI.js.map → DataTable-4GAVPIEG.js.map} +0 -0
  226. /package/dist/{chunk-ZTT2AXMX.js.map → chunk-KK73ZB4E.js.map} +0 -0
@@ -0,0 +1,383 @@
1
+ /**
2
+ * @file File Reference Service Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/__tests__
5
+ * @since 1.0.0
6
+ *
7
+ * Tests for file reference service covering CRUD operations, error handling, and edge cases.
8
+ */
9
+
10
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
11
+ import { FileReferenceServiceImpl } from '../file-reference';
12
+ import type { FileReference, FileUploadOptions } from '../../types/file-reference';
13
+
14
+ // Mock Supabase client
15
+ const createMockSupabaseClient = () => ({
16
+ rpc: vi.fn(),
17
+ from: vi.fn(),
18
+ });
19
+
20
+ // Mock storage helpers
21
+ vi.mock('../storage/helpers', () => ({
22
+ generateFilePath: vi.fn((_, filename) => `path/to/${filename}`),
23
+ uploadFile: vi.fn().mockResolvedValue({ success: true, path: 'path/to/file.jpg' }),
24
+ getPublicUrl: vi.fn((_, path) => `https://example.com/${path}`),
25
+ getSignedUrl: vi.fn().mockResolvedValue('https://example.com/signed-url'),
26
+ deleteFile: vi.fn().mockResolvedValue(true),
27
+ extractFileMetadata: vi.fn().mockReturnValue({ category: 'IMAGES' }), // Sync function
28
+ }));
29
+
30
+ describe('FileReferenceServiceImpl', () => {
31
+ let mockSupabase: ReturnType<typeof createMockSupabaseClient>;
32
+ let fileReferenceService: FileReferenceServiceImpl;
33
+
34
+ beforeEach(() => {
35
+ vi.clearAllMocks();
36
+ mockSupabase = createMockSupabaseClient();
37
+ fileReferenceService = new FileReferenceServiceImpl(mockSupabase as any);
38
+
39
+ // Default mocks
40
+ mockSupabase.rpc.mockResolvedValue({ data: 'ref-123', error: null });
41
+ mockSupabase.from.mockReturnValue({
42
+ select: vi.fn().mockReturnThis(),
43
+ eq: vi.fn().mockReturnThis(),
44
+ single: vi.fn().mockResolvedValue({
45
+ data: {
46
+ id: 'ref-123',
47
+ table_name: 'test_table',
48
+ record_id: 'record-123',
49
+ file_path: 'path/to/file.jpg',
50
+ organisation_id: 'org-123',
51
+ },
52
+ error: null
53
+ })
54
+ });
55
+ });
56
+
57
+ describe('createFileReference', () => {
58
+ const mockOptions: FileUploadOptions = {
59
+ table_name: 'test_table',
60
+ record_id: 'record-123',
61
+ organisation_id: 'org-123',
62
+ app_id: 'app-123',
63
+ category: 'IMAGES',
64
+ is_public: false
65
+ };
66
+
67
+ const mockFile = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
68
+
69
+ it.skip('creates file reference successfully - requires complex mock setup', async () => {
70
+ // Skipped: Complex integration test requiring storage helpers, file metadata extraction
71
+ // Current implementation has dependencies that need comprehensive mocking
72
+ // The other tests cover the service methods without storage integration
73
+ const result = await fileReferenceService.createFileReference(mockOptions, mockFile);
74
+
75
+ expect(result).toBeDefined();
76
+ });
77
+ });
78
+
79
+ describe('getFileReference', () => {
80
+ it('returns file reference when found', async () => {
81
+ const result = await fileReferenceService.getFileReference(
82
+ 'test_table',
83
+ 'record-123',
84
+ 'org-123'
85
+ );
86
+
87
+ expect(result).toBeDefined();
88
+ expect(result?.id).toBe('ref-123');
89
+ expect(mockSupabase.from).toHaveBeenCalledWith('file_references');
90
+ });
91
+
92
+ it('returns null when file reference not found', async () => {
93
+ const mockError = new Error('Not found');
94
+ (mockError as any).code = 'PGRST116';
95
+ mockSupabase.from().single.mockResolvedValueOnce({ data: null, error: mockError });
96
+
97
+ const result = await fileReferenceService.getFileReference(
98
+ 'test_table',
99
+ 'record-123',
100
+ 'org-123'
101
+ );
102
+
103
+ expect(result).toBeNull();
104
+ });
105
+
106
+ it('throws error for non-PGRST116 errors', async () => {
107
+ mockSupabase.from().single.mockResolvedValueOnce({
108
+ data: null,
109
+ error: new Error('Network error')
110
+ });
111
+
112
+ await expect(
113
+ fileReferenceService.getFileReference('test_table', 'record-123', 'org-123')
114
+ ).rejects.toThrow('Failed to get file reference');
115
+ });
116
+ });
117
+
118
+ describe('getFileUrl', () => {
119
+ it('returns file URL successfully', async () => {
120
+ mockSupabase.rpc.mockResolvedValueOnce({
121
+ data: 'https://example.com/file.jpg',
122
+ error: null
123
+ });
124
+
125
+ const result = await fileReferenceService.getFileUrl(
126
+ 'test_table',
127
+ 'record-123',
128
+ 'org-123'
129
+ );
130
+
131
+ expect(result).toBe('https://example.com/file.jpg');
132
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
133
+ 'get_file_url',
134
+ expect.objectContaining({
135
+ p_table_name: 'test_table',
136
+ p_record_id: 'record-123',
137
+ p_organisation_id: 'org-123'
138
+ })
139
+ );
140
+ });
141
+
142
+ it('throws error on RPC failure', async () => {
143
+ mockSupabase.rpc.mockRejectedValueOnce(new Error('RPC error'));
144
+
145
+ await expect(
146
+ fileReferenceService.getFileUrl('test_table', 'record-123', 'org-123')
147
+ ).rejects.toThrow();
148
+ });
149
+ });
150
+
151
+ describe('getSignedUrl', () => {
152
+ it('returns signed URL with default expiration', async () => {
153
+ mockSupabase.rpc.mockResolvedValueOnce({
154
+ data: 'https://example.com/signed-url',
155
+ error: null
156
+ });
157
+
158
+ const result = await fileReferenceService.getSignedUrl(
159
+ 'test_table',
160
+ 'record-123',
161
+ 'org-123'
162
+ );
163
+
164
+ expect(result).toBe('https://example.com/signed-url');
165
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
166
+ 'get_file_signed_url',
167
+ expect.objectContaining({
168
+ p_expires_in: 3600
169
+ })
170
+ );
171
+ });
172
+
173
+ it('returns signed URL with custom expiration', async () => {
174
+ mockSupabase.rpc.mockResolvedValueOnce({
175
+ data: 'https://example.com/signed-url',
176
+ error: null
177
+ });
178
+
179
+ const result = await fileReferenceService.getSignedUrl(
180
+ 'test_table',
181
+ 'record-123',
182
+ 'org-123',
183
+ 7200
184
+ );
185
+
186
+ expect(result).toBe('https://example.com/signed-url');
187
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
188
+ 'get_file_signed_url',
189
+ expect.objectContaining({
190
+ p_expires_in: 7200
191
+ })
192
+ );
193
+ });
194
+
195
+ it('throws error on RPC failure', async () => {
196
+ mockSupabase.rpc.mockRejectedValueOnce(new Error('RPC error'));
197
+
198
+ await expect(
199
+ fileReferenceService.getSignedUrl('test_table', 'record-123', 'org-123')
200
+ ).rejects.toThrow();
201
+ });
202
+ });
203
+
204
+ describe('updateFileReference', () => {
205
+ it('updates file reference successfully', async () => {
206
+ const mockFromReturn = {
207
+ update: vi.fn().mockReturnThis(),
208
+ eq: vi.fn().mockReturnThis(),
209
+ select: vi.fn().mockReturnThis(),
210
+ single: vi.fn().mockResolvedValue({
211
+ data: { id: 'ref-123', table_name: 'test_table' },
212
+ error: null
213
+ })
214
+ };
215
+ mockSupabase.from.mockReturnValue(mockFromReturn as any);
216
+
217
+ const result = await fileReferenceService.updateFileReference('ref-123', {
218
+ file_metadata: { description: 'Updated' }
219
+ });
220
+
221
+ expect(result).toBeDefined();
222
+ expect(mockFromReturn.update).toHaveBeenCalledWith(
223
+ expect.objectContaining({ file_metadata: { description: 'Updated' } })
224
+ );
225
+ });
226
+
227
+ it('throws error on update failure', async () => {
228
+ const mockFromReturn = {
229
+ update: vi.fn().mockReturnThis(),
230
+ eq: vi.fn().mockReturnThis(),
231
+ select: vi.fn().mockReturnThis(),
232
+ single: vi.fn().mockResolvedValue({
233
+ data: null,
234
+ error: new Error('Update failed')
235
+ })
236
+ };
237
+ mockSupabase.from.mockReturnValue(mockFromReturn as any);
238
+
239
+ await expect(
240
+ fileReferenceService.updateFileReference('ref-123', {})
241
+ ).rejects.toThrow();
242
+ });
243
+ });
244
+
245
+ describe('deleteFileReference', () => {
246
+ it('deletes file reference without deleting file', async () => {
247
+ mockSupabase.rpc.mockResolvedValueOnce({ data: true, error: null });
248
+
249
+ const result = await fileReferenceService.deleteFileReference(
250
+ 'test_table',
251
+ 'record-123',
252
+ 'org-123',
253
+ false
254
+ );
255
+
256
+ expect(result).toBe(true);
257
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
258
+ 'delete_file_reference',
259
+ expect.objectContaining({
260
+ p_delete_file: false
261
+ })
262
+ );
263
+ });
264
+
265
+ it('deletes file reference with file deletion', async () => {
266
+ mockSupabase.rpc.mockResolvedValueOnce({ data: true, error: null });
267
+
268
+ const result = await fileReferenceService.deleteFileReference(
269
+ 'test_table',
270
+ 'record-123',
271
+ 'org-123',
272
+ true
273
+ );
274
+
275
+ expect(result).toBe(true);
276
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
277
+ 'delete_file_reference',
278
+ expect.objectContaining({
279
+ p_delete_file: true
280
+ })
281
+ );
282
+ });
283
+
284
+ it('throws error on delete failure', async () => {
285
+ mockSupabase.rpc.mockRejectedValueOnce(new Error('Delete failed'));
286
+
287
+ await expect(
288
+ fileReferenceService.deleteFileReference('test_table', 'record-123', 'org-123')
289
+ ).rejects.toThrow();
290
+ });
291
+ });
292
+
293
+ describe('listFileReferences', () => {
294
+ it('lists file references successfully', async () => {
295
+ mockSupabase.rpc.mockResolvedValueOnce({
296
+ data: [
297
+ { id: 'ref-1', file_path: 'path1.jpg' },
298
+ { id: 'ref-2', file_path: 'path2.jpg' }
299
+ ],
300
+ error: null
301
+ });
302
+
303
+ const result = await fileReferenceService.listFileReferences(
304
+ 'test_table',
305
+ 'record-123',
306
+ 'org-123'
307
+ );
308
+
309
+ expect(result).toHaveLength(2);
310
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
311
+ 'get_record_files',
312
+ expect.objectContaining({
313
+ p_table_name: 'test_table',
314
+ p_record_id: 'record-123',
315
+ p_organisation_id: 'org-123'
316
+ })
317
+ );
318
+ });
319
+
320
+ it('returns empty array when no files found', async () => {
321
+ mockSupabase.rpc.mockResolvedValueOnce({ data: [], error: null });
322
+
323
+ const result = await fileReferenceService.listFileReferences(
324
+ 'test_table',
325
+ 'record-123',
326
+ 'org-123'
327
+ );
328
+
329
+ expect(result).toEqual([]);
330
+ });
331
+
332
+ it('throws error on RPC failure', async () => {
333
+ mockSupabase.rpc.mockRejectedValueOnce(new Error('RPC error'));
334
+
335
+ await expect(
336
+ fileReferenceService.listFileReferences('test_table', 'record-123', 'org-123')
337
+ ).rejects.toThrow();
338
+ });
339
+ });
340
+
341
+ describe('getFileCount', () => {
342
+ it('returns file count successfully', async () => {
343
+ mockSupabase.rpc.mockResolvedValueOnce({ data: 5, error: null });
344
+
345
+ const result = await fileReferenceService.getFileCount(
346
+ 'test_table',
347
+ 'record-123',
348
+ 'org-123'
349
+ );
350
+
351
+ expect(result).toBe(5);
352
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
353
+ 'get_record_file_count',
354
+ expect.objectContaining({
355
+ p_table_name: 'test_table',
356
+ p_record_id: 'record-123',
357
+ p_organisation_id: 'org-123'
358
+ })
359
+ );
360
+ });
361
+
362
+ it('returns 0 when count query returns null', async () => {
363
+ mockSupabase.rpc.mockResolvedValueOnce({ data: null, error: null });
364
+
365
+ const result = await fileReferenceService.getFileCount(
366
+ 'test_table',
367
+ 'record-123',
368
+ 'org-123'
369
+ );
370
+
371
+ expect(result).toBe(0);
372
+ });
373
+
374
+ it('throws error on RPC failure', async () => {
375
+ mockSupabase.rpc.mockRejectedValueOnce(new Error('RPC error'));
376
+
377
+ await expect(
378
+ fileReferenceService.getFileCount('test_table', 'record-123', 'org-123')
379
+ ).rejects.toThrow();
380
+ });
381
+ });
382
+ });
383
+
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @file Performance Benchmark Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/__tests__
5
+ * @since 1.0.0
6
+ *
7
+ * Tests for performance benchmarking utilities.
8
+ */
9
+
10
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
11
+ import {
12
+ createPerformanceBenchmark,
13
+ measureRenderPerformance,
14
+ type PerformanceMetrics
15
+ } from '../performanceBenchmark';
16
+
17
+ describe('Performance Benchmark', () => {
18
+ beforeEach(() => {
19
+ vi.clearAllMocks();
20
+ });
21
+
22
+ describe('createPerformanceBenchmark', () => {
23
+ it('creates benchmark with name', () => {
24
+ const benchmark = createPerformanceBenchmark('test-benchmark');
25
+ expect(benchmark).toBeDefined();
26
+ expect(typeof benchmark.end).toBe('function');
27
+ });
28
+
29
+ it('end returns performance metrics', () => {
30
+ const benchmark = createPerformanceBenchmark('test');
31
+ const metrics = benchmark.end();
32
+
33
+ expect(metrics).toBeDefined();
34
+ expect(typeof metrics.renderTime).toBe('number');
35
+ expect(typeof metrics.interactionTime).toBe('number');
36
+ expect(typeof metrics.memoryUsage).toBe('number');
37
+ expect(typeof metrics.bundleSize).toBe('number');
38
+ });
39
+
40
+ it('measures time between start and end', () => {
41
+ const benchmark = createPerformanceBenchmark('test');
42
+
43
+ // Small delay to ensure measurable time
44
+ const start = performance.now();
45
+ while (performance.now() - start < 5) {} // Minimal delay
46
+
47
+ const metrics = benchmark.end();
48
+ expect(metrics.renderTime).toBeGreaterThanOrEqual(0);
49
+ });
50
+
51
+ it('calculates memory usage difference', () => {
52
+ const benchmark = createPerformanceBenchmark('memory-test');
53
+
54
+ // Create some objects to increase memory usage
55
+ const testObjects = new Array(100).fill(null).map(() => ({ data: 'test' }));
56
+
57
+ const metrics = benchmark.end();
58
+ expect(metrics.memoryUsage).toBeGreaterThanOrEqual(0);
59
+
60
+ // Clean up
61
+ testObjects.length = 0;
62
+ });
63
+
64
+ it('returns zero values for non-measurable metrics', () => {
65
+ const benchmark = createPerformanceBenchmark('test');
66
+ const metrics = benchmark.end();
67
+
68
+ expect(metrics.interactionTime).toBe(0);
69
+ expect(metrics.bundleSize).toBe(0);
70
+ });
71
+ });
72
+
73
+ describe('measureRenderPerformance', () => {
74
+ it('measures render performance for component', () => {
75
+ const mockRender = vi.fn(() => {
76
+ // Simulate rendering work
77
+ const start = performance.now();
78
+ while (performance.now() - start < 5) {}
79
+ });
80
+
81
+ const metrics = measureRenderPerformance('TestComponent', mockRender);
82
+
83
+ expect(metrics).toBeDefined();
84
+ expect(metrics.renderTime).toBeGreaterThanOrEqual(0);
85
+ expect(mockRender).toHaveBeenCalledTimes(1);
86
+ });
87
+
88
+ it('measures performance for multiple renders', () => {
89
+ const mockRender = vi.fn(() => {
90
+ const start = performance.now();
91
+ while (performance.now() - start < 2) {}
92
+ });
93
+
94
+ const metrics1 = measureRenderPerformance('Component1', mockRender);
95
+ const metrics2 = measureRenderPerformance('Component2', mockRender);
96
+
97
+ expect(metrics1.renderTime).toBeGreaterThanOrEqual(0);
98
+ expect(metrics2.renderTime).toBeGreaterThanOrEqual(0);
99
+ expect(mockRender).toHaveBeenCalledTimes(2);
100
+ });
101
+
102
+ it('handles render functions that throw', () => {
103
+ const failingRender = vi.fn(() => {
104
+ throw new Error('Render failed');
105
+ });
106
+
107
+ expect(() => {
108
+ measureRenderPerformance('FailingComponent', failingRender);
109
+ }).toThrow('Render failed');
110
+ });
111
+
112
+ it('measures performance for async operations', async () => {
113
+ const asyncRender = vi.fn(async () => {
114
+ const start = performance.now();
115
+ while (performance.now() - start < 10) {}
116
+ });
117
+
118
+ const metrics = measureRenderPerformance('AsyncComponent', asyncRender);
119
+
120
+ expect(metrics).toBeDefined();
121
+ expect(metrics.renderTime).toBeGreaterThanOrEqual(0);
122
+ });
123
+ });
124
+
125
+ describe('PerformanceMetrics interface', () => {
126
+ it('returns metrics with all required properties', () => {
127
+ const benchmark = createPerformanceBenchmark('interface-test');
128
+ const metrics = benchmark.end();
129
+
130
+ expect(metrics).toHaveProperty('renderTime');
131
+ expect(metrics).toHaveProperty('interactionTime');
132
+ expect(metrics).toHaveProperty('memoryUsage');
133
+ expect(metrics).toHaveProperty('bundleSize');
134
+ });
135
+
136
+ it('returns numeric values for all metrics', () => {
137
+ const benchmark = createPerformanceBenchmark('numeric-test');
138
+ const metrics = benchmark.end();
139
+
140
+ expect(typeof metrics.renderTime).toBe('number');
141
+ expect(typeof metrics.interactionTime).toBe('number');
142
+ expect(typeof metrics.memoryUsage).toBe('number');
143
+ expect(typeof metrics.bundleSize).toBe('number');
144
+ });
145
+ });
146
+
147
+ describe('Edge Cases', () => {
148
+ it('handles empty render function', () => {
149
+ const emptyRender = vi.fn();
150
+ const metrics = measureRenderPerformance('EmptyComponent', emptyRender);
151
+
152
+ expect(metrics).toBeDefined();
153
+ expect(emptyRender).toHaveBeenCalledTimes(1);
154
+ });
155
+
156
+ it('handles benchmark called multiple times', () => {
157
+ const benchmark = createPerformanceBenchmark('multi-call');
158
+
159
+ const metrics1 = benchmark.end();
160
+ const metrics2 = benchmark.end();
161
+
162
+ expect(metrics1).toBeDefined();
163
+ expect(metrics2).toBeDefined();
164
+ });
165
+
166
+ it('handles performance.now being unavailable', () => {
167
+ // This test verifies the function doesn't crash in edge cases
168
+ const benchmark = createPerformanceBenchmark('no-performance-api');
169
+ const metrics = benchmark.end();
170
+
171
+ expect(metrics).toBeDefined();
172
+ });
173
+ });
174
+ });
175
+
@@ -64,4 +64,58 @@ describe('App Name Resolver - Simple Tests', () => {
64
64
  expect(result === null || typeof result === 'string').toBe(true);
65
65
  });
66
66
  });
67
+
68
+ describe('Edge Cases', () => {
69
+ it('handles global variable being empty string', () => {
70
+ // In browser environment, if RBAC_APP_NAME is set to empty string
71
+ const originalGlobal = (globalThis as any).RBAC_APP_NAME;
72
+ (globalThis as any).RBAC_APP_NAME = '';
73
+
74
+ const result = getAppNameFromGlobal();
75
+ expect(result === null || typeof result === 'string').toBe(true);
76
+
77
+ // Restore original value
78
+ if (originalGlobal !== undefined) {
79
+ (globalThis as any).RBAC_APP_NAME = originalGlobal;
80
+ } else {
81
+ delete (globalThis as any).RBAC_APP_NAME;
82
+ }
83
+ });
84
+
85
+ it('handles global variable being whitespace', () => {
86
+ const originalGlobal = (globalThis as any).RBAC_APP_NAME;
87
+ (globalThis as any).RBAC_APP_NAME = ' ';
88
+
89
+ const result = getAppNameFromGlobal();
90
+ // Should trim whitespace and return empty string (treated as null)
91
+ expect(result === null || result === '').toBe(true);
92
+
93
+ // Restore
94
+ if (originalGlobal !== undefined) {
95
+ (globalThis as any).RBAC_APP_NAME = originalGlobal;
96
+ } else {
97
+ delete (globalThis as any).RBAC_APP_NAME;
98
+ }
99
+ });
100
+
101
+ it('handles setRBACAppName with valid name', () => {
102
+ const testName = 'test-app-name';
103
+ setRBACAppName(testName);
104
+
105
+ const result = getAppNameFromGlobal();
106
+ expect(typeof result).toBe('string');
107
+ });
108
+
109
+ it('handles setRBACAppName with empty string', () => {
110
+ setRBACAppName('');
111
+ const result = getAppNameFromGlobal();
112
+ expect(result === null || result === '').toBe(true);
113
+ });
114
+
115
+ it('getCurrentAppName follows priority order', () => {
116
+ // This test verifies that getCurrentAppName follows proper fallback chain
117
+ const result = getCurrentAppName();
118
+ expect(result === null || typeof result === 'string').toBe(true);
119
+ });
120
+ });
67
121
  });
@@ -299,4 +299,67 @@ describe('CSRF Protection', () => {
299
299
  expect(typeof tokenData.used).toBe('boolean');
300
300
  });
301
301
  });
302
+
303
+ describe('Edge Cases', () => {
304
+ it('should handle very long session IDs', async () => {
305
+ const longSessionId = 'a'.repeat(500);
306
+ const token = await generateCSRFToken(longSessionId);
307
+
308
+ expect(token).toBeDefined();
309
+ expect(token.length).toBe(64);
310
+
311
+ // Should be valid for that session
312
+ const isValid = await validateCSRFToken(token, longSessionId);
313
+ expect(isValid).toBe(true);
314
+ });
315
+
316
+ it('should handle rapid token generation', async () => {
317
+ const tokens = await Promise.all(
318
+ Array.from({ length: 20 }, () => generateCSRFToken(testSessionId))
319
+ );
320
+
321
+ expect(tokens).toHaveLength(20);
322
+ // All should be unique 32-byte tokens
323
+ const uniqueTokens = new Set(tokens);
324
+ expect(uniqueTokens.size).toBe(20);
325
+
326
+ // Last token should be valid
327
+ const lastToken = tokens[tokens.length - 1];
328
+ expect(await validateCSRFToken(lastToken, testSessionId)).toBe(true);
329
+ });
330
+
331
+ it('should handle storage corruption by clearing cache', async () => {
332
+ const { secureStorage } = await import('../../utils/secureStorage');
333
+
334
+ // Simulate malformed storage data
335
+ vi.mocked(secureStorage.getItem).mockResolvedValueOnce('{{invalid json}');
336
+
337
+ // Should handle gracefully and generate new token
338
+ const token = await getCSRFToken(testSessionId);
339
+ expect(token).toBeDefined();
340
+ expect(typeof token).toBe('string');
341
+ });
342
+
343
+ it('should handle concurrent token generation safely', async () => {
344
+ const promises = Array.from({ length: 10 }, (_, i) =>
345
+ generateCSRFToken(`session-${i}`)
346
+ );
347
+
348
+ const tokens = await Promise.all(promises);
349
+
350
+ expect(tokens).toHaveLength(10);
351
+ expect(new Set(tokens).size).toBe(10);
352
+ });
353
+
354
+ it('should handle session ID collision edge cases', async () => {
355
+ const sameSession = 'collision-session';
356
+
357
+ const token1 = await generateCSRFToken(sameSession);
358
+ const token2 = await generateCSRFToken(sameSession);
359
+
360
+ // Both should be valid for same session
361
+ expect(await validateCSRFToken(token1, sameSession)).toBe(true);
362
+ expect(await validateCSRFToken(token2, sameSession)).toBe(true);
363
+ });
364
+ });
302
365
  });