@schmock/core 2.2.3 → 2.3.1

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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/dist/abort.d.ts +3 -0
  4. package/dist/abort.d.ts.map +1 -0
  5. package/dist/abort.js +30 -0
  6. package/dist/binary.d.ts +8 -0
  7. package/dist/binary.d.ts.map +1 -0
  8. package/dist/binary.js +9 -0
  9. package/dist/builder.d.ts +31 -10
  10. package/dist/builder.d.ts.map +1 -1
  11. package/dist/builder.js +804 -203
  12. package/dist/constants.d.ts +38 -0
  13. package/dist/constants.d.ts.map +1 -1
  14. package/dist/constants.js +123 -2
  15. package/dist/errors.d.ts +14 -3
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +41 -6
  18. package/dist/helpers.d.ts +9 -0
  19. package/dist/helpers.d.ts.map +1 -1
  20. package/dist/helpers.js +18 -2
  21. package/dist/http-helpers.d.ts +52 -5
  22. package/dist/http-helpers.d.ts.map +1 -1
  23. package/dist/http-helpers.js +153 -37
  24. package/dist/index.d.ts +381 -60
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +9 -3
  27. package/dist/interceptor.d.ts +11 -1
  28. package/dist/interceptor.d.ts.map +1 -1
  29. package/dist/interceptor.js +354 -156
  30. package/dist/parser.d.ts.map +1 -1
  31. package/dist/parser.js +12 -3
  32. package/dist/plugin-pipeline.d.ts +11 -4
  33. package/dist/plugin-pipeline.d.ts.map +1 -1
  34. package/dist/plugin-pipeline.js +117 -39
  35. package/dist/response-normalizer.d.ts +16 -0
  36. package/dist/response-normalizer.d.ts.map +1 -0
  37. package/dist/response-normalizer.js +316 -0
  38. package/dist/response-parser.d.ts.map +1 -1
  39. package/dist/response-parser.js +85 -18
  40. package/dist/route-matcher.d.ts +3 -0
  41. package/dist/route-matcher.d.ts.map +1 -1
  42. package/dist/route-matcher.js +12 -6
  43. package/dist/types.d.ts +7 -2
  44. package/dist/types.d.ts.map +1 -1
  45. package/package.json +18 -8
  46. package/src/audit-core-builder.test.ts +0 -218
  47. package/src/audit-response-guard.test.ts +0 -38
  48. package/src/builder.test.ts +0 -289
  49. package/src/builder.ts +0 -701
  50. package/src/constants.test.ts +0 -99
  51. package/src/constants.ts +0 -73
  52. package/src/debug.test.ts +0 -241
  53. package/src/delay.test.ts +0 -319
  54. package/src/dist-shape.test.ts +0 -35
  55. package/src/errors.test.ts +0 -223
  56. package/src/errors.ts +0 -130
  57. package/src/factory.test.ts +0 -133
  58. package/src/helpers.test.ts +0 -147
  59. package/src/helpers.ts +0 -58
  60. package/src/http-helpers.ts +0 -113
  61. package/src/index.ts +0 -151
  62. package/src/interceptor.test.ts +0 -291
  63. package/src/interceptor.ts +0 -320
  64. package/src/namespace.test.ts +0 -274
  65. package/src/parser.property.test.ts +0 -594
  66. package/src/parser.test.ts +0 -148
  67. package/src/parser.ts +0 -64
  68. package/src/plugin-pipeline.ts +0 -103
  69. package/src/plugin-system.test.ts +0 -602
  70. package/src/response-parser.ts +0 -75
  71. package/src/response-parsing.test.ts +0 -333
  72. package/src/route-matcher.ts +0 -69
  73. package/src/route-matching.test.ts +0 -394
  74. package/src/server.test.ts +0 -165
  75. package/src/smart-defaults.test.ts +0 -361
  76. package/src/steps/async-support.steps.ts +0 -400
  77. package/src/steps/audit-core-builder.steps.ts +0 -188
  78. package/src/steps/audit-onerror-tuple.steps.ts +0 -99
  79. package/src/steps/basic-usage.steps.ts +0 -245
  80. package/src/steps/developer-experience.steps.ts +0 -204
  81. package/src/steps/error-handling.steps.ts +0 -345
  82. package/src/steps/fluent-api.steps.ts +0 -255
  83. package/src/steps/http-methods.steps.ts +0 -331
  84. package/src/steps/interceptor.steps.ts +0 -260
  85. package/src/steps/lifecycle-events.steps.ts +0 -142
  86. package/src/steps/performance-reliability.steps.ts +0 -423
  87. package/src/steps/plugin-integration.steps.ts +0 -280
  88. package/src/steps/request-history.steps.ts +0 -276
  89. package/src/steps/response-delay.steps.ts +0 -88
  90. package/src/steps/route-key-format.steps.ts +0 -99
  91. package/src/steps/standalone-server.steps.ts +0 -233
  92. package/src/steps/state-concurrency.steps.ts +0 -739
  93. package/src/steps/stateful-workflows.steps.ts +0 -353
  94. package/src/types.ts +0 -36
@@ -1,602 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { schmock } from "./index";
3
-
4
- describe("plugin system", () => {
5
- describe("plugin registration", () => {
6
- it("registers plugins in order", async () => {
7
- const mock = schmock();
8
- const executionOrder: string[] = [];
9
-
10
- const plugin1 = {
11
- name: "first",
12
- process: (ctx: any, res: any) => {
13
- executionOrder.push("first");
14
- return { context: ctx, response: res };
15
- },
16
- };
17
-
18
- const plugin2 = {
19
- name: "second",
20
- process: (ctx: any, res: any) => {
21
- executionOrder.push("second");
22
- return { context: ctx, response: res };
23
- },
24
- };
25
-
26
- mock("GET /test", "response").pipe(plugin1).pipe(plugin2);
27
-
28
- await mock.handle("GET", "/test");
29
- expect(executionOrder).toEqual(["first", "second"]);
30
- });
31
-
32
- it("stores plugin metadata", async () => {
33
- const mock = schmock({ debug: true });
34
- const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
35
-
36
- const plugin = {
37
- name: "test-plugin",
38
- version: "1.2.3",
39
- process: (ctx: any, res: any) => ({ context: ctx, response: res }),
40
- };
41
-
42
- mock.pipe(plugin);
43
-
44
- // Check actual console calls - should have at least plugin registration
45
- expect(consoleSpy).toHaveBeenCalled();
46
-
47
- // Check that plugin registration was logged
48
- const pluginCall = consoleSpy.mock.calls.find((call) =>
49
- call[0].includes("[SCHMOCK:PLUGIN]"),
50
- );
51
-
52
- expect(pluginCall).toBeDefined();
53
- expect(pluginCall[0]).toContain("Registered plugin: test-plugin@1.2.3");
54
- expect(pluginCall[1]).toMatchObject({
55
- name: "test-plugin",
56
- version: "1.2.3",
57
- hasProcess: true,
58
- hasOnError: false,
59
- });
60
-
61
- consoleSpy.mockRestore();
62
- });
63
-
64
- it("handles plugins without version", async () => {
65
- const mock = schmock({ debug: true });
66
- const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
67
-
68
- const plugin = {
69
- name: "no-version",
70
- process: (ctx: any, res: any) => ({ context: ctx, response: res }),
71
- };
72
-
73
- mock.pipe(plugin);
74
-
75
- // Check that plugin without version was logged
76
- const pluginCall = consoleSpy.mock.calls.find(
77
- (call) =>
78
- call[0].includes("[SCHMOCK:PLUGIN]") &&
79
- call[0].includes("no-version@unknown"),
80
- );
81
-
82
- expect(pluginCall).toBeDefined();
83
-
84
- consoleSpy.mockRestore();
85
- });
86
- });
87
-
88
- describe("plugin execution pipeline", () => {
89
- it("passes context between plugins", async () => {
90
- const mock = schmock();
91
- let receivedContext: any;
92
-
93
- const plugin1 = {
94
- name: "setter",
95
- process: (ctx: any, res: any) => {
96
- ctx.state.set("modified", "plugin1");
97
- return { context: ctx, response: res };
98
- },
99
- };
100
-
101
- const plugin2 = {
102
- name: "getter",
103
- process: (ctx: any, res: any) => {
104
- receivedContext = ctx;
105
- return { context: ctx, response: res };
106
- },
107
- };
108
-
109
- mock("GET /test", "original").pipe(plugin1).pipe(plugin2);
110
-
111
- await mock.handle("GET", "/test");
112
- expect(receivedContext.state.get("modified")).toBe("plugin1");
113
- });
114
-
115
- it("allows first plugin to generate response", async () => {
116
- const mock = schmock();
117
-
118
- const plugin = {
119
- name: "generator",
120
- process: (ctx: any, _res: any) => {
121
- return { context: ctx, response: "plugin-generated" };
122
- },
123
- };
124
-
125
- mock("GET /test", null).pipe(plugin);
126
-
127
- const response = await mock.handle("GET", "/test");
128
- expect(response.body).toBe("plugin-generated");
129
- });
130
-
131
- it("allows later plugins to transform response", async () => {
132
- const mock = schmock();
133
-
134
- const plugin1 = {
135
- name: "generator",
136
- process: (ctx: any, res: any) => {
137
- return { context: ctx, response: res || "initial" };
138
- },
139
- };
140
-
141
- const plugin2 = {
142
- name: "transformer",
143
- process: (ctx: any, res: any) => {
144
- return { context: ctx, response: `transformed-${res}` };
145
- },
146
- };
147
-
148
- mock("GET /test", "original").pipe(plugin1).pipe(plugin2);
149
-
150
- const response = await mock.handle("GET", "/test");
151
- expect(response.body).toBe("transformed-original");
152
- });
153
-
154
- it("preserves response if plugin doesn't modify it", async () => {
155
- const mock = schmock();
156
-
157
- const plugin = {
158
- name: "passthrough",
159
- process: (ctx: any, res: any) => {
160
- ctx.state.set("processed", true);
161
- return { context: ctx, response: res };
162
- },
163
- };
164
-
165
- mock("GET /test", "unchanged").pipe(plugin);
166
-
167
- const response = await mock.handle("GET", "/test");
168
- expect(response.body).toBe("unchanged");
169
- });
170
- });
171
-
172
- describe("plugin context", () => {
173
- it("provides complete plugin context", async () => {
174
- const mock = schmock();
175
- let pluginContext: any;
176
-
177
- const plugin = {
178
- name: "inspector",
179
- process: (ctx: any, res: any) => {
180
- pluginContext = ctx;
181
- return { context: ctx, response: res };
182
- },
183
- };
184
-
185
- mock("GET /users/:id", "response").pipe(plugin);
186
-
187
- await mock.handle("GET", "/users/123", {
188
- query: { limit: "10" },
189
- headers: { authorization: "Bearer token" },
190
- body: { test: "data" },
191
- });
192
-
193
- expect(pluginContext).toMatchObject({
194
- path: "/users/123",
195
- method: "GET",
196
- params: { id: "123" },
197
- query: { limit: "10" },
198
- headers: { authorization: "Bearer token" },
199
- body: { test: "data" },
200
- });
201
- expect(pluginContext.state).toBeInstanceOf(Map);
202
- expect(pluginContext.route).toBeDefined();
203
- });
204
-
205
- it("provides route configuration in context", async () => {
206
- const mock = schmock();
207
- let routeConfig: any;
208
-
209
- const plugin = {
210
- name: "config-reader",
211
- process: (ctx: any, res: any) => {
212
- routeConfig = ctx.route;
213
- return { context: ctx, response: res };
214
- },
215
- };
216
-
217
- mock("GET /test", "response", {
218
- contentType: "text/plain",
219
- custom: "value",
220
- }).pipe(plugin);
221
-
222
- await mock.handle("GET", "/test");
223
-
224
- expect(routeConfig).toMatchObject({
225
- contentType: "text/plain",
226
- custom: "value",
227
- });
228
- });
229
-
230
- it("isolates state between requests", async () => {
231
- const mock = schmock();
232
- const states: any[] = [];
233
-
234
- const plugin = {
235
- name: "state-tracker",
236
- process: (ctx: any, res: any) => {
237
- ctx.state.set("requestId", Math.random());
238
- states.push(ctx.state.get("requestId"));
239
- return { context: ctx, response: res };
240
- },
241
- };
242
-
243
- mock("GET /test", "response").pipe(plugin);
244
-
245
- await mock.handle("GET", "/test");
246
- await mock.handle("GET", "/test");
247
-
248
- expect(states).toHaveLength(2);
249
- expect(states[0]).not.toBe(states[1]);
250
- });
251
- });
252
-
253
- describe("plugin error handling", () => {
254
- it("throws PluginError when plugin process fails", async () => {
255
- const mock = schmock();
256
-
257
- const plugin = {
258
- name: "failing-plugin",
259
- process: () => {
260
- throw new Error("Plugin failed");
261
- },
262
- };
263
-
264
- mock("GET /test", "response").pipe(plugin);
265
-
266
- const response = await mock.handle("GET", "/test");
267
- expect(response.status).toBe(500);
268
- expect(response.body.error).toContain('Plugin "failing-plugin" failed');
269
- expect(response.body.code).toBe("PLUGIN_ERROR");
270
- });
271
-
272
- it("calls onError hook when plugin fails", async () => {
273
- const mock = schmock();
274
- const onErrorSpy = vi.fn();
275
-
276
- const plugin = {
277
- name: "recoverable-plugin",
278
- process: () => {
279
- throw new Error("Initial failure");
280
- },
281
- onError: (error: Error, ctx: any) => {
282
- onErrorSpy(error, ctx);
283
- return { status: 200, body: "recovered", headers: {} };
284
- },
285
- };
286
-
287
- mock("GET /test", "response").pipe(plugin);
288
-
289
- const response = await mock.handle("GET", "/test");
290
-
291
- expect(onErrorSpy).toHaveBeenCalledWith(
292
- expect.objectContaining({ message: "Initial failure" }),
293
- expect.any(Object),
294
- );
295
- expect(response.status).toBe(200);
296
- expect(response.body).toBe("recovered");
297
- });
298
-
299
- it("continues with error propagation if onError doesn't return response", async () => {
300
- const mock = schmock();
301
-
302
- const plugin = {
303
- name: "non-recovering-plugin",
304
- process: () => {
305
- throw new Error("Plugin failed");
306
- },
307
- onError: (_error: Error, _ctx: any) => {
308
- // Just log, don't return response
309
- return undefined;
310
- },
311
- };
312
-
313
- mock("GET /test", "response").pipe(plugin);
314
-
315
- const response = await mock.handle("GET", "/test");
316
- expect(response.status).toBe(500);
317
- expect(response.body.code).toBe("PLUGIN_ERROR");
318
- });
319
-
320
- it("handles onError hook failures", async () => {
321
- const mock = schmock({ debug: true });
322
- const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
323
-
324
- const plugin = {
325
- name: "broken-error-handler",
326
- process: () => {
327
- throw new Error("Initial failure");
328
- },
329
- onError: () => {
330
- throw new Error("Error handler failed");
331
- },
332
- };
333
-
334
- mock("GET /test", "response").pipe(plugin);
335
-
336
- const response = await mock.handle("GET", "/test");
337
-
338
- // Check that error handler failure was logged
339
- const errorCall = consoleSpy.mock.calls.find(
340
- (call) =>
341
- call[0].includes("[SCHMOCK:PIPELINE]") &&
342
- call[0].includes("error handler failed"),
343
- );
344
-
345
- expect(errorCall).toBeDefined();
346
- expect(response.status).toBe(500);
347
-
348
- consoleSpy.mockRestore();
349
- });
350
-
351
- it("validates plugin returns proper result structure", async () => {
352
- const mock = schmock();
353
-
354
- const plugin = {
355
- name: "invalid-plugin",
356
- process: () => {
357
- return { invalidStructure: true }; // Missing context
358
- },
359
- };
360
-
361
- mock("GET /test", "response").pipe(plugin);
362
-
363
- const response = await mock.handle("GET", "/test");
364
- expect(response.status).toBe(500);
365
- expect(response.body.error).toContain("didn't return valid result");
366
- });
367
-
368
- it("handles plugin returning null/undefined", async () => {
369
- const mock = schmock();
370
-
371
- const plugin = {
372
- name: "null-plugin",
373
- process: () => null,
374
- };
375
-
376
- mock("GET /test", "response").pipe(plugin);
377
-
378
- const response = await mock.handle("GET", "/test");
379
- expect(response.status).toBe(500);
380
- expect(response.body.error).toContain("didn't return valid result");
381
- });
382
- });
383
-
384
- describe("async plugin support", () => {
385
- it("handles async plugin process methods", async () => {
386
- const mock = schmock();
387
-
388
- const plugin = {
389
- name: "async-plugin",
390
- process: async (ctx: any, res: any) => {
391
- await new Promise((resolve) => setTimeout(resolve, 10));
392
- return { context: ctx, response: `async-${res}` };
393
- },
394
- };
395
-
396
- mock("GET /test", "response").pipe(plugin);
397
-
398
- const response = await mock.handle("GET", "/test");
399
- expect(response.body).toBe("async-response");
400
- });
401
-
402
- it("handles async onError hooks", async () => {
403
- const mock = schmock();
404
-
405
- const plugin = {
406
- name: "async-error-plugin",
407
- process: () => {
408
- throw new Error("Async failure");
409
- },
410
- onError: async (_error: Error, _ctx: any) => {
411
- await new Promise((resolve) => setTimeout(resolve, 10));
412
- return { status: 200, body: "async-recovered", headers: {} };
413
- },
414
- };
415
-
416
- mock("GET /test", "response").pipe(plugin);
417
-
418
- const response = await mock.handle("GET", "/test");
419
- expect(response.body).toBe("async-recovered");
420
- });
421
- });
422
-
423
- describe("plugin install hook", () => {
424
- it("calls install with callable instance when pipe() is invoked", () => {
425
- const mock = schmock();
426
- let receivedInstance: unknown;
427
-
428
- const plugin: Schmock.Plugin = {
429
- name: "install-test",
430
- install(instance) {
431
- receivedInstance = instance;
432
- },
433
- process: (ctx: any, res: any) => ({ context: ctx, response: res }),
434
- };
435
-
436
- mock.pipe(plugin);
437
- expect(receivedInstance).toBe(mock);
438
- });
439
-
440
- it("works normally when plugin has no install method", async () => {
441
- const mock = schmock();
442
-
443
- const plugin: Schmock.Plugin = {
444
- name: "no-install",
445
- process: (ctx: any, res: any) => ({ context: ctx, response: res }),
446
- };
447
-
448
- mock("GET /test", "hello").pipe(plugin);
449
- const response = await mock.handle("GET", "/test");
450
- expect(response.body).toBe("hello");
451
- });
452
-
453
- it("allows install to register routes on the instance", async () => {
454
- const mock = schmock();
455
-
456
- const plugin: Schmock.Plugin = {
457
- name: "route-installer",
458
- install(instance) {
459
- instance("GET /installed", { message: "from-install" });
460
- },
461
- process: (ctx: any, res: any) => ({ context: ctx, response: res }),
462
- };
463
-
464
- mock.pipe(plugin);
465
-
466
- const response = await mock.handle("GET", "/installed");
467
- expect(response.status).toBe(200);
468
- expect(response.body).toEqual({ message: "from-install" });
469
- });
470
-
471
- it("allows install to register multiple routes", async () => {
472
- const mock = schmock();
473
-
474
- const plugin: Schmock.Plugin = {
475
- name: "multi-route-installer",
476
- install(instance) {
477
- instance("GET /a", "route-a");
478
- instance("POST /b", "route-b");
479
- },
480
- process: (ctx: any, res: any) => ({ context: ctx, response: res }),
481
- };
482
-
483
- mock.pipe(plugin);
484
-
485
- const responseA = await mock.handle("GET", "/a");
486
- expect(responseA.body).toBe("route-a");
487
-
488
- const responseB = await mock.handle("POST", "/b");
489
- expect(responseB.body).toBe("route-b");
490
- });
491
-
492
- it("routes registered in install work with generator functions", async () => {
493
- const mock = schmock();
494
-
495
- const plugin: Schmock.Plugin = {
496
- name: "generator-installer",
497
- install(instance) {
498
- instance("GET /items/:id", (ctx) => ({
499
- id: ctx.params.id,
500
- name: "test",
501
- }));
502
- },
503
- process: (ctx: any, res: any) => ({ context: ctx, response: res }),
504
- };
505
-
506
- mock.pipe(plugin);
507
-
508
- const response = await mock.handle("GET", "/items/42");
509
- expect(response.status).toBe(200);
510
- expect(response.body).toEqual({ id: "42", name: "test" });
511
- });
512
- });
513
-
514
- describe("debug logging", () => {
515
- it("logs plugin pipeline execution with debug enabled", async () => {
516
- const mock = schmock({ debug: true });
517
- const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
518
-
519
- const plugin = {
520
- name: "logged-plugin",
521
- process: (ctx: any, res: any) => ({ context: ctx, response: res }),
522
- };
523
-
524
- mock("GET /test", "response").pipe(plugin);
525
-
526
- await mock.handle("GET", "/test");
527
-
528
- // Check that pipeline execution was logged
529
- const pipelineCall = consoleSpy.mock.calls.find(
530
- (call) =>
531
- call[0].includes("[SCHMOCK:PIPELINE]") &&
532
- call[0].includes("Running plugin pipeline for 1 plugins"),
533
- );
534
- const processingCall = consoleSpy.mock.calls.find(
535
- (call) =>
536
- call[0].includes("[SCHMOCK:PIPELINE]") &&
537
- call[0].includes("Processing plugin: logged-plugin"),
538
- );
539
-
540
- expect(pipelineCall).toBeDefined();
541
- expect(processingCall).toBeDefined();
542
-
543
- consoleSpy.mockRestore();
544
- });
545
-
546
- it("logs plugin response generation", async () => {
547
- const mock = schmock({ debug: true });
548
- const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
549
-
550
- const plugin = {
551
- name: "generator-plugin",
552
- process: (ctx: any, _res: any) => ({
553
- context: ctx,
554
- response: "generated",
555
- }),
556
- };
557
-
558
- mock("GET /test", null).pipe(plugin);
559
-
560
- await mock.handle("GET", "/test");
561
-
562
- // Check that plugin response generation was logged
563
- const generatedCall = consoleSpy.mock.calls.find(
564
- (call) =>
565
- call[0].includes("[SCHMOCK:PIPELINE]") &&
566
- call[0].includes("Plugin generator-plugin generated response"),
567
- );
568
-
569
- expect(generatedCall).toBeDefined();
570
-
571
- consoleSpy.mockRestore();
572
- });
573
-
574
- it("logs plugin response transformation", async () => {
575
- const mock = schmock({ debug: true });
576
- const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
577
-
578
- const plugin = {
579
- name: "transformer-plugin",
580
- process: (ctx: any, res: any) => ({
581
- context: ctx,
582
- response: `transformed-${res}`,
583
- }),
584
- };
585
-
586
- mock("GET /test", "original").pipe(plugin);
587
-
588
- await mock.handle("GET", "/test");
589
-
590
- // Check that plugin response transformation was logged
591
- const transformedCall = consoleSpy.mock.calls.find(
592
- (call) =>
593
- call[0].includes("[SCHMOCK:PIPELINE]") &&
594
- call[0].includes("Plugin transformer-plugin transformed response"),
595
- );
596
-
597
- expect(transformedCall).toBeDefined();
598
-
599
- consoleSpy.mockRestore();
600
- });
601
- });
602
- });
@@ -1,75 +0,0 @@
1
- import { isStatusTuple } from "./constants.js";
2
-
3
- function isResponseObject(value: unknown): value is {
4
- status: number;
5
- body: unknown;
6
- headers?: Record<string, string>;
7
- } {
8
- return (
9
- typeof value === "object" &&
10
- value !== null &&
11
- "status" in value &&
12
- typeof (value as Record<string, unknown>).status === "number" &&
13
- "body" in value
14
- );
15
- }
16
-
17
- /**
18
- * Parse and normalize response result into Response object
19
- * Handles tuple format [status, body, headers], direct values, and response objects
20
- */
21
- export function parseResponse(
22
- result: unknown,
23
- routeConfig: Schmock.RouteConfig,
24
- ): Schmock.Response {
25
- let status = 200;
26
- let body: unknown = result;
27
- let headers: Record<string, string> = {};
28
-
29
- let tupleFormat = false;
30
-
31
- // Handle already-formed response objects (from plugin error recovery)
32
- if (isResponseObject(result)) {
33
- return {
34
- status: result.status,
35
- body: result.body,
36
- headers: result.headers || {},
37
- };
38
- }
39
-
40
- // Handle tuple response format [status, body, headers?]
41
- if (isStatusTuple(result)) {
42
- [status, body, headers = {}] = result;
43
- tupleFormat = true;
44
- }
45
-
46
- // Handle null/undefined responses with 204 No Content
47
- // But don't auto-convert if tuple format was used (status was explicitly provided)
48
- if (body === null || body === undefined) {
49
- if (!tupleFormat) {
50
- status = status === 200 ? 204 : status; // Only change to 204 if status wasn't explicitly set via tuple
51
- }
52
- body = undefined; // Ensure body is undefined for null responses
53
- }
54
-
55
- // Add content-type header from route config if it exists and headers don't already have it
56
- // But only if this isn't a tuple response (where headers are explicitly controlled)
57
- if (!headers["content-type"] && routeConfig.contentType && !tupleFormat) {
58
- headers["content-type"] = routeConfig.contentType;
59
-
60
- // Handle special conversion cases when contentType is explicitly set
61
- if (routeConfig.contentType === "text/plain" && body !== undefined) {
62
- if (typeof body === "object" && !Buffer.isBuffer(body)) {
63
- body = JSON.stringify(body);
64
- } else if (typeof body !== "string") {
65
- body = String(body);
66
- }
67
- }
68
- }
69
-
70
- return {
71
- status,
72
- body,
73
- headers,
74
- };
75
- }