@frontmcp/testing 0.6.1 → 0.6.3
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/esm/fixtures/index.mjs +2377 -0
- package/esm/index.mjs +4768 -0
- package/esm/matchers/index.mjs +646 -0
- package/esm/package.json +122 -0
- package/esm/playwright/index.mjs +19 -0
- package/esm/setup.mjs +680 -0
- package/fixtures/index.js +2418 -0
- package/index.js +4866 -0
- package/jest-preset.js +3 -3
- package/matchers/index.js +673 -0
- package/package.json +51 -23
- package/playwright/index.js +46 -0
- package/setup.js +651 -0
- package/src/assertions/index.js +0 -18
- package/src/assertions/index.js.map +0 -1
- package/src/assertions/mcp-assertions.js +0 -220
- package/src/assertions/mcp-assertions.js.map +0 -1
- package/src/auth/auth-headers.js +0 -62
- package/src/auth/auth-headers.js.map +0 -1
- package/src/auth/index.js +0 -15
- package/src/auth/index.js.map +0 -1
- package/src/auth/mock-api-server.js +0 -200
- package/src/auth/mock-api-server.js.map +0 -1
- package/src/auth/mock-oauth-server.js +0 -253
- package/src/auth/mock-oauth-server.js.map +0 -1
- package/src/auth/token-factory.js +0 -181
- package/src/auth/token-factory.js.map +0 -1
- package/src/auth/user-fixtures.js +0 -92
- package/src/auth/user-fixtures.js.map +0 -1
- package/src/client/index.js +0 -12
- package/src/client/index.js.map +0 -1
- package/src/client/mcp-test-client.builder.js +0 -163
- package/src/client/mcp-test-client.builder.js.map +0 -1
- package/src/client/mcp-test-client.js +0 -937
- package/src/client/mcp-test-client.js.map +0 -1
- package/src/client/mcp-test-client.types.js +0 -16
- package/src/client/mcp-test-client.types.js.map +0 -1
- package/src/errors/index.js +0 -85
- package/src/errors/index.js.map +0 -1
- package/src/example-tools/index.js +0 -40
- package/src/example-tools/index.js.map +0 -1
- package/src/example-tools/tool-configs.js +0 -222
- package/src/example-tools/tool-configs.js.map +0 -1
- package/src/expect.js +0 -31
- package/src/expect.js.map +0 -1
- package/src/fixtures/fixture-types.js +0 -7
- package/src/fixtures/fixture-types.js.map +0 -1
- package/src/fixtures/index.js +0 -16
- package/src/fixtures/index.js.map +0 -1
- package/src/fixtures/test-fixture.js +0 -311
- package/src/fixtures/test-fixture.js.map +0 -1
- package/src/http-mock/http-mock.js +0 -544
- package/src/http-mock/http-mock.js.map +0 -1
- package/src/http-mock/http-mock.types.js +0 -10
- package/src/http-mock/http-mock.types.js.map +0 -1
- package/src/http-mock/index.js +0 -11
- package/src/http-mock/index.js.map +0 -1
- package/src/index.js +0 -167
- package/src/index.js.map +0 -1
- package/src/interceptor/index.js +0 -15
- package/src/interceptor/index.js.map +0 -1
- package/src/interceptor/interceptor-chain.js +0 -207
- package/src/interceptor/interceptor-chain.js.map +0 -1
- package/src/interceptor/interceptor.types.js +0 -7
- package/src/interceptor/interceptor.types.js.map +0 -1
- package/src/interceptor/mock-registry.js +0 -189
- package/src/interceptor/mock-registry.js.map +0 -1
- package/src/matchers/index.js +0 -12
- package/src/matchers/index.js.map +0 -1
- package/src/matchers/matcher-types.js +0 -10
- package/src/matchers/matcher-types.js.map +0 -1
- package/src/matchers/mcp-matchers.js +0 -395
- package/src/matchers/mcp-matchers.js.map +0 -1
- package/src/platform/index.js +0 -47
- package/src/platform/index.js.map +0 -1
- package/src/platform/platform-client-info.js +0 -155
- package/src/platform/platform-client-info.js.map +0 -1
- package/src/platform/platform-types.js +0 -110
- package/src/platform/platform-types.js.map +0 -1
- package/src/playwright/index.js +0 -49
- package/src/playwright/index.js.map +0 -1
- package/src/server/index.js +0 -10
- package/src/server/index.js.map +0 -1
- package/src/server/test-server.js +0 -341
- package/src/server/test-server.js.map +0 -1
- package/src/setup.js +0 -30
- package/src/setup.js.map +0 -1
- package/src/transport/index.js +0 -10
- package/src/transport/index.js.map +0 -1
- package/src/transport/streamable-http.transport.js +0 -438
- package/src/transport/streamable-http.transport.js.map +0 -1
- package/src/transport/transport.interface.js +0 -7
- package/src/transport/transport.interface.js.map +0 -1
- package/src/ui/index.js +0 -23
- package/src/ui/index.js.map +0 -1
- package/src/ui/ui-assertions.js +0 -367
- package/src/ui/ui-assertions.js.map +0 -1
- package/src/ui/ui-matchers.js +0 -493
- package/src/ui/ui-matchers.js.map +0 -1
- /package/{src/assertions → assertions}/index.d.ts +0 -0
- /package/{src/assertions → assertions}/mcp-assertions.d.ts +0 -0
- /package/{src/auth → auth}/auth-headers.d.ts +0 -0
- /package/{src/auth → auth}/index.d.ts +0 -0
- /package/{src/auth → auth}/mock-api-server.d.ts +0 -0
- /package/{src/auth → auth}/mock-oauth-server.d.ts +0 -0
- /package/{src/auth → auth}/token-factory.d.ts +0 -0
- /package/{src/auth → auth}/user-fixtures.d.ts +0 -0
- /package/{src/client → client}/index.d.ts +0 -0
- /package/{src/client → client}/mcp-test-client.builder.d.ts +0 -0
- /package/{src/client → client}/mcp-test-client.d.ts +0 -0
- /package/{src/client → client}/mcp-test-client.types.d.ts +0 -0
- /package/{src/errors → errors}/index.d.ts +0 -0
- /package/{src/example-tools → example-tools}/index.d.ts +0 -0
- /package/{src/example-tools → example-tools}/tool-configs.d.ts +0 -0
- /package/{src/expect.d.ts → expect.d.ts} +0 -0
- /package/{src/fixtures → fixtures}/fixture-types.d.ts +0 -0
- /package/{src/fixtures → fixtures}/index.d.ts +0 -0
- /package/{src/fixtures → fixtures}/test-fixture.d.ts +0 -0
- /package/{src/http-mock → http-mock}/http-mock.d.ts +0 -0
- /package/{src/http-mock → http-mock}/http-mock.types.d.ts +0 -0
- /package/{src/http-mock → http-mock}/index.d.ts +0 -0
- /package/{src/index.d.ts → index.d.ts} +0 -0
- /package/{src/interceptor → interceptor}/index.d.ts +0 -0
- /package/{src/interceptor → interceptor}/interceptor-chain.d.ts +0 -0
- /package/{src/interceptor → interceptor}/interceptor.types.d.ts +0 -0
- /package/{src/interceptor → interceptor}/mock-registry.d.ts +0 -0
- /package/{src/matchers → matchers}/index.d.ts +0 -0
- /package/{src/matchers → matchers}/matcher-types.d.ts +0 -0
- /package/{src/matchers → matchers}/mcp-matchers.d.ts +0 -0
- /package/{src/platform → platform}/index.d.ts +0 -0
- /package/{src/platform → platform}/platform-client-info.d.ts +0 -0
- /package/{src/platform → platform}/platform-types.d.ts +0 -0
- /package/{src/playwright → playwright}/index.d.ts +0 -0
- /package/{src/server → server}/index.d.ts +0 -0
- /package/{src/server → server}/test-server.d.ts +0 -0
- /package/{src/setup.d.ts → setup.d.ts} +0 -0
- /package/{src/transport → transport}/index.d.ts +0 -0
- /package/{src/transport → transport}/streamable-http.transport.d.ts +0 -0
- /package/{src/transport → transport}/transport.interface.d.ts +0 -0
- /package/{src/ui → ui}/index.d.ts +0 -0
- /package/{src/ui → ui}/ui-assertions.d.ts +0 -0
- /package/{src/ui → ui}/ui-matchers.d.ts +0 -0
|
@@ -1,544 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* @file http-mock.ts
|
|
4
|
-
* @description HTTP request mocking implementation for offline MCP server testing
|
|
5
|
-
*
|
|
6
|
-
* This module intercepts fetch() and XMLHttpRequest calls, allowing tools to be
|
|
7
|
-
* tested without making real HTTP requests.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```typescript
|
|
11
|
-
* import { httpMock } from '@frontmcp/testing';
|
|
12
|
-
*
|
|
13
|
-
* // Enable HTTP mocking
|
|
14
|
-
* const interceptor = httpMock.interceptor();
|
|
15
|
-
*
|
|
16
|
-
* // Mock a GET request
|
|
17
|
-
* interceptor.get('https://api.example.com/users', {
|
|
18
|
-
* body: [{ id: 1, name: 'John' }],
|
|
19
|
-
* });
|
|
20
|
-
*
|
|
21
|
-
* // Mock a POST request with pattern matching
|
|
22
|
-
* interceptor.post(/api\.example\.com\/users/, {
|
|
23
|
-
* status: 201,
|
|
24
|
-
* body: { id: 2, name: 'Jane' },
|
|
25
|
-
* });
|
|
26
|
-
*
|
|
27
|
-
* // Now run your MCP server test - HTTP calls will be intercepted
|
|
28
|
-
* const result = await mcp.tools.call('fetch-users', {});
|
|
29
|
-
*
|
|
30
|
-
* // Verify all mocks were used
|
|
31
|
-
* interceptor.assertDone();
|
|
32
|
-
*
|
|
33
|
-
* // Restore original fetch
|
|
34
|
-
* interceptor.restore();
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
-
exports.httpResponse = exports.httpMock = void 0;
|
|
39
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
40
|
-
// GLOBAL STATE
|
|
41
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
42
|
-
/** Original fetch function */
|
|
43
|
-
let originalFetch = null;
|
|
44
|
-
/** Whether mocking is enabled */
|
|
45
|
-
let mockingEnabled = false;
|
|
46
|
-
/** All active interceptors */
|
|
47
|
-
const activeInterceptors = [];
|
|
48
|
-
class HttpInterceptorImpl {
|
|
49
|
-
mocks = [];
|
|
50
|
-
_allowPassthrough = false;
|
|
51
|
-
_isActive = true;
|
|
52
|
-
mock(definition) {
|
|
53
|
-
const entry = {
|
|
54
|
-
definition,
|
|
55
|
-
callCount: 0,
|
|
56
|
-
calls: [],
|
|
57
|
-
remainingUses: definition.times ?? Infinity,
|
|
58
|
-
};
|
|
59
|
-
this.mocks.push(entry);
|
|
60
|
-
return {
|
|
61
|
-
remove: () => {
|
|
62
|
-
const index = this.mocks.indexOf(entry);
|
|
63
|
-
if (index !== -1) {
|
|
64
|
-
this.mocks.splice(index, 1);
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
callCount: () => entry.callCount,
|
|
68
|
-
calls: () => [...entry.calls],
|
|
69
|
-
waitForCalls: async (count, timeoutMs = 5000) => {
|
|
70
|
-
const deadline = Date.now() + timeoutMs;
|
|
71
|
-
while (Date.now() < deadline) {
|
|
72
|
-
if (entry.callCount >= count) {
|
|
73
|
-
return entry.calls.slice(0, count);
|
|
74
|
-
}
|
|
75
|
-
await sleep(50);
|
|
76
|
-
}
|
|
77
|
-
throw new Error(`Timeout waiting for ${count} calls, got ${entry.callCount}`);
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
get(url, response) {
|
|
82
|
-
return this.mock({
|
|
83
|
-
match: { url, method: 'GET' },
|
|
84
|
-
response: normalizeResponse(response),
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
post(url, response) {
|
|
88
|
-
return this.mock({
|
|
89
|
-
match: { url, method: 'POST' },
|
|
90
|
-
response: normalizeResponse(response),
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
put(url, response) {
|
|
94
|
-
return this.mock({
|
|
95
|
-
match: { url, method: 'PUT' },
|
|
96
|
-
response: normalizeResponse(response),
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
delete(url, response) {
|
|
100
|
-
return this.mock({
|
|
101
|
-
match: { url, method: 'DELETE' },
|
|
102
|
-
response: normalizeResponse(response),
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
any(url, response) {
|
|
106
|
-
return this.mock({
|
|
107
|
-
match: { url },
|
|
108
|
-
response: normalizeResponse(response),
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
clear() {
|
|
112
|
-
this.mocks = [];
|
|
113
|
-
}
|
|
114
|
-
pending() {
|
|
115
|
-
return this.mocks.filter((m) => m.remainingUses > 0).map((m) => m.definition);
|
|
116
|
-
}
|
|
117
|
-
isDone() {
|
|
118
|
-
return this.mocks.every((m) => m.remainingUses <= 0 || m.definition.times === undefined);
|
|
119
|
-
}
|
|
120
|
-
assertDone() {
|
|
121
|
-
const pending = this.pending().filter((m) => m.times !== undefined);
|
|
122
|
-
if (pending.length > 0) {
|
|
123
|
-
const descriptions = pending.map((m) => {
|
|
124
|
-
const url = m.match.url instanceof RegExp ? m.match.url.toString() : m.match.url;
|
|
125
|
-
return ` - ${m.match.method ?? 'ANY'} ${url}`;
|
|
126
|
-
});
|
|
127
|
-
throw new Error(`Unused HTTP mocks:\n${descriptions.join('\n')}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
allowPassthrough(allow) {
|
|
131
|
-
this._allowPassthrough = allow;
|
|
132
|
-
}
|
|
133
|
-
restore() {
|
|
134
|
-
this._isActive = false;
|
|
135
|
-
const index = activeInterceptors.indexOf(this);
|
|
136
|
-
if (index !== -1) {
|
|
137
|
-
activeInterceptors.splice(index, 1);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
// Internal methods
|
|
141
|
-
isActive() {
|
|
142
|
-
return this._isActive;
|
|
143
|
-
}
|
|
144
|
-
canPassthrough() {
|
|
145
|
-
return this._allowPassthrough;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Try to match a request against mocks in this scope
|
|
149
|
-
*/
|
|
150
|
-
async matchRequest(info) {
|
|
151
|
-
if (!this._isActive)
|
|
152
|
-
return null;
|
|
153
|
-
for (const entry of this.mocks) {
|
|
154
|
-
if (entry.remainingUses <= 0)
|
|
155
|
-
continue;
|
|
156
|
-
if (this.requestMatches(info, entry.definition.match)) {
|
|
157
|
-
// Found a match
|
|
158
|
-
entry.callCount++;
|
|
159
|
-
entry.calls.push(info);
|
|
160
|
-
entry.remainingUses--;
|
|
161
|
-
// Get response
|
|
162
|
-
let mockResponse;
|
|
163
|
-
if (typeof entry.definition.response === 'function') {
|
|
164
|
-
mockResponse = await entry.definition.response(info);
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
mockResponse = entry.definition.response;
|
|
168
|
-
}
|
|
169
|
-
// Apply delay
|
|
170
|
-
if (mockResponse.delay && mockResponse.delay > 0) {
|
|
171
|
-
await sleep(mockResponse.delay);
|
|
172
|
-
}
|
|
173
|
-
return createMockResponse(mockResponse);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
requestMatches(info, matcher) {
|
|
179
|
-
// Check URL
|
|
180
|
-
if (!this.urlMatches(info.url, matcher.url)) {
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
// Check method
|
|
184
|
-
if (matcher.method) {
|
|
185
|
-
const methods = Array.isArray(matcher.method) ? matcher.method : [matcher.method];
|
|
186
|
-
if (!methods.includes(info.method)) {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
// Check headers
|
|
191
|
-
if (matcher.headers) {
|
|
192
|
-
for (const [key, expected] of Object.entries(matcher.headers)) {
|
|
193
|
-
const actual = info.headers[key.toLowerCase()];
|
|
194
|
-
if (!actual)
|
|
195
|
-
return false;
|
|
196
|
-
if (expected instanceof RegExp) {
|
|
197
|
-
if (!expected.test(actual))
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
else if (actual !== expected) {
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
// Check body
|
|
206
|
-
if (matcher.body !== undefined) {
|
|
207
|
-
if (!this.bodyMatches(info.body, matcher.body)) {
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return true;
|
|
212
|
-
}
|
|
213
|
-
urlMatches(url, matcher) {
|
|
214
|
-
if (typeof matcher === 'string') {
|
|
215
|
-
// Exact match or contains
|
|
216
|
-
return url === matcher || url.includes(matcher);
|
|
217
|
-
}
|
|
218
|
-
else if (matcher instanceof RegExp) {
|
|
219
|
-
return matcher.test(url);
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
return matcher(url);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
bodyMatches(actual, expected) {
|
|
226
|
-
if (typeof expected === 'function') {
|
|
227
|
-
return expected(actual);
|
|
228
|
-
}
|
|
229
|
-
if (typeof expected === 'string') {
|
|
230
|
-
const actualStr = typeof actual === 'string' ? actual : JSON.stringify(actual);
|
|
231
|
-
return actualStr === expected || actualStr.includes(expected);
|
|
232
|
-
}
|
|
233
|
-
if (expected instanceof RegExp) {
|
|
234
|
-
const actualStr = typeof actual === 'string' ? actual : JSON.stringify(actual);
|
|
235
|
-
return expected.test(actualStr);
|
|
236
|
-
}
|
|
237
|
-
// Object comparison - check that all expected keys match
|
|
238
|
-
if (typeof actual !== 'object' || actual === null) {
|
|
239
|
-
return false;
|
|
240
|
-
}
|
|
241
|
-
for (const [key, value] of Object.entries(expected)) {
|
|
242
|
-
const actualValue = actual[key];
|
|
243
|
-
if (typeof value === 'object' && value !== null) {
|
|
244
|
-
if (!this.bodyMatches(actualValue, value)) {
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
else if (actualValue !== value) {
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return true;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
256
|
-
// FETCH INTERCEPTOR
|
|
257
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
258
|
-
async function interceptedFetch(input, init) {
|
|
259
|
-
// Build request info
|
|
260
|
-
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
|
|
261
|
-
const method = (init?.method ?? 'GET').toUpperCase();
|
|
262
|
-
// Parse headers
|
|
263
|
-
const headers = {};
|
|
264
|
-
if (init?.headers) {
|
|
265
|
-
if (init.headers instanceof Headers) {
|
|
266
|
-
init.headers.forEach((value, key) => {
|
|
267
|
-
headers[key.toLowerCase()] = value;
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
else if (Array.isArray(init.headers)) {
|
|
271
|
-
for (const [key, value] of init.headers) {
|
|
272
|
-
headers[key.toLowerCase()] = value;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
else {
|
|
276
|
-
for (const [key, value] of Object.entries(init.headers)) {
|
|
277
|
-
headers[key.toLowerCase()] = value;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
// Parse body
|
|
282
|
-
let body;
|
|
283
|
-
let rawBody;
|
|
284
|
-
if (init?.body) {
|
|
285
|
-
if (typeof init.body === 'string') {
|
|
286
|
-
rawBody = init.body;
|
|
287
|
-
try {
|
|
288
|
-
body = JSON.parse(init.body);
|
|
289
|
-
}
|
|
290
|
-
catch {
|
|
291
|
-
body = init.body;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
else if (init.body instanceof ArrayBuffer || init.body instanceof Uint8Array) {
|
|
295
|
-
rawBody = new TextDecoder().decode(init.body);
|
|
296
|
-
try {
|
|
297
|
-
body = JSON.parse(rawBody);
|
|
298
|
-
}
|
|
299
|
-
catch {
|
|
300
|
-
body = rawBody;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
body = init.body;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
const requestInfo = {
|
|
308
|
-
url,
|
|
309
|
-
method,
|
|
310
|
-
headers,
|
|
311
|
-
body,
|
|
312
|
-
rawBody,
|
|
313
|
-
};
|
|
314
|
-
// Try to match against active interceptors (in reverse order - last added first)
|
|
315
|
-
for (let i = activeInterceptors.length - 1; i >= 0; i--) {
|
|
316
|
-
const interceptor = activeInterceptors[i];
|
|
317
|
-
if (!interceptor.isActive())
|
|
318
|
-
continue;
|
|
319
|
-
const mockResponse = await interceptor.matchRequest(requestInfo);
|
|
320
|
-
if (mockResponse) {
|
|
321
|
-
return mockResponse;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
// No match found - check if passthrough is allowed
|
|
325
|
-
for (const interceptor of activeInterceptors) {
|
|
326
|
-
if (interceptor.isActive() && interceptor.canPassthrough()) {
|
|
327
|
-
// Passthrough to real fetch
|
|
328
|
-
if (originalFetch) {
|
|
329
|
-
return originalFetch(input, init);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
// No match and no passthrough - throw error
|
|
334
|
-
throw new Error(`No HTTP mock found for ${method} ${url}\n` +
|
|
335
|
-
`Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`);
|
|
336
|
-
}
|
|
337
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
338
|
-
// HELPERS
|
|
339
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
340
|
-
function normalizeResponse(response) {
|
|
341
|
-
// If it looks like a plain object (response body), wrap it
|
|
342
|
-
if (!('status' in response) && !('body' in response) && !('headers' in response)) {
|
|
343
|
-
return { body: response };
|
|
344
|
-
}
|
|
345
|
-
return response;
|
|
346
|
-
}
|
|
347
|
-
function createMockResponse(mock) {
|
|
348
|
-
// Check for network error - throw instead of returning Response
|
|
349
|
-
// This simulates real network failures where fetch rejects
|
|
350
|
-
const mockAny = mock;
|
|
351
|
-
if (mockAny._throwError) {
|
|
352
|
-
const body = mock.body;
|
|
353
|
-
const message = body && typeof body === 'object' && 'message' in body ? String(body['message']) : 'Network error';
|
|
354
|
-
throw new TypeError(`fetch failed: ${message}`);
|
|
355
|
-
}
|
|
356
|
-
const status = mock.status ?? 200;
|
|
357
|
-
const statusText = mock.statusText ?? 'OK';
|
|
358
|
-
// Build headers
|
|
359
|
-
const headers = new Headers(mock.headers ?? {});
|
|
360
|
-
// Build body
|
|
361
|
-
let body = null;
|
|
362
|
-
if (mock.body !== undefined) {
|
|
363
|
-
if (typeof mock.body === 'string') {
|
|
364
|
-
body = mock.body;
|
|
365
|
-
if (!headers.has('content-type')) {
|
|
366
|
-
headers.set('content-type', 'text/plain');
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
else if (mock.body instanceof ArrayBuffer) {
|
|
370
|
-
body = mock.body;
|
|
371
|
-
}
|
|
372
|
-
else if (Buffer.isBuffer(mock.body)) {
|
|
373
|
-
// Convert Buffer to ArrayBuffer for Response compatibility
|
|
374
|
-
body = new Uint8Array(mock.body).buffer;
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
body = JSON.stringify(mock.body);
|
|
378
|
-
if (!headers.has('content-type')) {
|
|
379
|
-
headers.set('content-type', 'application/json');
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
return new Response(body, { status, statusText, headers });
|
|
384
|
-
}
|
|
385
|
-
function sleep(ms) {
|
|
386
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
387
|
-
}
|
|
388
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
389
|
-
// MANAGER IMPLEMENTATION
|
|
390
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
391
|
-
class HttpMockManagerImpl {
|
|
392
|
-
interceptor() {
|
|
393
|
-
// Auto-enable mocking when creating an interceptor
|
|
394
|
-
if (!mockingEnabled) {
|
|
395
|
-
this.enable();
|
|
396
|
-
}
|
|
397
|
-
const interceptor = new HttpInterceptorImpl();
|
|
398
|
-
activeInterceptors.push(interceptor);
|
|
399
|
-
return interceptor;
|
|
400
|
-
}
|
|
401
|
-
enable() {
|
|
402
|
-
if (mockingEnabled)
|
|
403
|
-
return;
|
|
404
|
-
// Store original fetch
|
|
405
|
-
if (typeof globalThis.fetch === 'function') {
|
|
406
|
-
originalFetch = globalThis.fetch;
|
|
407
|
-
globalThis.fetch = interceptedFetch;
|
|
408
|
-
}
|
|
409
|
-
mockingEnabled = true;
|
|
410
|
-
}
|
|
411
|
-
disable() {
|
|
412
|
-
if (!mockingEnabled)
|
|
413
|
-
return;
|
|
414
|
-
// Restore original fetch
|
|
415
|
-
if (originalFetch) {
|
|
416
|
-
globalThis.fetch = originalFetch;
|
|
417
|
-
originalFetch = null;
|
|
418
|
-
}
|
|
419
|
-
// Clear all interceptors
|
|
420
|
-
activeInterceptors.length = 0;
|
|
421
|
-
mockingEnabled = false;
|
|
422
|
-
}
|
|
423
|
-
isEnabled() {
|
|
424
|
-
return mockingEnabled;
|
|
425
|
-
}
|
|
426
|
-
clearAll() {
|
|
427
|
-
for (const interceptor of activeInterceptors) {
|
|
428
|
-
interceptor.clear();
|
|
429
|
-
}
|
|
430
|
-
activeInterceptors.length = 0;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
434
|
-
// EXPORTS
|
|
435
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
436
|
-
/**
|
|
437
|
-
* Global HTTP mock manager
|
|
438
|
-
*
|
|
439
|
-
* @example
|
|
440
|
-
* ```typescript
|
|
441
|
-
* import { httpMock } from '@frontmcp/testing';
|
|
442
|
-
*
|
|
443
|
-
* // Create an HTTP interceptor
|
|
444
|
-
* const interceptor = httpMock.interceptor();
|
|
445
|
-
*
|
|
446
|
-
* // Mock requests
|
|
447
|
-
* interceptor.get('https://api.example.com/data', { id: 1, name: 'Test' });
|
|
448
|
-
*
|
|
449
|
-
* // Run tests...
|
|
450
|
-
*
|
|
451
|
-
* // Clean up
|
|
452
|
-
* interceptor.restore();
|
|
453
|
-
* ```
|
|
454
|
-
*/
|
|
455
|
-
exports.httpMock = new HttpMockManagerImpl();
|
|
456
|
-
/**
|
|
457
|
-
* Helper to create mock responses
|
|
458
|
-
*/
|
|
459
|
-
exports.httpResponse = {
|
|
460
|
-
/** Create a JSON response */
|
|
461
|
-
json(data, status = 200) {
|
|
462
|
-
return {
|
|
463
|
-
status,
|
|
464
|
-
headers: { 'content-type': 'application/json' },
|
|
465
|
-
body: data,
|
|
466
|
-
};
|
|
467
|
-
},
|
|
468
|
-
/** Create a text response */
|
|
469
|
-
text(data, status = 200) {
|
|
470
|
-
return {
|
|
471
|
-
status,
|
|
472
|
-
headers: { 'content-type': 'text/plain' },
|
|
473
|
-
body: data,
|
|
474
|
-
};
|
|
475
|
-
},
|
|
476
|
-
/** Create an HTML response */
|
|
477
|
-
html(data, status = 200) {
|
|
478
|
-
return {
|
|
479
|
-
status,
|
|
480
|
-
headers: { 'content-type': 'text/html' },
|
|
481
|
-
body: data,
|
|
482
|
-
};
|
|
483
|
-
},
|
|
484
|
-
/** Create an error response */
|
|
485
|
-
error(status, message) {
|
|
486
|
-
return {
|
|
487
|
-
status,
|
|
488
|
-
statusText: message ?? getStatusText(status),
|
|
489
|
-
body: message ? { error: message } : undefined,
|
|
490
|
-
};
|
|
491
|
-
},
|
|
492
|
-
/** Create a 404 Not Found response */
|
|
493
|
-
notFound(message = 'Not Found') {
|
|
494
|
-
return exports.httpResponse.error(404, message);
|
|
495
|
-
},
|
|
496
|
-
/** Create a 500 Internal Server Error response */
|
|
497
|
-
serverError(message = 'Internal Server Error') {
|
|
498
|
-
return exports.httpResponse.error(500, message);
|
|
499
|
-
},
|
|
500
|
-
/** Create a 401 Unauthorized response */
|
|
501
|
-
unauthorized(message = 'Unauthorized') {
|
|
502
|
-
return exports.httpResponse.error(401, message);
|
|
503
|
-
},
|
|
504
|
-
/** Create a 403 Forbidden response */
|
|
505
|
-
forbidden(message = 'Forbidden') {
|
|
506
|
-
return exports.httpResponse.error(403, message);
|
|
507
|
-
},
|
|
508
|
-
/**
|
|
509
|
-
* Create a network error that causes fetch to reject
|
|
510
|
-
* This simulates real network failures where fetch throws instead of returning a Response
|
|
511
|
-
*/
|
|
512
|
-
networkError(message = 'Network error') {
|
|
513
|
-
return {
|
|
514
|
-
status: 0,
|
|
515
|
-
body: { _networkError: true, message },
|
|
516
|
-
// Mark this as a network error for the interceptor to throw
|
|
517
|
-
_throwError: true,
|
|
518
|
-
};
|
|
519
|
-
},
|
|
520
|
-
/** Create a delayed response */
|
|
521
|
-
delayed(data, delayMs, status = 200) {
|
|
522
|
-
return {
|
|
523
|
-
status,
|
|
524
|
-
body: data,
|
|
525
|
-
delay: delayMs,
|
|
526
|
-
};
|
|
527
|
-
},
|
|
528
|
-
};
|
|
529
|
-
function getStatusText(status) {
|
|
530
|
-
const texts = {
|
|
531
|
-
200: 'OK',
|
|
532
|
-
201: 'Created',
|
|
533
|
-
204: 'No Content',
|
|
534
|
-
400: 'Bad Request',
|
|
535
|
-
401: 'Unauthorized',
|
|
536
|
-
403: 'Forbidden',
|
|
537
|
-
404: 'Not Found',
|
|
538
|
-
500: 'Internal Server Error',
|
|
539
|
-
502: 'Bad Gateway',
|
|
540
|
-
503: 'Service Unavailable',
|
|
541
|
-
};
|
|
542
|
-
return texts[status] ?? 'Unknown';
|
|
543
|
-
}
|
|
544
|
-
//# sourceMappingURL=http-mock.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"http-mock.js","sourceRoot":"","sources":["../../../src/http-mock/http-mock.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @file http-mock.ts\n * @description HTTP request mocking implementation for offline MCP server testing\n *\n * This module intercepts fetch() and XMLHttpRequest calls, allowing tools to be\n * tested without making real HTTP requests.\n *\n * @example\n * ```typescript\n * import { httpMock } from '@frontmcp/testing';\n *\n * // Enable HTTP mocking\n * const interceptor = httpMock.interceptor();\n *\n * // Mock a GET request\n * interceptor.get('https://api.example.com/users', {\n * body: [{ id: 1, name: 'John' }],\n * });\n *\n * // Mock a POST request with pattern matching\n * interceptor.post(/api\\.example\\.com\\/users/, {\n * status: 201,\n * body: { id: 2, name: 'Jane' },\n * });\n *\n * // Now run your MCP server test - HTTP calls will be intercepted\n * const result = await mcp.tools.call('fetch-users', {});\n *\n * // Verify all mocks were used\n * interceptor.assertDone();\n *\n * // Restore original fetch\n * interceptor.restore();\n * ```\n */\n\nimport type {\n HttpMethod,\n HttpRequestMatcher,\n HttpMockResponse,\n HttpMockDefinition,\n HttpRequestInfo,\n HttpMockHandle,\n HttpInterceptor,\n HttpMockManager,\n} from './http-mock.types';\n\n// ═══════════════════════════════════════════════════════════════════\n// GLOBAL STATE\n// ═══════════════════════════════════════════════════════════════════\n\n/** Original fetch function */\nlet originalFetch: typeof globalThis.fetch | null = null;\n\n/** Whether mocking is enabled */\nlet mockingEnabled = false;\n\n/** All active interceptors */\nconst activeInterceptors: HttpInterceptorImpl[] = [];\n\n// ═══════════════════════════════════════════════════════════════════\n// HTTP INTERCEPTOR IMPLEMENTATION\n// ═══════════════════════════════════════════════════════════════════\n\ninterface MockEntry {\n definition: HttpMockDefinition;\n callCount: number;\n calls: HttpRequestInfo[];\n remainingUses: number;\n}\n\nclass HttpInterceptorImpl implements HttpInterceptor {\n private mocks: MockEntry[] = [];\n private _allowPassthrough = false;\n private _isActive = true;\n\n mock(definition: HttpMockDefinition): HttpMockHandle {\n const entry: MockEntry = {\n definition,\n callCount: 0,\n calls: [],\n remainingUses: definition.times ?? Infinity,\n };\n\n this.mocks.push(entry);\n\n return {\n remove: () => {\n const index = this.mocks.indexOf(entry);\n if (index !== -1) {\n this.mocks.splice(index, 1);\n }\n },\n callCount: () => entry.callCount,\n calls: () => [...entry.calls],\n waitForCalls: async (count: number, timeoutMs = 5000) => {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (entry.callCount >= count) {\n return entry.calls.slice(0, count);\n }\n await sleep(50);\n }\n throw new Error(`Timeout waiting for ${count} calls, got ${entry.callCount}`);\n },\n };\n }\n\n get(url: string | RegExp, response: HttpMockResponse | Record<string, unknown>): HttpMockHandle {\n return this.mock({\n match: { url, method: 'GET' },\n response: normalizeResponse(response),\n });\n }\n\n post(url: string | RegExp, response: HttpMockResponse | Record<string, unknown>): HttpMockHandle {\n return this.mock({\n match: { url, method: 'POST' },\n response: normalizeResponse(response),\n });\n }\n\n put(url: string | RegExp, response: HttpMockResponse | Record<string, unknown>): HttpMockHandle {\n return this.mock({\n match: { url, method: 'PUT' },\n response: normalizeResponse(response),\n });\n }\n\n delete(url: string | RegExp, response: HttpMockResponse | Record<string, unknown>): HttpMockHandle {\n return this.mock({\n match: { url, method: 'DELETE' },\n response: normalizeResponse(response),\n });\n }\n\n any(url: string | RegExp, response: HttpMockResponse | Record<string, unknown>): HttpMockHandle {\n return this.mock({\n match: { url },\n response: normalizeResponse(response),\n });\n }\n\n clear(): void {\n this.mocks = [];\n }\n\n pending(): HttpMockDefinition[] {\n return this.mocks.filter((m) => m.remainingUses > 0).map((m) => m.definition);\n }\n\n isDone(): boolean {\n return this.mocks.every((m) => m.remainingUses <= 0 || m.definition.times === undefined);\n }\n\n assertDone(): void {\n const pending = this.pending().filter((m) => m.times !== undefined);\n if (pending.length > 0) {\n const descriptions = pending.map((m) => {\n const url = m.match.url instanceof RegExp ? m.match.url.toString() : m.match.url;\n return ` - ${m.match.method ?? 'ANY'} ${url}`;\n });\n throw new Error(`Unused HTTP mocks:\\n${descriptions.join('\\n')}`);\n }\n }\n\n allowPassthrough(allow: boolean): void {\n this._allowPassthrough = allow;\n }\n\n restore(): void {\n this._isActive = false;\n const index = activeInterceptors.indexOf(this);\n if (index !== -1) {\n activeInterceptors.splice(index, 1);\n }\n }\n\n // Internal methods\n\n isActive(): boolean {\n return this._isActive;\n }\n\n canPassthrough(): boolean {\n return this._allowPassthrough;\n }\n\n /**\n * Try to match a request against mocks in this scope\n */\n async matchRequest(info: HttpRequestInfo): Promise<Response | null> {\n if (!this._isActive) return null;\n\n for (const entry of this.mocks) {\n if (entry.remainingUses <= 0) continue;\n\n if (this.requestMatches(info, entry.definition.match)) {\n // Found a match\n entry.callCount++;\n entry.calls.push(info);\n entry.remainingUses--;\n\n // Get response\n let mockResponse: HttpMockResponse;\n if (typeof entry.definition.response === 'function') {\n mockResponse = await entry.definition.response(info);\n } else {\n mockResponse = entry.definition.response;\n }\n\n // Apply delay\n if (mockResponse.delay && mockResponse.delay > 0) {\n await sleep(mockResponse.delay);\n }\n\n return createMockResponse(mockResponse);\n }\n }\n\n return null;\n }\n\n private requestMatches(info: HttpRequestInfo, matcher: HttpRequestMatcher): boolean {\n // Check URL\n if (!this.urlMatches(info.url, matcher.url)) {\n return false;\n }\n\n // Check method\n if (matcher.method) {\n const methods = Array.isArray(matcher.method) ? matcher.method : [matcher.method];\n if (!methods.includes(info.method)) {\n return false;\n }\n }\n\n // Check headers\n if (matcher.headers) {\n for (const [key, expected] of Object.entries(matcher.headers)) {\n const actual = info.headers[key.toLowerCase()];\n if (!actual) return false;\n if (expected instanceof RegExp) {\n if (!expected.test(actual)) return false;\n } else if (actual !== expected) {\n return false;\n }\n }\n }\n\n // Check body\n if (matcher.body !== undefined) {\n if (!this.bodyMatches(info.body, matcher.body)) {\n return false;\n }\n }\n\n return true;\n }\n\n private urlMatches(url: string, matcher: string | RegExp | ((url: string) => boolean)): boolean {\n if (typeof matcher === 'string') {\n // Exact match or contains\n return url === matcher || url.includes(matcher);\n } else if (matcher instanceof RegExp) {\n return matcher.test(url);\n } else {\n return matcher(url);\n }\n }\n\n private bodyMatches(\n actual: unknown,\n expected: string | RegExp | Record<string, unknown> | ((body: unknown) => boolean),\n ): boolean {\n if (typeof expected === 'function') {\n return expected(actual);\n }\n\n if (typeof expected === 'string') {\n const actualStr = typeof actual === 'string' ? actual : JSON.stringify(actual);\n return actualStr === expected || actualStr.includes(expected);\n }\n\n if (expected instanceof RegExp) {\n const actualStr = typeof actual === 'string' ? actual : JSON.stringify(actual);\n return expected.test(actualStr);\n }\n\n // Object comparison - check that all expected keys match\n if (typeof actual !== 'object' || actual === null) {\n return false;\n }\n\n for (const [key, value] of Object.entries(expected)) {\n const actualValue = (actual as Record<string, unknown>)[key];\n if (typeof value === 'object' && value !== null) {\n if (!this.bodyMatches(actualValue, value as Record<string, unknown>)) {\n return false;\n }\n } else if (actualValue !== value) {\n return false;\n }\n }\n\n return true;\n }\n}\n\n// ═══════════════════════════════════════════════════════════════════\n// FETCH INTERCEPTOR\n// ═══════════════════════════════════════════════════════════════════\n\nasync function interceptedFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n // Build request info\n const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;\n const method = ((init?.method ?? 'GET') as HttpMethod).toUpperCase() as HttpMethod;\n\n // Parse headers\n const headers: Record<string, string> = {};\n if (init?.headers) {\n if (init.headers instanceof Headers) {\n init.headers.forEach((value, key) => {\n headers[key.toLowerCase()] = value;\n });\n } else if (Array.isArray(init.headers)) {\n for (const [key, value] of init.headers) {\n headers[key.toLowerCase()] = value;\n }\n } else {\n for (const [key, value] of Object.entries(init.headers)) {\n headers[key.toLowerCase()] = value;\n }\n }\n }\n\n // Parse body\n let body: unknown;\n let rawBody: string | undefined;\n if (init?.body) {\n if (typeof init.body === 'string') {\n rawBody = init.body;\n try {\n body = JSON.parse(init.body);\n } catch {\n body = init.body;\n }\n } else if (init.body instanceof ArrayBuffer || init.body instanceof Uint8Array) {\n rawBody = new TextDecoder().decode(init.body);\n try {\n body = JSON.parse(rawBody);\n } catch {\n body = rawBody;\n }\n } else {\n body = init.body;\n }\n }\n\n const requestInfo: HttpRequestInfo = {\n url,\n method,\n headers,\n body,\n rawBody,\n };\n\n // Try to match against active interceptors (in reverse order - last added first)\n for (let i = activeInterceptors.length - 1; i >= 0; i--) {\n const interceptor = activeInterceptors[i];\n if (!interceptor.isActive()) continue;\n\n const mockResponse = await interceptor.matchRequest(requestInfo);\n if (mockResponse) {\n return mockResponse;\n }\n }\n\n // No match found - check if passthrough is allowed\n for (const interceptor of activeInterceptors) {\n if (interceptor.isActive() && interceptor.canPassthrough()) {\n // Passthrough to real fetch\n if (originalFetch) {\n return originalFetch(input, init);\n }\n }\n }\n\n // No match and no passthrough - throw error\n throw new Error(\n `No HTTP mock found for ${method} ${url}\\n` +\n `Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`,\n );\n}\n\n// ═══════════════════════════════════════════════════════════════════\n// HELPERS\n// ═══════════════════════════════════════════════════════════════════\n\nfunction normalizeResponse(response: HttpMockResponse | Record<string, unknown>): HttpMockResponse {\n // If it looks like a plain object (response body), wrap it\n if (!('status' in response) && !('body' in response) && !('headers' in response)) {\n return { body: response as Record<string, unknown> };\n }\n return response as HttpMockResponse;\n}\n\nfunction createMockResponse(mock: HttpMockResponse): Response {\n // Check for network error - throw instead of returning Response\n // This simulates real network failures where fetch rejects\n const mockAny = mock as HttpMockResponse & { _throwError?: boolean };\n if (mockAny._throwError) {\n const body = mock.body as Record<string, unknown> | undefined;\n const message = body && typeof body === 'object' && 'message' in body ? String(body['message']) : 'Network error';\n throw new TypeError(`fetch failed: ${message}`);\n }\n\n const status = mock.status ?? 200;\n const statusText = mock.statusText ?? 'OK';\n\n // Build headers\n const headers = new Headers(mock.headers ?? {});\n\n // Build body\n let body: BodyInit | null = null;\n if (mock.body !== undefined) {\n if (typeof mock.body === 'string') {\n body = mock.body;\n if (!headers.has('content-type')) {\n headers.set('content-type', 'text/plain');\n }\n } else if (mock.body instanceof ArrayBuffer) {\n body = mock.body;\n } else if (Buffer.isBuffer(mock.body)) {\n // Convert Buffer to ArrayBuffer for Response compatibility\n body = new Uint8Array(mock.body).buffer;\n } else {\n body = JSON.stringify(mock.body);\n if (!headers.has('content-type')) {\n headers.set('content-type', 'application/json');\n }\n }\n }\n\n return new Response(body, { status, statusText, headers });\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// ═══════════════════════════════════════════════════════════════════\n// MANAGER IMPLEMENTATION\n// ═══════════════════════════════════════════════════════════════════\n\nclass HttpMockManagerImpl implements HttpMockManager {\n interceptor(): HttpInterceptor {\n // Auto-enable mocking when creating an interceptor\n if (!mockingEnabled) {\n this.enable();\n }\n\n const interceptor = new HttpInterceptorImpl();\n activeInterceptors.push(interceptor);\n return interceptor;\n }\n\n enable(): void {\n if (mockingEnabled) return;\n\n // Store original fetch\n if (typeof globalThis.fetch === 'function') {\n originalFetch = globalThis.fetch;\n globalThis.fetch = interceptedFetch;\n }\n\n mockingEnabled = true;\n }\n\n disable(): void {\n if (!mockingEnabled) return;\n\n // Restore original fetch\n if (originalFetch) {\n globalThis.fetch = originalFetch;\n originalFetch = null;\n }\n\n // Clear all interceptors\n activeInterceptors.length = 0;\n mockingEnabled = false;\n }\n\n isEnabled(): boolean {\n return mockingEnabled;\n }\n\n clearAll(): void {\n for (const interceptor of activeInterceptors) {\n interceptor.clear();\n }\n activeInterceptors.length = 0;\n }\n}\n\n// ═══════════════════════════════════════════════════════════════════\n// EXPORTS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Global HTTP mock manager\n *\n * @example\n * ```typescript\n * import { httpMock } from '@frontmcp/testing';\n *\n * // Create an HTTP interceptor\n * const interceptor = httpMock.interceptor();\n *\n * // Mock requests\n * interceptor.get('https://api.example.com/data', { id: 1, name: 'Test' });\n *\n * // Run tests...\n *\n * // Clean up\n * interceptor.restore();\n * ```\n */\nexport const httpMock: HttpMockManager = new HttpMockManagerImpl();\n\n/**\n * Helper to create mock responses\n */\nexport const httpResponse = {\n /** Create a JSON response */\n json<T>(data: T, status = 200): HttpMockResponse {\n return {\n status,\n headers: { 'content-type': 'application/json' },\n body: data as Record<string, unknown>,\n };\n },\n\n /** Create a text response */\n text(data: string, status = 200): HttpMockResponse {\n return {\n status,\n headers: { 'content-type': 'text/plain' },\n body: data,\n };\n },\n\n /** Create an HTML response */\n html(data: string, status = 200): HttpMockResponse {\n return {\n status,\n headers: { 'content-type': 'text/html' },\n body: data,\n };\n },\n\n /** Create an error response */\n error(status: number, message?: string): HttpMockResponse {\n return {\n status,\n statusText: message ?? getStatusText(status),\n body: message ? { error: message } : undefined,\n };\n },\n\n /** Create a 404 Not Found response */\n notFound(message = 'Not Found'): HttpMockResponse {\n return httpResponse.error(404, message);\n },\n\n /** Create a 500 Internal Server Error response */\n serverError(message = 'Internal Server Error'): HttpMockResponse {\n return httpResponse.error(500, message);\n },\n\n /** Create a 401 Unauthorized response */\n unauthorized(message = 'Unauthorized'): HttpMockResponse {\n return httpResponse.error(401, message);\n },\n\n /** Create a 403 Forbidden response */\n forbidden(message = 'Forbidden'): HttpMockResponse {\n return httpResponse.error(403, message);\n },\n\n /**\n * Create a network error that causes fetch to reject\n * This simulates real network failures where fetch throws instead of returning a Response\n */\n networkError(message = 'Network error'): HttpMockResponse {\n return {\n status: 0,\n body: { _networkError: true, message },\n // Mark this as a network error for the interceptor to throw\n _throwError: true,\n } as HttpMockResponse;\n },\n\n /** Create a delayed response */\n delayed<T>(data: T, delayMs: number, status = 200): HttpMockResponse {\n return {\n status,\n body: data as Record<string, unknown>,\n delay: delayMs,\n };\n },\n};\n\nfunction getStatusText(status: number): string {\n const texts: Record<number, string> = {\n 200: 'OK',\n 201: 'Created',\n 204: 'No Content',\n 400: 'Bad Request',\n 401: 'Unauthorized',\n 403: 'Forbidden',\n 404: 'Not Found',\n 500: 'Internal Server Error',\n 502: 'Bad Gateway',\n 503: 'Service Unavailable',\n };\n return texts[status] ?? 'Unknown';\n}\n"]}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* @file http-mock.types.ts
|
|
4
|
-
* @description Types for HTTP request mocking (fetch/XHR interception)
|
|
5
|
-
*
|
|
6
|
-
* This module allows mocking HTTP requests made by tools during MCP server testing,
|
|
7
|
-
* enabling fully offline testing of MCP servers.
|
|
8
|
-
*/
|
|
9
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
//# sourceMappingURL=http-mock.types.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"http-mock.types.js","sourceRoot":"","sources":["../../../src/http-mock/http-mock.types.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG","sourcesContent":["/**\n * @file http-mock.types.ts\n * @description Types for HTTP request mocking (fetch/XHR interception)\n *\n * This module allows mocking HTTP requests made by tools during MCP server testing,\n * enabling fully offline testing of MCP servers.\n */\n\n/**\n * HTTP method types\n */\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';\n\n/**\n * Request matcher - defines which requests to intercept\n */\nexport interface HttpRequestMatcher {\n /** URL pattern - string for exact match, RegExp for pattern, or function for custom */\n url: string | RegExp | ((url: string) => boolean);\n /** HTTP method(s) to match (default: all methods) */\n method?: HttpMethod | HttpMethod[];\n /** Headers that must be present (partial match) */\n headers?: Record<string, string | RegExp>;\n /** Body matcher for POST/PUT/PATCH */\n body?: string | RegExp | Record<string, unknown> | ((body: unknown) => boolean);\n}\n\n/**\n * Mocked HTTP response\n */\nexport interface HttpMockResponse {\n /** HTTP status code (default: 200) */\n status?: number;\n /** Status text (default: 'OK') */\n statusText?: string;\n /** Response headers */\n headers?: Record<string, string>;\n /** Response body - string, object/array (will be JSON.stringify'd), or Buffer */\n body?: string | Record<string, unknown> | unknown[] | Buffer | ArrayBuffer;\n /** Delay before responding in ms */\n delay?: number;\n}\n\n/**\n * HTTP mock definition\n */\nexport interface HttpMockDefinition {\n /** Request matcher */\n match: HttpRequestMatcher;\n /** Response to return */\n response: HttpMockResponse | ((request: HttpRequestInfo) => HttpMockResponse | Promise<HttpMockResponse>);\n /** Number of times to use this mock (default: Infinity) */\n times?: number;\n /** Name for debugging/tracking */\n name?: string;\n}\n\n/**\n * Information about an intercepted HTTP request\n */\nexport interface HttpRequestInfo {\n /** Full URL */\n url: string;\n /** HTTP method */\n method: HttpMethod;\n /** Request headers */\n headers: Record<string, string>;\n /** Request body (parsed if JSON) */\n body?: unknown;\n /** Raw body string */\n rawBody?: string;\n}\n\n/**\n * Handle returned when adding an HTTP mock\n */\nexport interface HttpMockHandle {\n /** Remove this mock */\n remove(): void;\n /** Get call count */\n callCount(): number;\n /** Get all intercepted requests */\n calls(): HttpRequestInfo[];\n /** Wait for a specific number of calls */\n waitForCalls(count: number, timeoutMs?: number): Promise<HttpRequestInfo[]>;\n}\n\n/**\n * HTTP interceptor - controls when mocks are active\n */\nexport interface HttpInterceptor {\n /** Add an HTTP mock */\n mock(definition: HttpMockDefinition): HttpMockHandle;\n\n /** Convenience: mock a GET request */\n get(url: string | RegExp, response: HttpMockResponse | Record<string, unknown>): HttpMockHandle;\n\n /** Convenience: mock a POST request */\n post(url: string | RegExp, response: HttpMockResponse | Record<string, unknown>): HttpMockHandle;\n\n /** Convenience: mock a PUT request */\n put(url: string | RegExp, response: HttpMockResponse | Record<string, unknown>): HttpMockHandle;\n\n /** Convenience: mock a DELETE request */\n delete(url: string | RegExp, response: HttpMockResponse | Record<string, unknown>): HttpMockHandle;\n\n /** Convenience: mock any method */\n any(url: string | RegExp, response: HttpMockResponse | Record<string, unknown>): HttpMockHandle;\n\n /** Clear all mocks in this scope */\n clear(): void;\n\n /** Get all pending (unused) mocks */\n pending(): HttpMockDefinition[];\n\n /** Check if all mocks were used */\n isDone(): boolean;\n\n /** Assert all mocks were used (throws if not) */\n assertDone(): void;\n\n /** Enable passthrough for unmatched requests (default: false, throws error) */\n allowPassthrough(allow: boolean): void;\n\n /** Restore original fetch/XHR */\n restore(): void;\n}\n\n/**\n * Global HTTP mock manager\n */\nexport interface HttpMockManager {\n /** Create a new HTTP interceptor */\n interceptor(): HttpInterceptor;\n\n /** Enable HTTP mocking globally */\n enable(): void;\n\n /** Disable HTTP mocking and restore originals */\n disable(): void;\n\n /** Check if mocking is enabled */\n isEnabled(): boolean;\n\n /** Clear all scopes and mocks */\n clearAll(): void;\n}\n"]}
|
package/src/http-mock/index.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* @file index.ts
|
|
4
|
-
* @description Barrel exports for HTTP mock module
|
|
5
|
-
*/
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.httpResponse = exports.httpMock = void 0;
|
|
8
|
-
var http_mock_1 = require("./http-mock");
|
|
9
|
-
Object.defineProperty(exports, "httpMock", { enumerable: true, get: function () { return http_mock_1.httpMock; } });
|
|
10
|
-
Object.defineProperty(exports, "httpResponse", { enumerable: true, get: function () { return http_mock_1.httpResponse; } });
|
|
11
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/http-mock/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,yCAAqD;AAA5C,qGAAA,QAAQ,OAAA;AAAE,yGAAA,YAAY,OAAA","sourcesContent":["/**\n * @file index.ts\n * @description Barrel exports for HTTP mock module\n */\n\nexport { httpMock, httpResponse } from './http-mock';\n\nexport type {\n HttpMethod,\n HttpRequestMatcher,\n HttpMockResponse,\n HttpMockDefinition,\n HttpRequestInfo,\n HttpMockHandle,\n HttpInterceptor,\n HttpMockManager,\n} from './http-mock.types';\n"]}
|