@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,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
|
+
});
|