@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.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/abort.d.ts +3 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +30 -0
- package/dist/binary.d.ts +8 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +9 -0
- package/dist/builder.d.ts +31 -10
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +804 -203
- package/dist/constants.d.ts +38 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +123 -2
- package/dist/errors.d.ts +14 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +41 -6
- package/dist/helpers.d.ts +9 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +18 -2
- package/dist/http-helpers.d.ts +52 -5
- package/dist/http-helpers.d.ts.map +1 -1
- package/dist/http-helpers.js +153 -37
- package/dist/index.d.ts +381 -60
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/interceptor.d.ts +11 -1
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/interceptor.js +354 -156
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +12 -3
- package/dist/plugin-pipeline.d.ts +11 -4
- package/dist/plugin-pipeline.d.ts.map +1 -1
- package/dist/plugin-pipeline.js +117 -39
- package/dist/response-normalizer.d.ts +16 -0
- package/dist/response-normalizer.d.ts.map +1 -0
- package/dist/response-normalizer.js +316 -0
- package/dist/response-parser.d.ts.map +1 -1
- package/dist/response-parser.js +85 -18
- package/dist/route-matcher.d.ts +3 -0
- package/dist/route-matcher.d.ts.map +1 -1
- package/dist/route-matcher.js +12 -6
- package/dist/types.d.ts +7 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +18 -8
- package/src/audit-core-builder.test.ts +0 -218
- package/src/audit-response-guard.test.ts +0 -38
- package/src/builder.test.ts +0 -289
- package/src/builder.ts +0 -701
- package/src/constants.test.ts +0 -99
- package/src/constants.ts +0 -73
- package/src/debug.test.ts +0 -241
- package/src/delay.test.ts +0 -319
- package/src/dist-shape.test.ts +0 -35
- package/src/errors.test.ts +0 -223
- package/src/errors.ts +0 -130
- package/src/factory.test.ts +0 -133
- package/src/helpers.test.ts +0 -147
- package/src/helpers.ts +0 -58
- package/src/http-helpers.ts +0 -113
- package/src/index.ts +0 -151
- package/src/interceptor.test.ts +0 -291
- package/src/interceptor.ts +0 -320
- package/src/namespace.test.ts +0 -274
- package/src/parser.property.test.ts +0 -594
- package/src/parser.test.ts +0 -148
- package/src/parser.ts +0 -64
- package/src/plugin-pipeline.ts +0 -103
- package/src/plugin-system.test.ts +0 -602
- package/src/response-parser.ts +0 -75
- package/src/response-parsing.test.ts +0 -333
- package/src/route-matcher.ts +0 -69
- package/src/route-matching.test.ts +0 -394
- package/src/server.test.ts +0 -165
- package/src/smart-defaults.test.ts +0 -361
- package/src/steps/async-support.steps.ts +0 -400
- package/src/steps/audit-core-builder.steps.ts +0 -188
- package/src/steps/audit-onerror-tuple.steps.ts +0 -99
- package/src/steps/basic-usage.steps.ts +0 -245
- package/src/steps/developer-experience.steps.ts +0 -204
- package/src/steps/error-handling.steps.ts +0 -345
- package/src/steps/fluent-api.steps.ts +0 -255
- package/src/steps/http-methods.steps.ts +0 -331
- package/src/steps/interceptor.steps.ts +0 -260
- package/src/steps/lifecycle-events.steps.ts +0 -142
- package/src/steps/performance-reliability.steps.ts +0 -423
- package/src/steps/plugin-integration.steps.ts +0 -280
- package/src/steps/request-history.steps.ts +0 -276
- package/src/steps/response-delay.steps.ts +0 -88
- package/src/steps/route-key-format.steps.ts +0 -99
- package/src/steps/standalone-server.steps.ts +0 -233
- package/src/steps/state-concurrency.steps.ts +0 -739
- package/src/steps/stateful-workflows.steps.ts +0 -353
- 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
|
-
});
|
package/src/response-parser.ts
DELETED
|
@@ -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
|
-
}
|