@schmock/schema 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+ });