@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,507 @@
|
|
|
1
|
+
import type { JSONSchema7 } from "json-schema";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { generateFromSchema, schemaPlugin } from "./index";
|
|
4
|
+
import { schemas } from "./test-utils";
|
|
5
|
+
|
|
6
|
+
describe("Schema Error Handling", () => {
|
|
7
|
+
describe("Validation Error Messages", () => {
|
|
8
|
+
it("provides clear error for empty schemas", () => {
|
|
9
|
+
try {
|
|
10
|
+
generateFromSchema({ schema: {} as any });
|
|
11
|
+
expect.fail("Should have thrown");
|
|
12
|
+
} catch (error: any) {
|
|
13
|
+
expect(error.name).toBe("SchemaValidationError");
|
|
14
|
+
expect(error.message).toContain("Schema cannot be empty");
|
|
15
|
+
expect(error.code).toBe("SCHEMA_VALIDATION_ERROR");
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("provides clear error for invalid types", () => {
|
|
20
|
+
try {
|
|
21
|
+
generateFromSchema({ schema: { type: "invalid" as any } });
|
|
22
|
+
expect.fail("Should have thrown");
|
|
23
|
+
} catch (error: any) {
|
|
24
|
+
expect(error.name).toBe("SchemaValidationError");
|
|
25
|
+
expect(error.message).toContain("Invalid schema type");
|
|
26
|
+
expect(error.message).toContain("invalid");
|
|
27
|
+
expect(error.message).toContain("Supported types are");
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("provides helpful suggestions for common mistakes", () => {
|
|
32
|
+
try {
|
|
33
|
+
generateFromSchema({
|
|
34
|
+
schema: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: "should be object" as any,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
expect.fail("Should have thrown");
|
|
40
|
+
} catch (error: any) {
|
|
41
|
+
expect(error.message).toContain("Properties must be an object");
|
|
42
|
+
expect(error.message).toContain(
|
|
43
|
+
'Use { "propertyName": { "type": "string" } } format',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("includes schema path in error messages", () => {
|
|
49
|
+
try {
|
|
50
|
+
generateFromSchema({
|
|
51
|
+
schema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
user: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
email: {
|
|
58
|
+
type: "invalid" as any,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
expect.fail("Should have thrown");
|
|
66
|
+
} catch (error: any) {
|
|
67
|
+
expect(error.context.schemaPath).toContain("user");
|
|
68
|
+
expect(error.context.schemaPath).toContain("email");
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("validates faker method namespaces with helpful errors", () => {
|
|
73
|
+
try {
|
|
74
|
+
generateFromSchema({
|
|
75
|
+
schema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {
|
|
78
|
+
field: {
|
|
79
|
+
type: "string",
|
|
80
|
+
faker: "badnamespace.method" as any,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
expect.fail("Should have thrown");
|
|
86
|
+
} catch (error: any) {
|
|
87
|
+
expect(error.message).toContain("Unknown faker namespace");
|
|
88
|
+
expect(error.message).toContain("badnamespace");
|
|
89
|
+
expect(error.message).toContain("Valid namespaces include");
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("validates array schemas must have items", () => {
|
|
94
|
+
try {
|
|
95
|
+
generateFromSchema({
|
|
96
|
+
schema: {
|
|
97
|
+
type: "array",
|
|
98
|
+
items: null as any,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
expect.fail("Should have thrown");
|
|
102
|
+
} catch (error: any) {
|
|
103
|
+
expect(error.message).toContain(
|
|
104
|
+
"Array schema must have valid items definition",
|
|
105
|
+
);
|
|
106
|
+
expect(error.message).toContain("Define items as a schema object");
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("Resource Limit Errors", () => {
|
|
112
|
+
it("provides clear error for array size limits", () => {
|
|
113
|
+
try {
|
|
114
|
+
generateFromSchema({
|
|
115
|
+
schema: {
|
|
116
|
+
type: "array",
|
|
117
|
+
items: { type: "string" },
|
|
118
|
+
maxItems: 50000,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
expect.fail("Should have thrown");
|
|
122
|
+
} catch (error: any) {
|
|
123
|
+
// Should throw some kind of error for resource limits
|
|
124
|
+
expect(error).toBeDefined();
|
|
125
|
+
expect(error.message).toContain("array_max_items");
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("provides clear error for nesting depth", () => {
|
|
130
|
+
try {
|
|
131
|
+
const deepSchema = schemas.nested.deep(15);
|
|
132
|
+
generateFromSchema({ schema: deepSchema });
|
|
133
|
+
expect.fail("Should have thrown");
|
|
134
|
+
} catch (error: any) {
|
|
135
|
+
// Should throw some kind of error for nesting depth
|
|
136
|
+
expect(error).toBeDefined();
|
|
137
|
+
expect(error.message).toContain("schema_nesting_depth");
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("detects memory risks from nested arrays", () => {
|
|
142
|
+
try {
|
|
143
|
+
generateFromSchema({
|
|
144
|
+
schema: {
|
|
145
|
+
type: "object",
|
|
146
|
+
properties: {
|
|
147
|
+
level1: {
|
|
148
|
+
type: "array",
|
|
149
|
+
items: {
|
|
150
|
+
type: "array",
|
|
151
|
+
items: {
|
|
152
|
+
type: "array",
|
|
153
|
+
items: {
|
|
154
|
+
type: "object",
|
|
155
|
+
properties: {
|
|
156
|
+
data: { type: "string" },
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
maxItems: 200,
|
|
160
|
+
},
|
|
161
|
+
maxItems: 200,
|
|
162
|
+
},
|
|
163
|
+
maxItems: 200,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
expect.fail("Should have thrown");
|
|
169
|
+
} catch (error: any) {
|
|
170
|
+
expect(error.name).toBe("ResourceLimitError");
|
|
171
|
+
expect(error.message).toContain("memory");
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("provides actionable error messages for limits", () => {
|
|
176
|
+
try {
|
|
177
|
+
generateFromSchema({
|
|
178
|
+
schema: {
|
|
179
|
+
type: "array",
|
|
180
|
+
items: { type: "string" },
|
|
181
|
+
minItems: 20000,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
expect.fail("Should have thrown");
|
|
185
|
+
} catch (error: any) {
|
|
186
|
+
expect(error.message).toContain("Resource limit exceeded");
|
|
187
|
+
expect(error.message).toContain("array");
|
|
188
|
+
// Message should indicate what limit was hit
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("Schema Generation Errors", () => {
|
|
194
|
+
it("wraps json-schema-faker errors appropriately", () => {
|
|
195
|
+
try {
|
|
196
|
+
generateFromSchema({
|
|
197
|
+
schema: {
|
|
198
|
+
type: "string",
|
|
199
|
+
pattern: "[", // Invalid regex
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
expect.fail("Should have thrown");
|
|
203
|
+
} catch (error: any) {
|
|
204
|
+
// Should throw some kind of error for invalid pattern
|
|
205
|
+
expect(error).toBeDefined();
|
|
206
|
+
expect(error.name).toContain("Error");
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("includes context in generation errors", () => {
|
|
211
|
+
const plugin = schemaPlugin({
|
|
212
|
+
schema: {
|
|
213
|
+
type: "string",
|
|
214
|
+
pattern: "[",
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const context = {
|
|
219
|
+
method: "GET",
|
|
220
|
+
path: "/test/123",
|
|
221
|
+
params: { id: "123" },
|
|
222
|
+
query: {},
|
|
223
|
+
state: {},
|
|
224
|
+
headers: {},
|
|
225
|
+
body: null,
|
|
226
|
+
route: {},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
plugin.process(context);
|
|
231
|
+
expect.fail("Should have thrown");
|
|
232
|
+
} catch (error: any) {
|
|
233
|
+
// Should throw some kind of error
|
|
234
|
+
expect(error).toBeDefined();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("handles circular reference errors", () => {
|
|
239
|
+
try {
|
|
240
|
+
generateFromSchema({
|
|
241
|
+
schema: {
|
|
242
|
+
type: "object",
|
|
243
|
+
properties: {
|
|
244
|
+
self: { $ref: "#" },
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
expect.fail("Should have thrown");
|
|
249
|
+
} catch (error: any) {
|
|
250
|
+
expect(error.message).toContain("circular");
|
|
251
|
+
expect(error.name).toBe("SchemaValidationError");
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("handles missing reference errors", () => {
|
|
256
|
+
try {
|
|
257
|
+
generateFromSchema({
|
|
258
|
+
schema: {
|
|
259
|
+
type: "object",
|
|
260
|
+
properties: {
|
|
261
|
+
ref: { $ref: "#/definitions/nonexistent" },
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
expect.fail("Should have thrown");
|
|
266
|
+
} catch (error: any) {
|
|
267
|
+
// json-schema-faker throws its own error
|
|
268
|
+
expect(error.message).toContain("not found");
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("Plugin Error Handling", () => {
|
|
274
|
+
it("validates schema at plugin creation", () => {
|
|
275
|
+
try {
|
|
276
|
+
schemaPlugin({
|
|
277
|
+
schema: null as any,
|
|
278
|
+
});
|
|
279
|
+
expect.fail("Should have thrown");
|
|
280
|
+
} catch (error: any) {
|
|
281
|
+
expect(error.name).toBe("SchemaValidationError");
|
|
282
|
+
// Error happens at plugin creation, not processing
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("handles null context gracefully", () => {
|
|
287
|
+
const plugin = schemaPlugin({
|
|
288
|
+
schema: schemas.simple.object({ id: schemas.simple.number() }),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Should not crash with null params
|
|
292
|
+
const result = plugin.process({
|
|
293
|
+
method: "GET",
|
|
294
|
+
path: "/test",
|
|
295
|
+
params: null as any,
|
|
296
|
+
query: null as any,
|
|
297
|
+
state: null as any,
|
|
298
|
+
headers: {},
|
|
299
|
+
body: null,
|
|
300
|
+
route: {},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
expect(result.response).toHaveProperty("id");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("preserves original error stack traces", () => {
|
|
307
|
+
try {
|
|
308
|
+
generateFromSchema({
|
|
309
|
+
schema: {
|
|
310
|
+
type: "string",
|
|
311
|
+
pattern: "[",
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
expect.fail("Should have thrown");
|
|
315
|
+
} catch (error: any) {
|
|
316
|
+
// Should preserve error info
|
|
317
|
+
expect(error).toBeDefined();
|
|
318
|
+
expect(error.stack).toBeDefined();
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe("Error Recovery", () => {
|
|
324
|
+
it("can generate after validation errors", () => {
|
|
325
|
+
// First attempt with invalid schema
|
|
326
|
+
try {
|
|
327
|
+
generateFromSchema({ schema: { type: "invalid" as any } });
|
|
328
|
+
} catch (_error) {
|
|
329
|
+
// Expected
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Should be able to generate with valid schema
|
|
333
|
+
const result = generateFromSchema({
|
|
334
|
+
schema: schemas.simple.object({ id: schemas.simple.number() }),
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
expect(result).toHaveProperty("id");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("plugin continues to work after errors", () => {
|
|
341
|
+
const plugin = schemaPlugin({
|
|
342
|
+
schema: schemas.simple.object({ id: schemas.simple.number() }),
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const context = {
|
|
346
|
+
method: "GET",
|
|
347
|
+
path: "/test",
|
|
348
|
+
params: {},
|
|
349
|
+
query: {},
|
|
350
|
+
state: {},
|
|
351
|
+
headers: {},
|
|
352
|
+
body: null,
|
|
353
|
+
route: {},
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// Multiple calls should work
|
|
357
|
+
const result1 = plugin.process(context);
|
|
358
|
+
const result2 = plugin.process(context);
|
|
359
|
+
|
|
360
|
+
expect(result1.response).toHaveProperty("id");
|
|
361
|
+
expect(result2.response).toHaveProperty("id");
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe("Edge Case Error Handling", () => {
|
|
366
|
+
it("handles deeply nested validation errors", () => {
|
|
367
|
+
try {
|
|
368
|
+
generateFromSchema({
|
|
369
|
+
schema: {
|
|
370
|
+
type: "object",
|
|
371
|
+
properties: {
|
|
372
|
+
a: {
|
|
373
|
+
type: "object",
|
|
374
|
+
properties: {
|
|
375
|
+
b: {
|
|
376
|
+
type: "object",
|
|
377
|
+
properties: {
|
|
378
|
+
c: {
|
|
379
|
+
type: "object",
|
|
380
|
+
properties: {
|
|
381
|
+
d: {
|
|
382
|
+
type: "array",
|
|
383
|
+
items: null as any,
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
expect.fail("Should have thrown");
|
|
395
|
+
} catch (error: any) {
|
|
396
|
+
expect(error.context.schemaPath).toContain("a");
|
|
397
|
+
expect(error.context.schemaPath).toContain("b");
|
|
398
|
+
expect(error.context.schemaPath).toContain("c");
|
|
399
|
+
expect(error.context.schemaPath).toContain("d");
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("handles multiple validation errors (reports first)", () => {
|
|
404
|
+
try {
|
|
405
|
+
generateFromSchema({
|
|
406
|
+
schema: {
|
|
407
|
+
type: "invalid" as any,
|
|
408
|
+
properties: "also invalid" as any,
|
|
409
|
+
items: null as any,
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
expect.fail("Should have thrown");
|
|
413
|
+
} catch (error: any) {
|
|
414
|
+
// Should report the first error encountered
|
|
415
|
+
expect(error.message).toContain("Invalid schema type");
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("handles non-Error objects in catch blocks", () => {
|
|
420
|
+
// This is more about the implementation being defensive
|
|
421
|
+
const plugin = schemaPlugin({
|
|
422
|
+
schema: schemas.simple.object({ id: schemas.simple.number() }),
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Even with weird inputs, should handle gracefully
|
|
426
|
+
expect(() => {
|
|
427
|
+
plugin.process({} as any);
|
|
428
|
+
}).not.toThrow();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("handles schemas that generate invalid JSON", () => {
|
|
432
|
+
// Some edge cases might generate circular structures
|
|
433
|
+
const schema: JSONSchema7 = {
|
|
434
|
+
type: "object",
|
|
435
|
+
properties: {
|
|
436
|
+
normal: { type: "string" },
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const result = generateFromSchema({ schema });
|
|
441
|
+
|
|
442
|
+
// Should be serializable
|
|
443
|
+
expect(() => JSON.stringify(result)).not.toThrow();
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe("Error Message Quality", () => {
|
|
448
|
+
it("uses consistent error message format", () => {
|
|
449
|
+
const errors: any[] = [];
|
|
450
|
+
|
|
451
|
+
// Collect various errors
|
|
452
|
+
try {
|
|
453
|
+
generateFromSchema({ schema: {} as any });
|
|
454
|
+
} catch (e) {
|
|
455
|
+
errors.push(e);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
generateFromSchema({ schema: { type: "invalid" as any } });
|
|
460
|
+
} catch (e) {
|
|
461
|
+
errors.push(e);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// All should have consistent structure
|
|
465
|
+
errors.forEach((error) => {
|
|
466
|
+
expect(error).toHaveProperty("name");
|
|
467
|
+
expect(error).toHaveProperty("message");
|
|
468
|
+
expect(error).toHaveProperty("code");
|
|
469
|
+
expect(error).toHaveProperty("context");
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("avoids exposing internal implementation details", () => {
|
|
474
|
+
try {
|
|
475
|
+
generateFromSchema({
|
|
476
|
+
schema: {
|
|
477
|
+
type: "string",
|
|
478
|
+
pattern: "[",
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
expect.fail("Should have thrown");
|
|
482
|
+
} catch (error: any) {
|
|
483
|
+
// Should not expose internal file paths or function names
|
|
484
|
+
expect(error.message).not.toContain("node_modules");
|
|
485
|
+
expect(error.message).not.toContain("dist/");
|
|
486
|
+
// Should have some useful error info
|
|
487
|
+
expect(error.message.length).toBeGreaterThan(5);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("provides actionable error messages", () => {
|
|
492
|
+
try {
|
|
493
|
+
generateFromSchema({
|
|
494
|
+
schema: {
|
|
495
|
+
type: "object",
|
|
496
|
+
properties: [] as any, // Wrong type
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
expect.fail("Should have thrown");
|
|
500
|
+
} catch (error: any) {
|
|
501
|
+
// Should tell user what to do
|
|
502
|
+
expect(error.message).toContain("must be an object");
|
|
503
|
+
expect(error.context.suggestion).toBeDefined();
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
});
|