test-proxy-recorder 0.1.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,92 @@
1
+ const INTERNAL_API_URL = process.env.INTERNAL_API_URL || 'http://localhost:8100';
2
+ /**
3
+ * Set the proxy mode for a given session
4
+ * @param mode - The proxy mode to set (recording, replay, transparent)
5
+ * @param sessionId - Unique identifier for the session
6
+ * @param timeout - Optional timeout in milliseconds
7
+ */
8
+ export async function setProxyMode(mode, sessionId, timeout) {
9
+ if (!INTERNAL_API_URL) {
10
+ console.warn('INTERNAL_API_URL not set, proxy mode not changed');
11
+ return;
12
+ }
13
+ try {
14
+ const body = {
15
+ mode,
16
+ id: sessionId,
17
+ ...(timeout && { timeout }),
18
+ };
19
+ const response = await fetch(`${INTERNAL_API_URL}/__control`, {
20
+ method: 'POST',
21
+ headers: { 'Content-Type': 'application/json' },
22
+ body: JSON.stringify(body),
23
+ });
24
+ if (!response.ok) {
25
+ const text = await response.text();
26
+ console.error(`Failed to set proxy mode to ${mode}:`, text);
27
+ throw new Error(`Failed to set proxy mode: ${text}`);
28
+ }
29
+ await response.json();
30
+ console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
31
+ }
32
+ catch (error) {
33
+ console.error(`Error setting proxy mode:`, error);
34
+ throw error;
35
+ }
36
+ }
37
+ /**
38
+ * Generate a session ID from test info
39
+ * @param testInfo - Playwright test info object
40
+ */
41
+ export function generateSessionId(testInfo) {
42
+ return testInfo.title.toLowerCase().replaceAll(/\s+/g, '-');
43
+ }
44
+ /**
45
+ * Start recording for a test
46
+ * @param testInfo - Playwright test info object
47
+ */
48
+ export async function startRecording(testInfo) {
49
+ const sessionId = generateSessionId(testInfo);
50
+ await setProxyMode('recording', sessionId);
51
+ }
52
+ /**
53
+ * Start replay for a test
54
+ * @param testInfo - Playwright test info object
55
+ */
56
+ export async function startReplay(testInfo) {
57
+ const sessionId = generateSessionId(testInfo);
58
+ await setProxyMode('replay', sessionId);
59
+ }
60
+ /**
61
+ * Stop recording/replay and return to transparent mode
62
+ * @param testInfo - Playwright test info object
63
+ */
64
+ export async function stopProxy(testInfo) {
65
+ const sessionId = generateSessionId(testInfo);
66
+ await setProxyMode('transparent', sessionId);
67
+ }
68
+ /**
69
+ * Playwright test fixture helper for managing proxy mode
70
+ * Use this in beforeEach/afterEach hooks
71
+ */
72
+ export const playwrightProxy = {
73
+ /**
74
+ * Setup before test - sets the proxy mode
75
+ * @param testInfo - Playwright test info object
76
+ * @param mode - The proxy mode to use for this test
77
+ */
78
+ async before(testInfo, mode) {
79
+ const sessionId = generateSessionId(testInfo);
80
+ console.log('Proxy setup:', { mode, sessionId });
81
+ await setProxyMode(mode, sessionId);
82
+ },
83
+ /**
84
+ * Cleanup after test - returns to transparent mode
85
+ * @param testInfo - Playwright test info object
86
+ */
87
+ async after(testInfo) {
88
+ const sessionId = generateSessionId(testInfo);
89
+ await setProxyMode('transparent', sessionId);
90
+ },
91
+ };
92
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":""}
package/dist/proxy.js ADDED
@@ -0,0 +1,8 @@
1
+ import { parseCliArgs } from './cli.js';
2
+ import { ProxyServer } from './ProxyServer.js';
3
+ const { targets, port, recordingsDir } = parseCliArgs();
4
+ const proxy = new ProxyServer(targets, recordingsDir);
5
+ await proxy.init();
6
+ proxy.listen(port);
7
+ console.log(`Recordings will be saved to: ${recordingsDir}`);
8
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1,46 @@
1
+ import http from 'node:http';
2
+ export declare const Modes: {
3
+ readonly transparent: "transparent";
4
+ readonly record: "record";
5
+ readonly replay: "replay";
6
+ };
7
+ export type Mode = (typeof Modes)[keyof typeof Modes];
8
+ export interface ControlRequest {
9
+ mode: Mode;
10
+ id?: string;
11
+ timeout?: number;
12
+ }
13
+ export interface RecordedRequest {
14
+ method: string;
15
+ url: string;
16
+ headers: http.IncomingHttpHeaders;
17
+ body: string | null;
18
+ }
19
+ export interface RecordedResponse {
20
+ statusCode: number;
21
+ headers: http.IncomingHttpHeaders;
22
+ body: string | null;
23
+ }
24
+ export interface Recording {
25
+ request: RecordedRequest;
26
+ response?: RecordedResponse;
27
+ timestamp: string;
28
+ key: string;
29
+ }
30
+ export interface WebSocketMessage {
31
+ direction: 'client-to-server' | 'server-to-client';
32
+ data: string;
33
+ timestamp: string;
34
+ }
35
+ export interface WebSocketRecording {
36
+ url: string;
37
+ messages: WebSocketMessage[];
38
+ timestamp: string;
39
+ key: string;
40
+ }
41
+ export interface RecordingSession {
42
+ id: string;
43
+ recordings: Recording[];
44
+ websocketRecordings: WebSocketRecording[];
45
+ }
46
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,eAAO,MAAM,KAAK;;;;CAIR,CAAC;AAEX,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,OAAO,KAAK,CAAC,CAAC;AAEtD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,IAAI,CAAC;IACX,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,IAAI,CAAC,mBAAmB,CAAC;IAClC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,IAAI,CAAC,mBAAmB,CAAC;IAClC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,eAAe,CAAC;IACzB,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,kBAAkB,GAAG,kBAAkB,CAAC;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,mBAAmB,EAAE,kBAAkB,EAAE,CAAC;CAC3C"}
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ export const Modes = {
2
+ transparent: 'transparent',
3
+ record: 'record',
4
+ replay: 'replay',
5
+ };
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,5 @@
1
+ import type { RecordingSession } from '../types.js';
2
+ export declare function getRecordingPath(recordingsDir: string, id: string): string;
3
+ export declare function loadRecordingSession(filePath: string): Promise<RecordingSession>;
4
+ export declare function saveRecordingSession(recordingsDir: string, session: RecordingSession): Promise<void>;
5
+ //# sourceMappingURL=fileUtils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileUtils.d.ts","sourceRoot":"","sources":["../../src/utils/fileUtils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,gBAAgB,CAAC,CAG3B;AAED,wBAAsB,oBAAoB,CACxC,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,IAAI,CAAC,CASf"}
@@ -0,0 +1,16 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ const JSON_INDENT_SPACES = 2;
4
+ export function getRecordingPath(recordingsDir, id) {
5
+ return path.join(recordingsDir, `${id}.json`);
6
+ }
7
+ export async function loadRecordingSession(filePath) {
8
+ const fileContent = await fs.readFile(filePath, 'utf8');
9
+ return JSON.parse(fileContent);
10
+ }
11
+ export async function saveRecordingSession(recordingsDir, session) {
12
+ const filePath = getRecordingPath(recordingsDir, session.id);
13
+ await fs.writeFile(filePath, JSON.stringify(session, null, JSON_INDENT_SPACES));
14
+ console.log(`Saved ${session.recordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`);
15
+ }
16
+ //# sourceMappingURL=fileUtils.js.map
@@ -0,0 +1,4 @@
1
+ import http from 'node:http';
2
+ export declare function readRequestBody(req: http.IncomingMessage): Promise<string>;
3
+ export declare function sendJsonResponse(res: http.ServerResponse, statusCode: number, data: Record<string, unknown>): void;
4
+ //# sourceMappingURL=httpHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"httpHelpers.d.ts","sourceRoot":"","sources":["../../src/utils/httpHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAI7B,wBAAsB,eAAe,CACnC,GAAG,EAAE,IAAI,CAAC,eAAe,GACxB,OAAO,CAAC,MAAM,CAAC,CAMjB;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,IAAI,CAGN"}
@@ -0,0 +1,13 @@
1
+ const CONTENT_TYPE_JSON = 'application/json';
2
+ export async function readRequestBody(req) {
3
+ let body = '';
4
+ for await (const chunk of req) {
5
+ body += chunk.toString();
6
+ }
7
+ return body;
8
+ }
9
+ export function sendJsonResponse(res, statusCode, data) {
10
+ res.writeHead(statusCode, { 'Content-Type': CONTENT_TYPE_JSON });
11
+ res.end(JSON.stringify(data));
12
+ }
13
+ //# sourceMappingURL=httpHelpers.js.map
@@ -0,0 +1,4 @@
1
+ export * from './fileUtils.js';
2
+ export * from './httpHelpers.js';
3
+ export * from './requestKeyGenerator.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC"}
@@ -0,0 +1,4 @@
1
+ export * from './fileUtils.js';
2
+ export * from './httpHelpers.js';
3
+ export * from './requestKeyGenerator.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import http from 'node:http';
2
+ export declare function generateRequestKey(req: http.IncomingMessage): string;
3
+ //# sourceMappingURL=requestKeyGenerator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requestKeyGenerator.d.ts","sourceRoot":"","sources":["../../src/utils/requestKeyGenerator.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAI7B,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,MAAM,CASpE"}
@@ -0,0 +1,24 @@
1
+ const QUERY_HASH_LENGTH = 8;
2
+ export function generateRequestKey(req) {
3
+ const urlParts = req.url.split('?');
4
+ const pathname = urlParts[0];
5
+ const query = urlParts[1] || '';
6
+ const normalizedPath = normalizePathname(pathname);
7
+ const queryHash = generateQueryHash(query);
8
+ return `${req.method}_${normalizedPath}${queryHash}.json`;
9
+ }
10
+ function normalizePathname(pathname) {
11
+ const normalized = pathname.replaceAll('/', '_').replace(/^_/, '');
12
+ return normalized || 'root';
13
+ }
14
+ function generateQueryHash(query) {
15
+ if (!query) {
16
+ return '';
17
+ }
18
+ const hash = Buffer.from(query)
19
+ .toString('base64')
20
+ .replaceAll(/[^a-zA-Z0-9]/g, '')
21
+ .slice(0, Math.max(0, QUERY_HASH_LENGTH));
22
+ return `_${hash}`;
23
+ }
24
+ //# sourceMappingURL=requestKeyGenerator.js.map
package/package.json ADDED
@@ -0,0 +1,100 @@
1
+ {
2
+ "name": "test-proxy-recorder",
3
+ "version": "0.1.0",
4
+ "description": "HTTP proxy server for recording and replaying network requests in testing. Works seamlessly with Playwright testing framework.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./playwright": {
14
+ "types": "./dist/playwright/index.d.ts",
15
+ "import": "./dist/playwright/index.js"
16
+ }
17
+ },
18
+ "bin": {
19
+ "test-proxy-recorder": "./dist/proxy.js"
20
+ },
21
+ "files": [
22
+ "dist/**/*.js",
23
+ "dist/**/*.d.ts",
24
+ "dist/**/*.d.ts.map",
25
+ "!dist/**/*.test.*",
26
+ "!dist/**/*.integration.test.*",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd",
31
+ "scripts": {
32
+ "start": "node dist/proxy.js",
33
+ "dev": "tsx src/proxy.ts",
34
+ "build": "tsc",
35
+ "prepublish": "pnpm run build && pnpm run test:run && pnpm run lint",
36
+ "lint": "eslint src --ext .ts",
37
+ "lint:fix": "eslint src --ext .ts --fix",
38
+ "typecheck": "tsc --noEmit",
39
+ "test": "vitest",
40
+ "test:run": "vitest run"
41
+ },
42
+ "keywords": [
43
+ "playwright",
44
+ "testing",
45
+ "proxy",
46
+ "mock",
47
+ "record",
48
+ "replay",
49
+ "har",
50
+ "http",
51
+ "websocket",
52
+ "intercept",
53
+ "fixture",
54
+ "vcr",
55
+ "stub",
56
+ "e2e",
57
+ "integration-testing",
58
+ "test-automation",
59
+ "api-mocking",
60
+ "network-recording"
61
+ ],
62
+ "author": "asmyshlyaev177",
63
+ "license": "MIT",
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "https://github.com/asmyshlyaev177/test-proxy-recorder.git"
67
+ },
68
+ "bugs": {
69
+ "url": "https://github.com/asmyshlyaev177/test-proxy-recorder/issues"
70
+ },
71
+ "homepage": "https://github.com/asmyshlyaev177/test-proxy-recorder#readme",
72
+ "engines": {
73
+ "node": ">=22.0.0"
74
+ },
75
+ "dependencies": {
76
+ "commander": "^12.0.0",
77
+ "http-proxy": "^1.18.1"
78
+ },
79
+ "peerDependencies": {
80
+ "@playwright/test": ">=1.0.0"
81
+ },
82
+ "devDependencies": {
83
+ "@types/http-proxy": "^1.17.15",
84
+ "@types/node": "^22.0.0",
85
+ "@types/ws": "^8.18.1",
86
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
87
+ "@typescript-eslint/parser": "^8.0.0",
88
+ "eslint": "^9.0.0",
89
+ "eslint-config-prettier": "^10.1.8",
90
+ "eslint-plugin-prettier": "^5.5.4",
91
+ "eslint-plugin-simple-import-sort": "^12.1.1",
92
+ "eslint-plugin-sonarjs": "^3.0.5",
93
+ "eslint-plugin-unicorn": "^62.0.0",
94
+ "prettier": "^3.6.2",
95
+ "tsx": "^4.19.0",
96
+ "typescript": "^5.6.0",
97
+ "vitest": "^3.2.4",
98
+ "ws": "^8.18.3"
99
+ }
100
+ }