@jmruthers/pace-core 0.5.136 → 0.5.139
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.
- package/dist/{DataTable-CYOHOX3O.js → DataTable-JXFCA2BJ.js} +10 -9
- package/dist/{EventLogo-801uofbR.d.ts → EventLogo-rFL_kRjk.d.ts} +73 -1
- package/dist/{UnifiedAuthProvider-5E5TUNMS.js → UnifiedAuthProvider-XIQQ7LVU.js} +4 -5
- package/dist/{chunk-YLKIDTUK.js → chunk-22WKWKRX.js} +4 -4
- package/dist/{chunk-TVYPTYOY.js → chunk-4C7EXCAR.js} +60 -24
- package/dist/chunk-4C7EXCAR.js.map +1 -0
- package/dist/{chunk-NOHEVYVX.js → chunk-5JMOHWDI.js} +417 -319
- package/dist/chunk-5JMOHWDI.js.map +1 -0
- package/dist/{chunk-FHWWBIHA.js → chunk-6DXZ6V5Q.js} +5 -5
- package/dist/{chunk-2TWNJ46Y.js → chunk-6LAAY47Q.js} +2 -2
- package/dist/{chunk-444EZN6N.js → chunk-7QCC6MCP.js} +88 -1
- package/dist/chunk-7QCC6MCP.js.map +1 -0
- package/dist/chunk-BJPBT3CU.js +21 -0
- package/dist/chunk-BJPBT3CU.js.map +1 -0
- package/dist/{chunk-L6PGMCMD.js → chunk-BOOI7GK2.js} +38 -12
- package/dist/chunk-BOOI7GK2.js.map +1 -0
- package/dist/{chunk-XARJS7CD.js → chunk-INQLMHPF.js} +2 -2
- package/dist/chunk-JISYG63F.js +70 -0
- package/dist/chunk-JISYG63F.js.map +1 -0
- package/dist/{chunk-SL2YQDR6.js → chunk-MA6EPSGZ.js} +2 -2
- package/dist/{chunk-5DPZ5EAT.js → chunk-OWAG3GSU.js} +1 -3
- package/dist/{chunk-LTV3XIJJ.js → chunk-T6JN6LH6.js} +4 -4
- package/dist/{chunk-HJGGOMQ6.js → chunk-TLT2ZR3L.js} +147 -103
- package/dist/chunk-TLT2ZR3L.js.map +1 -0
- package/dist/{chunk-4MT5BGGL.js → chunk-YCWDTTUK.js} +4 -6
- package/dist/{chunk-4MT5BGGL.js.map → chunk-YCWDTTUK.js.map} +1 -1
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -11
- package/dist/components.js.map +1 -1
- package/dist/hooks.js +8 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +15 -14
- package/dist/index.js.map +1 -1
- package/dist/providers.js +3 -4
- package/dist/rbac/index.js +8 -9
- package/dist/schema-DTDZQe2u.d.ts +28 -0
- package/dist/types.d.ts +152 -3
- package/dist/types.js +51 -16
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +89 -4
- package/dist/utils.js +214 -96
- package/dist/utils.js.map +1 -1
- package/dist/validation.d.ts +1 -343
- package/dist/validation.js +3 -100
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +27 -0
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +84 -15
- package/docs/architecture/README.md +0 -1
- package/docs/styles/README.md +0 -2
- package/examples/RBAC/CompleteRBACExample.tsx +324 -0
- package/examples/RBAC/EventBasedApp.tsx +239 -0
- package/examples/RBAC/PermissionExample.tsx +151 -0
- package/examples/RBAC/index.ts +13 -0
- package/examples/public-pages/CorrectPublicPageImplementation.tsx +301 -0
- package/examples/public-pages/PublicEventPage.tsx +274 -0
- package/examples/public-pages/PublicPageApp.tsx +308 -0
- package/examples/public-pages/PublicPageUsageExample.tsx +216 -0
- package/examples/public-pages/index.ts +14 -0
- package/package.json +1 -10
- package/src/__tests__/TEST_STANDARD.md +92 -0
- package/src/components/Badge/Badge.test.tsx +314 -0
- package/src/components/Badge/Badge.tsx +304 -0
- package/src/components/Badge/index.ts +3 -0
- package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +217 -0
- package/src/components/DataTable/__tests__/styles.test.ts +1 -1
- package/src/components/DataTable/components/ColumnFilter.tsx +8 -4
- package/src/components/DataTable/components/DataTableBody.tsx +461 -0
- package/src/components/DataTable/components/DraggableColumnHeader.tsx +144 -0
- package/src/components/DataTable/components/FilterRow.tsx +9 -3
- package/src/components/DataTable/components/PaginationControls.tsx +1 -0
- package/src/components/DataTable/components/VirtualizedDataTable.tsx +513 -0
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +14 -68
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +62 -0
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +43 -0
- package/src/components/DataTable/core/ActionManager.ts +235 -0
- package/src/components/DataTable/core/ColumnManager.ts +205 -0
- package/src/components/DataTable/core/DataManager.ts +188 -0
- package/src/components/DataTable/core/DataTableContext.tsx +181 -0
- package/src/components/DataTable/core/LocalDataAdapter.ts +273 -0
- package/src/components/DataTable/core/PluginRegistry.ts +229 -0
- package/src/components/DataTable/core/StateManager.ts +311 -0
- package/src/components/DataTable/core/interfaces.ts +338 -0
- package/src/components/DataTable/styles.ts +27 -6
- package/src/components/DataTable/utils/__tests__/columnUtils.test.ts +94 -0
- package/src/components/DataTable/utils/columnUtils.ts +40 -0
- package/src/components/DataTable/utils/debugTools.ts +609 -0
- package/src/components/DataTable/utils/index.ts +1 -0
- package/src/components/Dialog/README.md +804 -0
- package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +611 -0
- package/src/components/Dialog/utils/safeHtml.ts +185 -0
- package/src/components/Footer/Footer.test.tsx +1 -1
- package/src/components/Form/Form.test.tsx +1 -1
- package/src/components/Form/FormErrorSummary.tsx +113 -0
- package/src/components/Form/FormFieldset.tsx +127 -0
- package/src/components/Form/FormLiveRegion.tsx +198 -0
- package/src/components/LoginForm/LoginForm.test.tsx +1 -1
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +76 -10
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
- package/src/components/PasswordReset/PasswordResetForm.test.tsx +597 -0
- package/src/components/PasswordReset/PasswordResetForm.tsx +201 -0
- package/src/components/PublicLayout/PublicPageDebugger.tsx +104 -0
- package/src/components/PublicLayout/PublicPageDiagnostic.tsx +162 -0
- package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
- package/src/components/Select/Select.test.tsx +1 -1
- package/src/components/Select/Select.tsx +20 -8
- package/src/components/Table/__tests__/Table.test.tsx +1 -1
- package/src/components/index.ts +3 -0
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +83 -85
- package/src/index.ts +4 -0
- package/src/rbac/hooks/useCan.test.ts +24 -0
- package/src/rbac/hooks/usePermissions.ts +49 -12
- package/src/styles/core.css +3 -0
- package/src/utils/appConfig.ts +47 -0
- package/src/utils/appIdResolver.test.ts +499 -0
- package/src/utils/appIdResolver.ts +130 -0
- package/src/utils/appNameResolver.simple.test.ts +212 -0
- package/src/utils/appNameResolver.test.ts +121 -0
- package/src/utils/appNameResolver.ts +191 -0
- package/src/utils/audit.ts +127 -0
- package/src/utils/auth-utils.ts +96 -0
- package/src/utils/bundleAnalysis.ts +129 -0
- package/src/utils/cn.ts +7 -0
- package/src/utils/debugLogger.ts +67 -0
- package/src/utils/deviceFingerprint.ts +215 -0
- package/src/utils/dynamicUtils.ts +105 -0
- package/src/utils/file-reference.test.ts +788 -0
- package/src/utils/file-reference.ts +519 -0
- package/src/utils/formatDate.test.ts +237 -0
- package/src/utils/formatting.ts +133 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/lazyLoad.tsx +44 -0
- package/src/utils/logger.ts +179 -0
- package/src/utils/organisationContext.test.ts +322 -0
- package/src/utils/organisationContext.ts +153 -0
- package/src/utils/performanceBenchmark.ts +64 -0
- package/src/utils/performanceBudgets.ts +110 -0
- package/src/utils/permissionTypes.ts +37 -0
- package/src/utils/permissionUtils.test.ts +393 -0
- package/src/utils/permissionUtils.ts +34 -0
- package/src/utils/sanitization.ts +264 -0
- package/src/utils/schemaUtils.ts +37 -0
- package/src/utils/secureDataAccess.test.ts +711 -0
- package/src/utils/secureDataAccess.ts +377 -0
- package/src/utils/secureErrors.ts +79 -0
- package/src/utils/secureStorage.ts +244 -0
- package/src/utils/security.ts +156 -0
- package/src/utils/securityMonitor.ts +45 -0
- package/src/utils/sessionTracking.ts +126 -0
- package/src/utils/validation.ts +111 -0
- package/src/utils/validationUtils.ts +120 -0
- package/src/validation/index.ts +2 -2
- package/dist/chunk-444EZN6N.js.map +0 -1
- package/dist/chunk-APIBCTL2.js +0 -670
- package/dist/chunk-APIBCTL2.js.map +0 -1
- package/dist/chunk-HJGGOMQ6.js.map +0 -1
- package/dist/chunk-K2WWTH7O.js +0 -94
- package/dist/chunk-K2WWTH7O.js.map +0 -1
- package/dist/chunk-L6PGMCMD.js.map +0 -1
- package/dist/chunk-LMC26NLJ.js +0 -84
- package/dist/chunk-LMC26NLJ.js.map +0 -1
- package/dist/chunk-NOHEVYVX.js.map +0 -1
- package/dist/chunk-TVYPTYOY.js.map +0 -1
- package/dist/validation-8npbysjg.d.ts +0 -177
- /package/dist/{DataTable-CYOHOX3O.js.map → DataTable-JXFCA2BJ.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-5E5TUNMS.js.map → UnifiedAuthProvider-XIQQ7LVU.js.map} +0 -0
- /package/dist/{chunk-YLKIDTUK.js.map → chunk-22WKWKRX.js.map} +0 -0
- /package/dist/{chunk-FHWWBIHA.js.map → chunk-6DXZ6V5Q.js.map} +0 -0
- /package/dist/{chunk-2TWNJ46Y.js.map → chunk-6LAAY47Q.js.map} +0 -0
- /package/dist/{chunk-XARJS7CD.js.map → chunk-INQLMHPF.js.map} +0 -0
- /package/dist/{chunk-SL2YQDR6.js.map → chunk-MA6EPSGZ.js.map} +0 -0
- /package/dist/{chunk-5DPZ5EAT.js.map → chunk-OWAG3GSU.js.map} +0 -0
- /package/dist/{chunk-LTV3XIJJ.js.map → chunk-T6JN6LH6.js.map} +0 -0
- /package/examples/{components → components 2}/DataTable/HierarchicalActionsExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/HierarchicalExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/InitialPageSizeExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/PerformanceExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/index.ts +0 -0
- /package/examples/{components → components 2}/Dialog/BasicHtmlTest.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/DebugHtmlExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/HtmlDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/ScrollableDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/SimpleHtmlTest.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/SmartDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/index.ts +0 -0
- /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
|
+
});
|