@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.
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +165 -0
- package/dist/test-utils.d.ts +59 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +269 -0
- package/package.json +39 -0
- package/src/advanced-features.test.ts +911 -0
- package/src/data-quality.test.ts +415 -0
- package/src/error-handling.test.ts +507 -0
- package/src/index.test.ts +1208 -0
- package/src/index.ts +859 -0
- package/src/integration.test.ts +632 -0
- package/src/performance.test.ts +477 -0
- package/src/plugin-integration.test.ts +574 -0
- package/src/real-world.test.ts +636 -0
- package/src/test-utils.ts +357 -0
|
@@ -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
|
+
});
|