@schmock/schema 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,1208 @@
1
+ import type { JSONSchema7 } from "json-schema";
2
+ import { describe, expect, it } from "vitest";
3
+ import { generateFromSchema, schemaPlugin } from "./index";
4
+ import {
5
+ generate,
6
+ performance as perf,
7
+ schemas,
8
+ schemaTests,
9
+ stats,
10
+ validators,
11
+ } from "./test-utils";
12
+
13
+ describe("Schema Generator", () => {
14
+ describe("Core Functionality", () => {
15
+ it("generates data from simple schemas", () => {
16
+ const result = generateFromSchema({
17
+ schema: schemas.simple.object({
18
+ id: schemas.simple.number(),
19
+ name: schemas.simple.string(),
20
+ }),
21
+ });
22
+
23
+ expect(result).toHaveProperty("id");
24
+ expect(result).toHaveProperty("name");
25
+ expect(typeof result.id).toBe("number");
26
+ expect(typeof result.name).toBe("string");
27
+ });
28
+
29
+ it("generates arrays with specified count", () => {
30
+ const schema = schemas.simple.array(
31
+ schemas.simple.object({ id: schemas.simple.number() }),
32
+ );
33
+ const result = generateFromSchema({ schema, count: 5 });
34
+
35
+ expect(Array.isArray(result)).toBe(true);
36
+ expect(result).toHaveLength(5);
37
+ result.forEach((item) => {
38
+ expect(item).toHaveProperty("id");
39
+ expect(typeof item.id).toBe("number");
40
+ });
41
+ });
42
+
43
+ it("respects array constraints from schema", () => {
44
+ const schema = schemas.simple.array(schemas.simple.string(), {
45
+ minItems: 2,
46
+ maxItems: 5,
47
+ });
48
+ const results = generate.samples<string[]>(schema, 20);
49
+
50
+ results.forEach((result) => {
51
+ expect(result.length).toBeGreaterThanOrEqual(2);
52
+ expect(result.length).toBeLessThanOrEqual(5);
53
+ });
54
+ });
55
+
56
+ it("handles nested schemas correctly", () => {
57
+ const schema = schemas.nested.deep(3, schemas.simple.string());
58
+ const result = generateFromSchema({ schema });
59
+
60
+ expect(result).toHaveProperty("nested");
61
+ expect(result.nested).toHaveProperty("nested");
62
+ expect(result.nested.nested).toHaveProperty("nested");
63
+ expect(typeof result.nested.nested.nested).toBe("string");
64
+ });
65
+
66
+ it("generates consistent data types", () => {
67
+ const schema = schemas.complex.apiResponse();
68
+ const samples = generate.samples(schema, 10);
69
+
70
+ samples.forEach((sample) => {
71
+ expect(typeof sample.success).toBe("boolean");
72
+ expect(Array.isArray(sample.data)).toBe(true);
73
+ expect(typeof sample.meta.page).toBe("number");
74
+ expect(typeof sample.meta.total).toBe("number");
75
+ });
76
+ });
77
+ });
78
+
79
+ describe("Schema Validation", () => {
80
+ describe("invalid schemas", () => {
81
+ it("rejects empty schema objects", () => {
82
+ schemaTests.expectInvalid({}, "Schema cannot be empty");
83
+ });
84
+
85
+ it("rejects null and undefined schemas", () => {
86
+ schemaTests.expectInvalid(
87
+ null,
88
+ "Schema must be a valid JSON Schema object",
89
+ );
90
+ schemaTests.expectInvalid(
91
+ undefined,
92
+ "Schema must be a valid JSON Schema object",
93
+ );
94
+ });
95
+
96
+ it("rejects non-object schema types", () => {
97
+ schemaTests.expectInvalid(
98
+ "string",
99
+ "Schema must be a valid JSON Schema object",
100
+ );
101
+ schemaTests.expectInvalid(
102
+ 123,
103
+ "Schema must be a valid JSON Schema object",
104
+ );
105
+ schemaTests.expectInvalid(
106
+ true,
107
+ "Schema must be a valid JSON Schema object",
108
+ );
109
+ schemaTests.expectInvalid([], "Schema cannot be empty");
110
+ });
111
+
112
+ it("rejects invalid type values", () => {
113
+ schemaTests.expectSchemaError(
114
+ { type: "invalid" },
115
+ "$",
116
+ 'Invalid schema type: "invalid"',
117
+ );
118
+ });
119
+
120
+ it("rejects malformed object properties", () => {
121
+ schemaTests.expectSchemaError(
122
+ { type: "object", properties: "invalid" },
123
+ "$.properties",
124
+ "Properties must be an object mapping",
125
+ );
126
+ });
127
+
128
+ it("rejects arrays without items", () => {
129
+ schemaTests.expectSchemaError(
130
+ { type: "array", items: null },
131
+ "$.items",
132
+ "Array schema must have valid items definition",
133
+ );
134
+ });
135
+
136
+ it("validates nested schemas recursively", () => {
137
+ schemaTests.expectSchemaError(
138
+ {
139
+ type: "object",
140
+ properties: {
141
+ nested: {
142
+ type: "object",
143
+ properties: {
144
+ bad: { type: "invalid" },
145
+ },
146
+ },
147
+ },
148
+ },
149
+ "$.properties.nested.properties.bad",
150
+ "Invalid schema type",
151
+ );
152
+ });
153
+ });
154
+
155
+ describe("resource limits", () => {
156
+ it("enforces array size limits", () => {
157
+ const schema = schemas.simple.array(schemas.simple.string(), {
158
+ maxItems: 50000,
159
+ });
160
+ expect(() => generateFromSchema({ schema })).toThrow("array_max_items");
161
+ });
162
+
163
+ it("enforces nesting depth limits", () => {
164
+ const deepSchema = schemas.nested.deep(15);
165
+ expect(() => generateFromSchema({ schema: deepSchema })).toThrow(
166
+ "schema_nesting_depth",
167
+ );
168
+ });
169
+
170
+ it("detects circular references", () => {
171
+ // Create a circular reference
172
+ const schema: any = {
173
+ type: "object",
174
+ properties: {
175
+ self: { $ref: "#" },
176
+ },
177
+ };
178
+
179
+ schemaTests.expectInvalid(schema, /circular/i);
180
+ });
181
+
182
+ it("prevents memory exhaustion from deep nesting with large arrays", () => {
183
+ const schema = {
184
+ type: "object",
185
+ properties: {
186
+ level1: {
187
+ type: "object",
188
+ properties: {
189
+ level2: {
190
+ type: "object",
191
+ properties: {
192
+ level3: {
193
+ type: "object",
194
+ properties: {
195
+ level4: {
196
+ type: "array",
197
+ items: { type: "string" },
198
+ maxItems: 1000,
199
+ },
200
+ },
201
+ },
202
+ },
203
+ },
204
+ },
205
+ },
206
+ },
207
+ };
208
+
209
+ expect(() => generateFromSchema({ schema })).toThrow(
210
+ /memory|deep_nesting/,
211
+ );
212
+ });
213
+ });
214
+
215
+ describe("edge cases", () => {
216
+ it("handles schemas without explicit type", () => {
217
+ const schema = { properties: { name: { type: "string" as const } } };
218
+ schemaTests.expectValid(schema);
219
+ });
220
+
221
+ it("handles empty properties object", () => {
222
+ const schema = { type: "object" as const, properties: {} };
223
+ const result = generateFromSchema({ schema });
224
+ expect(typeof result).toBe("object");
225
+ });
226
+
227
+ it("handles array with multiple item types", () => {
228
+ const schema = {
229
+ type: "array" as const,
230
+ items: [
231
+ { type: "string" as const },
232
+ { type: "number" as const },
233
+ { type: "boolean" as const },
234
+ ],
235
+ };
236
+ schemaTests.expectValid(schema);
237
+ });
238
+ });
239
+ });
240
+
241
+ describe("Smart Field Mapping", () => {
242
+ describe("mapping behavior", () => {
243
+ it("maps email fields to appropriate generator", async () => {
244
+ const samples = generate.samples<any>(
245
+ schemas.simple.object({
246
+ email: schemas.simple.string(),
247
+ userEmail: schemas.simple.string(),
248
+ contact_email: schemas.simple.string(),
249
+ }),
250
+ 20,
251
+ );
252
+
253
+ // All email fields should contain @ and .
254
+ samples.forEach((sample) => {
255
+ expect(
256
+ validators.appearsToBeFromCategory([sample.email], "email"),
257
+ ).toBe(true);
258
+ expect(
259
+ validators.appearsToBeFromCategory([sample.userEmail], "email"),
260
+ ).toBe(true);
261
+ expect(
262
+ validators.appearsToBeFromCategory([sample.contact_email], "email"),
263
+ ).toBe(true);
264
+ });
265
+
266
+ // Should have good uniqueness (not all the same)
267
+ const emails = samples.map((s) => s.email);
268
+ expect(validators.uniquenessRatio(emails)).toBeGreaterThan(0.7);
269
+ });
270
+
271
+ it("maps name fields appropriately", () => {
272
+ const samples = generate.samples<any>(
273
+ schemas.simple.object({
274
+ firstName: schemas.simple.string(),
275
+ first_name: schemas.simple.string(),
276
+ lastName: schemas.simple.string(),
277
+ last_name: schemas.simple.string(),
278
+ name: schemas.simple.string(),
279
+ fullname: schemas.simple.string(),
280
+ }),
281
+ 20,
282
+ );
283
+
284
+ samples.forEach((sample) => {
285
+ // First names should be single words
286
+ expect(sample.firstName).not.toContain(" ");
287
+ expect(sample.first_name).not.toContain(" ");
288
+
289
+ // Last names should be single words
290
+ expect(sample.lastName).not.toContain(" ");
291
+ expect(sample.last_name).not.toContain(" ");
292
+
293
+ // Full names should contain spaces
294
+ expect(sample.name).toContain(" ");
295
+ expect(sample.fullname).toContain(" ");
296
+
297
+ // All should start with capital letters
298
+ expect(
299
+ validators.appearsToBeFromCategory([sample.firstName], "name"),
300
+ ).toBe(true);
301
+ expect(
302
+ validators.appearsToBeFromCategory([sample.lastName], "name"),
303
+ ).toBe(true);
304
+ expect(
305
+ validators.appearsToBeFromCategory([sample.name], "name"),
306
+ ).toBe(true);
307
+ });
308
+
309
+ // Check uniqueness
310
+ const firstNames = samples.map((s) => s.firstName);
311
+ expect(validators.uniquenessRatio(firstNames)).toBeGreaterThan(0.5);
312
+ });
313
+
314
+ it("maps date fields to date generators", () => {
315
+ const samples = generate.samples<any>(
316
+ schemas.simple.object({
317
+ createdAt: schemas.simple.string(),
318
+ created_at: schemas.simple.string(),
319
+ updatedAt: schemas.simple.string(),
320
+ updated_at: schemas.simple.string(),
321
+ }),
322
+ 10,
323
+ );
324
+
325
+ samples.forEach((sample) => {
326
+ expect(
327
+ validators.appearsToBeFromCategory([sample.createdAt], "date"),
328
+ ).toBe(true);
329
+ expect(
330
+ validators.appearsToBeFromCategory([sample.created_at], "date"),
331
+ ).toBe(true);
332
+ expect(
333
+ validators.appearsToBeFromCategory([sample.updatedAt], "date"),
334
+ ).toBe(true);
335
+ expect(
336
+ validators.appearsToBeFromCategory([sample.updated_at], "date"),
337
+ ).toBe(true);
338
+ });
339
+ });
340
+
341
+ it("maps UUID fields correctly", () => {
342
+ const samples = generate.samples<any>(
343
+ schemas.simple.object({
344
+ uuid: schemas.simple.string(),
345
+ id: { type: "string" as const, format: "uuid" as const },
346
+ }),
347
+ 10,
348
+ );
349
+
350
+ samples.forEach((sample) => {
351
+ expect(
352
+ validators.appearsToBeFromCategory([sample.uuid], "uuid"),
353
+ ).toBe(true);
354
+ expect(validators.appearsToBeFromCategory([sample.id], "uuid")).toBe(
355
+ true,
356
+ );
357
+ });
358
+ });
359
+
360
+ it("does not map unrecognized fields", async () => {
361
+ const samples = generate.samples<any>(
362
+ schemas.simple.object({
363
+ randomField: schemas.simple.string(),
364
+ customProperty: schemas.simple.string(),
365
+ someValue: schemas.simple.string(),
366
+ }),
367
+ 20,
368
+ );
369
+
370
+ // These should be random strings, not following any specific pattern
371
+ const randomFields = samples.map((s) => s.randomField);
372
+ const _customProps = samples.map((s) => s.customProperty);
373
+
374
+ // Should not appear to be from any specific category
375
+ expect(validators.appearsToBeFromCategory(randomFields, "email")).toBe(
376
+ false,
377
+ );
378
+ expect(validators.appearsToBeFromCategory(randomFields, "name")).toBe(
379
+ false,
380
+ );
381
+ expect(validators.appearsToBeFromCategory(randomFields, "uuid")).toBe(
382
+ false,
383
+ );
384
+ });
385
+
386
+ it("preserves explicit faker methods over smart mapping", () => {
387
+ const schema = schemas.simple.object({
388
+ email: schemas.withFaker("string", "person.firstName"),
389
+ });
390
+
391
+ const samples = generate.samples<any>(schema, 10);
392
+
393
+ // Should NOT be emails since we explicitly set it to firstName
394
+ samples.forEach((sample) => {
395
+ expect(sample.email).not.toContain("@");
396
+ expect(
397
+ validators.appearsToBeFromCategory([sample.email], "email"),
398
+ ).toBe(false);
399
+ });
400
+ });
401
+
402
+ it("validates faker method namespaces", () => {
403
+ const schema = schemas.simple.object({
404
+ field: schemas.withFaker("string", "invalidnamespace.method"),
405
+ });
406
+
407
+ schemaTests.expectInvalid(schema, /Unknown faker namespace/);
408
+ });
409
+
410
+ it("handles all common field mapping categories", () => {
411
+ const schema = schemas.simple.object({
412
+ // Names
413
+ firstName: schemas.simple.string(),
414
+ lastName: schemas.simple.string(),
415
+
416
+ // Contact
417
+ email: schemas.simple.string(),
418
+ phone: schemas.simple.string(),
419
+ mobile: schemas.simple.string(),
420
+
421
+ // Address
422
+ street: schemas.simple.string(),
423
+ city: schemas.simple.string(),
424
+ zipcode: schemas.simple.string(),
425
+
426
+ // Business
427
+ company: schemas.simple.string(),
428
+ position: schemas.simple.string(),
429
+
430
+ // Money
431
+ price: schemas.simple.string(),
432
+ amount: schemas.simple.string(),
433
+
434
+ // Time
435
+ createdAt: schemas.simple.string(),
436
+ updatedAt: schemas.simple.string(),
437
+
438
+ // IDs
439
+ uuid: schemas.simple.string(),
440
+ });
441
+
442
+ const result = generateFromSchema({ schema });
443
+
444
+ // Just verify all fields are generated without checking specific patterns
445
+ Object.keys(schema.properties).forEach((key) => {
446
+ expect(result).toHaveProperty(key);
447
+ expect(typeof result[key]).toBe("string");
448
+ expect(result[key].length).toBeGreaterThan(0);
449
+ });
450
+ });
451
+ });
452
+
453
+ describe("mapping effectiveness", () => {
454
+ it("generates diverse data for mapped fields", () => {
455
+ const schema = schemas.simple.object({
456
+ email: schemas.simple.string(),
457
+ name: schemas.simple.string(),
458
+ phone: schemas.simple.string(),
459
+ });
460
+
461
+ const samples = generate.samples<any>(schema, 50);
462
+
463
+ // Check entropy/diversity
464
+ const emails = samples.map((s) => s.email);
465
+ const names = samples.map((s) => s.name);
466
+ const phones = samples.map((s) => s.phone);
467
+
468
+ // Should have high uniqueness for these fields
469
+ expect(validators.uniquenessRatio(emails)).toBeGreaterThan(0.8);
470
+ expect(validators.uniquenessRatio(names)).toBeGreaterThan(0.7);
471
+ expect(validators.uniquenessRatio(phones)).toBeGreaterThan(0.8);
472
+
473
+ // Should have good entropy
474
+ expect(stats.entropy(emails)).toBeGreaterThan(3);
475
+ expect(stats.entropy(names)).toBeGreaterThan(3);
476
+ });
477
+
478
+ it("mapped fields generate different patterns than unmapped fields", async () => {
479
+ // This test verifies that our smart mapping actually does something
480
+ const mappedSamples = generate
481
+ .samples<any>(
482
+ schemas.simple.object({
483
+ email: schemas.simple.string(),
484
+ }),
485
+ 20,
486
+ )
487
+ .map((s) => s.email);
488
+
489
+ const unmappedSamples = generate
490
+ .samples<any>(
491
+ schemas.simple.object({
492
+ randomFieldXYZ123: schemas.simple.string(),
493
+ }),
494
+ 20,
495
+ )
496
+ .map((s) => s.randomFieldXYZ123);
497
+
498
+ // Email fields should all have @ sign
499
+ const mappedHasAt = mappedSamples.every((s) => s.includes("@"));
500
+ const unmappedHasAt = unmappedSamples.every((s) => s.includes("@"));
501
+
502
+ expect(mappedHasAt).toBe(true);
503
+ expect(unmappedHasAt).toBe(false);
504
+ });
505
+ });
506
+ });
507
+
508
+ describe("Template Processing", () => {
509
+ describe("basic template substitution", () => {
510
+ it("processes param templates", () => {
511
+ const schema = schemas.simple.object({
512
+ userId: schemas.simple.string(),
513
+ });
514
+ const result = generateFromSchema({
515
+ schema,
516
+ overrides: { userId: "{{params.id}}" },
517
+ params: { id: "123" },
518
+ });
519
+
520
+ expect(result.userId).toBe("123"); // Templates return string values
521
+ });
522
+
523
+ it("processes state templates", () => {
524
+ const schema = schemas.simple.object({
525
+ username: schemas.simple.string(),
526
+ });
527
+ const result = generateFromSchema({
528
+ schema,
529
+ overrides: { username: "{{state.currentUser}}" },
530
+ state: { currentUser: "alice" },
531
+ });
532
+
533
+ expect(result.username).toBe("alice");
534
+ });
535
+
536
+ it("processes query templates", () => {
537
+ const schema = schemas.simple.object({
538
+ filter: schemas.simple.string(),
539
+ });
540
+ const result = generateFromSchema({
541
+ schema,
542
+ overrides: { filter: "{{query.category}}" },
543
+ query: { category: "electronics" },
544
+ });
545
+
546
+ expect(result.filter).toBe("electronics");
547
+ });
548
+ });
549
+
550
+ describe("nested templates", () => {
551
+ it("resolves deeply nested properties", () => {
552
+ const schema = schemas.simple.object({
553
+ value: schemas.simple.string(),
554
+ });
555
+ const result = generateFromSchema({
556
+ schema,
557
+ overrides: { value: "{{state.user.profile.settings.theme}}" },
558
+ state: {
559
+ user: {
560
+ profile: {
561
+ settings: {
562
+ theme: "dark",
563
+ },
564
+ },
565
+ },
566
+ },
567
+ });
568
+
569
+ expect(result.value).toBe("dark");
570
+ });
571
+
572
+ it("handles missing nested properties gracefully", () => {
573
+ const schema = schemas.simple.object({
574
+ value: schemas.simple.string(),
575
+ });
576
+ const result = generateFromSchema({
577
+ schema,
578
+ overrides: { value: "{{state.nonexistent.property}}" },
579
+ state: { other: "value" },
580
+ });
581
+
582
+ expect(result.value).toBe("{{state.nonexistent.property}}");
583
+ });
584
+ });
585
+
586
+ describe("template edge cases", () => {
587
+ it("handles multiple templates in one string", () => {
588
+ const schema = schemas.simple.object({
589
+ message: schemas.simple.string(),
590
+ });
591
+ const result = generateFromSchema({
592
+ schema,
593
+ overrides: { message: "User {{params.id}} in {{state.location}}" },
594
+ params: { id: "123" },
595
+ state: { location: "NYC" },
596
+ });
597
+
598
+ expect(result.message).toBe("User 123 in NYC");
599
+ });
600
+
601
+ it("preserves non-template content", () => {
602
+ const schema = schemas.simple.object({ text: schemas.simple.string() });
603
+ const result = generateFromSchema({
604
+ schema,
605
+ overrides: { text: "Static text with {{params.id}} and more static" },
606
+ params: { id: "456" },
607
+ });
608
+
609
+ expect(result.text).toBe("Static text with 456 and more static");
610
+ });
611
+
612
+ it("handles malformed templates", () => {
613
+ const schema = schemas.simple.object({
614
+ bad1: schemas.simple.string(),
615
+ bad2: schemas.simple.string(),
616
+ bad3: schemas.simple.string(),
617
+ });
618
+
619
+ const result = generateFromSchema({
620
+ schema,
621
+ overrides: {
622
+ bad1: "{params.id}", // Missing one brace
623
+ bad2: "{{}}", // Empty template
624
+ bad3: "{{ }}", // Just spaces
625
+ },
626
+ params: { id: "123" },
627
+ });
628
+
629
+ expect(result.bad1).toBe("{params.id}");
630
+ expect(result.bad2).toBe("{{}}");
631
+ expect(result.bad3).toBe("{{ }}");
632
+ });
633
+
634
+ it("converts numeric strings appropriately", () => {
635
+ const schema = schemas.simple.object({
636
+ intValue: schemas.simple.number(),
637
+ floatValue: schemas.simple.number(),
638
+ stringValue: schemas.simple.string(),
639
+ });
640
+
641
+ const result = generateFromSchema({
642
+ schema,
643
+ overrides: {
644
+ intValue: "{{params.int}}",
645
+ floatValue: "{{params.float}}",
646
+ stringValue: "{{params.mixed}}",
647
+ },
648
+ params: {
649
+ int: "42",
650
+ float: "3.14",
651
+ mixed: "abc123",
652
+ },
653
+ });
654
+
655
+ expect(result.intValue).toBe("42"); // All template values are strings
656
+ expect(result.floatValue).toBe("3.14");
657
+ expect(result.stringValue).toBe("abc123");
658
+ });
659
+
660
+ it("handles null and undefined in templates", () => {
661
+ const schema = schemas.simple.object({
662
+ nullValue: schemas.simple.string(),
663
+ undefinedValue: schemas.simple.string(),
664
+ });
665
+
666
+ const result = generateFromSchema({
667
+ schema,
668
+ overrides: {
669
+ nullValue: "{{state.nullVal}}",
670
+ undefinedValue: "{{state.undefinedVal}}",
671
+ },
672
+ state: {
673
+ nullVal: null,
674
+ undefinedVal: undefined,
675
+ },
676
+ });
677
+
678
+ expect(result.nullValue).toBe(null); // null values preserved
679
+ expect(result.undefinedValue).toBe("{{state.undefinedVal}}"); // Template returns original when undefined
680
+ });
681
+ });
682
+
683
+ describe("template in arrays", () => {
684
+ it("applies templates to array items", () => {
685
+ const schema = schemas.simple.array(
686
+ schemas.simple.object({ userId: schemas.simple.string() }),
687
+ );
688
+
689
+ const result = generateFromSchema({
690
+ schema,
691
+ count: 3,
692
+ overrides: { userId: "{{params.baseId}}" },
693
+ params: { baseId: "user_" },
694
+ });
695
+
696
+ expect(Array.isArray(result)).toBe(true);
697
+ result.forEach((item) => {
698
+ expect(item.userId).toBe("user_");
699
+ });
700
+ });
701
+ });
702
+ });
703
+
704
+ describe("Performance", () => {
705
+ it("generates simple schemas quickly", async () => {
706
+ const schema = schemas.simple.object({
707
+ id: schemas.simple.number(),
708
+ name: schemas.simple.string(),
709
+ });
710
+
711
+ const { duration } = await perf.measure(() =>
712
+ generateFromSchema({ schema }),
713
+ );
714
+
715
+ expect(duration).toBeLessThan(100); // Should be fast (but reasonable for CI)
716
+ });
717
+
718
+ it("handles large arrays efficiently", async () => {
719
+ const schema = schemas.simple.array(
720
+ schemas.simple.object({ id: schemas.simple.number() }),
721
+ { maxItems: 100 },
722
+ );
723
+
724
+ const { duration } = await perf.measure(() =>
725
+ generateFromSchema({ schema }),
726
+ );
727
+
728
+ expect(duration).toBeLessThan(500); // Reasonable time for 100 items
729
+ });
730
+
731
+ it("benchmarks show consistent performance", async () => {
732
+ const schema = schemas.complex.user();
733
+
734
+ const benchmark = await perf.benchmark(
735
+ "user generation",
736
+ () => generateFromSchema({ schema }),
737
+ 50,
738
+ );
739
+
740
+ expect(benchmark.mean).toBeLessThan(50); // Reasonable for CI
741
+ // Just check that performance is reasonable, not strict ratios for small values
742
+ expect(benchmark.max).toBeLessThan(100); // No huge outliers
743
+ });
744
+
745
+ it("deep nesting doesn't cause exponential slowdown", async () => {
746
+ const shallow = schemas.nested.deep(2);
747
+ const deep = schemas.nested.deep(5);
748
+
749
+ await perf.measure(() => generateFromSchema({ schema: shallow }));
750
+
751
+ const { duration: deepTime } = await perf.measure(() =>
752
+ generateFromSchema({ schema: deep }),
753
+ );
754
+
755
+ // Just ensure it completes in reasonable time
756
+ expect(deepTime).toBeLessThan(100); // Should complete quickly
757
+ // The times might be too small to compare ratios reliably
758
+ });
759
+ });
760
+
761
+ describe("Schema Plugin", () => {
762
+ it("creates plugin with correct interface", () => {
763
+ const plugin = schemaPlugin({
764
+ schema: schemas.simple.object({ id: schemas.simple.number() }),
765
+ });
766
+
767
+ expect(plugin).toHaveProperty("name", "schema");
768
+ expect(plugin).toHaveProperty("version", "1.0.0");
769
+ expect(plugin).toHaveProperty("process");
770
+ expect(typeof plugin.process).toBe("function");
771
+ });
772
+
773
+ it("validates schema at plugin creation time", () => {
774
+ expect(() => {
775
+ schemaPlugin({ schema: {} as any });
776
+ }).toThrow("Schema cannot be empty");
777
+
778
+ expect(() => {
779
+ schemaPlugin({ schema: { type: "invalid" as any } });
780
+ }).toThrow("Invalid schema type");
781
+ });
782
+
783
+ it("generates data when processing context", () => {
784
+ const plugin = schemaPlugin({
785
+ schema: schemas.simple.object({
786
+ id: schemas.simple.number(),
787
+ name: schemas.simple.string(),
788
+ }),
789
+ });
790
+
791
+ const mockContext = {
792
+ method: "GET",
793
+ path: "/test",
794
+ params: {},
795
+ query: {},
796
+ state: {},
797
+ headers: {},
798
+ body: null,
799
+ route: {},
800
+ };
801
+
802
+ const result = plugin.process(mockContext);
803
+
804
+ expect(result.response).toHaveProperty("id");
805
+ expect(result.response).toHaveProperty("name");
806
+ expect(typeof result.response.id).toBe("number");
807
+ expect(typeof result.response.name).toBe("string");
808
+ });
809
+
810
+ it("passes through existing responses", () => {
811
+ const plugin = schemaPlugin({
812
+ schema: schemas.simple.object({ id: schemas.simple.number() }),
813
+ });
814
+
815
+ const mockContext = {
816
+ method: "GET",
817
+ path: "/test",
818
+ params: {},
819
+ query: {},
820
+ state: {},
821
+ headers: {},
822
+ body: null,
823
+ route: {},
824
+ };
825
+
826
+ const existingResponse = { custom: "response", data: [1, 2, 3] };
827
+ const result = plugin.process(mockContext, existingResponse);
828
+
829
+ expect(result.response).toEqual(existingResponse);
830
+ });
831
+
832
+ it("applies overrides with template processing", () => {
833
+ const plugin = schemaPlugin({
834
+ schema: schemas.simple.object({
835
+ userId: schemas.simple.string(),
836
+ timestamp: schemas.simple.string(),
837
+ }),
838
+ overrides: {
839
+ userId: "{{params.id}}",
840
+ timestamp: "{{state.currentTime}}",
841
+ },
842
+ });
843
+
844
+ const mockContext = {
845
+ method: "GET",
846
+ path: "/test/123",
847
+ params: { id: "123" },
848
+ query: {},
849
+ state: new Map(),
850
+ routeState: { currentTime: "2024-01-01T00:00:00Z" },
851
+ headers: {},
852
+ body: null,
853
+ route: {},
854
+ };
855
+
856
+ const result = plugin.process(mockContext);
857
+
858
+ expect(result.response.userId).toBe("123"); // Template values are strings
859
+ expect(result.response.timestamp).toBe("2024-01-01T00:00:00Z");
860
+ });
861
+
862
+ it("handles errors gracefully", () => {
863
+ const plugin = schemaPlugin({
864
+ schema: schemas.simple.object({ id: schemas.simple.number() }),
865
+ });
866
+
867
+ // Create a context that might cause issues
868
+ const badContext = {
869
+ method: "GET",
870
+ path: "/test",
871
+ params: null as any, // This might cause template processing to fail
872
+ query: {},
873
+ state: {},
874
+ headers: {},
875
+ body: null,
876
+ route: {},
877
+ };
878
+
879
+ // Should not throw, should handle gracefully
880
+ expect(() => plugin.process(badContext)).not.toThrow();
881
+ });
882
+ });
883
+
884
+ describe("Error Handling", () => {
885
+ it("provides clear error messages for validation failures", () => {
886
+ try {
887
+ generateFromSchema({
888
+ schema: {
889
+ type: "object",
890
+ properties: {
891
+ nested: {
892
+ type: "array",
893
+ items: {
894
+ type: "object",
895
+ properties: {
896
+ field: {
897
+ type: "string",
898
+ faker: "invalid.namespace.method",
899
+ },
900
+ },
901
+ },
902
+ },
903
+ },
904
+ },
905
+ });
906
+ expect.fail("Should have thrown");
907
+ } catch (error: any) {
908
+ expect(error.message).toContain("Unknown faker namespace");
909
+ expect(error.context?.schemaPath).toContain("nested");
910
+ expect(error.context?.schemaPath).toContain("field");
911
+ }
912
+ });
913
+
914
+ it("wraps generation errors appropriately", () => {
915
+ const schema = schemas.simple.object({
916
+ field: { type: "string" as const, pattern: "[" } as any, // Invalid regex
917
+ });
918
+
919
+ // json-schema-faker validates regex patterns and will throw
920
+ expect(() => generateFromSchema({ schema })).toThrow();
921
+ });
922
+ });
923
+
924
+ describe("Integration", () => {
925
+ it("works with real-world schemas", () => {
926
+ const openAPISchema: JSONSchema7 = {
927
+ type: "object",
928
+ properties: {
929
+ id: { type: "string", format: "uuid" },
930
+ email: { type: "string", format: "email" },
931
+ profile: {
932
+ type: "object",
933
+ properties: {
934
+ firstName: { type: "string" },
935
+ lastName: { type: "string" },
936
+ age: { type: "integer", minimum: 0, maximum: 120 },
937
+ interests: {
938
+ type: "array",
939
+ items: { type: "string" },
940
+ maxItems: 10,
941
+ },
942
+ },
943
+ required: ["firstName", "lastName"],
944
+ },
945
+ createdAt: { type: "string", format: "date-time" },
946
+ isActive: { type: "boolean" },
947
+ },
948
+ required: ["id", "email", "profile"],
949
+ };
950
+
951
+ const result = generateFromSchema({ schema: openAPISchema });
952
+
953
+ // Verify structure
954
+ expect(result).toHaveProperty("id");
955
+ expect(result).toHaveProperty("email");
956
+ expect(result).toHaveProperty("profile");
957
+ expect(result.profile).toHaveProperty("firstName");
958
+ expect(result.profile).toHaveProperty("lastName");
959
+
960
+ // Verify formats
961
+ expect(validators.appearsToBeFromCategory([result.id], "uuid")).toBe(
962
+ true,
963
+ );
964
+ expect(validators.appearsToBeFromCategory([result.email], "email")).toBe(
965
+ true,
966
+ );
967
+ expect(
968
+ validators.appearsToBeFromCategory([result.createdAt], "date"),
969
+ ).toBe(true);
970
+ });
971
+
972
+ it("integrates with plugin pipeline", () => {
973
+ const plugin = schemaPlugin({
974
+ schema: schemas.complex.apiResponse(),
975
+ count: 5,
976
+ });
977
+
978
+ // Simulate plugin pipeline
979
+ const context = {
980
+ method: "GET",
981
+ path: "/api/users",
982
+ params: {},
983
+ query: { page: "1" },
984
+ state: {},
985
+ headers: {},
986
+ body: null,
987
+ route: {},
988
+ };
989
+
990
+ const result1 = plugin.process(context);
991
+ const result2 = plugin.process(context);
992
+
993
+ // Should generate different data each time
994
+ expect(result1.response).not.toEqual(result2.response);
995
+ expect(Array.isArray(result1.response.data)).toBe(true);
996
+ expect(Array.isArray(result2.response.data)).toBe(true);
997
+ // Count was specified at plugin level, not in the schema response
998
+ });
999
+ });
1000
+ });
1001
+
1002
+ describe("Additional Coverage Tests", () => {
1003
+ describe("Schema Enhancement", () => {
1004
+ it("enhances simple fields without existing faker methods", () => {
1005
+ const schema = schemas.simple.object({
1006
+ email: schemas.simple.string(),
1007
+ phone: schemas.simple.string(),
1008
+ uuid: schemas.simple.string(),
1009
+ });
1010
+
1011
+ const samples = generate.samples<any>(schema, 5);
1012
+
1013
+ samples.forEach((sample) => {
1014
+ expect(
1015
+ validators.appearsToBeFromCategory([sample.email], "email"),
1016
+ ).toBe(true);
1017
+ expect(
1018
+ validators.appearsToBeFromCategory([sample.phone], "phone"),
1019
+ ).toBe(true);
1020
+ expect(validators.appearsToBeFromCategory([sample.uuid], "uuid")).toBe(
1021
+ true,
1022
+ );
1023
+ });
1024
+ });
1025
+
1026
+ it("preserves explicit faker methods over enhancements", () => {
1027
+ const schema = {
1028
+ type: "object" as const,
1029
+ properties: {
1030
+ email: {
1031
+ type: "string" as const,
1032
+ faker: "lorem.word" as any,
1033
+ },
1034
+ },
1035
+ };
1036
+
1037
+ const result = generateFromSchema({ schema });
1038
+
1039
+ // Should use lorem.word, not email pattern
1040
+ expect(result.email).not.toContain("@");
1041
+ });
1042
+
1043
+ it("handles array items with smart field mapping", () => {
1044
+ const schema = schemas.simple.array(
1045
+ schemas.simple.object({
1046
+ email: schemas.simple.string(),
1047
+ createdAt: schemas.simple.string(),
1048
+ }),
1049
+ );
1050
+
1051
+ const result = generateFromSchema({ schema, count: 3 });
1052
+
1053
+ result.forEach((item) => {
1054
+ expect(validators.appearsToBeFromCategory([item.email], "email")).toBe(
1055
+ true,
1056
+ );
1057
+ expect(
1058
+ validators.appearsToBeFromCategory([item.createdAt], "date"),
1059
+ ).toBe(true);
1060
+ });
1061
+ });
1062
+ });
1063
+
1064
+ describe("Edge Cases", () => {
1065
+ it("handles empty string patterns", () => {
1066
+ const schema = schemas.simple.object({
1067
+ value: { type: "string" as const, pattern: "" as const },
1068
+ });
1069
+
1070
+ const result = generateFromSchema({ schema });
1071
+ expect(typeof result.value).toBe("string");
1072
+ });
1073
+
1074
+ it("handles whitespace in templates", () => {
1075
+ const schema = schemas.simple.object({ value: schemas.simple.string() });
1076
+
1077
+ const result = generateFromSchema({
1078
+ schema,
1079
+ overrides: { value: " {{ params.id }} " },
1080
+ params: { id: "test" },
1081
+ });
1082
+
1083
+ expect(result.value).toBe(" test "); // Preserves outer whitespace
1084
+ });
1085
+
1086
+ it("handles boolean type with schema", () => {
1087
+ const schema = schemas.simple.object({
1088
+ flag: { type: "boolean" as const },
1089
+ });
1090
+
1091
+ const samples = generate.samples<any>(schema, 20);
1092
+ const trueCount = samples.filter((s) => s.flag === true).length;
1093
+ const falseCount = samples.filter((s) => s.flag === false).length;
1094
+
1095
+ expect(trueCount).toBeGreaterThan(0);
1096
+ expect(falseCount).toBeGreaterThan(0);
1097
+ });
1098
+
1099
+ it("handles integer vs number types", () => {
1100
+ const schema = schemas.simple.object({
1101
+ intValue: { type: "integer" as const },
1102
+ numValue: { type: "number" as const },
1103
+ });
1104
+
1105
+ const samples = generate.samples<any>(schema, 10);
1106
+
1107
+ samples.forEach((sample) => {
1108
+ expect(Number.isInteger(sample.intValue)).toBe(true);
1109
+ expect(typeof sample.numValue).toBe("number");
1110
+ });
1111
+ });
1112
+
1113
+ it("handles null type", () => {
1114
+ const schema = schemas.simple.object({
1115
+ nullValue: { type: "null" as const },
1116
+ });
1117
+
1118
+ const result = generateFromSchema({ schema });
1119
+ expect(result.nullValue).toBe(null);
1120
+ });
1121
+
1122
+ it("handles format without explicit type", () => {
1123
+ const schema = {
1124
+ type: "object" as const,
1125
+ properties: {
1126
+ email: { format: "email" as const },
1127
+ },
1128
+ };
1129
+
1130
+ const result = generateFromSchema({ schema });
1131
+ expect(result.email).toContain("@");
1132
+ });
1133
+
1134
+ it("generates consistent results with same schema instance", () => {
1135
+ const schema = schemas.complex.user();
1136
+
1137
+ const results = Array.from({ length: 5 }, () =>
1138
+ generateFromSchema({ schema }),
1139
+ );
1140
+
1141
+ // All should be valid but different
1142
+ results.forEach((result) => {
1143
+ expect(validators.appearsToBeFromCategory([result.id], "uuid")).toBe(
1144
+ true,
1145
+ );
1146
+ expect(
1147
+ validators.appearsToBeFromCategory([result.email], "email"),
1148
+ ).toBe(true);
1149
+ });
1150
+
1151
+ // Should be different instances
1152
+ const emails = results.map((r) => r.email);
1153
+ const uniqueEmails = new Set(emails);
1154
+ expect(uniqueEmails.size).toBeGreaterThan(1);
1155
+ });
1156
+
1157
+ it("handles minProperties and maxProperties constraints", () => {
1158
+ const schema = {
1159
+ type: "object" as const,
1160
+ properties: {
1161
+ prop1: { type: "string" as const },
1162
+ prop2: { type: "string" as const },
1163
+ },
1164
+ minProperties: 2,
1165
+ maxProperties: 4,
1166
+ additionalProperties: { type: "string" as const },
1167
+ };
1168
+
1169
+ const samples = generate.samples<any>(schema, 10);
1170
+
1171
+ samples.forEach((sample) => {
1172
+ const propCount = Object.keys(sample).length;
1173
+ // Should have at least the defined properties
1174
+ expect(propCount).toBeGreaterThanOrEqual(2);
1175
+ // May not respect maxProperties perfectly but should be reasonable
1176
+ expect(propCount).toBeLessThan(20); // Sanity check
1177
+ });
1178
+ });
1179
+
1180
+ it("handles required fields correctly", () => {
1181
+ const schema = {
1182
+ type: "object" as const,
1183
+ properties: {
1184
+ required1: { type: "string" as const },
1185
+ required2: { type: "number" as const },
1186
+ optional1: { type: "boolean" as const },
1187
+ optional2: { type: "string" as const },
1188
+ },
1189
+ required: ["required1", "required2"],
1190
+ };
1191
+
1192
+ const samples = generate.samples<any>(schema, 10);
1193
+
1194
+ samples.forEach((sample) => {
1195
+ // Required fields should always be present
1196
+ expect(sample).toHaveProperty("required1");
1197
+ expect(sample).toHaveProperty("required2");
1198
+ expect(typeof sample.required1).toBe("string");
1199
+ expect(typeof sample.required2).toBe("number");
1200
+
1201
+ // Optional fields may or may not be present
1202
+ if ("optional1" in sample) {
1203
+ expect(typeof sample.optional1).toBe("boolean");
1204
+ }
1205
+ });
1206
+ });
1207
+ });
1208
+ });