@bedrockio/yada 1.0.0

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,1855 @@
1
+ import yd from '../index';
2
+ import { isSchema, isSchemaError } from '../utils';
3
+
4
+ async function assertPass(schema, obj, expected, options) {
5
+ try {
6
+ const result = await schema.validate(obj, options);
7
+ if (expected) {
8
+ expect(result).toEqual(expected);
9
+ } else {
10
+ expect(true).toBe(true);
11
+ }
12
+ } catch (error) {
13
+ // eslint-disable-next-line
14
+ console.error(error);
15
+ throw error;
16
+ }
17
+ }
18
+
19
+ async function assertFail(schema, obj, errors) {
20
+ try {
21
+ await schema.validate(obj);
22
+ throw new Error('Expected failure but passed.');
23
+ } catch (error) {
24
+ if (!error.details) {
25
+ throw error;
26
+ }
27
+ expect(mapErrorMessages(error)).toEqual(errors);
28
+ }
29
+ }
30
+
31
+ function mapErrorMessages(error) {
32
+ if (error.details) {
33
+ return error.details.flatMap(mapErrorMessages);
34
+ } else {
35
+ return [error.message];
36
+ }
37
+ }
38
+
39
+ async function assertErrorMessage(schema, obj, message) {
40
+ let error;
41
+ try {
42
+ await schema.validate(obj);
43
+ } catch (err) {
44
+ error = err;
45
+ }
46
+ expect(error.message).toEqual(message);
47
+ }
48
+
49
+ describe('string', () => {
50
+ it('should validate an optional string', async () => {
51
+ const schema = yd.string();
52
+ await assertPass(schema, 'a');
53
+ await assertPass(schema, undefined);
54
+ await assertFail(schema, null, ['Must be a string.']);
55
+ await assertFail(schema, 1, ['Must be a string.']);
56
+ });
57
+
58
+ it('should validate a required string', async () => {
59
+ const schema = yd.string().required();
60
+ await assertPass(schema, 'a');
61
+ await assertFail(schema, undefined, ['Value is required.']);
62
+ await assertFail(schema, 1, ['Must be a string.']);
63
+ });
64
+
65
+ it('should validate a minimum length', async () => {
66
+ const schema = yd.string().min(4);
67
+ await assertPass(schema, 'abcd');
68
+ await assertFail(schema, 'abc', ['Must be 4 characters or more.']);
69
+ });
70
+
71
+ it('should validate a maximum length', async () => {
72
+ const schema = yd.string().max(4);
73
+ await assertPass(schema, 'a');
74
+ await assertFail(schema, 'abcde', ['Must be 4 characters or less.']);
75
+ });
76
+
77
+ it('should validate an email', async () => {
78
+ const schema = yd.string().email();
79
+ await assertPass(schema, undefined);
80
+ await assertPass(schema, 'foo@bar.com');
81
+ await assertFail(schema, 'foo@bar', ['Must be an email address.']);
82
+ });
83
+
84
+ it('should validate a regex pattern', async () => {
85
+ expect(() => {
86
+ yd.string().match();
87
+ }).toThrow('Argument must be a regular expression');
88
+ expect(() => {
89
+ yd.string().match('foo');
90
+ }).toThrow('Argument must be a regular expression');
91
+
92
+ const reg = /^[A-Z]+$/;
93
+ const schema = yd.string().match(reg);
94
+ await assertPass(schema, 'A');
95
+ await assertFail(schema, 'a', [`Must match pattern ${reg}.`]);
96
+ });
97
+
98
+ it('should trim a string', async () => {
99
+ const schema = yd.string().email().trim();
100
+ expect(await schema.validate(' foo@bar.com ')).toBe('foo@bar.com');
101
+ expect(await schema.validate(' foo@bar.com')).toBe('foo@bar.com');
102
+ expect(await schema.validate('foo@bar.com ')).toBe('foo@bar.com');
103
+ expect(await schema.validate('foo@bar.com')).toBe('foo@bar.com');
104
+ });
105
+
106
+ it('should convert to lower case', async () => {
107
+ const schema = yd.string().lowercase();
108
+ expect(await schema.validate('FOO')).toBe('foo');
109
+ expect(await schema.validate('foo')).toBe('foo');
110
+ });
111
+
112
+ it('should convert to assert lower case', async () => {
113
+ const schema = yd.string().lowercase(true);
114
+ await assertPass(schema, 'foo');
115
+ await assertFail(schema, 'Foo', ['Must be in lower case.']);
116
+ await assertFail(schema, 'FOO', ['Must be in lower case.']);
117
+ });
118
+
119
+ it('should convert to upper case', async () => {
120
+ const schema = yd.string().uppercase();
121
+ expect(await schema.validate('foo')).toBe('FOO');
122
+ expect(await schema.validate('FOO')).toBe('FOO');
123
+ });
124
+
125
+ it('should convert to assert upper case', async () => {
126
+ const schema = yd.string().uppercase(true);
127
+ await assertPass(schema, 'FOO');
128
+ await assertFail(schema, 'Foo', ['Must be in upper case.']);
129
+ await assertFail(schema, 'foo', ['Must be in upper case.']);
130
+ });
131
+
132
+ it('should validate a hexadecimal string', async () => {
133
+ const schema = yd.string().hex();
134
+ await assertPass(schema, 'abc123456789');
135
+ await assertFail(schema, 'zzz', ['Must be hexadecimal.']);
136
+ });
137
+
138
+ it('should validate an MD5 hash', async () => {
139
+ await assertPass(yd.string().md5(), 'bed1e4d90fb9261a80ae92d339949559');
140
+ await assertFail(yd.string().md5(), 'aaaa', [
141
+ 'Must be a hash in md5 format.',
142
+ ]);
143
+ });
144
+
145
+ it('should validate a SHA1 hash', async () => {
146
+ await assertPass(
147
+ yd.string().sha1(),
148
+ 'c9b09f7f254eb6aaeeff30abeb0b92bea732855a'
149
+ );
150
+
151
+ await assertFail(yd.string().sha1(), 'bed1e4d90fb9261a80ae92d339949559', [
152
+ 'Must be a hash in sha1 format.',
153
+ ]);
154
+ });
155
+
156
+ it('should validate an ascii string', async () => {
157
+ const schema = yd.string().ascii();
158
+ await assertPass(schema, 'abc123456789%&#');
159
+ await assertFail(schema, '¥¢£©', ['Must be ASCII.']);
160
+ });
161
+
162
+ it('should validate a base64 string', async () => {
163
+ const schema = yd.string().base64();
164
+ await assertPass(schema, 'Zm9vYmFy');
165
+ await assertFail(schema, 'a', ['Must be base64.']);
166
+ });
167
+
168
+ it('should validate a credit card', async () => {
169
+ const schema = yd.string().creditCard();
170
+ await assertPass(schema, '4111111111111111');
171
+ await assertFail(schema, '5111111111111111', [
172
+ 'Must be a valid credit card number.',
173
+ ]);
174
+ await assertFail(schema, 'foo', ['Must be a valid credit card number.']);
175
+ });
176
+
177
+ it('should validate an ip address', async () => {
178
+ const schema = yd.string().ip();
179
+ await assertPass(schema, '192.168.0.0');
180
+ await assertFail(schema, '192.168.0', ['Must be a valid IP address.']);
181
+ });
182
+
183
+ it('should validate an ISO 3166-1 alpha-2 country code', async () => {
184
+ const schema = yd.string().country();
185
+ await assertPass(schema, 'jp');
186
+ await assertFail(schema, 'zz', ['Must be a valid country code.']);
187
+ });
188
+
189
+ it('should validate a locale code', async () => {
190
+ const schema = yd.string().locale();
191
+ await assertPass(schema, 'ja-JP');
192
+ await assertFail(schema, 'japan', ['Must be a valid locale code.']);
193
+ });
194
+
195
+ it('should validate a JWT token', async () => {
196
+ const schema = yd.string().jwt();
197
+ const token =
198
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9lIn0.2dDMbovRrOV-rp-6_zl2ZwrckDpodOnBcg8KY7mBjw4';
199
+ await assertPass(schema, token);
200
+ await assertFail(schema, 'token', ['Must be a valid JWT token.']);
201
+ });
202
+
203
+ it('should validate a latitude-longitude string', async () => {
204
+ const schema = yd.string().latlng();
205
+ await assertPass(schema, '41.7708727,140.7125196');
206
+ await assertFail(schema, '41.7708727', [
207
+ 'Must be a valid lat,lng coordinate.',
208
+ ]);
209
+ });
210
+
211
+ it('should validate a postal code', async () => {
212
+ const schema = yd.string().postalCode();
213
+ await assertPass(schema, '80906');
214
+ await assertFail(schema, '80906z', ['Must be a valid postal code.']);
215
+ });
216
+
217
+ it('should validate a slug', async () => {
218
+ const schema = yd.string().slug();
219
+ await assertPass(schema, 'foo-bar');
220
+ await assertFail(schema, 'foo#-bar', ['Must be a valid slug.']);
221
+ });
222
+
223
+ it('should validate a password', async () => {
224
+ const schema = yd.string().password();
225
+ await assertPass(schema, '123456789abcde');
226
+ await assertFail(schema, '1234', ['Must be at least 12 characters.']);
227
+ });
228
+
229
+ it('should validate a password with options', async () => {
230
+ const schema = yd.string().password({
231
+ minLength: 4,
232
+ minLowercase: 1,
233
+ minUppercase: 1,
234
+ minNumbers: 1,
235
+ minSymbols: 1,
236
+ });
237
+ await assertPass(schema, 'aB1%');
238
+ await assertFail(schema, '123456789abcde', [
239
+ 'Must contain at least 1 uppercase character.',
240
+ 'Must contain at least 1 symbol.',
241
+ ]);
242
+ });
243
+
244
+ it('should validate a URL', async () => {
245
+ const schema = yd.string().url();
246
+ await assertPass(schema, 'http://foo.com');
247
+ await assertFail(schema, 'http://foo', ['Must be a valid URL.']);
248
+ });
249
+
250
+ it('should validate a UUID v4', async () => {
251
+ const schema = yd.string().uuid();
252
+ await assertPass(schema, '60648997-e80c-45e2-8467-2084fc207dce');
253
+ await assertFail(schema, '60648997-e80c', ['Must be a valid unique id.']);
254
+ });
255
+
256
+ it('should validate a domain', async () => {
257
+ const schema = yd.string().domain();
258
+ await assertPass(schema, 'foo.com');
259
+ await assertFail(schema, 'foo', ['Must be a valid domain.']);
260
+ });
261
+
262
+ it('should validate a Bitcoin address', async () => {
263
+ const schema = yd.string().btc();
264
+ await assertPass(schema, '3FZbgi29cpjq2GjdwV8eyHuJJnkLtktZc5');
265
+ await assertFail(schema, 'foo', ['Must be a valid Bitcoin address.']);
266
+ });
267
+
268
+ it('should validate a Ethereum address', async () => {
269
+ const schema = yd.string().eth();
270
+ await assertPass(schema, '0xb794f5ea0ba39494ce839613fffba74279579268');
271
+ await assertFail(schema, 'foo', ['Must be a valid Ethereum address.']);
272
+ });
273
+
274
+ it('should validate a SWIFT bank code', async () => {
275
+ const schema = yd.string().swift();
276
+ await assertPass(schema, 'AXISINBB250');
277
+ await assertFail(schema, 'foo', ['Must be a valid SWIFT code.']);
278
+ });
279
+
280
+ it('should validate a MongoDB ObjectId', async () => {
281
+ const schema = yd.string().mongo();
282
+ await assertPass(schema, '61b8b032cac265007c34ce09');
283
+ await assertFail(schema, 'foo', ['Must be a valid ObjectId.']);
284
+ });
285
+ });
286
+
287
+ describe('number', () => {
288
+ it('should validate an optional number', async () => {
289
+ const schema = yd.number();
290
+ await assertPass(schema, 1);
291
+ await assertPass(schema, undefined);
292
+ await assertFail(schema, null, ['Must be a number.']);
293
+ await assertFail(schema, 'a', ['Must be a number.']);
294
+ });
295
+
296
+ it('should validate a required number', async () => {
297
+ const schema = yd.number().required();
298
+ await assertPass(schema, 1);
299
+ await assertFail(schema, undefined, ['Value is required.']);
300
+ await assertFail(schema, 'a', ['Must be a number.']);
301
+ });
302
+
303
+ it('should validate a minimum value', async () => {
304
+ const schema = yd.number().min(4);
305
+ await assertPass(schema, 5);
306
+ await assertFail(schema, 1, ['Must be greater than 4.']);
307
+ });
308
+
309
+ it('should validate a maximum value', async () => {
310
+ const schema = yd.number().max(4);
311
+ await assertPass(schema, 1);
312
+ await assertFail(schema, 5, ['Must be less than 4.']);
313
+ });
314
+
315
+ it('should validate an integer', async () => {
316
+ const schema = yd.number().integer();
317
+ await assertPass(schema, 1);
318
+ await assertFail(schema, 1.1, ['Must be an integer.']);
319
+ });
320
+
321
+ it('should validate a positive number', async () => {
322
+ const schema = yd.number().positive();
323
+ await assertPass(schema, 1);
324
+ await assertFail(schema, -1, ['Must be positive.']);
325
+ });
326
+
327
+ it('should validate a negative number', async () => {
328
+ const schema = yd.number().negative();
329
+ await assertPass(schema, -1);
330
+ await assertFail(schema, 1, ['Must be negative.']);
331
+ });
332
+
333
+ it('should validate a multiple', async () => {
334
+ const schema = yd.number().multiple(3);
335
+ await assertPass(schema, 3);
336
+ await assertPass(schema, 6);
337
+ await assertPass(schema, 9);
338
+ await assertFail(schema, 10, ['Must be a multiple of 3.']);
339
+ });
340
+ });
341
+
342
+ describe('boolean', () => {
343
+ it('should validate an optional boolean', async () => {
344
+ const schema = yd.boolean();
345
+ await assertPass(schema, true);
346
+ await assertPass(schema, false);
347
+ await assertPass(schema, undefined);
348
+ await assertFail(schema, 1, ['Must be a boolean.']);
349
+ });
350
+
351
+ it('should validate a required boolean', async () => {
352
+ const schema = yd.boolean().required();
353
+ await assertPass(schema, true);
354
+ await assertPass(schema, false);
355
+ await assertFail(schema, undefined, ['Value is required.']);
356
+ await assertFail(schema, 1, ['Must be a boolean.']);
357
+ });
358
+ });
359
+
360
+ describe('allow', () => {
361
+ it('should validate an enum', async () => {
362
+ const schema = yd.allow('one', 'two');
363
+ await assertPass(schema, 'one');
364
+ await assertPass(schema, 'two');
365
+ await assertFail(schema, 'three', ['Must be one of ["one", "two"].']);
366
+ });
367
+
368
+ it('should pass an array', async () => {
369
+ const schema = yd.allow(['one', 'two']);
370
+ await assertPass(schema, 'one');
371
+ await assertPass(schema, 'two');
372
+ await assertFail(schema, 'three', ['Must be one of ["one", "two"].']);
373
+ });
374
+
375
+ it('should allow passing other schemas', async () => {
376
+ const schema = yd.allow([yd.string(), yd.number()]);
377
+ await assertPass(schema, 'a');
378
+ await assertPass(schema, 5);
379
+ await assertFail(schema, true, ['Must be one of [string, number].']);
380
+ await assertFail(schema, null, ['Must be one of [string, number].']);
381
+ });
382
+ });
383
+
384
+ describe('reject', () => {
385
+ it('should validate an enum', async () => {
386
+ const schema = yd.reject('one');
387
+ await assertPass(schema, 'two');
388
+ await assertFail(schema, 'one', ['Must not be one of ["one"].']);
389
+ });
390
+
391
+ it('should allow passing an array', async () => {
392
+ const schema = yd.reject(['one']);
393
+ await assertPass(schema, 'two');
394
+ await assertFail(schema, 'one', ['Must not be one of ["one"].']);
395
+ });
396
+ });
397
+
398
+ describe('object', () => {
399
+ it('should validate a basic object', async () => {
400
+ const schema = yd.object({
401
+ name: yd.string(),
402
+ });
403
+ await assertPass(schema, undefined);
404
+ await assertPass(schema, { name: 'a' });
405
+ await assertFail(schema, { name: 1 }, ['Must be a string.']);
406
+ await assertFail(schema, 1, ['Must be an object.']);
407
+ await assertFail(schema, null, ['Must be an object.']);
408
+ });
409
+
410
+ it('should validate an object with a required field', async () => {
411
+ const schema = yd.object({
412
+ name: yd.string().required(),
413
+ });
414
+ await assertPass(schema, undefined);
415
+ await assertPass(schema, { name: 'a' });
416
+ await assertFail(schema, {}, ['Value is required.']);
417
+ await assertFail(schema, { name: 1 }, ['Must be a string.']);
418
+ await assertFail(schema, 1, ['Must be an object.']);
419
+ await assertFail(schema, null, ['Must be an object.']);
420
+ });
421
+
422
+ it('should validate a required object', async () => {
423
+ const schema = yd
424
+ .object({
425
+ name: yd.string(),
426
+ })
427
+ .required();
428
+ await assertPass(schema, { name: 'a' });
429
+ await assertFail(schema, undefined, ['Value is required.']);
430
+ await assertFail(schema, { name: 1 }, ['Must be a string.']);
431
+ await assertFail(schema, 1, ['Must be an object.']);
432
+ });
433
+
434
+ it('should validate all fields', async () => {
435
+ const schema = yd
436
+ .object({
437
+ a: yd.string().required(),
438
+ b: yd.string().required(),
439
+ })
440
+ .required();
441
+ await assertFail(schema, {}, ['Value is required.', 'Value is required.']);
442
+ });
443
+
444
+ it('should allow a custom validation', async () => {
445
+ const schema = yd
446
+ .object({
447
+ name: yd
448
+ .string()
449
+ .required()
450
+ .custom((val) => {
451
+ if (val.match(/^[A-Z]/)) {
452
+ throw new Error('Must start with lower case letter.');
453
+ }
454
+ })
455
+ .custom((val) => {
456
+ if (val.length < 4) {
457
+ throw new Error('Must be at least 4 characters.');
458
+ }
459
+ }),
460
+ })
461
+ .required();
462
+ await assertPass(schema, { name: 'abcd' });
463
+ await assertFail(schema, { name: 12 }, ['Must be a string.']);
464
+ await assertFail(schema, { name: 'ABCD' }, [
465
+ 'Must start with lower case letter.',
466
+ ]);
467
+ await assertFail(schema, { name: 'abc' }, [
468
+ 'Must be at least 4 characters.',
469
+ ]);
470
+ await assertFail(schema, { name: 'Abc' }, [
471
+ 'Must start with lower case letter.',
472
+ 'Must be at least 4 characters.',
473
+ ]);
474
+ });
475
+
476
+ it('should convert date fields', async () => {
477
+ const schema = yd.object({
478
+ start: yd.date().iso(),
479
+ });
480
+ const { start } = await schema.validate({ start: '2020-01-01' });
481
+ expect(start).toBeInstanceOf(Date);
482
+ });
483
+
484
+ it('should convert custom fields', async () => {
485
+ const schema = yd.object({
486
+ name: yd.custom(() => {
487
+ return 'hello';
488
+ }),
489
+ });
490
+ const { name } = await schema.validate({ name: '2020-01-01' });
491
+ expect(name).toBe('hello');
492
+ });
493
+
494
+ it('should validate xnor as custom validator', async () => {
495
+ const schema = yd
496
+ .object({
497
+ a: yd.string(),
498
+ b: yd.string(),
499
+ })
500
+ .custom((obj) => {
501
+ if (!!obj.a === !!obj.b) {
502
+ throw new Error('Either "a" or "b" must be passed.');
503
+ }
504
+ });
505
+ await assertPass(schema, { a: 'a' });
506
+ await assertPass(schema, { b: 'b' });
507
+ await assertFail(schema, {}, ['Either "a" or "b" must be passed.']);
508
+ await assertFail(schema, { a: 'a', b: 'b' }, [
509
+ 'Either "a" or "b" must be passed.',
510
+ ]);
511
+ });
512
+
513
+ it('should fail on unknown keys by default', async () => {
514
+ const schema = yd.object({
515
+ a: yd.string(),
516
+ b: yd.string(),
517
+ });
518
+ await assertPass(schema, {
519
+ a: 'a',
520
+ b: 'b',
521
+ });
522
+ await assertFail(
523
+ schema,
524
+ {
525
+ a: 'a',
526
+ b: 'b',
527
+ c: 'c',
528
+ },
529
+ ['Unknown field "c".']
530
+ );
531
+ });
532
+
533
+ it('not fail on unknown keys when no schema is defined', async () => {
534
+ const schema = yd.object();
535
+ const result = await schema.validate({
536
+ foo: 'bar',
537
+ });
538
+ expect(result).toEqual({
539
+ foo: 'bar',
540
+ });
541
+ });
542
+
543
+ it('should allow appending an object schema', async () => {
544
+ const schema1 = yd.object({
545
+ foo: yd.string().required(),
546
+ });
547
+ const schema2 = yd.object({
548
+ bar: yd.string().required(),
549
+ });
550
+ const schema = schema1.append(schema2);
551
+ await assertPass(schema, { foo: 'foo', bar: 'bar' });
552
+ await assertFail(schema, { foo: 'foo' }, ['Value is required.']);
553
+ await assertFail(schema, { bar: 'bar' }, ['Value is required.']);
554
+ });
555
+
556
+ it('should allow appending a plain object', async () => {
557
+ const schema1 = yd.object({
558
+ foo: yd.string().required(),
559
+ });
560
+ const schema2 = {
561
+ bar: yd.string().required(),
562
+ };
563
+ const schema = schema1.append(schema2);
564
+ await assertPass(schema, { foo: 'foo', bar: 'bar' });
565
+ await assertFail(schema, { foo: 'foo' }, ['Value is required.']);
566
+ await assertFail(schema, { bar: 'bar' }, ['Value is required.']);
567
+ });
568
+
569
+ it('should not merge default values', async () => {
570
+ const schema = yd
571
+ .object({
572
+ a: yd.string(),
573
+ })
574
+ .append(
575
+ yd.object({
576
+ nested: yd
577
+ .object({
578
+ b: yd.string(),
579
+ })
580
+ .default({
581
+ b: 'c',
582
+ }),
583
+ })
584
+ );
585
+ await assertPass(schema, { a: 'a' });
586
+ });
587
+
588
+ it('should pass through all options to nested schemas', async () => {
589
+ let foo;
590
+ const schema = yd.object({
591
+ a: yd.custom((val, options) => {
592
+ foo = options.foo;
593
+ }),
594
+ });
595
+ await assertPass(
596
+ schema,
597
+ { a: 'b' },
598
+ { a: 'b' },
599
+ {
600
+ foo: 'bar',
601
+ }
602
+ );
603
+ expect(foo).toBe('bar');
604
+ });
605
+ });
606
+
607
+ describe('custom', () => {
608
+ it('should allow an optional root validator', async () => {
609
+ const schema = yd.custom((val) => {
610
+ if (val === 'goodbye') {
611
+ throw new Error('Must not be goodbye.');
612
+ }
613
+ });
614
+ await assertPass(schema, undefined);
615
+ await assertPass(schema, '');
616
+ await assertPass(schema, 'hello');
617
+ await assertFail(schema, 'goodbye', ['Must not be goodbye.']);
618
+ });
619
+
620
+ it('should allow a required root validator', async () => {
621
+ const schema = yd
622
+ .custom((val) => {
623
+ if (val === 'goodbye') {
624
+ throw new Error('Must not be goodbye.');
625
+ }
626
+ })
627
+ .required();
628
+ await assertPass(schema, '');
629
+ await assertPass(schema, 'hello');
630
+ await assertFail(schema, undefined, ['Value is required.']);
631
+ await assertFail(schema, 'goodbye', ['Must not be goodbye.']);
632
+ });
633
+
634
+ it('should convert result', async () => {
635
+ const schema = yd.custom(() => {
636
+ return 'goodbye';
637
+ });
638
+ expect(await schema.validate('hello')).toBe('goodbye');
639
+ });
640
+
641
+ it('should allow a custom assertion type', async () => {
642
+ const schema = yd.custom('permissions', () => {
643
+ throw new Error('Not enough permissions!');
644
+ });
645
+ let error;
646
+ try {
647
+ await schema.validate('foo');
648
+ } catch (err) {
649
+ error = err;
650
+ }
651
+ expect(JSON.parse(JSON.stringify(error))).toEqual({
652
+ type: 'validation',
653
+ message: 'Input failed validation.',
654
+ details: [
655
+ {
656
+ type: 'permissions',
657
+ message: 'Not enough permissions!',
658
+ },
659
+ ],
660
+ });
661
+ });
662
+
663
+ it('should pass options on validation to custom assertion', async () => {
664
+ let result;
665
+ const schema = yd.custom((val, { foo }) => {
666
+ result = foo;
667
+ });
668
+ await schema.validate(null, {
669
+ foo: 'bar',
670
+ });
671
+ expect(result).toBe('bar');
672
+ });
673
+ });
674
+
675
+ describe('array', () => {
676
+ it('should validate an optional array', async () => {
677
+ const schema = yd.array();
678
+ await assertPass(schema, []);
679
+ await assertPass(schema, ['a']);
680
+ await assertPass(schema, undefined);
681
+ await assertFail(schema, 1, ['Must be an array.']);
682
+ });
683
+
684
+ it('should validate a required array', async () => {
685
+ const schema = yd.array().required();
686
+ await assertPass(schema, []);
687
+ await assertPass(schema, ['a']);
688
+ await assertFail(schema, undefined, ['Value is required.']);
689
+ await assertFail(schema, 1, ['Must be an array.']);
690
+ });
691
+
692
+ it('should validate an array of strings', async () => {
693
+ const schema = yd.array(yd.string());
694
+ await assertPass(schema, []);
695
+ await assertPass(schema, ['a']);
696
+ await assertPass(schema, undefined);
697
+ await assertFail(schema, [1], ['Must be a string.']);
698
+ await assertFail(schema, 1, ['Must be an array.']);
699
+ });
700
+
701
+ it('should validate all elements', async () => {
702
+ await assertFail(
703
+ yd.array(yd.string()),
704
+ [1, 2],
705
+ ['Must be a string.', 'Must be a string.']
706
+ );
707
+ });
708
+
709
+ it('should contain details of assertion failures', async () => {
710
+ expect.assertions(1);
711
+ const schema = yd.array(yd.string());
712
+ try {
713
+ await schema.validate([1, 2]);
714
+ } catch (error) {
715
+ expect(error.details).toEqual([
716
+ new Error('Must be a string.'),
717
+ new Error('Must be a string.'),
718
+ ]);
719
+ }
720
+ });
721
+
722
+ it('should validate an array of different types', async () => {
723
+ const schema = yd.array(yd.string(), yd.number());
724
+ await assertPass(schema, []);
725
+ await assertPass(schema, ['a']);
726
+ await assertPass(schema, [1]);
727
+ await assertPass(schema, undefined);
728
+ await assertFail(schema, [true], ['Must be one of [string, number].']);
729
+ await assertFail(schema, [null], ['Must be one of [string, number].']);
730
+ });
731
+
732
+ it('should validate an array of different types with array', async () => {
733
+ const schema = yd.array([yd.string(), yd.number()]);
734
+ await assertPass(schema, []);
735
+ await assertPass(schema, ['a']);
736
+ await assertPass(schema, [1]);
737
+ await assertPass(schema, undefined);
738
+ await assertFail(schema, [true], ['Must be one of [string, number].']);
739
+ await assertFail(schema, [null], ['Must be one of [string, number].']);
740
+ });
741
+
742
+ it('should validate an array of objects', async () => {
743
+ const schema = yd.array(
744
+ yd.object({
745
+ foo: yd.string().required(),
746
+ })
747
+ );
748
+ await assertPass(schema, [{ foo: 'hi' }]);
749
+ await assertFail(schema, [{ bar: 'hi' }], ['Unknown field "bar".']);
750
+ });
751
+
752
+ it('should validate a minimum length', async () => {
753
+ const schema = yd.array().min(1);
754
+ await assertPass(schema, ['one']);
755
+ await assertFail(schema, [], ['Must contain at least 1 element.']);
756
+ });
757
+
758
+ it('should validate a maximum length', async () => {
759
+ const schema = yd.array().max(1);
760
+ await assertPass(schema, []);
761
+ await assertPass(schema, ['one']);
762
+ await assertFail(
763
+ schema,
764
+ ['one', 'two'],
765
+ ['Cannot contain more than 1 element.']
766
+ );
767
+ });
768
+
769
+ it('should a lat/lng tuple', async () => {
770
+ const schema = yd.array().latlng();
771
+ await assertPass(schema, [35, 139]);
772
+ await assertFail(schema, [], ['Must be an array of length 2.']);
773
+ await assertFail(schema, [35], ['Must be an array of length 2.']);
774
+ await assertFail(schema, [null, 139], ['Invalid latitude.']);
775
+ await assertFail(schema, [35, null], ['Invalid longitude.']);
776
+ await assertFail(schema, [100, 130], ['Invalid latitude.']);
777
+ await assertFail(schema, [35, 200], ['Invalid longitude.']);
778
+ });
779
+ });
780
+
781
+ describe('date', () => {
782
+ it('should validate an optional date', async () => {
783
+ const schema = yd.date();
784
+ await assertPass(schema, new Date());
785
+ await assertPass(schema, '2020-01-01');
786
+ await assertPass(schema, 1642232606911);
787
+ await assertPass(schema, undefined);
788
+ await assertPass(schema, 0);
789
+ await assertFail(schema, null, ['Must be a valid date input.']);
790
+ await assertFail(schema, false, ['Must be a valid date input.']);
791
+ await assertFail(schema, NaN, ['Must be a valid date input.']);
792
+ await assertFail(schema, 'invalid', ['Must be a valid date input.']);
793
+ });
794
+
795
+ it('should validate a required date', async () => {
796
+ const schema = yd.date().required();
797
+ await assertPass(schema, new Date());
798
+ await assertPass(schema, '2020-01-01');
799
+ await assertPass(schema, 1642232606911);
800
+ await assertPass(schema, 0);
801
+ await assertFail(schema, undefined, ['Value is required.']);
802
+ await assertFail(schema, null, ['Must be a valid date input.']);
803
+ await assertFail(schema, false, ['Must be a valid date input.']);
804
+ await assertFail(schema, NaN, ['Must be a valid date input.']);
805
+ await assertFail(schema, 'invalid', ['Must be a valid date input.']);
806
+ });
807
+
808
+ it('should validate an iso date', async () => {
809
+ const schema = yd.date().iso().required();
810
+ await assertPass(schema, '2022-01-15T08:27:36.114Z');
811
+ await assertPass(schema, '2022-01-15T08:27:36.114');
812
+ await assertPass(schema, '2022-01-15T08:27:36');
813
+ await assertPass(schema, '2022-01-15T08:27');
814
+ await assertPass(schema, '2022-01-15');
815
+ await assertPass(schema, '2022-01');
816
+
817
+ await assertFail(schema, new Date(), ['Must be a string.']);
818
+ await assertFail(schema, 1642232606911, ['Must be a string.']);
819
+ await assertFail(schema, undefined, ['Value is required.']);
820
+ await assertFail(schema, null, ['Must be a valid date input.']);
821
+ await assertFail(schema, false, ['Must be a valid date input.']);
822
+ await assertFail(schema, NaN, ['Must be a valid date input.']);
823
+ await assertFail(schema, 'invalid', ['Must be a valid date input.']);
824
+ await assertFail(schema, '01 Jan 1970 00:00:00 GMT', [
825
+ 'Must be in ISO 8601 format.',
826
+ ]);
827
+ });
828
+
829
+ it('should convert string to date', async () => {
830
+ const schema = yd.date();
831
+ const date = await schema.validate('2020-01-01');
832
+ expect(date).toBeInstanceOf(Date);
833
+ });
834
+
835
+ it('should validate a minimum date', async () => {
836
+ const schema = yd.date().min('2020-01-01');
837
+ await assertPass(schema, '2020-12-02');
838
+ await assertPass(schema, '2020-01-01');
839
+ await assertFail(schema, '2019-01-01', [
840
+ 'Must be after 2020-01-01T00:00:00.000Z.',
841
+ ]);
842
+ });
843
+
844
+ it('should validate a maximum date', async () => {
845
+ const schema = yd.date().max('2020-01-01');
846
+ await assertPass(schema, '2019-01-01');
847
+ await assertPass(schema, '2020-01-01');
848
+ await assertFail(schema, '2020-12-02', [
849
+ 'Must be before 2020-01-01T00:00:00.000Z.',
850
+ ]);
851
+ });
852
+
853
+ it('should validate a past date', async () => {
854
+ const schema = yd.date().past();
855
+ const future = new Date(Date.now() + 24 * 60 * 60 * 1000);
856
+ await assertPass(schema, '2019-01-01');
857
+ await assertFail(schema, future, ['Must be in the past.']);
858
+ });
859
+
860
+ it('should validate a future date', async () => {
861
+ const schema = yd.date().future();
862
+ const future = new Date(Date.now() + 24 * 60 * 60 * 1000);
863
+ await assertPass(schema, future);
864
+ await assertFail(schema, '2019-01-01', ['Must be in the future.']);
865
+ });
866
+
867
+ it('should validate a date before', async () => {
868
+ const schema = yd.date().before('2020-01-01');
869
+ await assertPass(schema, '2019-01-01');
870
+ await assertPass(schema, '2019-12-31');
871
+ await assertFail(schema, '2020-01-01', [
872
+ 'Must be before 2020-01-01T00:00:00.000Z.',
873
+ ]);
874
+ });
875
+
876
+ it('should validate a date after', async () => {
877
+ const schema = yd.date().after('2020-01-01');
878
+ await assertPass(schema, '2020-01-02');
879
+ await assertPass(schema, '2021-01-01');
880
+ await assertFail(schema, '2020-01-01', [
881
+ 'Must be after 2020-01-01T00:00:00.000Z.',
882
+ ]);
883
+ });
884
+
885
+ it('should validate a timestamp', async () => {
886
+ const schema = yd.date().timestamp();
887
+ await assertPass(schema, 1642342419713);
888
+ await assertFail(schema, '2019-01-01', [
889
+ 'Must be a timestamp in milliseconds.',
890
+ ]);
891
+ const now = new Date();
892
+ const val = await schema.validate(now.getTime());
893
+ expect(val).toEqual(now);
894
+ });
895
+
896
+ it('should validate a unix timestamp', async () => {
897
+ const schema = yd.date().unix();
898
+ await assertPass(schema, 1642342419713);
899
+ await assertFail(schema, '2019-01-01', ['Must be a timestamp in seconds.']);
900
+
901
+ const now = new Date();
902
+ const val = await schema.validate(now.getTime() / 1000);
903
+ expect(val).toEqual(now);
904
+ });
905
+ });
906
+
907
+ describe('default', () => {
908
+ it('should set a default value', async () => {
909
+ const schema = yd.any().default('a');
910
+ expect(await schema.validate()).toBe('a');
911
+ expect(await schema.validate(undefined)).toBe('a');
912
+ expect(await schema.validate(null)).toBe(null);
913
+ expect(await schema.validate('b')).toBe('b');
914
+ });
915
+
916
+ it('should set a default value in an object', async () => {
917
+ const schema = yd.object({
918
+ a: yd.any().default('a'),
919
+ b: yd.string(),
920
+ });
921
+ expect(await schema.validate()).toBe(undefined);
922
+ expect(await schema.validate({})).toEqual({ a: 'a' });
923
+ expect(await schema.validate({ a: undefined })).toEqual({ a: 'a' });
924
+ expect(await schema.validate({ a: null })).toEqual({ a: null });
925
+ expect(await schema.validate({ a: 'b' })).toEqual({ a: 'b' });
926
+ expect(await schema.validate({ b: 'b' })).toEqual({ a: 'a', b: 'b' });
927
+ });
928
+ });
929
+
930
+ describe('serialization', () => {
931
+ it('should correctly serialize object error', async () => {
932
+ const schema = yd.object({
933
+ a: yd.string().required(),
934
+ b: yd.string().required(),
935
+ });
936
+ let error;
937
+ try {
938
+ await schema.validate({
939
+ b: 1,
940
+ });
941
+ } catch (err) {
942
+ error = err;
943
+ }
944
+ expect(JSON.parse(JSON.stringify(error))).toEqual({
945
+ type: 'validation',
946
+ message: 'Object failed validation.',
947
+ details: [
948
+ {
949
+ type: 'field',
950
+ field: 'a',
951
+ message: 'Value is required.',
952
+ },
953
+ {
954
+ type: 'field',
955
+ field: 'b',
956
+ message: 'Must be a string.',
957
+ },
958
+ ],
959
+ });
960
+ });
961
+
962
+ it('should correctly serialize array error', async () => {
963
+ const schema = yd.array(yd.string());
964
+ let error;
965
+ try {
966
+ await schema.validate([1, 2]);
967
+ } catch (err) {
968
+ error = err;
969
+ }
970
+ expect(JSON.parse(JSON.stringify(error))).toEqual({
971
+ type: 'validation',
972
+ message: 'Array failed validation.',
973
+ details: [
974
+ {
975
+ type: 'element',
976
+ index: 0,
977
+ message: 'Must be a string.',
978
+ },
979
+ {
980
+ type: 'element',
981
+ index: 1,
982
+ message: 'Must be a string.',
983
+ },
984
+ ],
985
+ });
986
+ });
987
+
988
+ it('should correctly serialize password error', async () => {
989
+ const schema = yd.string().password({
990
+ minLength: 6,
991
+ minLowercase: 1,
992
+ minUppercase: 1,
993
+ minNumbers: 1,
994
+ minSymbols: 1,
995
+ });
996
+ let error;
997
+ try {
998
+ expect.assertions(1);
999
+ await schema.validate('');
1000
+ } catch (err) {
1001
+ error = err;
1002
+ }
1003
+ expect(JSON.parse(JSON.stringify(error))).toEqual({
1004
+ type: 'validation',
1005
+ message: 'Input failed validation.',
1006
+ details: [
1007
+ {
1008
+ type: 'password',
1009
+ message: 'Must be at least 6 characters.',
1010
+ },
1011
+ {
1012
+ type: 'password',
1013
+ message: 'Must contain at least 1 lowercase character.',
1014
+ },
1015
+ {
1016
+ type: 'password',
1017
+ message: 'Must contain at least 1 uppercase character.',
1018
+ },
1019
+ {
1020
+ type: 'password',
1021
+ message: 'Must contain at least 1 number.',
1022
+ },
1023
+ {
1024
+ type: 'password',
1025
+ message: 'Must contain at least 1 symbol.',
1026
+ },
1027
+ ],
1028
+ });
1029
+ });
1030
+ });
1031
+
1032
+ describe('isSchema', () => {
1033
+ it('should correctly identify a schema', () => {
1034
+ expect(isSchema(yd.string())).toBe(true);
1035
+ expect(isSchema(yd.date())).toBe(true);
1036
+ expect(isSchema(yd.object({}))).toBe(true);
1037
+ expect(isSchema(yd.custom(() => {}))).toBe(true);
1038
+ expect(isSchema(undefined)).toBe(false);
1039
+ expect(isSchema(null)).toBe(false);
1040
+ expect(isSchema({})).toBe(false);
1041
+ expect(isSchema('a')).toBe(false);
1042
+ });
1043
+ });
1044
+
1045
+ describe('isSchemaError', () => {
1046
+ it('should correctly identify a schema error', async () => {
1047
+ let error;
1048
+ try {
1049
+ await yd.string().validate(1);
1050
+ } catch (err) {
1051
+ error = err;
1052
+ }
1053
+ expect(isSchemaError(error)).toBe(true);
1054
+ expect(isSchemaError(new Error())).toBe(false);
1055
+ });
1056
+ });
1057
+
1058
+ describe('toOpenApi', () => {
1059
+ it('should convert a string schema', async () => {
1060
+ expect(yd.string().toOpenApi()).toEqual({
1061
+ type: 'string',
1062
+ });
1063
+ expect(yd.string().required().toOpenApi()).toEqual({
1064
+ type: 'string',
1065
+ required: true,
1066
+ });
1067
+ expect(yd.string().default('foo').toOpenApi()).toEqual({
1068
+ type: 'string',
1069
+ default: 'foo',
1070
+ });
1071
+ expect(yd.string().allow('foo', 'bar').toOpenApi()).toEqual({
1072
+ type: 'string',
1073
+ enum: ['foo', 'bar'],
1074
+ });
1075
+ expect(yd.string().email().toOpenApi()).toEqual({
1076
+ type: 'string',
1077
+ format: 'email',
1078
+ });
1079
+ });
1080
+
1081
+ it('should convert an object schema', async () => {
1082
+ expect(yd.object().toOpenApi()).toEqual({
1083
+ type: 'object',
1084
+ });
1085
+ expect(yd.object({ foo: yd.string() }).toOpenApi()).toEqual({
1086
+ type: 'object',
1087
+ properties: {
1088
+ foo: {
1089
+ type: 'string',
1090
+ },
1091
+ },
1092
+ });
1093
+ });
1094
+
1095
+ it('should convert an array schema', async () => {
1096
+ expect(yd.array().toOpenApi()).toEqual({
1097
+ type: 'array',
1098
+ });
1099
+ expect(yd.array(yd.string()).toOpenApi()).toEqual({
1100
+ type: 'array',
1101
+ items: {
1102
+ type: 'string',
1103
+ },
1104
+ });
1105
+ expect(yd.array(yd.string(), yd.number()).toOpenApi()).toEqual({
1106
+ type: 'array',
1107
+ oneOf: [
1108
+ {
1109
+ type: 'string',
1110
+ },
1111
+ {
1112
+ type: 'number',
1113
+ },
1114
+ ],
1115
+ });
1116
+ });
1117
+
1118
+ it('should convert enum types', async () => {
1119
+ const schema = yd.allow(yd.string(), yd.array(yd.string()));
1120
+ expect(schema.toOpenApi()).toEqual({
1121
+ oneOf: [
1122
+ {
1123
+ type: 'string',
1124
+ },
1125
+
1126
+ {
1127
+ type: 'array',
1128
+ items: {
1129
+ type: 'string',
1130
+ },
1131
+ },
1132
+ ],
1133
+ });
1134
+ });
1135
+
1136
+ it('should convert string enum types', async () => {
1137
+ const schema = yd.string().allow('foo', 'bar');
1138
+ expect(schema.toOpenApi()).toEqual({
1139
+ type: 'string',
1140
+ enum: ['foo', 'bar'],
1141
+ });
1142
+ });
1143
+
1144
+ it('should convert mixed enum types', async () => {
1145
+ const schema = yd.allow(1, 2, yd.string());
1146
+ expect(schema.toOpenApi()).toEqual({
1147
+ oneOf: [
1148
+ {
1149
+ type: 'number',
1150
+ enum: [1, 2],
1151
+ },
1152
+ {
1153
+ type: 'string',
1154
+ },
1155
+ ],
1156
+ });
1157
+ });
1158
+
1159
+ it('should convert date formats', async () => {
1160
+ let schema = yd.date().iso();
1161
+ expect(schema.toOpenApi()).toEqual({
1162
+ type: 'string',
1163
+ format: 'date-time',
1164
+ });
1165
+
1166
+ schema = yd.date().iso('date');
1167
+ expect(schema.toOpenApi()).toEqual({
1168
+ type: 'string',
1169
+ format: 'date',
1170
+ });
1171
+
1172
+ schema = yd.date().timestamp();
1173
+ expect(schema.toOpenApi()).toEqual({
1174
+ type: 'number',
1175
+ format: 'timestamp',
1176
+ });
1177
+
1178
+ schema = yd.date().unix();
1179
+ expect(schema.toOpenApi()).toEqual({
1180
+ type: 'number',
1181
+ format: 'unix timestamp',
1182
+ });
1183
+ });
1184
+
1185
+ it('should convert number min/max', async () => {
1186
+ let schema = yd.number().min(5).max(50);
1187
+ expect(schema.toOpenApi()).toEqual({
1188
+ type: 'number',
1189
+ minimum: 5,
1190
+ maximum: 50,
1191
+ });
1192
+
1193
+ schema = yd.number().multiple(5);
1194
+ expect(schema.toOpenApi()).toEqual({
1195
+ type: 'number',
1196
+ multipleOf: 5,
1197
+ });
1198
+ });
1199
+
1200
+ it('should convert string minLength/maxLength', async () => {
1201
+ const schema = yd.string().min(5).max(50);
1202
+ expect(schema.toOpenApi()).toEqual({
1203
+ type: 'string',
1204
+ minLength: 5,
1205
+ maxLength: 50,
1206
+ });
1207
+ });
1208
+
1209
+ it('should allow tagging a schema', async () => {
1210
+ const schema = yd
1211
+ .object({
1212
+ num: yd.number(),
1213
+ str: yd.string(),
1214
+ })
1215
+ .tag({
1216
+ 'x-schema': 'my-schema',
1217
+ });
1218
+
1219
+ expect(schema.toOpenApi()).toEqual({
1220
+ type: 'object',
1221
+ properties: {
1222
+ num: {
1223
+ type: 'number',
1224
+ },
1225
+ str: {
1226
+ type: 'string',
1227
+ },
1228
+ },
1229
+ 'x-schema': 'my-schema',
1230
+ });
1231
+ });
1232
+
1233
+ it('should allow a description as a shortcut', async () => {
1234
+ const schema = yd
1235
+ .object({
1236
+ num: yd.number(),
1237
+ str: yd.string(),
1238
+ })
1239
+ .description('My Schema!');
1240
+
1241
+ expect(schema.toOpenApi()).toEqual({
1242
+ type: 'object',
1243
+ description: 'My Schema!',
1244
+ properties: {
1245
+ num: {
1246
+ type: 'number',
1247
+ },
1248
+ str: {
1249
+ type: 'string',
1250
+ },
1251
+ },
1252
+ });
1253
+ });
1254
+
1255
+ it('should not override other tags', async () => {
1256
+ const schema = yd
1257
+ .string()
1258
+ .tag({
1259
+ 'x-schema': 'my-schema',
1260
+ })
1261
+ .description('My Schema!');
1262
+
1263
+ expect(schema.toOpenApi()).toEqual({
1264
+ type: 'string',
1265
+ 'x-schema': 'my-schema',
1266
+ description: 'My Schema!',
1267
+ });
1268
+ });
1269
+
1270
+ it('should be able to set metadata in the method', async () => {
1271
+ const schema = yd.string();
1272
+ expect(
1273
+ schema.toOpenApi({
1274
+ 'x-schema': 'my-schema',
1275
+ description: 'My Schema!',
1276
+ })
1277
+ ).toEqual({
1278
+ type: 'string',
1279
+ 'x-schema': 'my-schema',
1280
+ description: 'My Schema!',
1281
+ });
1282
+ });
1283
+ });
1284
+
1285
+ describe('options', () => {
1286
+ describe('stripUnknown', () => {
1287
+ it('should optionally strip out unknown keys', async () => {
1288
+ const schema = yd.object({
1289
+ a: yd.string(),
1290
+ b: yd.string(),
1291
+ });
1292
+
1293
+ const options = {
1294
+ stripUnknown: true,
1295
+ };
1296
+
1297
+ await assertPass(schema, undefined, undefined, options);
1298
+ await assertPass(schema, {}, {}, options);
1299
+
1300
+ await assertPass(
1301
+ schema,
1302
+ { a: 'a', b: 'b', c: 'c' },
1303
+ { a: 'a', b: 'b' },
1304
+ options
1305
+ );
1306
+ });
1307
+
1308
+ it('should respect preceeding append and custom validations', async () => {
1309
+ const schema = yd
1310
+ .object({
1311
+ a: yd.string(),
1312
+ b: yd.string(),
1313
+ })
1314
+ .append({
1315
+ c: yd.string(),
1316
+ })
1317
+ .custom((val) => {
1318
+ if (Object.keys(val).length === 0) {
1319
+ throw new Error('Object must not be empty.');
1320
+ }
1321
+ });
1322
+
1323
+ const options = {
1324
+ stripUnknown: true,
1325
+ };
1326
+
1327
+ await assertPass(
1328
+ schema,
1329
+ {
1330
+ a: 'a',
1331
+ b: 'b',
1332
+ c: 'c',
1333
+ d: 'd',
1334
+ },
1335
+ {
1336
+ a: 'a',
1337
+ b: 'b',
1338
+ c: 'c',
1339
+ },
1340
+ options
1341
+ );
1342
+ await assertFail(schema, {}, ['Object must not be empty.']);
1343
+ });
1344
+
1345
+ it('should respect following append and custom validations', async () => {
1346
+ const schema = yd
1347
+ .object({
1348
+ a: yd.string(),
1349
+ b: yd.string(),
1350
+ })
1351
+ .custom((val) => {
1352
+ if (Object.keys(val).length === 0) {
1353
+ throw new Error('Object must not be empty.');
1354
+ }
1355
+ })
1356
+ .append({
1357
+ c: yd.string(),
1358
+ });
1359
+
1360
+ const options = {
1361
+ stripUnknown: true,
1362
+ };
1363
+
1364
+ await assertPass(
1365
+ schema,
1366
+ {
1367
+ a: 'a',
1368
+ b: 'b',
1369
+ c: 'c',
1370
+ d: 'd',
1371
+ },
1372
+ {
1373
+ a: 'a',
1374
+ b: 'b',
1375
+ c: 'c',
1376
+ },
1377
+ options
1378
+ );
1379
+ await assertFail(schema, {}, ['Object must not be empty.']);
1380
+ });
1381
+ });
1382
+
1383
+ describe('casting', () => {
1384
+ it('should cast a boolean', async () => {
1385
+ const schema = yd.boolean();
1386
+ const options = {
1387
+ cast: true,
1388
+ };
1389
+ await assertPass(schema, 'true', true, options);
1390
+ await assertPass(schema, 'True', true, options);
1391
+ await assertPass(schema, 'false', false, options);
1392
+ await assertPass(schema, 'False', false, options);
1393
+ await assertPass(schema, '0', false, options);
1394
+ await assertPass(schema, '1', true, options);
1395
+ await assertFail(schema, 'foo', ['Must be a boolean.']);
1396
+ });
1397
+
1398
+ it('should cast a nested boolean', async () => {
1399
+ const schema = yd.object({
1400
+ a: yd.boolean(),
1401
+ });
1402
+ const options = {
1403
+ cast: true,
1404
+ };
1405
+ await assertPass(schema, { a: 'true' }, { a: true }, options);
1406
+ await assertPass(schema, { a: 'True' }, { a: true }, options);
1407
+ await assertPass(schema, { a: 'false' }, { a: false }, options);
1408
+ await assertPass(schema, { a: 'False' }, { a: false }, options);
1409
+ await assertFail(schema, { a: 'foo' }, ['Must be a boolean.']);
1410
+ });
1411
+
1412
+ it('should not cast a boolean without flag', async () => {
1413
+ const schema = yd.boolean();
1414
+ await assertFail(schema, 'true', ['Must be a boolean.']);
1415
+ });
1416
+
1417
+ it('should cast a number', async () => {
1418
+ const schema = yd.number();
1419
+ const options = {
1420
+ cast: true,
1421
+ };
1422
+ await assertPass(schema, '0', 0, options);
1423
+ await assertPass(schema, '1', 1, options);
1424
+ await assertPass(schema, '1.1', 1.1, options);
1425
+ await assertFail(schema, 'foo', ['Must be a number.']);
1426
+ await assertFail(schema, 'null', ['Must be a number.']);
1427
+ });
1428
+
1429
+ it('should cast a nested number', async () => {
1430
+ const schema = yd.object({
1431
+ a: yd.number(),
1432
+ });
1433
+ const options = {
1434
+ cast: true,
1435
+ };
1436
+ await assertPass(schema, { a: '0' }, { a: 0 }, options);
1437
+ await assertPass(schema, { a: '1' }, { a: 1 }, options);
1438
+ await assertPass(schema, { a: '1.1' }, { a: 1.1 }, options);
1439
+ await assertFail(schema, { a: 'foo' }, ['Must be a number.']);
1440
+ await assertFail(schema, { a: 'null' }, ['Must be a number.']);
1441
+ });
1442
+
1443
+ it('should not cast a number without flag', async () => {
1444
+ const schema = yd.number();
1445
+ await assertFail(schema, '0', ['Must be a number.']);
1446
+ });
1447
+
1448
+ it('should cast to an array from commas', async () => {
1449
+ const schema = yd.object({
1450
+ a: yd.array(),
1451
+ b: yd.string(),
1452
+ });
1453
+ const options = {
1454
+ cast: true,
1455
+ };
1456
+ const result = await schema.validate({ a: 'a,b,c', b: 'b' }, options);
1457
+ expect(result.a).toEqual(['a', 'b', 'c']);
1458
+ expect(result.b).toBe('b');
1459
+ });
1460
+
1461
+ it('should cast to an array of specific type', async () => {
1462
+ const schema = yd.object({
1463
+ a: yd.array(yd.number()),
1464
+ b: yd.string(),
1465
+ });
1466
+ const options = {
1467
+ cast: true,
1468
+ };
1469
+ const result = await schema.validate({ a: '1,2,3', b: 'b' }, options);
1470
+ expect(result.a).toEqual([1, 2, 3]);
1471
+ expect(result.b).toBe('b');
1472
+ });
1473
+
1474
+ it('should not cast to an array without flag', async () => {
1475
+ const schema = yd.object({
1476
+ a: yd.array(yd.number()),
1477
+ b: yd.string(),
1478
+ });
1479
+ await assertFail(schema, { a: '1,2,3', b: 'b' }, ['Must be an array.']);
1480
+ });
1481
+
1482
+ it('should cast a string', async () => {
1483
+ const schema = yd.string();
1484
+ const options = {
1485
+ cast: true,
1486
+ };
1487
+ await assertPass(schema, '1', '1', options);
1488
+ await assertPass(schema, 1, '1', options);
1489
+ });
1490
+ });
1491
+
1492
+ describe('chaining', () => {
1493
+ it('should allow options to be burned in with chained method', async () => {
1494
+ const schema = yd
1495
+ .object({
1496
+ a: yd.array(yd.number()),
1497
+ b: yd.number(),
1498
+ })
1499
+ .options({
1500
+ cast: true,
1501
+ stripUnknown: true,
1502
+ });
1503
+
1504
+ await assertPass(
1505
+ schema,
1506
+ {
1507
+ a: '1,2,3',
1508
+ b: '4',
1509
+ c: '5',
1510
+ },
1511
+ {
1512
+ a: [1, 2, 3],
1513
+ b: 4,
1514
+ }
1515
+ );
1516
+
1517
+ await assertFail(
1518
+ schema,
1519
+ {
1520
+ b: 'b',
1521
+ },
1522
+ ['Must be a number.']
1523
+ );
1524
+ });
1525
+ });
1526
+ });
1527
+
1528
+ describe('other', () => {
1529
+ it('should provide a default error message', async () => {
1530
+ await assertErrorMessage(yd.string(), 3, 'Input failed validation.');
1531
+ await assertErrorMessage(yd.object(), 3, 'Object failed validation.');
1532
+ });
1533
+
1534
+ it('should allow a custom message', async () => {
1535
+ const schema = yd.string().message('Needs a string');
1536
+ await assertErrorMessage(schema, 3, 'Needs a string');
1537
+ });
1538
+
1539
+ it('should have access to root object', async () => {
1540
+ const schema = yd.object({
1541
+ a: yd.array(yd.number()),
1542
+ b: yd.number().custom((arr, { root }) => {
1543
+ if (!root.a.includes(root.b)) {
1544
+ throw new Error('"a" must include "b"');
1545
+ }
1546
+ }),
1547
+ });
1548
+ await assertPass(schema, { a: [1, 2, 3], b: 1 });
1549
+ await assertFail(schema, { a: [1, 2, 3], b: 4 }, ['"a" must include "b"']);
1550
+ });
1551
+
1552
+ it('should correctly validate chained formats', async () => {
1553
+ let schema;
1554
+
1555
+ schema = yd.string().lowercase().email();
1556
+ await assertPass(schema, undefined, undefined);
1557
+ await assertPass(schema, 'bar@foo.com', 'bar@foo.com');
1558
+ await assertPass(schema, 'BAR@FOO.COM', 'bar@foo.com');
1559
+
1560
+ schema = yd.string().lowercase().email().default('Foo@bar.com');
1561
+ await assertPass(schema, undefined, 'foo@bar.com');
1562
+ await assertPass(schema, 'bar@foo.com', 'bar@foo.com');
1563
+ await assertPass(schema, 'BAR@foo.com', 'bar@foo.com');
1564
+
1565
+ schema = yd.string().required().email().default('foo@bar.com');
1566
+ await assertPass(schema, undefined, 'foo@bar.com');
1567
+ await assertPass(schema, 'bar@foo.com', 'bar@foo.com');
1568
+ await assertPass(schema, 'BAR@foo.com', 'BAR@foo.com');
1569
+ });
1570
+
1571
+ it('should expose original error', async () => {
1572
+ const err = new Error('Bad!');
1573
+ const schema = yd.custom(() => {
1574
+ throw err;
1575
+ });
1576
+ try {
1577
+ await schema.validate('test');
1578
+ } catch (error) {
1579
+ expect(error.details[0].original).toBe(err);
1580
+ expect(JSON.parse(JSON.stringify(error))).toEqual({
1581
+ type: 'validation',
1582
+ message: 'Input failed validation.',
1583
+ details: [
1584
+ {
1585
+ message: 'Bad!',
1586
+ type: 'custom',
1587
+ },
1588
+ ],
1589
+ });
1590
+ }
1591
+ });
1592
+
1593
+ it('should expose original error on field', async () => {
1594
+ const err = new Error('Bad!');
1595
+ const schema = yd.object({
1596
+ a: yd.custom(() => {
1597
+ throw err;
1598
+ }),
1599
+ });
1600
+ try {
1601
+ await schema.validate({
1602
+ a: 'test',
1603
+ });
1604
+ } catch (error) {
1605
+ expect(error.details[0].original).toBe(err);
1606
+ expect(JSON.parse(JSON.stringify(error))).toEqual({
1607
+ type: 'validation',
1608
+ message: 'Object failed validation.',
1609
+ details: [
1610
+ {
1611
+ type: 'field',
1612
+ field: 'a',
1613
+ message: 'Bad!',
1614
+ },
1615
+ ],
1616
+ });
1617
+ }
1618
+ });
1619
+ });
1620
+
1621
+ describe('getFullMessage', () => {
1622
+ it('should get full error message', async () => {
1623
+ const schema = yd.object({
1624
+ a: yd.string(),
1625
+ b: yd.number(),
1626
+ });
1627
+
1628
+ let error;
1629
+ try {
1630
+ await schema.validate({
1631
+ a: 1,
1632
+ b: 'a',
1633
+ });
1634
+ } catch (err) {
1635
+ error = err;
1636
+ }
1637
+ expect(error.getFullMessage()).toBe(
1638
+ '"a" must be a string. "b" must be a number.'
1639
+ );
1640
+ });
1641
+
1642
+ it('should get full error message with delimiter', async () => {
1643
+ const schema = yd.object({
1644
+ a: yd.string(),
1645
+ b: yd.number(),
1646
+ });
1647
+
1648
+ let error;
1649
+ try {
1650
+ await schema.validate({
1651
+ a: 1,
1652
+ b: 'a',
1653
+ });
1654
+ } catch (err) {
1655
+ error = err;
1656
+ }
1657
+ expect(
1658
+ error.getFullMessage({
1659
+ delimiter: '\n',
1660
+ })
1661
+ ).toBe('"a" must be a string.\n"b" must be a number.');
1662
+ });
1663
+
1664
+ it('should get full error message for password fields', async () => {
1665
+ const schema = yd.object({
1666
+ password: yd.string().password({
1667
+ minLength: 12,
1668
+ minNumbers: 1,
1669
+ }),
1670
+ });
1671
+
1672
+ let error;
1673
+ try {
1674
+ await schema.validate({ password: 'a' });
1675
+ } catch (err) {
1676
+ error = err;
1677
+ }
1678
+ expect(error.getFullMessage()).toBe(
1679
+ '"password" must be at least 12 characters. "password" must contain at least 1 number.'
1680
+ );
1681
+ });
1682
+
1683
+ it('should get full error message with natural fields', async () => {
1684
+ const schema = yd.object({
1685
+ authCode: yd.string().required(),
1686
+ pass_code: yd.string().required(),
1687
+ 'my-token': yd.string().required(),
1688
+ });
1689
+
1690
+ let error;
1691
+ try {
1692
+ await schema.validate({});
1693
+ } catch (err) {
1694
+ error = err;
1695
+ }
1696
+ expect(
1697
+ error.getFullMessage({
1698
+ natural: true,
1699
+ })
1700
+ ).toBe(
1701
+ 'Auth code is required. Pass code is required. My token is required.'
1702
+ );
1703
+ });
1704
+ });
1705
+
1706
+ describe('localization', () => {
1707
+ it('should be able to pass an object to useLocalizer', async () => {
1708
+ yd.useLocalizer({
1709
+ 'Must be a string.': 'Gotta be a string.',
1710
+ });
1711
+ let error;
1712
+ try {
1713
+ await yd.string().validate(1);
1714
+ } catch (err) {
1715
+ error = err;
1716
+ }
1717
+ expect(error.details[0].message).toBe('Gotta be a string.');
1718
+ });
1719
+
1720
+ it('should be able to pass a function to useLocalizer', async () => {
1721
+ const strings = {
1722
+ 'Must be at least {length} character{s}.':
1723
+ '{length}文字以上入力して下さい。',
1724
+ 'Object failed validation.': '不正な入力がありました。',
1725
+ };
1726
+ yd.useLocalizer((template) => {
1727
+ return strings[template];
1728
+ });
1729
+ const schema = yd.object({
1730
+ password: yd.string().password({
1731
+ minLength: 6,
1732
+ minNumbers: 1,
1733
+ }),
1734
+ });
1735
+
1736
+ let error;
1737
+ try {
1738
+ await schema.validate({
1739
+ password: 'a',
1740
+ });
1741
+ } catch (err) {
1742
+ error = err;
1743
+ }
1744
+ expect(error.message).toBe('不正な入力がありました。');
1745
+ expect(error.details[0].details[0].message).toBe(
1746
+ '6文字以上入力して下さい。'
1747
+ );
1748
+ });
1749
+
1750
+ it('should be able to pass a function for complex localizations', async () => {
1751
+ const strings = {
1752
+ 'Must be at least {length} character{s}.': ({ length }) => {
1753
+ const chars = length === 1 ? 'carattere' : 'caratteri';
1754
+ return `Deve contenere almeno ${length} ${chars}.`;
1755
+ },
1756
+ };
1757
+ yd.useLocalizer((template) => {
1758
+ return strings[template];
1759
+ });
1760
+ const schema = yd.object({
1761
+ password: yd.string().password({
1762
+ minLength: 6,
1763
+ }),
1764
+ });
1765
+
1766
+ let error;
1767
+ try {
1768
+ await schema.validate({
1769
+ password: 'a',
1770
+ });
1771
+ } catch (err) {
1772
+ error = err;
1773
+ }
1774
+ expect(error.details[0].message).toBe('Deve contenere almeno 6 caratteri.');
1775
+ });
1776
+
1777
+ it('should be able to inspect localization templates', async () => {
1778
+ yd.useLocalizer({
1779
+ 'Input failed validation.': '不正な入力がありました。',
1780
+ });
1781
+ const schema = yd.object({
1782
+ password: yd.string().password({
1783
+ minLength: 6,
1784
+ minNumbers: 1,
1785
+ }),
1786
+ });
1787
+ try {
1788
+ await schema.validate({
1789
+ password: 'a',
1790
+ });
1791
+ } catch (err) {
1792
+ const templates = yd.getLocalizerTemplates();
1793
+ expect(templates).toEqual({
1794
+ 'Must be at least {length} character{s}.':
1795
+ 'Must be at least {length} character{s}.',
1796
+ 'Must contain at least {length} number{s}.':
1797
+ 'Must contain at least {length} number{s}.',
1798
+ 'Input failed validation.': '不正な入力がありました。',
1799
+ 'Field failed validation.': 'Field failed validation.',
1800
+ 'Object failed validation.': 'Object failed validation.',
1801
+ });
1802
+ }
1803
+ });
1804
+
1805
+ it('should localize the full message', async () => {
1806
+ yd.useLocalizer({
1807
+ '{field} must be a string.': '{field}: 文字列を入力してください。',
1808
+ '{field} must be a number.': '{field}: 数字を入力してください。',
1809
+ });
1810
+ const schema = yd.object({
1811
+ name: yd.string(),
1812
+ age: yd.number(),
1813
+ });
1814
+ try {
1815
+ await schema.validate({
1816
+ name: 1,
1817
+ age: '15',
1818
+ });
1819
+ } catch (err) {
1820
+ expect(err.getFullMessage()).toBe(
1821
+ '"name": 文字列を入力してください。 "age": 数字を入力してください。'
1822
+ );
1823
+ expect(yd.getLocalizerTemplates()).toMatchObject({
1824
+ '{field} must be a number.': '{field}: 数字を入力してください。',
1825
+ '{field} must be a string.': '{field}: 文字列を入力してください。',
1826
+ });
1827
+ }
1828
+ });
1829
+
1830
+ it('should allow a custom localized error to be thrown', async () => {
1831
+ yd.useLocalizer({
1832
+ 'Invalid coordinates': '座標に不正な入力がありました。',
1833
+ });
1834
+ const schema = yd.custom(() => {
1835
+ throw new yd.LocalizedError('Invalid coordinates');
1836
+ });
1837
+ try {
1838
+ await schema.validate({
1839
+ coords: 'coords',
1840
+ });
1841
+ } catch (err) {
1842
+ expect(JSON.parse(JSON.stringify(err))).toEqual({
1843
+ type: 'validation',
1844
+ message: 'Input failed validation.',
1845
+ details: [
1846
+ {
1847
+ type: 'custom',
1848
+ message: '座標に不正な入力がありました。',
1849
+ },
1850
+ ],
1851
+ });
1852
+ expect(err.getFullMessage()).toBe('座標に不正な入力がありました。');
1853
+ }
1854
+ });
1855
+ });