@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,466 @@
1
+ /**
2
+ * @file SQL Injection Protection Unit Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Validation/SQLInjectionProtection/Tests
5
+ * @since 0.1.0
6
+ */
7
+
8
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
9
+ import {
10
+ detectSQLInjection,
11
+ sanitizeSearchQuery,
12
+ sanitizeFilters,
13
+ buildSafeQueryParams,
14
+ searchQuerySchema,
15
+ sqlIdentifierSchema,
16
+ orderBySchema,
17
+ limitOffsetSchema,
18
+ SafeQueryParams,
19
+ } from '../sqlInjectionProtection';
20
+
21
+ describe('SQL Injection Protection', () => {
22
+ beforeEach(() => {
23
+ vi.clearAllMocks();
24
+ });
25
+
26
+ describe('detectSQLInjection', () => {
27
+ it('should detect basic SQL injection patterns', () => {
28
+ const maliciousInputs = [
29
+ "'; DROP TABLE users; --",
30
+ "admin'; --",
31
+ "1 OR 1=1",
32
+ "UNION SELECT * FROM passwords",
33
+ "'; INSERT INTO users VALUES ('hacker', 'password'); --",
34
+ "' OR '1'='1",
35
+ "1; DELETE FROM users; --",
36
+ "'; EXEC xp_cmdshell('format C:'); --",
37
+ "' UNION ALL SELECT creditcard FROM creditcards WHERE 1=1--",
38
+ ];
39
+
40
+ maliciousInputs.forEach(input => {
41
+ const result = detectSQLInjection(input);
42
+ expect(result.isSuspicious).toBe(true);
43
+ expect(result.patterns.length).toBeGreaterThan(0);
44
+ expect(['low', 'medium', 'high', 'critical']).toContain(result.riskLevel);
45
+ });
46
+ });
47
+
48
+ it('should not detect legitimate queries', () => {
49
+ const legitimateInputs = [
50
+ "test",
51
+ "data",
52
+ "app",
53
+ "web",
54
+ "dev",
55
+ "key",
56
+ "tag",
57
+ "cat",
58
+ "xyz",
59
+ ];
60
+
61
+ legitimateInputs.forEach(input => {
62
+ const result = detectSQLInjection(input);
63
+ expect(result.isSuspicious).toBe(false);
64
+ expect(result.patterns).toHaveLength(0);
65
+ expect(result.riskLevel).toBe('low');
66
+ });
67
+ });
68
+
69
+ it('should handle case-insensitive patterns', () => {
70
+ const caseSensitiveInputs = [
71
+ "admin' OR '1'='1",
72
+ "ADMIN' or '1'='1",
73
+ "Admin' Or '1'='1",
74
+ "'; drop table users; --",
75
+ "'; DROP TABLE USERS; --",
76
+ "'; Drop Table Users; --",
77
+ ];
78
+
79
+ caseSensitiveInputs.forEach(input => {
80
+ const result = detectSQLInjection(input);
81
+ expect(result.isSuspicious).toBe(true);
82
+ expect(result.patterns.length).toBeGreaterThan(0);
83
+ });
84
+ });
85
+
86
+ it('should assign appropriate risk levels', () => {
87
+ const criticalInput = "'; DROP TABLE users; --";
88
+ const criticalResult = detectSQLInjection(criticalInput);
89
+ expect(criticalResult.riskLevel).toBe('critical');
90
+
91
+ const mediumInput = "1 OR 1=1";
92
+ const mediumResult = detectSQLInjection(mediumInput);
93
+ expect(['medium', 'high', 'critical']).toContain(mediumResult.riskLevel);
94
+ });
95
+
96
+ it('should handle empty and null inputs', () => {
97
+ expect(() => detectSQLInjection('')).not.toThrow();
98
+ expect(() => detectSQLInjection(' ')).not.toThrow();
99
+ });
100
+ });
101
+
102
+ describe('sanitizeSearchQuery', () => {
103
+ it('should remove dangerous characters', () => {
104
+ const dangerousInput = "test';\"%\\";
105
+ const sanitized = sanitizeSearchQuery(dangerousInput);
106
+
107
+ // Only these characters are removed by DANGEROUS_CHARS regex: /[';\"\\%]/g
108
+ expect(sanitized).not.toContain("'");
109
+ expect(sanitized).not.toContain(';');
110
+ expect(sanitized).not.toContain('"');
111
+ expect(sanitized).not.toContain('%');
112
+ expect(sanitized).not.toContain('\\');
113
+ expect(sanitized).toBe('test'); // All dangerous chars removed
114
+ });
115
+
116
+ it('should preserve safe content', () => {
117
+ const safeInput = "John Smith";
118
+ const sanitized = sanitizeSearchQuery(safeInput);
119
+
120
+ expect(sanitized).toBe(safeInput);
121
+ });
122
+
123
+ it('should handle various dangerous characters', () => {
124
+ const inputWithDangerousChars = "test';\"%\\";
125
+ const sanitized = sanitizeSearchQuery(inputWithDangerousChars);
126
+
127
+ expect(sanitized).not.toContain("'");
128
+ expect(sanitized).not.toContain(';');
129
+ expect(sanitized).not.toContain('"');
130
+ expect(sanitized).not.toContain('%');
131
+ expect(sanitized).not.toContain('\\');
132
+ });
133
+
134
+ it('should handle empty input', () => {
135
+ const empty = sanitizeSearchQuery('');
136
+ expect(empty).toBe('');
137
+ });
138
+ });
139
+
140
+ describe('sanitizeFilters', () => {
141
+ it('should sanitize valid filter objects', () => {
142
+ const filters = {
143
+ name: "test", // Safe string that passes validation
144
+ age: 25,
145
+ active: true,
146
+ tags: ["web", "app", "dev"], // Safe strings
147
+ };
148
+
149
+ const sanitized = sanitizeFilters(filters);
150
+
151
+ expect(sanitized.name).toBe("test");
152
+ expect(sanitized.age).toBe(25);
153
+ expect(sanitized.active).toBe(true);
154
+ expect(sanitized.tags).toEqual(["web", "app", "dev"]);
155
+ });
156
+
157
+ it('should remove invalid filter keys', () => {
158
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
159
+
160
+ const filters = {
161
+ "testfield": "data", // Safe field name and value
162
+ "'; DROP TABLE users; --": "malicious",
163
+ "appkey": 123, // Safe field name
164
+ };
165
+
166
+ const sanitized = sanitizeFilters(filters);
167
+
168
+ expect(sanitized.testfield).toBe("data");
169
+ expect(sanitized.appkey).toBe(123);
170
+ expect(sanitized["'; DROP TABLE users; --"]).toBeUndefined();
171
+ expect(consoleSpy).toHaveBeenCalled();
172
+
173
+ consoleSpy.mockRestore();
174
+ });
175
+
176
+ it('should sanitize string values in filters', () => {
177
+ const filters = {
178
+ search: "test%data", // Using % which will be removed
179
+ category: "web", // Safe category
180
+ };
181
+
182
+ const sanitized = sanitizeFilters(filters);
183
+
184
+ // The dangerous % character should be removed by sanitizeSearchQuery
185
+ expect(sanitized.search).toBe("testdata"); // % removed
186
+ expect(sanitized.category).toBe("web");
187
+ });
188
+
189
+ it('should handle array filters', () => {
190
+ const filters = {
191
+ categories: ["electronics", "books", "clothing"],
192
+ ids: [1, 2, 3, 4, 5],
193
+ mixed: ["valid", 123, "test';--"],
194
+ };
195
+
196
+ const sanitized = sanitizeFilters(filters);
197
+
198
+ expect(sanitized.categories).toEqual(["electronics", "books", "clothing"]);
199
+ expect(sanitized.ids).toEqual([1, 2, 3, 4, 5]);
200
+ expect(sanitized.mixed).toHaveLength(3);
201
+ expect(sanitized.mixed[2]).not.toContain("'");
202
+ });
203
+
204
+ it('should limit array size', () => {
205
+ const largeArray = Array.from({ length: 150 }, (_, i) => i);
206
+ const filters = {
207
+ ids: largeArray,
208
+ };
209
+
210
+ const sanitized = sanitizeFilters(filters);
211
+
212
+ expect(sanitized.ids).toHaveLength(100); // Limited to 100
213
+ });
214
+
215
+ it('should handle edge cases', () => {
216
+ const filters = {
217
+ nullValue: null,
218
+ undefinedValue: undefined,
219
+ objectValue: { nested: "value" },
220
+ functionValue: () => "test",
221
+ invalidNumber: NaN,
222
+ validNumber: 42,
223
+ };
224
+
225
+ const sanitized = sanitizeFilters(filters);
226
+
227
+ expect(sanitized.nullValue).toBeUndefined();
228
+ expect(sanitized.undefinedValue).toBeUndefined();
229
+ expect(sanitized.objectValue).toBeUndefined();
230
+ expect(sanitized.functionValue).toBeUndefined();
231
+ expect(sanitized.invalidNumber).toBeUndefined();
232
+ expect(sanitized.validNumber).toBe(42);
233
+ });
234
+ });
235
+
236
+ describe('buildSafeQueryParams', () => {
237
+ it('should build safe query parameters', () => {
238
+ const params: SafeQueryParams = {
239
+ select: "id, name, email",
240
+ filters: {
241
+ active: true,
242
+ type: "app", // Safe string
243
+ },
244
+ orderBy: "created_at",
245
+ limit: 10,
246
+ offset: 0,
247
+ search: "test", // Safe search term
248
+ };
249
+
250
+ const safe = buildSafeQueryParams(params);
251
+
252
+ expect(safe.select).toBe("id, name, email");
253
+ expect(safe.filters?.active).toBe(true);
254
+ expect(safe.filters?.type).toBe("app");
255
+ expect(safe.orderBy).toBe("created_at");
256
+ expect(safe.limit).toBe(10);
257
+ expect(safe.offset).toBe(0);
258
+ expect(safe.search).toBe("test");
259
+ });
260
+
261
+ it('should filter out invalid select fields', () => {
262
+ const params: SafeQueryParams = {
263
+ select: "id, name, '; DROP TABLE users; --, email",
264
+ };
265
+
266
+ const safe = buildSafeQueryParams(params);
267
+
268
+ expect(safe.select).toBe("id, name, email");
269
+ expect(safe.select).not.toContain("DROP");
270
+ });
271
+
272
+ it('should handle empty parameters', () => {
273
+ const safe = buildSafeQueryParams({});
274
+
275
+ expect(safe.select).toBeUndefined();
276
+ expect(safe.filters).toBeUndefined();
277
+ expect(safe.orderBy).toBeUndefined();
278
+ expect(safe.limit).toBeUndefined();
279
+ expect(safe.offset).toBeUndefined();
280
+ expect(safe.search).toBeUndefined();
281
+ });
282
+
283
+ it('should validate numeric parameters', () => {
284
+ const params: SafeQueryParams = {
285
+ limit: -5, // Should be rejected
286
+ offset: 1000, // Should be accepted
287
+ };
288
+
289
+ const safe = buildSafeQueryParams(params);
290
+
291
+ expect(safe.limit).toBeUndefined(); // Invalid negative limit
292
+ expect(safe.offset).toBe(1000); // Valid offset
293
+ });
294
+ });
295
+
296
+ describe('Schema Validations', () => {
297
+ describe('searchQuerySchema', () => {
298
+ it('should validate safe search queries', () => {
299
+ const validQueries = [
300
+ "test",
301
+ "data",
302
+ "app",
303
+ "web",
304
+ ];
305
+
306
+ validQueries.forEach(query => {
307
+ const result = searchQuerySchema.safeParse(query);
308
+ expect(result.success).toBe(true);
309
+ if (result.success) {
310
+ expect(result.data).toBe(query);
311
+ }
312
+ });
313
+ });
314
+
315
+ it('should reject malicious queries', () => {
316
+ const maliciousQueries = [
317
+ "'; DROP TABLE users; --",
318
+ "admin' OR '1'='1",
319
+ "UNION SELECT password FROM users",
320
+ ];
321
+
322
+ maliciousQueries.forEach(query => {
323
+ const result = searchQuerySchema.safeParse(query);
324
+ expect(result.success).toBe(false);
325
+ });
326
+ });
327
+
328
+ it('should reject overly long queries', () => {
329
+ const longQuery = "a".repeat(501);
330
+ const result = searchQuerySchema.safeParse(longQuery);
331
+
332
+ expect(result.success).toBe(false);
333
+ if (!result.success) {
334
+ expect(result.error.issues[0].message).toContain('too long');
335
+ }
336
+ });
337
+ });
338
+
339
+ describe('sqlIdentifierSchema', () => {
340
+ it('should validate safe SQL identifiers', () => {
341
+ const validIdentifiers = [
342
+ "user_id",
343
+ "firstName",
344
+ "created_at",
345
+ "table1",
346
+ "column_name_123",
347
+ ];
348
+
349
+ validIdentifiers.forEach(identifier => {
350
+ const result = sqlIdentifierSchema.safeParse(identifier);
351
+ expect(result.success).toBe(true);
352
+ });
353
+ });
354
+
355
+ it('should reject invalid SQL identifiers', () => {
356
+ const invalidIdentifiers = [
357
+ "'; DROP TABLE users; --",
358
+ "user id", // spaces
359
+ "123invalid", // starts with number
360
+ "user-name", // contains hyphen
361
+ "", // empty
362
+ ];
363
+
364
+ invalidIdentifiers.forEach(identifier => {
365
+ const result = sqlIdentifierSchema.safeParse(identifier);
366
+ expect(result.success).toBe(false);
367
+ });
368
+ });
369
+ });
370
+
371
+ describe('orderBySchema', () => {
372
+ it('should validate safe order by clauses', () => {
373
+ const validOrderBy = [
374
+ "created_at",
375
+ "name ASC",
376
+ "updated_at DESC",
377
+ "id",
378
+ ];
379
+
380
+ validOrderBy.forEach(orderBy => {
381
+ const result = orderBySchema.safeParse(orderBy);
382
+ expect(result.success).toBe(true);
383
+ });
384
+ });
385
+
386
+ it('should reject invalid order by clauses', () => {
387
+ const invalidOrderBy = [
388
+ "'; DROP TABLE users; --",
389
+ "name; DELETE FROM users",
390
+ "id, (SELECT password FROM users)",
391
+ ];
392
+
393
+ invalidOrderBy.forEach(orderBy => {
394
+ const result = orderBySchema.safeParse(orderBy);
395
+ expect(result.success).toBe(false);
396
+ });
397
+ });
398
+ });
399
+
400
+ describe('limitOffsetSchema', () => {
401
+ it('should validate positive numbers', () => {
402
+ const validNumbers = [1, 10, 100, 1000];
403
+
404
+ validNumbers.forEach(num => {
405
+ const result = limitOffsetSchema.safeParse(num);
406
+ expect(result.success).toBe(true);
407
+ });
408
+ });
409
+
410
+ it('should reject invalid numbers', () => {
411
+ const invalidNumbers = [-1, 1001, NaN, Infinity]; // Removed 0 (valid), changed 10001 to 1001
412
+
413
+ invalidNumbers.forEach(num => {
414
+ const result = limitOffsetSchema.safeParse(num);
415
+ expect(result.success).toBe(false);
416
+ });
417
+ });
418
+ });
419
+ });
420
+
421
+ describe('Performance and Edge Cases', () => {
422
+ it('should handle concurrent filter sanitization', async () => {
423
+ const filters = Array.from({ length: 100 }, (_, i) => ({
424
+ [`test${i}`]: `data`, // Safe field names and values
425
+ active: true,
426
+ }));
427
+
428
+ const promises = filters.map(filter => Promise.resolve(sanitizeFilters(filter)));
429
+ const results = await Promise.all(promises);
430
+
431
+ expect(results).toHaveLength(100);
432
+ results.forEach((result, index) => {
433
+ expect(result[`test${index}`]).toBe(`data`);
434
+ expect(result.active).toBe(true);
435
+ });
436
+ });
437
+
438
+ it('should handle large filter objects efficiently', () => {
439
+ const largeFilters: Record<string, any> = {};
440
+ for (let i = 0; i < 1000; i++) {
441
+ largeFilters[`test${i}`] = `data`; // Safe field names and values
442
+ }
443
+
444
+ const startTime = Date.now();
445
+ const sanitized = sanitizeFilters(largeFilters);
446
+ const endTime = Date.now();
447
+
448
+ expect(endTime - startTime).toBeLessThan(100); // Should complete quickly
449
+ expect(Object.keys(sanitized)).toHaveLength(1000);
450
+ });
451
+
452
+ it('should handle deeply nested objects safely', () => {
453
+ const nestedFilters = {
454
+ level1: {
455
+ level2: {
456
+ level3: "value"
457
+ }
458
+ }
459
+ };
460
+
461
+ // Should not process nested objects
462
+ const sanitized = sanitizeFilters(nestedFilters);
463
+ expect(sanitized.level1).toBeUndefined();
464
+ });
465
+ });
466
+ });