mmt-testlight 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/dist/cli.js +407 -0
- package/dist/cli.js.map +1 -0
- package/dist/loadTest.js +36 -0
- package/dist/loadTest.js.map +1 -0
- package/dist/mockRunner.js +189 -0
- package/dist/mockRunner.js.map +1 -0
- package/dist/runArgs.js +304 -0
- package/dist/runArgs.js.map +1 -0
- package/dist/runArgs.test.js +83 -0
- package/dist/runArgs.test.js.map +1 -0
- package/dist/runTest.js +187 -0
- package/dist/runTest.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist-cjs/cli.js +445 -0
- package/dist-cjs/cli.js.map +1 -0
- package/dist-cjs/loadTest.js +44 -0
- package/dist-cjs/loadTest.js.map +1 -0
- package/dist-cjs/mockRunner.js +229 -0
- package/dist-cjs/mockRunner.js.map +1 -0
- package/dist-cjs/runArgs.js +345 -0
- package/dist-cjs/runArgs.js.map +1 -0
- package/dist-cjs/types.js +3 -0
- package/dist-cjs/types.js.map +1 -0
- package/package.json +51 -0
- package/src/cli-bridge.mjs +41 -0
- package/src/cli.ts +450 -0
- package/src/loadTest.ts +38 -0
- package/src/mockRunner.ts +212 -0
- package/src/pkg-entry.cjs +1096 -0
- package/src/runArgs.ts +353 -0
- package/src/types.ts +11 -0
- package/tsconfig.cjs.json +13 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI mock server runner – starts HTTP/HTTPS mock servers from .mmt server files.
|
|
3
|
+
* Mirrors the functionality in src/mmtAPI/mockRunner.ts but without VS Code dependencies.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import http from 'http';
|
|
7
|
+
import https from 'https';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import yaml from 'js-yaml';
|
|
10
|
+
import * as mmtcore from 'mmt-core';
|
|
11
|
+
|
|
12
|
+
const {mockParsePack, mockServer, variableReplacer} = mmtcore;
|
|
13
|
+
|
|
14
|
+
/** Track active servers so we can clean them all up at exit. */
|
|
15
|
+
const activeServers = new Map<string, {server: http.Server | https.Server; port: number; dispose: () => void}>();
|
|
16
|
+
|
|
17
|
+
function resolveFilePath(relative: string, basePath: string): string {
|
|
18
|
+
if (path.isAbsolute(relative)) {
|
|
19
|
+
return relative;
|
|
20
|
+
}
|
|
21
|
+
return path.resolve(path.dirname(basePath), relative);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Start a mock server from a .mmt server file.
|
|
26
|
+
* Returns a cleanup function that stops the server.
|
|
27
|
+
*/
|
|
28
|
+
export async function startMockServerFromPath(
|
|
29
|
+
filePath: string,
|
|
30
|
+
envVars: Record<string, any> = {},
|
|
31
|
+
): Promise<() => void> {
|
|
32
|
+
// Stop existing server on this path if any
|
|
33
|
+
const existing = activeServers.get(filePath);
|
|
34
|
+
if (existing) {
|
|
35
|
+
existing.dispose();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
39
|
+
let parsed: any;
|
|
40
|
+
try {
|
|
41
|
+
parsed = yaml.load(rawContent);
|
|
42
|
+
} catch (err: any) {
|
|
43
|
+
throw new Error(`Mock server: YAML parse error in ${path.basename(filePath)}: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const {data, errors} = mockParsePack.parseMockData(parsed);
|
|
47
|
+
if (errors.length > 0 || !data) {
|
|
48
|
+
const msg = errors.map((e: any) => e.message).join('; ');
|
|
49
|
+
throw new Error(`Mock server validation errors in ${path.basename(filePath)}: ${msg}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if a server is already running on this port
|
|
53
|
+
for (const [, handle] of activeServers) {
|
|
54
|
+
if (handle.port === data.port) {
|
|
55
|
+
// Server already running on this port — return a no‑op cleanup
|
|
56
|
+
return () => {};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create token resolver
|
|
61
|
+
const tokenResolver = (value: any): any => {
|
|
62
|
+
variableReplacer.resetRandomTokenCache();
|
|
63
|
+
variableReplacer.resetCurrentTokenCache();
|
|
64
|
+
return variableReplacer.resolveEmbeddedTokens(value, envVars);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Resolve tokens in global headers
|
|
68
|
+
if (data.headers) {
|
|
69
|
+
for (const [k, v] of Object.entries(data.headers)) {
|
|
70
|
+
if (typeof v === 'string') {
|
|
71
|
+
(data.headers as Record<string, string>)[k] = String(variableReplacer.resolveEmbeddedTokens(v, envVars));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Build the router
|
|
77
|
+
const router = mockServer.createMockRouter(data, tokenResolver);
|
|
78
|
+
|
|
79
|
+
const requestHandler = (req: http.IncomingMessage, res: http.ServerResponse) => {
|
|
80
|
+
const method = (req.method || 'GET').toLowerCase();
|
|
81
|
+
const urlStr = req.url || '/';
|
|
82
|
+
|
|
83
|
+
// Handle CORS preflight
|
|
84
|
+
if (data.cors) {
|
|
85
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
86
|
+
res.setHeader('Access-Control-Allow-Methods', '*');
|
|
87
|
+
res.setHeader('Access-Control-Allow-Headers', '*');
|
|
88
|
+
if (method === 'options') {
|
|
89
|
+
res.statusCode = 204;
|
|
90
|
+
res.end();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let body = '';
|
|
96
|
+
req.on('data', (chunk: Buffer) => { body += chunk; });
|
|
97
|
+
req.on('end', async () => {
|
|
98
|
+
let pathname = urlStr;
|
|
99
|
+
const queryObj: Record<string, string> = {};
|
|
100
|
+
const qIdx = urlStr.indexOf('?');
|
|
101
|
+
if (qIdx >= 0) {
|
|
102
|
+
pathname = urlStr.slice(0, qIdx);
|
|
103
|
+
const searchParams = new URLSearchParams(urlStr.slice(qIdx + 1));
|
|
104
|
+
searchParams.forEach((v, k) => { queryObj[k] = v; });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let parsedBody: any;
|
|
108
|
+
try {
|
|
109
|
+
parsedBody = JSON.parse(body);
|
|
110
|
+
} catch {
|
|
111
|
+
parsedBody = body || undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const mockReq = {
|
|
115
|
+
method,
|
|
116
|
+
path: pathname,
|
|
117
|
+
headers: (req.headers || {}) as Record<string, string>,
|
|
118
|
+
query: queryObj,
|
|
119
|
+
body: parsedBody,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
let mockRes: ReturnType<typeof router>;
|
|
123
|
+
try {
|
|
124
|
+
mockRes = router(mockReq);
|
|
125
|
+
} catch (err: any) {
|
|
126
|
+
res.statusCode = 500;
|
|
127
|
+
res.end(JSON.stringify({error: 'Mock router error', message: err.message}));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Apply delay
|
|
132
|
+
if (mockRes.delay && mockRes.delay > 0) {
|
|
133
|
+
await new Promise<void>(resolve => setTimeout(resolve, mockRes.delay));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Resolve tokens in response headers per-request
|
|
137
|
+
if (mockRes.headers) {
|
|
138
|
+
for (const [k, v] of Object.entries(mockRes.headers)) {
|
|
139
|
+
if (typeof v === 'string') {
|
|
140
|
+
res.setHeader(k, String(variableReplacer.resolveEmbeddedTokens(v, envVars)));
|
|
141
|
+
} else {
|
|
142
|
+
res.setHeader(k, v);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
res.statusCode = mockRes.status;
|
|
148
|
+
const responseBody = mockRes.body !== undefined ? (
|
|
149
|
+
typeof mockRes.body === 'string' ? mockRes.body : JSON.stringify(mockRes.body)
|
|
150
|
+
) : '';
|
|
151
|
+
res.end(responseBody);
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Create server based on protocol
|
|
156
|
+
let server: http.Server | https.Server;
|
|
157
|
+
const protocol = data.protocol || 'http';
|
|
158
|
+
|
|
159
|
+
if (protocol === 'https' && data.tls) {
|
|
160
|
+
const certPath = resolveFilePath(data.tls.cert, filePath);
|
|
161
|
+
const keyPath = resolveFilePath(data.tls.key, filePath);
|
|
162
|
+
const tlsOptions: https.ServerOptions = {
|
|
163
|
+
cert: fs.readFileSync(certPath),
|
|
164
|
+
key: fs.readFileSync(keyPath),
|
|
165
|
+
};
|
|
166
|
+
if (data.tls.ca) {
|
|
167
|
+
tlsOptions.ca = fs.readFileSync(resolveFilePath(data.tls.ca, filePath));
|
|
168
|
+
}
|
|
169
|
+
if (data.tls.requestCert) {
|
|
170
|
+
tlsOptions.requestCert = true;
|
|
171
|
+
tlsOptions.rejectUnauthorized = false;
|
|
172
|
+
}
|
|
173
|
+
server = https.createServer(tlsOptions, requestHandler);
|
|
174
|
+
} else {
|
|
175
|
+
server = http.createServer(requestHandler);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return new Promise<() => void>((resolve, reject) => {
|
|
179
|
+
server.on('listening', () => {
|
|
180
|
+
const dispose = () => {
|
|
181
|
+
try {
|
|
182
|
+
server.close();
|
|
183
|
+
} catch {
|
|
184
|
+
// ignore
|
|
185
|
+
}
|
|
186
|
+
activeServers.delete(filePath);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
activeServers.set(filePath, {server, port: data.port, dispose});
|
|
190
|
+
resolve(dispose);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
server.on('error', (err: any) => {
|
|
194
|
+
activeServers.delete(filePath);
|
|
195
|
+
if (err.code === 'EADDRINUSE') {
|
|
196
|
+
reject(new Error(`Mock server: port ${data.port} is already in use.`));
|
|
197
|
+
} else {
|
|
198
|
+
reject(new Error(`Mock server error: ${err.message}`));
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
server.listen(data.port);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Stop all active mock servers. */
|
|
207
|
+
export function stopAllServers(): void {
|
|
208
|
+
for (const [, handle] of activeServers) {
|
|
209
|
+
handle.dispose();
|
|
210
|
+
}
|
|
211
|
+
activeServers.clear();
|
|
212
|
+
}
|