@latte-macchiat-io/latte-payload 1.0.1

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 (253) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.env.example +29 -0
  3. package/.github/workflows/ci.yml +160 -0
  4. package/.github/workflows/publish.yml +126 -0
  5. package/.nvmrc +1 -0
  6. package/.prettierignore +2 -0
  7. package/.prettierrc +11 -0
  8. package/CHANGELOG.md +87 -0
  9. package/README.md +364 -0
  10. package/TESTING_AND_DOCUMENTATION_SETUP.md +348 -0
  11. package/dist/access/adminAccessOnly.d.ts +25 -0
  12. package/dist/access/adminAccessOnly.d.ts.map +1 -0
  13. package/dist/access/adminAccessOnly.js +11 -0
  14. package/dist/access/adminAccessOnly.js.map +1 -0
  15. package/dist/access/admins.d.ts +72 -0
  16. package/dist/access/admins.d.ts.map +1 -0
  17. package/dist/access/admins.js +76 -0
  18. package/dist/access/admins.js.map +1 -0
  19. package/dist/access/anyone.d.ts +35 -0
  20. package/dist/access/anyone.d.ts.map +1 -0
  21. package/dist/access/anyone.js +34 -0
  22. package/dist/access/anyone.js.map +1 -0
  23. package/dist/access/authenticated.d.ts +63 -0
  24. package/dist/access/authenticated.d.ts.map +1 -0
  25. package/dist/access/authenticated.js +68 -0
  26. package/dist/access/authenticated.js.map +1 -0
  27. package/dist/access/authenticatedAccessOnly.d.ts +13 -0
  28. package/dist/access/authenticatedAccessOnly.d.ts.map +1 -0
  29. package/dist/access/authenticatedAccessOnly.js +11 -0
  30. package/dist/access/authenticatedAccessOnly.js.map +1 -0
  31. package/dist/access/index.d.ts +7 -0
  32. package/dist/access/index.d.ts.map +1 -0
  33. package/dist/access/index.js +7 -0
  34. package/dist/access/index.js.map +1 -0
  35. package/dist/access/publicReadAuthenticatedAccess.d.ts +13 -0
  36. package/dist/access/publicReadAuthenticatedAccess.d.ts.map +1 -0
  37. package/dist/access/publicReadAuthenticatedAccess.js +12 -0
  38. package/dist/access/publicReadAuthenticatedAccess.js.map +1 -0
  39. package/dist/collections/ContactMessages/hooks/sendEmailNotification.d.ts +31 -0
  40. package/dist/collections/ContactMessages/hooks/sendEmailNotification.d.ts.map +1 -0
  41. package/dist/collections/ContactMessages/hooks/sendEmailNotification.js +29 -0
  42. package/dist/collections/ContactMessages/hooks/sendEmailNotification.js.map +1 -0
  43. package/dist/collections/ContactMessages/index.d.ts +27 -0
  44. package/dist/collections/ContactMessages/index.d.ts.map +1 -0
  45. package/dist/collections/ContactMessages/index.js +81 -0
  46. package/dist/collections/ContactMessages/index.js.map +1 -0
  47. package/dist/collections/EmailTemplates/index.d.ts +26 -0
  48. package/dist/collections/EmailTemplates/index.d.ts.map +1 -0
  49. package/dist/collections/EmailTemplates/index.js +74 -0
  50. package/dist/collections/EmailTemplates/index.js.map +1 -0
  51. package/dist/collections/Media/index.d.ts +3 -0
  52. package/dist/collections/Media/index.d.ts.map +1 -0
  53. package/dist/collections/Media/index.js +22 -0
  54. package/dist/collections/Media/index.js.map +1 -0
  55. package/dist/collections/QueuedEmails/components/HtmlViewer.d.ts +3 -0
  56. package/dist/collections/QueuedEmails/components/HtmlViewer.d.ts.map +1 -0
  57. package/dist/collections/QueuedEmails/components/HtmlViewer.js +11 -0
  58. package/dist/collections/QueuedEmails/components/HtmlViewer.js.map +1 -0
  59. package/dist/collections/QueuedEmails/components/RetryEmailButtons.d.ts +2 -0
  60. package/dist/collections/QueuedEmails/components/RetryEmailButtons.d.ts.map +1 -0
  61. package/dist/collections/QueuedEmails/components/RetryEmailButtons.js +79 -0
  62. package/dist/collections/QueuedEmails/components/RetryEmailButtons.js.map +1 -0
  63. package/dist/collections/QueuedEmails/index.d.ts +3 -0
  64. package/dist/collections/QueuedEmails/index.d.ts.map +1 -0
  65. package/dist/collections/QueuedEmails/index.js +245 -0
  66. package/dist/collections/QueuedEmails/index.js.map +1 -0
  67. package/dist/collections/Users/auth-emails/forgot-password-email.d.ts +51 -0
  68. package/dist/collections/Users/auth-emails/forgot-password-email.d.ts.map +1 -0
  69. package/dist/collections/Users/auth-emails/forgot-password-email.js +90 -0
  70. package/dist/collections/Users/auth-emails/forgot-password-email.js.map +1 -0
  71. package/dist/collections/Users/auth-emails/verify-email.d.ts +51 -0
  72. package/dist/collections/Users/auth-emails/verify-email.d.ts.map +1 -0
  73. package/dist/collections/Users/auth-emails/verify-email.js +80 -0
  74. package/dist/collections/Users/auth-emails/verify-email.js.map +1 -0
  75. package/dist/collections/Users/hooks/ensureFirstUserIsAdmin.d.ts +3 -0
  76. package/dist/collections/Users/hooks/ensureFirstUserIsAdmin.d.ts.map +1 -0
  77. package/dist/collections/Users/hooks/ensureFirstUserIsAdmin.js +19 -0
  78. package/dist/collections/Users/hooks/ensureFirstUserIsAdmin.js.map +1 -0
  79. package/dist/collections/Users/index.d.ts +76 -0
  80. package/dist/collections/Users/index.d.ts.map +1 -0
  81. package/dist/collections/Users/index.js +116 -0
  82. package/dist/collections/Users/index.js.map +1 -0
  83. package/dist/collections/index.d.ts +10 -0
  84. package/dist/collections/index.d.ts.map +1 -0
  85. package/dist/collections/index.js +14 -0
  86. package/dist/collections/index.js.map +1 -0
  87. package/dist/components/index.d.ts +3 -0
  88. package/dist/components/index.d.ts.map +1 -0
  89. package/dist/components/index.js +4 -0
  90. package/dist/components/index.js.map +1 -0
  91. package/dist/forms/states.d.ts +8 -0
  92. package/dist/forms/states.d.ts.map +1 -0
  93. package/dist/forms/states.js +16 -0
  94. package/dist/forms/states.js.map +1 -0
  95. package/dist/forms/translate-errors.d.ts +8 -0
  96. package/dist/forms/translate-errors.d.ts.map +1 -0
  97. package/dist/forms/translate-errors.js +11 -0
  98. package/dist/forms/translate-errors.js.map +1 -0
  99. package/dist/forms/validators.d.ts +10 -0
  100. package/dist/forms/validators.d.ts.map +1 -0
  101. package/dist/forms/validators.js +23 -0
  102. package/dist/forms/validators.js.map +1 -0
  103. package/dist/globals/PrivacyPolicy/hooks/revalidate-cache.d.ts +2 -0
  104. package/dist/globals/PrivacyPolicy/hooks/revalidate-cache.d.ts.map +1 -0
  105. package/dist/globals/PrivacyPolicy/hooks/revalidate-cache.js +5 -0
  106. package/dist/globals/PrivacyPolicy/hooks/revalidate-cache.js.map +1 -0
  107. package/dist/globals/PrivacyPolicy/index.d.ts +3 -0
  108. package/dist/globals/PrivacyPolicy/index.d.ts.map +1 -0
  109. package/dist/globals/PrivacyPolicy/index.js +31 -0
  110. package/dist/globals/PrivacyPolicy/index.js.map +1 -0
  111. package/dist/globals/TermsOfUse/hooks/revalidate-cache.d.ts +2 -0
  112. package/dist/globals/TermsOfUse/hooks/revalidate-cache.d.ts.map +1 -0
  113. package/dist/globals/TermsOfUse/hooks/revalidate-cache.js +5 -0
  114. package/dist/globals/TermsOfUse/hooks/revalidate-cache.js.map +1 -0
  115. package/dist/globals/TermsOfUse/index.d.ts +3 -0
  116. package/dist/globals/TermsOfUse/index.d.ts.map +1 -0
  117. package/dist/globals/TermsOfUse/index.js +31 -0
  118. package/dist/globals/TermsOfUse/index.js.map +1 -0
  119. package/dist/globals/index.d.ts +3 -0
  120. package/dist/globals/index.d.ts.map +1 -0
  121. package/dist/globals/index.js +3 -0
  122. package/dist/globals/index.js.map +1 -0
  123. package/dist/index.d.ts +13 -0
  124. package/dist/index.d.ts.map +1 -0
  125. package/dist/index.js +20 -0
  126. package/dist/index.js.map +1 -0
  127. package/dist/tasks/index.d.ts +2 -0
  128. package/dist/tasks/index.d.ts.map +1 -0
  129. package/dist/tasks/index.js +2 -0
  130. package/dist/tasks/index.js.map +1 -0
  131. package/dist/tasks/process-email-queue.d.ts +46 -0
  132. package/dist/tasks/process-email-queue.d.ts.map +1 -0
  133. package/dist/tasks/process-email-queue.js +199 -0
  134. package/dist/tasks/process-email-queue.js.map +1 -0
  135. package/dist/types/index.d.ts +14 -0
  136. package/dist/types/index.d.ts.map +1 -0
  137. package/dist/types/index.js +2 -0
  138. package/dist/types/index.js.map +1 -0
  139. package/dist/types/slug.d.ts +2 -0
  140. package/dist/types/slug.d.ts.map +1 -0
  141. package/dist/types/slug.js +11 -0
  142. package/dist/types/slug.js.map +1 -0
  143. package/dist/utils/database-dates.d.ts +3 -0
  144. package/dist/utils/database-dates.d.ts.map +1 -0
  145. package/dist/utils/database-dates.js +6 -0
  146. package/dist/utils/database-dates.js.map +1 -0
  147. package/dist/utils/email/generate-email-html.d.ts +23 -0
  148. package/dist/utils/email/generate-email-html.d.ts.map +1 -0
  149. package/dist/utils/email/generate-email-html.js +42 -0
  150. package/dist/utils/email/generate-email-html.js.map +1 -0
  151. package/dist/utils/email/get-email-template.d.ts +8 -0
  152. package/dist/utils/email/get-email-template.d.ts.map +1 -0
  153. package/dist/utils/email/get-email-template.js +14 -0
  154. package/dist/utils/email/get-email-template.js.map +1 -0
  155. package/dist/utils/email/index.d.ts +4 -0
  156. package/dist/utils/email/index.d.ts.map +1 -0
  157. package/dist/utils/email/index.js +4 -0
  158. package/dist/utils/email/index.js.map +1 -0
  159. package/dist/utils/email/queue-email.d.ts +29 -0
  160. package/dist/utils/email/queue-email.d.ts.map +1 -0
  161. package/dist/utils/email/queue-email.js +79 -0
  162. package/dist/utils/email/queue-email.js.map +1 -0
  163. package/dist/utils/get-global.d.ts +6 -0
  164. package/dist/utils/get-global.d.ts.map +1 -0
  165. package/dist/utils/get-global.js +20 -0
  166. package/dist/utils/get-global.js.map +1 -0
  167. package/dist/utils/id-from-payload.d.ts +4 -0
  168. package/dist/utils/id-from-payload.d.ts.map +1 -0
  169. package/dist/utils/id-from-payload.js +10 -0
  170. package/dist/utils/id-from-payload.js.map +1 -0
  171. package/dist/utils/index.d.ts +4 -0
  172. package/dist/utils/index.d.ts.map +1 -0
  173. package/dist/utils/index.js +4 -0
  174. package/dist/utils/index.js.map +1 -0
  175. package/dist/utils/migrations.d.ts +2 -0
  176. package/dist/utils/migrations.d.ts.map +1 -0
  177. package/dist/utils/migrations.js +18 -0
  178. package/dist/utils/migrations.js.map +1 -0
  179. package/dist/utils/payload-client.d.ts +10 -0
  180. package/dist/utils/payload-client.d.ts.map +1 -0
  181. package/dist/utils/payload-client.js +14 -0
  182. package/dist/utils/payload-client.js.map +1 -0
  183. package/dist/utils/slugify.d.ts +8 -0
  184. package/dist/utils/slugify.d.ts.map +1 -0
  185. package/dist/utils/slugify.js +15 -0
  186. package/dist/utils/slugify.js.map +1 -0
  187. package/eslint.config.mjs +90 -0
  188. package/package.json +139 -0
  189. package/pnpm-workspace.yaml +4 -0
  190. package/src/access/adminAccessOnly.ts +13 -0
  191. package/src/access/admins.ts +78 -0
  192. package/src/access/anyone.ts +35 -0
  193. package/src/access/authenticated.ts +70 -0
  194. package/src/access/authenticatedAccessOnly.ts +13 -0
  195. package/src/access/index.ts +6 -0
  196. package/src/access/publicReadAuthenticatedAccess.ts +14 -0
  197. package/src/collections/ContactMessages/hooks/sendEmailNotification.ts +58 -0
  198. package/src/collections/ContactMessages/index.ts +100 -0
  199. package/src/collections/EmailTemplates/index.ts +89 -0
  200. package/src/collections/Media/index.ts +24 -0
  201. package/src/collections/QueuedEmails/components/HtmlViewer.tsx +16 -0
  202. package/src/collections/QueuedEmails/components/RetryEmailButtons.tsx +115 -0
  203. package/src/collections/QueuedEmails/index.ts +246 -0
  204. package/src/collections/Users/auth-emails/forgot-password-email.ts +135 -0
  205. package/src/collections/Users/auth-emails/verify-email.ts +123 -0
  206. package/src/collections/Users/hooks/ensureFirstUserIsAdmin.ts +22 -0
  207. package/src/collections/Users/index.ts +201 -0
  208. package/src/collections/index.ts +23 -0
  209. package/src/components/index.ts +3 -0
  210. package/src/forms/states.ts +23 -0
  211. package/src/forms/translate-errors.ts +13 -0
  212. package/src/forms/validators.ts +33 -0
  213. package/src/globals/PrivacyPolicy/hooks/revalidate-cache.ts +5 -0
  214. package/src/globals/PrivacyPolicy/index.ts +33 -0
  215. package/src/globals/TermsOfUse/hooks/revalidate-cache.ts +5 -0
  216. package/src/globals/TermsOfUse/index.ts +33 -0
  217. package/src/globals/index.ts +2 -0
  218. package/src/index.ts +26 -0
  219. package/src/tasks/index.ts +7 -0
  220. package/src/tasks/process-email-queue.ts +261 -0
  221. package/src/types/index.ts +15 -0
  222. package/src/types/slug.ts +11 -0
  223. package/src/utils/database-dates.ts +6 -0
  224. package/src/utils/email/generate-email-html.ts +63 -0
  225. package/src/utils/email/get-email-template.ts +18 -0
  226. package/src/utils/email/index.ts +3 -0
  227. package/src/utils/email/queue-email.ts +109 -0
  228. package/src/utils/get-global.ts +25 -0
  229. package/src/utils/id-from-payload.ts +11 -0
  230. package/src/utils/index.ts +3 -0
  231. package/src/utils/migrations.ts +18 -0
  232. package/src/utils/payload-client.ts +16 -0
  233. package/src/utils/slugify.ts +21 -0
  234. package/tests/fixtures/email-template.html +58 -0
  235. package/tests/fixtures/sample-data.ts +56 -0
  236. package/tests/helpers/create-test-user.ts +37 -0
  237. package/tests/helpers/init-payload.ts +59 -0
  238. package/tests/setup.integration.ts +9 -0
  239. package/tests/setup.ts +4 -0
  240. package/tests/unit/access/adminAccessOnly.spec.ts +117 -0
  241. package/tests/unit/access/admins.spec.ts +68 -0
  242. package/tests/unit/access/anyone.spec.ts +28 -0
  243. package/tests/unit/access/authenticated.spec.ts +53 -0
  244. package/tests/unit/access/authenticatedAccessOnly.spec.ts +112 -0
  245. package/tests/unit/access/publicReadAuthenticatedAccess.spec.ts +112 -0
  246. package/tests/unit/forms/validators.spec.ts +348 -0
  247. package/tests/unit/utils/database-dates.spec.ts +97 -0
  248. package/tests/unit/utils/id-from-payload.spec.ts +142 -0
  249. package/tests/unit/utils/slugify.spec.ts +185 -0
  250. package/tsconfig.json +31 -0
  251. package/typedoc.json +40 -0
  252. package/vitest.config.ts +31 -0
  253. package/vitest.integration.config.ts +27 -0
@@ -0,0 +1,348 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { requiredCheckbox, requiredEmailString, requiredPasswordString, requiredString, validHoneyPot } from '@/forms/validators';
3
+
4
+ describe('form validators', () => {
5
+ describe('requiredString', () => {
6
+ const validator = requiredString('This field is required');
7
+
8
+ it('should accept valid non-empty strings', () => {
9
+ const result = validator.safeParse('hello');
10
+ expect(result.success).toBe(true);
11
+ if (result.success) {
12
+ expect(result.data).toBe('hello');
13
+ }
14
+ });
15
+
16
+ it('should trim whitespace', () => {
17
+ const result = validator.safeParse(' hello ');
18
+ expect(result.success).toBe(true);
19
+ if (result.success) {
20
+ expect(result.data).toBe('hello');
21
+ }
22
+ });
23
+
24
+ it('should reject empty strings', () => {
25
+ const result = validator.safeParse('');
26
+ expect(result.success).toBe(false);
27
+ if (!result.success) {
28
+ expect(result.error.issues[0].message).toBe('This field is required');
29
+ }
30
+ });
31
+
32
+ it('should reject whitespace-only strings', () => {
33
+ const result = validator.safeParse(' ');
34
+ expect(result.success).toBe(false);
35
+ if (!result.success) {
36
+ expect(result.error.issues[0].message).toBe('This field is required');
37
+ }
38
+ });
39
+
40
+ it('should reject non-string values', () => {
41
+ const result = validator.safeParse(123);
42
+ expect(result.success).toBe(false);
43
+ });
44
+
45
+ it('should accept strings with numbers', () => {
46
+ const result = validator.safeParse('hello123');
47
+ expect(result.success).toBe(true);
48
+ });
49
+
50
+ it('should accept strings with special characters', () => {
51
+ const result = validator.safeParse('hello@world!');
52
+ expect(result.success).toBe(true);
53
+ });
54
+
55
+ it('should use custom error message', () => {
56
+ const customValidator = requiredString('Custom error');
57
+ const result = customValidator.safeParse('');
58
+ expect(result.success).toBe(false);
59
+ if (!result.success) {
60
+ expect(result.error.issues[0].message).toBe('Custom error');
61
+ }
62
+ });
63
+ });
64
+
65
+ describe('requiredPasswordString', () => {
66
+ const validator = requiredPasswordString(8, 'Password is required', 'Password must be at least 8 characters');
67
+
68
+ it('should accept valid passwords meeting minimum length', () => {
69
+ const result = validator.safeParse('password123');
70
+ expect(result.success).toBe(true);
71
+ if (result.success) {
72
+ expect(result.data).toBe('password123');
73
+ }
74
+ });
75
+
76
+ it('should accept passwords exactly at minimum length', () => {
77
+ const result = validator.safeParse('12345678');
78
+ expect(result.success).toBe(true);
79
+ if (result.success) {
80
+ expect(result.data).toBe('12345678');
81
+ }
82
+ });
83
+
84
+ it('should reject passwords shorter than minimum length', () => {
85
+ const result = validator.safeParse('short');
86
+ expect(result.success).toBe(false);
87
+ if (!result.success) {
88
+ expect(result.error.issues[0].message).toBe('Password must be at least 8 characters');
89
+ }
90
+ });
91
+
92
+ it('should reject empty passwords', () => {
93
+ const result = validator.safeParse('');
94
+ expect(result.success).toBe(false);
95
+ if (!result.success) {
96
+ expect(result.error.issues[0].message).toBe('Password is required');
97
+ }
98
+ });
99
+
100
+ it('should trim whitespace before checking length', () => {
101
+ const result = validator.safeParse(' 12345678 ');
102
+ expect(result.success).toBe(true);
103
+ if (result.success) {
104
+ expect(result.data).toBe('12345678');
105
+ }
106
+ });
107
+
108
+ it('should reject whitespace-only passwords', () => {
109
+ const result = validator.safeParse(' ');
110
+ expect(result.success).toBe(false);
111
+ });
112
+
113
+ it('should handle different minimum lengths', () => {
114
+ const validator12 = requiredPasswordString(12, 'Required', 'Min 12 chars');
115
+
116
+ const resultShort = validator12.safeParse('password123');
117
+ expect(resultShort.success).toBe(false);
118
+
119
+ const resultValid = validator12.safeParse('password1234');
120
+ expect(resultValid.success).toBe(true);
121
+ });
122
+
123
+ it('should accept long passwords', () => {
124
+ const result = validator.safeParse('a'.repeat(100));
125
+ expect(result.success).toBe(true);
126
+ });
127
+
128
+ it('should accept passwords with special characters', () => {
129
+ const result = validator.safeParse('P@ssw0rd!');
130
+ expect(result.success).toBe(true);
131
+ });
132
+ });
133
+
134
+ describe('requiredEmailString', () => {
135
+ const validator = requiredEmailString({
136
+ required: 'Email is required',
137
+ email: 'Invalid email address',
138
+ });
139
+
140
+ it('should accept valid email addresses', () => {
141
+ const validEmails = ['test@example.com', 'user.name@example.com', 'user+tag@example.co.uk', 'test123@test-domain.com'];
142
+
143
+ validEmails.forEach((email) => {
144
+ const result = validator.safeParse(email);
145
+ expect(result.success).toBe(true);
146
+ });
147
+ });
148
+
149
+ it('should trim whitespace', () => {
150
+ const result = validator.safeParse(' test@example.com ');
151
+ expect(result.success).toBe(true);
152
+ if (result.success) {
153
+ expect(result.data).toBe('test@example.com');
154
+ }
155
+ });
156
+
157
+ it('should reject empty strings', () => {
158
+ const result = validator.safeParse('');
159
+ expect(result.success).toBe(false);
160
+ if (!result.success) {
161
+ expect(result.error.issues[0].message).toBe('Email is required');
162
+ }
163
+ });
164
+
165
+ it('should reject invalid email formats', () => {
166
+ const invalidEmails = ['notanemail', 'missing@domain', '@example.com', 'test@', 'test @example.com', 'test..double@example.com'];
167
+
168
+ invalidEmails.forEach((email) => {
169
+ const result = validator.safeParse(email);
170
+ expect(result.success).toBe(false);
171
+ if (!result.success) {
172
+ expect(result.error.issues[0].message).toBe('Invalid email address');
173
+ }
174
+ });
175
+ });
176
+
177
+ it('should reject whitespace-only strings', () => {
178
+ const result = validator.safeParse(' ');
179
+ expect(result.success).toBe(false);
180
+ if (!result.success) {
181
+ expect(result.error.issues[0].message).toBe('Email is required');
182
+ }
183
+ });
184
+
185
+ it('should use custom error messages', () => {
186
+ const customValidator = requiredEmailString({
187
+ required: 'Custom required message',
188
+ email: 'Custom email message',
189
+ });
190
+
191
+ const emptyResult = customValidator.safeParse('');
192
+ expect(emptyResult.success).toBe(false);
193
+ if (!emptyResult.success) {
194
+ expect(emptyResult.error.issues[0].message).toBe('Custom required message');
195
+ }
196
+
197
+ const invalidResult = customValidator.safeParse('invalid');
198
+ expect(invalidResult.success).toBe(false);
199
+ if (!invalidResult.success) {
200
+ expect(invalidResult.error.issues[0].message).toBe('Custom email message');
201
+ }
202
+ });
203
+
204
+ it('should handle edge case email formats', () => {
205
+ // These should be valid
206
+ const edgeCases = ['test_underscore@example.com', 'test-hyphen@example.com', 'a@b.co'];
207
+
208
+ edgeCases.forEach((email) => {
209
+ const result = validator.safeParse(email);
210
+ expect(result.success).toBe(true);
211
+ });
212
+ });
213
+ });
214
+
215
+ describe('validHoneyPot', () => {
216
+ const validator = validHoneyPot('Bot detected');
217
+
218
+ it('should accept empty strings (legitimate users)', () => {
219
+ const result = validator.safeParse('');
220
+ expect(result.success).toBe(true);
221
+ if (result.success) {
222
+ expect(result.data).toBe('');
223
+ }
224
+ });
225
+
226
+ it('should reject non-empty strings (bots)', () => {
227
+ const result = validator.safeParse('I am a bot');
228
+ expect(result.success).toBe(false);
229
+ // Note: Zod refine uses generic "Invalid input" message by default
230
+ });
231
+
232
+ it('should reject single character', () => {
233
+ const result = validator.safeParse('x');
234
+ expect(result.success).toBe(false);
235
+ });
236
+
237
+ it('should reject whitespace', () => {
238
+ const result = validator.safeParse(' ');
239
+ expect(result.success).toBe(false);
240
+ });
241
+
242
+ it('should reject numbers', () => {
243
+ const result = validator.safeParse('123');
244
+ expect(result.success).toBe(false);
245
+ });
246
+
247
+ it('should use custom error message', () => {
248
+ const customValidator = validHoneyPot('Custom bot message');
249
+ const result = customValidator.safeParse('filled');
250
+ expect(result.success).toBe(false);
251
+ // Zod refine validation failed - message is set but overridden
252
+ });
253
+
254
+ it('should accept empty string consistently', () => {
255
+ // Multiple tests to ensure honeypot works as expected
256
+ expect(validator.safeParse('').success).toBe(true);
257
+ expect(validator.safeParse('anything').success).toBe(false);
258
+ });
259
+ });
260
+
261
+ describe('requiredCheckbox', () => {
262
+ const validator = requiredCheckbox('You must accept the terms');
263
+
264
+ it('should accept true', () => {
265
+ const result = validator.safeParse(true);
266
+ expect(result.success).toBe(true);
267
+ if (result.success) {
268
+ expect(result.data).toBe(true);
269
+ }
270
+ });
271
+
272
+ it('should reject false', () => {
273
+ const result = validator.safeParse(false);
274
+ expect(result.success).toBe(false);
275
+ // Zod refine uses generic "Invalid input" message
276
+ });
277
+
278
+ it('should reject non-boolean values', () => {
279
+ const invalidValues = [1, 0, 'true', 'false', null, undefined, {}, []];
280
+
281
+ invalidValues.forEach((value) => {
282
+ const result = validator.safeParse(value);
283
+ expect(result.success).toBe(false);
284
+ });
285
+ });
286
+
287
+ it('should use custom error message', () => {
288
+ const customValidator = requiredCheckbox('Custom checkbox message');
289
+ const result = customValidator.safeParse(false);
290
+ expect(result.success).toBe(false);
291
+ // Zod refine validation - message set via errorMap but refine overrides
292
+ });
293
+
294
+ it('should only accept boolean true', () => {
295
+ // Truthy values should not work
296
+ expect(validator.safeParse(1 as any).success).toBe(false);
297
+ expect(validator.safeParse('true' as any).success).toBe(false);
298
+ expect(validator.safeParse(true).success).toBe(true);
299
+ });
300
+ });
301
+
302
+ describe('validators integration', () => {
303
+ it('should work together in a form schema', () => {
304
+ // Simulate a typical form validation
305
+ const formData = {
306
+ name: 'John Doe',
307
+ email: 'john@example.com',
308
+ password: 'SecurePassword123',
309
+ honeypot: '',
310
+ terms: true,
311
+ };
312
+
313
+ const nameResult = requiredString('Name required').safeParse(formData.name);
314
+ const emailResult = requiredEmailString({ required: 'Email required', email: 'Invalid email' }).safeParse(formData.email);
315
+ const passwordResult = requiredPasswordString(8, 'Password required', 'Min 8 chars').safeParse(formData.password);
316
+ const honeypotResult = validHoneyPot('Bot detected').safeParse(formData.honeypot);
317
+ const termsResult = requiredCheckbox('Accept terms').safeParse(formData.terms);
318
+
319
+ expect(nameResult.success).toBe(true);
320
+ expect(emailResult.success).toBe(true);
321
+ expect(passwordResult.success).toBe(true);
322
+ expect(honeypotResult.success).toBe(true);
323
+ expect(termsResult.success).toBe(true);
324
+ });
325
+
326
+ it('should catch all validation errors in invalid form', () => {
327
+ const invalidFormData = {
328
+ name: '',
329
+ email: 'invalid',
330
+ password: 'short',
331
+ honeypot: 'bot-filled',
332
+ terms: false,
333
+ };
334
+
335
+ const nameResult = requiredString('Name required').safeParse(invalidFormData.name);
336
+ const emailResult = requiredEmailString({ required: 'Email required', email: 'Invalid email' }).safeParse(invalidFormData.email);
337
+ const passwordResult = requiredPasswordString(8, 'Password required', 'Min 8 chars').safeParse(invalidFormData.password);
338
+ const honeypotResult = validHoneyPot('Bot detected').safeParse(invalidFormData.honeypot);
339
+ const termsResult = requiredCheckbox('Accept terms').safeParse(invalidFormData.terms);
340
+
341
+ expect(nameResult.success).toBe(false);
342
+ expect(emailResult.success).toBe(false);
343
+ expect(passwordResult.success).toBe(false);
344
+ expect(honeypotResult.success).toBe(false);
345
+ expect(termsResult.success).toBe(false);
346
+ });
347
+ });
348
+ });
@@ -0,0 +1,97 @@
1
+ import { TZDate } from '@date-fns/tz';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { toDatabaseTZDate } from '@/utils/database-dates';
4
+
5
+ describe('database-dates utility', () => {
6
+ describe('toDatabaseTZDate', () => {
7
+ describe('string input', () => {
8
+ it('should return the same string when given a string', () => {
9
+ const dateString = '2024-01-15T10:30:00.000Z';
10
+ expect(toDatabaseTZDate(dateString)).toBe(dateString);
11
+ });
12
+
13
+ it('should handle empty string', () => {
14
+ expect(toDatabaseTZDate('')).toBe('');
15
+ });
16
+
17
+ it('should handle ISO date string', () => {
18
+ const isoDate = '2024-12-25T00:00:00.000Z';
19
+ expect(toDatabaseTZDate(isoDate)).toBe(isoDate);
20
+ });
21
+
22
+ it('should handle non-ISO date string (passthrough)', () => {
23
+ const dateStr = '2024-01-15';
24
+ expect(toDatabaseTZDate(dateStr)).toBe(dateStr);
25
+ });
26
+ });
27
+
28
+ describe('TZDate input', () => {
29
+ it('should convert TZDate to ISO string', () => {
30
+ const tzDate = new TZDate(2024, 0, 15, 'UTC'); // January 15, 2024
31
+ const result = toDatabaseTZDate(tzDate);
32
+ expect(typeof result).toBe('string');
33
+ // TZDate preserves timezone info in ISO format (can be +00:00 or Z)
34
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})$/);
35
+ });
36
+
37
+ it('should handle TZDate with timezone', () => {
38
+ const tzDate = new TZDate(2024, 0, 15, 'America/New_York');
39
+ const result = toDatabaseTZDate(tzDate);
40
+ expect(typeof result).toBe('string');
41
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})$/);
42
+ });
43
+
44
+ it('should handle TZDate with time components', () => {
45
+ const tzDate = new TZDate(2024, 0, 15, 10, 30, 45, 'UTC'); // 10:30:45
46
+ const result = toDatabaseTZDate(tzDate);
47
+ expect(result).toContain('2024-01-15T10:30:45');
48
+ });
49
+
50
+ it('should convert different timezones to UTC ISO string', () => {
51
+ const tzDateUTC = new TZDate(2024, 0, 15, 12, 0, 0, 'UTC');
52
+ const tzDateNY = new TZDate(2024, 0, 15, 12, 0, 0, 'America/New_York');
53
+ const tzDateTokyo = new TZDate(2024, 0, 15, 12, 0, 0, 'Asia/Tokyo');
54
+
55
+ const resultUTC = toDatabaseTZDate(tzDateUTC);
56
+ const resultNY = toDatabaseTZDate(tzDateNY);
57
+ const resultTokyo = toDatabaseTZDate(tzDateTokyo);
58
+
59
+ expect(resultUTC).toContain('T12:00:00');
60
+ expect(resultNY).not.toBe(resultUTC); // Different timezone offsets
61
+ expect(resultTokyo).not.toBe(resultUTC);
62
+ });
63
+ });
64
+
65
+ describe('consistency', () => {
66
+ it('should produce valid ISO 8601 format', () => {
67
+ const tzDate = new TZDate(2024, 11, 25, 'UTC'); // December 25, 2024
68
+ const result = toDatabaseTZDate(tzDate);
69
+
70
+ // Valid ISO 8601 format (with timezone offset)
71
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})$/);
72
+
73
+ // Can be parsed back to Date
74
+ const parsed = new Date(result);
75
+ expect(parsed instanceof Date).toBe(true);
76
+ expect(isNaN(parsed.getTime())).toBe(false);
77
+ });
78
+
79
+ it('should be idempotent for strings', () => {
80
+ const dateString = '2024-01-15T10:30:00.000Z';
81
+ const result1 = toDatabaseTZDate(dateString);
82
+ const result2 = toDatabaseTZDate(result1);
83
+ expect(result1).toBe(result2);
84
+ });
85
+ });
86
+
87
+ describe('type safety', () => {
88
+ it('should handle TZDate | string union type correctly', () => {
89
+ const input1: TZDate | string = '2024-01-15T00:00:00.000Z';
90
+ const input2: TZDate | string = new TZDate(2024, 0, 15, 'UTC');
91
+
92
+ expect(typeof toDatabaseTZDate(input1)).toBe('string');
93
+ expect(typeof toDatabaseTZDate(input2)).toBe('string');
94
+ });
95
+ });
96
+ });
97
+ });
@@ -0,0 +1,142 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getId } from '@/utils/id-from-payload';
3
+
4
+ describe('id-from-payload utility', () => {
5
+ describe('getId', () => {
6
+ describe('number input', () => {
7
+ it('should return the number when given a number', () => {
8
+ expect(getId(42)).toBe(42);
9
+ });
10
+
11
+ it('should return 0 when given 0', () => {
12
+ expect(getId(0)).toBe(0);
13
+ });
14
+
15
+ it('should handle negative numbers', () => {
16
+ expect(getId(-1)).toBe(-1);
17
+ });
18
+
19
+ it('should handle large numbers', () => {
20
+ expect(getId(999999999)).toBe(999999999);
21
+ });
22
+
23
+ it('should handle decimal numbers', () => {
24
+ expect(getId(3.14)).toBe(3.14);
25
+ });
26
+ });
27
+
28
+ describe('object input with id property', () => {
29
+ it('should extract id from object', () => {
30
+ expect(getId({ id: 42 })).toBe(42);
31
+ });
32
+
33
+ it('should extract 0 from object', () => {
34
+ expect(getId({ id: 0 })).toBe(0);
35
+ });
36
+
37
+ it('should handle negative id in object', () => {
38
+ expect(getId({ id: -1 })).toBe(-1);
39
+ });
40
+
41
+ it('should extract id from object with other properties', () => {
42
+ expect(getId({ id: 123, name: 'test', email: 'test@example.com' })).toBe(123);
43
+ });
44
+
45
+ it('should handle object with only id property', () => {
46
+ expect(getId({ id: 456 })).toBe(456);
47
+ });
48
+ });
49
+
50
+ describe('error cases', () => {
51
+ it('should throw error for null', () => {
52
+ expect(() => getId(null as any)).toThrow('Invalid ID or object');
53
+ });
54
+
55
+ it('should throw error for undefined', () => {
56
+ expect(() => getId(undefined as any)).toThrow('Invalid ID or object');
57
+ });
58
+
59
+ it('should throw error for string', () => {
60
+ expect(() => getId('123' as any)).toThrow('Invalid ID or object');
61
+ });
62
+
63
+ it('should throw error for boolean', () => {
64
+ expect(() => getId(true as any)).toThrow('Invalid ID or object');
65
+ });
66
+
67
+ it('should handle array (returns undefined id property)', () => {
68
+ // Arrays are objects, so they access the id property which doesn't exist
69
+ const result = getId([1, 2, 3] as any);
70
+ expect(result).toBeUndefined();
71
+ });
72
+
73
+ it('should handle object without id property (returns undefined)', () => {
74
+ const result = getId({ name: 'test' } as any);
75
+ expect(result).toBeUndefined();
76
+ });
77
+
78
+ it('should handle empty object (returns undefined)', () => {
79
+ const result = getId({} as any);
80
+ expect(result).toBeUndefined();
81
+ });
82
+
83
+ it('should throw error for function', () => {
84
+ expect(() => getId((() => 42) as any)).toThrow('Invalid ID or object');
85
+ });
86
+ });
87
+
88
+ describe('type coercion', () => {
89
+ it('should handle object with numeric id', () => {
90
+ expect(getId({ id: 100 })).toBe(100);
91
+ });
92
+
93
+ it('should not coerce string id to number', () => {
94
+ const obj = { id: '123' as any };
95
+ expect(getId(obj)).toBe('123');
96
+ });
97
+ });
98
+
99
+ describe('union type handling', () => {
100
+ it('should correctly handle number | { id: number } union type', () => {
101
+ const input1: number | { id: number } = 42;
102
+ const input2: number | { id: number } = { id: 42 };
103
+
104
+ expect(getId(input1)).toBe(42);
105
+ expect(getId(input2)).toBe(42);
106
+ });
107
+
108
+ it('should work with different object shapes', () => {
109
+ interface User {
110
+ id: number;
111
+ name: string;
112
+ }
113
+
114
+ const user: User = { id: 1, name: 'John' };
115
+ expect(getId(user)).toBe(1);
116
+ });
117
+ });
118
+
119
+ describe('edge cases', () => {
120
+ it('should handle NaN as number input', () => {
121
+ expect(getId(NaN)).toBe(NaN);
122
+ expect(Number.isNaN(getId(NaN))).toBe(true);
123
+ });
124
+
125
+ it('should handle Infinity as number input', () => {
126
+ expect(getId(Infinity)).toBe(Infinity);
127
+ });
128
+
129
+ it('should handle negative Infinity as number input', () => {
130
+ expect(getId(-Infinity)).toBe(-Infinity);
131
+ });
132
+
133
+ it('should handle object with NaN id', () => {
134
+ expect(getId({ id: NaN })).toBe(NaN);
135
+ });
136
+
137
+ it('should handle object with Infinity id', () => {
138
+ expect(getId({ id: Infinity })).toBe(Infinity);
139
+ });
140
+ });
141
+ });
142
+ });