test-proxy-recorder 0.1.3 → 0.1.4
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/dist/{index-DfFpm8mB.d.cts → index-De4mgziH.d.cts} +2 -1
- package/dist/{index-DfFpm8mB.d.ts → index-De4mgziH.d.ts} +2 -1
- package/dist/index.cjs +93 -12
- package/dist/index.d.cts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.mjs +92 -12
- package/dist/playwright/index.d.cts +1 -1
- package/dist/playwright/index.d.ts +1 -1
- package/dist/proxy.js +92 -12
- package/package.json +1 -1
|
@@ -28,6 +28,7 @@ interface Recording {
|
|
|
28
28
|
response?: RecordedResponse;
|
|
29
29
|
timestamp: string;
|
|
30
30
|
key: string;
|
|
31
|
+
sequence: number;
|
|
31
32
|
}
|
|
32
33
|
interface WebSocketMessage {
|
|
33
34
|
direction: 'client-to-server' | 'server-to-client';
|
|
@@ -53,7 +54,7 @@ type PlaywrightTestInfo = Pick<TestInfo, 'title'>;
|
|
|
53
54
|
* @param sessionId - Unique identifier for the session
|
|
54
55
|
* @param timeout - Optional timeout in milliseconds
|
|
55
56
|
*/
|
|
56
|
-
declare function setProxyMode(mode: Mode, sessionId
|
|
57
|
+
declare function setProxyMode(mode: Mode, sessionId?: string, timeout?: number): Promise<void>;
|
|
57
58
|
/**
|
|
58
59
|
* Generate a session ID from test info
|
|
59
60
|
* @param testInfo - Playwright test info object
|
|
@@ -28,6 +28,7 @@ interface Recording {
|
|
|
28
28
|
response?: RecordedResponse;
|
|
29
29
|
timestamp: string;
|
|
30
30
|
key: string;
|
|
31
|
+
sequence: number;
|
|
31
32
|
}
|
|
32
33
|
interface WebSocketMessage {
|
|
33
34
|
direction: 'client-to-server' | 'server-to-client';
|
|
@@ -53,7 +54,7 @@ type PlaywrightTestInfo = Pick<TestInfo, 'title'>;
|
|
|
53
54
|
* @param sessionId - Unique identifier for the session
|
|
54
55
|
* @param timeout - Optional timeout in milliseconds
|
|
55
56
|
*/
|
|
56
|
-
declare function setProxyMode(mode: Mode, sessionId
|
|
57
|
+
declare function setProxyMode(mode: Mode, sessionId?: string, timeout?: number): Promise<void>;
|
|
57
58
|
/**
|
|
58
59
|
* Generate a session ID from test info
|
|
59
60
|
* @param testInfo - Playwright test info object
|
package/dist/index.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var fs = require('fs/promises');
|
|
4
4
|
var http = require('http');
|
|
5
|
+
var https = require('https');
|
|
5
6
|
var httpProxy = require('http-proxy');
|
|
6
7
|
var ws = require('ws');
|
|
7
8
|
var path = require('path');
|
|
@@ -11,6 +12,7 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
11
12
|
|
|
12
13
|
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
13
14
|
var http__default = /*#__PURE__*/_interopDefault(http);
|
|
15
|
+
var https__default = /*#__PURE__*/_interopDefault(https);
|
|
14
16
|
var httpProxy__default = /*#__PURE__*/_interopDefault(httpProxy);
|
|
15
17
|
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
16
18
|
var filenamify__default = /*#__PURE__*/_interopDefault(filenamify);
|
|
@@ -93,6 +95,10 @@ var ProxyServer = class {
|
|
|
93
95
|
proxy;
|
|
94
96
|
currentSession;
|
|
95
97
|
recordingsDir;
|
|
98
|
+
requestSequenceMap;
|
|
99
|
+
// Track sequence per request key
|
|
100
|
+
replaySequenceMap;
|
|
101
|
+
// Track replay position per request key
|
|
96
102
|
constructor(targets, recordingsDir) {
|
|
97
103
|
this.targets = targets;
|
|
98
104
|
this.currentTargetIndex = 0;
|
|
@@ -102,6 +108,8 @@ var ProxyServer = class {
|
|
|
102
108
|
this.modeTimeout = null;
|
|
103
109
|
this.currentSession = null;
|
|
104
110
|
this.recordingsDir = recordingsDir;
|
|
111
|
+
this.requestSequenceMap = /* @__PURE__ */ new Map();
|
|
112
|
+
this.replaySequenceMap = /* @__PURE__ */ new Map();
|
|
105
113
|
this.proxy = httpProxy__default.default.createProxyServer({
|
|
106
114
|
secure: false,
|
|
107
115
|
changeOrigin: true
|
|
@@ -140,10 +148,19 @@ var ProxyServer = class {
|
|
|
140
148
|
res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
|
|
141
149
|
}
|
|
142
150
|
handleProxyResponse(proxyRes, req) {
|
|
151
|
+
this.addCorsHeaders(proxyRes, req);
|
|
143
152
|
if (this.mode === Modes.record && this.recordingId) {
|
|
144
153
|
this.recordResponse(req, proxyRes);
|
|
145
154
|
}
|
|
146
155
|
}
|
|
156
|
+
addCorsHeaders(proxyRes, req) {
|
|
157
|
+
const origin = req.headers.origin;
|
|
158
|
+
proxyRes.headers["access-control-allow-origin"] = origin || "*";
|
|
159
|
+
proxyRes.headers["access-control-allow-credentials"] = "true";
|
|
160
|
+
proxyRes.headers["access-control-allow-headers"] = req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization";
|
|
161
|
+
proxyRes.headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
|
|
162
|
+
proxyRes.headers["access-control-expose-headers"] = "*";
|
|
163
|
+
}
|
|
147
164
|
getTarget() {
|
|
148
165
|
const target = this.targets[this.currentTargetIndex];
|
|
149
166
|
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
@@ -218,6 +235,7 @@ var ProxyServer = class {
|
|
|
218
235
|
this.recordingId = id;
|
|
219
236
|
this.replayId = null;
|
|
220
237
|
this.currentSession = { id, recordings: [], websocketRecordings: [] };
|
|
238
|
+
this.requestSequenceMap.clear();
|
|
221
239
|
console.log(`Switched to record mode with ID: ${id}`);
|
|
222
240
|
}
|
|
223
241
|
switchToReplayMode(id) {
|
|
@@ -228,6 +246,7 @@ var ProxyServer = class {
|
|
|
228
246
|
this.replayId = id;
|
|
229
247
|
this.recordingId = null;
|
|
230
248
|
this.currentSession = null;
|
|
249
|
+
this.replaySequenceMap.clear();
|
|
231
250
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
232
251
|
}
|
|
233
252
|
setupModeTimeout(timeout) {
|
|
@@ -259,6 +278,8 @@ var ProxyServer = class {
|
|
|
259
278
|
return;
|
|
260
279
|
}
|
|
261
280
|
const key = getReqID(req);
|
|
281
|
+
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
282
|
+
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
262
283
|
const record = {
|
|
263
284
|
request: {
|
|
264
285
|
method: req.method,
|
|
@@ -267,7 +288,8 @@ var ProxyServer = class {
|
|
|
267
288
|
body: body || null
|
|
268
289
|
},
|
|
269
290
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
270
|
-
key
|
|
291
|
+
key,
|
|
292
|
+
sequence: currentSequence
|
|
271
293
|
};
|
|
272
294
|
this.currentSession.recordings.push(record);
|
|
273
295
|
}
|
|
@@ -276,7 +298,9 @@ var ProxyServer = class {
|
|
|
276
298
|
return;
|
|
277
299
|
}
|
|
278
300
|
const key = getReqID(req);
|
|
279
|
-
const record = this.currentSession.recordings.
|
|
301
|
+
const record = this.currentSession.recordings.findLast(
|
|
302
|
+
(r) => r.key === key && !r.response
|
|
303
|
+
);
|
|
280
304
|
if (!record) {
|
|
281
305
|
console.error("Request record not found for response:", key);
|
|
282
306
|
return;
|
|
@@ -301,17 +325,31 @@ var ProxyServer = class {
|
|
|
301
325
|
const filePath = getRecordingPath(this.recordingsDir, this.replayId);
|
|
302
326
|
try {
|
|
303
327
|
const session = await loadRecordingSession(filePath);
|
|
304
|
-
const
|
|
328
|
+
const currentSequence = this.replaySequenceMap.get(key) || 0;
|
|
329
|
+
const record = session.recordings.find(
|
|
330
|
+
(r) => r.key === key && r.sequence === currentSequence
|
|
331
|
+
);
|
|
305
332
|
if (!record) {
|
|
306
|
-
throw new Error(
|
|
333
|
+
throw new Error(
|
|
334
|
+
`No recording found for ${key} with sequence ${currentSequence}`
|
|
335
|
+
);
|
|
307
336
|
}
|
|
308
337
|
if (!record.response) {
|
|
309
338
|
throw new Error("No response recorded for this request");
|
|
310
339
|
}
|
|
340
|
+
this.replaySequenceMap.set(key, currentSequence + 1);
|
|
311
341
|
const { statusCode, headers, body } = record.response;
|
|
312
|
-
|
|
342
|
+
const origin = req.headers.origin;
|
|
343
|
+
const responseHeaders = {
|
|
344
|
+
...headers,
|
|
345
|
+
"access-control-allow-origin": origin || "*",
|
|
346
|
+
"access-control-allow-credentials": "true"
|
|
347
|
+
};
|
|
348
|
+
res.writeHead(statusCode, responseHeaders);
|
|
313
349
|
res.end(body);
|
|
314
|
-
console.log(
|
|
350
|
+
console.log(
|
|
351
|
+
`Replayed: ${req.method} ${req.url} (sequence: ${currentSequence})`
|
|
352
|
+
);
|
|
315
353
|
} catch (error) {
|
|
316
354
|
this.handleReplayError(res, error, key, filePath);
|
|
317
355
|
}
|
|
@@ -327,6 +365,9 @@ var ProxyServer = class {
|
|
|
327
365
|
});
|
|
328
366
|
}
|
|
329
367
|
async handleRequest(req, res) {
|
|
368
|
+
if (req.method === "OPTIONS") {
|
|
369
|
+
return this.handleCorsPreflightRequest(req, res);
|
|
370
|
+
}
|
|
330
371
|
if (req.url === CONTROL_ENDPOINT) {
|
|
331
372
|
return this.handleControlRequest(req, res);
|
|
332
373
|
}
|
|
@@ -335,23 +376,63 @@ var ProxyServer = class {
|
|
|
335
376
|
}
|
|
336
377
|
await this.handleProxyRequest(req, res);
|
|
337
378
|
}
|
|
379
|
+
handleCorsPreflightRequest(req, res) {
|
|
380
|
+
const origin = req.headers.origin;
|
|
381
|
+
res.writeHead(HTTP_STATUS_OK, {
|
|
382
|
+
"Access-Control-Allow-Origin": origin || "*",
|
|
383
|
+
"Access-Control-Allow-Credentials": "true",
|
|
384
|
+
"Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
385
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
386
|
+
"Access-Control-Max-Age": "86400"
|
|
387
|
+
// 24 hours
|
|
388
|
+
});
|
|
389
|
+
res.end();
|
|
390
|
+
}
|
|
338
391
|
async handleProxyRequest(req, res) {
|
|
339
392
|
const target = this.getTarget();
|
|
340
393
|
console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
|
|
341
394
|
if (this.mode === Modes.record) {
|
|
342
|
-
await this.
|
|
395
|
+
await this.bufferAndProxyRequest(req, res, target);
|
|
396
|
+
} else {
|
|
397
|
+
this.proxy.web(req, res, { target });
|
|
343
398
|
}
|
|
344
|
-
this.proxy.web(req, res, { target });
|
|
345
399
|
}
|
|
346
|
-
async
|
|
400
|
+
async bufferAndProxyRequest(req, res, target) {
|
|
347
401
|
const chunks = [];
|
|
348
402
|
req.on("data", (chunk) => {
|
|
349
403
|
chunks.push(chunk);
|
|
350
404
|
});
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
await this.saveRequestRecord(req, body);
|
|
405
|
+
await new Promise((resolve) => {
|
|
406
|
+
req.on("end", () => resolve());
|
|
354
407
|
});
|
|
408
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
409
|
+
await this.saveRequestRecord(req, body);
|
|
410
|
+
const targetUrl = new URL(target);
|
|
411
|
+
const isHttps = targetUrl.protocol === "https:";
|
|
412
|
+
const requestModule = isHttps ? https__default.default : http__default.default;
|
|
413
|
+
const defaultPort = isHttps ? 443 : 80;
|
|
414
|
+
const proxyReq = requestModule.request(
|
|
415
|
+
{
|
|
416
|
+
hostname: targetUrl.hostname,
|
|
417
|
+
port: targetUrl.port || defaultPort,
|
|
418
|
+
path: req.url,
|
|
419
|
+
method: req.method,
|
|
420
|
+
headers: req.headers
|
|
421
|
+
},
|
|
422
|
+
(proxyRes) => {
|
|
423
|
+
this.addCorsHeaders(proxyRes, req);
|
|
424
|
+
this.recordResponse(req, proxyRes);
|
|
425
|
+
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
426
|
+
proxyRes.pipe(res);
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
proxyReq.on("error", (err) => {
|
|
430
|
+
this.handleProxyError(err, req, res);
|
|
431
|
+
});
|
|
432
|
+
if (chunks.length > 0) {
|
|
433
|
+
proxyReq.write(Buffer.concat(chunks));
|
|
434
|
+
}
|
|
435
|
+
proxyReq.end();
|
|
355
436
|
}
|
|
356
437
|
handleUpgrade(req, socket, head) {
|
|
357
438
|
if (this.mode === Modes.replay) {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
-
export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-
|
|
2
|
+
export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-De4mgziH.cjs';
|
|
3
3
|
import '@playwright/test';
|
|
4
4
|
|
|
5
5
|
declare class ProxyServer {
|
|
@@ -12,12 +12,15 @@ declare class ProxyServer {
|
|
|
12
12
|
private proxy;
|
|
13
13
|
private currentSession;
|
|
14
14
|
private recordingsDir;
|
|
15
|
+
private requestSequenceMap;
|
|
16
|
+
private replaySequenceMap;
|
|
15
17
|
constructor(targets: string[], recordingsDir: string);
|
|
16
18
|
init(): Promise<void>;
|
|
17
19
|
listen(port: number): http.Server;
|
|
18
20
|
private setupProxyEventHandlers;
|
|
19
21
|
private handleProxyError;
|
|
20
22
|
private handleProxyResponse;
|
|
23
|
+
private addCorsHeaders;
|
|
21
24
|
private getTarget;
|
|
22
25
|
private handleControlRequest;
|
|
23
26
|
private clearModeTimeout;
|
|
@@ -32,8 +35,9 @@ declare class ProxyServer {
|
|
|
32
35
|
private handleReplayRequest;
|
|
33
36
|
private handleReplayError;
|
|
34
37
|
private handleRequest;
|
|
38
|
+
private handleCorsPreflightRequest;
|
|
35
39
|
private handleProxyRequest;
|
|
36
|
-
private
|
|
40
|
+
private bufferAndProxyRequest;
|
|
37
41
|
private handleUpgrade;
|
|
38
42
|
private handleRecordWebSocket;
|
|
39
43
|
private handleReplayWebSocket;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
-
export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-
|
|
2
|
+
export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-De4mgziH.js';
|
|
3
3
|
import '@playwright/test';
|
|
4
4
|
|
|
5
5
|
declare class ProxyServer {
|
|
@@ -12,12 +12,15 @@ declare class ProxyServer {
|
|
|
12
12
|
private proxy;
|
|
13
13
|
private currentSession;
|
|
14
14
|
private recordingsDir;
|
|
15
|
+
private requestSequenceMap;
|
|
16
|
+
private replaySequenceMap;
|
|
15
17
|
constructor(targets: string[], recordingsDir: string);
|
|
16
18
|
init(): Promise<void>;
|
|
17
19
|
listen(port: number): http.Server;
|
|
18
20
|
private setupProxyEventHandlers;
|
|
19
21
|
private handleProxyError;
|
|
20
22
|
private handleProxyResponse;
|
|
23
|
+
private addCorsHeaders;
|
|
21
24
|
private getTarget;
|
|
22
25
|
private handleControlRequest;
|
|
23
26
|
private clearModeTimeout;
|
|
@@ -32,8 +35,9 @@ declare class ProxyServer {
|
|
|
32
35
|
private handleReplayRequest;
|
|
33
36
|
private handleReplayError;
|
|
34
37
|
private handleRequest;
|
|
38
|
+
private handleCorsPreflightRequest;
|
|
35
39
|
private handleProxyRequest;
|
|
36
|
-
private
|
|
40
|
+
private bufferAndProxyRequest;
|
|
37
41
|
private handleUpgrade;
|
|
38
42
|
private handleRecordWebSocket;
|
|
39
43
|
private handleReplayWebSocket;
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import http from 'http';
|
|
3
|
+
import https from 'https';
|
|
3
4
|
import httpProxy from 'http-proxy';
|
|
4
5
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
5
6
|
import path from 'path';
|
|
@@ -83,6 +84,10 @@ var ProxyServer = class {
|
|
|
83
84
|
proxy;
|
|
84
85
|
currentSession;
|
|
85
86
|
recordingsDir;
|
|
87
|
+
requestSequenceMap;
|
|
88
|
+
// Track sequence per request key
|
|
89
|
+
replaySequenceMap;
|
|
90
|
+
// Track replay position per request key
|
|
86
91
|
constructor(targets, recordingsDir) {
|
|
87
92
|
this.targets = targets;
|
|
88
93
|
this.currentTargetIndex = 0;
|
|
@@ -92,6 +97,8 @@ var ProxyServer = class {
|
|
|
92
97
|
this.modeTimeout = null;
|
|
93
98
|
this.currentSession = null;
|
|
94
99
|
this.recordingsDir = recordingsDir;
|
|
100
|
+
this.requestSequenceMap = /* @__PURE__ */ new Map();
|
|
101
|
+
this.replaySequenceMap = /* @__PURE__ */ new Map();
|
|
95
102
|
this.proxy = httpProxy.createProxyServer({
|
|
96
103
|
secure: false,
|
|
97
104
|
changeOrigin: true
|
|
@@ -130,10 +137,19 @@ var ProxyServer = class {
|
|
|
130
137
|
res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
|
|
131
138
|
}
|
|
132
139
|
handleProxyResponse(proxyRes, req) {
|
|
140
|
+
this.addCorsHeaders(proxyRes, req);
|
|
133
141
|
if (this.mode === Modes.record && this.recordingId) {
|
|
134
142
|
this.recordResponse(req, proxyRes);
|
|
135
143
|
}
|
|
136
144
|
}
|
|
145
|
+
addCorsHeaders(proxyRes, req) {
|
|
146
|
+
const origin = req.headers.origin;
|
|
147
|
+
proxyRes.headers["access-control-allow-origin"] = origin || "*";
|
|
148
|
+
proxyRes.headers["access-control-allow-credentials"] = "true";
|
|
149
|
+
proxyRes.headers["access-control-allow-headers"] = req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization";
|
|
150
|
+
proxyRes.headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
|
|
151
|
+
proxyRes.headers["access-control-expose-headers"] = "*";
|
|
152
|
+
}
|
|
137
153
|
getTarget() {
|
|
138
154
|
const target = this.targets[this.currentTargetIndex];
|
|
139
155
|
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
@@ -208,6 +224,7 @@ var ProxyServer = class {
|
|
|
208
224
|
this.recordingId = id;
|
|
209
225
|
this.replayId = null;
|
|
210
226
|
this.currentSession = { id, recordings: [], websocketRecordings: [] };
|
|
227
|
+
this.requestSequenceMap.clear();
|
|
211
228
|
console.log(`Switched to record mode with ID: ${id}`);
|
|
212
229
|
}
|
|
213
230
|
switchToReplayMode(id) {
|
|
@@ -218,6 +235,7 @@ var ProxyServer = class {
|
|
|
218
235
|
this.replayId = id;
|
|
219
236
|
this.recordingId = null;
|
|
220
237
|
this.currentSession = null;
|
|
238
|
+
this.replaySequenceMap.clear();
|
|
221
239
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
222
240
|
}
|
|
223
241
|
setupModeTimeout(timeout) {
|
|
@@ -249,6 +267,8 @@ var ProxyServer = class {
|
|
|
249
267
|
return;
|
|
250
268
|
}
|
|
251
269
|
const key = getReqID(req);
|
|
270
|
+
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
271
|
+
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
252
272
|
const record = {
|
|
253
273
|
request: {
|
|
254
274
|
method: req.method,
|
|
@@ -257,7 +277,8 @@ var ProxyServer = class {
|
|
|
257
277
|
body: body || null
|
|
258
278
|
},
|
|
259
279
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
260
|
-
key
|
|
280
|
+
key,
|
|
281
|
+
sequence: currentSequence
|
|
261
282
|
};
|
|
262
283
|
this.currentSession.recordings.push(record);
|
|
263
284
|
}
|
|
@@ -266,7 +287,9 @@ var ProxyServer = class {
|
|
|
266
287
|
return;
|
|
267
288
|
}
|
|
268
289
|
const key = getReqID(req);
|
|
269
|
-
const record = this.currentSession.recordings.
|
|
290
|
+
const record = this.currentSession.recordings.findLast(
|
|
291
|
+
(r) => r.key === key && !r.response
|
|
292
|
+
);
|
|
270
293
|
if (!record) {
|
|
271
294
|
console.error("Request record not found for response:", key);
|
|
272
295
|
return;
|
|
@@ -291,17 +314,31 @@ var ProxyServer = class {
|
|
|
291
314
|
const filePath = getRecordingPath(this.recordingsDir, this.replayId);
|
|
292
315
|
try {
|
|
293
316
|
const session = await loadRecordingSession(filePath);
|
|
294
|
-
const
|
|
317
|
+
const currentSequence = this.replaySequenceMap.get(key) || 0;
|
|
318
|
+
const record = session.recordings.find(
|
|
319
|
+
(r) => r.key === key && r.sequence === currentSequence
|
|
320
|
+
);
|
|
295
321
|
if (!record) {
|
|
296
|
-
throw new Error(
|
|
322
|
+
throw new Error(
|
|
323
|
+
`No recording found for ${key} with sequence ${currentSequence}`
|
|
324
|
+
);
|
|
297
325
|
}
|
|
298
326
|
if (!record.response) {
|
|
299
327
|
throw new Error("No response recorded for this request");
|
|
300
328
|
}
|
|
329
|
+
this.replaySequenceMap.set(key, currentSequence + 1);
|
|
301
330
|
const { statusCode, headers, body } = record.response;
|
|
302
|
-
|
|
331
|
+
const origin = req.headers.origin;
|
|
332
|
+
const responseHeaders = {
|
|
333
|
+
...headers,
|
|
334
|
+
"access-control-allow-origin": origin || "*",
|
|
335
|
+
"access-control-allow-credentials": "true"
|
|
336
|
+
};
|
|
337
|
+
res.writeHead(statusCode, responseHeaders);
|
|
303
338
|
res.end(body);
|
|
304
|
-
console.log(
|
|
339
|
+
console.log(
|
|
340
|
+
`Replayed: ${req.method} ${req.url} (sequence: ${currentSequence})`
|
|
341
|
+
);
|
|
305
342
|
} catch (error) {
|
|
306
343
|
this.handleReplayError(res, error, key, filePath);
|
|
307
344
|
}
|
|
@@ -317,6 +354,9 @@ var ProxyServer = class {
|
|
|
317
354
|
});
|
|
318
355
|
}
|
|
319
356
|
async handleRequest(req, res) {
|
|
357
|
+
if (req.method === "OPTIONS") {
|
|
358
|
+
return this.handleCorsPreflightRequest(req, res);
|
|
359
|
+
}
|
|
320
360
|
if (req.url === CONTROL_ENDPOINT) {
|
|
321
361
|
return this.handleControlRequest(req, res);
|
|
322
362
|
}
|
|
@@ -325,23 +365,63 @@ var ProxyServer = class {
|
|
|
325
365
|
}
|
|
326
366
|
await this.handleProxyRequest(req, res);
|
|
327
367
|
}
|
|
368
|
+
handleCorsPreflightRequest(req, res) {
|
|
369
|
+
const origin = req.headers.origin;
|
|
370
|
+
res.writeHead(HTTP_STATUS_OK, {
|
|
371
|
+
"Access-Control-Allow-Origin": origin || "*",
|
|
372
|
+
"Access-Control-Allow-Credentials": "true",
|
|
373
|
+
"Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
374
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
375
|
+
"Access-Control-Max-Age": "86400"
|
|
376
|
+
// 24 hours
|
|
377
|
+
});
|
|
378
|
+
res.end();
|
|
379
|
+
}
|
|
328
380
|
async handleProxyRequest(req, res) {
|
|
329
381
|
const target = this.getTarget();
|
|
330
382
|
console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
|
|
331
383
|
if (this.mode === Modes.record) {
|
|
332
|
-
await this.
|
|
384
|
+
await this.bufferAndProxyRequest(req, res, target);
|
|
385
|
+
} else {
|
|
386
|
+
this.proxy.web(req, res, { target });
|
|
333
387
|
}
|
|
334
|
-
this.proxy.web(req, res, { target });
|
|
335
388
|
}
|
|
336
|
-
async
|
|
389
|
+
async bufferAndProxyRequest(req, res, target) {
|
|
337
390
|
const chunks = [];
|
|
338
391
|
req.on("data", (chunk) => {
|
|
339
392
|
chunks.push(chunk);
|
|
340
393
|
});
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
await this.saveRequestRecord(req, body);
|
|
394
|
+
await new Promise((resolve) => {
|
|
395
|
+
req.on("end", () => resolve());
|
|
344
396
|
});
|
|
397
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
398
|
+
await this.saveRequestRecord(req, body);
|
|
399
|
+
const targetUrl = new URL(target);
|
|
400
|
+
const isHttps = targetUrl.protocol === "https:";
|
|
401
|
+
const requestModule = isHttps ? https : http;
|
|
402
|
+
const defaultPort = isHttps ? 443 : 80;
|
|
403
|
+
const proxyReq = requestModule.request(
|
|
404
|
+
{
|
|
405
|
+
hostname: targetUrl.hostname,
|
|
406
|
+
port: targetUrl.port || defaultPort,
|
|
407
|
+
path: req.url,
|
|
408
|
+
method: req.method,
|
|
409
|
+
headers: req.headers
|
|
410
|
+
},
|
|
411
|
+
(proxyRes) => {
|
|
412
|
+
this.addCorsHeaders(proxyRes, req);
|
|
413
|
+
this.recordResponse(req, proxyRes);
|
|
414
|
+
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
415
|
+
proxyRes.pipe(res);
|
|
416
|
+
}
|
|
417
|
+
);
|
|
418
|
+
proxyReq.on("error", (err) => {
|
|
419
|
+
this.handleProxyError(err, req, res);
|
|
420
|
+
});
|
|
421
|
+
if (chunks.length > 0) {
|
|
422
|
+
proxyReq.write(Buffer.concat(chunks));
|
|
423
|
+
}
|
|
424
|
+
proxyReq.end();
|
|
345
425
|
}
|
|
346
426
|
handleUpgrade(req, socket, head) {
|
|
347
427
|
if (this.mode === Modes.replay) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import '@playwright/test';
|
|
2
|
-
export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-
|
|
2
|
+
export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-De4mgziH.cjs';
|
|
3
3
|
import 'node:http';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import '@playwright/test';
|
|
2
|
-
export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-
|
|
2
|
+
export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-De4mgziH.js';
|
|
3
3
|
import 'node:http';
|
package/dist/proxy.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from 'path';
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import http from 'http';
|
|
5
|
+
import https from 'https';
|
|
5
6
|
import httpProxy from 'http-proxy';
|
|
6
7
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
7
8
|
import filenamify from 'filenamify';
|
|
@@ -117,6 +118,10 @@ var ProxyServer = class {
|
|
|
117
118
|
proxy;
|
|
118
119
|
currentSession;
|
|
119
120
|
recordingsDir;
|
|
121
|
+
requestSequenceMap;
|
|
122
|
+
// Track sequence per request key
|
|
123
|
+
replaySequenceMap;
|
|
124
|
+
// Track replay position per request key
|
|
120
125
|
constructor(targets2, recordingsDir2) {
|
|
121
126
|
this.targets = targets2;
|
|
122
127
|
this.currentTargetIndex = 0;
|
|
@@ -126,6 +131,8 @@ var ProxyServer = class {
|
|
|
126
131
|
this.modeTimeout = null;
|
|
127
132
|
this.currentSession = null;
|
|
128
133
|
this.recordingsDir = recordingsDir2;
|
|
134
|
+
this.requestSequenceMap = /* @__PURE__ */ new Map();
|
|
135
|
+
this.replaySequenceMap = /* @__PURE__ */ new Map();
|
|
129
136
|
this.proxy = httpProxy.createProxyServer({
|
|
130
137
|
secure: false,
|
|
131
138
|
changeOrigin: true
|
|
@@ -164,10 +171,19 @@ var ProxyServer = class {
|
|
|
164
171
|
res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
|
|
165
172
|
}
|
|
166
173
|
handleProxyResponse(proxyRes, req) {
|
|
174
|
+
this.addCorsHeaders(proxyRes, req);
|
|
167
175
|
if (this.mode === Modes.record && this.recordingId) {
|
|
168
176
|
this.recordResponse(req, proxyRes);
|
|
169
177
|
}
|
|
170
178
|
}
|
|
179
|
+
addCorsHeaders(proxyRes, req) {
|
|
180
|
+
const origin = req.headers.origin;
|
|
181
|
+
proxyRes.headers["access-control-allow-origin"] = origin || "*";
|
|
182
|
+
proxyRes.headers["access-control-allow-credentials"] = "true";
|
|
183
|
+
proxyRes.headers["access-control-allow-headers"] = req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization";
|
|
184
|
+
proxyRes.headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
|
|
185
|
+
proxyRes.headers["access-control-expose-headers"] = "*";
|
|
186
|
+
}
|
|
171
187
|
getTarget() {
|
|
172
188
|
const target = this.targets[this.currentTargetIndex];
|
|
173
189
|
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
@@ -242,6 +258,7 @@ var ProxyServer = class {
|
|
|
242
258
|
this.recordingId = id;
|
|
243
259
|
this.replayId = null;
|
|
244
260
|
this.currentSession = { id, recordings: [], websocketRecordings: [] };
|
|
261
|
+
this.requestSequenceMap.clear();
|
|
245
262
|
console.log(`Switched to record mode with ID: ${id}`);
|
|
246
263
|
}
|
|
247
264
|
switchToReplayMode(id) {
|
|
@@ -252,6 +269,7 @@ var ProxyServer = class {
|
|
|
252
269
|
this.replayId = id;
|
|
253
270
|
this.recordingId = null;
|
|
254
271
|
this.currentSession = null;
|
|
272
|
+
this.replaySequenceMap.clear();
|
|
255
273
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
256
274
|
}
|
|
257
275
|
setupModeTimeout(timeout) {
|
|
@@ -283,6 +301,8 @@ var ProxyServer = class {
|
|
|
283
301
|
return;
|
|
284
302
|
}
|
|
285
303
|
const key = getReqID(req);
|
|
304
|
+
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
305
|
+
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
286
306
|
const record = {
|
|
287
307
|
request: {
|
|
288
308
|
method: req.method,
|
|
@@ -291,7 +311,8 @@ var ProxyServer = class {
|
|
|
291
311
|
body: body || null
|
|
292
312
|
},
|
|
293
313
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
294
|
-
key
|
|
314
|
+
key,
|
|
315
|
+
sequence: currentSequence
|
|
295
316
|
};
|
|
296
317
|
this.currentSession.recordings.push(record);
|
|
297
318
|
}
|
|
@@ -300,7 +321,9 @@ var ProxyServer = class {
|
|
|
300
321
|
return;
|
|
301
322
|
}
|
|
302
323
|
const key = getReqID(req);
|
|
303
|
-
const record = this.currentSession.recordings.
|
|
324
|
+
const record = this.currentSession.recordings.findLast(
|
|
325
|
+
(r) => r.key === key && !r.response
|
|
326
|
+
);
|
|
304
327
|
if (!record) {
|
|
305
328
|
console.error("Request record not found for response:", key);
|
|
306
329
|
return;
|
|
@@ -325,17 +348,31 @@ var ProxyServer = class {
|
|
|
325
348
|
const filePath = getRecordingPath(this.recordingsDir, this.replayId);
|
|
326
349
|
try {
|
|
327
350
|
const session = await loadRecordingSession(filePath);
|
|
328
|
-
const
|
|
351
|
+
const currentSequence = this.replaySequenceMap.get(key) || 0;
|
|
352
|
+
const record = session.recordings.find(
|
|
353
|
+
(r) => r.key === key && r.sequence === currentSequence
|
|
354
|
+
);
|
|
329
355
|
if (!record) {
|
|
330
|
-
throw new Error(
|
|
356
|
+
throw new Error(
|
|
357
|
+
`No recording found for ${key} with sequence ${currentSequence}`
|
|
358
|
+
);
|
|
331
359
|
}
|
|
332
360
|
if (!record.response) {
|
|
333
361
|
throw new Error("No response recorded for this request");
|
|
334
362
|
}
|
|
363
|
+
this.replaySequenceMap.set(key, currentSequence + 1);
|
|
335
364
|
const { statusCode, headers, body } = record.response;
|
|
336
|
-
|
|
365
|
+
const origin = req.headers.origin;
|
|
366
|
+
const responseHeaders = {
|
|
367
|
+
...headers,
|
|
368
|
+
"access-control-allow-origin": origin || "*",
|
|
369
|
+
"access-control-allow-credentials": "true"
|
|
370
|
+
};
|
|
371
|
+
res.writeHead(statusCode, responseHeaders);
|
|
337
372
|
res.end(body);
|
|
338
|
-
console.log(
|
|
373
|
+
console.log(
|
|
374
|
+
`Replayed: ${req.method} ${req.url} (sequence: ${currentSequence})`
|
|
375
|
+
);
|
|
339
376
|
} catch (error) {
|
|
340
377
|
this.handleReplayError(res, error, key, filePath);
|
|
341
378
|
}
|
|
@@ -351,6 +388,9 @@ var ProxyServer = class {
|
|
|
351
388
|
});
|
|
352
389
|
}
|
|
353
390
|
async handleRequest(req, res) {
|
|
391
|
+
if (req.method === "OPTIONS") {
|
|
392
|
+
return this.handleCorsPreflightRequest(req, res);
|
|
393
|
+
}
|
|
354
394
|
if (req.url === CONTROL_ENDPOINT) {
|
|
355
395
|
return this.handleControlRequest(req, res);
|
|
356
396
|
}
|
|
@@ -359,23 +399,63 @@ var ProxyServer = class {
|
|
|
359
399
|
}
|
|
360
400
|
await this.handleProxyRequest(req, res);
|
|
361
401
|
}
|
|
402
|
+
handleCorsPreflightRequest(req, res) {
|
|
403
|
+
const origin = req.headers.origin;
|
|
404
|
+
res.writeHead(HTTP_STATUS_OK, {
|
|
405
|
+
"Access-Control-Allow-Origin": origin || "*",
|
|
406
|
+
"Access-Control-Allow-Credentials": "true",
|
|
407
|
+
"Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
408
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
409
|
+
"Access-Control-Max-Age": "86400"
|
|
410
|
+
// 24 hours
|
|
411
|
+
});
|
|
412
|
+
res.end();
|
|
413
|
+
}
|
|
362
414
|
async handleProxyRequest(req, res) {
|
|
363
415
|
const target = this.getTarget();
|
|
364
416
|
console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
|
|
365
417
|
if (this.mode === Modes.record) {
|
|
366
|
-
await this.
|
|
418
|
+
await this.bufferAndProxyRequest(req, res, target);
|
|
419
|
+
} else {
|
|
420
|
+
this.proxy.web(req, res, { target });
|
|
367
421
|
}
|
|
368
|
-
this.proxy.web(req, res, { target });
|
|
369
422
|
}
|
|
370
|
-
async
|
|
423
|
+
async bufferAndProxyRequest(req, res, target) {
|
|
371
424
|
const chunks = [];
|
|
372
425
|
req.on("data", (chunk) => {
|
|
373
426
|
chunks.push(chunk);
|
|
374
427
|
});
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
await this.saveRequestRecord(req, body);
|
|
428
|
+
await new Promise((resolve) => {
|
|
429
|
+
req.on("end", () => resolve());
|
|
378
430
|
});
|
|
431
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
432
|
+
await this.saveRequestRecord(req, body);
|
|
433
|
+
const targetUrl = new URL(target);
|
|
434
|
+
const isHttps = targetUrl.protocol === "https:";
|
|
435
|
+
const requestModule = isHttps ? https : http;
|
|
436
|
+
const defaultPort = isHttps ? 443 : 80;
|
|
437
|
+
const proxyReq = requestModule.request(
|
|
438
|
+
{
|
|
439
|
+
hostname: targetUrl.hostname,
|
|
440
|
+
port: targetUrl.port || defaultPort,
|
|
441
|
+
path: req.url,
|
|
442
|
+
method: req.method,
|
|
443
|
+
headers: req.headers
|
|
444
|
+
},
|
|
445
|
+
(proxyRes) => {
|
|
446
|
+
this.addCorsHeaders(proxyRes, req);
|
|
447
|
+
this.recordResponse(req, proxyRes);
|
|
448
|
+
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
449
|
+
proxyRes.pipe(res);
|
|
450
|
+
}
|
|
451
|
+
);
|
|
452
|
+
proxyReq.on("error", (err) => {
|
|
453
|
+
this.handleProxyError(err, req, res);
|
|
454
|
+
});
|
|
455
|
+
if (chunks.length > 0) {
|
|
456
|
+
proxyReq.write(Buffer.concat(chunks));
|
|
457
|
+
}
|
|
458
|
+
proxyReq.end();
|
|
379
459
|
}
|
|
380
460
|
handleUpgrade(req, socket, head) {
|
|
381
461
|
if (this.mode === Modes.replay) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "test-proxy-recorder",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "HTTP proxy server for recording and replaying network requests in testing. Works seamlessly with Playwright testing framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|