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.
@@ -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
+ }