@jmruthers/pace-core 0.5.136 → 0.5.137

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 (289) hide show
  1. package/dist/{DataTable-CYOHOX3O.js → DataTable-6M4L6BI2.js} +10 -9
  2. package/dist/{EventLogo-801uofbR.d.ts → EventLogo-rFL_kRjk.d.ts} +73 -1
  3. package/dist/{UnifiedAuthProvider-5E5TUNMS.js → UnifiedAuthProvider-XIQQ7LVU.js} +4 -5
  4. package/dist/{chunk-YLKIDTUK.js → chunk-22WKWKRX.js} +4 -4
  5. package/dist/{chunk-TVYPTYOY.js → chunk-4C7EXCAR.js} +60 -24
  6. package/dist/chunk-4C7EXCAR.js.map +1 -0
  7. package/dist/{chunk-2TWNJ46Y.js → chunk-6LAAY47Q.js} +2 -2
  8. package/dist/{chunk-444EZN6N.js → chunk-7QCC6MCP.js} +88 -1
  9. package/dist/chunk-7QCC6MCP.js.map +1 -0
  10. package/dist/{chunk-FHWWBIHA.js → chunk-BCIBECNB.js} +5 -5
  11. package/dist/chunk-BJPBT3CU.js +21 -0
  12. package/dist/chunk-BJPBT3CU.js.map +1 -0
  13. package/dist/{chunk-L6PGMCMD.js → chunk-BLCXZEYF.js} +3 -3
  14. package/dist/{chunk-HJGGOMQ6.js → chunk-HAWZXGR2.js} +147 -103
  15. package/dist/chunk-HAWZXGR2.js.map +1 -0
  16. package/dist/{chunk-XARJS7CD.js → chunk-INQLMHPF.js} +2 -2
  17. package/dist/chunk-JISYG63F.js +70 -0
  18. package/dist/chunk-JISYG63F.js.map +1 -0
  19. package/dist/{chunk-NOHEVYVX.js → chunk-KYRHUBIU.js} +417 -319
  20. package/dist/chunk-KYRHUBIU.js.map +1 -0
  21. package/dist/{chunk-SL2YQDR6.js → chunk-MA6EPSGZ.js} +2 -2
  22. package/dist/{chunk-5DPZ5EAT.js → chunk-OWAG3GSU.js} +1 -3
  23. package/dist/{chunk-LTV3XIJJ.js → chunk-T6JN6LH6.js} +4 -4
  24. package/dist/{chunk-4MT5BGGL.js → chunk-YCWDTTUK.js} +4 -6
  25. package/dist/{chunk-4MT5BGGL.js.map → chunk-YCWDTTUK.js.map} +1 -1
  26. package/dist/components.d.ts +1 -1
  27. package/dist/components.js +12 -11
  28. package/dist/components.js.map +1 -1
  29. package/dist/hooks.js +8 -9
  30. package/dist/hooks.js.map +1 -1
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +15 -14
  33. package/dist/index.js.map +1 -1
  34. package/dist/providers.js +3 -4
  35. package/dist/rbac/index.js +8 -9
  36. package/dist/schema-DTDZQe2u.d.ts +28 -0
  37. package/dist/types.d.ts +152 -3
  38. package/dist/types.js +51 -16
  39. package/dist/types.js.map +1 -1
  40. package/dist/utils.d.ts +89 -4
  41. package/dist/utils.js +214 -96
  42. package/dist/utils.js.map +1 -1
  43. package/dist/validation.d.ts +1 -343
  44. package/dist/validation.js +3 -100
  45. package/docs/api/classes/ColumnFactory.md +1 -1
  46. package/docs/api/classes/ErrorBoundary.md +1 -1
  47. package/docs/api/classes/InvalidScopeError.md +1 -1
  48. package/docs/api/classes/MissingUserContextError.md +1 -1
  49. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  50. package/docs/api/classes/PermissionDeniedError.md +1 -1
  51. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  52. package/docs/api/classes/RBACAuditManager.md +1 -1
  53. package/docs/api/classes/RBACCache.md +1 -1
  54. package/docs/api/classes/RBACEngine.md +1 -1
  55. package/docs/api/classes/RBACError.md +1 -1
  56. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  57. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  58. package/docs/api/classes/StorageUtils.md +1 -1
  59. package/docs/api/enums/FileCategory.md +1 -1
  60. package/docs/api/interfaces/AggregateConfig.md +1 -1
  61. package/docs/api/interfaces/BadgeProps.md +27 -0
  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/EventAppRoleData.md +1 -1
  75. package/docs/api/interfaces/EventLogoProps.md +1 -1
  76. package/docs/api/interfaces/ExportColumn.md +1 -1
  77. package/docs/api/interfaces/ExportOptions.md +1 -1
  78. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  79. package/docs/api/interfaces/FileMetadata.md +1 -1
  80. package/docs/api/interfaces/FileReference.md +1 -1
  81. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  82. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  83. package/docs/api/interfaces/FileUploadProps.md +1 -1
  84. package/docs/api/interfaces/FooterProps.md +1 -1
  85. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  86. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  87. package/docs/api/interfaces/InputProps.md +1 -1
  88. package/docs/api/interfaces/LabelProps.md +1 -1
  89. package/docs/api/interfaces/LoginFormProps.md +1 -1
  90. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  91. package/docs/api/interfaces/NavigationContextType.md +1 -1
  92. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  93. package/docs/api/interfaces/NavigationItem.md +1 -1
  94. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  95. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  96. package/docs/api/interfaces/Organisation.md +1 -1
  97. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  98. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  99. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  100. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  101. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  102. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  103. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  104. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  105. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  106. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  107. package/docs/api/interfaces/PaletteData.md +1 -1
  108. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  109. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  110. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  111. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  112. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  113. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  114. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  115. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  116. package/docs/api/interfaces/RBACConfig.md +1 -1
  117. package/docs/api/interfaces/RBACLogger.md +1 -1
  118. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  119. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  120. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  121. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  122. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  123. package/docs/api/interfaces/RouteConfig.md +1 -1
  124. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  125. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  126. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  127. package/docs/api/interfaces/StorageConfig.md +1 -1
  128. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  129. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  130. package/docs/api/interfaces/StorageListOptions.md +1 -1
  131. package/docs/api/interfaces/StorageListResult.md +1 -1
  132. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  133. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  134. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  135. package/docs/api/interfaces/StyleImport.md +1 -1
  136. package/docs/api/interfaces/SwitchProps.md +1 -1
  137. package/docs/api/interfaces/ToastActionElement.md +1 -1
  138. package/docs/api/interfaces/ToastProps.md +1 -1
  139. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  140. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  141. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  142. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  143. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  144. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  145. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  146. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  147. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  148. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  149. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  150. package/docs/api/interfaces/UserEventAccess.md +1 -1
  151. package/docs/api/interfaces/UserMenuProps.md +1 -1
  152. package/docs/api/interfaces/UserProfile.md +1 -1
  153. package/docs/api/modules.md +79 -10
  154. package/docs/architecture/README.md +0 -1
  155. package/docs/styles/README.md +0 -2
  156. package/examples/RBAC/CompleteRBACExample.tsx +324 -0
  157. package/examples/RBAC/EventBasedApp.tsx +239 -0
  158. package/examples/RBAC/PermissionExample.tsx +151 -0
  159. package/examples/RBAC/index.ts +13 -0
  160. package/examples/public-pages/CorrectPublicPageImplementation.tsx +301 -0
  161. package/examples/public-pages/PublicEventPage.tsx +274 -0
  162. package/examples/public-pages/PublicPageApp.tsx +308 -0
  163. package/examples/public-pages/PublicPageUsageExample.tsx +216 -0
  164. package/examples/public-pages/index.ts +14 -0
  165. package/package.json +1 -10
  166. package/src/__tests__/TEST_STANDARD.md +92 -0
  167. package/src/components/Badge/Badge.test.tsx +314 -0
  168. package/src/components/Badge/Badge.tsx +304 -0
  169. package/src/components/Badge/index.ts +3 -0
  170. package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +217 -0
  171. package/src/components/DataTable/__tests__/styles.test.ts +1 -1
  172. package/src/components/DataTable/components/ColumnFilter.tsx +8 -4
  173. package/src/components/DataTable/components/DataTableBody.tsx +461 -0
  174. package/src/components/DataTable/components/DraggableColumnHeader.tsx +144 -0
  175. package/src/components/DataTable/components/FilterRow.tsx +9 -3
  176. package/src/components/DataTable/components/PaginationControls.tsx +1 -0
  177. package/src/components/DataTable/components/VirtualizedDataTable.tsx +513 -0
  178. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +14 -68
  179. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +62 -0
  180. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +43 -0
  181. package/src/components/DataTable/core/ActionManager.ts +235 -0
  182. package/src/components/DataTable/core/ColumnManager.ts +205 -0
  183. package/src/components/DataTable/core/DataManager.ts +188 -0
  184. package/src/components/DataTable/core/DataTableContext.tsx +181 -0
  185. package/src/components/DataTable/core/LocalDataAdapter.ts +273 -0
  186. package/src/components/DataTable/core/PluginRegistry.ts +229 -0
  187. package/src/components/DataTable/core/StateManager.ts +311 -0
  188. package/src/components/DataTable/core/interfaces.ts +338 -0
  189. package/src/components/DataTable/styles.ts +27 -6
  190. package/src/components/DataTable/utils/__tests__/columnUtils.test.ts +94 -0
  191. package/src/components/DataTable/utils/columnUtils.ts +40 -0
  192. package/src/components/DataTable/utils/debugTools.ts +609 -0
  193. package/src/components/DataTable/utils/index.ts +1 -0
  194. package/src/components/Dialog/README.md +804 -0
  195. package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +611 -0
  196. package/src/components/Dialog/utils/safeHtml.ts +185 -0
  197. package/src/components/Footer/Footer.test.tsx +1 -1
  198. package/src/components/Form/Form.test.tsx +1 -1
  199. package/src/components/Form/FormErrorSummary.tsx +113 -0
  200. package/src/components/Form/FormFieldset.tsx +127 -0
  201. package/src/components/Form/FormLiveRegion.tsx +198 -0
  202. package/src/components/LoginForm/LoginForm.test.tsx +1 -1
  203. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +76 -10
  204. package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
  205. package/src/components/PasswordReset/PasswordResetForm.test.tsx +597 -0
  206. package/src/components/PasswordReset/PasswordResetForm.tsx +201 -0
  207. package/src/components/PublicLayout/PublicPageDebugger.tsx +104 -0
  208. package/src/components/PublicLayout/PublicPageDiagnostic.tsx +162 -0
  209. package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
  210. package/src/components/Select/Select.test.tsx +1 -1
  211. package/src/components/Select/Select.tsx +20 -8
  212. package/src/components/Table/__tests__/Table.test.tsx +1 -1
  213. package/src/components/index.ts +3 -0
  214. package/src/hooks/__tests__/useFileUrl.unit.test.ts +83 -85
  215. package/src/index.ts +4 -0
  216. package/src/styles/core.css +3 -0
  217. package/src/utils/appConfig.ts +47 -0
  218. package/src/utils/appIdResolver.test.ts +499 -0
  219. package/src/utils/appIdResolver.ts +130 -0
  220. package/src/utils/appNameResolver.simple.test.ts +212 -0
  221. package/src/utils/appNameResolver.test.ts +121 -0
  222. package/src/utils/appNameResolver.ts +191 -0
  223. package/src/utils/audit.ts +127 -0
  224. package/src/utils/auth-utils.ts +96 -0
  225. package/src/utils/bundleAnalysis.ts +129 -0
  226. package/src/utils/cn.ts +7 -0
  227. package/src/utils/debugLogger.ts +67 -0
  228. package/src/utils/deviceFingerprint.ts +215 -0
  229. package/src/utils/dynamicUtils.ts +105 -0
  230. package/src/utils/file-reference.test.ts +788 -0
  231. package/src/utils/file-reference.ts +519 -0
  232. package/src/utils/formatDate.test.ts +237 -0
  233. package/src/utils/formatting.ts +133 -0
  234. package/src/utils/index.ts +7 -0
  235. package/src/utils/lazyLoad.tsx +44 -0
  236. package/src/utils/logger.ts +179 -0
  237. package/src/utils/organisationContext.test.ts +322 -0
  238. package/src/utils/organisationContext.ts +153 -0
  239. package/src/utils/performanceBenchmark.ts +64 -0
  240. package/src/utils/performanceBudgets.ts +110 -0
  241. package/src/utils/permissionTypes.ts +37 -0
  242. package/src/utils/permissionUtils.test.ts +393 -0
  243. package/src/utils/permissionUtils.ts +34 -0
  244. package/src/utils/sanitization.ts +264 -0
  245. package/src/utils/schemaUtils.ts +37 -0
  246. package/src/utils/secureDataAccess.test.ts +711 -0
  247. package/src/utils/secureDataAccess.ts +377 -0
  248. package/src/utils/secureErrors.ts +79 -0
  249. package/src/utils/secureStorage.ts +244 -0
  250. package/src/utils/security.ts +156 -0
  251. package/src/utils/securityMonitor.ts +45 -0
  252. package/src/utils/sessionTracking.ts +126 -0
  253. package/src/utils/validation.ts +111 -0
  254. package/src/utils/validationUtils.ts +120 -0
  255. package/src/validation/index.ts +2 -2
  256. package/dist/chunk-444EZN6N.js.map +0 -1
  257. package/dist/chunk-APIBCTL2.js +0 -670
  258. package/dist/chunk-APIBCTL2.js.map +0 -1
  259. package/dist/chunk-HJGGOMQ6.js.map +0 -1
  260. package/dist/chunk-K2WWTH7O.js +0 -94
  261. package/dist/chunk-K2WWTH7O.js.map +0 -1
  262. package/dist/chunk-LMC26NLJ.js +0 -84
  263. package/dist/chunk-LMC26NLJ.js.map +0 -1
  264. package/dist/chunk-NOHEVYVX.js.map +0 -1
  265. package/dist/chunk-TVYPTYOY.js.map +0 -1
  266. package/dist/validation-8npbysjg.d.ts +0 -177
  267. /package/dist/{DataTable-CYOHOX3O.js.map → DataTable-6M4L6BI2.js.map} +0 -0
  268. /package/dist/{UnifiedAuthProvider-5E5TUNMS.js.map → UnifiedAuthProvider-XIQQ7LVU.js.map} +0 -0
  269. /package/dist/{chunk-YLKIDTUK.js.map → chunk-22WKWKRX.js.map} +0 -0
  270. /package/dist/{chunk-2TWNJ46Y.js.map → chunk-6LAAY47Q.js.map} +0 -0
  271. /package/dist/{chunk-FHWWBIHA.js.map → chunk-BCIBECNB.js.map} +0 -0
  272. /package/dist/{chunk-L6PGMCMD.js.map → chunk-BLCXZEYF.js.map} +0 -0
  273. /package/dist/{chunk-XARJS7CD.js.map → chunk-INQLMHPF.js.map} +0 -0
  274. /package/dist/{chunk-SL2YQDR6.js.map → chunk-MA6EPSGZ.js.map} +0 -0
  275. /package/dist/{chunk-5DPZ5EAT.js.map → chunk-OWAG3GSU.js.map} +0 -0
  276. /package/dist/{chunk-LTV3XIJJ.js.map → chunk-T6JN6LH6.js.map} +0 -0
  277. /package/examples/{components → components 2}/DataTable/HierarchicalActionsExample.tsx +0 -0
  278. /package/examples/{components → components 2}/DataTable/HierarchicalExample.tsx +0 -0
  279. /package/examples/{components → components 2}/DataTable/InitialPageSizeExample.tsx +0 -0
  280. /package/examples/{components → components 2}/DataTable/PerformanceExample.tsx +0 -0
  281. /package/examples/{components → components 2}/DataTable/index.ts +0 -0
  282. /package/examples/{components → components 2}/Dialog/BasicHtmlTest.tsx +0 -0
  283. /package/examples/{components → components 2}/Dialog/DebugHtmlExample.tsx +0 -0
  284. /package/examples/{components → components 2}/Dialog/HtmlDialogExample.tsx +0 -0
  285. /package/examples/{components → components 2}/Dialog/ScrollableDialogExample.tsx +0 -0
  286. /package/examples/{components → components 2}/Dialog/SimpleHtmlTest.tsx +0 -0
  287. /package/examples/{components → components 2}/Dialog/SmartDialogExample.tsx +0 -0
  288. /package/examples/{components → components 2}/Dialog/index.ts +0 -0
  289. /package/examples/{components → components 2}/index.ts +0 -0
@@ -0,0 +1,711 @@
1
+ /**
2
+ * @file Secure Data Access Utility Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/SecureDataAccess
5
+ * @since 0.4.0
6
+ *
7
+ * Comprehensive tests for secure data access utilities that enforce organisation context.
8
+ */
9
+
10
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11
+ import type { SupabaseClient } from '@supabase/supabase-js';
12
+ import { createSecureDataAccess, useSecureDataAccess } from './secureDataAccess';
13
+ import type { DatabaseRecord, DatabaseData, DatabaseFilters, SecureQueryOptions } from './secureDataAccess';
14
+
15
+ // Mock Supabase client
16
+ const mockSupabaseClient = {
17
+ from: vi.fn(),
18
+ } as unknown as SupabaseClient;
19
+
20
+ // Mock query builder with proper Promise-like behavior
21
+ let mockQueryBuilder: any;
22
+
23
+ const createMockQueryBuilder = (customData?: any, customError?: any) => {
24
+ const builder: any = {
25
+ select: vi.fn(function(this: any) { return this; }),
26
+ eq: vi.fn(function(this: any) { return this; }),
27
+ order: vi.fn(function(this: any) { return this; }),
28
+ limit: vi.fn(function(this: any) { return this; }),
29
+ range: vi.fn(function(this: any) { return this; }),
30
+ insert: vi.fn(function(this: any) { return this; }),
31
+ update: vi.fn(function(this: any) { return this; }),
32
+ delete: vi.fn(function(this: any) { return this; }),
33
+ single: vi.fn().mockImplementation(() => {
34
+ const data = customData !== undefined ? customData : { id: 'test-123' };
35
+ const error = customError !== undefined ? customError : null;
36
+
37
+ // For single queries, return { data, error } structure, don't reject
38
+ return Promise.resolve({ data, error });
39
+ }),
40
+ };
41
+
42
+ // Make it thenable (awaitable) with custom data/error support
43
+ builder.then = function(resolve: any, reject: any) {
44
+ const data = customData !== undefined ? customData : [{ id: 'test-123' }];
45
+ const error = customError !== undefined ? customError : null;
46
+
47
+ if (error) {
48
+ return Promise.reject(error).then(resolve, reject);
49
+ }
50
+ return Promise.resolve({ data, error }).then(resolve, reject);
51
+ };
52
+
53
+ return builder;
54
+ };
55
+
56
+ describe('secureDataAccess', () => {
57
+ let secureDataAccess: ReturnType<typeof createSecureDataAccess>;
58
+ const mockOrganisationId = 'org-123';
59
+ const mockTable = 'pace_person'; // Use a table that has organisation_id column
60
+ const mockSelect = 'id, name, organisation_id';
61
+
62
+ beforeEach(() => {
63
+ vi.clearAllMocks();
64
+ // Create a fresh mock query builder for each test
65
+ mockQueryBuilder = createMockQueryBuilder();
66
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
67
+ });
68
+
69
+ describe('createSecureDataAccess', () => {
70
+ describe('Regular User (Non-Super Admin)', () => {
71
+ beforeEach(() => {
72
+ secureDataAccess = createSecureDataAccess(mockSupabaseClient, mockOrganisationId, false);
73
+ });
74
+
75
+ describe('validateOrganisationContext', () => {
76
+ it('should throw error when organisation ID is empty', () => {
77
+ expect(() => {
78
+ secureDataAccess.validateOrganisationContext('');
79
+ }).toThrow('Organisation context is required for secure data access');
80
+ });
81
+
82
+ it('should throw error when organisation ID is null', () => {
83
+ expect(() => {
84
+ secureDataAccess.validateOrganisationContext(null as any);
85
+ }).toThrow('Organisation context is required for secure data access');
86
+ });
87
+
88
+ it('should throw error when organisation ID is undefined', () => {
89
+ expect(() => {
90
+ secureDataAccess.validateOrganisationContext(undefined as any);
91
+ }).toThrow('Organisation context is required for secure data access');
92
+ });
93
+
94
+ it('should not throw error when organisation ID is valid', () => {
95
+ expect(() => {
96
+ secureDataAccess.validateOrganisationContext(mockOrganisationId);
97
+ }).not.toThrow();
98
+ });
99
+ });
100
+
101
+ describe('ensureOrganisationColumn', () => {
102
+ it('should return true for tables with organisation_id column', () => {
103
+ expect(secureDataAccess.ensureOrganisationColumn('event')).toBe(true);
104
+ expect(secureDataAccess.ensureOrganisationColumn('organisation_settings')).toBe(true);
105
+ expect(secureDataAccess.ensureOrganisationColumn('rbac_event_app_roles')).toBe(true);
106
+ });
107
+
108
+ it('should return false for tables without organisation_id column', () => {
109
+ expect(secureDataAccess.ensureOrganisationColumn('unknown_table')).toBe(false);
110
+ expect(secureDataAccess.ensureOrganisationColumn('system_table')).toBe(false);
111
+ });
112
+ });
113
+
114
+ describe('secureQuery', () => {
115
+ it('should execute query with organisation filter', async () => {
116
+ const mockData = [
117
+ { id: '1', name: 'Test 1', organisation_id: mockOrganisationId },
118
+ { id: '2', name: 'Test 2', organisation_id: mockOrganisationId }
119
+ ];
120
+
121
+ // Create a new mock with the expected data
122
+ mockQueryBuilder = createMockQueryBuilder(mockData);
123
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
124
+
125
+ const options: SecureQueryOptions = {
126
+ table: mockTable,
127
+ select: mockSelect,
128
+ organisationId: mockOrganisationId
129
+ };
130
+
131
+ const result = await secureDataAccess.secureQuery(options);
132
+
133
+ expect(mockSupabaseClient.from).toHaveBeenCalledWith(mockTable);
134
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('organisation_id', mockOrganisationId);
135
+ expect(result).toEqual(mockData);
136
+ });
137
+
138
+ it('should handle query errors', async () => {
139
+ const mockError = new Error('Database error');
140
+ // Create a new mock with the error
141
+ mockQueryBuilder = createMockQueryBuilder(null, mockError);
142
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
143
+
144
+ const options: SecureQueryOptions = {
145
+ table: mockTable,
146
+ select: mockSelect,
147
+ organisationId: mockOrganisationId
148
+ };
149
+
150
+ await expect(secureDataAccess.secureQuery(options)).rejects.toThrow('Database error');
151
+ });
152
+
153
+ it('should return empty array when data is not an array', async () => {
154
+ // Create a new mock with null data (not an array)
155
+ mockQueryBuilder = createMockQueryBuilder(null);
156
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
157
+
158
+ const options: SecureQueryOptions = {
159
+ table: mockTable,
160
+ select: mockSelect,
161
+ organisationId: mockOrganisationId
162
+ };
163
+
164
+ const result = await secureDataAccess.secureQuery(options);
165
+ expect(result).toEqual([]);
166
+ });
167
+
168
+ it('should apply additional filters', async () => {
169
+ const mockData = [{ id: '1', name: 'Test', organisation_id: mockOrganisationId }];
170
+ // Create a new mock with the expected data
171
+ mockQueryBuilder = createMockQueryBuilder(mockData);
172
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
173
+
174
+ const options: SecureQueryOptions = {
175
+ table: mockTable,
176
+ select: mockSelect,
177
+ organisationId: mockOrganisationId,
178
+ filters: { name: 'Test', status: 'active' }
179
+ };
180
+
181
+ await secureDataAccess.secureQuery(options);
182
+
183
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('organisation_id', mockOrganisationId);
184
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('name', 'Test');
185
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('status', 'active');
186
+ });
187
+
188
+ it('should handle qualified column names in filters', async () => {
189
+ const mockData = [{ id: '1', name: 'Test', organisation_id: mockOrganisationId }];
190
+ // Create a new mock with the expected data
191
+ mockQueryBuilder = createMockQueryBuilder(mockData);
192
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
193
+
194
+ const options: SecureQueryOptions = {
195
+ table: mockTable,
196
+ select: mockSelect,
197
+ organisationId: mockOrganisationId,
198
+ filters: { 'users.role': 'admin' }
199
+ };
200
+
201
+ await secureDataAccess.secureQuery(options);
202
+
203
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('role', 'admin');
204
+ });
205
+
206
+ it('should skip undefined and null filter values', async () => {
207
+ const mockData = [{ id: '1', name: 'Test', organisation_id: mockOrganisationId }];
208
+ // Create a new mock with the expected data
209
+ mockQueryBuilder = createMockQueryBuilder(mockData);
210
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
211
+
212
+ const options: SecureQueryOptions = {
213
+ table: mockTable,
214
+ select: mockSelect,
215
+ organisationId: mockOrganisationId,
216
+ filters: { name: 'Test', status: undefined, active: null }
217
+ };
218
+
219
+ await secureDataAccess.secureQuery(options);
220
+
221
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('organisation_id', mockOrganisationId);
222
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('name', 'Test');
223
+ expect(mockQueryBuilder.eq).not.toHaveBeenCalledWith('status', undefined);
224
+ expect(mockQueryBuilder.eq).not.toHaveBeenCalledWith('active', null);
225
+ });
226
+ });
227
+
228
+ describe('secureSingleQuery', () => {
229
+ it('should execute single query with organisation filter', async () => {
230
+ const mockData = { id: '1', name: 'Test', organisation_id: mockOrganisationId };
231
+ // Create a new mock with the expected data
232
+ mockQueryBuilder = createMockQueryBuilder(mockData);
233
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
234
+
235
+ const options: SecureQueryOptions = {
236
+ table: mockTable,
237
+ select: mockSelect,
238
+ organisationId: mockOrganisationId
239
+ };
240
+
241
+ const result = await secureDataAccess.secureSingleQuery(options);
242
+
243
+ expect(mockSupabaseClient.from).toHaveBeenCalledWith(mockTable);
244
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('organisation_id', mockOrganisationId);
245
+ expect(result).toEqual(mockData);
246
+ });
247
+
248
+ it('should return null when no rows found', async () => {
249
+ const mockError = { code: 'PGRST116', message: 'No rows returned' };
250
+ // Create a new mock with the error
251
+ mockQueryBuilder = createMockQueryBuilder(null, mockError);
252
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
253
+
254
+ const options: SecureQueryOptions = {
255
+ table: mockTable,
256
+ select: mockSelect,
257
+ organisationId: mockOrganisationId
258
+ };
259
+
260
+ const result = await secureDataAccess.secureSingleQuery(options);
261
+ expect(result).toBeNull();
262
+ });
263
+
264
+ it('should handle query errors', async () => {
265
+ const mockError = new Error('Database error');
266
+ // Create a new mock with the error
267
+ mockQueryBuilder = createMockQueryBuilder(null, mockError);
268
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
269
+
270
+ const options: SecureQueryOptions = {
271
+ table: mockTable,
272
+ select: mockSelect,
273
+ organisationId: mockOrganisationId
274
+ };
275
+
276
+ await expect(secureDataAccess.secureSingleQuery(options)).rejects.toThrow('Database error');
277
+ });
278
+
279
+ it('should return null when data is error type', async () => {
280
+ const mockData = { code: 'ERROR', message: 'Invalid data' };
281
+ // Create a new mock with the expected data
282
+ mockQueryBuilder = createMockQueryBuilder(mockData);
283
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
284
+
285
+ const options: SecureQueryOptions = {
286
+ table: mockTable,
287
+ select: mockSelect,
288
+ organisationId: mockOrganisationId
289
+ };
290
+
291
+ const result = await secureDataAccess.secureSingleQuery(options);
292
+ expect(result).toBeNull();
293
+ });
294
+ });
295
+
296
+ describe('secureInsert', () => {
297
+ it('should insert data with organisation context', async () => {
298
+ const mockData = { name: 'Test Item' };
299
+ const mockResult = { id: '1', name: 'Test Item', organisation_id: mockOrganisationId };
300
+
301
+ mockQueryBuilder.insert = vi.fn().mockReturnThis();
302
+ mockQueryBuilder.select = vi.fn().mockReturnThis();
303
+ mockQueryBuilder.single = vi.fn().mockResolvedValue({ data: mockResult, error: null });
304
+
305
+ const result = await secureDataAccess.secureInsert(mockTable, mockData, mockOrganisationId);
306
+
307
+ expect(mockQueryBuilder.insert).toHaveBeenCalledWith({
308
+ ...mockData,
309
+ organisation_id: mockOrganisationId
310
+ });
311
+ expect(result).toEqual(mockResult);
312
+ });
313
+
314
+ it('should handle insert errors', async () => {
315
+ const mockData = { name: 'Test Item' };
316
+ const mockError = new Error('Insert failed');
317
+
318
+ mockQueryBuilder.insert = vi.fn().mockReturnThis();
319
+ mockQueryBuilder.select = vi.fn().mockReturnThis();
320
+ // Create a new mock with the error
321
+ mockQueryBuilder = createMockQueryBuilder(null, mockError);
322
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
323
+
324
+ await expect(secureDataAccess.secureInsert(mockTable, mockData, mockOrganisationId))
325
+ .rejects.toThrow('Insert failed');
326
+ });
327
+ });
328
+
329
+ describe('secureUpdate', () => {
330
+ it('should update data with organisation filter', async () => {
331
+ const mockData = { name: 'Updated Item' };
332
+ const mockFilters = { id: '1' };
333
+ const mockResult = { id: '1', name: 'Updated Item', organisation_id: mockOrganisationId };
334
+
335
+ mockQueryBuilder.update = vi.fn().mockReturnThis();
336
+ mockQueryBuilder.eq = vi.fn().mockReturnThis();
337
+ mockQueryBuilder.select = vi.fn().mockReturnThis();
338
+ mockQueryBuilder.single = vi.fn().mockResolvedValue({ data: mockResult, error: null });
339
+
340
+ const result = await secureDataAccess.secureUpdate(mockTable, mockData, mockFilters, mockOrganisationId);
341
+
342
+ expect(mockQueryBuilder.update).toHaveBeenCalledWith(mockData);
343
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('organisation_id', mockOrganisationId);
344
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('id', '1');
345
+ expect(result).toEqual(mockResult);
346
+ });
347
+
348
+ it('should handle update errors', async () => {
349
+ const mockData = { name: 'Updated Item' };
350
+ const mockFilters = { id: '1' };
351
+ const mockError = new Error('Update failed');
352
+
353
+ mockQueryBuilder.update = vi.fn().mockReturnThis();
354
+ mockQueryBuilder.eq = vi.fn().mockReturnThis();
355
+ mockQueryBuilder.select = vi.fn().mockReturnThis();
356
+ // Create a new mock with the error
357
+ mockQueryBuilder = createMockQueryBuilder(null, mockError);
358
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
359
+
360
+ await expect(secureDataAccess.secureUpdate(mockTable, mockData, mockFilters, mockOrganisationId))
361
+ .rejects.toThrow('Update failed');
362
+ });
363
+ });
364
+
365
+ describe('secureDelete', () => {
366
+ it('should delete data with organisation filter', async () => {
367
+ const mockFilters = { id: '1' };
368
+
369
+ mockQueryBuilder.delete = vi.fn().mockReturnThis();
370
+ mockQueryBuilder.eq = vi.fn().mockReturnThis();
371
+ mockQueryBuilder.single = vi.fn().mockResolvedValue({ data: null, error: null });
372
+
373
+ const result = await secureDataAccess.secureDelete(mockTable, mockFilters, mockOrganisationId);
374
+
375
+ expect(mockQueryBuilder.delete).toHaveBeenCalled();
376
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('organisation_id', mockOrganisationId);
377
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('id', '1');
378
+ expect(result).toBe(true);
379
+ });
380
+
381
+ it('should handle delete errors', async () => {
382
+ const mockFilters = { id: '1' };
383
+ const mockError = new Error('Delete failed');
384
+
385
+ mockQueryBuilder.delete = vi.fn().mockReturnThis();
386
+ mockQueryBuilder.eq = vi.fn().mockReturnThis();
387
+ // Create a new mock with the error
388
+ mockQueryBuilder = createMockQueryBuilder(null, mockError);
389
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
390
+
391
+ await expect(secureDataAccess.secureDelete(mockTable, mockFilters, mockOrganisationId))
392
+ .rejects.toThrow('Delete failed');
393
+ });
394
+ });
395
+
396
+ describe('queryByOrganisation', () => {
397
+ it('should query by organisation using secureQuery', async () => {
398
+ const mockData = [{ id: '1', name: 'Test', organisation_id: mockOrganisationId }];
399
+ // Create a new mock with the expected data
400
+ mockQueryBuilder = createMockQueryBuilder(mockData);
401
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
402
+
403
+ const result = await secureDataAccess.queryByOrganisation(mockTable, mockSelect, mockOrganisationId);
404
+
405
+ expect(mockSupabaseClient.from).toHaveBeenCalledWith(mockTable);
406
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('organisation_id', mockOrganisationId);
407
+ expect(result).toEqual(mockData);
408
+ });
409
+
410
+ it('should apply additional filters when provided', async () => {
411
+ const mockData = [{ id: '1', name: 'Test', organisation_id: mockOrganisationId }];
412
+ // Create a new mock with the expected data
413
+ mockQueryBuilder = createMockQueryBuilder(mockData);
414
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
415
+ const filters = { status: 'active' };
416
+ // Create a new mock with the expected data
417
+ mockQueryBuilder = createMockQueryBuilder(mockData);
418
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
419
+
420
+ await secureDataAccess.queryByOrganisation(mockTable, mockSelect, mockOrganisationId, filters);
421
+
422
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('organisation_id', mockOrganisationId);
423
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('status', 'active');
424
+ });
425
+ });
426
+ });
427
+
428
+ describe('Super Admin User', () => {
429
+ beforeEach(() => {
430
+ secureDataAccess = createSecureDataAccess(mockSupabaseClient, mockOrganisationId, true);
431
+ });
432
+
433
+ it('should not apply organisation filter for super admin', async () => {
434
+ const mockData = [{ id: '1', name: 'Test', organisation_id: 'other-org' }];
435
+ // Create a new mock with the expected data
436
+ mockQueryBuilder = createMockQueryBuilder(mockData);
437
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
438
+
439
+ const options: SecureQueryOptions = {
440
+ table: mockTable,
441
+ select: mockSelect,
442
+ organisationId: mockOrganisationId
443
+ };
444
+
445
+ await secureDataAccess.secureQuery(options);
446
+
447
+ expect(mockQueryBuilder.eq).not.toHaveBeenCalledWith('organisation_id', mockOrganisationId);
448
+ });
449
+
450
+ it('should not apply organisation filter for super admin updates', async () => {
451
+ const mockData = { name: 'Updated Item' };
452
+ const mockFilters = { id: '1' };
453
+ const mockResult = { id: '1', name: 'Updated Item', organisation_id: 'other-org' };
454
+
455
+ mockQueryBuilder.update = vi.fn().mockReturnThis();
456
+ mockQueryBuilder.eq = vi.fn().mockReturnThis();
457
+ mockQueryBuilder.select = vi.fn().mockReturnThis();
458
+ mockQueryBuilder.single = vi.fn().mockResolvedValue({ data: mockResult, error: null });
459
+
460
+ await secureDataAccess.secureUpdate(mockTable, mockData, mockFilters, mockOrganisationId);
461
+
462
+ expect(mockQueryBuilder.eq).not.toHaveBeenCalledWith('organisation_id', mockOrganisationId);
463
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('id', '1');
464
+ });
465
+
466
+ it('should not apply organisation filter for super admin deletes', async () => {
467
+ const mockFilters = { id: '1' };
468
+
469
+ mockQueryBuilder.delete = vi.fn().mockReturnThis();
470
+ mockQueryBuilder.eq = vi.fn().mockReturnThis();
471
+ mockQueryBuilder.single = vi.fn().mockResolvedValue({ data: null, error: null });
472
+
473
+ await secureDataAccess.secureDelete(mockTable, mockFilters, mockOrganisationId);
474
+
475
+ expect(mockQueryBuilder.eq).not.toHaveBeenCalledWith('organisation_id', mockOrganisationId);
476
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('id', '1');
477
+ });
478
+ });
479
+
480
+ describe('Query Building Features', () => {
481
+ beforeEach(() => {
482
+ secureDataAccess = createSecureDataAccess(mockSupabaseClient, mockOrganisationId, false);
483
+ });
484
+
485
+ it('should handle ordering with qualified column names', async () => {
486
+ const mockData = [{ id: '1', name: 'Test', organisation_id: mockOrganisationId }];
487
+ // Create a new mock with the expected data
488
+ mockQueryBuilder = createMockQueryBuilder(mockData);
489
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
490
+ // Create a new mock with the expected data
491
+ mockQueryBuilder = createMockQueryBuilder(mockData);
492
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
493
+
494
+ const options: SecureQueryOptions = {
495
+ table: mockTable,
496
+ select: mockSelect,
497
+ organisationId: mockOrganisationId,
498
+ orderBy: 'users.created_at'
499
+ };
500
+
501
+ await secureDataAccess.secureQuery(options);
502
+
503
+ expect(mockQueryBuilder.order).toHaveBeenCalledWith('created_at');
504
+ });
505
+
506
+ it('should handle pagination with limit and offset', async () => {
507
+ const mockData = [{ id: '1', name: 'Test', organisation_id: mockOrganisationId }];
508
+ // Create a new mock with the expected data
509
+ mockQueryBuilder = createMockQueryBuilder(mockData);
510
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
511
+ // Create a new mock with the expected data
512
+ mockQueryBuilder = createMockQueryBuilder(mockData);
513
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
514
+
515
+ const options: SecureQueryOptions = {
516
+ table: mockTable,
517
+ select: mockSelect,
518
+ organisationId: mockOrganisationId,
519
+ limit: 10,
520
+ offset: 20
521
+ };
522
+
523
+ await secureDataAccess.secureQuery(options);
524
+
525
+ expect(mockQueryBuilder.limit).toHaveBeenCalledWith(10);
526
+ expect(mockQueryBuilder.range).toHaveBeenCalledWith(20, 29);
527
+ });
528
+
529
+ it('should handle pagination with limit only', async () => {
530
+ const mockData = [{ id: '1', name: 'Test', organisation_id: mockOrganisationId }];
531
+ // Create a new mock with the expected data
532
+ mockQueryBuilder = createMockQueryBuilder(mockData);
533
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
534
+ // Create a new mock with the expected data
535
+ mockQueryBuilder = createMockQueryBuilder(mockData);
536
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
537
+
538
+ const options: SecureQueryOptions = {
539
+ table: mockTable,
540
+ select: mockSelect,
541
+ organisationId: mockOrganisationId,
542
+ limit: 5
543
+ };
544
+
545
+ await secureDataAccess.secureQuery(options);
546
+
547
+ expect(mockQueryBuilder.limit).toHaveBeenCalledWith(5);
548
+ expect(mockQueryBuilder.range).not.toHaveBeenCalled();
549
+ });
550
+ });
551
+ });
552
+
553
+ describe('useSecureDataAccess', () => {
554
+ it('should throw error when used without explicit parameters', () => {
555
+ expect(() => {
556
+ useSecureDataAccess();
557
+ }).toThrow('useSecureDataAccess must be used with explicit parameters. Use createSecureDataAccess instead.');
558
+ });
559
+ });
560
+
561
+ describe('Error Handling', () => {
562
+ beforeEach(() => {
563
+ secureDataAccess = createSecureDataAccess(mockSupabaseClient, mockOrganisationId, false);
564
+ });
565
+
566
+ it('should propagate errors from secureQuery', async () => {
567
+ const mockError = new Error('Network error');
568
+ // Create a new mock with the error for the main query (not single)
569
+ mockQueryBuilder = createMockQueryBuilder(null, mockError);
570
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
571
+
572
+ const options: SecureQueryOptions = {
573
+ table: mockTable,
574
+ select: mockSelect,
575
+ organisationId: mockOrganisationId
576
+ };
577
+
578
+ await expect(secureDataAccess.secureQuery(options)).rejects.toThrow('Network error');
579
+ });
580
+
581
+ it('should propagate errors from secureSingleQuery', async () => {
582
+ const mockError = new Error('Database connection failed');
583
+ mockQueryBuilder.single = vi.fn().mockRejectedValue(mockError);
584
+
585
+ const options: SecureQueryOptions = {
586
+ table: mockTable,
587
+ select: mockSelect,
588
+ organisationId: mockOrganisationId
589
+ };
590
+
591
+ await expect(secureDataAccess.secureSingleQuery(options)).rejects.toThrow('Database connection failed');
592
+ });
593
+
594
+ it('should propagate errors from secureInsert', async () => {
595
+ const mockData = { name: 'Test Item' };
596
+ const mockError = new Error('Insert constraint violation');
597
+ mockQueryBuilder.insert = vi.fn().mockReturnThis();
598
+ mockQueryBuilder.select = vi.fn().mockReturnThis();
599
+ mockQueryBuilder.single = vi.fn().mockRejectedValue(mockError);
600
+
601
+ await expect(secureDataAccess.secureInsert(mockTable, mockData, mockOrganisationId))
602
+ .rejects.toThrow('Insert constraint violation');
603
+ });
604
+
605
+ it('should propagate errors from secureUpdate', async () => {
606
+ const mockData = { name: 'Updated Item' };
607
+ const mockFilters = { id: '1' };
608
+ const mockError = new Error('Update constraint violation');
609
+ mockQueryBuilder.update = vi.fn().mockReturnThis();
610
+ mockQueryBuilder.eq = vi.fn().mockReturnThis();
611
+ mockQueryBuilder.select = vi.fn().mockReturnThis();
612
+ mockQueryBuilder.single = vi.fn().mockRejectedValue(mockError);
613
+
614
+ await expect(secureDataAccess.secureUpdate(mockTable, mockData, mockFilters, mockOrganisationId))
615
+ .rejects.toThrow('Update constraint violation');
616
+ });
617
+
618
+ it('should propagate errors from secureDelete', async () => {
619
+ const mockFilters = { id: '1' };
620
+ const mockError = new Error('Delete constraint violation');
621
+ // Create a new mock with the error for the single query
622
+ mockQueryBuilder = createMockQueryBuilder(null, mockError);
623
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
624
+
625
+ await expect(secureDataAccess.secureDelete(mockTable, mockFilters, mockOrganisationId))
626
+ .rejects.toThrow('Delete constraint violation');
627
+ });
628
+ });
629
+
630
+ describe('Type Safety', () => {
631
+ beforeEach(() => {
632
+ secureDataAccess = createSecureDataAccess(mockSupabaseClient, mockOrganisationId, false);
633
+ });
634
+
635
+ it('should maintain type safety for DatabaseRecord', async () => {
636
+ interface TestRecord extends DatabaseRecord {
637
+ name: string;
638
+ status: string;
639
+ }
640
+
641
+ const mockData: TestRecord[] = [
642
+ { id: '1', name: 'Test 1', status: 'active', organisation_id: mockOrganisationId },
643
+ { id: '2', name: 'Test 2', status: 'inactive', organisation_id: mockOrganisationId }
644
+ ];
645
+
646
+ // Create a new mock with the expected data
647
+ mockQueryBuilder = createMockQueryBuilder(mockData);
648
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
649
+
650
+ const options: SecureQueryOptions = {
651
+ table: mockTable,
652
+ select: mockSelect,
653
+ organisationId: mockOrganisationId
654
+ };
655
+
656
+ const result = await secureDataAccess.secureQuery<TestRecord>(options);
657
+
658
+ expect(result).toEqual(mockData);
659
+ expect(result[0]).toHaveProperty('name');
660
+ expect(result[0]).toHaveProperty('status');
661
+ expect(result[0]).toHaveProperty('organisation_id');
662
+ });
663
+
664
+ it('should maintain type safety for DatabaseData', async () => {
665
+ const mockData: DatabaseData = {
666
+ name: 'Test Item',
667
+ status: 'active',
668
+ metadata: { key: 'value' }
669
+ };
670
+
671
+ const mockResult = { id: '1', ...mockData, organisation_id: mockOrganisationId };
672
+
673
+ mockQueryBuilder.insert = vi.fn().mockReturnThis();
674
+ mockQueryBuilder.select = vi.fn().mockReturnThis();
675
+ mockQueryBuilder.single = vi.fn().mockResolvedValue({ data: mockResult, error: null });
676
+
677
+ const result = await secureDataAccess.secureInsert(mockTable, mockData, mockOrganisationId);
678
+
679
+ expect(result).toEqual(mockResult);
680
+ });
681
+
682
+ it('should maintain type safety for DatabaseFilters', async () => {
683
+ const mockFilters: DatabaseFilters = {
684
+ status: 'active',
685
+ category: 'test',
686
+ metadata: { key: 'value' }
687
+ };
688
+
689
+ const mockData = [{ id: '1', name: 'Test', organisation_id: mockOrganisationId }];
690
+ // Create a new mock with the expected data
691
+ mockQueryBuilder = createMockQueryBuilder(mockData);
692
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
693
+ // Create a new mock with the expected data
694
+ mockQueryBuilder = createMockQueryBuilder(mockData);
695
+ mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
696
+
697
+ const options: SecureQueryOptions = {
698
+ table: mockTable,
699
+ select: mockSelect,
700
+ organisationId: mockOrganisationId,
701
+ filters: mockFilters
702
+ };
703
+
704
+ await secureDataAccess.secureQuery(options);
705
+
706
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('organisation_id', mockOrganisationId);
707
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('status', 'active');
708
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('category', 'test');
709
+ });
710
+ });
711
+ });