@schmock/openapi 1.1.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.
Files changed (46) hide show
  1. package/dist/crud-detector.d.ts +35 -0
  2. package/dist/crud-detector.d.ts.map +1 -0
  3. package/dist/crud-detector.js +153 -0
  4. package/dist/generators.d.ts +14 -0
  5. package/dist/generators.d.ts.map +1 -0
  6. package/dist/generators.js +158 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +221 -0
  10. package/dist/normalizer.d.ts +14 -0
  11. package/dist/normalizer.d.ts.map +1 -0
  12. package/dist/normalizer.js +194 -0
  13. package/dist/parser.d.ts +32 -0
  14. package/dist/parser.d.ts.map +1 -0
  15. package/dist/parser.js +282 -0
  16. package/dist/plugin.d.ts +32 -0
  17. package/dist/plugin.d.ts.map +1 -0
  18. package/dist/plugin.js +129 -0
  19. package/dist/seed.d.ts +15 -0
  20. package/dist/seed.d.ts.map +1 -0
  21. package/dist/seed.js +41 -0
  22. package/package.json +45 -0
  23. package/src/__fixtures__/faker-stress-test.openapi.yaml +1030 -0
  24. package/src/__fixtures__/openapi31.json +34 -0
  25. package/src/__fixtures__/petstore-openapi3.json +168 -0
  26. package/src/__fixtures__/petstore-swagger2.json +141 -0
  27. package/src/__fixtures__/scalar-galaxy.yaml +1314 -0
  28. package/src/__fixtures__/stripe-fixtures3.json +6542 -0
  29. package/src/__fixtures__/stripe-spec3.yaml +161621 -0
  30. package/src/__fixtures__/train-travel.yaml +1264 -0
  31. package/src/crud-detector.test.ts +150 -0
  32. package/src/crud-detector.ts +194 -0
  33. package/src/generators.test.ts +214 -0
  34. package/src/generators.ts +212 -0
  35. package/src/index.ts +4 -0
  36. package/src/normalizer.test.ts +253 -0
  37. package/src/normalizer.ts +233 -0
  38. package/src/parser.test.ts +181 -0
  39. package/src/parser.ts +389 -0
  40. package/src/plugin.test.ts +205 -0
  41. package/src/plugin.ts +185 -0
  42. package/src/seed.ts +62 -0
  43. package/src/steps/openapi-crud.steps.ts +132 -0
  44. package/src/steps/openapi-parsing.steps.ts +111 -0
  45. package/src/steps/openapi-seed.steps.ts +94 -0
  46. package/src/stress.test.ts +2814 -0
@@ -0,0 +1,2814 @@
1
+ /// <reference path="../../../types/schmock.d.ts" />
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { schmock } from "@schmock/core";
6
+ import { beforeAll, describe, expect, it } from "vitest";
7
+ import { detectCrudResources } from "./crud-detector";
8
+ import { normalizeSchema } from "./normalizer";
9
+ import type { ParsedSpec } from "./parser";
10
+ import { parseSpec } from "./parser";
11
+ import { openapi } from "./plugin";
12
+
13
+ const fixturesDir = resolve(import.meta.dirname, "__fixtures__");
14
+ const trainTravelSpec = resolve(fixturesDir, "train-travel.yaml");
15
+
16
+ // ════════════════════════════════════════════════════════════════════
17
+ // 1. PARSER — real-world Train Travel API (OpenAPI 3.1)
18
+ // ════════════════════════════════════════════════════════════════════
19
+ describe("stress: parser — train-travel.yaml", () => {
20
+ it("parses the OpenAPI 3.1 Train Travel spec", async () => {
21
+ const spec = await parseSpec(trainTravelSpec);
22
+ expect(spec.title).toBe("Train Travel API");
23
+ expect(spec.version).toBe("1.2.1");
24
+ });
25
+
26
+ it("extracts basePath from first server URL", async () => {
27
+ const spec = await parseSpec(trainTravelSpec);
28
+ // servers[0] = "https://try.microcks.io/rest/Train+Travel+API/1.0.0"
29
+ expect(spec.basePath).toBe("/rest/Train+Travel+API/1.0.0");
30
+ });
31
+
32
+ it("extracts all path operations", async () => {
33
+ const spec = await parseSpec(trainTravelSpec);
34
+ const sigs = spec.paths.map((p) => `${p.method} ${p.path}`);
35
+
36
+ expect(sigs).toContain("GET /stations");
37
+ expect(sigs).toContain("GET /trips");
38
+ expect(sigs).toContain("GET /bookings");
39
+ expect(sigs).toContain("POST /bookings");
40
+ expect(sigs).toContain("GET /bookings/:bookingId");
41
+ expect(sigs).toContain("DELETE /bookings/:bookingId");
42
+ expect(sigs).toContain("POST /bookings/:bookingId/payment");
43
+ });
44
+
45
+ it("resolves $ref'd parameters (page, limit)", async () => {
46
+ const spec = await parseSpec(trainTravelSpec);
47
+ const getStations = spec.paths.find(
48
+ (p) => p.method === "GET" && p.path === "/stations",
49
+ );
50
+ expect(getStations).toBeDefined();
51
+
52
+ const paramNames = getStations?.parameters.map((p) => p.name) ?? [];
53
+ expect(paramNames).toContain("page");
54
+ expect(paramNames).toContain("limit");
55
+ expect(paramNames).toContain("coordinates");
56
+ expect(paramNames).toContain("search");
57
+ expect(paramNames).toContain("country");
58
+ });
59
+
60
+ it("resolves $ref'd response schemas (allOf composition)", async () => {
61
+ const spec = await parseSpec(trainTravelSpec);
62
+ const getStations = spec.paths.find(
63
+ (p) => p.method === "GET" && p.path === "/stations",
64
+ );
65
+
66
+ // 200 response exists with a schema
67
+ expect(getStations?.responses.has(200)).toBe(true);
68
+ const schema200 = getStations?.responses.get(200)?.schema;
69
+ expect(schema200).toBeDefined();
70
+ // The schema is an allOf composition — no unresolved $ref
71
+ const schemaStr = JSON.stringify(schema200);
72
+ expect(schemaStr).not.toContain("$ref");
73
+ });
74
+
75
+ it("resolves $ref'd error responses (400, 401, etc.)", async () => {
76
+ const spec = await parseSpec(trainTravelSpec);
77
+ const getStations = spec.paths.find(
78
+ (p) => p.method === "GET" && p.path === "/stations",
79
+ );
80
+
81
+ // Error responses use application/problem+json — parser should find them
82
+ expect(getStations?.responses.has(400)).toBe(true);
83
+ expect(getStations?.responses.has(401)).toBe(true);
84
+ expect(getStations?.responses.has(500)).toBe(true);
85
+ });
86
+
87
+ it("extracts path-level parameters for bookings/:bookingId", async () => {
88
+ const spec = await parseSpec(trainTravelSpec);
89
+ const getBooking = spec.paths.find(
90
+ (p) => p.method === "GET" && p.path === "/bookings/:bookingId",
91
+ );
92
+ expect(getBooking).toBeDefined();
93
+ expect(getBooking?.parameters).toContainEqual(
94
+ expect.objectContaining({ name: "bookingId", in: "path" }),
95
+ );
96
+ });
97
+
98
+ it("extracts requestBody from POST /bookings", async () => {
99
+ const spec = await parseSpec(trainTravelSpec);
100
+ const createBooking = spec.paths.find(
101
+ (p) => p.method === "POST" && p.path === "/bookings",
102
+ );
103
+ expect(createBooking?.requestBody).toBeDefined();
104
+ });
105
+
106
+ it("handles non-application/json content types (application/problem+json)", async () => {
107
+ const spec = await parseSpec(trainTravelSpec);
108
+ const getStations = spec.paths.find(
109
+ (p) => p.method === "GET" && p.path === "/stations",
110
+ );
111
+
112
+ // 400 response uses application/problem+json — parser should still extract schema
113
+ const resp400 = getStations?.responses.get(400);
114
+ expect(resp400?.schema).toBeDefined();
115
+ });
116
+
117
+ it("strips x-* extensions during normalization", async () => {
118
+ const spec = await parseSpec(trainTravelSpec);
119
+ // Verify parsed schemas don't contain x-* extensions
120
+ for (const p of spec.paths) {
121
+ if (p.requestBody) {
122
+ const str = JSON.stringify(p.requestBody);
123
+ expect(str).not.toContain('"x-');
124
+ }
125
+ }
126
+ });
127
+ });
128
+
129
+ // ════════════════════════════════════════════════════════════════════
130
+ // 2. CRUD DETECTOR — Train Travel spec
131
+ // ════════════════════════════════════════════════════════════════════
132
+ describe("stress: crud-detector — train-travel.yaml", () => {
133
+ it("detects bookings as a CRUD resource", async () => {
134
+ const spec = await parseSpec(trainTravelSpec);
135
+ const result = detectCrudResources(spec.paths);
136
+
137
+ const bookings = result.resources.find((r) => r.name === "bookings");
138
+ expect(bookings).toBeDefined();
139
+ expect(bookings?.basePath).toBe("/bookings");
140
+ expect(bookings?.itemPath).toBe("/bookings/:bookingId");
141
+ expect(bookings?.idParam).toBe("bookingId");
142
+ expect(bookings?.operations).toContain("list");
143
+ expect(bookings?.operations).toContain("create");
144
+ expect(bookings?.operations).toContain("read");
145
+ expect(bookings?.operations).toContain("delete");
146
+ });
147
+
148
+ it("classifies stations as non-CRUD (GET only, no create)", async () => {
149
+ const spec = await parseSpec(trainTravelSpec);
150
+ const result = detectCrudResources(spec.paths);
151
+
152
+ const stationsResource = result.resources.find(
153
+ (r) => r.name === "stations",
154
+ );
155
+ expect(stationsResource).toBeUndefined();
156
+
157
+ const stationsNonCrud = result.nonCrudPaths.find(
158
+ (p) => p.path === "/stations",
159
+ );
160
+ expect(stationsNonCrud).toBeDefined();
161
+ });
162
+
163
+ it("classifies trips as non-CRUD (GET only, no create)", async () => {
164
+ const spec = await parseSpec(trainTravelSpec);
165
+ const result = detectCrudResources(spec.paths);
166
+
167
+ const tripsResource = result.resources.find((r) => r.name === "trips");
168
+ expect(tripsResource).toBeUndefined();
169
+ });
170
+
171
+ it("classifies /bookings/:bookingId/payment as non-CRUD", async () => {
172
+ const spec = await parseSpec(trainTravelSpec);
173
+ const result = detectCrudResources(spec.paths);
174
+
175
+ // payment is nested 2 levels deep under bookings, not a simple item path
176
+ const paymentNonCrud = result.nonCrudPaths.find(
177
+ (p) => p.path === "/bookings/:bookingId/payment",
178
+ );
179
+ expect(paymentNonCrud).toBeDefined();
180
+ });
181
+ });
182
+
183
+ // ════════════════════════════════════════════════════════════════════
184
+ // 3. FULL INTEGRATION — Train Travel spec
185
+ // ════════════════════════════════════════════════════════════════════
186
+ describe("stress: integration — train-travel.yaml", () => {
187
+ it("auto-registers all routes and handles requests", async () => {
188
+ const mock = schmock({ state: {} });
189
+ mock.pipe(await openapi({ spec: trainTravelSpec }));
190
+
191
+ // Stations: non-CRUD static endpoint → should return generated response
192
+ const stations = await mock.handle("GET", "/stations");
193
+ expect(stations.status).toBe(200);
194
+ expect(stations.body).toBeDefined();
195
+
196
+ // Trips: non-CRUD static endpoint
197
+ const trips = await mock.handle("GET", "/trips");
198
+ expect(trips.status).toBe(200);
199
+
200
+ // Bookings: CRUD resource
201
+ const emptyList = await mock.handle("GET", "/bookings");
202
+ expect(emptyList.status).toBe(200);
203
+ expect(emptyList.body).toEqual([]);
204
+ });
205
+
206
+ it("bookings CRUD lifecycle", async () => {
207
+ const mock = schmock({ state: {} });
208
+ mock.pipe(await openapi({ spec: trainTravelSpec }));
209
+
210
+ // Create
211
+ const created = await mock.handle("POST", "/bookings", {
212
+ body: {
213
+ trip_id: "ea399ba1-6d95-433f-92d1-83f67b775594",
214
+ passenger_name: "John Doe",
215
+ has_bicycle: true,
216
+ has_dog: false,
217
+ },
218
+ });
219
+ expect(created.status).toBe(201);
220
+ const booking = created.body as Record<string, unknown>;
221
+ expect(booking.passenger_name).toBe("John Doe");
222
+ expect(booking.bookingId).toBe(1);
223
+
224
+ // Read
225
+ const read = await mock.handle("GET", "/bookings/1");
226
+ expect(read.status).toBe(200);
227
+ expect(read.body).toMatchObject({ passenger_name: "John Doe" });
228
+
229
+ // List
230
+ const list = await mock.handle("GET", "/bookings");
231
+ expect(list.status).toBe(200);
232
+ expect(list.body).toHaveLength(1);
233
+
234
+ // Delete
235
+ const deleted = await mock.handle("DELETE", "/bookings/1");
236
+ expect(deleted.status).toBe(204);
237
+
238
+ // Verify deletion
239
+ const afterDelete = await mock.handle("GET", "/bookings/1");
240
+ expect(afterDelete.status).toBe(404);
241
+ });
242
+
243
+ it("payment endpoint returns a response (non-CRUD)", async () => {
244
+ const mock = schmock({ state: {} });
245
+ mock.pipe(await openapi({ spec: trainTravelSpec }));
246
+
247
+ // POST /bookings/:bookingId/payment — non-CRUD static
248
+ const payment = await mock.handle("POST", "/bookings/123/payment", {
249
+ body: {
250
+ amount: 49.99,
251
+ currency: "gbp",
252
+ source: { object: "card", name: "J. Doe", number: "4242..." },
253
+ },
254
+ });
255
+ expect(payment.status).toBe(200);
256
+ expect(payment.body).toBeDefined();
257
+ });
258
+
259
+ it("multiple bookings with sequential IDs", async () => {
260
+ const mock = schmock({ state: {} });
261
+ mock.pipe(await openapi({ spec: trainTravelSpec }));
262
+
263
+ for (let i = 0; i < 5; i++) {
264
+ const res = await mock.handle("POST", "/bookings", {
265
+ body: { passenger_name: `Passenger-${i}`, trip_id: "trip-1" },
266
+ });
267
+ expect(res.status).toBe(201);
268
+ const body = res.body as Record<string, unknown>;
269
+ expect(body.bookingId).toBe(i + 1);
270
+ }
271
+
272
+ const list = await mock.handle("GET", "/bookings");
273
+ expect(list.body).toHaveLength(5);
274
+ });
275
+ });
276
+
277
+ // ════════════════════════════════════════════════════════════════════
278
+ // 4. NORMALIZER STRESS
279
+ // ════════════════════════════════════════════════════════════════════
280
+ describe("stress: normalizer", () => {
281
+ it("handles nullable + exclusiveMinimum combo", () => {
282
+ const result = normalizeSchema(
283
+ {
284
+ type: "number",
285
+ format: "double",
286
+ exclusiveMinimum: true,
287
+ minimum: 0,
288
+ nullable: true,
289
+ },
290
+ "response",
291
+ );
292
+ expect(result.oneOf).toBeDefined();
293
+ const branches = result.oneOf;
294
+ expect(branches).toHaveLength(2);
295
+ const numericBranch = (branches as Record<string, unknown>[])[0];
296
+ expect(numericBranch.exclusiveMinimum).toBe(0);
297
+ expect(numericBranch).not.toHaveProperty("minimum");
298
+ });
299
+
300
+ it("handles exclusiveMaximum: true (boolean → number)", () => {
301
+ const result = normalizeSchema(
302
+ {
303
+ type: "number",
304
+ minimum: 0,
305
+ exclusiveMaximum: true,
306
+ maximum: 300000,
307
+ },
308
+ "response",
309
+ );
310
+ expect(result.exclusiveMaximum).toBe(300000);
311
+ expect(result).not.toHaveProperty("maximum");
312
+ });
313
+
314
+ it("passes through exclusiveMinimum already as number (OpenAPI 3.1)", () => {
315
+ // In 3.1, exclusiveMinimum is already a number — should not be transformed
316
+ const result = normalizeSchema(
317
+ {
318
+ type: "number",
319
+ exclusiveMinimum: 0,
320
+ },
321
+ "response",
322
+ );
323
+ expect(result.exclusiveMinimum).toBe(0);
324
+ });
325
+
326
+ it("strips readOnly from request, keeps in response", () => {
327
+ const schema = {
328
+ type: "object",
329
+ required: ["id", "name"],
330
+ properties: {
331
+ id: { type: "string", format: "uuid", readOnly: true },
332
+ name: { type: "string" },
333
+ createdAt: { type: "string", format: "date-time", readOnly: true },
334
+ },
335
+ };
336
+
337
+ const request = normalizeSchema(schema, "request");
338
+ expect(request.properties).not.toHaveProperty("id");
339
+ expect(request.properties).not.toHaveProperty("createdAt");
340
+ expect(request.required).not.toContain("id");
341
+
342
+ const response = normalizeSchema(schema, "response");
343
+ expect(response.properties).toHaveProperty("id");
344
+ expect(response.properties).toHaveProperty("createdAt");
345
+ });
346
+
347
+ it("strips writeOnly from response, keeps in request", () => {
348
+ const schema = {
349
+ type: "object",
350
+ properties: {
351
+ username: { type: "string" },
352
+ cvc: { type: "string", writeOnly: true },
353
+ address_line1: { type: "string", writeOnly: true },
354
+ },
355
+ };
356
+
357
+ const response = normalizeSchema(schema, "response");
358
+ expect(response.properties).not.toHaveProperty("cvc");
359
+ expect(response.properties).not.toHaveProperty("address_line1");
360
+
361
+ const request = normalizeSchema(schema, "request");
362
+ expect(request.properties).toHaveProperty("cvc");
363
+ expect(request.properties).toHaveProperty("address_line1");
364
+ });
365
+
366
+ it("handles allOf compositions (Wrapper-Collection pattern)", () => {
367
+ const result = normalizeSchema(
368
+ {
369
+ allOf: [
370
+ {
371
+ type: "object",
372
+ properties: {
373
+ data: { type: "array", items: { type: "object" } },
374
+ links: { type: "object", readOnly: true },
375
+ },
376
+ },
377
+ {
378
+ properties: {
379
+ data: {
380
+ type: "array",
381
+ items: {
382
+ type: "object",
383
+ properties: {
384
+ id: { type: "string", format: "uuid" },
385
+ name: { type: "string", example: "Berlin Hbf" },
386
+ },
387
+ },
388
+ },
389
+ },
390
+ },
391
+ ],
392
+ },
393
+ "response",
394
+ );
395
+
396
+ expect(result.allOf).toBeDefined();
397
+ expect(result.allOf).toHaveLength(2);
398
+ });
399
+
400
+ it("handles oneOf with const (card vs bank_account)", () => {
401
+ const result = normalizeSchema(
402
+ {
403
+ oneOf: [
404
+ {
405
+ type: "object",
406
+ properties: {
407
+ object: { type: "string", const: "card" },
408
+ name: { type: "string" },
409
+ },
410
+ },
411
+ {
412
+ type: "object",
413
+ properties: {
414
+ object: { type: "string", const: "bank_account" },
415
+ name: { type: "string" },
416
+ },
417
+ },
418
+ ],
419
+ },
420
+ "response",
421
+ );
422
+
423
+ expect(result.oneOf).toHaveLength(2);
424
+ // const should pass through (standard JSON Schema keyword)
425
+ const branches = result.oneOf as Record<string, unknown>[];
426
+ const branch0Props = (branches[0] as Record<string, unknown>)
427
+ .properties as Record<string, unknown>;
428
+ const obj0 = branch0Props.object as Record<string, unknown>;
429
+ expect(obj0.const).toBe("card");
430
+ });
431
+
432
+ it("handles additionalProperties: true (free-form)", () => {
433
+ const result = normalizeSchema(
434
+ { type: "object", additionalProperties: true },
435
+ "response",
436
+ );
437
+ expect(result.additionalProperties).toBe(true);
438
+ });
439
+
440
+ it("handles additionalProperties as schema with nullable", () => {
441
+ const result = normalizeSchema(
442
+ {
443
+ type: "object",
444
+ additionalProperties: { type: "string", nullable: true },
445
+ },
446
+ "response",
447
+ );
448
+ const ap = result.additionalProperties;
449
+ expect(typeof ap === "object" && ap !== null && "oneOf" in ap).toBe(true);
450
+ });
451
+
452
+ it("does not mutate the input schema", () => {
453
+ const input = {
454
+ type: "string",
455
+ nullable: true,
456
+ "x-custom": "value",
457
+ };
458
+ const copy = JSON.parse(JSON.stringify(input));
459
+ normalizeSchema(input, "response");
460
+ expect(input).toEqual(copy);
461
+ });
462
+
463
+ it("handles empty schema", () => {
464
+ expect(normalizeSchema({}, "response")).toEqual({});
465
+ });
466
+
467
+ it("handles deeply nested: allOf → properties → oneOf → nullable", () => {
468
+ const result = normalizeSchema(
469
+ {
470
+ allOf: [
471
+ {
472
+ type: "object",
473
+ properties: {
474
+ source: {
475
+ oneOf: [
476
+ {
477
+ type: "object",
478
+ properties: {
479
+ cvc: { type: "string", writeOnly: true },
480
+ },
481
+ },
482
+ {
483
+ type: "object",
484
+ properties: {
485
+ sortCode: { type: "string", nullable: true },
486
+ },
487
+ },
488
+ ],
489
+ },
490
+ },
491
+ },
492
+ ],
493
+ },
494
+ "response",
495
+ );
496
+
497
+ const allOf = result.allOf as Record<string, unknown>[];
498
+ const props = allOf[0].properties as Record<string, unknown>;
499
+ const source = props.source as Record<string, unknown>;
500
+ const branches = source.oneOf as Record<string, unknown>[];
501
+
502
+ // writeOnly cvc stripped from response
503
+ const cardProps = branches[0].properties as Record<string, unknown>;
504
+ expect(cardProps).not.toHaveProperty("cvc");
505
+
506
+ // nullable sortCode → oneOf
507
+ const bankProps = branches[1].properties as Record<string, unknown>;
508
+ const sortCode = bankProps.sortCode as Record<string, unknown>;
509
+ expect(sortCode.oneOf).toBeDefined();
510
+ });
511
+ });
512
+
513
+ // ════════════════════════════════════════════════════════════════════
514
+ // 5. CRUD DETECTOR — edge cases
515
+ // ════════════════════════════════════════════════════════════════════
516
+ describe("stress: crud-detector edge cases", () => {
517
+ it("single-POST-only endpoints are non-CRUD", () => {
518
+ const result = detectCrudResources([
519
+ {
520
+ path: "/webhook",
521
+ method: "POST",
522
+ parameters: [],
523
+ responses: new Map(),
524
+ tags: [],
525
+ },
526
+ ]);
527
+ expect(result.resources).toHaveLength(0);
528
+ expect(result.nonCrudPaths).toHaveLength(1);
529
+ });
530
+
531
+ it("deeply nested resource paths", () => {
532
+ const result = detectCrudResources([
533
+ {
534
+ path: "/org/:orgId/team/:teamId/members",
535
+ method: "GET",
536
+ parameters: [],
537
+ responses: new Map(),
538
+ tags: [],
539
+ },
540
+ {
541
+ path: "/org/:orgId/team/:teamId/members",
542
+ method: "POST",
543
+ parameters: [],
544
+ responses: new Map(),
545
+ tags: [],
546
+ },
547
+ {
548
+ path: "/org/:orgId/team/:teamId/members/:memberId",
549
+ method: "GET",
550
+ parameters: [],
551
+ responses: new Map(),
552
+ tags: [],
553
+ },
554
+ ]);
555
+
556
+ expect(result.resources).toHaveLength(1);
557
+ expect(result.resources[0].name).toBe("members");
558
+ expect(result.resources[0].basePath).toBe(
559
+ "/org/:orgId/team/:teamId/members",
560
+ );
561
+ expect(result.resources[0].idParam).toBe("memberId");
562
+ });
563
+
564
+ it("root path / is non-CRUD", () => {
565
+ const result = detectCrudResources([
566
+ {
567
+ path: "/",
568
+ method: "GET",
569
+ parameters: [],
570
+ responses: new Map(),
571
+ tags: [],
572
+ },
573
+ ]);
574
+ expect(result.resources).toHaveLength(0);
575
+ expect(result.nonCrudPaths).toHaveLength(1);
576
+ });
577
+
578
+ it("GET-only collection is non-CRUD", () => {
579
+ const result = detectCrudResources([
580
+ {
581
+ path: "/readonly-things",
582
+ method: "GET",
583
+ parameters: [],
584
+ responses: new Map(),
585
+ tags: [],
586
+ },
587
+ ]);
588
+ expect(result.resources).toHaveLength(0);
589
+ });
590
+ });
591
+
592
+ // ════════════════════════════════════════════════════════════════════
593
+ // 6. GENERATOR STRESS — via plugin integration
594
+ // ════════════════════════════════════════════════════════════════════
595
+ describe("stress: generators via plugin", () => {
596
+ it("rapid sequential creates (100 items)", async () => {
597
+ const mock = schmock({ state: {} });
598
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
599
+
600
+ for (let i = 0; i < 100; i++) {
601
+ const res = await mock.handle("POST", "/pets", {
602
+ body: { name: `Pet-${i}` },
603
+ });
604
+ expect(res.status).toBe(201);
605
+ const body = res.body as Record<string, unknown>;
606
+ expect(body.petId).toBe(i + 1);
607
+ }
608
+
609
+ const list = await mock.handle("GET", "/pets");
610
+ expect(list.body).toHaveLength(100);
611
+ });
612
+
613
+ it("create with empty body", async () => {
614
+ const mock = schmock({ state: {} });
615
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
616
+
617
+ const res = await mock.handle("POST", "/pets");
618
+ expect(res.status).toBe(201);
619
+ const body = res.body as Record<string, unknown>;
620
+ expect(body.petId).toBe(1);
621
+ });
622
+
623
+ it("update with empty body preserves existing", async () => {
624
+ const mock = schmock({ state: {} });
625
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
626
+
627
+ await mock.handle("POST", "/pets", {
628
+ body: { name: "Buddy", tag: "dog" },
629
+ });
630
+ const updated = await mock.handle("PUT", "/pets/1");
631
+ expect(updated.status).toBe(200);
632
+ const body = updated.body as Record<string, unknown>;
633
+ expect(body.name).toBe("Buddy");
634
+ expect(body.petId).toBe(1);
635
+ });
636
+
637
+ it("update cannot overwrite ID", async () => {
638
+ const mock = schmock({ state: {} });
639
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
640
+
641
+ await mock.handle("POST", "/pets", { body: { name: "Buddy" } });
642
+ const updated = await mock.handle("PUT", "/pets/1", {
643
+ body: { name: "Max", petId: 999 },
644
+ });
645
+ const body = updated.body as Record<string, unknown>;
646
+ expect(body.petId).toBe(1);
647
+ });
648
+
649
+ it("double-delete returns 404", async () => {
650
+ const mock = schmock({ state: {} });
651
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
652
+
653
+ await mock.handle("POST", "/pets", { body: { name: "Buddy" } });
654
+ await mock.handle("DELETE", "/pets/1");
655
+ const second = await mock.handle("DELETE", "/pets/1");
656
+ expect(second.status).toBe(404);
657
+ });
658
+
659
+ it("create-delete-list: only even IDs survive", async () => {
660
+ const mock = schmock({ state: {} });
661
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
662
+
663
+ for (let i = 0; i < 10; i++) {
664
+ await mock.handle("POST", "/pets", { body: { name: `Pet-${i}` } });
665
+ }
666
+ for (let i = 1; i <= 10; i += 2) {
667
+ await mock.handle("DELETE", `/pets/${i}`);
668
+ }
669
+
670
+ const list = await mock.handle("GET", "/pets");
671
+ const items = list.body as Record<string, unknown>[];
672
+ expect(items).toHaveLength(5);
673
+ for (const item of items) {
674
+ expect((item.petId as number) % 2).toBe(0);
675
+ }
676
+ });
677
+
678
+ it("resetState clears CRUD collections", async () => {
679
+ const mock = schmock({ state: {} });
680
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
681
+
682
+ await mock.handle("POST", "/pets", { body: { name: "Buddy" } });
683
+ expect((await mock.handle("GET", "/pets")).body).toHaveLength(1);
684
+
685
+ mock.resetState();
686
+
687
+ const after = await mock.handle("GET", "/pets");
688
+ expect(after.body).toEqual([]);
689
+ });
690
+
691
+ it("string IDs work with seed data", async () => {
692
+ const mock = schmock({ state: {} });
693
+ mock.pipe(
694
+ await openapi({
695
+ spec: `${fixturesDir}/petstore-swagger2.json`,
696
+ seed: { pets: [{ petId: "abc-123", name: "Luna" }] },
697
+ }),
698
+ );
699
+
700
+ const read = await mock.handle("GET", "/pets/abc-123");
701
+ expect(read.status).toBe(200);
702
+ expect(read.body).toMatchObject({ name: "Luna" });
703
+ });
704
+ });
705
+
706
+ // ════════════════════════════════════════════════════════════════════
707
+ // 7. SEED STRESS
708
+ // ════════════════════════════════════════════════════════════════════
709
+ describe("stress: seed data", () => {
710
+ it("large inline dataset (200 items)", async () => {
711
+ const items = Array.from({ length: 200 }, (_, i) => ({
712
+ petId: i + 1,
713
+ name: `Pet-${i + 1}`,
714
+ }));
715
+
716
+ const mock = schmock({ state: {} });
717
+ mock.pipe(
718
+ await openapi({
719
+ spec: `${fixturesDir}/petstore-swagger2.json`,
720
+ seed: { pets: items },
721
+ }),
722
+ );
723
+
724
+ const list = await mock.handle("GET", "/pets");
725
+ expect(list.body).toHaveLength(200);
726
+ });
727
+
728
+ it("auto-increment continues after seed", async () => {
729
+ const mock = schmock({ state: {} });
730
+ mock.pipe(
731
+ await openapi({
732
+ spec: `${fixturesDir}/petstore-swagger2.json`,
733
+ seed: {
734
+ pets: Array.from({ length: 50 }, (_, i) => ({
735
+ petId: i + 1,
736
+ name: `Pet-${i + 1}`,
737
+ })),
738
+ },
739
+ }),
740
+ );
741
+
742
+ const created = await mock.handle("POST", "/pets", {
743
+ body: { name: "New" },
744
+ });
745
+ const body = created.body as Record<string, unknown>;
746
+ expect(body.petId).toBe(51);
747
+ });
748
+
749
+ it("non-sequential IDs: max is picked correctly", async () => {
750
+ const mock = schmock({ state: {} });
751
+ mock.pipe(
752
+ await openapi({
753
+ spec: `${fixturesDir}/petstore-swagger2.json`,
754
+ seed: {
755
+ pets: [
756
+ { petId: 10, name: "A" },
757
+ { petId: 5, name: "B" },
758
+ { petId: 100, name: "C" },
759
+ ],
760
+ },
761
+ }),
762
+ );
763
+
764
+ const created = await mock.handle("POST", "/pets", {
765
+ body: { name: "New" },
766
+ });
767
+ const body = created.body as Record<string, unknown>;
768
+ expect(body.petId).toBe(101);
769
+ });
770
+ });
771
+
772
+ // ════════════════════════════════════════════════════════════════════
773
+ // 8. PLUGIN EDGE CASES
774
+ // ════════════════════════════════════════════════════════════════════
775
+ describe("stress: plugin edge cases", () => {
776
+ it("all-static spec (no CRUD resources)", async () => {
777
+ const mock = schmock({ state: {} });
778
+ mock.pipe(
779
+ await openapi({
780
+ spec: {
781
+ openapi: "3.0.3",
782
+ info: { title: "Static", version: "1.0.0" },
783
+ paths: {
784
+ "/health": {
785
+ get: {
786
+ responses: {
787
+ "200": {
788
+ description: "OK",
789
+ content: {
790
+ "application/json": {
791
+ schema: {
792
+ type: "object",
793
+ properties: { status: { type: "string" } },
794
+ },
795
+ },
796
+ },
797
+ },
798
+ },
799
+ },
800
+ },
801
+ },
802
+ },
803
+ }),
804
+ );
805
+
806
+ const health = await mock.handle("GET", "/health");
807
+ expect(health.status).toBe(200);
808
+ });
809
+
810
+ it("multiple openapi plugins coexist", async () => {
811
+ const mock = schmock({ state: {} });
812
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
813
+ mock.pipe(
814
+ await openapi({
815
+ spec: {
816
+ openapi: "3.0.3",
817
+ info: { title: "Users", version: "1.0.0" },
818
+ paths: {
819
+ "/users": {
820
+ get: {
821
+ responses: {
822
+ "200": {
823
+ description: "List",
824
+ content: {
825
+ "application/json": {
826
+ schema: {
827
+ type: "array",
828
+ items: {
829
+ type: "object",
830
+ properties: { userId: { type: "integer" } },
831
+ },
832
+ },
833
+ },
834
+ },
835
+ },
836
+ },
837
+ },
838
+ post: {
839
+ responses: { "201": { description: "Created" } },
840
+ },
841
+ },
842
+ },
843
+ },
844
+ }),
845
+ );
846
+
847
+ await mock.handle("POST", "/pets", { body: { name: "Buddy" } });
848
+ await mock.handle("POST", "/users", { body: { name: "Alice" } });
849
+
850
+ expect((await mock.handle("GET", "/pets")).body).toHaveLength(1);
851
+ expect((await mock.handle("GET", "/users")).body).toHaveLength(1);
852
+ });
853
+
854
+ it("unregistered routes return 404", async () => {
855
+ const mock = schmock({ state: {} });
856
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
857
+
858
+ const res = await mock.handle("GET", "/nonexistent");
859
+ expect(res.status).toBe(404);
860
+ });
861
+
862
+ it("spec with no response schemas returns empty object", async () => {
863
+ const mock = schmock({ state: {} });
864
+ mock.pipe(
865
+ await openapi({
866
+ spec: {
867
+ openapi: "3.0.3",
868
+ info: { title: "NoSchema", version: "1.0.0" },
869
+ paths: {
870
+ "/ping": {
871
+ get: {
872
+ responses: { "204": { description: "No Content" } },
873
+ },
874
+ },
875
+ },
876
+ },
877
+ }),
878
+ );
879
+
880
+ const ping = await mock.handle("GET", "/ping");
881
+ expect(ping.status).toBe(200);
882
+ });
883
+ });
884
+
885
+ // ════════════════════════════════════════════════════════════════════
886
+ // 9. PARSER EDGE CASES
887
+ // ════════════════════════════════════════════════════════════════════
888
+ describe("stress: parser edge cases", () => {
889
+ it("empty paths object", async () => {
890
+ const spec = await parseSpec({
891
+ openapi: "3.0.3",
892
+ info: { title: "Empty", version: "0.0.0" },
893
+ paths: {},
894
+ });
895
+ expect(spec.paths).toEqual([]);
896
+ });
897
+
898
+ it("no paths key at all", async () => {
899
+ const spec = await parseSpec({
900
+ openapi: "3.0.3",
901
+ info: { title: "NoPaths", version: "0.0.0" },
902
+ });
903
+ expect(spec.paths).toEqual([]);
904
+ });
905
+
906
+ it("servers with relative path", async () => {
907
+ const spec = await parseSpec({
908
+ openapi: "3.0.3",
909
+ info: { title: "Rel", version: "0.0.0" },
910
+ servers: [{ url: "/v3" }],
911
+ paths: {},
912
+ });
913
+ expect(spec.basePath).toBe("/v3");
914
+ });
915
+
916
+ it("servers with root path only", async () => {
917
+ const spec = await parseSpec({
918
+ openapi: "3.0.3",
919
+ info: { title: "Root", version: "0.0.0" },
920
+ servers: [{ url: "/" }],
921
+ paths: {},
922
+ });
923
+ expect(spec.basePath).toBe("");
924
+ });
925
+
926
+ it("Swagger 2.0 basePath is extracted", async () => {
927
+ const spec = await parseSpec(`${fixturesDir}/petstore-swagger2.json`);
928
+ expect(spec.basePath).toBe("/api");
929
+ });
930
+ });
931
+
932
+ // ════════════════════════════════════════════════════════════════════
933
+ // 10. SCALAR GALAXY — OpenAPI 3.1.1 BREAD stress
934
+ // ════════════════════════════════════════════════════════════════════
935
+
936
+ const scalarGalaxySpec = resolve(fixturesDir, "scalar-galaxy.yaml");
937
+
938
+ // Real solar system seed data — planetId matches the CRUD resource's idParam
939
+ const solarSystemPlanets = [
940
+ {
941
+ planetId: 1,
942
+ name: "Mercury",
943
+ type: "terrestrial",
944
+ habitabilityIndex: 0.01,
945
+ physicalProperties: { mass: 0.055, radius: 0.383, gravity: 0.38 },
946
+ atmosphere: [],
947
+ tags: ["solar-system", "rocky", "inner"],
948
+ },
949
+ {
950
+ planetId: 2,
951
+ name: "Venus",
952
+ type: "terrestrial",
953
+ habitabilityIndex: 0.04,
954
+ physicalProperties: { mass: 0.815, radius: 0.95, gravity: 0.9 },
955
+ atmosphere: [{ compound: "CO2", percentage: 96.5 }],
956
+ tags: ["solar-system", "rocky", "inner"],
957
+ },
958
+ {
959
+ planetId: 3,
960
+ name: "Earth",
961
+ type: "terrestrial",
962
+ habitabilityIndex: 1.0,
963
+ physicalProperties: { mass: 1.0, radius: 1.0, gravity: 1.0 },
964
+ atmosphere: [
965
+ { compound: "N2", percentage: 78.1 },
966
+ { compound: "O2", percentage: 20.9 },
967
+ ],
968
+ tags: ["solar-system", "rocky", "habitable"],
969
+ },
970
+ {
971
+ planetId: 4,
972
+ name: "Mars",
973
+ type: "terrestrial",
974
+ habitabilityIndex: 0.68,
975
+ physicalProperties: { mass: 0.107, radius: 0.532, gravity: 0.378 },
976
+ atmosphere: [{ compound: "CO2", percentage: 95.3 }],
977
+ tags: ["solar-system", "rocky", "explored"],
978
+ },
979
+ {
980
+ planetId: 5,
981
+ name: "Jupiter",
982
+ type: "gas_giant",
983
+ habitabilityIndex: 0.0,
984
+ physicalProperties: { mass: 317.8, radius: 11.21, gravity: 2.53 },
985
+ atmosphere: [
986
+ { compound: "H2", percentage: 89.8 },
987
+ { compound: "He", percentage: 10.2 },
988
+ ],
989
+ tags: ["solar-system", "gas", "outer"],
990
+ },
991
+ {
992
+ planetId: 6,
993
+ name: "Saturn",
994
+ type: "gas_giant",
995
+ habitabilityIndex: 0.0,
996
+ physicalProperties: { mass: 95.16, radius: 9.45, gravity: 1.065 },
997
+ atmosphere: [
998
+ { compound: "H2", percentage: 96.3 },
999
+ { compound: "He", percentage: 3.25 },
1000
+ ],
1001
+ tags: ["solar-system", "gas", "ringed"],
1002
+ },
1003
+ {
1004
+ planetId: 7,
1005
+ name: "Uranus",
1006
+ type: "ice_giant",
1007
+ habitabilityIndex: 0.0,
1008
+ physicalProperties: { mass: 14.54, radius: 4.01, gravity: 0.886 },
1009
+ atmosphere: [
1010
+ { compound: "H2", percentage: 82.5 },
1011
+ { compound: "He", percentage: 15.2 },
1012
+ ],
1013
+ tags: ["solar-system", "ice", "outer"],
1014
+ },
1015
+ {
1016
+ planetId: 8,
1017
+ name: "Neptune",
1018
+ type: "ice_giant",
1019
+ habitabilityIndex: 0.0,
1020
+ physicalProperties: { mass: 17.15, radius: 3.88, gravity: 1.14 },
1021
+ atmosphere: [
1022
+ { compound: "H2", percentage: 80.0 },
1023
+ { compound: "He", percentage: 19.0 },
1024
+ ],
1025
+ tags: ["solar-system", "ice", "outer"],
1026
+ },
1027
+ ];
1028
+
1029
+ describe("stress: scalar-galaxy.yaml — parser", () => {
1030
+ it("parses the OpenAPI 3.1.1 Scalar Galaxy spec", async () => {
1031
+ const spec = await parseSpec(scalarGalaxySpec);
1032
+ expect(spec.title).toBe("Scalar Galaxy");
1033
+ expect(spec.version).toBe("0.5.12");
1034
+ });
1035
+
1036
+ it("strips x-speakeasy-webhooks and x-scalar-* extensions", async () => {
1037
+ const spec = await parseSpec(scalarGalaxySpec);
1038
+ for (const p of spec.paths) {
1039
+ if (p.requestBody) {
1040
+ const str = JSON.stringify(p.requestBody);
1041
+ expect(str).not.toContain('"x-');
1042
+ }
1043
+ for (const [, resp] of p.responses) {
1044
+ if (resp.schema) {
1045
+ const str = JSON.stringify(resp.schema);
1046
+ expect(str).not.toContain('"x-');
1047
+ }
1048
+ }
1049
+ }
1050
+ });
1051
+
1052
+ it("extracts all planet routes", async () => {
1053
+ const spec = await parseSpec(scalarGalaxySpec);
1054
+ const sigs = spec.paths.map((p) => `${p.method} ${p.path}`);
1055
+ expect(sigs).toContain("GET /planets");
1056
+ expect(sigs).toContain("POST /planets");
1057
+ expect(sigs).toContain("GET /planets/:planetId");
1058
+ expect(sigs).toContain("PUT /planets/:planetId");
1059
+ expect(sigs).toContain("DELETE /planets/:planetId");
1060
+ expect(sigs).toContain("POST /planets/:planetId/image");
1061
+ });
1062
+
1063
+ it("extracts auth routes", async () => {
1064
+ const spec = await parseSpec(scalarGalaxySpec);
1065
+ const sigs = spec.paths.map((p) => `${p.method} ${p.path}`);
1066
+ expect(sigs).toContain("POST /user/signup");
1067
+ expect(sigs).toContain("POST /auth/token");
1068
+ expect(sigs).toContain("GET /me");
1069
+ });
1070
+
1071
+ it("extracts $ref'd parameters (limit, offset, planetId)", async () => {
1072
+ const spec = await parseSpec(scalarGalaxySpec);
1073
+ const listPlanets = spec.paths.find(
1074
+ (p) => p.method === "GET" && p.path === "/planets",
1075
+ );
1076
+ const paramNames = listPlanets?.parameters.map((p) => p.name) ?? [];
1077
+ expect(paramNames).toContain("limit");
1078
+ expect(paramNames).toContain("offset");
1079
+
1080
+ const getPlanet = spec.paths.find(
1081
+ (p) => p.method === "GET" && p.path === "/planets/:planetId",
1082
+ );
1083
+ const itemParamNames = getPlanet?.parameters.map((p) => p.name) ?? [];
1084
+ expect(itemParamNames).toContain("planetId");
1085
+ });
1086
+
1087
+ it("extracts Planet schema with nested physicalProperties", async () => {
1088
+ const spec = await parseSpec(scalarGalaxySpec);
1089
+ const getPlanet = spec.paths.find(
1090
+ (p) => p.method === "GET" && p.path === "/planets/:planetId",
1091
+ );
1092
+ const schema = getPlanet?.responses.get(200)?.schema;
1093
+ expect(schema).toBeDefined();
1094
+ expect(schema?.properties).toHaveProperty("name");
1095
+ expect(schema?.properties).toHaveProperty("physicalProperties");
1096
+ expect(schema?.properties).toHaveProperty("atmosphere");
1097
+ });
1098
+
1099
+ it("strips readOnly fields (id, lastUpdated) from request schemas", async () => {
1100
+ const spec = await parseSpec(scalarGalaxySpec);
1101
+ const createPlanet = spec.paths.find(
1102
+ (p) => p.method === "POST" && p.path === "/planets",
1103
+ );
1104
+ expect(createPlanet?.requestBody).toBeDefined();
1105
+ // id and lastUpdated are readOnly — should be stripped from request
1106
+ expect(createPlanet?.requestBody?.properties).not.toHaveProperty("id");
1107
+ expect(createPlanet?.requestBody?.properties).not.toHaveProperty(
1108
+ "lastUpdated",
1109
+ );
1110
+ // name should remain
1111
+ expect(createPlanet?.requestBody?.properties).toHaveProperty("name");
1112
+ });
1113
+ });
1114
+
1115
+ describe("stress: scalar-galaxy.yaml — CRUD detection", () => {
1116
+ it("detects planets as a full CRUD resource", async () => {
1117
+ const spec = await parseSpec(scalarGalaxySpec);
1118
+ const result = detectCrudResources(spec.paths);
1119
+ const planets = result.resources.find((r) => r.name === "planets");
1120
+ expect(planets).toBeDefined();
1121
+ expect(planets?.basePath).toBe("/planets");
1122
+ expect(planets?.itemPath).toBe("/planets/:planetId");
1123
+ expect(planets?.idParam).toBe("planetId");
1124
+ expect(planets?.operations).toContain("list");
1125
+ expect(planets?.operations).toContain("create");
1126
+ expect(planets?.operations).toContain("read");
1127
+ expect(planets?.operations).toContain("update");
1128
+ expect(planets?.operations).toContain("delete");
1129
+ });
1130
+
1131
+ it("classifies non-CRUD paths correctly", async () => {
1132
+ const spec = await parseSpec(scalarGalaxySpec);
1133
+ const result = detectCrudResources(spec.paths);
1134
+ const nonCrudPaths = result.nonCrudPaths.map((p) => p.path);
1135
+ expect(nonCrudPaths).toContain("/planets/:planetId/image");
1136
+ expect(nonCrudPaths).toContain("/user/signup");
1137
+ expect(nonCrudPaths).toContain("/auth/token");
1138
+ expect(nonCrudPaths).toContain("/me");
1139
+ });
1140
+ });
1141
+
1142
+ describe("stress: scalar-galaxy.yaml — BREAD operations", () => {
1143
+ it("full solar system lifecycle: seed → browse → read → add → edit → delete", async () => {
1144
+ const mock = schmock({ state: {} });
1145
+ mock.pipe(
1146
+ await openapi({
1147
+ spec: scalarGalaxySpec,
1148
+ seed: { planets: solarSystemPlanets },
1149
+ }),
1150
+ );
1151
+
1152
+ // BROWSE — list all 8 planets
1153
+ const allPlanets = await mock.handle("GET", "/planets");
1154
+ expect(allPlanets.status).toBe(200);
1155
+ expect(allPlanets.body).toHaveLength(8);
1156
+
1157
+ // READ — each planet is coherent
1158
+ for (let i = 1; i <= 8; i++) {
1159
+ const planet = await mock.handle("GET", `/planets/${i}`);
1160
+ expect(planet.status).toBe(200);
1161
+ const body = planet.body as Record<string, unknown>;
1162
+ expect(body.name).toBe(solarSystemPlanets[i - 1].name);
1163
+ expect(body.type).toBe(solarSystemPlanets[i - 1].type);
1164
+ }
1165
+
1166
+ // READ specific — Earth is habitable
1167
+ const earth = await mock.handle("GET", "/planets/3");
1168
+ const earthBody = earth.body as Record<string, unknown>;
1169
+ expect(earthBody.name).toBe("Earth");
1170
+ expect(earthBody.habitabilityIndex).toBe(1.0);
1171
+ expect(earthBody.type).toBe("terrestrial");
1172
+
1173
+ // ADD — discover a new planet
1174
+ const created = await mock.handle("POST", "/planets", {
1175
+ body: {
1176
+ name: "Kepler-442b",
1177
+ type: "super_earth",
1178
+ habitabilityIndex: 0.84,
1179
+ physicalProperties: { mass: 2.36, radius: 1.34, gravity: 1.31 },
1180
+ atmosphere: [{ compound: "N2", percentage: 70.0 }],
1181
+ tags: ["exoplanet", "habitable-zone"],
1182
+ },
1183
+ });
1184
+ expect(created.status).toBe(201);
1185
+ const newPlanet = created.body as Record<string, unknown>;
1186
+ expect(newPlanet.name).toBe("Kepler-442b");
1187
+ expect(newPlanet.planetId).toBe(9); // auto-incremented past seed max (8)
1188
+
1189
+ // BROWSE after ADD — 9 planets
1190
+ const afterAdd = await mock.handle("GET", "/planets");
1191
+ expect(afterAdd.body).toHaveLength(9);
1192
+
1193
+ // EDIT — update Mars terraforming progress
1194
+ const edited = await mock.handle("PUT", "/planets/4", {
1195
+ body: { habitabilityIndex: 0.75, tags: ["solar-system", "terraformed"] },
1196
+ });
1197
+ expect(edited.status).toBe(200);
1198
+ const marsUpdated = edited.body as Record<string, unknown>;
1199
+ expect(marsUpdated.name).toBe("Mars"); // preserved
1200
+ expect(marsUpdated.habitabilityIndex).toBe(0.75); // updated
1201
+
1202
+ // DELETE — Pluto was never a planet anyway
1203
+ const deleted = await mock.handle("DELETE", "/planets/9");
1204
+ expect(deleted.status).toBe(204);
1205
+
1206
+ // BROWSE after DELETE — back to 8
1207
+ const afterDelete = await mock.handle("GET", "/planets");
1208
+ expect(afterDelete.body).toHaveLength(8);
1209
+
1210
+ // READ deleted — 404
1211
+ const gone = await mock.handle("GET", "/planets/9");
1212
+ expect(gone.status).toBe(404);
1213
+ });
1214
+
1215
+ it("PATCH works for planet updates too", async () => {
1216
+ const mock = schmock({ state: {} });
1217
+ mock.pipe(
1218
+ await openapi({
1219
+ spec: scalarGalaxySpec,
1220
+ seed: {
1221
+ planets: [{ planetId: 1, name: "Mars", type: "terrestrial" }],
1222
+ },
1223
+ }),
1224
+ );
1225
+
1226
+ const patched = await mock.handle("PATCH", "/planets/1", {
1227
+ body: { habitabilityIndex: 0.42 },
1228
+ });
1229
+ expect(patched.status).toBe(200);
1230
+ const body = patched.body as Record<string, unknown>;
1231
+ expect(body.name).toBe("Mars");
1232
+ expect(body.habitabilityIndex).toBe(0.42);
1233
+ });
1234
+
1235
+ it("solar system physical properties are preserved through CRUD", async () => {
1236
+ const mock = schmock({ state: {} });
1237
+ mock.pipe(
1238
+ await openapi({
1239
+ spec: scalarGalaxySpec,
1240
+ seed: { planets: solarSystemPlanets },
1241
+ }),
1242
+ );
1243
+
1244
+ // Verify nested physicalProperties survive
1245
+ const jupiter = await mock.handle("GET", "/planets/5");
1246
+ const jupiterBody = jupiter.body as Record<string, unknown>;
1247
+ const physics = jupiterBody.physicalProperties as Record<string, unknown>;
1248
+ expect(physics.mass).toBe(317.8);
1249
+ expect(physics.radius).toBe(11.21);
1250
+ expect(physics.gravity).toBe(2.53);
1251
+
1252
+ // Verify atmosphere arrays survive
1253
+ const earth = await mock.handle("GET", "/planets/3");
1254
+ const earthBody = earth.body as Record<string, unknown>;
1255
+ const atmo = earthBody.atmosphere as Array<Record<string, unknown>>;
1256
+ expect(atmo).toHaveLength(2);
1257
+ expect(atmo[0].compound).toBe("N2");
1258
+ expect(atmo[0].percentage).toBe(78.1);
1259
+ });
1260
+
1261
+ it("non-CRUD endpoints work alongside CRUD", async () => {
1262
+ const mock = schmock({ state: {} });
1263
+ mock.pipe(
1264
+ await openapi({
1265
+ spec: scalarGalaxySpec,
1266
+ seed: { planets: solarSystemPlanets },
1267
+ }),
1268
+ );
1269
+
1270
+ // Auth endpoints — non-CRUD static
1271
+ const signup = await mock.handle("POST", "/user/signup", {
1272
+ body: {
1273
+ name: "Astronaut",
1274
+ email: "astro@galaxy.com",
1275
+ password: "s3cr3t",
1276
+ },
1277
+ });
1278
+ expect(signup.status).toBe(200);
1279
+
1280
+ const token = await mock.handle("POST", "/auth/token", {
1281
+ body: { email: "astro@galaxy.com", password: "s3cr3t" },
1282
+ });
1283
+ expect(token.status).toBe(200);
1284
+
1285
+ const me = await mock.handle("GET", "/me");
1286
+ expect(me.status).toBe(200);
1287
+
1288
+ // Image upload endpoint — non-CRUD static
1289
+ const image = await mock.handle("POST", "/planets/3/image");
1290
+ expect(image.status).toBe(200);
1291
+
1292
+ // Celestial bodies — non-CRUD (no GET collection)
1293
+ const celestial = await mock.handle("POST", "/celestial-bodies", {
1294
+ body: { name: "Phobos", type: "moon" },
1295
+ });
1296
+ expect(celestial.status).toBe(200);
1297
+ });
1298
+
1299
+ it("gas giants vs terrestrial planets maintain type integrity", async () => {
1300
+ const mock = schmock({ state: {} });
1301
+ mock.pipe(
1302
+ await openapi({
1303
+ spec: scalarGalaxySpec,
1304
+ seed: { planets: solarSystemPlanets },
1305
+ }),
1306
+ );
1307
+
1308
+ // Filter by reading each and checking type
1309
+ const types: Record<string, string[]> = {};
1310
+ for (let i = 1; i <= 8; i++) {
1311
+ const res = await mock.handle("GET", `/planets/${i}`);
1312
+ const body = res.body as Record<string, unknown>;
1313
+ const type = body.type as string;
1314
+ if (!types[type]) types[type] = [];
1315
+ types[type].push(body.name as string);
1316
+ }
1317
+
1318
+ expect(types.terrestrial).toEqual(["Mercury", "Venus", "Earth", "Mars"]);
1319
+ expect(types.gas_giant).toEqual(["Jupiter", "Saturn"]);
1320
+ expect(types.ice_giant).toEqual(["Uranus", "Neptune"]);
1321
+ });
1322
+
1323
+ it("mass ordering: Jupiter > Saturn > Neptune > Uranus > Earth > Venus > Mars > Mercury", async () => {
1324
+ const mock = schmock({ state: {} });
1325
+ mock.pipe(
1326
+ await openapi({
1327
+ spec: scalarGalaxySpec,
1328
+ seed: { planets: solarSystemPlanets },
1329
+ }),
1330
+ );
1331
+
1332
+ const masses: Array<{ name: string; mass: number }> = [];
1333
+ for (let i = 1; i <= 8; i++) {
1334
+ const res = await mock.handle("GET", `/planets/${i}`);
1335
+ const body = res.body as Record<string, unknown>;
1336
+ const physics = body.physicalProperties as Record<string, unknown>;
1337
+ masses.push({
1338
+ name: body.name as string,
1339
+ mass: physics.mass as number,
1340
+ });
1341
+ }
1342
+
1343
+ const sorted = [...masses].sort((a, b) => b.mass - a.mass);
1344
+ expect(sorted.map((p) => p.name)).toEqual([
1345
+ "Jupiter",
1346
+ "Saturn",
1347
+ "Neptune",
1348
+ "Uranus",
1349
+ "Earth",
1350
+ "Venus",
1351
+ "Mars",
1352
+ "Mercury",
1353
+ ]);
1354
+ });
1355
+ });
1356
+
1357
+ // ════════════════════════════════════════════════════════════════════
1358
+ // 11. THE CONFUSED DEVELOPER — doing things wrong or out-of-order
1359
+ // ════════════════════════════════════════════════════════════════════
1360
+ describe("stress: confused developer flows", () => {
1361
+ it("reads before any creates — empty collection, not crash", async () => {
1362
+ const mock = schmock({ state: {} });
1363
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1364
+
1365
+ const read = await mock.handle("GET", "/pets/42");
1366
+ expect(read.status).toBe(404);
1367
+
1368
+ const list = await mock.handle("GET", "/pets");
1369
+ expect(list.status).toBe(200);
1370
+ expect(list.body).toEqual([]);
1371
+ });
1372
+
1373
+ it("deletes from empty collection — 404, not crash", async () => {
1374
+ const mock = schmock({ state: {} });
1375
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1376
+
1377
+ const res = await mock.handle("DELETE", "/pets/1");
1378
+ expect(res.status).toBe(404);
1379
+ });
1380
+
1381
+ it("updates non-existent item — 404", async () => {
1382
+ const mock = schmock({ state: {} });
1383
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1384
+
1385
+ const res = await mock.handle("PUT", "/pets/999", {
1386
+ body: { name: "Ghost" },
1387
+ });
1388
+ expect(res.status).toBe(404);
1389
+ });
1390
+
1391
+ it("creates then reads with wrong ID format (string vs number)", async () => {
1392
+ const mock = schmock({ state: {} });
1393
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1394
+
1395
+ await mock.handle("POST", "/pets", { body: { name: "Buddy" } });
1396
+ // Path params are always strings — "1" should match item with petId: 1
1397
+ const res = await mock.handle("GET", "/pets/1");
1398
+ expect(res.status).toBe(200);
1399
+ });
1400
+
1401
+ it("sends completely irrelevant body fields — they get stored anyway", async () => {
1402
+ const mock = schmock({ state: {} });
1403
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1404
+
1405
+ const created = await mock.handle("POST", "/pets", {
1406
+ body: {
1407
+ name: "Buddy",
1408
+ favoriteFood: "bacon",
1409
+ socialSecurityNumber: "nope",
1410
+ nestedGarbage: { deeply: { nested: { stuff: true } } },
1411
+ },
1412
+ });
1413
+ expect(created.status).toBe(201);
1414
+ const body = created.body as Record<string, unknown>;
1415
+ expect(body.favoriteFood).toBe("bacon");
1416
+ expect(body.nestedGarbage).toEqual({
1417
+ deeply: { nested: { stuff: true } },
1418
+ });
1419
+ });
1420
+
1421
+ it("creates with body that tries to set the ID — gets overwritten", async () => {
1422
+ const mock = schmock({ state: {} });
1423
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1424
+
1425
+ const res = await mock.handle("POST", "/pets", {
1426
+ body: { petId: 42, name: "Impostor" },
1427
+ });
1428
+ const body = res.body as Record<string, unknown>;
1429
+ // Auto-increment wins over user-supplied ID
1430
+ expect(body.petId).toBe(1);
1431
+ });
1432
+
1433
+ it("creates with array body instead of object", async () => {
1434
+ const mock = schmock({ state: {} });
1435
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1436
+
1437
+ const res = await mock.handle("POST", "/pets", {
1438
+ body: [1, 2, 3],
1439
+ });
1440
+ // Array body isn't a record — should create item with just the ID
1441
+ expect(res.status).toBe(201);
1442
+ const body = res.body as Record<string, unknown>;
1443
+ expect(body.petId).toBe(1);
1444
+ });
1445
+
1446
+ it("creates with string body instead of object", async () => {
1447
+ const mock = schmock({ state: {} });
1448
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1449
+
1450
+ const res = await mock.handle("POST", "/pets", {
1451
+ body: "not an object",
1452
+ });
1453
+ expect(res.status).toBe(201);
1454
+ const body = res.body as Record<string, unknown>;
1455
+ expect(body.petId).toBe(1);
1456
+ });
1457
+
1458
+ it("reads with path param that has special characters", async () => {
1459
+ const mock = schmock({ state: {} });
1460
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1461
+
1462
+ const res = await mock.handle("GET", "/pets/hello%20world");
1463
+ expect(res.status).toBe(404);
1464
+ });
1465
+
1466
+ it("PATCHes a deleted item — still 404", async () => {
1467
+ const mock = schmock({ state: {} });
1468
+ mock.pipe(
1469
+ await openapi({
1470
+ spec: scalarGalaxySpec,
1471
+ seed: {
1472
+ planets: [{ planetId: 1, name: "Pluto", type: "dwarf" }],
1473
+ },
1474
+ }),
1475
+ );
1476
+
1477
+ await mock.handle("DELETE", "/planets/1");
1478
+ const res = await mock.handle("PATCH", "/planets/1", {
1479
+ body: { name: "Not Pluto" },
1480
+ });
1481
+ expect(res.status).toBe(404);
1482
+ });
1483
+ });
1484
+
1485
+ // ════════════════════════════════════════════════════════════════════
1486
+ // 12. THE LIFECYCLE DEVELOPER — reset, rebuild, multi-instance
1487
+ // ════════════════════════════════════════════════════════════════════
1488
+ describe("stress: lifecycle and multi-instance flows", () => {
1489
+ it("resetState then re-seed through requests", async () => {
1490
+ const mock = schmock({ state: {} });
1491
+ mock.pipe(
1492
+ await openapi({
1493
+ spec: scalarGalaxySpec,
1494
+ seed: { planets: solarSystemPlanets },
1495
+ }),
1496
+ );
1497
+
1498
+ expect((await mock.handle("GET", "/planets")).body).toHaveLength(8);
1499
+
1500
+ mock.resetState();
1501
+
1502
+ // After reset, collection is empty — seeder runs again on next request
1503
+ const list = await mock.handle("GET", "/planets");
1504
+ expect(list.body).toHaveLength(8); // re-seeded on access
1505
+
1506
+ // Can still create new items
1507
+ const created = await mock.handle("POST", "/planets", {
1508
+ body: { name: "Planet X" },
1509
+ });
1510
+ expect(created.status).toBe(201);
1511
+ expect((created.body as Record<string, unknown>).planetId).toBe(9);
1512
+ });
1513
+
1514
+ it("two independent mock instances with the same spec", async () => {
1515
+ const mockA = schmock({ state: {} });
1516
+ const mockB = schmock({ state: {} });
1517
+
1518
+ const plugin = await openapi({
1519
+ spec: `${fixturesDir}/petstore-swagger2.json`,
1520
+ });
1521
+ // Each gets its own plugin instance
1522
+ mockA.pipe(
1523
+ await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }),
1524
+ );
1525
+ mockB.pipe(plugin);
1526
+
1527
+ await mockA.handle("POST", "/pets", { body: { name: "Alpha" } });
1528
+ await mockA.handle("POST", "/pets", { body: { name: "Beta" } });
1529
+ await mockB.handle("POST", "/pets", { body: { name: "Gamma" } });
1530
+
1531
+ // Each instance has its own state
1532
+ expect((await mockA.handle("GET", "/pets")).body).toHaveLength(2);
1533
+ expect((await mockB.handle("GET", "/pets")).body).toHaveLength(1);
1534
+ });
1535
+
1536
+ it("resetState then create — IDs restart from 1", async () => {
1537
+ const mock = schmock({ state: {} });
1538
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1539
+
1540
+ await mock.handle("POST", "/pets", { body: { name: "First" } });
1541
+ await mock.handle("POST", "/pets", { body: { name: "Second" } });
1542
+ expect(
1543
+ ((await mock.handle("GET", "/pets/2")).body as Record<string, unknown>)
1544
+ .name,
1545
+ ).toBe("Second");
1546
+
1547
+ mock.resetState();
1548
+
1549
+ // After reset, new creates start from ID 1 again
1550
+ const created = await mock.handle("POST", "/pets", {
1551
+ body: { name: "Reborn" },
1552
+ });
1553
+ expect((created.body as Record<string, unknown>).petId).toBe(1);
1554
+ });
1555
+
1556
+ it("mix seed and runtime data in Scalar Galaxy", async () => {
1557
+ const mock = schmock({ state: {} });
1558
+ mock.pipe(
1559
+ await openapi({
1560
+ spec: scalarGalaxySpec,
1561
+ seed: {
1562
+ planets: [
1563
+ { planetId: 1, name: "Earth", type: "terrestrial" },
1564
+ { planetId: 2, name: "Mars", type: "terrestrial" },
1565
+ ],
1566
+ },
1567
+ }),
1568
+ );
1569
+
1570
+ // Runtime-created planets get IDs continuing from seed max
1571
+ await mock.handle("POST", "/planets", { body: { name: "Exo-1" } });
1572
+ await mock.handle("POST", "/planets", { body: { name: "Exo-2" } });
1573
+
1574
+ const list = await mock.handle("GET", "/planets");
1575
+ const items = list.body as Record<string, unknown>[];
1576
+ expect(items).toHaveLength(4);
1577
+
1578
+ // Seed items at IDs 1-2, runtime items at IDs 3-4
1579
+ expect(items[2].planetId).toBe(3);
1580
+ expect(items[3].planetId).toBe(4);
1581
+ expect(items[2].name).toBe("Exo-1");
1582
+ });
1583
+
1584
+ it("cross-spec: petstore + galaxy on same instance", async () => {
1585
+ const mock = schmock({ state: {} });
1586
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1587
+ mock.pipe(
1588
+ await openapi({
1589
+ spec: scalarGalaxySpec,
1590
+ seed: { planets: solarSystemPlanets },
1591
+ }),
1592
+ );
1593
+
1594
+ // Petstore CRUD
1595
+ await mock.handle("POST", "/pets", { body: { name: "Buddy" } });
1596
+ expect((await mock.handle("GET", "/pets")).body).toHaveLength(1);
1597
+
1598
+ // Galaxy CRUD — completely independent
1599
+ expect((await mock.handle("GET", "/planets")).body).toHaveLength(8);
1600
+ await mock.handle("POST", "/planets", { body: { name: "Nibiru" } });
1601
+ expect((await mock.handle("GET", "/planets")).body).toHaveLength(9);
1602
+
1603
+ // Petstore still has 1
1604
+ expect((await mock.handle("GET", "/pets")).body).toHaveLength(1);
1605
+ });
1606
+ });
1607
+
1608
+ // ════════════════════════════════════════════════════════════════════
1609
+ // 13. THE CHAOS MONKEY — rapid fire, edge IDs, bulk operations
1610
+ // ════════════════════════════════════════════════════════════════════
1611
+ describe("stress: chaos monkey flows", () => {
1612
+ it("create-delete-create-read cycle — IDs keep incrementing", async () => {
1613
+ const mock = schmock({ state: {} });
1614
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1615
+
1616
+ // Create 3
1617
+ for (let i = 0; i < 3; i++) {
1618
+ await mock.handle("POST", "/pets", { body: { name: `V1-${i}` } });
1619
+ }
1620
+ // Delete all
1621
+ for (let i = 1; i <= 3; i++) {
1622
+ await mock.handle("DELETE", `/pets/${i}`);
1623
+ }
1624
+ // Create 3 more — IDs should be 4, 5, 6 (not 1, 2, 3)
1625
+ for (let i = 0; i < 3; i++) {
1626
+ const res = await mock.handle("POST", "/pets", {
1627
+ body: { name: `V2-${i}` },
1628
+ });
1629
+ expect((res.body as Record<string, unknown>).petId).toBe(i + 4);
1630
+ }
1631
+
1632
+ const list = await mock.handle("GET", "/pets");
1633
+ expect(list.body).toHaveLength(3);
1634
+ });
1635
+
1636
+ it("update every field on every planet", async () => {
1637
+ const mock = schmock({ state: {} });
1638
+ mock.pipe(
1639
+ await openapi({
1640
+ spec: scalarGalaxySpec,
1641
+ seed: { planets: solarSystemPlanets },
1642
+ }),
1643
+ );
1644
+
1645
+ // Mass update — change every planet's name
1646
+ for (let i = 1; i <= 8; i++) {
1647
+ await mock.handle("PUT", `/planets/${i}`, {
1648
+ body: { name: `Renamed-${i}` },
1649
+ });
1650
+ }
1651
+
1652
+ // Verify all renames stuck
1653
+ for (let i = 1; i <= 8; i++) {
1654
+ const res = await mock.handle("GET", `/planets/${i}`);
1655
+ expect((res.body as Record<string, unknown>).name).toBe(`Renamed-${i}`);
1656
+ }
1657
+ });
1658
+
1659
+ it("interleaved create and delete — Swiss cheese collection", async () => {
1660
+ const mock = schmock({ state: {} });
1661
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1662
+
1663
+ // Create 20 items, delete every 3rd one
1664
+ for (let i = 0; i < 20; i++) {
1665
+ await mock.handle("POST", "/pets", { body: { name: `Pet-${i}` } });
1666
+ if ((i + 1) % 3 === 0) {
1667
+ await mock.handle("DELETE", `/pets/${i + 1}`);
1668
+ }
1669
+ }
1670
+
1671
+ const list = await mock.handle("GET", "/pets");
1672
+ const items = list.body as Record<string, unknown>[];
1673
+ // Deleted items: 3, 6, 9, 12, 15, 18 → 6 deleted, 14 remain
1674
+ expect(items).toHaveLength(14);
1675
+
1676
+ // Verify the deleted ones are really gone
1677
+ for (const deletedId of [3, 6, 9, 12, 15, 18]) {
1678
+ const res = await mock.handle("GET", `/pets/${deletedId}`);
1679
+ expect(res.status).toBe(404);
1680
+ }
1681
+ });
1682
+
1683
+ it("rapid overwrite — PUT same item 50 times", async () => {
1684
+ const mock = schmock({ state: {} });
1685
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1686
+
1687
+ await mock.handle("POST", "/pets", { body: { name: "Volatile" } });
1688
+
1689
+ for (let i = 0; i < 50; i++) {
1690
+ await mock.handle("PUT", "/pets/1", {
1691
+ body: { name: `Version-${i}`, counter: i },
1692
+ });
1693
+ }
1694
+
1695
+ const final = await mock.handle("GET", "/pets/1");
1696
+ const body = final.body as Record<string, unknown>;
1697
+ expect(body.name).toBe("Version-49");
1698
+ expect(body.counter).toBe(49);
1699
+ expect(body.petId).toBe(1); // ID preserved through 50 updates
1700
+ });
1701
+
1702
+ it("delete middle, read neighbors — no index corruption", async () => {
1703
+ const mock = schmock({ state: {} });
1704
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1705
+
1706
+ for (let i = 0; i < 5; i++) {
1707
+ await mock.handle("POST", "/pets", { body: { name: `Pet-${i}` } });
1708
+ }
1709
+
1710
+ // Delete middle item (ID 3)
1711
+ await mock.handle("DELETE", "/pets/3");
1712
+
1713
+ // Neighbors are still accessible
1714
+ const before = await mock.handle("GET", "/pets/2");
1715
+ expect(before.status).toBe(200);
1716
+ expect((before.body as Record<string, unknown>).name).toBe("Pet-1");
1717
+
1718
+ const after = await mock.handle("GET", "/pets/4");
1719
+ expect(after.status).toBe(200);
1720
+ expect((after.body as Record<string, unknown>).name).toBe("Pet-3");
1721
+
1722
+ // List is still ordered
1723
+ const list = await mock.handle("GET", "/pets");
1724
+ expect(list.body).toHaveLength(4);
1725
+ });
1726
+
1727
+ it("update preserves fields not in the body", async () => {
1728
+ const mock = schmock({ state: {} });
1729
+ mock.pipe(
1730
+ await openapi({
1731
+ spec: scalarGalaxySpec,
1732
+ seed: {
1733
+ planets: [
1734
+ {
1735
+ planetId: 1,
1736
+ name: "Earth",
1737
+ type: "terrestrial",
1738
+ habitabilityIndex: 1.0,
1739
+ physicalProperties: { mass: 1.0, radius: 1.0 },
1740
+ atmosphere: [{ compound: "N2", percentage: 78.1 }],
1741
+ },
1742
+ ],
1743
+ },
1744
+ }),
1745
+ );
1746
+
1747
+ // Only update one field
1748
+ await mock.handle("PATCH", "/planets/1", {
1749
+ body: { habitabilityIndex: 0.95 },
1750
+ });
1751
+
1752
+ const res = await mock.handle("GET", "/planets/1");
1753
+ const body = res.body as Record<string, unknown>;
1754
+ // Updated field changed
1755
+ expect(body.habitabilityIndex).toBe(0.95);
1756
+ // Other fields preserved
1757
+ expect(body.name).toBe("Earth");
1758
+ expect(body.type).toBe("terrestrial");
1759
+ expect(body.physicalProperties).toEqual({ mass: 1.0, radius: 1.0 });
1760
+ expect(body.atmosphere).toEqual([{ compound: "N2", percentage: 78.1 }]);
1761
+ });
1762
+ });
1763
+
1764
+ // ════════════════════════════════════════════════════════════════════
1765
+ // 14. THE INTEGRATION TESTER — realistic multi-spec E2E flows
1766
+ // ════════════════════════════════════════════════════════════════════
1767
+ describe("stress: realistic E2E flows", () => {
1768
+ it("train travel: book → pay → cancel flow", async () => {
1769
+ const mock = schmock({ state: {} });
1770
+ mock.pipe(await openapi({ spec: trainTravelSpec }));
1771
+
1772
+ // 1. Search stations (static)
1773
+ const stations = await mock.handle("GET", "/stations");
1774
+ expect(stations.status).toBe(200);
1775
+
1776
+ // 2. Search trips (static)
1777
+ const trips = await mock.handle("GET", "/trips");
1778
+ expect(trips.status).toBe(200);
1779
+
1780
+ // 3. Book a trip
1781
+ const booking = await mock.handle("POST", "/bookings", {
1782
+ body: {
1783
+ trip_id: "trip-123",
1784
+ passenger_name: "Jane Doe",
1785
+ has_bicycle: false,
1786
+ has_dog: true,
1787
+ },
1788
+ });
1789
+ expect(booking.status).toBe(201);
1790
+ const bookingBody = booking.body as Record<string, unknown>;
1791
+ const bookingId = bookingBody.bookingId;
1792
+
1793
+ // 4. Read the booking back
1794
+ const readBooking = await mock.handle("GET", `/bookings/${bookingId}`);
1795
+ expect(readBooking.status).toBe(200);
1796
+ expect((readBooking.body as Record<string, unknown>).passenger_name).toBe(
1797
+ "Jane Doe",
1798
+ );
1799
+
1800
+ // 5. Make payment (non-CRUD static endpoint)
1801
+ const payment = await mock.handle(
1802
+ "POST",
1803
+ `/bookings/${bookingId}/payment`,
1804
+ {
1805
+ body: {
1806
+ amount: 49.99,
1807
+ currency: "gbp",
1808
+ source: { object: "card", name: "J. Doe" },
1809
+ },
1810
+ },
1811
+ );
1812
+ expect(payment.status).toBe(200);
1813
+
1814
+ // 6. Cancel booking
1815
+ const cancel = await mock.handle("DELETE", `/bookings/${bookingId}`);
1816
+ expect(cancel.status).toBe(204);
1817
+
1818
+ // 7. Verify cancellation
1819
+ const gone = await mock.handle("GET", `/bookings/${bookingId}`);
1820
+ expect(gone.status).toBe(404);
1821
+ });
1822
+
1823
+ it("galaxy: signup → create planet → upload image → list", async () => {
1824
+ const mock = schmock({ state: {} });
1825
+ mock.pipe(await openapi({ spec: scalarGalaxySpec }));
1826
+
1827
+ // 1. Signup
1828
+ const signup = await mock.handle("POST", "/user/signup", {
1829
+ body: {
1830
+ name: "Explorer",
1831
+ email: "explorer@galaxy.com",
1832
+ password: "stars123",
1833
+ },
1834
+ });
1835
+ expect(signup.status).toBe(200);
1836
+
1837
+ // 2. Get token
1838
+ const token = await mock.handle("POST", "/auth/token", {
1839
+ body: { email: "explorer@galaxy.com", password: "stars123" },
1840
+ });
1841
+ expect(token.status).toBe(200);
1842
+
1843
+ // 3. Create a planet
1844
+ const created = await mock.handle("POST", "/planets", {
1845
+ body: { name: "Explorer-1", type: "super_earth" },
1846
+ });
1847
+ expect(created.status).toBe(201);
1848
+ const planet = created.body as Record<string, unknown>;
1849
+
1850
+ // 4. Upload an image
1851
+ const image = await mock.handle(
1852
+ "POST",
1853
+ `/planets/${planet.planetId}/image`,
1854
+ );
1855
+ expect(image.status).toBe(200);
1856
+
1857
+ // 5. Get user profile
1858
+ const me = await mock.handle("GET", "/me");
1859
+ expect(me.status).toBe(200);
1860
+
1861
+ // 6. List planets
1862
+ const list = await mock.handle("GET", "/planets");
1863
+ expect(list.body).toHaveLength(1);
1864
+ });
1865
+
1866
+ it("galaxy: populate solar system then explore", async () => {
1867
+ const mock = schmock({ state: {} });
1868
+ mock.pipe(await openapi({ spec: scalarGalaxySpec }));
1869
+
1870
+ // Manually add all 8 planets via POST
1871
+ const planetNames = [
1872
+ "Mercury",
1873
+ "Venus",
1874
+ "Earth",
1875
+ "Mars",
1876
+ "Jupiter",
1877
+ "Saturn",
1878
+ "Uranus",
1879
+ "Neptune",
1880
+ ];
1881
+ for (const name of planetNames) {
1882
+ const res = await mock.handle("POST", "/planets", {
1883
+ body: { name },
1884
+ });
1885
+ expect(res.status).toBe(201);
1886
+ }
1887
+
1888
+ // Verify count
1889
+ const list = await mock.handle("GET", "/planets");
1890
+ expect(list.body).toHaveLength(8);
1891
+
1892
+ // Read each by ID
1893
+ for (let i = 1; i <= 8; i++) {
1894
+ const res = await mock.handle("GET", `/planets/${i}`);
1895
+ expect(res.status).toBe(200);
1896
+ expect((res.body as Record<string, unknown>).name).toBe(
1897
+ planetNames[i - 1],
1898
+ );
1899
+ }
1900
+ });
1901
+
1902
+ it("petstore: multiple spec versions coexist", async () => {
1903
+ const mock = schmock({ state: {} });
1904
+ // Load Swagger 2.0 petstore
1905
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1906
+ // Load OpenAPI 3.0 petstore (different fixture)
1907
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-openapi3.json` }));
1908
+
1909
+ // Both specs define /pets — last one wins for route registration
1910
+ const created = await mock.handle("POST", "/pets", {
1911
+ body: { name: "Multi-version" },
1912
+ });
1913
+ expect(created.status).toBe(201);
1914
+ });
1915
+ });
1916
+
1917
+ // ════════════════════════════════════════════════════════════════════
1918
+ // 15. THE EDGE HUNTER — boundary conditions, weird inputs
1919
+ // ════════════════════════════════════════════════════════════════════
1920
+ describe("stress: boundary conditions", () => {
1921
+ it("create with empty object body", async () => {
1922
+ const mock = schmock({ state: {} });
1923
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1924
+
1925
+ const res = await mock.handle("POST", "/pets", { body: {} });
1926
+ expect(res.status).toBe(201);
1927
+ const body = res.body as Record<string, unknown>;
1928
+ expect(body.petId).toBe(1);
1929
+ });
1930
+
1931
+ it("update with body containing nested nulls", async () => {
1932
+ const mock = schmock({ state: {} });
1933
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
1934
+
1935
+ await mock.handle("POST", "/pets", { body: { name: "Buddy" } });
1936
+ const updated = await mock.handle("PUT", "/pets/1", {
1937
+ body: { name: null, extra: undefined },
1938
+ });
1939
+ expect(updated.status).toBe(200);
1940
+ const body = updated.body as Record<string, unknown>;
1941
+ expect(body.name).toBeNull();
1942
+ });
1943
+
1944
+ it("seed with zero items — collection starts empty", async () => {
1945
+ const mock = schmock({ state: {} });
1946
+ mock.pipe(
1947
+ await openapi({
1948
+ spec: `${fixturesDir}/petstore-swagger2.json`,
1949
+ seed: { pets: [] },
1950
+ }),
1951
+ );
1952
+
1953
+ const list = await mock.handle("GET", "/pets");
1954
+ expect(list.body).toEqual([]);
1955
+
1956
+ // Creating after empty seed still works
1957
+ const created = await mock.handle("POST", "/pets", {
1958
+ body: { name: "First" },
1959
+ });
1960
+ expect((created.body as Record<string, unknown>).petId).toBe(1);
1961
+ });
1962
+
1963
+ it("seed for non-existent resource is silently ignored", async () => {
1964
+ const mock = schmock({ state: {} });
1965
+ mock.pipe(
1966
+ await openapi({
1967
+ spec: `${fixturesDir}/petstore-swagger2.json`,
1968
+ seed: {
1969
+ pets: [{ petId: 1, name: "Real" }],
1970
+ unicorns: [{ unicornId: 1, name: "Sparkle" }],
1971
+ },
1972
+ }),
1973
+ );
1974
+
1975
+ // pets work fine
1976
+ expect((await mock.handle("GET", "/pets")).body).toHaveLength(1);
1977
+ // unicorns route doesn't exist
1978
+ const res = await mock.handle("GET", "/unicorns");
1979
+ expect(res.status).toBe(404);
1980
+ });
1981
+
1982
+ it("item with deeply nested data survives update cycle", async () => {
1983
+ const mock = schmock({ state: {} });
1984
+ mock.pipe(
1985
+ await openapi({
1986
+ spec: scalarGalaxySpec,
1987
+ seed: {
1988
+ planets: [
1989
+ {
1990
+ planetId: 1,
1991
+ name: "Deep",
1992
+ physicalProperties: {
1993
+ mass: 1.0,
1994
+ temperature: { min: 100, max: 400, average: 250 },
1995
+ },
1996
+ atmosphere: [
1997
+ { compound: "O2", percentage: 21 },
1998
+ { compound: "N2", percentage: 78 },
1999
+ { compound: "Ar", percentage: 0.9 },
2000
+ ],
2001
+ },
2002
+ ],
2003
+ },
2004
+ }),
2005
+ );
2006
+
2007
+ // Update only top-level field
2008
+ await mock.handle("PATCH", "/planets/1", { body: { name: "Deeper" } });
2009
+
2010
+ const res = await mock.handle("GET", "/planets/1");
2011
+ const body = res.body as Record<string, unknown>;
2012
+ expect(body.name).toBe("Deeper");
2013
+ const props = body.physicalProperties as Record<string, unknown>;
2014
+ const temp = props.temperature as Record<string, unknown>;
2015
+ expect(temp.min).toBe(100);
2016
+ expect(temp.max).toBe(400);
2017
+ const atmo = body.atmosphere as Array<Record<string, unknown>>;
2018
+ expect(atmo).toHaveLength(3);
2019
+ });
2020
+
2021
+ it("large ID values work correctly", async () => {
2022
+ const mock = schmock({ state: {} });
2023
+ mock.pipe(
2024
+ await openapi({
2025
+ spec: `${fixturesDir}/petstore-swagger2.json`,
2026
+ seed: { pets: [{ petId: 999999, name: "Big" }] },
2027
+ }),
2028
+ );
2029
+
2030
+ const read = await mock.handle("GET", "/pets/999999");
2031
+ expect(read.status).toBe(200);
2032
+
2033
+ const created = await mock.handle("POST", "/pets", {
2034
+ body: { name: "After Big" },
2035
+ });
2036
+ expect((created.body as Record<string, unknown>).petId).toBe(1000000);
2037
+ });
2038
+
2039
+ it("UUID-style string IDs", async () => {
2040
+ const mock = schmock({ state: {} });
2041
+ mock.pipe(
2042
+ await openapi({
2043
+ spec: `${fixturesDir}/petstore-swagger2.json`,
2044
+ seed: {
2045
+ pets: [
2046
+ { petId: "550e8400-e29b-41d4-a716-446655440000", name: "UUID-Pet" },
2047
+ ],
2048
+ },
2049
+ }),
2050
+ );
2051
+
2052
+ const res = await mock.handle(
2053
+ "GET",
2054
+ "/pets/550e8400-e29b-41d4-a716-446655440000",
2055
+ );
2056
+ expect(res.status).toBe(200);
2057
+ expect((res.body as Record<string, unknown>).name).toBe("UUID-Pet");
2058
+ });
2059
+
2060
+ it("concurrent-like rapid operations don't corrupt state", async () => {
2061
+ const mock = schmock({ state: {} });
2062
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
2063
+
2064
+ // Fire off many operations in parallel
2065
+ const ops: Promise<Schmock.ResponseResult>[] = [];
2066
+ for (let i = 0; i < 20; i++) {
2067
+ ops.push(
2068
+ mock.handle("POST", "/pets", { body: { name: `Parallel-${i}` } }),
2069
+ );
2070
+ }
2071
+ const results = await Promise.all(ops);
2072
+
2073
+ // All should succeed
2074
+ for (const res of results) {
2075
+ expect(res.status).toBe(201);
2076
+ }
2077
+
2078
+ // All should have unique IDs
2079
+ const ids = results.map((r) => (r.body as Record<string, unknown>).petId);
2080
+ const uniqueIds = new Set(ids);
2081
+ expect(uniqueIds.size).toBe(20);
2082
+
2083
+ // List should show all 20
2084
+ const list = await mock.handle("GET", "/pets");
2085
+ expect(list.body).toHaveLength(20);
2086
+ });
2087
+
2088
+ it("handles path with trailing slash gracefully", async () => {
2089
+ const mock = schmock({ state: {} });
2090
+ mock.pipe(await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }));
2091
+
2092
+ await mock.handle("POST", "/pets", { body: { name: "Buddy" } });
2093
+
2094
+ // Trailing slash should be handled by schmock's route matching
2095
+ const res = await mock.handle("GET", "/pets/1");
2096
+ expect(res.status).toBe(200);
2097
+ });
2098
+ });
2099
+
2100
+ // ════════════════════════════════════════════════════════════════════
2101
+ // 16. THE SPEC EXPLORER — inline specs with weird shapes
2102
+ // ════════════════════════════════════════════════════════════════════
2103
+ describe("stress: weird inline specs", () => {
2104
+ it("spec with only DELETE endpoints — still CRUD if has collection GET", async () => {
2105
+ const mock = schmock({ state: {} });
2106
+ mock.pipe(
2107
+ await openapi({
2108
+ spec: {
2109
+ openapi: "3.0.3",
2110
+ info: { title: "DeleteHeavy", version: "1.0.0" },
2111
+ paths: {
2112
+ "/items": {
2113
+ get: {
2114
+ responses: {
2115
+ "200": {
2116
+ description: "List",
2117
+ content: {
2118
+ "application/json": {
2119
+ schema: {
2120
+ type: "array",
2121
+ items: { type: "object" },
2122
+ },
2123
+ },
2124
+ },
2125
+ },
2126
+ },
2127
+ },
2128
+ post: {
2129
+ responses: { "201": { description: "Created" } },
2130
+ },
2131
+ },
2132
+ "/items/{itemId}": {
2133
+ delete: {
2134
+ parameters: [{ name: "itemId", in: "path", required: true }],
2135
+ responses: { "204": { description: "Deleted" } },
2136
+ },
2137
+ },
2138
+ },
2139
+ },
2140
+ }),
2141
+ );
2142
+
2143
+ await mock.handle("POST", "/items", { body: { x: 1 } });
2144
+ await mock.handle("POST", "/items", { body: { x: 2 } });
2145
+ expect((await mock.handle("GET", "/items")).body).toHaveLength(2);
2146
+
2147
+ await mock.handle("DELETE", "/items/1");
2148
+ expect((await mock.handle("GET", "/items")).body).toHaveLength(1);
2149
+ });
2150
+
2151
+ it("spec with 10 unrelated static endpoints", async () => {
2152
+ const paths: Record<string, Record<string, unknown>> = {};
2153
+ for (let i = 0; i < 10; i++) {
2154
+ paths[`/endpoint-${i}`] = {
2155
+ get: {
2156
+ responses: {
2157
+ "200": {
2158
+ description: `Response ${i}`,
2159
+ content: {
2160
+ "application/json": {
2161
+ schema: {
2162
+ type: "object",
2163
+ properties: { index: { type: "integer", default: i } },
2164
+ },
2165
+ },
2166
+ },
2167
+ },
2168
+ },
2169
+ },
2170
+ };
2171
+ }
2172
+
2173
+ const mock = schmock({ state: {} });
2174
+ mock.pipe(
2175
+ await openapi({
2176
+ spec: {
2177
+ openapi: "3.0.3",
2178
+ info: { title: "Many", version: "1.0.0" },
2179
+ paths,
2180
+ },
2181
+ }),
2182
+ );
2183
+
2184
+ // All 10 endpoints respond
2185
+ for (let i = 0; i < 10; i++) {
2186
+ const res = await mock.handle("GET", `/endpoint-${i}`);
2187
+ expect(res.status).toBe(200);
2188
+ }
2189
+ });
2190
+
2191
+ it("spec with multiple CRUD resources", async () => {
2192
+ const mock = schmock({ state: {} });
2193
+ mock.pipe(
2194
+ await openapi({
2195
+ spec: {
2196
+ openapi: "3.0.3",
2197
+ info: { title: "Multi", version: "1.0.0" },
2198
+ paths: {
2199
+ "/dogs": {
2200
+ get: { responses: { "200": { description: "List" } } },
2201
+ post: { responses: { "201": { description: "Created" } } },
2202
+ },
2203
+ "/dogs/{dogId}": {
2204
+ get: {
2205
+ parameters: [{ name: "dogId", in: "path", required: true }],
2206
+ responses: { "200": { description: "Dog" } },
2207
+ },
2208
+ delete: {
2209
+ parameters: [{ name: "dogId", in: "path", required: true }],
2210
+ responses: { "204": { description: "Deleted" } },
2211
+ },
2212
+ },
2213
+ "/cats": {
2214
+ get: { responses: { "200": { description: "List" } } },
2215
+ post: { responses: { "201": { description: "Created" } } },
2216
+ },
2217
+ "/cats/{catId}": {
2218
+ get: {
2219
+ parameters: [{ name: "catId", in: "path", required: true }],
2220
+ responses: { "200": { description: "Cat" } },
2221
+ },
2222
+ },
2223
+ },
2224
+ },
2225
+ }),
2226
+ );
2227
+
2228
+ // Dogs and cats are independent
2229
+ await mock.handle("POST", "/dogs", { body: { name: "Rex" } });
2230
+ await mock.handle("POST", "/dogs", { body: { name: "Spot" } });
2231
+ await mock.handle("POST", "/cats", { body: { name: "Whiskers" } });
2232
+
2233
+ expect((await mock.handle("GET", "/dogs")).body).toHaveLength(2);
2234
+ expect((await mock.handle("GET", "/cats")).body).toHaveLength(1);
2235
+
2236
+ // Delete a dog doesn't affect cats
2237
+ await mock.handle("DELETE", "/dogs/1");
2238
+ expect((await mock.handle("GET", "/dogs")).body).toHaveLength(1);
2239
+ expect((await mock.handle("GET", "/cats")).body).toHaveLength(1);
2240
+ });
2241
+
2242
+ it("multiple non-CRUD static endpoints with different methods", async () => {
2243
+ const mock = schmock({ state: {} });
2244
+ mock.pipe(
2245
+ await openapi({
2246
+ spec: {
2247
+ openapi: "3.0.3",
2248
+ info: { title: "Methods", version: "1.0.0" },
2249
+ paths: {
2250
+ "/health": {
2251
+ get: {
2252
+ responses: {
2253
+ "200": {
2254
+ description: "OK",
2255
+ content: {
2256
+ "application/json": {
2257
+ schema: {
2258
+ type: "object",
2259
+ properties: { up: { type: "boolean" } },
2260
+ },
2261
+ },
2262
+ },
2263
+ },
2264
+ },
2265
+ },
2266
+ },
2267
+ "/webhook": {
2268
+ post: {
2269
+ responses: { "200": { description: "Received" } },
2270
+ },
2271
+ },
2272
+ "/config": {
2273
+ put: {
2274
+ responses: {
2275
+ "200": {
2276
+ description: "Updated",
2277
+ content: {
2278
+ "application/json": {
2279
+ schema: {
2280
+ type: "object",
2281
+ properties: { ok: { type: "boolean" } },
2282
+ },
2283
+ },
2284
+ },
2285
+ },
2286
+ },
2287
+ },
2288
+ },
2289
+ "/cache": {
2290
+ delete: {
2291
+ responses: { "204": { description: "Cleared" } },
2292
+ },
2293
+ },
2294
+ },
2295
+ },
2296
+ }),
2297
+ );
2298
+
2299
+ expect((await mock.handle("GET", "/health")).status).toBe(200);
2300
+ expect((await mock.handle("POST", "/webhook")).status).toBe(200);
2301
+ expect((await mock.handle("PUT", "/config")).status).toBe(200);
2302
+ expect((await mock.handle("DELETE", "/cache")).status).toBe(200);
2303
+ });
2304
+ });
2305
+
2306
+ // ════════════════════════════════════════════════════════════════════
2307
+ // 17. STRIPE — 5.8MB / 415 paths / the ultimate parser stress test
2308
+ //
2309
+ // The Stripe OpenAPI spec is one of the largest real-world specs:
2310
+ // - 161K lines, 5.8MB YAML
2311
+ // - 415 path operations (137 plain + 278 parameterized)
2312
+ // - 113 potential CRUD resource pairs
2313
+ // - Heavy anyOf usage, x-stripe* extensions, form-urlencoded bodies
2314
+ // - Product item path uses {id} not {product}
2315
+ // - Customer GET returns anyOf: [customer, deleted_customer]
2316
+ //
2317
+ // To avoid OOM from repeated parsing, we parse ONCE and share state.
2318
+ // ════════════════════════════════════════════════════════════════════
2319
+
2320
+ const stripeSpec = resolve(fixturesDir, "stripe-spec3.yaml");
2321
+ const stripeFixtures: Record<string, unknown> = JSON.parse(
2322
+ readFileSync(resolve(fixturesDir, "stripe-fixtures3.json"), "utf8"),
2323
+ );
2324
+ const stripeResources = (
2325
+ stripeFixtures as { resources: Record<string, Record<string, unknown>> }
2326
+ ).resources;
2327
+
2328
+ // Module-level cache — parse the 5.8MB spec exactly once across all test blocks
2329
+ let cachedStripeSpec: ParsedSpec | undefined;
2330
+ async function getStripeSpec(): Promise<ParsedSpec> {
2331
+ if (!cachedStripeSpec) {
2332
+ cachedStripeSpec = await parseSpec(stripeSpec);
2333
+ }
2334
+ return cachedStripeSpec;
2335
+ }
2336
+
2337
+ // Shared plugin — also created once to avoid redundant parsing
2338
+ let cachedStripePlugin: Schmock.Plugin | undefined;
2339
+ async function getStripePlugin(): Promise<Schmock.Plugin> {
2340
+ if (!cachedStripePlugin) {
2341
+ cachedStripePlugin = await openapi({ spec: stripeSpec });
2342
+ }
2343
+ return cachedStripePlugin;
2344
+ }
2345
+
2346
+ describe("stress: stripe spec — parser (5.8MB)", () => {
2347
+ let spec: ParsedSpec;
2348
+
2349
+ beforeAll(async () => {
2350
+ spec = await getStripeSpec();
2351
+ }, 120_000);
2352
+
2353
+ it("parses the massive OpenAPI 3.0 Stripe spec without crashing", () => {
2354
+ expect(spec.title).toBe("Stripe API");
2355
+ expect(spec.version).toBe("2026-01-28.clover");
2356
+ }, 120_000);
2357
+
2358
+ it("extracts basePath from server URL", () => {
2359
+ // servers[0] = "https://api.stripe.com/" → pathname "/" → normalized to ""
2360
+ expect(spec.basePath).toBe("");
2361
+ });
2362
+
2363
+ it("extracts 400+ path operations", () => {
2364
+ expect(spec.paths.length).toBeGreaterThan(400);
2365
+ });
2366
+
2367
+ it("extracts customer CRUD paths", () => {
2368
+ const sigs = spec.paths.map((p) => `${p.method} ${p.path}`);
2369
+ expect(sigs).toContain("GET /v1/customers");
2370
+ expect(sigs).toContain("POST /v1/customers");
2371
+ expect(sigs).toContain("GET /v1/customers/:customer");
2372
+ expect(sigs).toContain("POST /v1/customers/:customer");
2373
+ expect(sigs).toContain("DELETE /v1/customers/:customer");
2374
+ });
2375
+
2376
+ it("extracts product paths (item path uses {id} not {product})", () => {
2377
+ const sigs = spec.paths.map((p) => `${p.method} ${p.path}`);
2378
+ expect(sigs).toContain("GET /v1/products");
2379
+ expect(sigs).toContain("POST /v1/products");
2380
+ // Stripe uses /v1/products/{id} — NOT /v1/products/{product}
2381
+ expect(sigs).toContain("GET /v1/products/:id");
2382
+ expect(sigs).toContain("POST /v1/products/:id");
2383
+ expect(sigs).toContain("DELETE /v1/products/:id");
2384
+ });
2385
+
2386
+ it("extracts coupon CRUD paths", () => {
2387
+ const sigs = spec.paths.map((p) => `${p.method} ${p.path}`);
2388
+ expect(sigs).toContain("GET /v1/coupons");
2389
+ expect(sigs).toContain("POST /v1/coupons");
2390
+ expect(sigs).toContain("GET /v1/coupons/:coupon");
2391
+ expect(sigs).toContain("POST /v1/coupons/:coupon");
2392
+ expect(sigs).toContain("DELETE /v1/coupons/:coupon");
2393
+ });
2394
+
2395
+ it("converts {param} path templates to :param", () => {
2396
+ const paramPaths = spec.paths.filter((p) => p.path.includes(":"));
2397
+ expect(paramPaths.length).toBeGreaterThan(200);
2398
+ // No unreplaced {param} templates
2399
+ const unreplaced = spec.paths.filter((p) => p.path.includes("{"));
2400
+ expect(unreplaced).toHaveLength(0);
2401
+ });
2402
+
2403
+ it("strips x-stripe* extensions from all schemas", () => {
2404
+ let extensionFound = false;
2405
+ for (const p of spec.paths) {
2406
+ if (p.requestBody) {
2407
+ const str = JSON.stringify(p.requestBody);
2408
+ if (str.includes('"x-')) {
2409
+ extensionFound = true;
2410
+ break;
2411
+ }
2412
+ }
2413
+ for (const [, resp] of p.responses) {
2414
+ if (resp.schema) {
2415
+ const str = JSON.stringify(resp.schema);
2416
+ if (str.includes('"x-')) {
2417
+ extensionFound = true;
2418
+ break;
2419
+ }
2420
+ }
2421
+ }
2422
+ if (extensionFound) break;
2423
+ }
2424
+ expect(extensionFound).toBe(false);
2425
+ });
2426
+
2427
+ it("no unresolved $ref in any path", () => {
2428
+ for (const p of spec.paths) {
2429
+ if (p.requestBody) {
2430
+ expect(JSON.stringify(p.requestBody)).not.toContain('"$ref"');
2431
+ }
2432
+ for (const [, resp] of p.responses) {
2433
+ if (resp.schema) {
2434
+ expect(JSON.stringify(resp.schema)).not.toContain('"$ref"');
2435
+ }
2436
+ }
2437
+ }
2438
+ });
2439
+
2440
+ it("handles anyOf response schemas (customer → anyOf [customer, deleted_customer])", () => {
2441
+ // Stripe's GET /v1/customers/{customer} returns anyOf: [customer, deleted_customer]
2442
+ const getCustomer = spec.paths.find(
2443
+ (p) => p.method === "GET" && p.path === "/v1/customers/:customer",
2444
+ );
2445
+ expect(getCustomer).toBeDefined();
2446
+ const schema = getCustomer?.responses.get(200)?.schema;
2447
+ expect(schema).toBeDefined();
2448
+ // Schema is anyOf at the top level — no direct properties
2449
+ expect(schema?.anyOf).toBeDefined();
2450
+ });
2451
+
2452
+ it("handles application/x-www-form-urlencoded request bodies", () => {
2453
+ // Stripe uses form-encoded bodies, not JSON — our fallback should find them
2454
+ const createCustomer = spec.paths.find(
2455
+ (p) => p.method === "POST" && p.path === "/v1/customers",
2456
+ );
2457
+ expect(createCustomer).toBeDefined();
2458
+ // Our findJsonContent falls back to any content type
2459
+ expect(createCustomer?.requestBody).toBeDefined();
2460
+ });
2461
+
2462
+ it("extracts query parameters (limit)", () => {
2463
+ const listCustomers = spec.paths.find(
2464
+ (p) => p.method === "GET" && p.path === "/v1/customers",
2465
+ );
2466
+ const paramNames = listCustomers?.parameters.map((p) => p.name) ?? [];
2467
+ expect(paramNames).toContain("limit");
2468
+ });
2469
+
2470
+ it("extracts path parameters for item endpoints", () => {
2471
+ const getCustomer = spec.paths.find(
2472
+ (p) => p.method === "GET" && p.path === "/v1/customers/:customer",
2473
+ );
2474
+ expect(getCustomer?.parameters).toContainEqual(
2475
+ expect.objectContaining({ name: "customer", in: "path" }),
2476
+ );
2477
+ });
2478
+
2479
+ it("extracts list response with data wrapper schema", () => {
2480
+ // Stripe list endpoints return { data: [...], has_more, url }
2481
+ const listCustomers = spec.paths.find(
2482
+ (p) => p.method === "GET" && p.path === "/v1/customers",
2483
+ );
2484
+ const schema = listCustomers?.responses.get(200)?.schema;
2485
+ expect(schema).toBeDefined();
2486
+ // Should have properties or be an allOf/anyOf composition
2487
+ expect(schema?.properties ?? schema?.allOf ?? schema?.anyOf).toBeDefined();
2488
+ });
2489
+
2490
+ it("extracts operationIds", () => {
2491
+ const ops = spec.paths.filter((p) => p.operationId);
2492
+ expect(ops.length).toBeGreaterThan(300);
2493
+ const listCustomers = spec.paths.find(
2494
+ (p) => p.method === "GET" && p.path === "/v1/customers",
2495
+ );
2496
+ expect(listCustomers?.operationId).toBe("GetCustomers");
2497
+ });
2498
+
2499
+ it("handles deeply nested paths (customers → balance_transactions)", () => {
2500
+ const sigs = spec.paths.map((p) => `${p.method} ${p.path}`);
2501
+ expect(sigs).toContain("GET /v1/customers/:customer/balance_transactions");
2502
+ expect(sigs).toContain(
2503
+ "GET /v1/customers/:customer/balance_transactions/:transaction",
2504
+ );
2505
+ });
2506
+ });
2507
+
2508
+ describe("stress: stripe spec — CRUD detection", () => {
2509
+ let spec: ParsedSpec;
2510
+
2511
+ beforeAll(async () => {
2512
+ spec = await getStripeSpec();
2513
+ }, 120_000);
2514
+
2515
+ it("detects CRUD resources from 415 paths", () => {
2516
+ const result = detectCrudResources(spec.paths);
2517
+ expect(result.resources.length).toBeGreaterThan(0);
2518
+ expect(result.nonCrudPaths.length).toBeGreaterThan(0);
2519
+ }, 120_000);
2520
+
2521
+ it("detects customers as a CRUD resource", () => {
2522
+ const result = detectCrudResources(spec.paths);
2523
+ const customers = result.resources.find((r) => r.name === "customers");
2524
+ expect(customers).toBeDefined();
2525
+ expect(customers?.basePath).toBe("/v1/customers");
2526
+ expect(customers?.itemPath).toBe("/v1/customers/:customer");
2527
+ expect(customers?.idParam).toBe("customer");
2528
+ expect(customers?.operations).toContain("list");
2529
+ expect(customers?.operations).toContain("create");
2530
+ expect(customers?.operations).toContain("read");
2531
+ expect(customers?.operations).toContain("delete");
2532
+ });
2533
+
2534
+ it("detects /v1/products as CRUD with idParam=id", () => {
2535
+ const result = detectCrudResources(spec.paths);
2536
+ // Use basePath to distinguish from /v1/climate/products (also named "products")
2537
+ const products = result.resources.find(
2538
+ (r) => r.basePath === "/v1/products",
2539
+ );
2540
+ expect(products).toBeDefined();
2541
+ // Stripe uses /v1/products/{id} — idParam is "id" (not "product")
2542
+ expect(products?.idParam).toBe("id");
2543
+ expect(products?.itemPath).toBe("/v1/products/:id");
2544
+ expect(products?.operations).toContain("list");
2545
+ expect(products?.operations).toContain("create");
2546
+ expect(products?.operations).toContain("read");
2547
+ expect(products?.operations).toContain("delete");
2548
+ });
2549
+
2550
+ it("detects coupons as a CRUD resource", () => {
2551
+ const result = detectCrudResources(spec.paths);
2552
+ const coupons = result.resources.find((r) => r.name === "coupons");
2553
+ expect(coupons).toBeDefined();
2554
+ expect(coupons?.idParam).toBe("coupon");
2555
+ });
2556
+
2557
+ it("classifies /v1/balance as non-CRUD (singleton)", () => {
2558
+ const result = detectCrudResources(spec.paths);
2559
+ const balanceResource = result.resources.find(
2560
+ (r) => r.basePath === "/v1/balance",
2561
+ );
2562
+ expect(balanceResource).toBeUndefined();
2563
+ const balanceNonCrud = result.nonCrudPaths.find(
2564
+ (p) => p.path === "/v1/balance",
2565
+ );
2566
+ expect(balanceNonCrud).toBeDefined();
2567
+ });
2568
+
2569
+ it("classifies search endpoints as non-CRUD", () => {
2570
+ const result = detectCrudResources(spec.paths);
2571
+ const searchPaths = result.nonCrudPaths.filter((p) =>
2572
+ p.path.endsWith("/search"),
2573
+ );
2574
+ expect(searchPaths.length).toBeGreaterThan(0);
2575
+ });
2576
+ });
2577
+
2578
+ describe("stress: stripe spec — CRUD lifecycle with fixtures", () => {
2579
+ // Share ONE plugin instance across all lifecycle tests to avoid OOM
2580
+ let plugin: Schmock.Plugin;
2581
+
2582
+ beforeAll(async () => {
2583
+ plugin = await getStripePlugin();
2584
+ }, 120_000);
2585
+
2586
+ it("customer CRUD lifecycle with real fixture data", async () => {
2587
+ const mock = schmock({ state: {} });
2588
+ mock.pipe(plugin);
2589
+
2590
+ // Create from fixture data
2591
+ const custFixture = stripeResources.customer;
2592
+ const created = await mock.handle("POST", "/v1/customers", {
2593
+ body: {
2594
+ email: custFixture.email,
2595
+ name: custFixture.name,
2596
+ description: custFixture.description,
2597
+ currency: custFixture.currency,
2598
+ },
2599
+ });
2600
+ expect(created.status).toBe(201);
2601
+ const cust = created.body as Record<string, unknown>;
2602
+ expect(cust.email).toBe(custFixture.email);
2603
+ expect(cust.customer).toBe(1);
2604
+
2605
+ // Read
2606
+ const read = await mock.handle("GET", "/v1/customers/1");
2607
+ expect(read.status).toBe(200);
2608
+ expect((read.body as Record<string, unknown>).email).toBe(
2609
+ custFixture.email,
2610
+ );
2611
+
2612
+ // List
2613
+ const list = await mock.handle("GET", "/v1/customers");
2614
+ expect(list.status).toBe(200);
2615
+ expect(list.body).toHaveLength(1);
2616
+
2617
+ // Delete
2618
+ const deleted = await mock.handle("DELETE", "/v1/customers/1");
2619
+ expect(deleted.status).toBe(204);
2620
+
2621
+ // Verify deleted
2622
+ const gone = await mock.handle("GET", "/v1/customers/1");
2623
+ expect(gone.status).toBe(404);
2624
+ }, 120_000);
2625
+
2626
+ it("product CRUD uses idParam=id (from Stripe's {id} path template)", async () => {
2627
+ const mock = schmock({ state: {} });
2628
+ mock.pipe(plugin);
2629
+
2630
+ // Create — product idParam is "id", auto-incremented
2631
+ const created = await mock.handle("POST", "/v1/products", {
2632
+ body: { name: "Premium Plan", active: true },
2633
+ });
2634
+ expect(created.status).toBe(201);
2635
+ const prod = created.body as Record<string, unknown>;
2636
+ expect(prod.id).toBe(1);
2637
+ expect(prod.name).toBe("Premium Plan");
2638
+
2639
+ // Read by "id"
2640
+ const read = await mock.handle("GET", "/v1/products/1");
2641
+ expect(read.status).toBe(200);
2642
+ expect((read.body as Record<string, unknown>).name).toBe("Premium Plan");
2643
+
2644
+ // List
2645
+ const list = await mock.handle("GET", "/v1/products");
2646
+ expect(list.body).toHaveLength(1);
2647
+
2648
+ // Delete
2649
+ const del = await mock.handle("DELETE", "/v1/products/1");
2650
+ expect(del.status).toBe(204);
2651
+ }, 120_000);
2652
+
2653
+ it("coupon CRUD with Stripe fixture data", async () => {
2654
+ const mock = schmock({ state: {} });
2655
+ mock.pipe(plugin);
2656
+
2657
+ const couponFixture = stripeResources.coupon;
2658
+ // Create with fixture data
2659
+ const created = await mock.handle("POST", "/v1/coupons", {
2660
+ body: {
2661
+ name: couponFixture.name,
2662
+ percent_off: couponFixture.percent_off,
2663
+ duration: couponFixture.duration,
2664
+ },
2665
+ });
2666
+ expect(created.status).toBe(201);
2667
+ const coupon = created.body as Record<string, unknown>;
2668
+ expect(coupon.coupon).toBe(1);
2669
+ expect(coupon.duration).toBe(couponFixture.duration);
2670
+
2671
+ // Delete
2672
+ const deleted = await mock.handle("DELETE", "/v1/coupons/1");
2673
+ expect(deleted.status).toBe(204);
2674
+
2675
+ // Gone
2676
+ const gone = await mock.handle("GET", "/v1/coupons/1");
2677
+ expect(gone.status).toBe(404);
2678
+ }, 120_000);
2679
+
2680
+ it("multi-resource: customers + products + coupons on same instance", async () => {
2681
+ const mock = schmock({ state: {} });
2682
+ mock.pipe(plugin);
2683
+
2684
+ // Create across 3 resources
2685
+ await mock.handle("POST", "/v1/customers", {
2686
+ body: { email: "alice@stripe.com" },
2687
+ });
2688
+ await mock.handle("POST", "/v1/customers", {
2689
+ body: { email: "bob@stripe.com" },
2690
+ });
2691
+ await mock.handle("POST", "/v1/products", {
2692
+ body: { name: "Basic" },
2693
+ });
2694
+ await mock.handle("POST", "/v1/coupons", {
2695
+ body: { percent_off: 25 },
2696
+ });
2697
+
2698
+ // Each resource is independent
2699
+ expect((await mock.handle("GET", "/v1/customers")).body).toHaveLength(2);
2700
+ expect((await mock.handle("GET", "/v1/products")).body).toHaveLength(1);
2701
+ expect((await mock.handle("GET", "/v1/coupons")).body).toHaveLength(1);
2702
+
2703
+ // Delete a customer doesn't affect products
2704
+ await mock.handle("DELETE", "/v1/customers/1");
2705
+ expect((await mock.handle("GET", "/v1/customers")).body).toHaveLength(1);
2706
+ expect((await mock.handle("GET", "/v1/products")).body).toHaveLength(1);
2707
+ }, 120_000);
2708
+
2709
+ it("non-CRUD endpoints respond alongside CRUD", async () => {
2710
+ const mock = schmock({ state: {} });
2711
+ mock.pipe(plugin);
2712
+
2713
+ // Singleton endpoint — /v1/balance (non-CRUD static)
2714
+ const balance = await mock.handle("GET", "/v1/balance");
2715
+ expect(balance.status).toBe(200);
2716
+
2717
+ // CRUD still works
2718
+ const created = await mock.handle("POST", "/v1/customers", {
2719
+ body: { email: "test@stripe.com" },
2720
+ });
2721
+ expect(created.status).toBe(201);
2722
+ }, 120_000);
2723
+
2724
+ it("resetState clears all Stripe resources", async () => {
2725
+ const mock = schmock({ state: {} });
2726
+ mock.pipe(plugin);
2727
+
2728
+ await mock.handle("POST", "/v1/customers", {
2729
+ body: { email: "temp@stripe.com" },
2730
+ });
2731
+ expect((await mock.handle("GET", "/v1/customers")).body).toHaveLength(1);
2732
+
2733
+ mock.resetState();
2734
+
2735
+ const list = await mock.handle("GET", "/v1/customers");
2736
+ expect(list.body).toEqual([]);
2737
+ }, 120_000);
2738
+ });
2739
+
2740
+ describe("stress: stripe spec — the confused Stripe developer", () => {
2741
+ let plugin: Schmock.Plugin;
2742
+
2743
+ beforeAll(async () => {
2744
+ plugin = await getStripePlugin();
2745
+ }, 120_000);
2746
+
2747
+ it("reads customer with Stripe-format ID (string) before any creates", async () => {
2748
+ const mock = schmock({ state: {} });
2749
+ mock.pipe(plugin);
2750
+
2751
+ // Stripe IDs look like "cus_QXg1o8vcGmoR32" — should 404 gracefully
2752
+ const read = await mock.handle("GET", "/v1/customers/cus_QXg1o8vcGmoR32");
2753
+ expect(read.status).toBe(404);
2754
+ }, 120_000);
2755
+
2756
+ it("rapid customer creation — 50 customers with fixture-like data", async () => {
2757
+ const mock = schmock({ state: {} });
2758
+ mock.pipe(plugin);
2759
+
2760
+ const emails = Array.from(
2761
+ { length: 50 },
2762
+ (_, i) => `user${i}@stripe-test.com`,
2763
+ );
2764
+
2765
+ for (const email of emails) {
2766
+ const res = await mock.handle("POST", "/v1/customers", {
2767
+ body: { email, name: `User ${email.split("@")[0]}` },
2768
+ });
2769
+ expect(res.status).toBe(201);
2770
+ }
2771
+
2772
+ const list = await mock.handle("GET", "/v1/customers");
2773
+ expect(list.body).toHaveLength(50);
2774
+ }, 120_000);
2775
+
2776
+ it("interleaved operations across Stripe resources", async () => {
2777
+ const mock = schmock({ state: {} });
2778
+ mock.pipe(plugin);
2779
+
2780
+ // Create customer → create product → create coupon → read customer → delete product
2781
+ const c1 = await mock.handle("POST", "/v1/customers", {
2782
+ body: { email: "x@test.com" },
2783
+ });
2784
+ const p1 = await mock.handle("POST", "/v1/products", {
2785
+ body: { name: "Widget" },
2786
+ });
2787
+ const co1 = await mock.handle("POST", "/v1/coupons", {
2788
+ body: { percent_off: 10 },
2789
+ });
2790
+
2791
+ // Read back customer
2792
+ const custId = (c1.body as Record<string, unknown>).customer;
2793
+ const read = await mock.handle("GET", `/v1/customers/${custId}`);
2794
+ expect(read.status).toBe(200);
2795
+
2796
+ // Delete product — idParam is "id" for products
2797
+ const prodId = (p1.body as Record<string, unknown>).id;
2798
+ await mock.handle("DELETE", `/v1/products/${prodId}`);
2799
+
2800
+ // Customer and coupon still exist
2801
+ expect((await mock.handle("GET", `/v1/customers/${custId}`)).status).toBe(
2802
+ 200,
2803
+ );
2804
+ const couponId = (co1.body as Record<string, unknown>).coupon;
2805
+ expect((await mock.handle("GET", `/v1/coupons/${couponId}`)).status).toBe(
2806
+ 200,
2807
+ );
2808
+
2809
+ // Product gone
2810
+ expect((await mock.handle("GET", `/v1/products/${prodId}`)).status).toBe(
2811
+ 404,
2812
+ );
2813
+ }, 120_000);
2814
+ });