@jmruthers/pace-core 0.5.4 → 0.5.6
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-ZQDRE46Q.js → DataTable-BEMN72L5.js} +2 -2
- package/dist/{chunk-5H3C2SWM.js → chunk-4EIBJ6DF.js} +2 -2
- package/dist/{chunk-M4RW7PIP.js → chunk-SFGUMWEE.js} +105 -81
- package/dist/chunk-SFGUMWEE.js.map +1 -0
- package/dist/components.js +2 -2
- package/dist/index.js +2 -2
- package/dist/utils.js +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/interfaces/AggregateConfig.md +1 -1
- 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/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +34 -34
- 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/EventContextType.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/EventProviderProps.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.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/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/RBACContextType.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACProviderProps.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.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/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/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/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.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 +3 -3
- package/docs/implementation-guides/data-tables.md +20 -0
- package/docs/quick-reference.md +9 -0
- package/docs/rbac/examples.md +4 -0
- package/package.json +1 -1
- package/src/__tests__/helpers/test-utils.tsx +147 -1
- package/src/components/DataTable/DataTable.tsx +20 -0
- package/src/components/DataTable/__tests__/DataTable.hooks.test 2.tsx +191 -0
- package/src/components/DataTable/__tests__/DataTable.hooks.test.tsx +191 -0
- package/src/components/DataTable/components/DataTableCore.tsx +164 -131
- package/src/hooks/__tests__/hooks.integration.test.tsx +575 -0
- package/src/hooks/__tests__/useApiFetch.unit.test.ts +115 -0
- package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +133 -0
- package/src/hooks/__tests__/useDebounce.unit.test.ts +82 -0
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +293 -0
- package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +385 -0
- package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +286 -0
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +838 -0
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +104 -0
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +633 -0
- package/src/hooks/__tests__/useRBAC.unit.test.ts +856 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +537 -0
- package/src/hooks/__tests__/useToast.unit.test.tsx +62 -0
- package/src/hooks/__tests__/useZodForm.unit.test.tsx +37 -0
- package/src/rbac/utils/__tests__/eventContext.test.ts +428 -0
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +428 -0
- package/src/utils/__tests__/appConfig.unit.test.ts +55 -0
- package/src/utils/__tests__/audit.unit.test.ts +69 -0
- package/src/utils/__tests__/auth-utils.unit.test.ts +70 -0
- package/src/utils/__tests__/bundleAnalysis.unit.test.ts +317 -0
- package/src/utils/__tests__/cn.unit.test.ts +34 -0
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +503 -0
- package/src/utils/__tests__/dynamicUtils.unit.test.ts +322 -0
- package/src/utils/__tests__/formatDate.unit.test.ts +109 -0
- package/src/utils/__tests__/formatting.unit.test.ts +66 -0
- package/src/utils/__tests__/index.unit.test.ts +251 -0
- package/src/utils/__tests__/lazyLoad.unit.test.tsx +309 -0
- package/src/utils/__tests__/organisationContext.unit.test.ts +192 -0
- package/src/utils/__tests__/performanceBudgets.unit.test.ts +259 -0
- package/src/utils/__tests__/permissionTypes.unit.test.ts +250 -0
- package/src/utils/__tests__/permissionUtils.unit.test.ts +362 -0
- package/src/utils/__tests__/sanitization.unit.test.ts +346 -0
- package/src/utils/__tests__/schemaUtils.unit.test.ts +441 -0
- package/src/utils/__tests__/secureDataAccess.unit.test.ts +334 -0
- package/src/utils/__tests__/secureErrors.unit.test.ts +377 -0
- package/src/utils/__tests__/secureStorage.unit.test.ts +293 -0
- package/src/utils/__tests__/security.unit.test.ts +127 -0
- package/src/utils/__tests__/securityMonitor.unit.test.ts +280 -0
- package/src/utils/__tests__/sessionTracking.unit.test.ts +356 -0
- package/src/utils/__tests__/validation.unit.test.ts +84 -0
- package/src/utils/__tests__/validationUtils.unit.test.ts +571 -0
- package/src/validation/__tests__/common.unit.test.ts +101 -0
- package/src/validation/__tests__/csrf.unit.test.ts +302 -0
- package/src/validation/__tests__/passwordSchema.unit.test 2.ts +98 -0
- package/src/validation/__tests__/passwordSchema.unit.test.ts +98 -0
- package/src/validation/__tests__/sqlInjectionProtection.unit.test.ts +466 -0
- package/dist/chunk-M4RW7PIP.js.map +0 -1
- /package/dist/{DataTable-ZQDRE46Q.js.map → DataTable-BEMN72L5.js.map} +0 -0
- /package/dist/{chunk-5H3C2SWM.js.map → chunk-4EIBJ6DF.js.map} +0 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react';
|
|
2
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
3
|
+
import { useSecureDataAccess } from '../useSecureDataAccess';
|
|
4
|
+
import { useUnifiedAuth } from '../../providers/UnifiedAuthProvider';
|
|
5
|
+
import { useOrganisations } from '../../providers/OrganisationProvider';
|
|
6
|
+
import { testDataGenerators } from '../../__tests__/helpers/test-utils';
|
|
7
|
+
|
|
8
|
+
// Mock dependencies
|
|
9
|
+
vi.mock('../../providers/UnifiedAuthProvider');
|
|
10
|
+
vi.mock('../../providers/OrganisationProvider');
|
|
11
|
+
|
|
12
|
+
const mockUseUnifiedAuth = {
|
|
13
|
+
user: null,
|
|
14
|
+
session: null,
|
|
15
|
+
supabase: null
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const mockUseOrganisations = {
|
|
19
|
+
ensureOrganisationContext: vi.fn()
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Helper function to create authenticated context
|
|
23
|
+
const createAuthenticatedContext = (supabase = { auth: {} }, org = { id: 'org-123' }) => {
|
|
24
|
+
const mockUser = testDataGenerators.createUser(1, { id: 'user-123' });
|
|
25
|
+
const mockSession = testDataGenerators.createSession(1, { access_token: 'test-token' });
|
|
26
|
+
|
|
27
|
+
vi.mocked(useUnifiedAuth).mockReturnValue({
|
|
28
|
+
...mockUseUnifiedAuth,
|
|
29
|
+
user: mockUser,
|
|
30
|
+
session: mockSession,
|
|
31
|
+
supabase
|
|
32
|
+
} as any);
|
|
33
|
+
vi.mocked(mockUseOrganisations.ensureOrganisationContext).mockReturnValue(org);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('useSecureDataAccess', () => {
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
vi.clearAllMocks();
|
|
39
|
+
vi.mocked(useUnifiedAuth).mockReturnValue(mockUseUnifiedAuth as any);
|
|
40
|
+
vi.mocked(useOrganisations).mockReturnValue(mockUseOrganisations as any);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('validateContext', () => {
|
|
44
|
+
it('should throw error when no supabase client', () => {
|
|
45
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
46
|
+
|
|
47
|
+
expect(() => result.current.validateContext()).toThrow('No Supabase client available');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should throw error when no organisation context', () => {
|
|
51
|
+
const mockSupabase = { auth: {} };
|
|
52
|
+
vi.mocked(useUnifiedAuth).mockReturnValue({
|
|
53
|
+
...mockUseUnifiedAuth,
|
|
54
|
+
supabase: mockSupabase
|
|
55
|
+
} as any);
|
|
56
|
+
vi.mocked(mockUseOrganisations.ensureOrganisationContext).mockImplementation(() => {
|
|
57
|
+
throw new Error('Organisation context is required');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
61
|
+
|
|
62
|
+
expect(() => result.current.validateContext()).toThrow('User must be authenticated with valid session');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should not throw when context is valid', () => {
|
|
66
|
+
createAuthenticatedContext();
|
|
67
|
+
|
|
68
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
69
|
+
|
|
70
|
+
expect(() => result.current.validateContext()).not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('getCurrentOrganisationId', () => {
|
|
75
|
+
it('should return organisation ID when context is valid', () => {
|
|
76
|
+
createAuthenticatedContext();
|
|
77
|
+
|
|
78
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
79
|
+
|
|
80
|
+
expect(result.current.getCurrentOrganisationId()).toBe('org-123');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should throw error when context is invalid', () => {
|
|
84
|
+
const mockSupabase = { auth: {} };
|
|
85
|
+
vi.mocked(useUnifiedAuth).mockReturnValue({
|
|
86
|
+
...mockUseUnifiedAuth,
|
|
87
|
+
supabase: mockSupabase
|
|
88
|
+
} as any);
|
|
89
|
+
vi.mocked(mockUseOrganisations.ensureOrganisationContext).mockImplementation(() => {
|
|
90
|
+
throw new Error('Organisation context is required');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
94
|
+
|
|
95
|
+
expect(() => result.current.getCurrentOrganisationId()).toThrow('User must be authenticated with valid session');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('secureQuery', () => {
|
|
100
|
+
it('should execute query with organisation filter', async () => {
|
|
101
|
+
// Create a mock that returns a promise when awaited
|
|
102
|
+
const mockQuery = {
|
|
103
|
+
eq: vi.fn().mockReturnThis(),
|
|
104
|
+
order: vi.fn().mockReturnThis(),
|
|
105
|
+
limit: vi.fn().mockReturnThis(),
|
|
106
|
+
range: vi.fn().mockReturnThis()
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Create a promise that resolves to the expected data
|
|
110
|
+
const mockPromise = Promise.resolve({ data: [{ id: 1, name: 'Test' }], error: null });
|
|
111
|
+
|
|
112
|
+
// Make the mock query chain behave like a promise
|
|
113
|
+
Object.defineProperty(mockQuery, 'then', {
|
|
114
|
+
value: mockPromise.then.bind(mockPromise),
|
|
115
|
+
writable: true,
|
|
116
|
+
configurable: true
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const mockSupabase = {
|
|
120
|
+
from: vi.fn().mockReturnValue({
|
|
121
|
+
select: vi.fn().mockReturnValue(mockQuery)
|
|
122
|
+
})
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
createAuthenticatedContext(mockSupabase);
|
|
126
|
+
|
|
127
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
128
|
+
|
|
129
|
+
const data = await result.current.secureQuery('test_table', '*');
|
|
130
|
+
|
|
131
|
+
expect(data).toEqual([{ id: 1, name: 'Test' }]);
|
|
132
|
+
expect(mockSupabase.from).toHaveBeenCalledWith('test_table');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should handle query errors', async () => {
|
|
136
|
+
// Create a mock that returns a promise with an error
|
|
137
|
+
const mockQuery = {
|
|
138
|
+
eq: vi.fn().mockReturnThis(),
|
|
139
|
+
order: vi.fn().mockReturnThis(),
|
|
140
|
+
limit: vi.fn().mockReturnThis(),
|
|
141
|
+
range: vi.fn().mockReturnThis()
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Create a promise that resolves to an error
|
|
145
|
+
const mockPromise = Promise.resolve({ data: null, error: new Error('Query failed') });
|
|
146
|
+
|
|
147
|
+
// Make the mock query chain behave like a promise
|
|
148
|
+
Object.defineProperty(mockQuery, 'then', {
|
|
149
|
+
value: mockPromise.then.bind(mockPromise),
|
|
150
|
+
writable: true,
|
|
151
|
+
configurable: true
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const mockSupabase = {
|
|
155
|
+
from: vi.fn().mockReturnValue({
|
|
156
|
+
select: vi.fn().mockReturnValue(mockQuery)
|
|
157
|
+
})
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
createAuthenticatedContext(mockSupabase);
|
|
161
|
+
|
|
162
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
163
|
+
|
|
164
|
+
await expect(result.current.secureQuery('test_table', '*')).rejects.toThrow('Query failed');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should handle null/undefined filter values', async () => {
|
|
168
|
+
const mockSupabase = {
|
|
169
|
+
from: vi.fn().mockReturnValue({
|
|
170
|
+
select: vi.fn().mockReturnValue({
|
|
171
|
+
eq: vi.fn().mockReturnValue({
|
|
172
|
+
eq: vi.fn().mockResolvedValue({ data: [], error: null })
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
createAuthenticatedContext(mockSupabase);
|
|
179
|
+
|
|
180
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
181
|
+
|
|
182
|
+
const data = await result.current.secureQuery('test_table', '*', {
|
|
183
|
+
status: 'active',
|
|
184
|
+
category: null,
|
|
185
|
+
type: undefined
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(data).toEqual([]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should return empty array when no data', async () => {
|
|
192
|
+
const mockSupabase = {
|
|
193
|
+
from: vi.fn().mockReturnValue({
|
|
194
|
+
select: vi.fn().mockReturnValue({
|
|
195
|
+
eq: vi.fn().mockResolvedValue({ data: null, error: null })
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
};
|
|
199
|
+
createAuthenticatedContext(mockSupabase);
|
|
200
|
+
|
|
201
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
202
|
+
|
|
203
|
+
const data = await result.current.secureQuery('test_table', '*');
|
|
204
|
+
|
|
205
|
+
expect(data).toEqual([]);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('secureInsert', () => {
|
|
210
|
+
it('should insert data with organisation ID', async () => {
|
|
211
|
+
const mockSupabase = {
|
|
212
|
+
from: vi.fn().mockReturnValue({
|
|
213
|
+
insert: vi.fn().mockReturnValue({
|
|
214
|
+
select: vi.fn().mockReturnValue({
|
|
215
|
+
single: vi.fn().mockResolvedValue({ data: { id: 1, name: 'Test', organisation_id: 'org-123' }, error: null })
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
};
|
|
220
|
+
createAuthenticatedContext(mockSupabase);
|
|
221
|
+
|
|
222
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
223
|
+
|
|
224
|
+
const data = await result.current.secureInsert('test_table', { name: 'Test' });
|
|
225
|
+
|
|
226
|
+
expect(data).toEqual({ id: 1, name: 'Test', organisation_id: 'org-123' });
|
|
227
|
+
expect(mockSupabase.from).toHaveBeenCalledWith('test_table');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should handle insert errors', async () => {
|
|
231
|
+
const mockSupabase = {
|
|
232
|
+
from: vi.fn().mockReturnValue({
|
|
233
|
+
insert: vi.fn().mockReturnValue({
|
|
234
|
+
select: vi.fn().mockReturnValue({
|
|
235
|
+
single: vi.fn().mockResolvedValue({ data: null, error: new Error('Insert failed') })
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
};
|
|
240
|
+
createAuthenticatedContext(mockSupabase);
|
|
241
|
+
|
|
242
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
243
|
+
|
|
244
|
+
await expect(result.current.secureInsert('test_table', { name: 'Test' })).rejects.toThrow('Insert failed');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('secureUpdate', () => {
|
|
249
|
+
it('should update data with organisation filter', async () => {
|
|
250
|
+
const mockSupabase = {
|
|
251
|
+
from: vi.fn().mockReturnValue({
|
|
252
|
+
update: vi.fn().mockReturnValue({
|
|
253
|
+
eq: vi.fn().mockReturnValue({
|
|
254
|
+
eq: vi.fn().mockReturnValue({
|
|
255
|
+
select: vi.fn().mockResolvedValue({ data: [{ id: 1, name: 'Updated', organisation_id: 'org-123' }], error: null })
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
};
|
|
261
|
+
createAuthenticatedContext(mockSupabase);
|
|
262
|
+
|
|
263
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
264
|
+
|
|
265
|
+
const data = await result.current.secureUpdate('event', { name: 'Updated' }, { id: 1 });
|
|
266
|
+
|
|
267
|
+
expect(data).toEqual([{ id: 1, name: 'Updated', organisation_id: 'org-123' }]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should handle update errors', async () => {
|
|
271
|
+
const mockSupabase = {
|
|
272
|
+
from: vi.fn().mockReturnValue({
|
|
273
|
+
update: vi.fn().mockReturnValue({
|
|
274
|
+
eq: vi.fn().mockReturnValue({
|
|
275
|
+
eq: vi.fn().mockReturnValue({
|
|
276
|
+
select: vi.fn().mockResolvedValue({ data: null, error: new Error('Update failed') })
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
};
|
|
282
|
+
createAuthenticatedContext(mockSupabase);
|
|
283
|
+
|
|
284
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
285
|
+
|
|
286
|
+
await expect(result.current.secureUpdate('event', { name: 'Updated' }, { id: 1 })).rejects.toThrow('Update failed');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should return empty array when no data updated', async () => {
|
|
290
|
+
const mockSupabase = {
|
|
291
|
+
from: vi.fn().mockReturnValue({
|
|
292
|
+
update: vi.fn().mockReturnValue({
|
|
293
|
+
eq: vi.fn().mockReturnValue({
|
|
294
|
+
eq: vi.fn().mockReturnValue({
|
|
295
|
+
select: vi.fn().mockResolvedValue({ data: null, error: null })
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
};
|
|
301
|
+
createAuthenticatedContext(mockSupabase);
|
|
302
|
+
|
|
303
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
304
|
+
|
|
305
|
+
const data = await result.current.secureUpdate('event', { name: 'Updated' }, { id: 1 });
|
|
306
|
+
|
|
307
|
+
expect(data).toEqual([]);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('secureDelete', () => {
|
|
312
|
+
it('should delete data with organisation filter', async () => {
|
|
313
|
+
const mockSupabase = {
|
|
314
|
+
from: vi.fn().mockReturnValue({
|
|
315
|
+
delete: vi.fn().mockReturnValue({
|
|
316
|
+
eq: vi.fn().mockReturnValue({
|
|
317
|
+
eq: vi.fn().mockResolvedValue({ error: null })
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
};
|
|
322
|
+
createAuthenticatedContext(mockSupabase);
|
|
323
|
+
|
|
324
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
325
|
+
|
|
326
|
+
await result.current.secureDelete('event', { id: 1 });
|
|
327
|
+
|
|
328
|
+
expect(mockSupabase.from).toHaveBeenCalledWith('event');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should handle delete errors', async () => {
|
|
332
|
+
const mockSupabase = {
|
|
333
|
+
from: vi.fn().mockReturnValue({
|
|
334
|
+
delete: vi.fn().mockReturnValue({
|
|
335
|
+
eq: vi.fn().mockReturnValue({
|
|
336
|
+
eq: vi.fn().mockResolvedValue({ error: new Error('Delete failed') })
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
};
|
|
341
|
+
createAuthenticatedContext(mockSupabase);
|
|
342
|
+
|
|
343
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
344
|
+
|
|
345
|
+
await expect(result.current.secureDelete('event', { id: 1 })).rejects.toThrow('Delete failed');
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('secureRpc', () => {
|
|
350
|
+
it('should call RPC with organisation ID', async () => {
|
|
351
|
+
const mockSupabase = {
|
|
352
|
+
rpc: vi.fn().mockResolvedValue({ data: { result: 'success' }, error: null })
|
|
353
|
+
};
|
|
354
|
+
createAuthenticatedContext(mockSupabase);
|
|
355
|
+
|
|
356
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
357
|
+
|
|
358
|
+
const data = await result.current.secureRpc('test_function', { param1: 'value1' });
|
|
359
|
+
|
|
360
|
+
expect(data).toEqual({ result: 'success' });
|
|
361
|
+
expect(mockSupabase.rpc).toHaveBeenCalledWith('test_function', {
|
|
362
|
+
param1: 'value1',
|
|
363
|
+
organisation_id: 'org-123'
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should handle RPC errors', async () => {
|
|
368
|
+
const mockSupabase = {
|
|
369
|
+
rpc: vi.fn().mockResolvedValue({ data: null, error: new Error('RPC failed') })
|
|
370
|
+
};
|
|
371
|
+
createAuthenticatedContext(mockSupabase);
|
|
372
|
+
|
|
373
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
374
|
+
|
|
375
|
+
await expect(result.current.secureRpc('test_function')).rejects.toThrow('RPC failed');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should call RPC without additional params', async () => {
|
|
379
|
+
const mockSupabase = {
|
|
380
|
+
rpc: vi.fn().mockResolvedValue({ data: { result: 'success' }, error: null })
|
|
381
|
+
};
|
|
382
|
+
createAuthenticatedContext(mockSupabase);
|
|
383
|
+
|
|
384
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
385
|
+
|
|
386
|
+
await result.current.secureRpc('test_function');
|
|
387
|
+
|
|
388
|
+
expect(mockSupabase.rpc).toHaveBeenCalledWith('test_function', {
|
|
389
|
+
organisation_id: 'org-123'
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe('Logging', () => {
|
|
395
|
+
it('should log secure query operations', async () => {
|
|
396
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
397
|
+
const mockSupabase = {
|
|
398
|
+
from: vi.fn().mockReturnValue({
|
|
399
|
+
select: vi.fn().mockReturnValue({
|
|
400
|
+
eq: vi.fn().mockReturnValue({
|
|
401
|
+
eq: vi.fn().mockResolvedValue({ data: [], error: null })
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
};
|
|
406
|
+
createAuthenticatedContext(mockSupabase);
|
|
407
|
+
|
|
408
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
409
|
+
|
|
410
|
+
await result.current.secureQuery('test_table', '*', { status: 'active' });
|
|
411
|
+
|
|
412
|
+
expect(consoleSpy).toHaveBeenCalledWith('[useSecureDataAccess] Executing secure query:', {
|
|
413
|
+
table: 'test_table',
|
|
414
|
+
organisationId: 'org-123',
|
|
415
|
+
filters: { status: 'active' }
|
|
416
|
+
});
|
|
417
|
+
expect(consoleSpy).toHaveBeenCalledWith('[useSecureDataAccess] Query successful:', {
|
|
418
|
+
table: 'test_table',
|
|
419
|
+
resultCount: 0
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
consoleSpy.mockRestore();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should log secure insert operations', async () => {
|
|
426
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
427
|
+
const mockSupabase = {
|
|
428
|
+
from: vi.fn().mockReturnValue({
|
|
429
|
+
insert: vi.fn().mockReturnValue({
|
|
430
|
+
select: vi.fn().mockReturnValue({
|
|
431
|
+
single: vi.fn().mockResolvedValue({ data: { id: 1 }, error: null })
|
|
432
|
+
})
|
|
433
|
+
})
|
|
434
|
+
})
|
|
435
|
+
};
|
|
436
|
+
createAuthenticatedContext(mockSupabase);
|
|
437
|
+
|
|
438
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
439
|
+
|
|
440
|
+
await result.current.secureInsert('test_table', { name: 'Test' });
|
|
441
|
+
|
|
442
|
+
expect(consoleSpy).toHaveBeenCalledWith('[useSecureDataAccess] Executing secure insert:', {
|
|
443
|
+
table: 'test_table',
|
|
444
|
+
organisationId: 'org-123'
|
|
445
|
+
});
|
|
446
|
+
expect(consoleSpy).toHaveBeenCalledWith('[useSecureDataAccess] Insert successful:', {
|
|
447
|
+
table: 'test_table',
|
|
448
|
+
id: 1
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
consoleSpy.mockRestore();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('should log secure update operations', async () => {
|
|
455
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
456
|
+
const mockSupabase = {
|
|
457
|
+
from: vi.fn().mockReturnValue({
|
|
458
|
+
update: vi.fn().mockReturnValue({
|
|
459
|
+
eq: vi.fn().mockReturnValue({
|
|
460
|
+
eq: vi.fn().mockReturnValue({
|
|
461
|
+
select: vi.fn().mockResolvedValue({ data: [{ id: 1 }], error: null })
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
})
|
|
465
|
+
})
|
|
466
|
+
};
|
|
467
|
+
createAuthenticatedContext(mockSupabase);
|
|
468
|
+
|
|
469
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
470
|
+
|
|
471
|
+
await result.current.secureUpdate('event', { name: 'Updated' }, { id: 1 });
|
|
472
|
+
|
|
473
|
+
expect(consoleSpy).toHaveBeenCalledWith('[useSecureDataAccess] Executing secure update:', {
|
|
474
|
+
table: 'event',
|
|
475
|
+
organisationId: 'org-123',
|
|
476
|
+
filters: { id: 1 }
|
|
477
|
+
});
|
|
478
|
+
expect(consoleSpy).toHaveBeenCalledWith('[useSecureDataAccess] Update successful:', {
|
|
479
|
+
table: 'event',
|
|
480
|
+
updatedCount: 1
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
consoleSpy.mockRestore();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should log secure delete operations', async () => {
|
|
487
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
488
|
+
const mockSupabase = {
|
|
489
|
+
from: vi.fn().mockReturnValue({
|
|
490
|
+
delete: vi.fn().mockReturnValue({
|
|
491
|
+
eq: vi.fn().mockReturnValue({
|
|
492
|
+
eq: vi.fn().mockResolvedValue({ error: null })
|
|
493
|
+
})
|
|
494
|
+
})
|
|
495
|
+
})
|
|
496
|
+
};
|
|
497
|
+
createAuthenticatedContext(mockSupabase);
|
|
498
|
+
|
|
499
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
500
|
+
|
|
501
|
+
await result.current.secureDelete('test_table', { id: 1 });
|
|
502
|
+
|
|
503
|
+
expect(consoleSpy).toHaveBeenCalledWith('[useSecureDataAccess] Executing secure delete:', {
|
|
504
|
+
table: 'test_table',
|
|
505
|
+
organisationId: 'org-123',
|
|
506
|
+
filters: { id: 1 }
|
|
507
|
+
});
|
|
508
|
+
expect(consoleSpy).toHaveBeenCalledWith('[useSecureDataAccess] Delete successful:', {
|
|
509
|
+
table: 'test_table'
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
consoleSpy.mockRestore();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('should log secure RPC operations', async () => {
|
|
516
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
517
|
+
const mockSupabase = {
|
|
518
|
+
rpc: vi.fn().mockResolvedValue({ data: { result: 'success' }, error: null })
|
|
519
|
+
};
|
|
520
|
+
createAuthenticatedContext(mockSupabase);
|
|
521
|
+
|
|
522
|
+
const { result } = renderHook(() => useSecureDataAccess());
|
|
523
|
+
|
|
524
|
+
await result.current.secureRpc('test_function', { param1: 'value1' });
|
|
525
|
+
|
|
526
|
+
expect(consoleSpy).toHaveBeenCalledWith('[useSecureDataAccess] Executing secure RPC:', {
|
|
527
|
+
functionName: 'test_function',
|
|
528
|
+
organisationId: 'org-123'
|
|
529
|
+
});
|
|
530
|
+
expect(consoleSpy).toHaveBeenCalledWith('[useSecureDataAccess] RPC successful:', {
|
|
531
|
+
functionName: 'test_function'
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
consoleSpy.mockRestore();
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
|
|
2
|
+
import { renderHook, act } from '@testing-library/react';
|
|
3
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
+
import { useToast, reset } from '../useToast';
|
|
5
|
+
|
|
6
|
+
describe('useToast', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
reset();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should add a toast', () => {
|
|
12
|
+
const { result } = renderHook(() => useToast());
|
|
13
|
+
|
|
14
|
+
act(() => {
|
|
15
|
+
result.current.toast({
|
|
16
|
+
title: 'Test Toast',
|
|
17
|
+
description: 'This is a test toast',
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(result.current.toasts).toHaveLength(1);
|
|
22
|
+
expect(result.current.toasts[0]).toMatchObject({
|
|
23
|
+
title: 'Test Toast',
|
|
24
|
+
description: 'This is a test toast',
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should dismiss a toast', () => {
|
|
29
|
+
const { result } = renderHook(() => useToast());
|
|
30
|
+
|
|
31
|
+
let dismissFn: (() => void) | undefined;
|
|
32
|
+
|
|
33
|
+
act(() => {
|
|
34
|
+
const response = result.current.toast({
|
|
35
|
+
title: 'Test Toast',
|
|
36
|
+
});
|
|
37
|
+
dismissFn = response.dismiss;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(result.current.toasts).toHaveLength(1);
|
|
41
|
+
|
|
42
|
+
act(() => {
|
|
43
|
+
dismissFn?.();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(result.current.toasts[0].open).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should limit the number of toasts', () => {
|
|
50
|
+
const { result } = renderHook(() => useToast());
|
|
51
|
+
|
|
52
|
+
act(() => {
|
|
53
|
+
for (let i = 0; i < 10; i++) {
|
|
54
|
+
result.current.toast({
|
|
55
|
+
title: `Toast ${i}`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(result.current.toasts.length).toBeLessThanOrEqual(5);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
|
|
2
|
+
import { renderHook } from '@testing-library/react';
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
import { useZodForm } from '../useZodForm';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
const testSchema = z.object({
|
|
8
|
+
name: z.string().min(1, 'Name is required'),
|
|
9
|
+
email: z.string().email('Invalid email'),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('useZodForm', () => {
|
|
13
|
+
it('should initialize with schema validation', () => {
|
|
14
|
+
const { result } = renderHook(() =>
|
|
15
|
+
useZodForm({
|
|
16
|
+
schema: testSchema,
|
|
17
|
+
defaultValues: { name: '', email: '' },
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(result.current.formState).toBeDefined();
|
|
22
|
+
expect(result.current.register).toBeDefined();
|
|
23
|
+
expect(result.current.handleSubmit).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should validate form data', async () => {
|
|
27
|
+
const { result } = renderHook(() =>
|
|
28
|
+
useZodForm({
|
|
29
|
+
schema: testSchema,
|
|
30
|
+
defaultValues: { name: '', email: '' },
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const isValid = await result.current.trigger();
|
|
35
|
+
expect(isValid).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
});
|