@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,632 @@
1
+ import { schmock } from "@schmock/core";
2
+ import type { JSONSchema7 } from "json-schema";
3
+ import { describe, expect, it } from "vitest";
4
+ import { generateFromSchema, schemaPlugin } from "./index";
5
+ import { generate, schemas, validators } from "./test-utils";
6
+
7
+ describe("Schema Generator Integration Tests", () => {
8
+ describe("End-to-End Scenarios", () => {
9
+ it("generates complete mock API response with relationships", () => {
10
+ const schema: JSONSchema7 = {
11
+ type: "object",
12
+ properties: {
13
+ user: {
14
+ type: "object",
15
+ properties: {
16
+ id: { type: "string", format: "uuid" },
17
+ username: { type: "string", pattern: "^[a-z0-9_]{3,20}$" },
18
+ email: { type: "string", format: "email" },
19
+ profile: {
20
+ type: "object",
21
+ properties: {
22
+ firstName: { type: "string" },
23
+ lastName: { type: "string" },
24
+ avatar: { type: "string", format: "uri" },
25
+ bio: { type: "string", maxLength: 500 },
26
+ },
27
+ },
28
+ posts: {
29
+ type: "array",
30
+ items: {
31
+ type: "object",
32
+ properties: {
33
+ id: { type: "string", format: "uuid" },
34
+ title: { type: "string", minLength: 1, maxLength: 200 },
35
+ content: { type: "string" },
36
+ tags: {
37
+ type: "array",
38
+ items: { type: "string" },
39
+ maxItems: 5,
40
+ },
41
+ publishedAt: { type: "string", format: "date-time" },
42
+ comments: {
43
+ type: "array",
44
+ items: {
45
+ type: "object",
46
+ properties: {
47
+ id: { type: "string", format: "uuid" },
48
+ author: { type: "string" },
49
+ text: { type: "string" },
50
+ createdAt: { type: "string", format: "date-time" },
51
+ },
52
+ },
53
+ },
54
+ },
55
+ },
56
+ },
57
+ },
58
+ },
59
+ },
60
+ };
61
+
62
+ const result = generateFromSchema({ schema });
63
+
64
+ // Verify complete structure
65
+ expect(result.user).toBeDefined();
66
+ expect(validators.appearsToBeFromCategory([result.user.id], "uuid")).toBe(
67
+ true,
68
+ );
69
+ expect(result.user.username).toMatch(/^[a-z0-9_]{3,20}$/);
70
+ expect(
71
+ validators.appearsToBeFromCategory([result.user.email], "email"),
72
+ ).toBe(true);
73
+
74
+ expect(result.user.profile).toBeDefined();
75
+ expect(typeof result.user.profile.firstName).toBe("string");
76
+ expect(result.user.profile.firstName.length).toBeGreaterThan(0);
77
+
78
+ if (result.user.posts && result.user.posts.length > 0) {
79
+ const post = result.user.posts[0];
80
+ expect(validators.appearsToBeFromCategory([post.id], "uuid")).toBe(
81
+ true,
82
+ );
83
+ expect(post.title.length).toBeGreaterThan(0);
84
+ expect(
85
+ validators.appearsToBeFromCategory([post.publishedAt], "date"),
86
+ ).toBe(true);
87
+ }
88
+ });
89
+
90
+ it("generates consistent data across related fields", () => {
91
+ const plugin = schemaPlugin({
92
+ schema: {
93
+ type: "object",
94
+ properties: {
95
+ order: {
96
+ type: "object",
97
+ properties: {
98
+ id: { type: "string", format: "uuid" },
99
+ customerId: { type: "string", format: "uuid" },
100
+ items: {
101
+ type: "array",
102
+ items: {
103
+ type: "object",
104
+ properties: {
105
+ productId: { type: "string", format: "uuid" },
106
+ quantity: { type: "integer", minimum: 1 },
107
+ unitPrice: { type: "number", minimum: 0 },
108
+ totalPrice: { type: "number", minimum: 0 },
109
+ },
110
+ },
111
+ },
112
+ subtotal: { type: "number", minimum: 0 },
113
+ tax: { type: "number", minimum: 0 },
114
+ total: { type: "number", minimum: 0 },
115
+ createdAt: { type: "string", format: "date-time" },
116
+ updatedAt: { type: "string", format: "date-time" },
117
+ },
118
+ },
119
+ },
120
+ },
121
+ overrides: {
122
+ "order.customerId": "{{params.customerId}}",
123
+ "order.createdAt": "{{state.timestamp}}",
124
+ "order.updatedAt": "{{state.timestamp}}",
125
+ },
126
+ });
127
+
128
+ const context = {
129
+ method: "GET",
130
+ path: "/api/orders/123",
131
+ params: { orderId: "123", customerId: "customer-456" },
132
+ query: {},
133
+ state: new Map(),
134
+ routeState: { timestamp: "2024-01-01T10:00:00Z" },
135
+ headers: {},
136
+ body: null,
137
+ route: {},
138
+ };
139
+
140
+ const result = plugin.process(context);
141
+
142
+ expect(result.response.order.customerId).toBe("customer-456");
143
+ expect(result.response.order.createdAt).toBe("2024-01-01T10:00:00Z");
144
+ expect(result.response.order.updatedAt).toBe("2024-01-01T10:00:00Z");
145
+
146
+ // All IDs should be valid UUIDs
147
+ expect(
148
+ validators.appearsToBeFromCategory([result.response.order.id], "uuid"),
149
+ ).toBe(true);
150
+
151
+ if (
152
+ result.response.order.items &&
153
+ result.response.order.items.length > 0
154
+ ) {
155
+ result.response.order.items.forEach((item) => {
156
+ expect(
157
+ validators.appearsToBeFromCategory([item.productId], "uuid"),
158
+ ).toBe(true);
159
+ expect(item.quantity).toBeGreaterThanOrEqual(1);
160
+ expect(item.unitPrice).toBeGreaterThanOrEqual(0);
161
+ });
162
+ }
163
+ });
164
+ });
165
+
166
+ describe("Cross-Package Integration", () => {
167
+ it("works with complex nested schemas from multiple sources", () => {
168
+ const addressSchema: JSONSchema7 = {
169
+ type: "object",
170
+ properties: {
171
+ street: { type: "string" },
172
+ city: { type: "string" },
173
+ state: { type: "string", pattern: "^[A-Z]{2}$" },
174
+ zipCode: { type: "string", pattern: "^\\d{5}(-\\d{4})?$" },
175
+ country: { type: "string", enum: ["US", "CA", "MX"] },
176
+ },
177
+ required: ["street", "city", "state", "zipCode", "country"],
178
+ };
179
+
180
+ const customerSchema: JSONSchema7 = {
181
+ type: "object",
182
+ properties: {
183
+ id: { type: "string", format: "uuid" },
184
+ name: { type: "string" },
185
+ email: { type: "string", format: "email" },
186
+ phone: { type: "string" },
187
+ billingAddress: addressSchema,
188
+ shippingAddress: addressSchema,
189
+ sameAsBilling: { type: "boolean" },
190
+ },
191
+ };
192
+
193
+ const result = generateFromSchema({ schema: customerSchema });
194
+
195
+ // Both addresses should be valid structures
196
+ expect(result.billingAddress.street).toBeDefined();
197
+ expect(typeof result.billingAddress.street).toBe("string");
198
+ expect(result.billingAddress.state).toMatch(/^[A-Z]{2}$/);
199
+ expect(result.billingAddress.zipCode).toMatch(/^\d{5}(-\d{4})?$/);
200
+
201
+ expect(result.shippingAddress.street).toBeDefined();
202
+ expect(typeof result.shippingAddress.street).toBe("string");
203
+ expect(["US", "CA", "MX"]).toContain(result.shippingAddress.country);
204
+ });
205
+
206
+ it("handles schema composition with allOf, anyOf, oneOf", () => {
207
+ const baseSchema: JSONSchema7 = {
208
+ type: "object",
209
+ properties: {
210
+ id: { type: "string", format: "uuid" },
211
+ createdAt: { type: "string", format: "date-time" },
212
+ },
213
+ required: ["id", "createdAt"],
214
+ };
215
+
216
+ const documentSchema: JSONSchema7 = {
217
+ allOf: [
218
+ baseSchema,
219
+ {
220
+ type: "object",
221
+ properties: {
222
+ title: { type: "string" },
223
+ content: {
224
+ anyOf: [
225
+ { type: "string" },
226
+ {
227
+ type: "object",
228
+ properties: {
229
+ html: { type: "string" },
230
+ markdown: { type: "string" },
231
+ },
232
+ },
233
+ ],
234
+ },
235
+ status: {
236
+ oneOf: [
237
+ { const: "draft" },
238
+ { const: "published" },
239
+ { const: "archived" },
240
+ ],
241
+ },
242
+ },
243
+ required: ["title", "content", "status"],
244
+ },
245
+ ],
246
+ };
247
+
248
+ const results = generate.samples<any>(documentSchema, 10);
249
+
250
+ results.forEach((result) => {
251
+ // Should have base properties
252
+ expect(validators.appearsToBeFromCategory([result.id], "uuid")).toBe(
253
+ true,
254
+ );
255
+ expect(
256
+ validators.appearsToBeFromCategory([result.createdAt], "date"),
257
+ ).toBe(true);
258
+
259
+ // Should have document properties
260
+ expect(result).toHaveProperty("title");
261
+ expect(result).toHaveProperty("content");
262
+ expect(["draft", "published", "archived"]).toContain(result.status);
263
+ });
264
+ });
265
+ });
266
+
267
+ describe("State Management Integration", () => {
268
+ it("maintains state across multiple generations", () => {
269
+ const _callCount = 0;
270
+ const plugin = schemaPlugin({
271
+ schema: {
272
+ type: "object",
273
+ properties: {
274
+ requestNumber: { type: "integer" },
275
+ sessionId: { type: "string" },
276
+ timestamp: { type: "string", format: "date-time" },
277
+ },
278
+ },
279
+ overrides: {
280
+ requestNumber: "{{state.requestCount}}",
281
+ sessionId: "{{state.sessionId}}",
282
+ },
283
+ });
284
+
285
+ const baseContext = {
286
+ method: "GET",
287
+ path: "/api/track",
288
+ params: {},
289
+ query: {},
290
+ headers: {},
291
+ body: null,
292
+ route: {},
293
+ };
294
+
295
+ // Simulate multiple requests with evolving state
296
+ const results = Array.from({ length: 5 }, (_, i) => {
297
+ const context = {
298
+ ...baseContext,
299
+ state: new Map(),
300
+ routeState: {
301
+ requestCount: i + 1,
302
+ sessionId: "session-123",
303
+ },
304
+ };
305
+ return plugin.process(context).response;
306
+ });
307
+
308
+ // Request numbers should increment
309
+ results.forEach((result, i) => {
310
+ expect(result.requestNumber).toBe(i + 1);
311
+ expect(result.sessionId).toBe("session-123");
312
+ });
313
+ });
314
+
315
+ it("generates stateful mock data", () => {
316
+ const cartSchema: JSONSchema7 = {
317
+ type: "object",
318
+ properties: {
319
+ id: { type: "string", format: "uuid" },
320
+ userId: { type: "string", format: "uuid" },
321
+ items: {
322
+ type: "array",
323
+ items: {
324
+ type: "object",
325
+ properties: {
326
+ productId: { type: "string", format: "uuid" },
327
+ quantity: { type: "integer", minimum: 1 },
328
+ addedAt: { type: "string", format: "date-time" },
329
+ },
330
+ },
331
+ },
332
+ lastModified: { type: "string", format: "date-time" },
333
+ },
334
+ };
335
+
336
+ // Generate initial cart
337
+ const cart1 = generateFromSchema({ schema: cartSchema });
338
+
339
+ // Simulate adding items (would be done through state in real usage)
340
+ const cart2 = generateFromSchema({
341
+ schema: cartSchema,
342
+ overrides: {
343
+ id: cart1.id, // Keep same cart ID
344
+ userId: cart1.userId, // Keep same user
345
+ lastModified: new Date().toISOString(),
346
+ },
347
+ });
348
+
349
+ expect(cart2.id).toBe(cart1.id);
350
+ expect(cart2.userId).toBe(cart1.userId);
351
+ });
352
+ });
353
+
354
+ describe("Performance at Scale", () => {
355
+ it("generates large datasets efficiently", () => {
356
+ const schema: JSONSchema7 = {
357
+ type: "array",
358
+ items: {
359
+ type: "object",
360
+ properties: {
361
+ id: { type: "integer" },
362
+ uuid: { type: "string", format: "uuid" },
363
+ name: { type: "string" },
364
+ email: { type: "string", format: "email" },
365
+ active: { type: "boolean" },
366
+ score: { type: "number", minimum: 0, maximum: 100 },
367
+ tags: {
368
+ type: "array",
369
+ items: { type: "string" },
370
+ maxItems: 3,
371
+ },
372
+ },
373
+ },
374
+ };
375
+
376
+ const startTime = Date.now();
377
+ const result = generateFromSchema({ schema, count: 100 });
378
+ const duration = Date.now() - startTime;
379
+
380
+ expect(result).toHaveLength(100);
381
+ expect(duration).toBeLessThan(1000); // Should complete in under 1 second
382
+
383
+ // Verify data quality at scale
384
+ const emails = result.map((item) => item.email);
385
+ const uniqueEmails = new Set(emails);
386
+ expect(uniqueEmails.size / emails.length).toBeGreaterThan(0.9); // High uniqueness
387
+ });
388
+
389
+ it("handles concurrent plugin processing", async () => {
390
+ const plugin = schemaPlugin({
391
+ schema: schemas.complex.user(),
392
+ });
393
+
394
+ const context = {
395
+ method: "GET",
396
+ path: "/api/user",
397
+ params: {},
398
+ query: {},
399
+ state: {},
400
+ headers: {},
401
+ body: null,
402
+ route: {},
403
+ };
404
+
405
+ // Simulate concurrent requests
406
+ const promises = Array.from({ length: 20 }, () =>
407
+ Promise.resolve(plugin.process(context)),
408
+ );
409
+
410
+ const results = await Promise.all(promises);
411
+
412
+ // All should complete successfully
413
+ expect(results).toHaveLength(20);
414
+
415
+ // Each should generate unique data
416
+ const emails = results.map((r) => r.response.email);
417
+ const uniqueEmails = new Set(emails);
418
+ expect(uniqueEmails.size).toBeGreaterThan(15); // Most should be unique
419
+ });
420
+ });
421
+
422
+ describe("Error Recovery and Resilience", () => {
423
+ it("recovers from validation errors gracefully", () => {
424
+ // First, try invalid schema
425
+ try {
426
+ generateFromSchema({
427
+ schema: {
428
+ type: "object",
429
+ properties: {
430
+ bad: { type: "invalid" as any },
431
+ },
432
+ },
433
+ });
434
+ } catch (_e) {
435
+ // Expected
436
+ }
437
+
438
+ // Should still work with valid schema
439
+ const result = generateFromSchema({
440
+ schema: schemas.simple.object({ id: schemas.simple.number() }),
441
+ });
442
+
443
+ expect(result).toHaveProperty("id");
444
+ });
445
+
446
+ it("handles partial template failures", () => {
447
+ const plugin = schemaPlugin({
448
+ schema: {
449
+ type: "object",
450
+ properties: {
451
+ field1: { type: "string" },
452
+ field2: { type: "string" },
453
+ field3: { type: "string" },
454
+ field4: { type: "string" },
455
+ },
456
+ },
457
+ overrides: {
458
+ field1: "{{params.value1}}",
459
+ field2: "{{state.missing.nested.value}}",
460
+ field3: "static",
461
+ field4: "{{query.value4}}",
462
+ },
463
+ });
464
+
465
+ const result = plugin.process({
466
+ method: "GET",
467
+ path: "/test",
468
+ params: { value1: "success" },
469
+ query: { value4: "works" },
470
+ state: new Map(),
471
+ routeState: {},
472
+ headers: {},
473
+ body: null,
474
+ route: {},
475
+ });
476
+
477
+ // Successful templates should work
478
+ expect(result.response.field1).toBe("success");
479
+ expect(result.response.field3).toBe("static");
480
+ expect(result.response.field4).toBe("works");
481
+
482
+ // Failed template returns original
483
+ expect(result.response.field2).toBe("{{state.missing.nested.value}}");
484
+ });
485
+ });
486
+
487
+ describe("Advanced Integration Patterns", () => {
488
+ it("supports pagination patterns", () => {
489
+ const paginatedSchema: JSONSchema7 = {
490
+ type: "object",
491
+ properties: {
492
+ items: {
493
+ type: "array",
494
+ items: {
495
+ type: "object",
496
+ properties: {
497
+ id: { type: "string", format: "uuid" },
498
+ name: { type: "string" },
499
+ },
500
+ },
501
+ },
502
+ pagination: {
503
+ type: "object",
504
+ properties: {
505
+ page: { type: "integer", minimum: 1 },
506
+ pageSize: { type: "integer", minimum: 1, maximum: 100 },
507
+ total: { type: "integer", minimum: 0 },
508
+ hasNext: { type: "boolean" },
509
+ hasPrev: { type: "boolean" },
510
+ },
511
+ },
512
+ },
513
+ };
514
+
515
+ const plugin = schemaPlugin({
516
+ schema: paginatedSchema,
517
+ overrides: {
518
+ "pagination.page": "{{query.page}}",
519
+ "pagination.pageSize": "{{query.limit}}",
520
+ "pagination.total": 150,
521
+ "pagination.hasNext": "{{state.hasNext}}",
522
+ "pagination.hasPrev": "{{state.hasPrev}}",
523
+ },
524
+ });
525
+
526
+ const result = plugin.process({
527
+ method: "GET",
528
+ path: "/api/items",
529
+ params: {},
530
+ query: { page: "2", limit: "20" },
531
+ state: new Map(),
532
+ routeState: { hasNext: true, hasPrev: true },
533
+ headers: {},
534
+ body: null,
535
+ route: {},
536
+ });
537
+
538
+ expect(result.response.pagination.page).toBe("2");
539
+ expect(result.response.pagination.pageSize).toBe("20");
540
+ expect(result.response.pagination.total).toBe(150);
541
+ expect(result.response.pagination.hasNext).toBe(true);
542
+ });
543
+
544
+ it("supports versioned API responses", () => {
545
+ const v1Schema: JSONSchema7 = {
546
+ type: "object",
547
+ properties: {
548
+ version: { const: "1.0" },
549
+ data: {
550
+ type: "object",
551
+ properties: {
552
+ id: { type: "number" },
553
+ name: { type: "string" },
554
+ },
555
+ },
556
+ },
557
+ };
558
+
559
+ const v2Schema: JSONSchema7 = {
560
+ type: "object",
561
+ properties: {
562
+ version: { const: "2.0" },
563
+ data: {
564
+ type: "object",
565
+ properties: {
566
+ id: { type: "string", format: "uuid" },
567
+ name: { type: "string" },
568
+ metadata: { type: "object" },
569
+ },
570
+ },
571
+ },
572
+ };
573
+
574
+ const v1Result = generateFromSchema({ schema: v1Schema });
575
+ const v2Result = generateFromSchema({ schema: v2Schema });
576
+
577
+ expect(v1Result.version).toBe("1.0");
578
+ expect(typeof v1Result.data.id).toBe("number");
579
+
580
+ expect(v2Result.version).toBe("2.0");
581
+ expect(
582
+ validators.appearsToBeFromCategory([v2Result.data.id], "uuid"),
583
+ ).toBe(true);
584
+ expect(v2Result.data).toHaveProperty("metadata");
585
+ });
586
+ });
587
+
588
+ describe("Schmock Handle Integration", () => {
589
+ it("resolves state overrides when using schmock.handle() with global state", async () => {
590
+ // This test verifies that state-driven template overrides work correctly
591
+ // when the schema plugin is used through schmock.handle() with global state
592
+ const userState = {
593
+ currentUser: {
594
+ id: "user-123",
595
+ name: "Test User",
596
+ },
597
+ settings: {
598
+ theme: "dark",
599
+ },
600
+ };
601
+
602
+ const mock = schmock({ state: userState });
603
+
604
+ const responseSchema: JSONSchema7 = {
605
+ type: "object",
606
+ properties: {
607
+ userId: { type: "string" },
608
+ userName: { type: "string" },
609
+ theme: { type: "string" },
610
+ timestamp: { type: "string" },
611
+ },
612
+ };
613
+
614
+ mock("GET /profile", null).pipe(
615
+ schemaPlugin({
616
+ schema: responseSchema,
617
+ overrides: {
618
+ userId: "{{state.currentUser.id}}",
619
+ userName: "{{state.currentUser.name}}",
620
+ theme: "{{state.settings.theme}}",
621
+ },
622
+ }),
623
+ );
624
+
625
+ const response = await mock.handle("GET", "/profile");
626
+
627
+ expect(response.body.userId).toBe("user-123");
628
+ expect(response.body.userName).toBe("Test User");
629
+ expect(response.body.theme).toBe("dark");
630
+ });
631
+ });
632
+ });