@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,574 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { schemaPlugin } from "./index";
3
+ import { schemas, validators } from "./test-utils";
4
+
5
+ describe("Schema Plugin Integration", () => {
6
+ describe("Plugin Lifecycle", () => {
7
+ it("validates schema during plugin creation", () => {
8
+ // Valid schema should create plugin
9
+ const validPlugin = schemaPlugin({
10
+ schema: schemas.complex.user(),
11
+ });
12
+ expect(validPlugin).toBeDefined();
13
+ expect(validPlugin.name).toBe("schema");
14
+ expect(validPlugin.version).toBe("1.0.0");
15
+
16
+ // Invalid schema should throw immediately
17
+ expect(() => {
18
+ schemaPlugin({
19
+ schema: { type: "invalid" as any },
20
+ });
21
+ }).toThrow("Invalid schema type");
22
+ });
23
+
24
+ it("processes context through plugin pipeline", () => {
25
+ const plugin = schemaPlugin({
26
+ schema: schemas.simple.object({
27
+ requestId: { type: "string" },
28
+ timestamp: { type: "string" },
29
+ data: { type: "object" },
30
+ }),
31
+ });
32
+
33
+ const context = {
34
+ method: "POST",
35
+ path: "/api/data",
36
+ params: {},
37
+ query: {},
38
+ state: { requestId: "req-123" },
39
+ headers: { "content-type": "application/json" },
40
+ body: { value: 42 },
41
+ route: { pattern: "/api/data" },
42
+ };
43
+
44
+ const result = plugin.process(context);
45
+
46
+ expect(result).toHaveProperty("context");
47
+ expect(result).toHaveProperty("response");
48
+ expect(result.context).toBe(context); // Context passed through
49
+ expect(result.response).toHaveProperty("requestId");
50
+ expect(result.response).toHaveProperty("timestamp");
51
+ expect(result.response).toHaveProperty("data");
52
+ });
53
+
54
+ it("preserves existing responses when present", () => {
55
+ const plugin = schemaPlugin({
56
+ schema: schemas.simple.object({ id: schemas.simple.number() }),
57
+ });
58
+
59
+ const context = {
60
+ method: "GET",
61
+ path: "/test",
62
+ params: {},
63
+ query: {},
64
+ state: {},
65
+ headers: {},
66
+ body: null,
67
+ route: {},
68
+ };
69
+
70
+ const existingResponse = {
71
+ cached: true,
72
+ data: [1, 2, 3],
73
+ metadata: { source: "cache" },
74
+ };
75
+
76
+ const result = plugin.process(context, existingResponse);
77
+
78
+ expect(result.response).toBe(existingResponse);
79
+ expect(result.response).toEqual(existingResponse);
80
+ });
81
+ });
82
+
83
+ describe("Context Integration", () => {
84
+ it("uses params in template overrides", () => {
85
+ const plugin = schemaPlugin({
86
+ schema: schemas.simple.object({
87
+ userId: { type: "string" },
88
+ postId: { type: "string" },
89
+ action: { type: "string" },
90
+ }),
91
+ overrides: {
92
+ userId: "{{params.userId}}",
93
+ postId: "{{params.postId}}",
94
+ action: "view",
95
+ },
96
+ });
97
+
98
+ const result = plugin.process({
99
+ method: "GET",
100
+ path: "/users/123/posts/456",
101
+ params: { userId: "123", postId: "456" },
102
+ query: {},
103
+ state: {},
104
+ headers: {},
105
+ body: null,
106
+ route: {},
107
+ });
108
+
109
+ expect(result.response.userId).toBe("123"); // Template values are strings
110
+ expect(result.response.postId).toBe("456");
111
+ expect(result.response.action).toBe("view");
112
+ });
113
+
114
+ it("uses query parameters in templates", () => {
115
+ const plugin = schemaPlugin({
116
+ schema: schemas.simple.object({
117
+ page: { type: "number" },
118
+ limit: { type: "number" },
119
+ sort: { type: "string" },
120
+ filter: { type: "string" },
121
+ }),
122
+ overrides: {
123
+ page: "{{query.page}}",
124
+ limit: "{{query.limit}}",
125
+ sort: "{{query.sort}}",
126
+ filter: "{{query.filter}}",
127
+ },
128
+ });
129
+
130
+ const result = plugin.process({
131
+ method: "GET",
132
+ path: "/api/items",
133
+ params: {},
134
+ query: {
135
+ page: "2",
136
+ limit: "50",
137
+ sort: "name",
138
+ filter: "active",
139
+ },
140
+ state: {},
141
+ headers: {},
142
+ body: null,
143
+ route: {},
144
+ });
145
+
146
+ expect(result.response.page).toBe("2"); // Query values are strings
147
+ expect(result.response.limit).toBe("50");
148
+ expect(result.response.sort).toBe("name");
149
+ expect(result.response.filter).toBe("active");
150
+ });
151
+
152
+ it("uses state in template processing", () => {
153
+ const plugin = schemaPlugin({
154
+ schema: schemas.simple.object({
155
+ currentUser: { type: "string" },
156
+ sessionId: { type: "string" },
157
+ permissions: { type: "array", items: { type: "string" } },
158
+ }),
159
+ overrides: {
160
+ currentUser: "{{state.user.name}}",
161
+ sessionId: "{{state.session.id}}",
162
+ permissions: ["read", "write"],
163
+ },
164
+ });
165
+
166
+ const result = plugin.process({
167
+ method: "GET",
168
+ path: "/api/profile",
169
+ params: {},
170
+ query: {},
171
+ state: new Map(),
172
+ routeState: {
173
+ user: { id: 1, name: "John Doe" },
174
+ session: { id: "sess-123", expires: "2024-12-31" },
175
+ },
176
+ headers: {},
177
+ body: null,
178
+ route: {},
179
+ });
180
+
181
+ expect(result.response.currentUser).toBe("John Doe");
182
+ expect(result.response.sessionId).toBe("sess-123");
183
+ expect(result.response.permissions).toEqual(["read", "write"]);
184
+ });
185
+
186
+ it("handles missing template values gracefully", () => {
187
+ const plugin = schemaPlugin({
188
+ schema: schemas.simple.object({
189
+ value1: { type: "string" },
190
+ value2: { type: "string" },
191
+ value3: { type: "string" },
192
+ }),
193
+ overrides: {
194
+ value1: "{{params.missing}}",
195
+ value2: "{{state.nonexistent.nested}}",
196
+ value3: "default",
197
+ },
198
+ });
199
+
200
+ const result = plugin.process({
201
+ method: "GET",
202
+ path: "/test",
203
+ params: { other: "value" },
204
+ query: {},
205
+ state: new Map(),
206
+ routeState: { different: "data" },
207
+ headers: {},
208
+ body: null,
209
+ route: {},
210
+ });
211
+
212
+ // Missing values return original template
213
+ expect(result.response.value1).toBe("{{params.missing}}");
214
+ expect(result.response.value2).toBe("{{state.nonexistent.nested}}");
215
+ expect(result.response.value3).toBe("default");
216
+ });
217
+ });
218
+
219
+ describe("Array Generation with Plugin", () => {
220
+ it("generates arrays with count parameter", () => {
221
+ const plugin = schemaPlugin({
222
+ schema: schemas.simple.array(
223
+ schemas.simple.object({
224
+ id: { type: "number" },
225
+ name: { type: "string" },
226
+ }),
227
+ ),
228
+ count: 5,
229
+ });
230
+
231
+ const result = plugin.process({
232
+ method: "GET",
233
+ path: "/api/items",
234
+ params: {},
235
+ query: {},
236
+ state: {},
237
+ headers: {},
238
+ body: null,
239
+ route: {},
240
+ });
241
+
242
+ expect(Array.isArray(result.response)).toBe(true);
243
+ expect(result.response).toHaveLength(5);
244
+ result.response.forEach((item) => {
245
+ expect(item).toHaveProperty("id");
246
+ expect(item).toHaveProperty("name");
247
+ });
248
+ });
249
+
250
+ it("applies overrides to array items", () => {
251
+ const plugin = schemaPlugin({
252
+ schema: schemas.simple.array(
253
+ schemas.simple.object({
254
+ index: { type: "number" },
255
+ category: { type: "string" },
256
+ }),
257
+ ),
258
+ count: 3,
259
+ overrides: {
260
+ category: "{{state.defaultCategory}}",
261
+ },
262
+ });
263
+
264
+ const result = plugin.process({
265
+ method: "GET",
266
+ path: "/api/items",
267
+ params: {},
268
+ query: {},
269
+ state: new Map(),
270
+ routeState: { defaultCategory: "electronics" },
271
+ headers: {},
272
+ body: null,
273
+ route: {},
274
+ });
275
+
276
+ expect(result.response).toHaveLength(3);
277
+ result.response.forEach((item) => {
278
+ expect(item.category).toBe("electronics");
279
+ });
280
+ });
281
+ });
282
+
283
+ describe("Error Handling in Plugin", () => {
284
+ it("wraps generation errors with context", () => {
285
+ const plugin = schemaPlugin({
286
+ schema: {
287
+ type: "string",
288
+ pattern: "[", // Invalid regex
289
+ },
290
+ });
291
+
292
+ const context = {
293
+ method: "GET",
294
+ path: "/api/test/123",
295
+ params: { id: "123" },
296
+ query: {},
297
+ state: {},
298
+ headers: {},
299
+ body: null,
300
+ route: {},
301
+ };
302
+
303
+ try {
304
+ plugin.process(context);
305
+ expect.fail("Should have thrown");
306
+ } catch (error: any) {
307
+ // Should throw some kind of error for invalid pattern
308
+ expect(error).toBeDefined();
309
+ expect(error.name).toContain("Error");
310
+ }
311
+ });
312
+
313
+ it("handles null or undefined context properties", () => {
314
+ const plugin = schemaPlugin({
315
+ schema: schemas.simple.object({
316
+ data: { type: "string" },
317
+ }),
318
+ });
319
+
320
+ // Should not crash with null/undefined properties
321
+ const result = plugin.process({
322
+ method: "GET",
323
+ path: "/test",
324
+ params: null as any,
325
+ query: undefined as any,
326
+ state: null as any,
327
+ headers: {},
328
+ body: null,
329
+ route: {},
330
+ });
331
+
332
+ expect(result.response).toHaveProperty("data");
333
+ expect(typeof result.response.data).toBe("string");
334
+ });
335
+ });
336
+
337
+ describe("Complex Schema Scenarios", () => {
338
+ it("handles conditional schemas in plugin", () => {
339
+ const plugin = schemaPlugin({
340
+ schema: {
341
+ type: "object",
342
+ properties: {
343
+ type: { type: "string", enum: ["user", "admin"] },
344
+ data: { type: "object" },
345
+ },
346
+ required: ["type", "data"],
347
+ },
348
+ });
349
+
350
+ const result = plugin.process({
351
+ method: "GET",
352
+ path: "/api/profile",
353
+ params: {},
354
+ query: {},
355
+ state: {},
356
+ headers: {},
357
+ body: null,
358
+ route: {},
359
+ });
360
+
361
+ expect(["user", "admin"]).toContain(result.response.type);
362
+ expect(result.response).toHaveProperty("data");
363
+ });
364
+
365
+ it("generates nested data with references", () => {
366
+ const plugin = schemaPlugin({
367
+ schema: {
368
+ definitions: {
369
+ timestamp: { type: "string", format: "date-time" },
370
+ user: {
371
+ type: "object",
372
+ properties: {
373
+ id: { type: "string", format: "uuid" },
374
+ name: { type: "string" },
375
+ },
376
+ },
377
+ },
378
+ type: "object",
379
+ properties: {
380
+ created: { $ref: "#/definitions/timestamp" },
381
+ updated: { $ref: "#/definitions/timestamp" },
382
+ author: { $ref: "#/definitions/user" },
383
+ editor: { $ref: "#/definitions/user" },
384
+ },
385
+ },
386
+ });
387
+
388
+ const result = plugin.process({
389
+ method: "GET",
390
+ path: "/api/document",
391
+ params: {},
392
+ query: {},
393
+ state: {},
394
+ headers: {},
395
+ body: null,
396
+ route: {},
397
+ });
398
+
399
+ expect(
400
+ validators.appearsToBeFromCategory([result.response.created], "date"),
401
+ ).toBe(true);
402
+ expect(
403
+ validators.appearsToBeFromCategory([result.response.updated], "date"),
404
+ ).toBe(true);
405
+ expect(
406
+ validators.appearsToBeFromCategory([result.response.author.id], "uuid"),
407
+ ).toBe(true);
408
+ expect(
409
+ validators.appearsToBeFromCategory([result.response.editor.id], "uuid"),
410
+ ).toBe(true);
411
+ });
412
+ });
413
+
414
+ describe("Performance Characteristics", () => {
415
+ it("maintains consistent performance across multiple calls", () => {
416
+ const plugin = schemaPlugin({
417
+ schema: schemas.complex.user(),
418
+ });
419
+
420
+ const context = {
421
+ method: "GET",
422
+ path: "/api/user",
423
+ params: {},
424
+ query: {},
425
+ state: {},
426
+ headers: {},
427
+ body: null,
428
+ route: {},
429
+ };
430
+
431
+ // Generate multiple times
432
+ const results = Array.from(
433
+ { length: 10 },
434
+ () => plugin.process(context).response,
435
+ );
436
+
437
+ // All should be valid but different
438
+ results.forEach((result) => {
439
+ expect(result).toHaveProperty("id");
440
+ expect(result).toHaveProperty("email");
441
+ });
442
+
443
+ // Should generate different data each time
444
+ const emails = results.map((r) => r.email);
445
+ const uniqueEmails = new Set(emails);
446
+ expect(uniqueEmails.size).toBeGreaterThan(5);
447
+ });
448
+
449
+ it("handles large schemas efficiently", () => {
450
+ const plugin = schemaPlugin({
451
+ schema: {
452
+ type: "object",
453
+ properties: Object.fromEntries(
454
+ Array.from({ length: 50 }, (_, i) => [
455
+ `field${i}`,
456
+ { type: i % 2 === 0 ? "string" : "number" },
457
+ ]),
458
+ ),
459
+ },
460
+ });
461
+
462
+ const context = {
463
+ method: "GET",
464
+ path: "/api/data",
465
+ params: {},
466
+ query: {},
467
+ state: {},
468
+ headers: {},
469
+ body: null,
470
+ route: {},
471
+ };
472
+
473
+ const result = plugin.process(context);
474
+
475
+ // Should have all fields
476
+ expect(Object.keys(result.response).length).toBeGreaterThanOrEqual(50);
477
+ });
478
+ });
479
+
480
+ describe("Real-world Plugin Usage", () => {
481
+ it("generates mock API responses for testing", () => {
482
+ const plugin = schemaPlugin({
483
+ schema: {
484
+ type: "object",
485
+ properties: {
486
+ success: { type: "boolean", const: true },
487
+ data: {
488
+ type: "object",
489
+ properties: {
490
+ id: { type: "string", format: "uuid" },
491
+ email: { type: "string", format: "email" },
492
+ profile: {
493
+ type: "object",
494
+ properties: {
495
+ name: { type: "string" },
496
+ avatar: { type: "string", format: "uri" },
497
+ },
498
+ },
499
+ },
500
+ },
501
+ timestamp: { type: "string", format: "date-time" },
502
+ },
503
+ },
504
+ overrides: {
505
+ "data.id": "{{params.userId}}",
506
+ timestamp: "{{state.currentTime}}",
507
+ },
508
+ });
509
+
510
+ const result = plugin.process({
511
+ method: "GET",
512
+ path: "/api/users/user-123",
513
+ params: { userId: "user-123" },
514
+ query: {},
515
+ state: new Map(),
516
+ routeState: { currentTime: "2024-01-01T12:00:00Z" },
517
+ headers: {},
518
+ body: null,
519
+ route: {},
520
+ });
521
+
522
+ expect(result.response.success).toBe(true);
523
+ expect(result.response.data.id).toBe("user-123");
524
+ expect(result.response.timestamp).toBe("2024-01-01T12:00:00Z");
525
+ expect(
526
+ validators.appearsToBeFromCategory(
527
+ [result.response.data.email],
528
+ "email",
529
+ ),
530
+ ).toBe(true);
531
+ });
532
+
533
+ it("works with schmock plugin pipeline", () => {
534
+ const schemaPlug = schemaPlugin({
535
+ schema: schemas.simple.object({
536
+ message: { type: "string" },
537
+ code: { type: "number" },
538
+ }),
539
+ });
540
+
541
+ // Mock another plugin in the pipeline
542
+ const loggingPlugin = {
543
+ name: "logger",
544
+ version: "1.0.0",
545
+ process: (ctx: any, response: any) => {
546
+ console.log(`Processing ${ctx.path}`);
547
+ return { context: ctx, response };
548
+ },
549
+ };
550
+
551
+ // Simulate pipeline execution
552
+ const context = {
553
+ method: "GET",
554
+ path: "/api/status",
555
+ params: {},
556
+ query: {},
557
+ state: {},
558
+ headers: {},
559
+ body: null,
560
+ route: {},
561
+ };
562
+
563
+ // First plugin generates response
564
+ const result1 = schemaPlug.process(context);
565
+
566
+ // Second plugin receives generated response
567
+ const result2 = loggingPlugin.process(result1.context, result1.response);
568
+
569
+ expect(result2.response).toHaveProperty("message");
570
+ expect(result2.response).toHaveProperty("code");
571
+ expect(result2.response).toBe(result1.response); // Same reference
572
+ });
573
+ });
574
+ });