@jmruthers/pace-core 0.5.3 → 0.5.5
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-3SSI644S.js} +2 -2
- package/dist/{chunk-M4RW7PIP.js → chunk-2BJFM2JC.js} +105 -81
- package/dist/chunk-2BJFM2JC.js.map +1 -0
- package/dist/{chunk-5H3C2SWM.js → chunk-RTCA5ZNK.js} +2 -2
- package/dist/components.js +2 -2
- package/dist/index.js +2 -2
- package/dist/styles/core.css +3 -0
- 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 +167 -138
- package/src/components/Header/Header.test.tsx +1 -1
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +1 -1
- 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/api.test.ts +511 -0
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -0
- package/src/rbac/hooks/useCan.test.ts +1 -1
- package/src/rbac/hooks/usePermissions.test.ts +10 -5
- package/src/rbac/hooks/useRBAC.test.ts +141 -93
- package/src/rbac/utils/__tests__/eventContext.test.ts +428 -0
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +428 -0
- package/src/styles/core.css +3 -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-3SSI644S.js.map} +0 -0
- /package/dist/{chunk-5H3C2SWM.js.map → chunk-RTCA5ZNK.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
|
+
});
|