@frontmcp/testing 0.5.1 → 0.6.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/package.json +4 -4
- package/src/auth/mock-api-server.d.ts +99 -0
- package/src/auth/mock-api-server.js +200 -0
- package/src/auth/mock-api-server.js.map +1 -0
- package/src/auth/mock-oauth-server.d.ts +85 -0
- package/src/auth/mock-oauth-server.js +253 -0
- package/src/auth/mock-oauth-server.js.map +1 -0
- package/src/client/mcp-test-client.builder.d.ts +43 -1
- package/src/client/mcp-test-client.builder.js +52 -0
- package/src/client/mcp-test-client.builder.js.map +1 -1
- package/src/client/mcp-test-client.js +22 -14
- package/src/client/mcp-test-client.js.map +1 -1
- package/src/client/mcp-test-client.types.d.ts +67 -6
- package/src/client/mcp-test-client.types.js +9 -0
- package/src/client/mcp-test-client.types.js.map +1 -1
- package/src/example-tools/index.d.ts +19 -0
- package/src/example-tools/index.js +40 -0
- package/src/example-tools/index.js.map +1 -0
- package/src/example-tools/tool-configs.d.ts +170 -0
- package/src/example-tools/tool-configs.js +222 -0
- package/src/example-tools/tool-configs.js.map +1 -0
- package/src/expect.d.ts +6 -5
- package/src/expect.js.map +1 -1
- package/src/fixtures/fixture-types.d.ts +19 -0
- package/src/fixtures/fixture-types.js.map +1 -1
- package/src/fixtures/test-fixture.d.ts +3 -1
- package/src/fixtures/test-fixture.js +35 -4
- package/src/fixtures/test-fixture.js.map +1 -1
- package/src/index.d.ts +7 -0
- package/src/index.js +40 -1
- package/src/index.js.map +1 -1
- package/src/matchers/matcher-types.js.map +1 -1
- package/src/matchers/mcp-matchers.d.ts +7 -0
- package/src/matchers/mcp-matchers.js +8 -4
- package/src/matchers/mcp-matchers.js.map +1 -1
- package/src/platform/index.d.ts +28 -0
- package/src/platform/index.js +47 -0
- package/src/platform/index.js.map +1 -0
- package/src/platform/platform-client-info.d.ts +97 -0
- package/src/platform/platform-client-info.js +155 -0
- package/src/platform/platform-client-info.js.map +1 -0
- package/src/platform/platform-types.d.ts +72 -0
- package/src/platform/platform-types.js +110 -0
- package/src/platform/platform-types.js.map +1 -0
- package/src/server/test-server.d.ts +4 -0
- package/src/server/test-server.js +58 -3
- package/src/server/test-server.js.map +1 -1
- package/src/transport/streamable-http.transport.js +6 -0
- package/src/transport/streamable-http.transport.js.map +1 -1
- package/src/transport/transport.interface.d.ts +3 -0
- package/src/transport/transport.interface.js.map +1 -1
- package/src/ui/ui-assertions.d.ts +59 -0
- package/src/ui/ui-assertions.js +152 -0
- package/src/ui/ui-assertions.js.map +1 -1
- package/src/ui/ui-matchers.d.ts +8 -0
- package/src/ui/ui-matchers.js +218 -0
- package/src/ui/ui-matchers.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "E2E testing framework for FrontMCP servers - MCP client, auth mocks, Playwright integration",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"homepage": "https://docs.agentfront.dev",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@frontmcp/sdk": "0.
|
|
62
|
-
"@frontmcp/ui": "0.
|
|
61
|
+
"@frontmcp/sdk": "0.6.1",
|
|
62
|
+
"@frontmcp/ui": "0.6.1",
|
|
63
63
|
"@playwright/test": "^1.40.0",
|
|
64
64
|
"jest": "^29.0.0",
|
|
65
65
|
"@jest/globals": "^29.0.0"
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"node": ">=22.0.0"
|
|
83
83
|
},
|
|
84
84
|
"dependencies": {
|
|
85
|
-
"@modelcontextprotocol/sdk": "1.
|
|
85
|
+
"@modelcontextprotocol/sdk": "1.25.1",
|
|
86
86
|
"jose": "^6.0.11",
|
|
87
87
|
"tslib": "^2.3.0"
|
|
88
88
|
},
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file mock-api-server.ts
|
|
3
|
+
* @description Mock API server for testing OpenAPI adapter
|
|
4
|
+
*
|
|
5
|
+
* This module provides a mock HTTP server that serves:
|
|
6
|
+
* - OpenAPI spec endpoint for adapter initialization
|
|
7
|
+
* - Mock API responses for generated tools
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { MockAPIServer } from '@frontmcp/testing';
|
|
12
|
+
*
|
|
13
|
+
* const apiServer = new MockAPIServer({
|
|
14
|
+
* openApiSpec: { openapi: '3.0.0', ... },
|
|
15
|
+
* routes: [
|
|
16
|
+
* { method: 'GET', path: '/products', response: { body: [...] } },
|
|
17
|
+
* ],
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* // Start the mock server
|
|
21
|
+
* const info = await apiServer.start();
|
|
22
|
+
*
|
|
23
|
+
* // Configure your MCP server to use this mock
|
|
24
|
+
* // OPENAPI_BASE_URL = info.baseUrl
|
|
25
|
+
*
|
|
26
|
+
* // Stop when done
|
|
27
|
+
* await apiServer.stop();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export interface MockRoute {
|
|
31
|
+
/** HTTP method */
|
|
32
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
33
|
+
/** Path to match (e.g., '/products', '/products/:id') */
|
|
34
|
+
path: string;
|
|
35
|
+
/** Response to return */
|
|
36
|
+
response: MockResponse;
|
|
37
|
+
}
|
|
38
|
+
export interface MockResponse {
|
|
39
|
+
/** HTTP status code (default: 200) */
|
|
40
|
+
status?: number;
|
|
41
|
+
/** Response headers */
|
|
42
|
+
headers?: Record<string, string>;
|
|
43
|
+
/** Response body (will be JSON serialized) */
|
|
44
|
+
body: unknown;
|
|
45
|
+
}
|
|
46
|
+
export interface MockAPIServerOptions {
|
|
47
|
+
/** Port to listen on (default: random available port) */
|
|
48
|
+
port?: number;
|
|
49
|
+
/** OpenAPI spec to serve at /openapi.json */
|
|
50
|
+
openApiSpec: unknown;
|
|
51
|
+
/** Routes to mock */
|
|
52
|
+
routes?: MockRoute[];
|
|
53
|
+
/** Enable debug logging */
|
|
54
|
+
debug?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface MockAPIServerInfo {
|
|
57
|
+
/** Base URL of the server */
|
|
58
|
+
baseUrl: string;
|
|
59
|
+
/** Port the server is listening on */
|
|
60
|
+
port: number;
|
|
61
|
+
/** OpenAPI spec URL */
|
|
62
|
+
specUrl: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Mock API server for testing OpenAPI adapter
|
|
66
|
+
*
|
|
67
|
+
* Serves an OpenAPI spec and mock API responses so that MCP servers
|
|
68
|
+
* can use the OpenAPI adapter without connecting to a real API.
|
|
69
|
+
*/
|
|
70
|
+
export declare class MockAPIServer {
|
|
71
|
+
private readonly options;
|
|
72
|
+
private server;
|
|
73
|
+
private _info;
|
|
74
|
+
private routes;
|
|
75
|
+
constructor(options: MockAPIServerOptions);
|
|
76
|
+
/**
|
|
77
|
+
* Start the mock API server
|
|
78
|
+
*/
|
|
79
|
+
start(): Promise<MockAPIServerInfo>;
|
|
80
|
+
/**
|
|
81
|
+
* Stop the mock API server
|
|
82
|
+
*/
|
|
83
|
+
stop(): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Get server info
|
|
86
|
+
*/
|
|
87
|
+
get info(): MockAPIServerInfo;
|
|
88
|
+
/**
|
|
89
|
+
* Add a route dynamically
|
|
90
|
+
*/
|
|
91
|
+
addRoute(route: MockRoute): void;
|
|
92
|
+
/**
|
|
93
|
+
* Clear all routes
|
|
94
|
+
*/
|
|
95
|
+
clearRoutes(): void;
|
|
96
|
+
private handleRequest;
|
|
97
|
+
private findRoute;
|
|
98
|
+
private log;
|
|
99
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @file mock-api-server.ts
|
|
4
|
+
* @description Mock API server for testing OpenAPI adapter
|
|
5
|
+
*
|
|
6
|
+
* This module provides a mock HTTP server that serves:
|
|
7
|
+
* - OpenAPI spec endpoint for adapter initialization
|
|
8
|
+
* - Mock API responses for generated tools
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { MockAPIServer } from '@frontmcp/testing';
|
|
13
|
+
*
|
|
14
|
+
* const apiServer = new MockAPIServer({
|
|
15
|
+
* openApiSpec: { openapi: '3.0.0', ... },
|
|
16
|
+
* routes: [
|
|
17
|
+
* { method: 'GET', path: '/products', response: { body: [...] } },
|
|
18
|
+
* ],
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Start the mock server
|
|
22
|
+
* const info = await apiServer.start();
|
|
23
|
+
*
|
|
24
|
+
* // Configure your MCP server to use this mock
|
|
25
|
+
* // OPENAPI_BASE_URL = info.baseUrl
|
|
26
|
+
*
|
|
27
|
+
* // Stop when done
|
|
28
|
+
* await apiServer.stop();
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.MockAPIServer = void 0;
|
|
33
|
+
const http_1 = require("http");
|
|
34
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
35
|
+
// MOCK API SERVER
|
|
36
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
37
|
+
/**
|
|
38
|
+
* Mock API server for testing OpenAPI adapter
|
|
39
|
+
*
|
|
40
|
+
* Serves an OpenAPI spec and mock API responses so that MCP servers
|
|
41
|
+
* can use the OpenAPI adapter without connecting to a real API.
|
|
42
|
+
*/
|
|
43
|
+
class MockAPIServer {
|
|
44
|
+
options;
|
|
45
|
+
server = null;
|
|
46
|
+
_info = null;
|
|
47
|
+
routes;
|
|
48
|
+
constructor(options) {
|
|
49
|
+
this.options = options;
|
|
50
|
+
this.routes = options.routes ?? [];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Start the mock API server
|
|
54
|
+
*/
|
|
55
|
+
async start() {
|
|
56
|
+
if (this.server) {
|
|
57
|
+
throw new Error('Mock API server is already running');
|
|
58
|
+
}
|
|
59
|
+
const port = this.options.port ?? 0; // 0 = random available port
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
this.server = (0, http_1.createServer)(this.handleRequest.bind(this));
|
|
62
|
+
this.server.on('error', (err) => {
|
|
63
|
+
this.log(`Server error: ${err.message}`);
|
|
64
|
+
reject(err);
|
|
65
|
+
});
|
|
66
|
+
this.server.listen(port, () => {
|
|
67
|
+
const address = this.server.address();
|
|
68
|
+
if (!address || typeof address === 'string') {
|
|
69
|
+
reject(new Error('Failed to get server address'));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const actualPort = address.port;
|
|
73
|
+
this._info = {
|
|
74
|
+
baseUrl: `http://localhost:${actualPort}`,
|
|
75
|
+
port: actualPort,
|
|
76
|
+
specUrl: `http://localhost:${actualPort}/openapi.json`,
|
|
77
|
+
};
|
|
78
|
+
this.log(`Mock API server started at ${this._info.baseUrl}`);
|
|
79
|
+
resolve(this._info);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Stop the mock API server
|
|
85
|
+
*/
|
|
86
|
+
async stop() {
|
|
87
|
+
if (!this.server) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
this.server.close((err) => {
|
|
92
|
+
if (err) {
|
|
93
|
+
reject(err);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
this.server = null;
|
|
97
|
+
this._info = null;
|
|
98
|
+
this.log('Mock API server stopped');
|
|
99
|
+
resolve();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get server info
|
|
106
|
+
*/
|
|
107
|
+
get info() {
|
|
108
|
+
if (!this._info) {
|
|
109
|
+
throw new Error('Mock API server is not running');
|
|
110
|
+
}
|
|
111
|
+
return this._info;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Add a route dynamically
|
|
115
|
+
*/
|
|
116
|
+
addRoute(route) {
|
|
117
|
+
this.routes.push(route);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Clear all routes
|
|
121
|
+
*/
|
|
122
|
+
clearRoutes() {
|
|
123
|
+
this.routes = [];
|
|
124
|
+
}
|
|
125
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
126
|
+
// PRIVATE
|
|
127
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
128
|
+
async handleRequest(req, res) {
|
|
129
|
+
const url = req.url ?? '/';
|
|
130
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
131
|
+
this.log(`${method} ${url}`);
|
|
132
|
+
// CORS headers
|
|
133
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
134
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
|
|
135
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
136
|
+
if (method === 'OPTIONS') {
|
|
137
|
+
res.writeHead(204);
|
|
138
|
+
res.end();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
// Serve OpenAPI spec
|
|
143
|
+
if (url === '/openapi.json' || url === '/openapi.yaml') {
|
|
144
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
145
|
+
res.end(JSON.stringify(this.options.openApiSpec));
|
|
146
|
+
this.log('Served OpenAPI spec');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Find matching route
|
|
150
|
+
const route = this.findRoute(method, url);
|
|
151
|
+
if (route) {
|
|
152
|
+
const status = route.response.status ?? 200;
|
|
153
|
+
const headers = {
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
...route.response.headers,
|
|
156
|
+
};
|
|
157
|
+
res.writeHead(status, headers);
|
|
158
|
+
res.end(JSON.stringify(route.response.body));
|
|
159
|
+
this.log(`Matched route: ${method} ${route.path} -> ${status}`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// No matching route
|
|
163
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
164
|
+
res.end(JSON.stringify({ error: 'not_found', message: `No mock for ${method} ${url}` }));
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
this.log(`Error handling request: ${error}`);
|
|
168
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
169
|
+
res.end(JSON.stringify({ error: 'server_error', message: 'Internal server error' }));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
findRoute(method, url) {
|
|
173
|
+
// Strip query string
|
|
174
|
+
const path = url.split('?')[0];
|
|
175
|
+
return this.routes.find((route) => {
|
|
176
|
+
if (route.method !== method)
|
|
177
|
+
return false;
|
|
178
|
+
// Simple path matching (exact match or path params)
|
|
179
|
+
if (route.path === path)
|
|
180
|
+
return true;
|
|
181
|
+
// Handle path parameters like /products/:id
|
|
182
|
+
const routeParts = route.path.split('/');
|
|
183
|
+
const urlParts = path.split('/');
|
|
184
|
+
if (routeParts.length !== urlParts.length)
|
|
185
|
+
return false;
|
|
186
|
+
return routeParts.every((part, i) => {
|
|
187
|
+
if (part.startsWith(':'))
|
|
188
|
+
return true; // Path parameter
|
|
189
|
+
return part === urlParts[i];
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
log(message) {
|
|
194
|
+
if (this.options.debug) {
|
|
195
|
+
console.log(`[MockAPIServer] ${message}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
exports.MockAPIServer = MockAPIServer;
|
|
200
|
+
//# sourceMappingURL=mock-api-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-api-server.js","sourceRoot":"","sources":["../../../src/auth/mock-api-server.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @file mock-api-server.ts\n * @description Mock API server for testing OpenAPI adapter\n *\n * This module provides a mock HTTP server that serves:\n * - OpenAPI spec endpoint for adapter initialization\n * - Mock API responses for generated tools\n *\n * @example\n * ```typescript\n * import { MockAPIServer } from '@frontmcp/testing';\n *\n * const apiServer = new MockAPIServer({\n * openApiSpec: { openapi: '3.0.0', ... },\n * routes: [\n * { method: 'GET', path: '/products', response: { body: [...] } },\n * ],\n * });\n *\n * // Start the mock server\n * const info = await apiServer.start();\n *\n * // Configure your MCP server to use this mock\n * // OPENAPI_BASE_URL = info.baseUrl\n *\n * // Stop when done\n * await apiServer.stop();\n * ```\n */\n\nimport { createServer, Server, IncomingMessage, ServerResponse } from 'http';\n\n// ═══════════════════════════════════════════════════════════════════\n// TYPES\n// ═══════════════════════════════════════════════════════════════════\n\nexport interface MockRoute {\n /** HTTP method */\n method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n /** Path to match (e.g., '/products', '/products/:id') */\n path: string;\n /** Response to return */\n response: MockResponse;\n}\n\nexport interface MockResponse {\n /** HTTP status code (default: 200) */\n status?: number;\n /** Response headers */\n headers?: Record<string, string>;\n /** Response body (will be JSON serialized) */\n body: unknown;\n}\n\nexport interface MockAPIServerOptions {\n /** Port to listen on (default: random available port) */\n port?: number;\n /** OpenAPI spec to serve at /openapi.json */\n openApiSpec: unknown;\n /** Routes to mock */\n routes?: MockRoute[];\n /** Enable debug logging */\n debug?: boolean;\n}\n\nexport interface MockAPIServerInfo {\n /** Base URL of the server */\n baseUrl: string;\n /** Port the server is listening on */\n port: number;\n /** OpenAPI spec URL */\n specUrl: string;\n}\n\n// ═══════════════════════════════════════════════════════════════════\n// MOCK API SERVER\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Mock API server for testing OpenAPI adapter\n *\n * Serves an OpenAPI spec and mock API responses so that MCP servers\n * can use the OpenAPI adapter without connecting to a real API.\n */\nexport class MockAPIServer {\n private readonly options: MockAPIServerOptions;\n private server: Server | null = null;\n private _info: MockAPIServerInfo | null = null;\n private routes: MockRoute[];\n\n constructor(options: MockAPIServerOptions) {\n this.options = options;\n this.routes = options.routes ?? [];\n }\n\n /**\n * Start the mock API server\n */\n async start(): Promise<MockAPIServerInfo> {\n if (this.server) {\n throw new Error('Mock API server is already running');\n }\n\n const port = this.options.port ?? 0; // 0 = random available port\n\n return new Promise((resolve, reject) => {\n this.server = createServer(this.handleRequest.bind(this));\n\n this.server.on('error', (err) => {\n this.log(`Server error: ${err.message}`);\n reject(err);\n });\n\n this.server.listen(port, () => {\n const address = this.server!.address();\n if (!address || typeof address === 'string') {\n reject(new Error('Failed to get server address'));\n return;\n }\n\n const actualPort = address.port;\n\n this._info = {\n baseUrl: `http://localhost:${actualPort}`,\n port: actualPort,\n specUrl: `http://localhost:${actualPort}/openapi.json`,\n };\n\n this.log(`Mock API server started at ${this._info.baseUrl}`);\n resolve(this._info);\n });\n });\n }\n\n /**\n * Stop the mock API server\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n\n return new Promise((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n reject(err);\n } else {\n this.server = null;\n this._info = null;\n this.log('Mock API server stopped');\n resolve();\n }\n });\n });\n }\n\n /**\n * Get server info\n */\n get info(): MockAPIServerInfo {\n if (!this._info) {\n throw new Error('Mock API server is not running');\n }\n return this._info;\n }\n\n /**\n * Add a route dynamically\n */\n addRoute(route: MockRoute): void {\n this.routes.push(route);\n }\n\n /**\n * Clear all routes\n */\n clearRoutes(): void {\n this.routes = [];\n }\n\n // ═══════════════════════════════════════════════════════════════════\n // PRIVATE\n // ═══════════════════════════════════════════════════════════════════\n\n private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const url = req.url ?? '/';\n const method = (req.method ?? 'GET').toUpperCase();\n this.log(`${method} ${url}`);\n\n // CORS headers\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');\n\n if (method === 'OPTIONS') {\n res.writeHead(204);\n res.end();\n return;\n }\n\n try {\n // Serve OpenAPI spec\n if (url === '/openapi.json' || url === '/openapi.yaml') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(this.options.openApiSpec));\n this.log('Served OpenAPI spec');\n return;\n }\n\n // Find matching route\n const route = this.findRoute(method, url);\n if (route) {\n const status = route.response.status ?? 200;\n const headers = {\n 'Content-Type': 'application/json',\n ...route.response.headers,\n };\n res.writeHead(status, headers);\n res.end(JSON.stringify(route.response.body));\n this.log(`Matched route: ${method} ${route.path} -> ${status}`);\n return;\n }\n\n // No matching route\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'not_found', message: `No mock for ${method} ${url}` }));\n } catch (error) {\n this.log(`Error handling request: ${error}`);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'server_error', message: 'Internal server error' }));\n }\n }\n\n private findRoute(method: string, url: string): MockRoute | undefined {\n // Strip query string\n const path = url.split('?')[0];\n\n return this.routes.find((route) => {\n if (route.method !== method) return false;\n\n // Simple path matching (exact match or path params)\n if (route.path === path) return true;\n\n // Handle path parameters like /products/:id\n const routeParts = route.path.split('/');\n const urlParts = path.split('/');\n\n if (routeParts.length !== urlParts.length) return false;\n\n return routeParts.every((part, i) => {\n if (part.startsWith(':')) return true; // Path parameter\n return part === urlParts[i];\n });\n });\n }\n\n private log(message: string): void {\n if (this.options.debug) {\n console.log(`[MockAPIServer] ${message}`);\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file mock-oauth-server.ts
|
|
3
|
+
* @description Mock OAuth server for testing transparent auth mode
|
|
4
|
+
*
|
|
5
|
+
* This module provides a mock OAuth/OIDC server that serves:
|
|
6
|
+
* - JWKS endpoint for token verification
|
|
7
|
+
* - OAuth metadata endpoint (optional)
|
|
8
|
+
* - Token endpoint for anonymous tokens (optional)
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { MockOAuthServer, TestTokenFactory } from '@frontmcp/testing';
|
|
13
|
+
*
|
|
14
|
+
* const tokenFactory = new TestTokenFactory();
|
|
15
|
+
* const oauthServer = new MockOAuthServer(tokenFactory);
|
|
16
|
+
*
|
|
17
|
+
* // Start the mock server
|
|
18
|
+
* await oauthServer.start();
|
|
19
|
+
*
|
|
20
|
+
* // Configure your MCP server to use this mock
|
|
21
|
+
* // IDP_PROVIDER_URL = oauthServer.baseUrl
|
|
22
|
+
*
|
|
23
|
+
* // Create tokens using the same factory
|
|
24
|
+
* const token = await tokenFactory.createTestToken({ sub: 'user-123' });
|
|
25
|
+
*
|
|
26
|
+
* // Stop when done
|
|
27
|
+
* await oauthServer.stop();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
import type { TestTokenFactory } from './token-factory';
|
|
31
|
+
export interface MockOAuthServerOptions {
|
|
32
|
+
/** Port to listen on (default: random available port) */
|
|
33
|
+
port?: number;
|
|
34
|
+
/** Issuer URL (default: http://localhost:{port}) */
|
|
35
|
+
issuer?: string;
|
|
36
|
+
/** Enable debug logging */
|
|
37
|
+
debug?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface MockOAuthServerInfo {
|
|
40
|
+
/** Base URL of the server */
|
|
41
|
+
baseUrl: string;
|
|
42
|
+
/** Port the server is listening on */
|
|
43
|
+
port: number;
|
|
44
|
+
/** Issuer URL */
|
|
45
|
+
issuer: string;
|
|
46
|
+
/** JWKS endpoint URL */
|
|
47
|
+
jwksUrl: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Mock OAuth/OIDC server for testing transparent auth mode
|
|
51
|
+
*
|
|
52
|
+
* Serves JWKS from a TestTokenFactory so that MCP servers can
|
|
53
|
+
* validate test tokens without connecting to a real IdP.
|
|
54
|
+
*/
|
|
55
|
+
export declare class MockOAuthServer {
|
|
56
|
+
private readonly tokenFactory;
|
|
57
|
+
private readonly options;
|
|
58
|
+
private server;
|
|
59
|
+
private _info;
|
|
60
|
+
private connections;
|
|
61
|
+
constructor(tokenFactory: TestTokenFactory, options?: MockOAuthServerOptions);
|
|
62
|
+
/**
|
|
63
|
+
* Start the mock OAuth server
|
|
64
|
+
*/
|
|
65
|
+
start(): Promise<MockOAuthServerInfo>;
|
|
66
|
+
/**
|
|
67
|
+
* Stop the mock OAuth server
|
|
68
|
+
*/
|
|
69
|
+
stop(): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Get server info
|
|
72
|
+
*/
|
|
73
|
+
get info(): MockOAuthServerInfo;
|
|
74
|
+
/**
|
|
75
|
+
* Get the token factory (for creating tokens)
|
|
76
|
+
*/
|
|
77
|
+
getTokenFactory(): TestTokenFactory;
|
|
78
|
+
private handleRequest;
|
|
79
|
+
private handleJwks;
|
|
80
|
+
private handleOidcConfig;
|
|
81
|
+
private handleOAuthMetadata;
|
|
82
|
+
private handleTokenEndpoint;
|
|
83
|
+
private readBody;
|
|
84
|
+
private log;
|
|
85
|
+
}
|