Skip to content

Commit dcac6f5

Browse files
committed
Add scenario recorder
This adds a new class which will setup the MSW server to record requests, save them to memory and save them to files when calling a separate save method.
1 parent 4bc7992 commit dcac6f5

1 file changed

Lines changed: 176 additions & 0 deletions

File tree

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import * as fs from 'fs-extra';
2+
import * as path from 'path';
3+
4+
import { MockedRequest } from 'msw';
5+
import { SetupServerApi } from 'msw/node';
6+
import { IsomorphicResponse } from '@mswjs/interceptors';
7+
8+
import { DisposableObject } from '../pure/disposable-object';
9+
10+
import { GitHubApiRequest, RequestKind } from './gh-api-request';
11+
12+
export class Recorder extends DisposableObject {
13+
private readonly allRequests = new Map<string, MockedRequest>();
14+
private currentRecordedScenario: GitHubApiRequest[] = [];
15+
16+
private _recording = false;
17+
18+
constructor(
19+
private readonly server: SetupServerApi,
20+
) {
21+
super();
22+
this.onRequestStart = this.onRequestStart.bind(this);
23+
this.onResponseBypass = this.onResponseBypass.bind(this);
24+
}
25+
26+
public get isRecording(): boolean {
27+
return this._recording;
28+
}
29+
30+
public get scenarioRequestCount(): number {
31+
return this.currentRecordedScenario.length;
32+
}
33+
34+
public start(): void {
35+
if (this._recording) {
36+
return;
37+
}
38+
39+
this._recording = true;
40+
41+
this.clear();
42+
43+
this.server.events.on('request:start', this.onRequestStart);
44+
this.server.events.on('response:bypass', this.onResponseBypass);
45+
}
46+
47+
public stop(): void {
48+
if (!this._recording) {
49+
return;
50+
}
51+
52+
this._recording = false;
53+
54+
this.server.events.removeListener('request:start', this.onRequestStart);
55+
this.server.events.removeListener('response:bypass', this.onResponseBypass);
56+
}
57+
58+
public clear() {
59+
this.currentRecordedScenario = [];
60+
this.allRequests.clear();
61+
}
62+
63+
public async save(scenariosDirectory: string, name: string): Promise<string> {
64+
const scenarioDirectory = path.join(scenariosDirectory, name);
65+
66+
await fs.ensureDir(scenarioDirectory);
67+
68+
for (let i = 0; i < this.currentRecordedScenario.length; i++) {
69+
const request = this.currentRecordedScenario[i];
70+
71+
const fileName = `${i}-${request.request.kind}.json`;
72+
const filePath = path.join(scenarioDirectory, fileName);
73+
await fs.writeFile(filePath, JSON.stringify(request, null, 2));
74+
}
75+
76+
this.stop();
77+
78+
return scenarioDirectory;
79+
}
80+
81+
private onRequestStart(request: MockedRequest): void {
82+
this.allRequests.set(request.id, request);
83+
}
84+
85+
private onResponseBypass(response: IsomorphicResponse, requestId: string): void {
86+
const request = this.allRequests.get(requestId);
87+
this.allRequests.delete(requestId);
88+
if (!request) {
89+
return;
90+
}
91+
92+
if (response.body === undefined) {
93+
return;
94+
}
95+
96+
const gitHubApiRequest = createGitHubApiRequest(request.url.toString(), response.status, response.body);
97+
if (!gitHubApiRequest) {
98+
return;
99+
}
100+
101+
this.currentRecordedScenario.push(gitHubApiRequest);
102+
}
103+
}
104+
105+
function createGitHubApiRequest(url: string, status: number, body: string): GitHubApiRequest | undefined {
106+
if (!url) {
107+
return undefined;
108+
}
109+
110+
if (url.match(/\/repos\/[a-zA-Z0-9-_.]+\/[a-zA-Z0-9-_.]+$/)) {
111+
return {
112+
request: {
113+
kind: RequestKind.GetRepo,
114+
},
115+
response: {
116+
status,
117+
body: JSON.parse(body),
118+
},
119+
};
120+
}
121+
122+
if (url.match(/\/repositories\/\d+\/code-scanning\/codeql\/variant-analyses$/)) {
123+
return {
124+
request: {
125+
kind: RequestKind.SubmitVariantAnalysis,
126+
},
127+
response: {
128+
status,
129+
body: JSON.parse(body),
130+
},
131+
};
132+
}
133+
134+
if (url.match(/\/repositories\/\d+\/code-scanning\/codeql\/variant-analyses\/\d+$/)) {
135+
return {
136+
request: {
137+
kind: RequestKind.GetVariantAnalysis,
138+
},
139+
response: {
140+
status,
141+
body: JSON.parse(body),
142+
},
143+
};
144+
}
145+
146+
const repoTaskMatch = url.match(/\/repositories\/\d+\/code-scanning\/codeql\/variant-analyses\/\d+\/repositories\/(?<repositoryId>\d+)$/);
147+
if (repoTaskMatch?.groups?.repositoryId) {
148+
return {
149+
request: {
150+
kind: RequestKind.GetVariantAnalysisRepo,
151+
repositoryId: parseInt(repoTaskMatch.groups.repositoryId, 10),
152+
},
153+
response: {
154+
status,
155+
body: JSON.parse(body),
156+
},
157+
};
158+
}
159+
160+
// if url is a download URL for a variant analysis result, then it's a get-variant-analysis-repoResult.
161+
const repoDownloadMatch = url.match(/objects-origin\.githubusercontent\.com\/codeql-query-console\/codeql-variant-analysis-repo-tasks\/\d+\/(?<repositoryId>\d+)/);
162+
if (repoDownloadMatch?.groups?.repositoryId) {
163+
return {
164+
request: {
165+
kind: RequestKind.GetVariantAnalysisRepoResult,
166+
repositoryId: parseInt(repoDownloadMatch.groups.repositoryId, 10),
167+
},
168+
response: {
169+
status,
170+
body: body as unknown as ArrayBuffer,
171+
}
172+
};
173+
}
174+
175+
return undefined;
176+
}

0 commit comments

Comments
 (0)