@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,346 @@
1
+ /**
2
+ * @file Sanitization Unit Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/Sanitization
5
+ * @since 0.4.0
6
+ */
7
+
8
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
9
+ import {
10
+ sanitizeUserInput,
11
+ sanitizeEmail,
12
+ sanitizePhoneNumber,
13
+ sanitizeUrl,
14
+ sanitizeFileName,
15
+ sanitizeSqlInput,
16
+ sanitizeFormData,
17
+ generateCSPHeader,
18
+ RateLimiter
19
+ } from '../sanitization';
20
+ import { z } from 'zod';
21
+
22
+ describe('Sanitization Utilities', () => {
23
+ describe('sanitizeUserInput', () => {
24
+ it('should sanitize basic text input', () => {
25
+ const input = '<script>alert("xss")</script>Hello World';
26
+ const result = sanitizeUserInput(input);
27
+ expect(result).toBe('&lt;script&gt;alert(&quot;xss&quot;)&lt;&#x2F;script&gt;Hello World');
28
+ });
29
+
30
+ it('should handle non-string input', () => {
31
+ expect(sanitizeUserInput(null as any)).toBe('');
32
+ expect(sanitizeUserInput(undefined as any)).toBe('');
33
+ expect(sanitizeUserInput(123 as any)).toBe('');
34
+ expect(sanitizeUserInput({} as any)).toBe('');
35
+ });
36
+
37
+ it('should trim whitespace by default', () => {
38
+ const input = ' Hello World ';
39
+ const result = sanitizeUserInput(input);
40
+ expect(result).toBe('Hello World');
41
+ });
42
+
43
+ it('should not trim when trim option is false', () => {
44
+ const input = ' Hello World ';
45
+ const result = sanitizeUserInput(input, { trim: false });
46
+ expect(result).toBe(' Hello World ');
47
+ });
48
+
49
+ it('should enforce maximum length', () => {
50
+ const input = 'This is a very long string that should be truncated';
51
+ const result = sanitizeUserInput(input, { maxLength: 10 });
52
+ expect(result).toBe('This is a ');
53
+ });
54
+
55
+ it('should allow HTML when specified', () => {
56
+ const input = '<strong>Hello</strong> <em>World</em>';
57
+ const result = sanitizeUserInput(input, { allowHtml: true });
58
+ expect(result).toBe(input);
59
+ });
60
+
61
+ it('should allow specific HTML tags', () => {
62
+ const input = '<strong>Hello</strong> <script>alert("xss")</script> <em>World</em>';
63
+ const result = sanitizeUserInput(input, {
64
+ allowHtml: true,
65
+ allowedTags: ['strong', 'em']
66
+ });
67
+ expect(result).toBe('<strong>Hello</strong> alert("xss") <em>World</em>');
68
+ });
69
+
70
+ it('should remove script tags and javascript protocols', () => {
71
+ const input = '<script>alert("xss")</script>javascript:alert("xss")vbscript:alert("xss")';
72
+ const result = sanitizeUserInput(input, { removeScripts: true });
73
+ expect(result).toBe('&lt;script&gt;alert(&quot;xss&quot;)&lt;&#x2F;script&gt;alert(&quot;xss&quot;)alert(&quot;xss&quot;)');
74
+ });
75
+
76
+ it('should remove event handlers', () => {
77
+ const input = '<div onclick="alert(\'xss\')" onmouseover="alert(\'xss\')">Click me</div>';
78
+ const result = sanitizeUserInput(input, { removeEvents: true });
79
+ expect(result).toBe('&lt;div &quot;alert(&#x27;xss&#x27;)&quot; &quot;alert(&#x27;xss&#x27;)&quot;&gt;Click me&lt;&#x2F;div&gt;');
80
+ });
81
+
82
+ it('should escape HTML entities', () => {
83
+ const input = '<>&"\'/';
84
+ const result = sanitizeUserInput(input);
85
+ expect(result).toBe('&lt;&gt;&&quot;&#x27;&#x2F;');
86
+ });
87
+ });
88
+
89
+ describe('sanitizeEmail', () => {
90
+ it('should sanitize valid email addresses', () => {
91
+ expect(sanitizeEmail(' TEST@EXAMPLE.COM ')).toBe('test@example.com');
92
+ expect(sanitizeEmail('user+tag@domain.co.uk')).toBe('usertag@domain.co.uk');
93
+ });
94
+
95
+ it('should handle invalid input', () => {
96
+ expect(sanitizeEmail('')).toBe('');
97
+ expect(sanitizeEmail(null as any)).toBe('');
98
+ expect(sanitizeEmail(undefined as any)).toBe('');
99
+ expect(sanitizeEmail(123 as any)).toBe('');
100
+ });
101
+
102
+ it('should preserve valid email structure', () => {
103
+ const email = 'user.name+tag@subdomain.example.co.uk';
104
+ const result = sanitizeEmail(email);
105
+ expect(result).toBe('user.nametag@subdomain.example.co.uk');
106
+ });
107
+ });
108
+
109
+ describe('sanitizePhoneNumber', () => {
110
+ it('should sanitize phone numbers', () => {
111
+ expect(sanitizePhoneNumber(' +1 (555) 123-4567 ')).toBe('+1 (555) 123-4567');
112
+ expect(sanitizePhoneNumber('555.123.4567')).toBe('5551234567');
113
+ expect(sanitizePhoneNumber('555-123-4567')).toBe('555-123-4567');
114
+ });
115
+
116
+ it('should handle invalid input', () => {
117
+ expect(sanitizePhoneNumber('')).toBe('');
118
+ expect(sanitizePhoneNumber(null as any)).toBe('');
119
+ expect(sanitizePhoneNumber(undefined as any)).toBe('');
120
+ });
121
+
122
+ it('should preserve international format', () => {
123
+ expect(sanitizePhoneNumber('+44 20 7946 0958')).toBe('+44 20 7946 0958');
124
+ });
125
+ });
126
+
127
+ describe('sanitizeUrl', () => {
128
+ it('should sanitize valid URLs', () => {
129
+ expect(sanitizeUrl(' HTTPS://EXAMPLE.COM/PATH ')).toBe('HTTPS://EXAMPLE.COM/PATH');
130
+ expect(sanitizeUrl('http://subdomain.example.co.uk/path?param=value')).toBe('http://subdomain.example.co.uk/path?param=value');
131
+ });
132
+
133
+ it('should handle invalid input', () => {
134
+ expect(sanitizeUrl('')).toBe('');
135
+ expect(sanitizeUrl(null as any)).toBe('');
136
+ expect(sanitizeUrl(undefined as any)).toBe('');
137
+ });
138
+
139
+ it('should reject URLs without protocol', () => {
140
+ expect(sanitizeUrl('example.com')).toBe('');
141
+ expect(sanitizeUrl('//example.com')).toBe('');
142
+ });
143
+
144
+ it('should preserve query parameters and fragments', () => {
145
+ const url = 'https://example.com/path?param=value&other=123#fragment';
146
+ const result = sanitizeUrl(url);
147
+ expect(result).toBe(url);
148
+ });
149
+ });
150
+
151
+ describe('sanitizeFileName', () => {
152
+ it('should sanitize file names', () => {
153
+ expect(sanitizeFileName(' My File Name.txt ')).toBe('My File Name.txt');
154
+ expect(sanitizeFileName('file/with\\invalid:chars')).toBe('filewithinvalidchars');
155
+ });
156
+
157
+ it('should handle invalid input', () => {
158
+ expect(sanitizeFileName('')).toBe('');
159
+ expect(sanitizeFileName(null as any)).toBe('');
160
+ expect(sanitizeFileName(undefined as any)).toBe('');
161
+ });
162
+
163
+ it('should preserve file extensions', () => {
164
+ expect(sanitizeFileName('document.pdf')).toBe('document.pdf');
165
+ expect(sanitizeFileName('image.jpg')).toBe('image.jpg');
166
+ });
167
+
168
+ it('should remove dangerous characters', () => {
169
+ expect(sanitizeFileName('file<script>.txt')).toBe('filescript.txt');
170
+ expect(sanitizeFileName('file"name".txt')).toBe('filename.txt');
171
+ });
172
+ });
173
+
174
+ describe('sanitizeSqlInput', () => {
175
+ it('should sanitize SQL input', () => {
176
+ expect(sanitizeSqlInput("'; DROP TABLE users; --")).toBe("DROP TABLE users");
177
+ expect(sanitizeSqlInput("' OR '1'='1")).toBe("OR 1=1");
178
+ });
179
+
180
+ it('should handle invalid input', () => {
181
+ expect(sanitizeSqlInput('')).toBe('');
182
+ expect(sanitizeSqlInput(null as any)).toBe('');
183
+ expect(sanitizeSqlInput(undefined as any)).toBe('');
184
+ });
185
+
186
+ it('should escape single quotes', () => {
187
+ expect(sanitizeSqlInput("O'Connor")).toBe("OConnor");
188
+ expect(sanitizeSqlInput("user's data")).toBe("users data");
189
+ });
190
+ });
191
+
192
+ describe('sanitizeFormData', () => {
193
+ const testSchema = z.object({
194
+ name: z.string().min(1),
195
+ email: z.string().email(),
196
+ age: z.number().min(0)
197
+ });
198
+
199
+ it('should sanitize and validate form data', () => {
200
+ const data = {
201
+ name: ' John Doe ',
202
+ email: ' TEST@EXAMPLE.COM ',
203
+ age: 25
204
+ };
205
+
206
+ const result = sanitizeFormData(data, testSchema);
207
+ expect(result.success).toBe(false); // Email validation will fail
208
+ expect(result.error).toBeDefined();
209
+ });
210
+
211
+ it('should handle validation errors', () => {
212
+ const data = {
213
+ name: '',
214
+ email: 'invalid-email',
215
+ age: -1
216
+ };
217
+
218
+ const result = sanitizeFormData(data, testSchema);
219
+ expect(result.success).toBe(false);
220
+ expect(result.error).toBeDefined();
221
+ });
222
+
223
+ it('should apply custom sanitization rules', () => {
224
+ const data = {
225
+ name: '<script>alert("xss")</script>John',
226
+ email: 'test@example.com',
227
+ age: 25
228
+ };
229
+
230
+ const sanitizationRules = {
231
+ name: { removeScripts: true, trim: true }
232
+ };
233
+
234
+ const result = sanitizeFormData(data, testSchema, sanitizationRules);
235
+ expect(result.success).toBe(true);
236
+ expect(result.data?.name).toBe('&lt;script&gt;alert(&quot;xss&quot;)&lt;&#x2F;script&gt;John');
237
+ });
238
+
239
+ it('should handle invalid input', () => {
240
+ const result = sanitizeFormData(null as any, testSchema);
241
+ expect(result.success).toBe(false);
242
+ expect(result.error).toBeDefined();
243
+ });
244
+ });
245
+
246
+ describe('generateCSPHeader', () => {
247
+ it('should generate default CSP header', () => {
248
+ const header = generateCSPHeader();
249
+ expect(header).toContain("default-src 'self'");
250
+ expect(header).toContain("script-src 'self'");
251
+ expect(header).toContain("style-src 'self'");
252
+ });
253
+
254
+ it('should include custom directives', () => {
255
+ const customDirectives = {
256
+ script: "'self' 'unsafe-inline'",
257
+ img: "'self' data: https:"
258
+ };
259
+
260
+ const header = generateCSPHeader(customDirectives);
261
+ expect(header).toContain("'self' 'unsafe-inline'");
262
+ expect(header).toContain("'self' data: https:");
263
+ });
264
+ });
265
+
266
+ describe('RateLimiter', () => {
267
+ let rateLimiter: RateLimiter;
268
+
269
+ beforeEach(() => {
270
+ rateLimiter = new RateLimiter(3, 1000); // 3 attempts per second
271
+ });
272
+
273
+ it('should allow requests within limit', () => {
274
+ expect(rateLimiter.isAllowed('user1')).toBe(true);
275
+ expect(rateLimiter.isAllowed('user1')).toBe(true);
276
+ expect(rateLimiter.isAllowed('user1')).toBe(true);
277
+ });
278
+
279
+ it('should block requests over limit', () => {
280
+ expect(rateLimiter.isAllowed('user1')).toBe(true);
281
+ expect(rateLimiter.isAllowed('user1')).toBe(true);
282
+ expect(rateLimiter.isAllowed('user1')).toBe(true);
283
+ expect(rateLimiter.isAllowed('user1')).toBe(false);
284
+ });
285
+
286
+ it('should track different identifiers separately', () => {
287
+ expect(rateLimiter.isAllowed('user1')).toBe(true);
288
+ expect(rateLimiter.isAllowed('user2')).toBe(true);
289
+ expect(rateLimiter.isAllowed('user1')).toBe(true);
290
+ expect(rateLimiter.isAllowed('user2')).toBe(true);
291
+ });
292
+
293
+ it('should return remaining attempts', () => {
294
+ expect(rateLimiter.getRemainingAttempts('user1')).toBe(3);
295
+ rateLimiter.isAllowed('user1');
296
+ expect(rateLimiter.getRemainingAttempts('user1')).toBe(2);
297
+ });
298
+
299
+ it('should reset attempts', () => {
300
+ rateLimiter.isAllowed('user1');
301
+ rateLimiter.isAllowed('user1');
302
+ expect(rateLimiter.getRemainingAttempts('user1')).toBe(1);
303
+
304
+ rateLimiter.reset('user1');
305
+ expect(rateLimiter.getRemainingAttempts('user1')).toBe(3);
306
+ });
307
+
308
+ it('should handle time-based expiration', () => {
309
+ // Mock Date.now to simulate time passing
310
+ const originalNow = Date.now;
311
+ let mockTime = 1000;
312
+ Date.now = vi.fn(() => mockTime);
313
+
314
+ rateLimiter.isAllowed('user1');
315
+ rateLimiter.isAllowed('user1');
316
+ rateLimiter.isAllowed('user1');
317
+ expect(rateLimiter.isAllowed('user1')).toBe(false);
318
+
319
+ // Advance time beyond window
320
+ mockTime = 2001;
321
+ expect(rateLimiter.isAllowed('user1')).toBe(true);
322
+
323
+ Date.now = originalNow;
324
+ });
325
+ });
326
+
327
+ describe('Edge Cases', () => {
328
+ it('should handle very long inputs', () => {
329
+ const longInput = 'a'.repeat(10000);
330
+ const result = sanitizeUserInput(longInput, { maxLength: 100 });
331
+ expect(result.length).toBe(100);
332
+ });
333
+
334
+ it('should handle special characters in URLs', () => {
335
+ const url = 'https://example.com/path with spaces/and+plus/signs?param=value with spaces#fragment';
336
+ const result = sanitizeUrl(url);
337
+ expect(result).toBe(url);
338
+ });
339
+
340
+ it('should handle empty sanitization rules', () => {
341
+ const data = { name: 'John', email: 'test@example.com' };
342
+ const result = sanitizeFormData(data, z.object({ name: z.string(), email: z.string() }));
343
+ expect(result.success).toBe(true);
344
+ });
345
+ });
346
+ });