@jmruthers/pace-core 0.5.3 → 0.5.5
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-3SSI644S.js} +2 -2
- package/dist/{chunk-M4RW7PIP.js → chunk-2BJFM2JC.js} +105 -81
- package/dist/chunk-2BJFM2JC.js.map +1 -0
- package/dist/{chunk-5H3C2SWM.js → chunk-RTCA5ZNK.js} +2 -2
- package/dist/components.js +2 -2
- package/dist/index.js +2 -2
- package/dist/styles/core.css +3 -0
- 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 +167 -138
- package/src/components/Header/Header.test.tsx +1 -1
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +1 -1
- 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/api.test.ts +511 -0
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -0
- package/src/rbac/hooks/useCan.test.ts +1 -1
- package/src/rbac/hooks/usePermissions.test.ts +10 -5
- package/src/rbac/hooks/useRBAC.test.ts +141 -93
- package/src/rbac/utils/__tests__/eventContext.test.ts +428 -0
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +428 -0
- package/src/styles/core.css +3 -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-3SSI644S.js.map} +0 -0
- /package/dist/{chunk-5H3C2SWM.js.map → chunk-RTCA5ZNK.js.map} +0 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Event Context Utilities Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module RBAC/EventContext/Tests
|
|
5
|
+
* @since 1.0.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive tests for event context utilities in the RBAC system.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
12
|
+
import { Database } from '../../../types/database';
|
|
13
|
+
import { UUID, Scope } from '../../types';
|
|
14
|
+
import {
|
|
15
|
+
getOrganisationFromEvent,
|
|
16
|
+
createScopeFromEvent,
|
|
17
|
+
isEventBasedScope,
|
|
18
|
+
isValidEventBasedScope
|
|
19
|
+
} from '../eventContext';
|
|
20
|
+
|
|
21
|
+
// Mock Supabase client
|
|
22
|
+
const createMockSupabaseClient = () => {
|
|
23
|
+
const mockQuery = {
|
|
24
|
+
select: vi.fn().mockReturnThis(),
|
|
25
|
+
eq: vi.fn().mockReturnThis(),
|
|
26
|
+
single: vi.fn()
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
from: vi.fn().mockReturnValue(mockQuery),
|
|
31
|
+
query: mockQuery
|
|
32
|
+
} as unknown as SupabaseClient<Database>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('Event Context Utilities', () => {
|
|
36
|
+
let mockSupabase: SupabaseClient<Database>;
|
|
37
|
+
let mockQuery: any;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
mockSupabase = createMockSupabaseClient();
|
|
41
|
+
mockQuery = mockSupabase.from('event');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('getOrganisationFromEvent', () => {
|
|
49
|
+
it('should return organisation ID when event exists', async () => {
|
|
50
|
+
const eventId = 'event-123';
|
|
51
|
+
const organisationId = 'org-456';
|
|
52
|
+
|
|
53
|
+
mockQuery.single.mockResolvedValue({
|
|
54
|
+
data: { organisation_id: organisationId },
|
|
55
|
+
error: null
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const result = await getOrganisationFromEvent(mockSupabase, eventId);
|
|
59
|
+
|
|
60
|
+
expect(result).toBe(organisationId);
|
|
61
|
+
expect(mockSupabase.from).toHaveBeenCalledWith('event');
|
|
62
|
+
expect(mockQuery.select).toHaveBeenCalledWith('organisation_id');
|
|
63
|
+
expect(mockQuery.eq).toHaveBeenCalledWith('event_id', eventId);
|
|
64
|
+
expect(mockQuery.single).toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should return null when event does not exist', async () => {
|
|
68
|
+
const eventId = 'nonexistent-event';
|
|
69
|
+
|
|
70
|
+
mockQuery.single.mockResolvedValue({
|
|
71
|
+
data: null,
|
|
72
|
+
error: { message: 'Event not found' }
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = await getOrganisationFromEvent(mockSupabase, eventId);
|
|
76
|
+
|
|
77
|
+
expect(result).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return null when data is null', async () => {
|
|
81
|
+
const eventId = 'event-123';
|
|
82
|
+
|
|
83
|
+
mockQuery.single.mockResolvedValue({
|
|
84
|
+
data: null,
|
|
85
|
+
error: null
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const result = await getOrganisationFromEvent(mockSupabase, eventId);
|
|
89
|
+
|
|
90
|
+
expect(result).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle database errors gracefully', async () => {
|
|
94
|
+
const eventId = 'event-123';
|
|
95
|
+
const dbError = new Error('Database connection failed');
|
|
96
|
+
|
|
97
|
+
mockQuery.single.mockRejectedValue(dbError);
|
|
98
|
+
|
|
99
|
+
await expect(getOrganisationFromEvent(mockSupabase, eventId))
|
|
100
|
+
.rejects.toThrow('Database connection failed');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should handle empty organisation_id', async () => {
|
|
104
|
+
const eventId = 'event-123';
|
|
105
|
+
|
|
106
|
+
mockQuery.single.mockResolvedValue({
|
|
107
|
+
data: { organisation_id: null },
|
|
108
|
+
error: null
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const result = await getOrganisationFromEvent(mockSupabase, eventId);
|
|
112
|
+
|
|
113
|
+
expect(result).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('createScopeFromEvent', () => {
|
|
118
|
+
it('should create complete scope when event exists', async () => {
|
|
119
|
+
const eventId = 'event-123';
|
|
120
|
+
const organisationId = 'org-456';
|
|
121
|
+
const appId = 'app-789';
|
|
122
|
+
|
|
123
|
+
mockQuery.single.mockResolvedValue({
|
|
124
|
+
data: { organisation_id: organisationId },
|
|
125
|
+
error: null
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const result = await createScopeFromEvent(mockSupabase, eventId, appId);
|
|
129
|
+
|
|
130
|
+
expect(result).toEqual({
|
|
131
|
+
organisationId,
|
|
132
|
+
eventId,
|
|
133
|
+
appId
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should create scope without appId when not provided', async () => {
|
|
138
|
+
const eventId = 'event-123';
|
|
139
|
+
const organisationId = 'org-456';
|
|
140
|
+
|
|
141
|
+
mockQuery.single.mockResolvedValue({
|
|
142
|
+
data: { organisation_id: organisationId },
|
|
143
|
+
error: null
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const result = await createScopeFromEvent(mockSupabase, eventId);
|
|
147
|
+
|
|
148
|
+
expect(result).toEqual({
|
|
149
|
+
organisationId,
|
|
150
|
+
eventId,
|
|
151
|
+
appId: undefined
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should return null when event does not exist', async () => {
|
|
156
|
+
const eventId = 'nonexistent-event';
|
|
157
|
+
|
|
158
|
+
mockQuery.single.mockResolvedValue({
|
|
159
|
+
data: null,
|
|
160
|
+
error: { message: 'Event not found' }
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const result = await createScopeFromEvent(mockSupabase, eventId);
|
|
164
|
+
|
|
165
|
+
expect(result).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should return null when organisation lookup fails', async () => {
|
|
169
|
+
const eventId = 'event-123';
|
|
170
|
+
|
|
171
|
+
mockQuery.single.mockResolvedValue({
|
|
172
|
+
data: { organisation_id: null },
|
|
173
|
+
error: null
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const result = await createScopeFromEvent(mockSupabase, eventId);
|
|
177
|
+
|
|
178
|
+
expect(result).toBeNull();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should handle database errors gracefully', async () => {
|
|
182
|
+
const eventId = 'event-123';
|
|
183
|
+
const dbError = new Error('Database connection failed');
|
|
184
|
+
|
|
185
|
+
mockQuery.single.mockRejectedValue(dbError);
|
|
186
|
+
|
|
187
|
+
await expect(createScopeFromEvent(mockSupabase, eventId))
|
|
188
|
+
.rejects.toThrow('Database connection failed');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('isEventBasedScope', () => {
|
|
193
|
+
it('should return true for event-based scope (no organisationId, has eventId)', () => {
|
|
194
|
+
const scope: Scope = {
|
|
195
|
+
eventId: 'event-123',
|
|
196
|
+
appId: 'app-456'
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
expect(isEventBasedScope(scope)).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should return false when organisationId is present', () => {
|
|
203
|
+
const scope: Scope = {
|
|
204
|
+
organisationId: 'org-123',
|
|
205
|
+
eventId: 'event-123',
|
|
206
|
+
appId: 'app-456'
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
expect(isEventBasedScope(scope)).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should return false when eventId is missing', () => {
|
|
213
|
+
const scope: Scope = {
|
|
214
|
+
appId: 'app-456'
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
expect(isEventBasedScope(scope)).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should return false when both organisationId and eventId are missing', () => {
|
|
221
|
+
const scope: Scope = {
|
|
222
|
+
appId: 'app-456'
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
expect(isEventBasedScope(scope)).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should return false when eventId is null', () => {
|
|
229
|
+
const scope: Scope = {
|
|
230
|
+
eventId: null,
|
|
231
|
+
appId: 'app-456'
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
expect(isEventBasedScope(scope)).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should return false when eventId is undefined', () => {
|
|
238
|
+
const scope: Scope = {
|
|
239
|
+
eventId: undefined,
|
|
240
|
+
appId: 'app-456'
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
expect(isEventBasedScope(scope)).toBe(false);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('isValidEventBasedScope', () => {
|
|
248
|
+
it('should return true for valid event-based scope', () => {
|
|
249
|
+
const scope: Scope = {
|
|
250
|
+
eventId: 'event-123',
|
|
251
|
+
appId: 'app-456'
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
expect(isValidEventBasedScope(scope)).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should return false when eventId is missing', () => {
|
|
258
|
+
const scope: Scope = {
|
|
259
|
+
appId: 'app-456'
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
expect(isValidEventBasedScope(scope)).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should return false when eventId is null', () => {
|
|
266
|
+
const scope: Scope = {
|
|
267
|
+
eventId: null,
|
|
268
|
+
appId: 'app-456'
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
expect(isValidEventBasedScope(scope)).toBe(false);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should return false when eventId is undefined', () => {
|
|
275
|
+
const scope: Scope = {
|
|
276
|
+
eventId: undefined,
|
|
277
|
+
appId: 'app-456'
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
expect(isValidEventBasedScope(scope)).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should return false when organisationId is present (not event-based)', () => {
|
|
284
|
+
const scope: Scope = {
|
|
285
|
+
organisationId: 'org-123',
|
|
286
|
+
eventId: 'event-123',
|
|
287
|
+
appId: 'app-456'
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
expect(isValidEventBasedScope(scope)).toBe(false);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should return true for event-based scope without appId', () => {
|
|
294
|
+
const scope: Scope = {
|
|
295
|
+
eventId: 'event-123'
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
expect(isValidEventBasedScope(scope)).toBe(true);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('Edge Cases and Error Handling', () => {
|
|
303
|
+
it('should handle malformed event IDs', async () => {
|
|
304
|
+
const malformedEventId = '';
|
|
305
|
+
|
|
306
|
+
mockQuery.single.mockResolvedValue({
|
|
307
|
+
data: null,
|
|
308
|
+
error: { message: 'Invalid event ID' }
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const result = await getOrganisationFromEvent(mockSupabase, malformedEventId);
|
|
312
|
+
|
|
313
|
+
expect(result).toBeNull();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should handle very long event IDs', async () => {
|
|
317
|
+
const longEventId = 'a'.repeat(1000);
|
|
318
|
+
const organisationId = 'org-456';
|
|
319
|
+
|
|
320
|
+
mockQuery.single.mockResolvedValue({
|
|
321
|
+
data: { organisation_id: organisationId },
|
|
322
|
+
error: null
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const result = await getOrganisationFromEvent(mockSupabase, longEventId);
|
|
326
|
+
|
|
327
|
+
expect(result).toBe(organisationId);
|
|
328
|
+
expect(mockQuery.eq).toHaveBeenCalledWith('event_id', longEventId);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should handle special characters in event IDs', async () => {
|
|
332
|
+
const specialEventId = 'event-123!@#$%^&*()';
|
|
333
|
+
const organisationId = 'org-456';
|
|
334
|
+
|
|
335
|
+
mockQuery.single.mockResolvedValue({
|
|
336
|
+
data: { organisation_id: organisationId },
|
|
337
|
+
error: null
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const result = await getOrganisationFromEvent(mockSupabase, specialEventId);
|
|
341
|
+
|
|
342
|
+
expect(result).toBe(organisationId);
|
|
343
|
+
expect(mockQuery.eq).toHaveBeenCalledWith('event_id', specialEventId);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should handle concurrent calls to getOrganisationFromEvent', async () => {
|
|
347
|
+
const eventId1 = 'event-123';
|
|
348
|
+
const eventId2 = 'event-456';
|
|
349
|
+
const organisationId1 = 'org-123';
|
|
350
|
+
const organisationId2 = 'org-456';
|
|
351
|
+
|
|
352
|
+
mockQuery.single
|
|
353
|
+
.mockResolvedValueOnce({
|
|
354
|
+
data: { organisation_id: organisationId1 },
|
|
355
|
+
error: null
|
|
356
|
+
})
|
|
357
|
+
.mockResolvedValueOnce({
|
|
358
|
+
data: { organisation_id: organisationId2 },
|
|
359
|
+
error: null
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const [result1, result2] = await Promise.all([
|
|
363
|
+
getOrganisationFromEvent(mockSupabase, eventId1),
|
|
364
|
+
getOrganisationFromEvent(mockSupabase, eventId2)
|
|
365
|
+
]);
|
|
366
|
+
|
|
367
|
+
expect(result1).toBe(organisationId1);
|
|
368
|
+
expect(result2).toBe(organisationId2);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should handle concurrent calls to createScopeFromEvent', async () => {
|
|
372
|
+
const eventId1 = 'event-123';
|
|
373
|
+
const eventId2 = 'event-456';
|
|
374
|
+
const organisationId1 = 'org-123';
|
|
375
|
+
const organisationId2 = 'org-456';
|
|
376
|
+
const appId1 = 'app-123';
|
|
377
|
+
const appId2 = 'app-456';
|
|
378
|
+
|
|
379
|
+
mockQuery.single
|
|
380
|
+
.mockResolvedValueOnce({
|
|
381
|
+
data: { organisation_id: organisationId1 },
|
|
382
|
+
error: null
|
|
383
|
+
})
|
|
384
|
+
.mockResolvedValueOnce({
|
|
385
|
+
data: { organisation_id: organisationId2 },
|
|
386
|
+
error: null
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const [result1, result2] = await Promise.all([
|
|
390
|
+
createScopeFromEvent(mockSupabase, eventId1, appId1),
|
|
391
|
+
createScopeFromEvent(mockSupabase, eventId2, appId2)
|
|
392
|
+
]);
|
|
393
|
+
|
|
394
|
+
expect(result1).toEqual({
|
|
395
|
+
organisationId: organisationId1,
|
|
396
|
+
eventId: eventId1,
|
|
397
|
+
appId: appId1
|
|
398
|
+
});
|
|
399
|
+
expect(result2).toEqual({
|
|
400
|
+
organisationId: organisationId2,
|
|
401
|
+
eventId: eventId2,
|
|
402
|
+
appId: appId2
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe('Type Safety', () => {
|
|
408
|
+
it('should handle UUID types correctly', () => {
|
|
409
|
+
const validUUID = '123e4567-e89b-12d3-a456-426614174000';
|
|
410
|
+
const scope: Scope = {
|
|
411
|
+
eventId: validUUID,
|
|
412
|
+
appId: validUUID
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
expect(isValidEventBasedScope(scope)).toBe(true);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should handle string types correctly', () => {
|
|
419
|
+
const stringId = 'event-123';
|
|
420
|
+
const scope: Scope = {
|
|
421
|
+
eventId: stringId,
|
|
422
|
+
appId: 'app-456'
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
expect(isValidEventBasedScope(scope)).toBe(true);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
});
|
package/src/styles/core.css
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { setAppConfig, getAppConfig, getCurrentAppName, getCurrentAppId } from '../appConfig';
|
|
3
|
+
|
|
4
|
+
describe('App Configuration Utilities', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// Clear any existing config by resetting the module
|
|
7
|
+
vi.resetModules();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should set app configuration', () => {
|
|
11
|
+
const config = {
|
|
12
|
+
appName: 'Test App',
|
|
13
|
+
appId: 'test-app'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
setAppConfig(config);
|
|
17
|
+
|
|
18
|
+
// Verify the config was set by getting it back
|
|
19
|
+
const result = getAppConfig();
|
|
20
|
+
expect(result).toEqual(config);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should get app configuration', () => {
|
|
24
|
+
const config = {
|
|
25
|
+
appName: 'Test App',
|
|
26
|
+
appId: 'test-app'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
setAppConfig(config);
|
|
30
|
+
const result = getAppConfig();
|
|
31
|
+
expect(result).toEqual(config);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should get current app name', () => {
|
|
35
|
+
const config = {
|
|
36
|
+
appName: 'Test App',
|
|
37
|
+
appId: 'test-app'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
setAppConfig(config);
|
|
41
|
+
const result = getCurrentAppName();
|
|
42
|
+
expect(result).toBe('Test App');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should get current app id', () => {
|
|
46
|
+
const config = {
|
|
47
|
+
appName: 'Test App',
|
|
48
|
+
appId: 'test-app'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
setAppConfig(config);
|
|
52
|
+
const result = getCurrentAppId();
|
|
53
|
+
expect(result).toBe('test-app');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
auditLogger,
|
|
4
|
+
logAuthEvent,
|
|
5
|
+
logPermissionEvent,
|
|
6
|
+
logSecurityEvent
|
|
7
|
+
} from '../audit';
|
|
8
|
+
|
|
9
|
+
describe('Audit Logger Utility', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
auditLogger.clearEvents();
|
|
12
|
+
vi.restoreAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('logs a generic event and stores it', () => {
|
|
16
|
+
auditLogger.log({ type: 'auth', action: 'login', user: 'alice' });
|
|
17
|
+
const events = auditLogger.getEvents();
|
|
18
|
+
expect(events.length).toBe(1);
|
|
19
|
+
expect(events[0].type).toBe('auth');
|
|
20
|
+
expect(events[0].action).toBe('login');
|
|
21
|
+
expect(events[0].user).toBe('alice');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('logs an auth event via logAuthEvent', () => {
|
|
25
|
+
logAuthEvent('login', 'bob', { ip: '127.0.0.1' });
|
|
26
|
+
const events = auditLogger.getEvents();
|
|
27
|
+
expect(events.length).toBe(1);
|
|
28
|
+
expect(events[0].type).toBe('auth');
|
|
29
|
+
expect(events[0].action).toBe('login');
|
|
30
|
+
expect(events[0].user).toBe('bob');
|
|
31
|
+
expect(events[0].details).toEqual({ ip: '127.0.0.1' });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('logs a permission event via logPermissionEvent', () => {
|
|
35
|
+
logPermissionEvent('grant', 'carol', { resource: 'file' });
|
|
36
|
+
const events = auditLogger.getEvents();
|
|
37
|
+
expect(events.length).toBe(1);
|
|
38
|
+
expect(events[0].type).toBe('permission');
|
|
39
|
+
expect(events[0].action).toBe('grant');
|
|
40
|
+
expect(events[0].user).toBe('carol');
|
|
41
|
+
expect(events[0].details).toEqual({ resource: 'file' });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('logs a security event via logSecurityEvent', () => {
|
|
45
|
+
logSecurityEvent('breach', 'dave', { severity: 'high' });
|
|
46
|
+
const events = auditLogger.getEvents();
|
|
47
|
+
expect(events.length).toBe(1);
|
|
48
|
+
expect(events[0].type).toBe('security');
|
|
49
|
+
expect(events[0].action).toBe('breach');
|
|
50
|
+
expect(events[0].user).toBe('dave');
|
|
51
|
+
expect(events[0].details).toEqual({ severity: 'high' });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('can retrieve only security events', async () => {
|
|
55
|
+
auditLogger.log({ type: 'auth', action: 'login' });
|
|
56
|
+
auditLogger.log({ type: 'security', action: 'breach' });
|
|
57
|
+
auditLogger.log({ type: 'security', action: 'scan' });
|
|
58
|
+
const securityEvents = await auditLogger.getSecurityEvents();
|
|
59
|
+
expect(securityEvents.length).toBe(2);
|
|
60
|
+
expect(securityEvents.every(e => e.type === 'security')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('clears events', () => {
|
|
64
|
+
auditLogger.log({ type: 'auth', action: 'login' });
|
|
65
|
+
expect(auditLogger.getEvents().length).toBe(1);
|
|
66
|
+
auditLogger.clearEvents();
|
|
67
|
+
expect(auditLogger.getEvents().length).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
signInWithEmail,
|
|
4
|
+
signUpWithEmail,
|
|
5
|
+
resetPassword,
|
|
6
|
+
updatePassword,
|
|
7
|
+
auditLogger
|
|
8
|
+
} from '../auth-utils';
|
|
9
|
+
|
|
10
|
+
describe('Auth Utils', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.restoreAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('signInWithEmail logs and returns error', async () => {
|
|
16
|
+
const result = await signInWithEmail({ email: 'test@example.com', password: 'pw' });
|
|
17
|
+
expect(result).toHaveProperty('error');
|
|
18
|
+
expect(result.error).toBeInstanceOf(Error);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('signUpWithEmail logs and returns error', async () => {
|
|
22
|
+
const result = await signUpWithEmail({ email: 'new@example.com', password: 'pw' });
|
|
23
|
+
expect(result).toHaveProperty('error');
|
|
24
|
+
expect(result.error).toBeInstanceOf(Error);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('resetPassword logs and returns error', async () => {
|
|
28
|
+
const result = await resetPassword('reset@example.com');
|
|
29
|
+
expect(result).toHaveProperty('error');
|
|
30
|
+
expect(result.error).toBeInstanceOf(Error);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('updatePassword calls supabaseClient.auth.updateUser and returns error if thrown', async () => {
|
|
34
|
+
const updateUser = vi.fn().mockResolvedValue({ error: undefined });
|
|
35
|
+
const supabaseClient = { auth: { updateUser } } as any;
|
|
36
|
+
const result = await updatePassword(supabaseClient, 'newpw');
|
|
37
|
+
expect(updateUser).toHaveBeenCalledWith({ password: 'newpw' });
|
|
38
|
+
expect(result).toEqual({ error: undefined });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('updatePassword returns error if thrown', async () => {
|
|
42
|
+
const updateUser = vi.fn().mockRejectedValue(new Error('fail'));
|
|
43
|
+
const supabaseClient = { auth: { updateUser } } as any;
|
|
44
|
+
const result = await updatePassword(supabaseClient, 'pw');
|
|
45
|
+
expect(result.error).toBeInstanceOf(Error);
|
|
46
|
+
if (result.error) {
|
|
47
|
+
expect(result.error.message).toBe('fail');
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('auditLogger.log logs event', () => {
|
|
52
|
+
// Test that the function exists and can be called without error
|
|
53
|
+
expect(() => auditLogger.log('event', { foo: 'bar' })).not.toThrow();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('auditLogger.logAuthEvent logs auth event', () => {
|
|
57
|
+
// Test that the function exists and can be called without error
|
|
58
|
+
expect(() => auditLogger.logAuthEvent('user1', 'LOGIN', { ip: '1.2.3.4' })).not.toThrow();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('auditLogger.logPermissionEvent logs permission check', () => {
|
|
62
|
+
// Test that the function exists and can be called without error
|
|
63
|
+
expect(() => auditLogger.logPermissionEvent('user2', 'admin', true)).not.toThrow();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('auditLogger.logEvent logs generic event', () => {
|
|
67
|
+
// Test that the function exists and can be called without error
|
|
68
|
+
expect(() => auditLogger.logEvent({ action: 'SOMETHING', details: { foo: 1 } })).not.toThrow();
|
|
69
|
+
});
|
|
70
|
+
});
|