@jmruthers/pace-core 0.5.73 → 0.5.75

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 (283) hide show
  1. package/dist/{DataTable-INW5YIFV.js → DataTable-HWZQGASI.js} +8 -8
  2. package/dist/{PublicLoadingSpinner-DLpF5bbs.d.ts → PublicLoadingSpinner-BKNBT6b6.d.ts} +2 -2
  3. package/dist/RBACService-C4udt_Zp.d.ts +528 -0
  4. package/dist/{UnifiedAuthProvider-6SYT5WFN.js → UnifiedAuthProvider-3NKDOSOK.js} +6 -4
  5. package/dist/UnifiedAuthProvider-Bj6YCf7c.d.ts +113 -0
  6. package/dist/{chunk-2PRPDH66.js → chunk-2CHATWBF.js} +5 -7
  7. package/dist/chunk-2CHATWBF.js.map +1 -0
  8. package/dist/{chunk-43C63KLH.js → chunk-2DFZ432F.js} +496 -30
  9. package/dist/chunk-2DFZ432F.js.map +1 -0
  10. package/dist/{chunk-M4UMXYNK.js → chunk-33PHABLB.js} +36 -3
  11. package/dist/chunk-33PHABLB.js.map +1 -0
  12. package/dist/chunk-5F3NDPJV.js +232 -0
  13. package/dist/chunk-5F3NDPJV.js.map +1 -0
  14. package/dist/chunk-A4FUBC7B.js +17 -0
  15. package/dist/chunk-A4FUBC7B.js.map +1 -0
  16. package/dist/{chunk-SMJZMKYN.js → chunk-A6HBIY5P.js} +2 -11
  17. package/dist/{chunk-SMJZMKYN.js.map → chunk-A6HBIY5P.js.map} +1 -1
  18. package/dist/{chunk-GBC5PC3N.js → chunk-CY3AHGO4.js} +6256 -1937
  19. package/dist/chunk-CY3AHGO4.js.map +1 -0
  20. package/dist/{chunk-BYG6OSTC.js → chunk-DAXLNIDY.js} +48 -50
  21. package/dist/chunk-DAXLNIDY.js.map +1 -0
  22. package/dist/{chunk-VKOCWWVY.js → chunk-L3RV2ALE.js} +1 -6
  23. package/dist/{chunk-VKOCWWVY.js.map → chunk-L3RV2ALE.js.map} +1 -1
  24. package/dist/chunk-LW7MMEAQ.js +59 -0
  25. package/dist/chunk-LW7MMEAQ.js.map +1 -0
  26. package/dist/{chunk-LANO5IFV.js → chunk-NTNILOBC.js} +7 -9
  27. package/dist/chunk-NTNILOBC.js.map +1 -0
  28. package/dist/chunk-PYUXFQJ3.js +11 -0
  29. package/dist/chunk-PYUXFQJ3.js.map +1 -0
  30. package/dist/chunk-URUTVZ7N.js +27 -0
  31. package/dist/chunk-URUTVZ7N.js.map +1 -0
  32. package/dist/chunk-WN6XJWOS.js +2468 -0
  33. package/dist/chunk-WN6XJWOS.js.map +1 -0
  34. package/dist/{chunk-3SP4P7NS.js → chunk-XLZ7U46Z.js} +59 -1
  35. package/dist/chunk-XLZ7U46Z.js.map +1 -0
  36. package/dist/{chunk-UC2BWIK7.js → chunk-ZTT2AXMX.js} +9 -14
  37. package/dist/chunk-ZTT2AXMX.js.map +1 -0
  38. package/dist/components.d.ts +4 -5
  39. package/dist/components.js +32 -39
  40. package/dist/components.js.map +1 -1
  41. package/dist/hooks.d.ts +3 -3
  42. package/dist/hooks.js +9 -8
  43. package/dist/hooks.js.map +1 -1
  44. package/dist/index.d.ts +156 -10
  45. package/dist/index.js +188 -93
  46. package/dist/index.js.map +1 -1
  47. package/dist/{organisation-t-vvQC3g.d.ts → organisation-BtshODVF.d.ts} +4 -3
  48. package/dist/providers.d.ts +27 -38
  49. package/dist/providers.js +33 -23
  50. package/dist/rbac/index.d.ts +61 -5
  51. package/dist/rbac/index.js +13 -14
  52. package/dist/styles/index.js +2 -2
  53. package/dist/theming/runtime.js +1 -3
  54. package/dist/types.d.ts +3 -3
  55. package/dist/types.js +1 -1
  56. package/dist/types.js.map +1 -1
  57. package/dist/{unified-CMPjE_fv.d.ts → unified-CM7T0aTK.d.ts} +1 -1
  58. package/dist/useInactivityTracker-MRUU55XI.js +10 -0
  59. package/dist/useInactivityTracker-MRUU55XI.js.map +1 -0
  60. package/dist/{usePublicRouteParams-Ua1Vz-HG.d.ts → usePublicRouteParams-B-CumWRc.d.ts} +3 -3
  61. package/dist/utils.js +7 -9
  62. package/dist/utils.js.map +1 -1
  63. package/dist/validation.d.ts +1 -1
  64. package/docs/api/classes/ColumnFactory.md +1 -1
  65. package/docs/api/classes/ErrorBoundary.md +1 -1
  66. package/docs/api/classes/InvalidScopeError.md +1 -1
  67. package/docs/api/classes/MissingUserContextError.md +1 -1
  68. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  69. package/docs/api/classes/PermissionDeniedError.md +1 -1
  70. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  71. package/docs/api/classes/RBACAuditManager.md +1 -1
  72. package/docs/api/classes/RBACCache.md +1 -1
  73. package/docs/api/classes/RBACEngine.md +1 -1
  74. package/docs/api/classes/RBACError.md +1 -1
  75. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  76. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  77. package/docs/api/classes/StorageUtils.md +1 -1
  78. package/docs/api/enums/FileCategory.md +1 -1
  79. package/docs/api/interfaces/AggregateConfig.md +1 -1
  80. package/docs/api/interfaces/ButtonProps.md +3 -3
  81. package/docs/api/interfaces/CardProps.md +2 -2
  82. package/docs/api/interfaces/ColorPalette.md +1 -1
  83. package/docs/api/interfaces/ColorShade.md +1 -1
  84. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  85. package/docs/api/interfaces/DataTableAction.md +1 -1
  86. package/docs/api/interfaces/DataTableColumn.md +1 -1
  87. package/docs/api/interfaces/DataTableProps.md +1 -1
  88. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  89. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  90. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  91. package/docs/api/interfaces/EventLogoProps.md +2 -2
  92. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  93. package/docs/api/interfaces/FileMetadata.md +1 -1
  94. package/docs/api/interfaces/FileReference.md +1 -1
  95. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  96. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  97. package/docs/api/interfaces/FileUploadProps.md +1 -1
  98. package/docs/api/interfaces/FooterProps.md +1 -1
  99. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  100. package/docs/api/interfaces/InputProps.md +2 -2
  101. package/docs/api/interfaces/LabelProps.md +1 -1
  102. package/docs/api/interfaces/LoginFormProps.md +1 -1
  103. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  104. package/docs/api/interfaces/NavigationContextType.md +1 -1
  105. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  106. package/docs/api/interfaces/NavigationItem.md +1 -1
  107. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  108. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  109. package/docs/api/interfaces/Organisation.md +1 -1
  110. package/docs/api/interfaces/OrganisationContextType.md +28 -17
  111. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  112. package/docs/api/interfaces/OrganisationProviderProps.md +2 -2
  113. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  114. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  115. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  116. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  117. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  118. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  119. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  120. package/docs/api/interfaces/PaletteData.md +1 -1
  121. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  122. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  123. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  124. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +2 -2
  125. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  126. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  127. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  128. package/docs/api/interfaces/RBACConfig.md +1 -1
  129. package/docs/api/interfaces/RBACContextType.md +5 -11
  130. package/docs/api/interfaces/RBACLogger.md +1 -1
  131. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  132. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  133. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  134. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  135. package/docs/api/interfaces/RouteConfig.md +1 -1
  136. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  137. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  138. package/docs/api/interfaces/StorageConfig.md +1 -1
  139. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  140. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  141. package/docs/api/interfaces/StorageListOptions.md +1 -1
  142. package/docs/api/interfaces/StorageListResult.md +1 -1
  143. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  144. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  145. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  146. package/docs/api/interfaces/StyleImport.md +1 -1
  147. package/docs/api/interfaces/SwitchProps.md +1 -1
  148. package/docs/api/interfaces/ToastActionElement.md +1 -1
  149. package/docs/api/interfaces/ToastProps.md +1 -1
  150. package/docs/api/interfaces/UnifiedAuthContextType.md +524 -440
  151. package/docs/api/interfaces/UnifiedAuthProviderProps.md +14 -14
  152. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  153. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  154. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  155. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  156. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  157. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  158. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  159. package/docs/api/interfaces/UserEventAccess.md +11 -11
  160. package/docs/api/interfaces/UserMenuProps.md +1 -1
  161. package/docs/api/interfaces/UserProfile.md +1 -1
  162. package/docs/api/modules.md +179 -52
  163. package/docs/architecture/services.md +30 -32
  164. package/docs/breaking-changes.md +2 -5
  165. package/docs/implementation-guides/data-tables.md +82 -1
  166. package/docs/migration/service-architecture.md +121 -260
  167. package/docs/rbac/README-rbac-rls-integration.md +48 -38
  168. package/{src/rbac/examples → examples/RBAC}/CompleteRBACExample.tsx +3 -2
  169. package/{src/rbac/examples → examples/RBAC}/EventBasedApp.tsx +5 -4
  170. package/{src/components/examples → examples/RBAC}/PermissionExample.tsx +7 -6
  171. package/examples/RBAC/__tests__/PermissionExample.test.tsx +150 -0
  172. package/examples/RBAC/index.ts +13 -0
  173. package/examples/README.md +37 -0
  174. package/examples/index.ts +22 -0
  175. package/{src/examples → examples/public-pages}/CorrectPublicPageImplementation.tsx +1 -1
  176. package/{src/examples → examples/public-pages}/PublicEventPage.tsx +1 -1
  177. package/{src/examples → examples/public-pages}/PublicPageApp.tsx +1 -1
  178. package/{src/examples → examples/public-pages}/PublicPageUsageExample.tsx +1 -1
  179. package/examples/public-pages/__tests__/PublicPageUsageExample.test.tsx +159 -0
  180. package/examples/public-pages/index.ts +14 -0
  181. package/package.json +22 -18
  182. package/src/__tests__/TEST_GUIDE_CURSOR.md +650 -9
  183. package/src/__tests__/helpers/README.md +255 -0
  184. package/src/__tests__/helpers/index.ts +62 -0
  185. package/src/__tests__/helpers/supabaseMock.ts +27 -3
  186. package/src/__tests__/rbac/PagePermissionGuard.test.tsx +6 -8
  187. package/src/components/DataTable/components/DataTableCore.tsx +37 -3
  188. package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +55 -0
  189. package/src/components/DataTable/core/ColumnManager.ts +10 -0
  190. package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +254 -0
  191. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +193 -0
  192. package/src/components/DataTable/examples/__tests__/HierarchicalExample.test.tsx +45 -0
  193. package/src/components/DataTable/examples/__tests__/PerformanceExample.test.tsx +117 -0
  194. package/src/components/Dialog/Dialog.tsx +2 -2
  195. package/src/components/Dialog/examples/__tests__/HtmlDialogExample.test.tsx +71 -0
  196. package/src/components/Dialog/examples/__tests__/SimpleHtmlTest.test.tsx +122 -0
  197. package/src/components/EventSelector/EventSelector.tsx +1 -1
  198. package/src/components/Header/Header.test.tsx +35 -1
  199. package/src/components/Header/Header.tsx +3 -1
  200. package/src/components/OrganisationSelector/OrganisationSelector.tsx +3 -3
  201. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.rbac.test.tsx +24 -4
  202. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +3 -2
  203. package/src/components/Toast/Toast.test.tsx +1 -1
  204. package/src/components/Toast/Toast.tsx +1 -1
  205. package/src/hooks/__tests__/useFocusManagement.unit.test.ts +220 -0
  206. package/src/hooks/__tests__/useIsMobile.unit.test.ts +117 -0
  207. package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +295 -0
  208. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +29 -19
  209. package/src/hooks/__tests__/useRBAC.unit.test.ts +7 -3
  210. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +115 -19
  211. package/src/hooks/useEventTheme.test.ts +350 -0
  212. package/src/hooks/useEventTheme.ts +1 -1
  213. package/src/hooks/useEvents.ts +61 -0
  214. package/src/hooks/useOrganisationSecurity.test.ts +4 -4
  215. package/src/hooks/useOrganisationSecurity.ts +2 -2
  216. package/src/hooks/useOrganisations.ts +64 -0
  217. package/src/hooks/useSecureDataAccess.test.ts +9 -5
  218. package/src/hooks/useSecureDataAccess.ts +2 -2
  219. package/src/index.ts +18 -3
  220. package/src/providers/AuthProvider.tsx +8 -292
  221. package/src/providers/EventProvider.tsx +15 -425
  222. package/src/providers/InactivityProvider.tsx +8 -231
  223. package/src/providers/OrganisationProvider.test.simple.tsx +3 -2
  224. package/src/providers/OrganisationProvider.tsx +11 -890
  225. package/src/providers/UnifiedAuthProvider.tsx +8 -320
  226. package/src/providers/__tests__/AuthProvider.test.tsx +18 -17
  227. package/src/providers/__tests__/EventProvider.test.tsx +253 -2
  228. package/src/providers/__tests__/InactivityProvider.test-helper.tsx +65 -0
  229. package/src/providers/__tests__/InactivityProvider.test.tsx +46 -114
  230. package/src/providers/__tests__/OrganisationProvider.test.tsx +313 -3
  231. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +383 -2
  232. package/src/providers/index.ts +8 -7
  233. package/src/providers/services/EventServiceProvider.tsx +3 -0
  234. package/src/providers/services/UnifiedAuthProvider.tsx +3 -0
  235. package/src/rbac/hooks/usePermissions.test.ts +296 -0
  236. package/src/rbac/hooks/useRBAC.test.ts +9 -5
  237. package/src/rbac/hooks/useRBAC.ts +3 -3
  238. package/src/rbac/providers/__tests__/RBACProvider.integration.test.tsx +688 -0
  239. package/src/rbac/providers/__tests__/RBACProvider.test.tsx +507 -0
  240. package/src/services/AuthService.ts +19 -4
  241. package/src/services/__tests__/AuthService.test.ts +288 -0
  242. package/src/styles/core.css +2 -0
  243. package/src/types/__tests__/guards.test.ts +246 -0
  244. package/src/types/guards.ts +1 -0
  245. package/src/types/organisation.ts +3 -2
  246. package/src/validation/__tests__/sanitization.unit.test.ts +250 -0
  247. package/src/validation/__tests__/schemaUtils.unit.test.ts +451 -0
  248. package/src/validation/__tests__/user.unit.test.ts +440 -0
  249. package/dist/RBACProvider-BO4ilsQB.d.ts +0 -63
  250. package/dist/UnifiedAuthProvider-D02AMXgO.d.ts +0 -103
  251. package/dist/chunk-2PRPDH66.js.map +0 -1
  252. package/dist/chunk-3SP4P7NS.js.map +0 -1
  253. package/dist/chunk-43C63KLH.js.map +0 -1
  254. package/dist/chunk-5A4RL4BC.js +0 -5670
  255. package/dist/chunk-5A4RL4BC.js.map +0 -1
  256. package/dist/chunk-BYG6OSTC.js.map +0 -1
  257. package/dist/chunk-CDDYJCYU.js +0 -79
  258. package/dist/chunk-CDDYJCYU.js.map +0 -1
  259. package/dist/chunk-F24P24TZ.js +0 -17
  260. package/dist/chunk-F24P24TZ.js.map +0 -1
  261. package/dist/chunk-GBC5PC3N.js.map +0 -1
  262. package/dist/chunk-LANO5IFV.js.map +0 -1
  263. package/dist/chunk-M4UMXYNK.js.map +0 -1
  264. package/dist/chunk-RJNE764D.js +0 -953
  265. package/dist/chunk-RJNE764D.js.map +0 -1
  266. package/dist/chunk-UC2BWIK7.js.map +0 -1
  267. package/dist/rbac/cli/policy-manager.js +0 -278
  268. package/dist/rbac/cli/policy-manager.js.map +0 -1
  269. package/docs/api/interfaces/EventContextType.md +0 -96
  270. package/docs/api/interfaces/EventProviderProps.md +0 -19
  271. package/src/providers/OrganisationProvider.test.tsx +0 -164
  272. package/src/providers/UnifiedAuthProvider.test.tsx +0 -124
  273. package/src/providers/__tests__/AuthProvider.test.tsx.backup +0 -771
  274. package/src/providers/__tests__/EventProvider.test.tsx.backup +0 -824
  275. package/src/providers/__tests__/OrganisationProvider.test.tsx.backup +0 -820
  276. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx.backup +0 -911
  277. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx.backup2 +0 -166
  278. package/src/rbac/cli/__tests__/policy-manager.test.ts +0 -339
  279. package/src/rbac/cli/policy-manager.ts +0 -443
  280. package/dist/{DataTable-INW5YIFV.js.map → DataTable-HWZQGASI.js.map} +0 -0
  281. package/dist/{UnifiedAuthProvider-6SYT5WFN.js.map → UnifiedAuthProvider-3NKDOSOK.js.map} +0 -0
  282. package/dist/{validation-PM_iOaTI.d.ts → validation-D8VcbTzC.d.ts} +2 -2
  283. /package/src/utils/{appNameResolver.test.ts.backup → appNameResolver.test 2.ts} +0 -0
@@ -0,0 +1,250 @@
1
+ /**
2
+ * @file Sanitization Function Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Validation/__tests__
5
+ * @since 0.4.0
6
+ *
7
+ * Comprehensive tests for input sanitization functions following TEST_GUIDE_CURSOR.md
8
+ */
9
+
10
+ import { describe, it, expect } from 'vitest';
11
+ import { sanitizeEmail, sanitizeString } from '../sanitization';
12
+
13
+ describe('[unit] Sanitization Functions', () => {
14
+ describe('sanitizeEmail', () => {
15
+ it('converts email to lowercase', () => {
16
+ expect(sanitizeEmail('TEST@EXAMPLE.COM')).toBe('test@example.com');
17
+ expect(sanitizeEmail('User@Domain.Com')).toBe('user@domain.com');
18
+ });
19
+
20
+ it('trims whitespace from email', () => {
21
+ expect(sanitizeEmail(' test@example.com ')).toBe('test@example.com');
22
+ expect(sanitizeEmail('\ttest@example.com\n')).toBe('test@example.com');
23
+ });
24
+
25
+ it('handles mixed case and whitespace', () => {
26
+ expect(sanitizeEmail(' TEST@EXAMPLE.COM ')).toBe('test@example.com');
27
+ });
28
+
29
+ it('handles valid email addresses', () => {
30
+ expect(sanitizeEmail('user@example.com')).toBe('user@example.com');
31
+ expect(sanitizeEmail('user.name@example.com')).toBe('user.name@example.com');
32
+ expect(sanitizeEmail('user+tag@example.co.uk')).toBe('user+tag@example.co.uk');
33
+ });
34
+
35
+ it('handles empty string', () => {
36
+ expect(sanitizeEmail('')).toBe('');
37
+ });
38
+
39
+ it('handles null input', () => {
40
+ expect(sanitizeEmail(null as any)).toBe('');
41
+ });
42
+
43
+ it('handles undefined input', () => {
44
+ expect(sanitizeEmail(undefined as any)).toBe('');
45
+ });
46
+
47
+ it('handles non-string input', () => {
48
+ expect(sanitizeEmail(123 as any)).toBe('');
49
+ expect(sanitizeEmail({} as any)).toBe('');
50
+ expect(sanitizeEmail([] as any)).toBe('');
51
+ });
52
+
53
+ it('preserves valid email structure', () => {
54
+ const email = 'user.name+tag@example.co.uk';
55
+ expect(sanitizeEmail(email)).toBe('user.name+tag@example.co.uk');
56
+ });
57
+
58
+ it('handles email with special characters', () => {
59
+ expect(sanitizeEmail('user_name@example.com')).toBe('user_name@example.com');
60
+ expect(sanitizeEmail('user-name@example.com')).toBe('user-name@example.com');
61
+ expect(sanitizeEmail('user.name@example.com')).toBe('user.name@example.com');
62
+ });
63
+ });
64
+
65
+ describe('sanitizeString', () => {
66
+ it('removes angle brackets from input', () => {
67
+ expect(sanitizeString('test<tag>value')).toBe('testtagvalue');
68
+ expect(sanitizeString('<script>alert("xss")</script>')).toBe('scriptalert("xss")/script');
69
+ });
70
+
71
+ it('removes javascript: protocol', () => {
72
+ expect(sanitizeString('javascript:alert("xss")')).toBe('alert("xss")');
73
+ expect(sanitizeString('testJAVASCRIPT:alert(1)')).toBe('testalert(1)');
74
+ expect(sanitizeString('testJavaScript:alert(1)')).toBe('testalert(1)');
75
+ });
76
+
77
+ it('removes event handlers', () => {
78
+ expect(sanitizeString('testOnClick="alert(1)"')).toBe('test"alert(1)"');
79
+ expect(sanitizeString('testOnLoad="malicious"')).toBe('test"malicious"');
80
+ expect(sanitizeString('testOnError="bad"')).toBe('test"bad"');
81
+ expect(sanitizeString('testONEVENT="evil"')).toBe('test"evil"');
82
+ });
83
+
84
+ it('trims whitespace', () => {
85
+ expect(sanitizeString(' test ')).toBe('test');
86
+ expect(sanitizeString('\ttest\n')).toBe('test');
87
+ });
88
+
89
+ it('handles normal text safely', () => {
90
+ expect(sanitizeString('This is a normal string')).toBe('This is a normal string');
91
+ expect(sanitizeString('User name')).toBe('User name');
92
+ });
93
+
94
+ it('handles empty string', () => {
95
+ expect(sanitizeString('')).toBe('');
96
+ });
97
+
98
+ it('handles null input', () => {
99
+ expect(sanitizeString(null as any)).toBe('');
100
+ });
101
+
102
+ it('handles undefined input', () => {
103
+ expect(sanitizeString(undefined as any)).toBe('');
104
+ });
105
+
106
+ it('handles non-string input', () => {
107
+ expect(sanitizeString(123 as any)).toBe('');
108
+ expect(sanitizeString({} as any)).toBe('');
109
+ expect(sanitizeString([] as any)).toBe('');
110
+ });
111
+
112
+ it('removes multiple security threats in one string', () => {
113
+ const malicious = ' <script>javascript:alert(1)</script> onClick="bad" ';
114
+ expect(sanitizeString(malicious)).toBe('scriptalert(1)/script "bad"');
115
+ });
116
+
117
+ it('preserves valid HTML-like text that is safe', () => {
118
+ expect(sanitizeString('User &amp; Company')).toBe('User &amp; Company');
119
+ expect(sanitizeString('5 < 10')).toBe('5 10');
120
+ expect(sanitizeString('10 > 5')).toBe('10 5');
121
+ });
122
+
123
+ it('handles SQL injection attempts', () => {
124
+ expect(sanitizeString("'; DROP TABLE users; --")).toBe("'; DROP TABLE users; --");
125
+ // sanitizeString doesn't specifically target SQL, but removes dangerous patterns
126
+ // This test ensures it doesn't corrupt legitimate apostrophes
127
+ });
128
+
129
+ it('handles XSS payloads', () => {
130
+ // The function removes 'onerror=' but keeps the equals sign
131
+ expect(sanitizeString('<img src=x onerror=alert(1)>')).toBe('img src=x alert(1)');
132
+ expect(sanitizeString('<svg/onload=alert(1)>')).toBe('svg/alert(1)');
133
+ });
134
+
135
+ it('preserves special characters in safe contexts', () => {
136
+ expect(sanitizeString('User@example.com')).toBe('User@example.com');
137
+ expect(sanitizeString('$100')).toBe('$100');
138
+ expect(sanitizeString('Test%20String')).toBe('Test%20String');
139
+ });
140
+
141
+ it('handles very long strings', () => {
142
+ const longString = 'a'.repeat(10000);
143
+ expect(sanitizeString(longString)).toBe('a'.repeat(10000));
144
+ });
145
+
146
+ it('handles strings with only whitespace', () => {
147
+ expect(sanitizeString(' ')).toBe('');
148
+ expect(sanitizeString('\t\n\r')).toBe('');
149
+ });
150
+
151
+ it('removes consecutive dangerous patterns', () => {
152
+ const multipleAttacks = 'test<script>alert(1)</script><script>alert(2)</script>javascript:doEvil()';
153
+ expect(sanitizeString(multipleAttacks)).toBe('testscriptalert(1)/scriptscriptalert(2)/scriptdoEvil()');
154
+ });
155
+ });
156
+
157
+ describe('Edge Cases', () => {
158
+ it('sanitizeEmail handles unicode characters', () => {
159
+ expect(sanitizeEmail('café@example.com')).toBe('café@example.com');
160
+ expect(sanitizeEmail('test@例え.com')).toBe('test@例え.com');
161
+ });
162
+
163
+ it('sanitizeString handles unicode correctly', () => {
164
+ expect(sanitizeString('Test café')).toBe('Test café');
165
+ expect(sanitizeString('中文测试')).toBe('中文测试');
166
+ expect(sanitizeString('مرحبا')).toBe('مرحبا');
167
+ });
168
+
169
+ it('sanitizeEmail handles international domains', () => {
170
+ expect(sanitizeEmail('test@xn--example.com')).toBe('test@xn--example.com');
171
+ });
172
+
173
+ it('sanitizeString handles emoji', () => {
174
+ expect(sanitizeString('Hello 😀')).toBe('Hello 😀');
175
+ expect(sanitizeString('User <script>😀</script>')).toBe('User script😀/script');
176
+ });
177
+
178
+ it('handles mixed scripts and vulnerabilities', () => {
179
+ const mixed = ' User Name<script>alert(1)</script> ';
180
+ expect(sanitizeString(mixed)).toBe('User Namescriptalert(1)/script');
181
+ });
182
+ });
183
+
184
+ describe('Performance', () => {
185
+ it('sanitizeEmail handles large inputs efficiently', () => {
186
+ const largeEmail = 'a'.repeat(1000) + '@' + 'b'.repeat(1000) + '.com';
187
+ const start = performance.now();
188
+ const result = sanitizeEmail(largeEmail);
189
+ const end = performance.now();
190
+
191
+ expect(result).toBeDefined();
192
+ expect(end - start).toBeLessThan(100); // Should complete in under 100ms
193
+ });
194
+
195
+ it('sanitizeString handles large inputs efficiently', () => {
196
+ const largeString = 'a'.repeat(10000);
197
+ const start = performance.now();
198
+ const result = sanitizeString(largeString);
199
+ const end = performance.now();
200
+
201
+ expect(result).toBeDefined();
202
+ expect(end - start).toBeLessThan(100); // Should complete in under 100ms
203
+ });
204
+ });
205
+
206
+ describe('Security Validation', () => {
207
+ it('sanitizeString prevents basic XSS attacks', () => {
208
+ const xssAttempts = [
209
+ '<script>alert("xss")</script>',
210
+ '<img src=x onerror=alert(1)>',
211
+ '<svg/onload=alert(1)>',
212
+ '<iframe src=javascript:alert(1)>',
213
+ ];
214
+
215
+ xssAttempts.forEach(attempt => {
216
+ const sanitized = sanitizeString(attempt);
217
+ expect(sanitized).not.toContain('<script>');
218
+ expect(sanitized).not.toContain('javascript:');
219
+ expect(sanitized).not.toContain('onerror=');
220
+ expect(sanitized).not.toContain('onload=');
221
+ });
222
+ });
223
+
224
+ it('sanitizeString removes event handlers regardless of case', () => {
225
+ const cases = [
226
+ 'onClick',
227
+ 'ONCLICK',
228
+ 'onclick',
229
+ 'onClick',
230
+ 'ONCLICK',
231
+ ];
232
+
233
+ cases.forEach(eventHandler => {
234
+ const test = `test ${eventHandler}="malicious" test`;
235
+ const sanitized = sanitizeString(test);
236
+ expect(sanitized).not.toContain(`${eventHandler}=`);
237
+ });
238
+ });
239
+
240
+ it('sanitizeEmail does not introduce security vulnerabilities', () => {
241
+ const safe = 'user@example.com';
242
+ const sanitized = sanitizeEmail(safe);
243
+ expect(sanitized).toBe('user@example.com');
244
+ expect(sanitized).not.toContain('<');
245
+ expect(sanitized).not.toContain('>');
246
+ expect(sanitized).not.toContain('javascript:');
247
+ });
248
+ });
249
+ });
250
+
@@ -0,0 +1,451 @@
1
+ /**
2
+ * @file Schema Utility Functions Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Validation/__tests__
5
+ * @since 0.4.0
6
+ *
7
+ * Comprehensive tests for schema utility functions following TEST_GUIDE_CURSOR.md
8
+ */
9
+
10
+ import { describe, it, expect } from 'vitest';
11
+ import { z } from 'zod';
12
+ import { pickSchema, combineSchemas } from '../schemaUtils';
13
+
14
+ describe('[unit] Schema Utility Functions', () => {
15
+ describe('pickSchema', () => {
16
+ it('creates a subset schema with selected keys', () => {
17
+ const originalSchema = z.object({
18
+ name: z.string(),
19
+ email: z.string().email(),
20
+ age: z.number(),
21
+ });
22
+
23
+ const pickedSchema = pickSchema(originalSchema, ['name', 'email']);
24
+ const result = pickedSchema.safeParse({
25
+ name: 'John Doe',
26
+ email: 'john@example.com',
27
+ age: 30, // This should be ignored
28
+ });
29
+
30
+ expect(result.success).toBe(true);
31
+ if (result.success) {
32
+ expect(result.data).toEqual({
33
+ name: 'John Doe',
34
+ email: 'john@example.com',
35
+ });
36
+ }
37
+ });
38
+
39
+ it('validates only picked fields', () => {
40
+ const originalSchema = z.object({
41
+ name: z.string().min(3),
42
+ email: z.string().email(),
43
+ phone: z.string().optional(),
44
+ });
45
+
46
+ const pickedSchema = pickSchema(originalSchema, ['name']);
47
+ const validResult = pickedSchema.safeParse({ name: 'John Doe' });
48
+ expect(validResult.success).toBe(true);
49
+
50
+ const invalidResult = pickedSchema.safeParse({ name: 'Jo' }); // Too short
51
+ expect(invalidResult.success).toBe(false);
52
+ });
53
+
54
+ it('handles single field selection', () => {
55
+ const originalSchema = z.object({
56
+ id: z.string(),
57
+ name: z.string(),
58
+ email: z.string(),
59
+ });
60
+
61
+ const pickedSchema = pickSchema(originalSchema, ['id']);
62
+ const result = pickedSchema.safeParse({ id: '123' });
63
+
64
+ expect(result.success).toBe(true);
65
+ if (result.success) {
66
+ expect(result.data).toEqual({ id: '123' });
67
+ }
68
+ });
69
+
70
+ it('preserves validation rules', () => {
71
+ const originalSchema = z.object({
72
+ email: z.string().email(),
73
+ password: z.string().min(8),
74
+ });
75
+
76
+ const pickedSchema = pickSchema(originalSchema, ['email']);
77
+
78
+ const validResult = pickedSchema.safeParse({ email: 'test@example.com' });
79
+ expect(validResult.success).toBe(true);
80
+
81
+ const invalidResult = pickedSchema.safeParse({ email: 'invalid-email' });
82
+ expect(invalidResult.success).toBe(false);
83
+ });
84
+
85
+ it('handles empty key array', () => {
86
+ const originalSchema = z.object({
87
+ name: z.string(),
88
+ email: z.string(),
89
+ });
90
+
91
+ const pickedSchema = pickSchema(originalSchema, []);
92
+ const result = pickedSchema.safeParse({ name: 'Test' });
93
+
94
+ expect(result.success).toBe(true);
95
+ if (result.success) {
96
+ expect(result.data).toEqual({});
97
+ }
98
+ });
99
+
100
+ it('handles key array with invalid keys', () => {
101
+ const originalSchema = z.object({
102
+ name: z.string(),
103
+ email: z.string(),
104
+ });
105
+
106
+ // Picking keys that don't exist should result in empty schema
107
+ const pickedSchema = pickSchema(originalSchema, ['invalidKey' as any]);
108
+ const result = pickedSchema.safeParse({ name: 'Test' });
109
+
110
+ expect(result.success).toBe(true);
111
+ if (result.success) {
112
+ expect(Object.keys(result.data)).toHaveLength(0);
113
+ }
114
+ });
115
+
116
+ it('handles complex nested validation', () => {
117
+ const originalSchema = z.object({
118
+ user: z.object({
119
+ name: z.string(),
120
+ email: z.string().email(),
121
+ }),
122
+ settings: z.object({
123
+ theme: z.string(),
124
+ }),
125
+ });
126
+
127
+ const pickedSchema = pickSchema(originalSchema, ['user']);
128
+ const result = pickedSchema.safeParse({
129
+ user: {
130
+ name: 'John',
131
+ email: 'john@example.com',
132
+ },
133
+ });
134
+
135
+ expect(result.success).toBe(true);
136
+ });
137
+ });
138
+
139
+ describe('combineSchemas', () => {
140
+ it('merges multiple schemas into one', () => {
141
+ const schema1 = z.object({
142
+ name: z.string(),
143
+ email: z.string(),
144
+ });
145
+
146
+ const schema2 = z.object({
147
+ age: z.number(),
148
+ phone: z.string(),
149
+ });
150
+
151
+ const combined = combineSchemas([schema1, schema2]);
152
+ const result = combined.safeParse({
153
+ name: 'John',
154
+ email: 'john@example.com',
155
+ age: 30,
156
+ phone: '1234567890',
157
+ });
158
+
159
+ expect(result.success).toBe(true);
160
+ if (result.success) {
161
+ expect(result.data).toEqual({
162
+ name: 'John',
163
+ email: 'john@example.com',
164
+ age: 30,
165
+ phone: '1234567890',
166
+ });
167
+ }
168
+ });
169
+
170
+ it('validates all fields from combined schemas', () => {
171
+ const schema1 = z.object({
172
+ email: z.string().email(),
173
+ });
174
+
175
+ const schema2 = z.object({
176
+ password: z.string().min(8),
177
+ });
178
+
179
+ const combined = combineSchemas([schema1, schema2]);
180
+
181
+ const validResult = combined.safeParse({
182
+ email: 'test@example.com',
183
+ password: 'SecurePass123',
184
+ });
185
+ expect(validResult.success).toBe(true);
186
+
187
+ const invalidResult = combined.safeParse({
188
+ email: 'invalid',
189
+ password: 'short',
190
+ });
191
+ expect(invalidResult.success).toBe(false);
192
+ });
193
+
194
+ it('handles overlapping field names (latter overwrites)', () => {
195
+ const schema1 = z.object({
196
+ name: z.string().min(1),
197
+ email: z.string(),
198
+ });
199
+
200
+ const schema2 = z.object({
201
+ email: z.string().email(),
202
+ age: z.number(),
203
+ });
204
+
205
+ const combined = combineSchemas([schema1, schema2]);
206
+ const result = combined.safeParse({
207
+ name: 'John',
208
+ email: 'john@example.com',
209
+ age: 30,
210
+ });
211
+
212
+ expect(result.success).toBe(true);
213
+ // email validation should be the stricter one from schema2
214
+ if (result.success) {
215
+ expect(result.data.email).toBe('john@example.com');
216
+ }
217
+ });
218
+
219
+ it('handles empty schema array', () => {
220
+ const combined = combineSchemas([]);
221
+ const result = combined.safeParse({});
222
+
223
+ expect(result.success).toBe(true);
224
+ if (result.success) {
225
+ expect(Object.keys(result.data)).toHaveLength(0);
226
+ }
227
+ });
228
+
229
+ it('handles single schema', () => {
230
+ const schema1 = z.object({
231
+ name: z.string(),
232
+ email: z.string().email(),
233
+ });
234
+
235
+ const combined = combineSchemas([schema1]);
236
+ const result = combined.safeParse({
237
+ name: 'John',
238
+ email: 'john@example.com',
239
+ });
240
+
241
+ expect(result.success).toBe(true);
242
+ if (result.success) {
243
+ expect(result.data).toEqual({
244
+ name: 'John',
245
+ email: 'john@example.com',
246
+ });
247
+ }
248
+ });
249
+
250
+ it('combines three or more schemas', () => {
251
+ const schema1 = z.object({ name: z.string() });
252
+ const schema2 = z.object({ email: z.string() });
253
+ const schema3 = z.object({ phone: z.string() });
254
+ const schema4 = z.object({ age: z.number() });
255
+
256
+ const combined = combineSchemas([schema1, schema2, schema3, schema4]);
257
+ const result = combined.safeParse({
258
+ name: 'John',
259
+ email: 'john@example.com',
260
+ phone: '1234567890',
261
+ age: 30,
262
+ });
263
+
264
+ expect(result.success).toBe(true);
265
+ if (result.success) {
266
+ expect(Object.keys(result.data)).toHaveLength(4);
267
+ }
268
+ });
269
+
270
+ it('preserves optional fields from original schemas', () => {
271
+ const schema1 = z.object({
272
+ name: z.string(),
273
+ email: z.string().optional(),
274
+ });
275
+
276
+ const schema2 = z.object({
277
+ phone: z.string().optional(),
278
+ });
279
+
280
+ const combined = combineSchemas([schema1, schema2]);
281
+
282
+ const resultWithAll = combined.safeParse({
283
+ name: 'John',
284
+ email: 'john@example.com',
285
+ phone: '1234567890',
286
+ });
287
+ expect(resultWithAll.success).toBe(true);
288
+
289
+ const resultWithoutOptional = combined.safeParse({
290
+ name: 'John',
291
+ });
292
+ expect(resultWithoutOptional.success).toBe(true);
293
+ });
294
+
295
+ it('handles complex nested schema merging', () => {
296
+ const schema1 = z.object({
297
+ user: z.object({
298
+ name: z.string(),
299
+ }),
300
+ });
301
+
302
+ const schema2 = z.object({
303
+ user: z.object({
304
+ email: z.string().email(),
305
+ }),
306
+ });
307
+
308
+ const combined = combineSchemas([schema1, schema2]);
309
+ const result = combined.safeParse({
310
+ user: {
311
+ name: 'John',
312
+ email: 'john@example.com',
313
+ },
314
+ });
315
+
316
+ expect(result.success).toBe(true);
317
+ });
318
+ });
319
+
320
+ describe('Edge Cases', () => {
321
+ it('pickSchema handles schema with no keys picked', () => {
322
+ const schema = z.object({
323
+ name: z.string(),
324
+ email: z.string(),
325
+ });
326
+
327
+ const picked = pickSchema(schema, []);
328
+ const result = picked.safeParse({ name: 'Test', email: 'test@example.com' });
329
+
330
+ expect(result.success).toBe(true);
331
+ if (result.success) {
332
+ expect(Object.keys(result.data)).toHaveLength(0);
333
+ }
334
+ });
335
+
336
+ it('combineSchemas handles schemas with conflicting validation rules', () => {
337
+ const schema1 = z.object({
338
+ value: z.string().min(5),
339
+ });
340
+
341
+ const schema2 = z.object({
342
+ value: z.string().max(10),
343
+ });
344
+
345
+ const combined = combineSchemas([schema1, schema2]);
346
+
347
+ // When schemas are merged, the last one's validation typically wins
348
+ // But we can test that it validates at least something
349
+ const longResult = combined.safeParse({ value: '123456789012' });
350
+ expect(longResult.success).toBe(false); // Too long
351
+
352
+ const validResult = combined.safeParse({ value: '12345' });
353
+ expect(validResult.success).toBe(true);
354
+
355
+ // Very short strings may still fail depending on merge order
356
+ const veryShortResult = combined.safeParse({ value: '123' });
357
+ // The result depends on merge order - just check it's defined
358
+ expect(veryShortResult.success).toBeDefined();
359
+ });
360
+
361
+ it('handles schemas with multiple field types', () => {
362
+ const schema1 = z.object({
363
+ string: z.string(),
364
+ number: z.number(),
365
+ boolean: z.boolean(),
366
+ });
367
+
368
+ const schema2 = z.object({
369
+ array: z.array(z.string()),
370
+ object: z.object({ nested: z.string() }),
371
+ });
372
+
373
+ const combined = combineSchemas([schema1, schema2]);
374
+ const result = combined.safeParse({
375
+ string: 'test',
376
+ number: 42,
377
+ boolean: true,
378
+ array: ['a', 'b'],
379
+ object: { nested: 'value' },
380
+ });
381
+
382
+ expect(result.success).toBe(true);
383
+ });
384
+ });
385
+
386
+ describe('Integration', () => {
387
+ it('combines picked schemas', () => {
388
+ const baseSchema = z.object({
389
+ name: z.string(),
390
+ email: z.string().email(),
391
+ age: z.number(),
392
+ phone: z.string(),
393
+ });
394
+
395
+ const userInfo = pickSchema(baseSchema, ['name', 'email']);
396
+ const contactInfo = pickSchema(baseSchema, ['email', 'phone']);
397
+
398
+ const combined = combineSchemas([userInfo, contactInfo]);
399
+ const result = combined.safeParse({
400
+ name: 'John',
401
+ email: 'john@example.com',
402
+ phone: '1234567890',
403
+ });
404
+
405
+ expect(result.success).toBe(true);
406
+ if (result.success) {
407
+ expect(result.data).toEqual({
408
+ name: 'John',
409
+ email: 'john@example.com',
410
+ phone: '1234567890',
411
+ });
412
+ }
413
+ });
414
+
415
+ it('handles real-world user registration scenario', () => {
416
+ const personalInfo = z.object({
417
+ firstName: z.string(),
418
+ lastName: z.string(),
419
+ dateOfBirth: z.string(),
420
+ });
421
+
422
+ const contactInfo = z.object({
423
+ email: z.string().email(),
424
+ phone: z.string(),
425
+ });
426
+
427
+ const preferences = z.object({
428
+ newsletter: z.boolean(),
429
+ notifications: z.boolean(),
430
+ });
431
+
432
+ const registrationSchema = combineSchemas([personalInfo, contactInfo, preferences]);
433
+
434
+ const result = registrationSchema.safeParse({
435
+ firstName: 'John',
436
+ lastName: 'Doe',
437
+ dateOfBirth: '1990-01-01',
438
+ email: 'john@example.com',
439
+ phone: '1234567890',
440
+ newsletter: true,
441
+ notifications: false,
442
+ });
443
+
444
+ expect(result.success).toBe(true);
445
+ if (result.success) {
446
+ expect(Object.keys(result.data)).toHaveLength(7);
447
+ }
448
+ });
449
+ });
450
+ });
451
+