@htlkg/data 0.0.23 → 0.0.25

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.
@@ -0,0 +1,975 @@
1
+ /**
2
+ * Contact Validation Schema Tests
3
+ *
4
+ * Comprehensive tests for all Contact Zod validation schemas,
5
+ * field validators, and helper functions.
6
+ */
7
+
8
+ import { describe, it, expect } from "vitest";
9
+ import {
10
+ // Field-level validators
11
+ emailSchema,
12
+ optionalEmailSchema,
13
+ phoneSchema,
14
+ optionalPhoneSchema,
15
+ firstNameSchema,
16
+ lastNameSchema,
17
+ optionalFirstNameSchema,
18
+ localeSchema,
19
+ optionalLocaleSchema,
20
+ isoDateStringSchema,
21
+ optionalIsoDateStringSchema,
22
+ tagSchema,
23
+ tagsArraySchema,
24
+ brandIdSchema,
25
+ // Custom field schemas
26
+ contactPreferencesSchema,
27
+ customFieldDefinitionSchema,
28
+ createCustomFieldValueSchema,
29
+ // Complete entity schemas
30
+ createContactSchema,
31
+ updateContactSchema,
32
+ mergeContactsSchema,
33
+ searchContactSchema,
34
+ bulkImportContactSchema,
35
+ // Helper functions
36
+ validateEmail,
37
+ validatePhone,
38
+ validateContactName,
39
+ validateCreateContact,
40
+ validateUpdateContact,
41
+ formatValidationErrors,
42
+ getValidationErrorDetails,
43
+ } from "./contact.schemas";
44
+
45
+ // ============================================
46
+ // Email Validation Tests
47
+ // ============================================
48
+
49
+ describe("Email Validation", () => {
50
+ describe("emailSchema", () => {
51
+ it("should accept valid email addresses", () => {
52
+ const validEmails = [
53
+ "test@example.com",
54
+ "user.name@domain.org",
55
+ "user+tag@subdomain.domain.com",
56
+ "firstname.lastname@company.co.uk",
57
+ ];
58
+
59
+ validEmails.forEach((email) => {
60
+ const result = emailSchema.safeParse(email);
61
+ expect(result.success).toBe(true);
62
+ });
63
+ });
64
+
65
+ it("should transform email to lowercase", () => {
66
+ const result = emailSchema.safeParse("TEST@EXAMPLE.COM");
67
+ expect(result.success).toBe(true);
68
+ if (result.success) {
69
+ expect(result.data).toBe("test@example.com");
70
+ }
71
+ });
72
+
73
+ it("should trim whitespace from email", () => {
74
+ const result = emailSchema.safeParse(" test@example.com ");
75
+ expect(result.success).toBe(true);
76
+ if (result.success) {
77
+ expect(result.data).toBe("test@example.com");
78
+ }
79
+ });
80
+
81
+ it("should reject invalid email formats", () => {
82
+ const invalidEmails = [
83
+ "invalid",
84
+ "@example.com",
85
+ "user@",
86
+ "user@.com",
87
+ "user name@example.com",
88
+ "",
89
+ ];
90
+
91
+ invalidEmails.forEach((email) => {
92
+ const result = emailSchema.safeParse(email);
93
+ expect(result.success).toBe(false);
94
+ });
95
+ });
96
+
97
+ it("should reject emails exceeding 254 characters", () => {
98
+ const longEmail = "a".repeat(250) + "@example.com";
99
+ const result = emailSchema.safeParse(longEmail);
100
+ expect(result.success).toBe(false);
101
+ });
102
+ });
103
+
104
+ describe("optionalEmailSchema", () => {
105
+ it("should accept undefined", () => {
106
+ const result = optionalEmailSchema.safeParse(undefined);
107
+ expect(result.success).toBe(true);
108
+ });
109
+
110
+ it("should validate email when provided", () => {
111
+ const result = optionalEmailSchema.safeParse("test@example.com");
112
+ expect(result.success).toBe(true);
113
+ });
114
+
115
+ it("should reject invalid email when provided", () => {
116
+ const result = optionalEmailSchema.safeParse("invalid-email");
117
+ expect(result.success).toBe(false);
118
+ });
119
+ });
120
+
121
+ describe("validateEmail helper", () => {
122
+ it("should return success for valid email", () => {
123
+ const result = validateEmail("user@example.com");
124
+ expect(result.success).toBe(true);
125
+ });
126
+
127
+ it("should return error for invalid email", () => {
128
+ const result = validateEmail("invalid");
129
+ expect(result.success).toBe(false);
130
+ });
131
+ });
132
+ });
133
+
134
+ // ============================================
135
+ // Phone Validation Tests
136
+ // ============================================
137
+
138
+ describe("Phone Validation", () => {
139
+ describe("phoneSchema", () => {
140
+ it("should accept valid phone formats", () => {
141
+ const validPhones = [
142
+ "+14155552671",
143
+ "+1 415-555-2671",
144
+ "(415) 555-2671",
145
+ "415-555-2671",
146
+ "4155552671",
147
+ "+44 20 7946 0958",
148
+ "+49 30 12345678",
149
+ ];
150
+
151
+ validPhones.forEach((phone) => {
152
+ const result = phoneSchema.safeParse(phone);
153
+ expect(result.success, `Expected ${phone} to be valid`).toBe(true);
154
+ });
155
+ });
156
+
157
+ it("should reject phones shorter than 7 characters", () => {
158
+ const result = phoneSchema.safeParse("12345");
159
+ expect(result.success).toBe(false);
160
+ });
161
+
162
+ it("should reject phones longer than 20 characters", () => {
163
+ const result = phoneSchema.safeParse("+1 234 567 890 123 456 789");
164
+ expect(result.success).toBe(false);
165
+ });
166
+
167
+ it("should reject invalid phone formats", () => {
168
+ const invalidPhones = ["abc", "phone: 123", "123"];
169
+
170
+ invalidPhones.forEach((phone) => {
171
+ const result = phoneSchema.safeParse(phone);
172
+ expect(result.success).toBe(false);
173
+ });
174
+ });
175
+
176
+ it("should trim whitespace", () => {
177
+ const result = phoneSchema.safeParse(" +14155552671 ");
178
+ expect(result.success).toBe(true);
179
+ if (result.success) {
180
+ expect(result.data).toBe("+14155552671");
181
+ }
182
+ });
183
+ });
184
+
185
+ describe("optionalPhoneSchema", () => {
186
+ it("should accept undefined", () => {
187
+ const result = optionalPhoneSchema.safeParse(undefined);
188
+ expect(result.success).toBe(true);
189
+ });
190
+
191
+ it("should accept empty string", () => {
192
+ const result = optionalPhoneSchema.safeParse("");
193
+ expect(result.success).toBe(true);
194
+ });
195
+
196
+ it("should validate phone when provided", () => {
197
+ const result = optionalPhoneSchema.safeParse("+14155552671");
198
+ expect(result.success).toBe(true);
199
+ });
200
+ });
201
+
202
+ describe("validatePhone helper", () => {
203
+ it("should return success for valid phone", () => {
204
+ const result = validatePhone("+14155552671");
205
+ expect(result.success).toBe(true);
206
+ });
207
+
208
+ it("should return error for invalid phone", () => {
209
+ const result = validatePhone("abc");
210
+ expect(result.success).toBe(false);
211
+ });
212
+ });
213
+ });
214
+
215
+ // ============================================
216
+ // Name Validation Tests
217
+ // ============================================
218
+
219
+ describe("Name Validation", () => {
220
+ describe("firstNameSchema", () => {
221
+ it("should accept valid first names", () => {
222
+ const validNames = [
223
+ "John",
224
+ "María",
225
+ "Jean-Pierre",
226
+ "O'Connor",
227
+ "李",
228
+ "محمد",
229
+ "Müller",
230
+ "Dr. Smith",
231
+ ];
232
+
233
+ validNames.forEach((name) => {
234
+ const result = firstNameSchema.safeParse(name);
235
+ expect(result.success, `Expected "${name}" to be valid`).toBe(true);
236
+ });
237
+ });
238
+
239
+ it("should reject empty first name", () => {
240
+ const result = firstNameSchema.safeParse("");
241
+ expect(result.success).toBe(false);
242
+ });
243
+
244
+ it("should reject first name exceeding 100 characters", () => {
245
+ const longName = "A".repeat(101);
246
+ const result = firstNameSchema.safeParse(longName);
247
+ expect(result.success).toBe(false);
248
+ });
249
+
250
+ it("should reject names with invalid characters", () => {
251
+ const invalidNames = ["John123", "Test@Name", "Name!"];
252
+
253
+ invalidNames.forEach((name) => {
254
+ const result = firstNameSchema.safeParse(name);
255
+ expect(result.success).toBe(false);
256
+ });
257
+ });
258
+
259
+ it("should trim whitespace", () => {
260
+ const result = firstNameSchema.safeParse(" John ");
261
+ expect(result.success).toBe(true);
262
+ if (result.success) {
263
+ expect(result.data).toBe("John");
264
+ }
265
+ });
266
+ });
267
+
268
+ describe("lastNameSchema", () => {
269
+ it("should accept valid last names", () => {
270
+ const validNames = ["Doe", "García", "van der Berg", "McDonald's"];
271
+
272
+ validNames.forEach((name) => {
273
+ const result = lastNameSchema.safeParse(name);
274
+ expect(result.success, `Expected "${name}" to be valid`).toBe(true);
275
+ });
276
+ });
277
+
278
+ it("should reject empty last name", () => {
279
+ const result = lastNameSchema.safeParse("");
280
+ expect(result.success).toBe(false);
281
+ });
282
+ });
283
+
284
+ describe("optionalFirstNameSchema", () => {
285
+ it("should accept undefined", () => {
286
+ const result = optionalFirstNameSchema.safeParse(undefined);
287
+ expect(result.success).toBe(true);
288
+ });
289
+
290
+ it("should reject empty string when provided", () => {
291
+ const result = optionalFirstNameSchema.safeParse("");
292
+ expect(result.success).toBe(false);
293
+ });
294
+ });
295
+
296
+ describe("validateContactName helper", () => {
297
+ it("should validate both names successfully", () => {
298
+ const result = validateContactName("John", "Doe");
299
+ expect(result.isValid).toBe(true);
300
+ expect(result.firstName.success).toBe(true);
301
+ expect(result.lastName.success).toBe(true);
302
+ });
303
+
304
+ it("should return false when first name is invalid", () => {
305
+ const result = validateContactName("", "Doe");
306
+ expect(result.isValid).toBe(false);
307
+ expect(result.firstName.success).toBe(false);
308
+ });
309
+
310
+ it("should return false when last name is invalid", () => {
311
+ const result = validateContactName("John", "");
312
+ expect(result.isValid).toBe(false);
313
+ expect(result.lastName.success).toBe(false);
314
+ });
315
+ });
316
+ });
317
+
318
+ // ============================================
319
+ // Locale Validation Tests
320
+ // ============================================
321
+
322
+ describe("Locale Validation", () => {
323
+ describe("localeSchema", () => {
324
+ it("should accept valid BCP 47 locales", () => {
325
+ const validLocales = ["en", "en-US", "pt-BR", "zh-Hans-CN", "es-419"];
326
+
327
+ validLocales.forEach((locale) => {
328
+ const result = localeSchema.safeParse(locale);
329
+ expect(result.success, `Expected "${locale}" to be valid`).toBe(true);
330
+ });
331
+ });
332
+
333
+ it("should reject invalid locale formats", () => {
334
+ const invalidLocales = ["english", "EN_US", "e", "12-34"];
335
+
336
+ invalidLocales.forEach((locale) => {
337
+ const result = localeSchema.safeParse(locale);
338
+ expect(result.success).toBe(false);
339
+ });
340
+ });
341
+ });
342
+
343
+ describe("optionalLocaleSchema", () => {
344
+ it("should accept undefined", () => {
345
+ const result = optionalLocaleSchema.safeParse(undefined);
346
+ expect(result.success).toBe(true);
347
+ });
348
+
349
+ it("should validate locale when provided", () => {
350
+ const result = optionalLocaleSchema.safeParse("en-US");
351
+ expect(result.success).toBe(true);
352
+ });
353
+ });
354
+ });
355
+
356
+ // ============================================
357
+ // Date Validation Tests
358
+ // ============================================
359
+
360
+ describe("ISO Date String Validation", () => {
361
+ describe("isoDateStringSchema", () => {
362
+ it("should accept valid ISO 8601 date strings", () => {
363
+ const validDates = [
364
+ "2024-01-15T10:30:00Z",
365
+ "2024-01-15T10:30:00.000Z",
366
+ "2024-01-15",
367
+ "2024-06-01T14:30:00+02:00",
368
+ ];
369
+
370
+ validDates.forEach((date) => {
371
+ const result = isoDateStringSchema.safeParse(date);
372
+ expect(result.success, `Expected "${date}" to be valid`).toBe(true);
373
+ });
374
+ });
375
+
376
+ it("should reject invalid date strings", () => {
377
+ const invalidDates = ["not-a-date", "2024/01/15", "15-01-2024", "invalid", "Jan 15, 2024"];
378
+
379
+ invalidDates.forEach((date) => {
380
+ const result = isoDateStringSchema.safeParse(date);
381
+ expect(result.success, `Expected "${date}" to be invalid`).toBe(false);
382
+ });
383
+ });
384
+ });
385
+
386
+ describe("optionalIsoDateStringSchema", () => {
387
+ it("should accept undefined", () => {
388
+ const result = optionalIsoDateStringSchema.safeParse(undefined);
389
+ expect(result.success).toBe(true);
390
+ });
391
+
392
+ it("should validate date when provided", () => {
393
+ const result = optionalIsoDateStringSchema.safeParse("2024-01-15T10:30:00Z");
394
+ expect(result.success).toBe(true);
395
+ });
396
+ });
397
+ });
398
+
399
+ // ============================================
400
+ // Tag Validation Tests
401
+ // ============================================
402
+
403
+ describe("Tag Validation", () => {
404
+ describe("tagSchema", () => {
405
+ it("should accept valid tags", () => {
406
+ const result = tagSchema.safeParse("vip");
407
+ expect(result.success).toBe(true);
408
+ });
409
+
410
+ it("should transform to lowercase and trim", () => {
411
+ const result = tagSchema.safeParse(" VIP ");
412
+ expect(result.success).toBe(true);
413
+ if (result.success) {
414
+ expect(result.data).toBe("vip");
415
+ }
416
+ });
417
+
418
+ it("should reject empty tags", () => {
419
+ const result = tagSchema.safeParse("");
420
+ expect(result.success).toBe(false);
421
+ });
422
+
423
+ it("should reject tags exceeding 50 characters", () => {
424
+ const longTag = "a".repeat(51);
425
+ const result = tagSchema.safeParse(longTag);
426
+ expect(result.success).toBe(false);
427
+ });
428
+ });
429
+
430
+ describe("tagsArraySchema", () => {
431
+ it("should accept valid tags array", () => {
432
+ const result = tagsArraySchema.safeParse(["vip", "returning", "newsletter"]);
433
+ expect(result.success).toBe(true);
434
+ });
435
+
436
+ it("should accept undefined", () => {
437
+ const result = tagsArraySchema.safeParse(undefined);
438
+ expect(result.success).toBe(true);
439
+ });
440
+
441
+ it("should reject arrays with more than 100 tags", () => {
442
+ const tooManyTags = Array(101).fill("tag");
443
+ const result = tagsArraySchema.safeParse(tooManyTags);
444
+ expect(result.success).toBe(false);
445
+ });
446
+
447
+ it("should reject arrays with empty tags", () => {
448
+ const result = tagsArraySchema.safeParse(["valid", ""]);
449
+ expect(result.success).toBe(false);
450
+ });
451
+ });
452
+ });
453
+
454
+ // ============================================
455
+ // Brand ID Validation Tests
456
+ // ============================================
457
+
458
+ describe("Brand ID Validation", () => {
459
+ describe("brandIdSchema", () => {
460
+ it("should accept non-empty brand ID", () => {
461
+ const result = brandIdSchema.safeParse("brand-123");
462
+ expect(result.success).toBe(true);
463
+ });
464
+
465
+ it("should reject empty brand ID", () => {
466
+ const result = brandIdSchema.safeParse("");
467
+ expect(result.success).toBe(false);
468
+ });
469
+ });
470
+ });
471
+
472
+ // ============================================
473
+ // Custom Field Validation Tests
474
+ // ============================================
475
+
476
+ describe("Custom Field Validation", () => {
477
+ describe("customFieldDefinitionSchema", () => {
478
+ it("should accept valid string field definition", () => {
479
+ const result = customFieldDefinitionSchema.safeParse({
480
+ type: "string",
481
+ maxLength: 100,
482
+ required: true,
483
+ });
484
+ expect(result.success).toBe(true);
485
+ });
486
+
487
+ it("should accept valid number field definition", () => {
488
+ const result = customFieldDefinitionSchema.safeParse({
489
+ type: "number",
490
+ min: 0,
491
+ max: 100,
492
+ });
493
+ expect(result.success).toBe(true);
494
+ });
495
+
496
+ it("should reject invalid field type", () => {
497
+ const result = customFieldDefinitionSchema.safeParse({
498
+ type: "invalid",
499
+ });
500
+ expect(result.success).toBe(false);
501
+ });
502
+ });
503
+
504
+ describe("createCustomFieldValueSchema", () => {
505
+ it("should create string schema with constraints", () => {
506
+ const schema = createCustomFieldValueSchema({
507
+ type: "string",
508
+ maxLength: 10,
509
+ required: true,
510
+ });
511
+
512
+ expect(schema.safeParse("short").success).toBe(true);
513
+ expect(schema.safeParse("this is too long").success).toBe(false);
514
+ });
515
+
516
+ it("should create number schema with constraints", () => {
517
+ const schema = createCustomFieldValueSchema({
518
+ type: "number",
519
+ min: 0,
520
+ max: 100,
521
+ required: true,
522
+ });
523
+
524
+ expect(schema.safeParse(50).success).toBe(true);
525
+ expect(schema.safeParse(150).success).toBe(false);
526
+ expect(schema.safeParse(-10).success).toBe(false);
527
+ });
528
+
529
+ it("should create boolean schema", () => {
530
+ const schema = createCustomFieldValueSchema({
531
+ type: "boolean",
532
+ required: true,
533
+ });
534
+
535
+ expect(schema.safeParse(true).success).toBe(true);
536
+ expect(schema.safeParse(false).success).toBe(true);
537
+ expect(schema.safeParse("true").success).toBe(false);
538
+ });
539
+
540
+ it("should create date schema", () => {
541
+ const schema = createCustomFieldValueSchema({
542
+ type: "date",
543
+ required: true,
544
+ });
545
+
546
+ expect(schema.safeParse("2024-01-15T10:30:00Z").success).toBe(true);
547
+ expect(schema.safeParse("invalid-date").success).toBe(false);
548
+ });
549
+ });
550
+
551
+ describe("contactPreferencesSchema", () => {
552
+ it("should accept valid preferences", () => {
553
+ const result = contactPreferencesSchema.safeParse({
554
+ theme: "dark",
555
+ language: "en-US",
556
+ notifications: { email: true, sms: false },
557
+ });
558
+ expect(result.success).toBe(true);
559
+ });
560
+
561
+ it("should accept undefined", () => {
562
+ const result = contactPreferencesSchema.safeParse(undefined);
563
+ expect(result.success).toBe(true);
564
+ });
565
+
566
+ it("should reject invalid theme value", () => {
567
+ const result = contactPreferencesSchema.safeParse({
568
+ theme: "invalid-theme",
569
+ });
570
+ expect(result.success).toBe(false);
571
+ });
572
+
573
+ it("should reject invalid language locale", () => {
574
+ const result = contactPreferencesSchema.safeParse({
575
+ language: "invalid-locale",
576
+ });
577
+ expect(result.success).toBe(false);
578
+ });
579
+
580
+ it("should accept custom preferences", () => {
581
+ const result = contactPreferencesSchema.safeParse({
582
+ customField: "value",
583
+ nestedObject: { key: "value" },
584
+ arrayField: [1, 2, 3],
585
+ });
586
+ expect(result.success).toBe(true);
587
+ });
588
+ });
589
+ });
590
+
591
+ // ============================================
592
+ // Create Contact Schema Tests
593
+ // ============================================
594
+
595
+ describe("Create Contact Schema", () => {
596
+ const validInput = {
597
+ brandId: "brand-123",
598
+ email: "john.doe@example.com",
599
+ firstName: "John",
600
+ lastName: "Doe",
601
+ gdprConsent: true,
602
+ };
603
+
604
+ it("should accept valid minimal input", () => {
605
+ const result = createContactSchema.safeParse(validInput);
606
+ expect(result.success).toBe(true);
607
+ });
608
+
609
+ it("should accept valid input with all optional fields", () => {
610
+ const fullInput = {
611
+ ...validInput,
612
+ phone: "+14155552671",
613
+ locale: "en-US",
614
+ gdprConsentDate: "2024-01-15T10:00:00Z",
615
+ marketingOptIn: true,
616
+ preferences: { theme: "dark" },
617
+ tags: ["vip", "returning"],
618
+ totalVisits: 5,
619
+ lastVisitDate: "2024-06-01T14:00:00Z",
620
+ firstVisitDate: "2023-01-15T10:00:00Z",
621
+ legacyId: "legacy-123",
622
+ createdAt: "2024-01-01T00:00:00Z",
623
+ createdBy: "admin@example.com",
624
+ };
625
+
626
+ const result = createContactSchema.safeParse(fullInput);
627
+ expect(result.success).toBe(true);
628
+ });
629
+
630
+ it("should reject missing required fields", () => {
631
+ const missingFields = {
632
+ email: "test@example.com",
633
+ };
634
+
635
+ const result = createContactSchema.safeParse(missingFields);
636
+ expect(result.success).toBe(false);
637
+ });
638
+
639
+ it("should reject invalid email", () => {
640
+ const result = createContactSchema.safeParse({
641
+ ...validInput,
642
+ email: "invalid-email",
643
+ });
644
+ expect(result.success).toBe(false);
645
+ });
646
+
647
+ it("should reject invalid phone format", () => {
648
+ const result = createContactSchema.safeParse({
649
+ ...validInput,
650
+ phone: "abc",
651
+ });
652
+ expect(result.success).toBe(false);
653
+ });
654
+
655
+ it("should reject negative totalVisits", () => {
656
+ const result = createContactSchema.safeParse({
657
+ ...validInput,
658
+ totalVisits: -1,
659
+ });
660
+ expect(result.success).toBe(false);
661
+ });
662
+
663
+ it("should transform email to lowercase", () => {
664
+ const result = createContactSchema.safeParse({
665
+ ...validInput,
666
+ email: "JOHN.DOE@EXAMPLE.COM",
667
+ });
668
+ expect(result.success).toBe(true);
669
+ if (result.success) {
670
+ expect(result.data.email).toBe("john.doe@example.com");
671
+ }
672
+ });
673
+
674
+ describe("validateCreateContact helper", () => {
675
+ it("should validate create input", () => {
676
+ const result = validateCreateContact(validInput);
677
+ expect(result.success).toBe(true);
678
+ });
679
+
680
+ it("should return errors for invalid input", () => {
681
+ const result = validateCreateContact({ email: "invalid" });
682
+ expect(result.success).toBe(false);
683
+ });
684
+ });
685
+ });
686
+
687
+ // ============================================
688
+ // Update Contact Schema Tests
689
+ // ============================================
690
+
691
+ describe("Update Contact Schema", () => {
692
+ it("should accept valid update with id only", () => {
693
+ const result = updateContactSchema.safeParse({
694
+ id: "contact-123",
695
+ });
696
+ expect(result.success).toBe(true);
697
+ });
698
+
699
+ it("should accept partial updates", () => {
700
+ const result = updateContactSchema.safeParse({
701
+ id: "contact-123",
702
+ firstName: "Jane",
703
+ marketingOptIn: true,
704
+ });
705
+ expect(result.success).toBe(true);
706
+ });
707
+
708
+ it("should reject missing id", () => {
709
+ const result = updateContactSchema.safeParse({
710
+ firstName: "Jane",
711
+ });
712
+ expect(result.success).toBe(false);
713
+ });
714
+
715
+ it("should reject empty id", () => {
716
+ const result = updateContactSchema.safeParse({
717
+ id: "",
718
+ });
719
+ expect(result.success).toBe(false);
720
+ });
721
+
722
+ it("should accept soft delete fields", () => {
723
+ const result = updateContactSchema.safeParse({
724
+ id: "contact-123",
725
+ deletedAt: "2024-01-15T10:00:00Z",
726
+ deletedBy: "admin@example.com",
727
+ });
728
+ expect(result.success).toBe(true);
729
+ });
730
+
731
+ it("should accept null for soft delete fields (restore)", () => {
732
+ const result = updateContactSchema.safeParse({
733
+ id: "contact-123",
734
+ deletedAt: null,
735
+ deletedBy: null,
736
+ });
737
+ expect(result.success).toBe(true);
738
+ });
739
+
740
+ describe("validateUpdateContact helper", () => {
741
+ it("should validate update input", () => {
742
+ const result = validateUpdateContact({ id: "contact-123", firstName: "Jane" });
743
+ expect(result.success).toBe(true);
744
+ });
745
+
746
+ it("should return errors for invalid input", () => {
747
+ const result = validateUpdateContact({ id: "" });
748
+ expect(result.success).toBe(false);
749
+ });
750
+ });
751
+ });
752
+
753
+ // ============================================
754
+ // Merge Contacts Schema Tests
755
+ // ============================================
756
+
757
+ describe("Merge Contacts Schema", () => {
758
+ it("should accept valid merge input", () => {
759
+ const result = mergeContactsSchema.safeParse({
760
+ primaryId: "contact-primary",
761
+ duplicateIds: ["contact-dup-1", "contact-dup-2"],
762
+ });
763
+ expect(result.success).toBe(true);
764
+ });
765
+
766
+ it("should reject missing primaryId", () => {
767
+ const result = mergeContactsSchema.safeParse({
768
+ duplicateIds: ["contact-dup-1"],
769
+ });
770
+ expect(result.success).toBe(false);
771
+ });
772
+
773
+ it("should reject empty primaryId", () => {
774
+ const result = mergeContactsSchema.safeParse({
775
+ primaryId: "",
776
+ duplicateIds: ["contact-dup-1"],
777
+ });
778
+ expect(result.success).toBe(false);
779
+ });
780
+
781
+ it("should reject empty duplicateIds array", () => {
782
+ const result = mergeContactsSchema.safeParse({
783
+ primaryId: "contact-primary",
784
+ duplicateIds: [],
785
+ });
786
+ expect(result.success).toBe(false);
787
+ });
788
+
789
+ it("should reject duplicateIds with empty strings", () => {
790
+ const result = mergeContactsSchema.safeParse({
791
+ primaryId: "contact-primary",
792
+ duplicateIds: ["contact-dup-1", ""],
793
+ });
794
+ expect(result.success).toBe(false);
795
+ });
796
+
797
+ it("should reject more than 50 duplicate contacts", () => {
798
+ const tooManyDuplicates = Array(51)
799
+ .fill(null)
800
+ .map((_, i) => `contact-dup-${i}`);
801
+
802
+ const result = mergeContactsSchema.safeParse({
803
+ primaryId: "contact-primary",
804
+ duplicateIds: tooManyDuplicates,
805
+ });
806
+ expect(result.success).toBe(false);
807
+ });
808
+ });
809
+
810
+ // ============================================
811
+ // Search Contact Schema Tests
812
+ // ============================================
813
+
814
+ describe("Search Contact Schema", () => {
815
+ it("should accept empty search (all contacts)", () => {
816
+ const result = searchContactSchema.safeParse({});
817
+ expect(result.success).toBe(true);
818
+ });
819
+
820
+ it("should accept valid search parameters", () => {
821
+ const result = searchContactSchema.safeParse({
822
+ brandId: "brand-123",
823
+ search: "john",
824
+ tags: ["vip"],
825
+ gdprConsent: true,
826
+ limit: 50,
827
+ });
828
+ expect(result.success).toBe(true);
829
+ });
830
+
831
+ it("should reject search query exceeding 255 characters", () => {
832
+ const result = searchContactSchema.safeParse({
833
+ search: "a".repeat(256),
834
+ });
835
+ expect(result.success).toBe(false);
836
+ });
837
+
838
+ it("should reject limit below 1", () => {
839
+ const result = searchContactSchema.safeParse({
840
+ limit: 0,
841
+ });
842
+ expect(result.success).toBe(false);
843
+ });
844
+
845
+ it("should reject limit above 250", () => {
846
+ const result = searchContactSchema.safeParse({
847
+ limit: 251,
848
+ });
849
+ expect(result.success).toBe(false);
850
+ });
851
+
852
+ it("should apply default values", () => {
853
+ const result = searchContactSchema.safeParse({});
854
+ expect(result.success).toBe(true);
855
+ if (result.success) {
856
+ expect(result.data.includeDeleted).toBe(false);
857
+ expect(result.data.limit).toBe(25);
858
+ }
859
+ });
860
+ });
861
+
862
+ // ============================================
863
+ // Bulk Import Schema Tests
864
+ // ============================================
865
+
866
+ describe("Bulk Import Contact Schema", () => {
867
+ const validContact = {
868
+ brandId: "brand-123",
869
+ email: "test@example.com",
870
+ firstName: "John",
871
+ lastName: "Doe",
872
+ gdprConsent: true,
873
+ };
874
+
875
+ it("should accept valid bulk import", () => {
876
+ const result = bulkImportContactSchema.safeParse({
877
+ contacts: [validContact, { ...validContact, email: "jane@example.com" }],
878
+ });
879
+ expect(result.success).toBe(true);
880
+ });
881
+
882
+ it("should reject empty contacts array", () => {
883
+ const result = bulkImportContactSchema.safeParse({
884
+ contacts: [],
885
+ });
886
+ expect(result.success).toBe(false);
887
+ });
888
+
889
+ it("should reject more than 1000 contacts", () => {
890
+ const tooManyContacts = Array(1001)
891
+ .fill(null)
892
+ .map((_, i) => ({
893
+ ...validContact,
894
+ email: `user${i}@example.com`,
895
+ }));
896
+
897
+ const result = bulkImportContactSchema.safeParse({
898
+ contacts: tooManyContacts,
899
+ });
900
+ expect(result.success).toBe(false);
901
+ });
902
+
903
+ it("should apply default values", () => {
904
+ const result = bulkImportContactSchema.safeParse({
905
+ contacts: [validContact],
906
+ });
907
+ expect(result.success).toBe(true);
908
+ if (result.success) {
909
+ expect(result.data.skipDuplicates).toBe(true);
910
+ expect(result.data.updateExisting).toBe(false);
911
+ }
912
+ });
913
+
914
+ it("should validate individual contacts", () => {
915
+ const result = bulkImportContactSchema.safeParse({
916
+ contacts: [{ ...validContact, email: "invalid-email" }],
917
+ });
918
+ expect(result.success).toBe(false);
919
+ });
920
+ });
921
+
922
+ // ============================================
923
+ // Error Formatting Tests
924
+ // ============================================
925
+
926
+ describe("Error Formatting Helpers", () => {
927
+ describe("formatValidationErrors", () => {
928
+ it("should format errors into readable string", () => {
929
+ const result = createContactSchema.safeParse({
930
+ email: "invalid",
931
+ });
932
+
933
+ if (!result.success) {
934
+ const formatted = formatValidationErrors(result.error);
935
+ expect(formatted).toContain("email");
936
+ expect(formatted).toContain("Invalid email");
937
+ }
938
+ });
939
+
940
+ it("should handle nested path errors", () => {
941
+ const result = createContactSchema.safeParse({
942
+ brandId: "brand-123",
943
+ email: "test@example.com",
944
+ firstName: "John",
945
+ lastName: "Doe",
946
+ gdprConsent: true,
947
+ preferences: {
948
+ theme: "invalid-theme",
949
+ },
950
+ });
951
+
952
+ if (!result.success) {
953
+ const formatted = formatValidationErrors(result.error);
954
+ expect(typeof formatted).toBe("string");
955
+ }
956
+ });
957
+ });
958
+
959
+ describe("getValidationErrorDetails", () => {
960
+ it("should return array of error details", () => {
961
+ const result = createContactSchema.safeParse({
962
+ email: "invalid",
963
+ });
964
+
965
+ if (!result.success) {
966
+ const details = getValidationErrorDetails(result.error);
967
+ expect(Array.isArray(details)).toBe(true);
968
+ expect(details.length).toBeGreaterThan(0);
969
+ expect(details[0]).toHaveProperty("path");
970
+ expect(details[0]).toHaveProperty("message");
971
+ expect(details[0]).toHaveProperty("code");
972
+ }
973
+ });
974
+ });
975
+ });