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