@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.
Files changed (158) hide show
  1. package/dist/{DataTable-ZQDRE46Q.js → DataTable-BEMN72L5.js} +2 -2
  2. package/dist/{chunk-5H3C2SWM.js → chunk-4EIBJ6DF.js} +2 -2
  3. package/dist/{chunk-M4RW7PIP.js → chunk-SFGUMWEE.js} +105 -81
  4. package/dist/chunk-SFGUMWEE.js.map +1 -0
  5. package/dist/components.js +2 -2
  6. package/dist/index.js +2 -2
  7. package/dist/utils.js +1 -1
  8. package/docs/api/classes/ErrorBoundary.md +1 -1
  9. package/docs/api/classes/InvalidScopeError.md +1 -1
  10. package/docs/api/classes/MissingUserContextError.md +1 -1
  11. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  12. package/docs/api/classes/PermissionDeniedError.md +1 -1
  13. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  14. package/docs/api/classes/RBACAuditManager.md +1 -1
  15. package/docs/api/classes/RBACCache.md +1 -1
  16. package/docs/api/classes/RBACEngine.md +1 -1
  17. package/docs/api/classes/RBACError.md +1 -1
  18. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  19. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  20. package/docs/api/interfaces/AggregateConfig.md +1 -1
  21. package/docs/api/interfaces/ButtonProps.md +1 -1
  22. package/docs/api/interfaces/CardProps.md +1 -1
  23. package/docs/api/interfaces/ColorPalette.md +1 -1
  24. package/docs/api/interfaces/ColorShade.md +1 -1
  25. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  26. package/docs/api/interfaces/DataTableAction.md +1 -1
  27. package/docs/api/interfaces/DataTableColumn.md +1 -1
  28. package/docs/api/interfaces/DataTableProps.md +34 -34
  29. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  30. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  31. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  32. package/docs/api/interfaces/EventContextType.md +1 -1
  33. package/docs/api/interfaces/EventLogoProps.md +1 -1
  34. package/docs/api/interfaces/EventProviderProps.md +1 -1
  35. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  36. package/docs/api/interfaces/FileUploadProps.md +1 -1
  37. package/docs/api/interfaces/FooterProps.md +1 -1
  38. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  39. package/docs/api/interfaces/InputProps.md +1 -1
  40. package/docs/api/interfaces/LabelProps.md +1 -1
  41. package/docs/api/interfaces/LoginFormProps.md +1 -1
  42. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  43. package/docs/api/interfaces/NavigationContextType.md +1 -1
  44. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  45. package/docs/api/interfaces/NavigationItem.md +1 -1
  46. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  47. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  48. package/docs/api/interfaces/Organisation.md +1 -1
  49. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  50. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  51. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  52. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  53. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  54. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  55. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  56. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  57. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  58. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  59. package/docs/api/interfaces/PaletteData.md +1 -1
  60. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  61. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  62. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  63. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  64. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  65. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  66. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  67. package/docs/api/interfaces/RBACConfig.md +1 -1
  68. package/docs/api/interfaces/RBACContextType.md +1 -1
  69. package/docs/api/interfaces/RBACLogger.md +1 -1
  70. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  71. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  72. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  73. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  74. package/docs/api/interfaces/RouteConfig.md +1 -1
  75. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  76. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  77. package/docs/api/interfaces/StorageConfig.md +1 -1
  78. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  79. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  80. package/docs/api/interfaces/StorageListOptions.md +1 -1
  81. package/docs/api/interfaces/StorageListResult.md +1 -1
  82. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  83. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  84. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  85. package/docs/api/interfaces/StyleImport.md +1 -1
  86. package/docs/api/interfaces/ToastActionElement.md +1 -1
  87. package/docs/api/interfaces/ToastProps.md +1 -1
  88. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  89. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  90. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  91. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  92. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  93. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  94. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  95. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  96. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  97. package/docs/api/interfaces/UserEventAccess.md +1 -1
  98. package/docs/api/interfaces/UserMenuProps.md +1 -1
  99. package/docs/api/interfaces/UserProfile.md +1 -1
  100. package/docs/api/modules.md +3 -3
  101. package/docs/implementation-guides/data-tables.md +20 -0
  102. package/docs/quick-reference.md +9 -0
  103. package/docs/rbac/examples.md +4 -0
  104. package/package.json +1 -1
  105. package/src/__tests__/helpers/test-utils.tsx +147 -1
  106. package/src/components/DataTable/DataTable.tsx +20 -0
  107. package/src/components/DataTable/__tests__/DataTable.hooks.test 2.tsx +191 -0
  108. package/src/components/DataTable/__tests__/DataTable.hooks.test.tsx +191 -0
  109. package/src/components/DataTable/components/DataTableCore.tsx +164 -131
  110. package/src/hooks/__tests__/hooks.integration.test.tsx +575 -0
  111. package/src/hooks/__tests__/useApiFetch.unit.test.ts +115 -0
  112. package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +133 -0
  113. package/src/hooks/__tests__/useDebounce.unit.test.ts +82 -0
  114. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +293 -0
  115. package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +385 -0
  116. package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +286 -0
  117. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +838 -0
  118. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +104 -0
  119. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +633 -0
  120. package/src/hooks/__tests__/useRBAC.unit.test.ts +856 -0
  121. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +537 -0
  122. package/src/hooks/__tests__/useToast.unit.test.tsx +62 -0
  123. package/src/hooks/__tests__/useZodForm.unit.test.tsx +37 -0
  124. package/src/rbac/utils/__tests__/eventContext.test.ts +428 -0
  125. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +428 -0
  126. package/src/utils/__tests__/appConfig.unit.test.ts +55 -0
  127. package/src/utils/__tests__/audit.unit.test.ts +69 -0
  128. package/src/utils/__tests__/auth-utils.unit.test.ts +70 -0
  129. package/src/utils/__tests__/bundleAnalysis.unit.test.ts +317 -0
  130. package/src/utils/__tests__/cn.unit.test.ts +34 -0
  131. package/src/utils/__tests__/deviceFingerprint.unit.test.ts +503 -0
  132. package/src/utils/__tests__/dynamicUtils.unit.test.ts +322 -0
  133. package/src/utils/__tests__/formatDate.unit.test.ts +109 -0
  134. package/src/utils/__tests__/formatting.unit.test.ts +66 -0
  135. package/src/utils/__tests__/index.unit.test.ts +251 -0
  136. package/src/utils/__tests__/lazyLoad.unit.test.tsx +309 -0
  137. package/src/utils/__tests__/organisationContext.unit.test.ts +192 -0
  138. package/src/utils/__tests__/performanceBudgets.unit.test.ts +259 -0
  139. package/src/utils/__tests__/permissionTypes.unit.test.ts +250 -0
  140. package/src/utils/__tests__/permissionUtils.unit.test.ts +362 -0
  141. package/src/utils/__tests__/sanitization.unit.test.ts +346 -0
  142. package/src/utils/__tests__/schemaUtils.unit.test.ts +441 -0
  143. package/src/utils/__tests__/secureDataAccess.unit.test.ts +334 -0
  144. package/src/utils/__tests__/secureErrors.unit.test.ts +377 -0
  145. package/src/utils/__tests__/secureStorage.unit.test.ts +293 -0
  146. package/src/utils/__tests__/security.unit.test.ts +127 -0
  147. package/src/utils/__tests__/securityMonitor.unit.test.ts +280 -0
  148. package/src/utils/__tests__/sessionTracking.unit.test.ts +356 -0
  149. package/src/utils/__tests__/validation.unit.test.ts +84 -0
  150. package/src/utils/__tests__/validationUtils.unit.test.ts +571 -0
  151. package/src/validation/__tests__/common.unit.test.ts +101 -0
  152. package/src/validation/__tests__/csrf.unit.test.ts +302 -0
  153. package/src/validation/__tests__/passwordSchema.unit.test 2.ts +98 -0
  154. package/src/validation/__tests__/passwordSchema.unit.test.ts +98 -0
  155. package/src/validation/__tests__/sqlInjectionProtection.unit.test.ts +466 -0
  156. package/dist/chunk-M4RW7PIP.js.map +0 -1
  157. /package/dist/{DataTable-ZQDRE46Q.js.map → DataTable-BEMN72L5.js.map} +0 -0
  158. /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
+ });