@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,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
|
+
});
|