@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,192 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
import {
|
|
4
|
+
setOrganisationContext,
|
|
5
|
+
clearOrganisationContext,
|
|
6
|
+
getOrganisationContext,
|
|
7
|
+
isOrganisationContextAvailable
|
|
8
|
+
} from '../organisationContext';
|
|
9
|
+
|
|
10
|
+
describe('organisationContext', () => {
|
|
11
|
+
let mockSupabase: SupabaseClient;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
// Reset all mocks
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
|
|
17
|
+
// Create a mock Supabase client
|
|
18
|
+
mockSupabase = {
|
|
19
|
+
rpc: vi.fn(),
|
|
20
|
+
auth: {
|
|
21
|
+
getUser: vi.fn()
|
|
22
|
+
}
|
|
23
|
+
} as unknown as SupabaseClient;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('setOrganisationContext', () => {
|
|
31
|
+
it('should set organisation context successfully', async () => {
|
|
32
|
+
const organisationId = 'org-123';
|
|
33
|
+
const mockRpc = vi.fn().mockResolvedValue({ error: null });
|
|
34
|
+
mockSupabase.rpc = mockRpc;
|
|
35
|
+
|
|
36
|
+
await setOrganisationContext(mockSupabase, organisationId);
|
|
37
|
+
|
|
38
|
+
expect(mockRpc).toHaveBeenCalledWith('set_organisation_context', {
|
|
39
|
+
org_id: organisationId
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should handle missing supabase client gracefully', async () => {
|
|
44
|
+
await setOrganisationContext(null as any, 'org-123');
|
|
45
|
+
// Function should complete without throwing
|
|
46
|
+
expect(true).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should handle missing organisation ID gracefully', async () => {
|
|
50
|
+
await setOrganisationContext(mockSupabase, '');
|
|
51
|
+
// Function should complete without throwing
|
|
52
|
+
expect(true).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should handle database function not available', async () => {
|
|
56
|
+
const mockRpc = vi.fn().mockResolvedValue({
|
|
57
|
+
error: { message: 'Function not found' }
|
|
58
|
+
});
|
|
59
|
+
mockSupabase.rpc = mockRpc;
|
|
60
|
+
|
|
61
|
+
await setOrganisationContext(mockSupabase, 'org-123');
|
|
62
|
+
// Function should complete without throwing
|
|
63
|
+
expect(true).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should handle unexpected errors gracefully', async () => {
|
|
67
|
+
const mockRpc = vi.fn().mockRejectedValue(new Error('Network error'));
|
|
68
|
+
mockSupabase.rpc = mockRpc;
|
|
69
|
+
|
|
70
|
+
await setOrganisationContext(mockSupabase, 'org-123');
|
|
71
|
+
// Function should complete without throwing
|
|
72
|
+
expect(true).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('clearOrganisationContext', () => {
|
|
77
|
+
it('should clear organisation context successfully', async () => {
|
|
78
|
+
const mockRpc = vi.fn().mockResolvedValue({ error: null });
|
|
79
|
+
mockSupabase.rpc = mockRpc;
|
|
80
|
+
|
|
81
|
+
await clearOrganisationContext(mockSupabase);
|
|
82
|
+
|
|
83
|
+
expect(mockRpc).toHaveBeenCalledWith('clear_organisation_context');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should handle missing supabase client gracefully', async () => {
|
|
87
|
+
await clearOrganisationContext(null as any);
|
|
88
|
+
// Function should complete without throwing
|
|
89
|
+
expect(true).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle database function not available', async () => {
|
|
93
|
+
const mockRpc = vi.fn().mockResolvedValue({
|
|
94
|
+
error: { message: 'Function not found' }
|
|
95
|
+
});
|
|
96
|
+
mockSupabase.rpc = mockRpc;
|
|
97
|
+
|
|
98
|
+
await clearOrganisationContext(mockSupabase);
|
|
99
|
+
// Function should complete without throwing
|
|
100
|
+
expect(true).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should handle unexpected errors gracefully', async () => {
|
|
104
|
+
const mockRpc = vi.fn().mockRejectedValue(new Error('Network error'));
|
|
105
|
+
mockSupabase.rpc = mockRpc;
|
|
106
|
+
|
|
107
|
+
await clearOrganisationContext(mockSupabase);
|
|
108
|
+
// Function should complete without throwing
|
|
109
|
+
expect(true).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('getOrganisationContext', () => {
|
|
114
|
+
it('should get organisation context successfully', async () => {
|
|
115
|
+
const mockRpc = vi.fn().mockResolvedValue({
|
|
116
|
+
data: 'org-123',
|
|
117
|
+
error: null
|
|
118
|
+
});
|
|
119
|
+
mockSupabase.rpc = mockRpc;
|
|
120
|
+
|
|
121
|
+
const result = await getOrganisationContext(mockSupabase);
|
|
122
|
+
|
|
123
|
+
expect(mockRpc).toHaveBeenCalledWith('get_organisation_context');
|
|
124
|
+
expect(result).toBe('org-123');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle missing supabase client gracefully', async () => {
|
|
128
|
+
const result = await getOrganisationContext(null as any);
|
|
129
|
+
|
|
130
|
+
expect(result).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should handle database function not available', async () => {
|
|
134
|
+
const mockRpc = vi.fn().mockResolvedValue({
|
|
135
|
+
data: null,
|
|
136
|
+
error: { message: 'Function not found' }
|
|
137
|
+
});
|
|
138
|
+
mockSupabase.rpc = mockRpc;
|
|
139
|
+
|
|
140
|
+
const result = await getOrganisationContext(mockSupabase);
|
|
141
|
+
|
|
142
|
+
expect(result).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should handle unexpected errors gracefully', async () => {
|
|
146
|
+
const mockRpc = vi.fn().mockRejectedValue(new Error('Network error'));
|
|
147
|
+
mockSupabase.rpc = mockRpc;
|
|
148
|
+
|
|
149
|
+
const result = await getOrganisationContext(mockSupabase);
|
|
150
|
+
|
|
151
|
+
expect(result).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('isOrganisationContextAvailable', () => {
|
|
156
|
+
it('should return true when functions are available', async () => {
|
|
157
|
+
const mockRpc = vi.fn().mockResolvedValue({ error: null });
|
|
158
|
+
mockSupabase.rpc = mockRpc;
|
|
159
|
+
|
|
160
|
+
const result = await isOrganisationContextAvailable(mockSupabase);
|
|
161
|
+
|
|
162
|
+
expect(mockRpc).toHaveBeenCalledWith('get_organisation_context');
|
|
163
|
+
expect(result).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should return false when supabase client is missing', async () => {
|
|
167
|
+
const result = await isOrganisationContextAvailable(null as any);
|
|
168
|
+
|
|
169
|
+
expect(result).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should return false when database function has error', async () => {
|
|
173
|
+
const mockRpc = vi.fn().mockResolvedValue({
|
|
174
|
+
error: { message: 'Function not found' }
|
|
175
|
+
});
|
|
176
|
+
mockSupabase.rpc = mockRpc;
|
|
177
|
+
|
|
178
|
+
const result = await isOrganisationContextAvailable(mockSupabase);
|
|
179
|
+
|
|
180
|
+
expect(result).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should return false when unexpected error occurs', async () => {
|
|
184
|
+
const mockRpc = vi.fn().mockRejectedValue(new Error('Network error'));
|
|
185
|
+
mockSupabase.rpc = mockRpc;
|
|
186
|
+
|
|
187
|
+
const result = await isOrganisationContextAvailable(mockSupabase);
|
|
188
|
+
|
|
189
|
+
expect(result).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { performanceBudgetMonitor, PERFORMANCE_BUDGETS } from '../performanceBudgets';
|
|
3
|
+
|
|
4
|
+
describe('performanceBudgets', () => {
|
|
5
|
+
let consoleSpy: {
|
|
6
|
+
log: ReturnType<typeof vi.spyOn>;
|
|
7
|
+
warn: ReturnType<typeof vi.spyOn>;
|
|
8
|
+
error: ReturnType<typeof vi.spyOn>;
|
|
9
|
+
info: ReturnType<typeof vi.spyOn>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
|
|
15
|
+
// Create console spies
|
|
16
|
+
consoleSpy = {
|
|
17
|
+
log: vi.spyOn(console, 'log').mockImplementation(() => {}),
|
|
18
|
+
warn: vi.spyOn(console, 'warn').mockImplementation(() => {}),
|
|
19
|
+
error: vi.spyOn(console, 'error').mockImplementation(() => {}),
|
|
20
|
+
info: vi.spyOn(console, 'info').mockImplementation(() => {})
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Reset the monitor before each test
|
|
24
|
+
performanceBudgetMonitor.reset();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
vi.restoreAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('PERFORMANCE_BUDGETS', () => {
|
|
32
|
+
it('should have all required performance budget thresholds', () => {
|
|
33
|
+
expect(PERFORMANCE_BUDGETS).toHaveProperty('COMPONENT_RENDER');
|
|
34
|
+
expect(PERFORMANCE_BUDGETS).toHaveProperty('BUNDLE_SIZE');
|
|
35
|
+
expect(PERFORMANCE_BUDGETS).toHaveProperty('CHUNK_COUNT');
|
|
36
|
+
expect(PERFORMANCE_BUDGETS).toHaveProperty('TREESHAKING_SCORE');
|
|
37
|
+
expect(PERFORMANCE_BUDGETS).toHaveProperty('ERROR_BOUNDARY_TRIGGER');
|
|
38
|
+
expect(PERFORMANCE_BUDGETS).toHaveProperty('MEMORY_INCREASE');
|
|
39
|
+
expect(PERFORMANCE_BUDGETS).toHaveProperty('LARGE_LIST_RENDER');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should have appropriate threshold values', () => {
|
|
43
|
+
expect(PERFORMANCE_BUDGETS.COMPONENT_RENDER.threshold).toBe(50);
|
|
44
|
+
expect(PERFORMANCE_BUDGETS.BUNDLE_SIZE.threshold).toBe(150000);
|
|
45
|
+
expect(PERFORMANCE_BUDGETS.CHUNK_COUNT.threshold).toBe(10);
|
|
46
|
+
expect(PERFORMANCE_BUDGETS.TREESHAKING_SCORE.threshold).toBe(70);
|
|
47
|
+
expect(PERFORMANCE_BUDGETS.ERROR_BOUNDARY_TRIGGER.threshold).toBe(5);
|
|
48
|
+
expect(PERFORMANCE_BUDGETS.MEMORY_INCREASE.threshold).toBe(1000);
|
|
49
|
+
expect(PERFORMANCE_BUDGETS.LARGE_LIST_RENDER.threshold).toBe(500);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('performanceBudgetMonitor.measure', () => {
|
|
54
|
+
it('should measure performance metrics correctly', () => {
|
|
55
|
+
const result = performanceBudgetMonitor.measure('COMPONENT_RENDER', 30);
|
|
56
|
+
|
|
57
|
+
expect(result.passed).toBe(true);
|
|
58
|
+
expect(result.value).toBe(30);
|
|
59
|
+
expect(result.threshold).toBe(50);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should fail when metric exceeds threshold', () => {
|
|
63
|
+
const result = performanceBudgetMonitor.measure('COMPONENT_RENDER', 60);
|
|
64
|
+
|
|
65
|
+
expect(result.passed).toBe(false);
|
|
66
|
+
expect(result.value).toBe(60);
|
|
67
|
+
expect(result.threshold).toBe(50);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should handle unknown metrics with default threshold', () => {
|
|
71
|
+
const result = performanceBudgetMonitor.measure('UNKNOWN_METRIC', 80);
|
|
72
|
+
|
|
73
|
+
expect(result.passed).toBe(true);
|
|
74
|
+
expect(result.value).toBe(80);
|
|
75
|
+
expect(result.threshold).toBe(100); // Default threshold
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should log metrics in development mode', () => {
|
|
79
|
+
const originalEnv = process.env.NODE_ENV;
|
|
80
|
+
process.env.NODE_ENV = 'development';
|
|
81
|
+
|
|
82
|
+
performanceBudgetMonitor.measure('TEST_METRIC', 25, { component: 'Button' });
|
|
83
|
+
|
|
84
|
+
expect(consoleSpy.log).toHaveBeenCalledWith(
|
|
85
|
+
'📊 Performance Metric: TEST_METRIC = 25',
|
|
86
|
+
{ component: 'Button' }
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
process.env.NODE_ENV = originalEnv;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should not log metrics in production mode', () => {
|
|
93
|
+
const originalEnv = process.env.NODE_ENV;
|
|
94
|
+
process.env.NODE_ENV = 'production';
|
|
95
|
+
|
|
96
|
+
performanceBudgetMonitor.measure('TEST_METRIC', 25);
|
|
97
|
+
|
|
98
|
+
expect(consoleSpy.log).not.toHaveBeenCalled();
|
|
99
|
+
|
|
100
|
+
process.env.NODE_ENV = originalEnv;
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('performanceBudgetMonitor.setBudget', () => {
|
|
105
|
+
it('should set budget with default threshold', () => {
|
|
106
|
+
performanceBudgetMonitor.setBudget('CUSTOM_METRIC', 100);
|
|
107
|
+
performanceBudgetMonitor.measure('CUSTOM_METRIC', 150); // Exceed the budget
|
|
108
|
+
|
|
109
|
+
const violations = performanceBudgetMonitor.checkBudgets();
|
|
110
|
+
expect(violations).toHaveLength(1);
|
|
111
|
+
expect(violations[0].metric).toBe('CUSTOM_METRIC');
|
|
112
|
+
expect(violations[0].budget).toBe(100);
|
|
113
|
+
expect(violations[0].threshold).toBe('warning');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should set budget with custom threshold', () => {
|
|
117
|
+
performanceBudgetMonitor.setBudget('CRITICAL_METRIC', 50, 'error');
|
|
118
|
+
performanceBudgetMonitor.measure('CRITICAL_METRIC', 75); // Exceed the budget
|
|
119
|
+
|
|
120
|
+
const violations = performanceBudgetMonitor.checkBudgets();
|
|
121
|
+
expect(violations).toHaveLength(1);
|
|
122
|
+
expect(violations[0].threshold).toBe('error');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should handle multiple budgets', () => {
|
|
126
|
+
performanceBudgetMonitor.setBudget('METRIC_1', 100);
|
|
127
|
+
performanceBudgetMonitor.setBudget('METRIC_2', 200, 'info');
|
|
128
|
+
performanceBudgetMonitor.measure('METRIC_1', 150); // Exceed budget
|
|
129
|
+
performanceBudgetMonitor.measure('METRIC_2', 250); // Exceed budget
|
|
130
|
+
|
|
131
|
+
const violations = performanceBudgetMonitor.checkBudgets();
|
|
132
|
+
expect(violations).toHaveLength(2);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('performanceBudgetMonitor.checkBudgets', () => {
|
|
137
|
+
it('should return empty array when no violations', () => {
|
|
138
|
+
performanceBudgetMonitor.setBudget('TEST_METRIC', 100);
|
|
139
|
+
performanceBudgetMonitor.measure('TEST_METRIC', 50);
|
|
140
|
+
|
|
141
|
+
const violations = performanceBudgetMonitor.checkBudgets();
|
|
142
|
+
|
|
143
|
+
expect(violations).toHaveLength(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should detect budget violations', () => {
|
|
147
|
+
performanceBudgetMonitor.setBudget('TEST_METRIC', 100);
|
|
148
|
+
performanceBudgetMonitor.measure('TEST_METRIC', 150);
|
|
149
|
+
|
|
150
|
+
const violations = performanceBudgetMonitor.checkBudgets();
|
|
151
|
+
|
|
152
|
+
expect(violations).toHaveLength(1);
|
|
153
|
+
expect(violations[0].metric).toBe('TEST_METRIC');
|
|
154
|
+
expect(violations[0].actual).toBe(150);
|
|
155
|
+
expect(violations[0].budget).toBe(100);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should log error for error threshold violations', () => {
|
|
159
|
+
performanceBudgetMonitor.setBudget('CRITICAL_METRIC', 100, 'error');
|
|
160
|
+
performanceBudgetMonitor.measure('CRITICAL_METRIC', 150);
|
|
161
|
+
|
|
162
|
+
performanceBudgetMonitor.checkBudgets();
|
|
163
|
+
|
|
164
|
+
expect(consoleSpy.error).toHaveBeenCalledWith(
|
|
165
|
+
'❌ Performance budget exceeded: CRITICAL_METRIC (150 > 100)'
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should log warning for warning threshold violations', () => {
|
|
170
|
+
performanceBudgetMonitor.setBudget('WARNING_METRIC', 100, 'warning');
|
|
171
|
+
performanceBudgetMonitor.measure('WARNING_METRIC', 150);
|
|
172
|
+
|
|
173
|
+
performanceBudgetMonitor.checkBudgets();
|
|
174
|
+
|
|
175
|
+
expect(consoleSpy.warn).toHaveBeenCalledWith(
|
|
176
|
+
'⚠️ Performance budget exceeded: WARNING_METRIC (150 > 100)'
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should log info for info threshold violations', () => {
|
|
181
|
+
performanceBudgetMonitor.setBudget('INFO_METRIC', 100, 'info');
|
|
182
|
+
performanceBudgetMonitor.measure('INFO_METRIC', 150);
|
|
183
|
+
|
|
184
|
+
performanceBudgetMonitor.checkBudgets();
|
|
185
|
+
|
|
186
|
+
expect(consoleSpy.info).toHaveBeenCalledWith(
|
|
187
|
+
'ℹ️ Performance budget exceeded: INFO_METRIC (150 > 100)'
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should handle multiple violations', () => {
|
|
192
|
+
performanceBudgetMonitor.setBudget('METRIC_1', 100);
|
|
193
|
+
performanceBudgetMonitor.setBudget('METRIC_2', 200);
|
|
194
|
+
performanceBudgetMonitor.measure('METRIC_1', 150);
|
|
195
|
+
performanceBudgetMonitor.measure('METRIC_2', 250);
|
|
196
|
+
|
|
197
|
+
const violations = performanceBudgetMonitor.checkBudgets();
|
|
198
|
+
|
|
199
|
+
expect(violations).toHaveLength(2);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('performanceBudgetMonitor.getMetrics', () => {
|
|
204
|
+
it('should return performance metrics', () => {
|
|
205
|
+
performanceBudgetMonitor.measure('BUNDLE_SIZE', 100000);
|
|
206
|
+
performanceBudgetMonitor.measure('CHUNK_COUNT', 5);
|
|
207
|
+
performanceBudgetMonitor.measure('TREESHAKING_SCORE', 80);
|
|
208
|
+
performanceBudgetMonitor.measure('DYNAMIC_IMPORTS', 3);
|
|
209
|
+
|
|
210
|
+
const metrics = performanceBudgetMonitor.getMetrics();
|
|
211
|
+
|
|
212
|
+
expect(metrics).toEqual({
|
|
213
|
+
bundleSize: 100000,
|
|
214
|
+
chunkCount: 5,
|
|
215
|
+
treeshakingEffectiveness: 80,
|
|
216
|
+
dynamicImportUsage: 3
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should return zero for unmeasured metrics', () => {
|
|
221
|
+
const metrics = performanceBudgetMonitor.getMetrics();
|
|
222
|
+
|
|
223
|
+
expect(metrics).toEqual({
|
|
224
|
+
bundleSize: 0,
|
|
225
|
+
chunkCount: 0,
|
|
226
|
+
treeshakingEffectiveness: 0,
|
|
227
|
+
dynamicImportUsage: 0
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('performanceBudgetMonitor.reset', () => {
|
|
233
|
+
it('should clear all metrics and budgets', () => {
|
|
234
|
+
performanceBudgetMonitor.measure('TEST_METRIC', 100);
|
|
235
|
+
performanceBudgetMonitor.setBudget('TEST_METRIC', 50);
|
|
236
|
+
|
|
237
|
+
performanceBudgetMonitor.reset();
|
|
238
|
+
|
|
239
|
+
const metrics = performanceBudgetMonitor.getMetrics();
|
|
240
|
+
const violations = performanceBudgetMonitor.checkBudgets();
|
|
241
|
+
|
|
242
|
+
expect(metrics.bundleSize).toBe(0);
|
|
243
|
+
expect(violations).toHaveLength(0);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('Default performance budgets', () => {
|
|
248
|
+
it('should have default budgets set', () => {
|
|
249
|
+
const violations = performanceBudgetMonitor.checkBudgets();
|
|
250
|
+
|
|
251
|
+
// Should have 4 default budgets
|
|
252
|
+
expect(violations.length).toBeGreaterThanOrEqual(0);
|
|
253
|
+
|
|
254
|
+
// Check that default budgets are accessible
|
|
255
|
+
const metrics = performanceBudgetMonitor.getMetrics();
|
|
256
|
+
expect(metrics).toBeDefined();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { PermissionType, parsePermission } from '../permissionTypes';
|
|
3
|
+
|
|
4
|
+
describe('permissionTypes', () => {
|
|
5
|
+
describe('PermissionType enum', () => {
|
|
6
|
+
it('should have all required permission types', () => {
|
|
7
|
+
expect(PermissionType.READ).toBe('read');
|
|
8
|
+
expect(PermissionType.WRITE).toBe('write');
|
|
9
|
+
expect(PermissionType.DELETE).toBe('delete');
|
|
10
|
+
expect(PermissionType.ADMIN).toBe('admin');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should have correct string values', () => {
|
|
14
|
+
expect(typeof PermissionType.READ).toBe('string');
|
|
15
|
+
expect(typeof PermissionType.WRITE).toBe('string');
|
|
16
|
+
expect(typeof PermissionType.DELETE).toBe('string');
|
|
17
|
+
expect(typeof PermissionType.ADMIN).toBe('string');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should have unique values', () => {
|
|
21
|
+
const values = Object.values(PermissionType);
|
|
22
|
+
const uniqueValues = new Set(values);
|
|
23
|
+
expect(uniqueValues.size).toBe(values.length);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('parsePermission', () => {
|
|
28
|
+
it('should parse valid permission strings', () => {
|
|
29
|
+
const testCases = [
|
|
30
|
+
{ input: 'read:users', expected: { resource: 'users', action: 'read' } },
|
|
31
|
+
{ input: 'write:reports', expected: { resource: 'reports', action: 'write' } },
|
|
32
|
+
{ input: 'delete:events', expected: { resource: 'events', action: 'delete' } },
|
|
33
|
+
{ input: 'admin:system', expected: { resource: 'system', action: 'admin' } }
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
testCases.forEach(({ input, expected }) => {
|
|
37
|
+
const result = parsePermission(input);
|
|
38
|
+
expect(result).toEqual(expected);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle complex resource names', () => {
|
|
43
|
+
const testCases = [
|
|
44
|
+
{ input: 'read:user_profiles', expected: { resource: 'user_profiles', action: 'read' } },
|
|
45
|
+
{ input: 'write:api_keys', expected: { resource: 'api_keys', action: 'write' } },
|
|
46
|
+
{ input: 'delete:system_logs', expected: { resource: 'system_logs', action: 'delete' } },
|
|
47
|
+
{ input: 'admin:user_management', expected: { resource: 'user_management', action: 'admin' } }
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
testCases.forEach(({ input, expected }) => {
|
|
51
|
+
const result = parsePermission(input);
|
|
52
|
+
expect(result).toEqual(expected);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should handle resource names with hyphens', () => {
|
|
57
|
+
const testCases = [
|
|
58
|
+
{ input: 'read:user-profiles', expected: { resource: 'user-profiles', action: 'read' } },
|
|
59
|
+
{ input: 'write:api-keys', expected: { resource: 'api-keys', action: 'write' } },
|
|
60
|
+
{ input: 'delete:system-logs', expected: { resource: 'system-logs', action: 'delete' } }
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
testCases.forEach(({ input, expected }) => {
|
|
64
|
+
const result = parsePermission(input);
|
|
65
|
+
expect(result).toEqual(expected);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle resource names with dots', () => {
|
|
70
|
+
const testCases = [
|
|
71
|
+
{ input: 'read:user.profiles', expected: { resource: 'user.profiles', action: 'read' } },
|
|
72
|
+
{ input: 'write:api.keys', expected: { resource: 'api.keys', action: 'write' } },
|
|
73
|
+
{ input: 'delete:system.logs', expected: { resource: 'system.logs', action: 'delete' } }
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
testCases.forEach(({ input, expected }) => {
|
|
77
|
+
const result = parsePermission(input);
|
|
78
|
+
expect(result).toEqual(expected);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should return null for invalid permission strings', () => {
|
|
83
|
+
const invalidPermissions = [
|
|
84
|
+
'', // Empty string
|
|
85
|
+
'read', // Missing resource
|
|
86
|
+
':users', // Missing action
|
|
87
|
+
'read:users:extra', // Too many parts
|
|
88
|
+
'read-users', // Wrong separator
|
|
89
|
+
'read/users', // Wrong separator
|
|
90
|
+
'read users', // Wrong separator
|
|
91
|
+
'read::users', // Double colon
|
|
92
|
+
':read:users', // Leading colon
|
|
93
|
+
'read:users:', // Trailing colon
|
|
94
|
+
'read:', // Action with trailing colon
|
|
95
|
+
':users', // Resource with leading colon
|
|
96
|
+
'read:users:admin', // Three parts
|
|
97
|
+
'read:users:admin:extra' // Four parts
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
invalidPermissions.forEach(permission => {
|
|
101
|
+
const result = parsePermission(permission);
|
|
102
|
+
expect(result).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should return null for non-string inputs', () => {
|
|
107
|
+
const invalidInputs = [
|
|
108
|
+
null,
|
|
109
|
+
undefined,
|
|
110
|
+
123,
|
|
111
|
+
true,
|
|
112
|
+
false,
|
|
113
|
+
{},
|
|
114
|
+
[],
|
|
115
|
+
() => {}
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
invalidInputs.forEach(input => {
|
|
119
|
+
const result = parsePermission(input as any);
|
|
120
|
+
expect(result).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should handle whitespace in permission strings', () => {
|
|
125
|
+
const testCases = [
|
|
126
|
+
{ input: ' read:users', expected: { resource: 'users', action: ' read' } }, // Leading space
|
|
127
|
+
{ input: 'read:users ', expected: { resource: 'users ', action: 'read' } }, // Trailing space
|
|
128
|
+
{ input: ' read:users ', expected: { resource: 'users ', action: ' read' } }, // Both spaces
|
|
129
|
+
{ input: 'read :users', expected: { resource: 'users', action: 'read ' } }, // Space around colon
|
|
130
|
+
{ input: 'read: users', expected: { resource: ' users', action: 'read' } }, // Space after colon
|
|
131
|
+
{ input: ' read : users ', expected: { resource: ' users ', action: ' read ' } } // Multiple spaces
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
testCases.forEach(({ input, expected }) => {
|
|
135
|
+
const result = parsePermission(input);
|
|
136
|
+
expect(result).toEqual(expected);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should handle case sensitivity', () => {
|
|
141
|
+
const testCases = [
|
|
142
|
+
{ input: 'READ:users', expected: { resource: 'users', action: 'READ' } },
|
|
143
|
+
{ input: 'read:USERS', expected: { resource: 'USERS', action: 'read' } },
|
|
144
|
+
{ input: 'READ:USERS', expected: { resource: 'USERS', action: 'READ' } },
|
|
145
|
+
{ input: 'Read:Users', expected: { resource: 'Users', action: 'Read' } }
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
testCases.forEach(({ input, expected }) => {
|
|
149
|
+
const result = parsePermission(input);
|
|
150
|
+
expect(result).toEqual(expected);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should handle special characters in resource names', () => {
|
|
155
|
+
const testCases = [
|
|
156
|
+
{ input: 'read:users-123', expected: { resource: 'users-123', action: 'read' } },
|
|
157
|
+
{ input: 'write:api_v2', expected: { resource: 'api_v2', action: 'write' } },
|
|
158
|
+
{ input: 'delete:test@example.com', expected: { resource: 'test@example.com', action: 'delete' } },
|
|
159
|
+
{ input: 'admin:user+profile', expected: { resource: 'user+profile', action: 'admin' } }
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
testCases.forEach(({ input, expected }) => {
|
|
163
|
+
const result = parsePermission(input);
|
|
164
|
+
expect(result).toEqual(expected);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('Integration tests', () => {
|
|
170
|
+
it('should work with PermissionType enum values', () => {
|
|
171
|
+
const testCases = [
|
|
172
|
+
{ permission: `${PermissionType.READ}:users`, expected: { resource: 'users', action: PermissionType.READ } },
|
|
173
|
+
{ permission: `${PermissionType.WRITE}:reports`, expected: { resource: 'reports', action: PermissionType.WRITE } },
|
|
174
|
+
{ permission: `${PermissionType.DELETE}:events`, expected: { resource: 'events', action: PermissionType.DELETE } },
|
|
175
|
+
{ permission: `${PermissionType.ADMIN}:system`, expected: { resource: 'system', action: PermissionType.ADMIN } }
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
testCases.forEach(({ permission, expected }) => {
|
|
179
|
+
const result = parsePermission(permission);
|
|
180
|
+
expect(result).toEqual(expected);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should handle realistic permission scenarios', () => {
|
|
185
|
+
const realisticPermissions = [
|
|
186
|
+
'read:user_profiles',
|
|
187
|
+
'write:user_profiles',
|
|
188
|
+
'delete:user_profiles',
|
|
189
|
+
'read:system_logs',
|
|
190
|
+
'write:system_logs',
|
|
191
|
+
'admin:user_management',
|
|
192
|
+
'read:api_keys',
|
|
193
|
+
'write:api_keys',
|
|
194
|
+
'delete:api_keys',
|
|
195
|
+
'admin:system_config'
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
realisticPermissions.forEach(permission => {
|
|
199
|
+
const result = parsePermission(permission);
|
|
200
|
+
expect(result).not.toBeNull();
|
|
201
|
+
expect(result).toHaveProperty('action');
|
|
202
|
+
expect(result).toHaveProperty('resource');
|
|
203
|
+
expect(typeof result!.action).toBe('string');
|
|
204
|
+
expect(typeof result!.resource).toBe('string');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should handle permission validation workflow', () => {
|
|
209
|
+
const userPermissions = [
|
|
210
|
+
'read:users',
|
|
211
|
+
'write:users',
|
|
212
|
+
'read:reports',
|
|
213
|
+
'admin:system'
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
const parsedPermissions = userPermissions
|
|
217
|
+
.map(permission => parsePermission(permission))
|
|
218
|
+
.filter(result => result !== null);
|
|
219
|
+
|
|
220
|
+
expect(parsedPermissions).toHaveLength(4);
|
|
221
|
+
expect(parsedPermissions[0]).toEqual({ action: 'read', resource: 'users' });
|
|
222
|
+
expect(parsedPermissions[1]).toEqual({ action: 'write', resource: 'users' });
|
|
223
|
+
expect(parsedPermissions[2]).toEqual({ action: 'read', resource: 'reports' });
|
|
224
|
+
expect(parsedPermissions[3]).toEqual({ action: 'admin', resource: 'system' });
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should handle mixed valid and invalid permissions', () => {
|
|
228
|
+
const mixedPermissions = [
|
|
229
|
+
'read:users',
|
|
230
|
+
'invalid-permission',
|
|
231
|
+
'write:reports',
|
|
232
|
+
'',
|
|
233
|
+
'read:',
|
|
234
|
+
':users',
|
|
235
|
+
'admin:system',
|
|
236
|
+
null,
|
|
237
|
+
'read:users:extra'
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
const validResults = mixedPermissions
|
|
241
|
+
.map(permission => parsePermission(permission))
|
|
242
|
+
.filter(result => result !== null);
|
|
243
|
+
|
|
244
|
+
expect(validResults).toHaveLength(3);
|
|
245
|
+
expect(validResults[0]).toEqual({ action: 'read', resource: 'users' });
|
|
246
|
+
expect(validResults[1]).toEqual({ action: 'write', resource: 'reports' });
|
|
247
|
+
expect(validResults[2]).toEqual({ action: 'admin', resource: 'system' });
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|