test-proxy-recorder 0.1.2 → 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.mjs CHANGED
@@ -1,8 +1,10 @@
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';
7
+ import filenamify from 'filenamify';
6
8
 
7
9
  // src/ProxyServer.ts
8
10
 
@@ -38,6 +40,24 @@ async function saveRecordingSession(recordingsDir, session) {
38
40
  `Saved ${session.recordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
39
41
  );
40
42
  }
43
+ var QUERY_HASH_LENGTH = 8;
44
+ function getReqID(req) {
45
+ const urlParts = req.url.split("?");
46
+ const pathname = urlParts[0];
47
+ const query = urlParts[1] || "";
48
+ const pathPart = pathname === "/" ? "root" : pathname.slice(1);
49
+ const normalizedPath = filenamify(pathPart, { replacement: "_" });
50
+ const queryHash = generateQueryHash(query);
51
+ const filename = `${req.method}_${normalizedPath}${queryHash}.json`;
52
+ return filenamify(filename, { replacement: "_" });
53
+ }
54
+ function generateQueryHash(query) {
55
+ if (!query) {
56
+ return "";
57
+ }
58
+ const hash = Buffer.from(query).toString("base64").replaceAll(/[^a-zA-Z0-9]/g, "").slice(0, Math.max(0, QUERY_HASH_LENGTH));
59
+ return `_${hash}`;
60
+ }
41
61
 
42
62
  // src/utils/httpHelpers.ts
43
63
  var CONTENT_TYPE_JSON = "application/json";
@@ -53,28 +73,6 @@ function sendJsonResponse(res, statusCode, data) {
53
73
  res.end(JSON.stringify(data));
54
74
  }
55
75
 
56
- // src/utils/requestKeyGenerator.ts
57
- var QUERY_HASH_LENGTH = 8;
58
- function generateRequestKey(req) {
59
- const urlParts = req.url.split("?");
60
- const pathname = urlParts[0];
61
- const query = urlParts[1] || "";
62
- const normalizedPath = normalizePathname(pathname);
63
- const queryHash = generateQueryHash(query);
64
- return `${req.method}_${normalizedPath}${queryHash}.json`;
65
- }
66
- function normalizePathname(pathname) {
67
- const normalized = pathname.replaceAll("/", "_").replace(/^_/, "");
68
- return normalized || "root";
69
- }
70
- function generateQueryHash(query) {
71
- if (!query) {
72
- return "";
73
- }
74
- const hash = Buffer.from(query).toString("base64").replaceAll(/[^a-zA-Z0-9]/g, "").slice(0, Math.max(0, QUERY_HASH_LENGTH));
75
- return `_${hash}`;
76
- }
77
-
78
76
  // src/ProxyServer.ts
79
77
  var ProxyServer = class {
80
78
  targets;
@@ -86,6 +84,10 @@ var ProxyServer = class {
86
84
  proxy;
87
85
  currentSession;
88
86
  recordingsDir;
87
+ requestSequenceMap;
88
+ // Track sequence per request key
89
+ replaySequenceMap;
90
+ // Track replay position per request key
89
91
  constructor(targets, recordingsDir) {
90
92
  this.targets = targets;
91
93
  this.currentTargetIndex = 0;
@@ -95,6 +97,8 @@ var ProxyServer = class {
95
97
  this.modeTimeout = null;
96
98
  this.currentSession = null;
97
99
  this.recordingsDir = recordingsDir;
100
+ this.requestSequenceMap = /* @__PURE__ */ new Map();
101
+ this.replaySequenceMap = /* @__PURE__ */ new Map();
98
102
  this.proxy = httpProxy.createProxyServer({
99
103
  secure: false,
100
104
  changeOrigin: true
@@ -133,10 +137,19 @@ var ProxyServer = class {
133
137
  res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
134
138
  }
135
139
  handleProxyResponse(proxyRes, req) {
140
+ this.addCorsHeaders(proxyRes, req);
136
141
  if (this.mode === Modes.record && this.recordingId) {
137
142
  this.recordResponse(req, proxyRes);
138
143
  }
139
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
+ }
140
153
  getTarget() {
141
154
  const target = this.targets[this.currentTargetIndex];
142
155
  this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
@@ -200,6 +213,7 @@ var ProxyServer = class {
200
213
  this.recordingId = null;
201
214
  this.replayId = null;
202
215
  this.currentSession = null;
216
+ clearTimeout(this.modeTimeout || 0);
203
217
  console.log("Switched to transparent mode");
204
218
  }
205
219
  switchToRecordMode(id) {
@@ -210,6 +224,7 @@ var ProxyServer = class {
210
224
  this.recordingId = id;
211
225
  this.replayId = null;
212
226
  this.currentSession = { id, recordings: [], websocketRecordings: [] };
227
+ this.requestSequenceMap.clear();
213
228
  console.log(`Switched to record mode with ID: ${id}`);
214
229
  }
215
230
  switchToReplayMode(id) {
@@ -220,6 +235,7 @@ var ProxyServer = class {
220
235
  this.replayId = id;
221
236
  this.recordingId = null;
222
237
  this.currentSession = null;
238
+ this.replaySequenceMap.clear();
223
239
  console.log(`Switched to replay mode with ID: ${id}`);
224
240
  }
225
241
  setupModeTimeout(timeout) {
@@ -250,7 +266,9 @@ var ProxyServer = class {
250
266
  if (!this.currentSession) {
251
267
  return;
252
268
  }
253
- const key = generateRequestKey(req);
269
+ const key = getReqID(req);
270
+ const currentSequence = this.requestSequenceMap.get(key) || 0;
271
+ this.requestSequenceMap.set(key, currentSequence + 1);
254
272
  const record = {
255
273
  request: {
256
274
  method: req.method,
@@ -259,7 +277,8 @@ var ProxyServer = class {
259
277
  body: body || null
260
278
  },
261
279
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
262
- key
280
+ key,
281
+ sequence: currentSequence
263
282
  };
264
283
  this.currentSession.recordings.push(record);
265
284
  }
@@ -267,8 +286,10 @@ var ProxyServer = class {
267
286
  if (!this.currentSession) {
268
287
  return;
269
288
  }
270
- const key = generateRequestKey(req);
271
- const record = this.currentSession.recordings.find((r) => r.key === key);
289
+ const key = getReqID(req);
290
+ const record = this.currentSession.recordings.findLast(
291
+ (r) => r.key === key && !r.response
292
+ );
272
293
  if (!record) {
273
294
  console.error("Request record not found for response:", key);
274
295
  return;
@@ -289,21 +310,35 @@ var ProxyServer = class {
289
310
  });
290
311
  }
291
312
  async handleReplayRequest(req, res) {
292
- const key = generateRequestKey(req);
313
+ const key = getReqID(req);
293
314
  const filePath = getRecordingPath(this.recordingsDir, this.replayId);
294
315
  try {
295
316
  const session = await loadRecordingSession(filePath);
296
- const record = session.recordings.find((r) => r.key === key);
317
+ const currentSequence = this.replaySequenceMap.get(key) || 0;
318
+ const record = session.recordings.find(
319
+ (r) => r.key === key && r.sequence === currentSequence
320
+ );
297
321
  if (!record) {
298
- throw new Error(`No recording found for ${key}`);
322
+ throw new Error(
323
+ `No recording found for ${key} with sequence ${currentSequence}`
324
+ );
299
325
  }
300
326
  if (!record.response) {
301
327
  throw new Error("No response recorded for this request");
302
328
  }
329
+ this.replaySequenceMap.set(key, currentSequence + 1);
303
330
  const { statusCode, headers, body } = record.response;
304
- res.writeHead(statusCode, headers);
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);
305
338
  res.end(body);
306
- console.log(`Replayed: ${req.method} ${req.url}`);
339
+ console.log(
340
+ `Replayed: ${req.method} ${req.url} (sequence: ${currentSequence})`
341
+ );
307
342
  } catch (error) {
308
343
  this.handleReplayError(res, error, key, filePath);
309
344
  }
@@ -319,6 +354,9 @@ var ProxyServer = class {
319
354
  });
320
355
  }
321
356
  async handleRequest(req, res) {
357
+ if (req.method === "OPTIONS") {
358
+ return this.handleCorsPreflightRequest(req, res);
359
+ }
322
360
  if (req.url === CONTROL_ENDPOINT) {
323
361
  return this.handleControlRequest(req, res);
324
362
  }
@@ -327,23 +365,63 @@ var ProxyServer = class {
327
365
  }
328
366
  await this.handleProxyRequest(req, res);
329
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
+ }
330
380
  async handleProxyRequest(req, res) {
331
381
  const target = this.getTarget();
332
382
  console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
333
383
  if (this.mode === Modes.record) {
334
- await this.bufferRequestForRecord(req);
384
+ await this.bufferAndProxyRequest(req, res, target);
385
+ } else {
386
+ this.proxy.web(req, res, { target });
335
387
  }
336
- this.proxy.web(req, res, { target });
337
388
  }
338
- async bufferRequestForRecord(req) {
389
+ async bufferAndProxyRequest(req, res, target) {
339
390
  const chunks = [];
340
391
  req.on("data", (chunk) => {
341
392
  chunks.push(chunk);
342
393
  });
343
- req.on("end", async () => {
344
- const body = Buffer.concat(chunks).toString("utf8");
345
- await this.saveRequestRecord(req, body);
394
+ await new Promise((resolve) => {
395
+ req.on("end", () => resolve());
346
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();
347
425
  }
348
426
  handleUpgrade(req, socket, head) {
349
427
  if (this.mode === Modes.replay) {
@@ -546,15 +624,15 @@ function generateSessionId(testInfo) {
546
624
  }
547
625
  async function startRecording(testInfo) {
548
626
  const sessionId = generateSessionId(testInfo);
549
- await setProxyMode("recording", sessionId);
627
+ await setProxyMode(Modes.record, sessionId);
550
628
  }
551
629
  async function startReplay(testInfo) {
552
630
  const sessionId = generateSessionId(testInfo);
553
- await setProxyMode("replay", sessionId);
631
+ await setProxyMode(Modes.replay, sessionId);
554
632
  }
555
633
  async function stopProxy(testInfo) {
556
634
  const sessionId = generateSessionId(testInfo);
557
- await setProxyMode("transparent", sessionId);
635
+ await setProxyMode(Modes.transparent, sessionId);
558
636
  }
559
637
  var playwrightProxy = {
560
638
  /**
@@ -573,7 +651,7 @@ var playwrightProxy = {
573
651
  */
574
652
  async after(testInfo) {
575
653
  const sessionId = generateSessionId(testInfo);
576
- await setProxyMode("transparent", sessionId);
654
+ await setProxyMode(Modes.transparent, sessionId);
577
655
  }
578
656
  };
579
657
 
@@ -1,5 +1,12 @@
1
1
  'use strict';
2
2
 
3
+ // src/types.ts
4
+ var Modes = {
5
+ transparent: "transparent",
6
+ record: "record",
7
+ replay: "replay"
8
+ };
9
+
3
10
  // src/playwright/index.ts
4
11
  var INTERNAL_API_URL = process.env.INTERNAL_API_URL || "http://localhost:8100";
5
12
  async function setProxyMode(mode, sessionId, timeout) {
@@ -31,15 +38,15 @@ function generateSessionId(testInfo) {
31
38
  }
32
39
  async function startRecording(testInfo) {
33
40
  const sessionId = generateSessionId(testInfo);
34
- await setProxyMode("recording", sessionId);
41
+ await setProxyMode(Modes.record, sessionId);
35
42
  }
36
43
  async function startReplay(testInfo) {
37
44
  const sessionId = generateSessionId(testInfo);
38
- await setProxyMode("replay", sessionId);
45
+ await setProxyMode(Modes.replay, sessionId);
39
46
  }
40
47
  async function stopProxy(testInfo) {
41
48
  const sessionId = generateSessionId(testInfo);
42
- await setProxyMode("transparent", sessionId);
49
+ await setProxyMode(Modes.transparent, sessionId);
43
50
  }
44
51
  var playwrightProxy = {
45
52
  /**
@@ -58,7 +65,7 @@ var playwrightProxy = {
58
65
  */
59
66
  async after(testInfo) {
60
67
  const sessionId = generateSessionId(testInfo);
61
- await setProxyMode("transparent", sessionId);
68
+ await setProxyMode(Modes.transparent, sessionId);
62
69
  }
63
70
  };
64
71
 
@@ -1,50 +1,3 @@
1
- import { TestInfo } from '@playwright/test';
2
-
3
- type ProxyMode = 'recording' | 'replay' | 'transparent';
4
- type PlaywrightTestInfo = Pick<TestInfo, 'title'>;
5
- /**
6
- * Set the proxy mode for a given session
7
- * @param mode - The proxy mode to set (recording, replay, transparent)
8
- * @param sessionId - Unique identifier for the session
9
- * @param timeout - Optional timeout in milliseconds
10
- */
11
- declare function setProxyMode(mode: ProxyMode, sessionId: string, timeout?: number): Promise<void>;
12
- /**
13
- * Generate a session ID from test info
14
- * @param testInfo - Playwright test info object
15
- */
16
- declare function generateSessionId(testInfo: PlaywrightTestInfo): string;
17
- /**
18
- * Start recording for a test
19
- * @param testInfo - Playwright test info object
20
- */
21
- declare function startRecording(testInfo: PlaywrightTestInfo): Promise<void>;
22
- /**
23
- * Start replay for a test
24
- * @param testInfo - Playwright test info object
25
- */
26
- declare function startReplay(testInfo: PlaywrightTestInfo): Promise<void>;
27
- /**
28
- * Stop recording/replay and return to transparent mode
29
- * @param testInfo - Playwright test info object
30
- */
31
- declare function stopProxy(testInfo: PlaywrightTestInfo): Promise<void>;
32
- /**
33
- * Playwright test fixture helper for managing proxy mode
34
- * Use this in beforeEach/afterEach hooks
35
- */
36
- declare const playwrightProxy: {
37
- /**
38
- * Setup before test - sets the proxy mode
39
- * @param testInfo - Playwright test info object
40
- * @param mode - The proxy mode to use for this test
41
- */
42
- before(testInfo: PlaywrightTestInfo, mode: ProxyMode): Promise<void>;
43
- /**
44
- * Cleanup after test - returns to transparent mode
45
- * @param testInfo - Playwright test info object
46
- */
47
- after(testInfo: PlaywrightTestInfo): Promise<void>;
48
- };
49
-
50
- export { type PlaywrightTestInfo, type ProxyMode, generateSessionId, playwrightProxy, setProxyMode, startRecording, startReplay, stopProxy };
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-De4mgziH.cjs';
3
+ import 'node:http';
@@ -1,50 +1,3 @@
1
- import { TestInfo } from '@playwright/test';
2
-
3
- type ProxyMode = 'recording' | 'replay' | 'transparent';
4
- type PlaywrightTestInfo = Pick<TestInfo, 'title'>;
5
- /**
6
- * Set the proxy mode for a given session
7
- * @param mode - The proxy mode to set (recording, replay, transparent)
8
- * @param sessionId - Unique identifier for the session
9
- * @param timeout - Optional timeout in milliseconds
10
- */
11
- declare function setProxyMode(mode: ProxyMode, sessionId: string, timeout?: number): Promise<void>;
12
- /**
13
- * Generate a session ID from test info
14
- * @param testInfo - Playwright test info object
15
- */
16
- declare function generateSessionId(testInfo: PlaywrightTestInfo): string;
17
- /**
18
- * Start recording for a test
19
- * @param testInfo - Playwright test info object
20
- */
21
- declare function startRecording(testInfo: PlaywrightTestInfo): Promise<void>;
22
- /**
23
- * Start replay for a test
24
- * @param testInfo - Playwright test info object
25
- */
26
- declare function startReplay(testInfo: PlaywrightTestInfo): Promise<void>;
27
- /**
28
- * Stop recording/replay and return to transparent mode
29
- * @param testInfo - Playwright test info object
30
- */
31
- declare function stopProxy(testInfo: PlaywrightTestInfo): Promise<void>;
32
- /**
33
- * Playwright test fixture helper for managing proxy mode
34
- * Use this in beforeEach/afterEach hooks
35
- */
36
- declare const playwrightProxy: {
37
- /**
38
- * Setup before test - sets the proxy mode
39
- * @param testInfo - Playwright test info object
40
- * @param mode - The proxy mode to use for this test
41
- */
42
- before(testInfo: PlaywrightTestInfo, mode: ProxyMode): Promise<void>;
43
- /**
44
- * Cleanup after test - returns to transparent mode
45
- * @param testInfo - Playwright test info object
46
- */
47
- after(testInfo: PlaywrightTestInfo): Promise<void>;
48
- };
49
-
50
- export { type PlaywrightTestInfo, type ProxyMode, generateSessionId, playwrightProxy, setProxyMode, startRecording, startReplay, stopProxy };
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-De4mgziH.js';
3
+ import 'node:http';
@@ -1,3 +1,10 @@
1
+ // src/types.ts
2
+ var Modes = {
3
+ transparent: "transparent",
4
+ record: "record",
5
+ replay: "replay"
6
+ };
7
+
1
8
  // src/playwright/index.ts
2
9
  var INTERNAL_API_URL = process.env.INTERNAL_API_URL || "http://localhost:8100";
3
10
  async function setProxyMode(mode, sessionId, timeout) {
@@ -29,15 +36,15 @@ function generateSessionId(testInfo) {
29
36
  }
30
37
  async function startRecording(testInfo) {
31
38
  const sessionId = generateSessionId(testInfo);
32
- await setProxyMode("recording", sessionId);
39
+ await setProxyMode(Modes.record, sessionId);
33
40
  }
34
41
  async function startReplay(testInfo) {
35
42
  const sessionId = generateSessionId(testInfo);
36
- await setProxyMode("replay", sessionId);
43
+ await setProxyMode(Modes.replay, sessionId);
37
44
  }
38
45
  async function stopProxy(testInfo) {
39
46
  const sessionId = generateSessionId(testInfo);
40
- await setProxyMode("transparent", sessionId);
47
+ await setProxyMode(Modes.transparent, sessionId);
41
48
  }
42
49
  var playwrightProxy = {
43
50
  /**
@@ -56,7 +63,7 @@ var playwrightProxy = {
56
63
  */
57
64
  async after(testInfo) {
58
65
  const sessionId = generateSessionId(testInfo);
59
- await setProxyMode("transparent", sessionId);
66
+ await setProxyMode(Modes.transparent, sessionId);
60
67
  }
61
68
  };
62
69