@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,571 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { z } from 'zod';
3
+ import {
4
+ validateUserInput,
5
+ sanitizeUserInput_deprecated,
6
+ emailSchema,
7
+ passwordSchema,
8
+ usernameSchema,
9
+ nameSchema,
10
+ phoneSchema,
11
+ urlSchema
12
+ } from '../validationUtils';
13
+
14
+ // Mock sanitization functions
15
+ vi.mock('../sanitization', () => ({
16
+ sanitizeUserInput: vi.fn((input: string) => `sanitized_${input}`),
17
+ sanitizeFormData: vi.fn((data: unknown, schema: z.ZodSchema, rules?: Record<string, any>) => {
18
+ // Try to parse with the schema to determine success
19
+ try {
20
+ const result = schema.parse(data);
21
+ return {
22
+ success: true,
23
+ data: result
24
+ };
25
+ } catch (error) {
26
+ return {
27
+ success: false,
28
+ error: error instanceof Error ? error.message : 'Validation failed'
29
+ };
30
+ }
31
+ })
32
+ }));
33
+
34
+ describe('validationUtils', () => {
35
+ describe('validateUserInput', () => {
36
+ it('should validate user input with sanitization', () => {
37
+ const schema = z.object({
38
+ name: z.string(),
39
+ email: z.string().email()
40
+ });
41
+
42
+ const data = { name: 'John', email: 'john@example.com' };
43
+ const result = validateUserInput(schema, data);
44
+
45
+ expect(result.success).toBe(true);
46
+ expect(result.data).toEqual(data);
47
+ });
48
+
49
+ it('should handle validation errors', () => {
50
+ const schema = z.object({
51
+ name: z.string().min(2),
52
+ email: z.string().email()
53
+ });
54
+
55
+ const data = { name: 'J', email: 'invalid-email' };
56
+ const result = validateUserInput(schema, data);
57
+
58
+ expect(result.success).toBe(false);
59
+ expect(result.error).toBeDefined();
60
+ });
61
+
62
+ it('should handle sanitization rules', () => {
63
+ const schema = z.object({
64
+ name: z.string(),
65
+ bio: z.string()
66
+ });
67
+
68
+ const data = { name: 'John', bio: 'Some bio' };
69
+ const sanitizationRules = {
70
+ name: { allowHtml: false, maxLength: 50 },
71
+ bio: { allowHtml: true, maxLength: 200 }
72
+ };
73
+
74
+ const result = validateUserInput(schema, data, sanitizationRules);
75
+
76
+ expect(result.success).toBe(true);
77
+ expect(result.data).toEqual(data);
78
+ });
79
+ });
80
+
81
+ describe('sanitizeUserInput_deprecated', () => {
82
+ it('should call sanitizeUserInput and log deprecation warning', () => {
83
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
84
+
85
+ const result = sanitizeUserInput_deprecated('test input');
86
+
87
+ expect(result).toBe('sanitized_test input');
88
+ expect(consoleSpy).toHaveBeenCalledWith(
89
+ 'sanitizeUserInput is deprecated. Use sanitizeUserInput from lib/sanitization instead.'
90
+ );
91
+
92
+ consoleSpy.mockRestore();
93
+ });
94
+ });
95
+
96
+ describe('emailSchema', () => {
97
+ it('should validate valid email addresses', () => {
98
+ const validEmails = [
99
+ 'test@example.com',
100
+ 'user.name@domain.co.uk',
101
+ 'user+tag@example.org',
102
+ '123@example.com',
103
+ 'test@subdomain.example.com'
104
+ ];
105
+
106
+ validEmails.forEach(email => {
107
+ const result = emailSchema.safeParse(email);
108
+ expect(result.success).toBe(true);
109
+ if (result.success) {
110
+ expect(result.data).toBe(email.toLowerCase().trim());
111
+ }
112
+ });
113
+ });
114
+
115
+ it('should reject invalid email addresses', () => {
116
+ const invalidEmails = [
117
+ '',
118
+ 'invalid-email',
119
+ '@example.com',
120
+ 'test@',
121
+ 'test@.com',
122
+ 'test..test@example.com',
123
+ 'test@example..com',
124
+ 'test@example.com.',
125
+ 'test@example.com'.repeat(100) // Too long
126
+ ];
127
+
128
+ invalidEmails.forEach(email => {
129
+ const result = emailSchema.safeParse(email);
130
+ expect(result.success).toBe(false);
131
+ });
132
+ });
133
+
134
+ it('should transform email to lowercase and trim', () => {
135
+ const result = emailSchema.safeParse(' TEST@EXAMPLE.COM ');
136
+ expect(result.success).toBe(true);
137
+ if (result.success) {
138
+ expect(result.data).toBe('test@example.com');
139
+ }
140
+ });
141
+
142
+ it('should enforce minimum length', () => {
143
+ const result = emailSchema.safeParse('');
144
+ expect(result.success).toBe(false);
145
+ if (!result.success) {
146
+ expect(result.error.issues[0].message).toBe('Email is required');
147
+ }
148
+ });
149
+
150
+ it('should enforce maximum length', () => {
151
+ const longEmail = 'a'.repeat(250) + '@example.com';
152
+ const result = emailSchema.safeParse(longEmail);
153
+ expect(result.success).toBe(false);
154
+ if (!result.success) {
155
+ expect(result.error.issues[0].message).toBe('Email too long');
156
+ }
157
+ });
158
+ });
159
+
160
+ describe('passwordSchema', () => {
161
+ it('should validate strong passwords', () => {
162
+ const validPasswords = [
163
+ 'Password123!',
164
+ 'MySecurePass1@',
165
+ 'ComplexP@ssw0rd',
166
+ 'Str0ng#Pass',
167
+ 'ValidP@ss1'
168
+ ];
169
+
170
+ validPasswords.forEach(password => {
171
+ const result = passwordSchema.safeParse(password);
172
+ expect(result.success).toBe(true);
173
+ });
174
+ });
175
+
176
+ it('should reject weak passwords', () => {
177
+ const invalidPasswords = [
178
+ '', // Empty
179
+ 'short', // Too short
180
+ 'password', // No uppercase, number, or special char
181
+ 'PASSWORD', // No lowercase, number, or special char
182
+ 'Password', // No number or special char
183
+ 'password123', // No uppercase or special char
184
+ 'PASSWORD123', // No lowercase or special char
185
+ 'Password123', // No special char
186
+ 'a'.repeat(129) // Too long
187
+ ];
188
+
189
+ invalidPasswords.forEach(password => {
190
+ const result = passwordSchema.safeParse(password);
191
+ expect(result.success).toBe(false);
192
+ });
193
+ });
194
+
195
+ it('should enforce minimum length', () => {
196
+ const result = passwordSchema.safeParse('Pass1!');
197
+ expect(result.success).toBe(false);
198
+ if (!result.success) {
199
+ expect(result.error.issues[0].message).toBe('Password must be at least 8 characters');
200
+ }
201
+ });
202
+
203
+ it('should enforce maximum length', () => {
204
+ const longPassword = 'A'.repeat(129);
205
+ const result = passwordSchema.safeParse(longPassword);
206
+ expect(result.success).toBe(false);
207
+ if (!result.success) {
208
+ expect(result.error.issues[0].message).toBe('Password too long');
209
+ }
210
+ });
211
+
212
+ it('should require uppercase letter', () => {
213
+ const result = passwordSchema.safeParse('password123!');
214
+ expect(result.success).toBe(false);
215
+ if (!result.success) {
216
+ expect(result.error.issues.some(issue => issue.message === 'Password must contain at least one uppercase letter')).toBe(true);
217
+ }
218
+ });
219
+
220
+ it('should require lowercase letter', () => {
221
+ const result = passwordSchema.safeParse('PASSWORD123!');
222
+ expect(result.success).toBe(false);
223
+ if (!result.success) {
224
+ expect(result.error.issues.some(issue => issue.message === 'Password must contain at least one lowercase letter')).toBe(true);
225
+ }
226
+ });
227
+
228
+ it('should require number', () => {
229
+ const result = passwordSchema.safeParse('Password!');
230
+ expect(result.success).toBe(false);
231
+ if (!result.success) {
232
+ expect(result.error.issues.some(issue => issue.message === 'Password must contain at least one number')).toBe(true);
233
+ }
234
+ });
235
+
236
+ it('should require special character', () => {
237
+ const result = passwordSchema.safeParse('Password123');
238
+ expect(result.success).toBe(false);
239
+ if (!result.success) {
240
+ expect(result.error.issues.some(issue => issue.message === 'Password must contain at least one special character')).toBe(true);
241
+ }
242
+ });
243
+ });
244
+
245
+ describe('usernameSchema', () => {
246
+ it('should validate valid usernames', () => {
247
+ const validUsernames = [
248
+ 'john_doe',
249
+ 'user123',
250
+ 'test-user',
251
+ 'admin',
252
+ 'user_name_123'
253
+ ];
254
+
255
+ validUsernames.forEach(username => {
256
+ const result = usernameSchema.safeParse(username);
257
+ expect(result.success).toBe(true);
258
+ if (result.success) {
259
+ expect(result.data).toBe(username.toLowerCase().trim());
260
+ }
261
+ });
262
+ });
263
+
264
+ it('should reject invalid usernames', () => {
265
+ const invalidUsernames = [
266
+ '', // Empty
267
+ 'ab', // Too short
268
+ 'a'.repeat(31), // Too long
269
+ 'user@name', // Invalid character
270
+ 'user name', // Space
271
+ 'user.name', // Dot
272
+ 'user/name', // Slash
273
+ 'user\\name', // Backslash
274
+ 'user:name', // Colon
275
+ 'user;name', // Semicolon
276
+ 'user,name', // Comma
277
+ 'user<name', // Less than
278
+ 'user>name', // Greater than
279
+ 'user[name', // Bracket
280
+ 'user]name', // Bracket
281
+ 'user{name', // Brace
282
+ 'user}name', // Brace
283
+ 'user|name', // Pipe
284
+ 'user`name', // Backtick
285
+ 'user~name' // Tilde
286
+ ];
287
+
288
+ invalidUsernames.forEach(username => {
289
+ const result = usernameSchema.safeParse(username);
290
+ expect(result.success).toBe(false);
291
+ });
292
+ });
293
+
294
+ it('should transform username to lowercase and trim', () => {
295
+ const result = usernameSchema.safeParse(' USER_NAME ');
296
+ expect(result.success).toBe(true);
297
+ if (result.success) {
298
+ expect(result.data).toBe('user_name');
299
+ }
300
+ });
301
+
302
+ it('should enforce minimum length', () => {
303
+ const result = usernameSchema.safeParse('ab');
304
+ expect(result.success).toBe(false);
305
+ if (!result.success) {
306
+ expect(result.error.issues[0].message).toBe('Username must be at least 3 characters');
307
+ }
308
+ });
309
+
310
+ it('should enforce maximum length', () => {
311
+ const longUsername = 'a'.repeat(31);
312
+ const result = usernameSchema.safeParse(longUsername);
313
+ expect(result.success).toBe(false);
314
+ if (!result.success) {
315
+ expect(result.error.issues[0].message).toBe('Username too long');
316
+ }
317
+ });
318
+ });
319
+
320
+ describe('nameSchema', () => {
321
+ it('should validate valid names', () => {
322
+ const validNames = [
323
+ 'John',
324
+ 'Mary Jane',
325
+ 'O\'Connor',
326
+ 'Jean-Pierre',
327
+ 'José',
328
+ 'Müller'
329
+ ];
330
+
331
+ validNames.forEach(name => {
332
+ const result = nameSchema.safeParse(name);
333
+ expect(result.success).toBe(true);
334
+ if (result.success) {
335
+ expect(result.data).toBe('sanitized_' + name);
336
+ }
337
+ });
338
+ });
339
+
340
+ it('should reject invalid names', () => {
341
+ const invalidNames = [
342
+ '', // Empty
343
+ 'a'.repeat(101), // Too long
344
+ 'John<script>alert("xss")</script>', // HTML injection
345
+ 'John<img src="x" onerror="alert(1)">', // XSS attempt
346
+ ];
347
+
348
+ invalidNames.forEach(name => {
349
+ const result = nameSchema.safeParse(name);
350
+ expect(result.success).toBe(false);
351
+ });
352
+ });
353
+
354
+ it('should enforce minimum length', () => {
355
+ const result = nameSchema.safeParse('');
356
+ expect(result.success).toBe(false);
357
+ if (!result.success) {
358
+ expect(result.error.issues[0].message).toBe('Name is required');
359
+ }
360
+ });
361
+
362
+ it('should enforce maximum length', () => {
363
+ const longName = 'a'.repeat(101);
364
+ const result = nameSchema.safeParse(longName);
365
+ expect(result.success).toBe(false);
366
+ if (!result.success) {
367
+ expect(result.error.issues[0].message).toBe('Name too long');
368
+ }
369
+ });
370
+ });
371
+
372
+ describe('phoneSchema', () => {
373
+ it('should validate valid phone numbers', () => {
374
+ const validPhones = [
375
+ '1234567890',
376
+ '+1234567890',
377
+ '+1-234-567-8900',
378
+ '+44 20 7946 0958',
379
+ '123-456-7890',
380
+ '123.456.7890',
381
+ '(123) 456-7890'
382
+ ];
383
+
384
+ validPhones.forEach(phone => {
385
+ const result = phoneSchema.safeParse(phone);
386
+ expect(result.success).toBe(true);
387
+ });
388
+ });
389
+
390
+ it('should reject invalid phone numbers', () => {
391
+ const invalidPhones = [
392
+ '', // Empty
393
+ '123', // Too short
394
+ 'a'.repeat(21), // Too long
395
+ '123-456-789', // Too short
396
+ 'abc-def-ghij', // Non-numeric
397
+ '123-456-789a', // Contains letter
398
+ '123-456-789!', // Contains special char
399
+ '123-456-789 ', // Trailing space
400
+ ' 123-456-789', // Leading space
401
+ ];
402
+
403
+ invalidPhones.forEach(phone => {
404
+ const result = phoneSchema.safeParse(phone);
405
+ expect(result.success).toBe(false);
406
+ });
407
+ });
408
+
409
+ it('should enforce minimum length', () => {
410
+ const result = phoneSchema.safeParse('123456789');
411
+ expect(result.success).toBe(false);
412
+ if (!result.success) {
413
+ expect(result.error.issues[0].message).toBe('Phone number must be at least 10 digits');
414
+ }
415
+ });
416
+
417
+ it('should enforce maximum length', () => {
418
+ const longPhone = '1'.repeat(21);
419
+ const result = phoneSchema.safeParse(longPhone);
420
+ expect(result.success).toBe(false);
421
+ if (!result.success) {
422
+ expect(result.error.issues[0].message).toBe('Phone number too long');
423
+ }
424
+ });
425
+ });
426
+
427
+ describe('urlSchema', () => {
428
+ it('should validate valid URLs', () => {
429
+ const validUrls = [
430
+ 'https://example.com',
431
+ 'http://example.com',
432
+ 'https://www.example.com',
433
+ 'https://example.com/path',
434
+ 'https://example.com/path?param=value',
435
+ 'https://example.com/path#fragment',
436
+ 'https://subdomain.example.com',
437
+ 'https://example.co.uk',
438
+ 'https://example.com:8080',
439
+ 'https://user:pass@example.com'
440
+ ];
441
+
442
+ validUrls.forEach(url => {
443
+ const result = urlSchema.safeParse(url);
444
+ expect(result.success).toBe(true);
445
+ });
446
+ });
447
+
448
+ it('should reject invalid URLs', () => {
449
+ const invalidUrls = [
450
+ '', // Empty
451
+ 'not-a-url', // Invalid format
452
+ 'ftp://example.com', // Wrong protocol
453
+ 'file://example.com', // Wrong protocol
454
+ 'mailto:user@example.com', // Wrong protocol
455
+ 'javascript:alert(1)', // Dangerous protocol
456
+ 'data:text/html,<script>alert(1)</script>', // Data URL
457
+ 'https://', // Incomplete
458
+ 'http://', // Incomplete
459
+ 'a'.repeat(2049), // Too long
460
+ ];
461
+
462
+ invalidUrls.forEach(url => {
463
+ const result = urlSchema.safeParse(url);
464
+ expect(result.success).toBe(false);
465
+ });
466
+ });
467
+
468
+ it('should enforce maximum length', () => {
469
+ const longUrl = 'https://example.com/' + 'a'.repeat(2048);
470
+ const result = urlSchema.safeParse(longUrl);
471
+ expect(result.success).toBe(false);
472
+ if (!result.success) {
473
+ expect(result.error.issues[0].message).toBe('URL too long');
474
+ }
475
+ });
476
+
477
+ it('should only allow HTTP and HTTPS protocols', () => {
478
+ const protocols = [
479
+ 'http://example.com',
480
+ 'https://example.com',
481
+ 'ftp://example.com',
482
+ 'file://example.com',
483
+ 'mailto:user@example.com',
484
+ 'javascript:alert(1)',
485
+ 'data:text/html,<script>alert(1)</script>'
486
+ ];
487
+
488
+ protocols.forEach(url => {
489
+ const result = urlSchema.safeParse(url);
490
+ if (url.startsWith('http://') || url.startsWith('https://')) {
491
+ expect(result.success).toBe(true);
492
+ } else {
493
+ expect(result.success).toBe(false);
494
+ }
495
+ });
496
+ });
497
+ });
498
+
499
+ describe('Integration tests', () => {
500
+ it('should work together in a user registration scenario', () => {
501
+ const userData = {
502
+ email: 'john.doe@example.com',
503
+ password: 'SecurePass123!',
504
+ username: 'john_doe',
505
+ name: 'John Doe',
506
+ phone: '+1-234-567-8900',
507
+ website: 'https://johndoe.com'
508
+ };
509
+
510
+ // Validate each field
511
+ const emailResult = emailSchema.safeParse(userData.email);
512
+ const passwordResult = passwordSchema.safeParse(userData.password);
513
+ const usernameResult = usernameSchema.safeParse(userData.username);
514
+ const nameResult = nameSchema.safeParse(userData.name);
515
+ const phoneResult = phoneSchema.safeParse(userData.phone);
516
+ const urlResult = urlSchema.safeParse(userData.website);
517
+
518
+ expect(emailResult.success).toBe(true);
519
+ expect(passwordResult.success).toBe(true);
520
+ expect(usernameResult.success).toBe(true);
521
+ expect(nameResult.success).toBe(true);
522
+ expect(phoneResult.success).toBe(true);
523
+ expect(urlResult.success).toBe(true);
524
+ });
525
+
526
+ it('should handle form validation with multiple schemas', () => {
527
+ const formSchema = z.object({
528
+ email: emailSchema,
529
+ password: passwordSchema,
530
+ username: usernameSchema,
531
+ name: nameSchema,
532
+ phone: phoneSchema.optional(),
533
+ website: urlSchema.optional()
534
+ });
535
+
536
+ const validFormData = {
537
+ email: 'user@example.com',
538
+ password: 'SecurePass123!',
539
+ username: 'user123',
540
+ name: 'John Doe',
541
+ phone: '+1-234-567-8900',
542
+ website: 'https://example.com'
543
+ };
544
+
545
+ const result = formSchema.safeParse(validFormData);
546
+ expect(result.success).toBe(true);
547
+ });
548
+
549
+ it('should handle validation errors in form data', () => {
550
+ const formSchema = z.object({
551
+ email: emailSchema,
552
+ password: passwordSchema,
553
+ username: usernameSchema,
554
+ name: nameSchema
555
+ });
556
+
557
+ const invalidFormData = {
558
+ email: 'invalid-email',
559
+ password: 'weak',
560
+ username: 'ab',
561
+ name: ''
562
+ };
563
+
564
+ const result = formSchema.safeParse(invalidFormData);
565
+ expect(result.success).toBe(false);
566
+ if (!result.success) {
567
+ expect(result.error.issues.length).toBeGreaterThan(0);
568
+ }
569
+ });
570
+ });
571
+ });
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ emailSchema,
4
+ nameSchema,
5
+ phoneSchema,
6
+ urlSchema,
7
+ dateSchema
8
+ } from '../common';
9
+
10
+ describe('Common Validation Schemas', () => {
11
+ describe('emailSchema', () => {
12
+ it('should validate correct email addresses', () => {
13
+ expect(emailSchema.safeParse('test@example.com').success).toBe(true);
14
+ expect(emailSchema.safeParse('user.name+tag@domain.co.uk').success).toBe(true);
15
+ expect(emailSchema.safeParse('123@domain.org').success).toBe(true);
16
+ });
17
+
18
+ it('should reject invalid email addresses', () => {
19
+ expect(emailSchema.safeParse('invalid-email').success).toBe(false);
20
+ expect(emailSchema.safeParse('@domain.com').success).toBe(false);
21
+ expect(emailSchema.safeParse('user@').success).toBe(false);
22
+ expect(emailSchema.safeParse('').success).toBe(false);
23
+ });
24
+
25
+ it('should reject emails that are too long', () => {
26
+ const longEmail = 'a'.repeat(255) + '@example.com';
27
+ expect(emailSchema.safeParse(longEmail).success).toBe(false);
28
+ });
29
+ });
30
+
31
+ describe('nameSchema', () => {
32
+ it('should validate correct names', () => {
33
+ expect(nameSchema.safeParse('John Doe').success).toBe(true);
34
+ expect(nameSchema.safeParse('Mary-Jane').success).toBe(true);
35
+ expect(nameSchema.safeParse("O'Connor").success).toBe(true);
36
+ });
37
+
38
+ it('should reject invalid names', () => {
39
+ expect(nameSchema.safeParse('').success).toBe(false);
40
+ expect(nameSchema.safeParse('John123').success).toBe(false);
41
+ expect(nameSchema.safeParse('John@Doe').success).toBe(false);
42
+ });
43
+
44
+ it('should reject names that are too long', () => {
45
+ const longName = 'A'.repeat(101);
46
+ expect(nameSchema.safeParse(longName).success).toBe(false);
47
+ });
48
+ });
49
+
50
+ describe('phoneSchema', () => {
51
+ it('should validate correct phone numbers', () => {
52
+ expect(phoneSchema.safeParse('+1-555-123-4567').success).toBe(true);
53
+ expect(phoneSchema.safeParse('555-123-4567').success).toBe(true);
54
+ expect(phoneSchema.safeParse('(555) 123-4567').success).toBe(true);
55
+ });
56
+
57
+ it('should reject invalid phone numbers', () => {
58
+ expect(phoneSchema.safeParse('123').success).toBe(false);
59
+ expect(phoneSchema.safeParse('not-a-number').success).toBe(false);
60
+ expect(phoneSchema.safeParse('').success).toBe(false);
61
+ });
62
+
63
+ it('should reject phone numbers that are too short or long', () => {
64
+ expect(phoneSchema.safeParse('123').success).toBe(false);
65
+ expect(phoneSchema.safeParse('1'.repeat(21)).success).toBe(false);
66
+ });
67
+ });
68
+
69
+ describe('urlSchema', () => {
70
+ it('should validate correct URLs', () => {
71
+ expect(urlSchema.safeParse('https://example.com').success).toBe(true);
72
+ expect(urlSchema.safeParse('http://sub.domain.co.uk/path').success).toBe(true);
73
+ expect(urlSchema.safeParse('https://example.com?param=value').success).toBe(true);
74
+ });
75
+
76
+ it('should reject invalid URLs', () => {
77
+ expect(urlSchema.safeParse('not-a-url').success).toBe(false);
78
+ expect(urlSchema.safeParse('').success).toBe(false);
79
+ });
80
+
81
+ it('should reject URLs that are too long', () => {
82
+ const longUrl = 'https://example.com/' + 'a'.repeat(2048);
83
+ expect(urlSchema.safeParse(longUrl).success).toBe(false);
84
+ });
85
+ });
86
+
87
+ describe('dateSchema', () => {
88
+ it('should validate correct dates', () => {
89
+ expect(dateSchema.safeParse('2023-12-25').success).toBe(true);
90
+ expect(dateSchema.safeParse('2023-01-01').success).toBe(true);
91
+ });
92
+
93
+ it('should reject invalid dates', () => {
94
+ expect(dateSchema.safeParse('invalid-date').success).toBe(false);
95
+ expect(dateSchema.safeParse('2023/12/25').success).toBe(false);
96
+ expect(dateSchema.safeParse('12/25/2023').success).toBe(false);
97
+ expect(dateSchema.safeParse('2023-13-45').success).toBe(false);
98
+ expect(dateSchema.safeParse('').success).toBe(false);
99
+ });
100
+ });
101
+ });