Improve scenario recording

This commit is contained in:
Koen Vlaswinkel
2023-09-29 14:08:34 +02:00
parent ae2d6ce16e
commit 40b79f2e61
6 changed files with 54 additions and 65 deletions

View File

@@ -74,6 +74,13 @@ export interface GetVariantAnalysisRepoResultRequest {
}; };
} }
interface CodeSearchResponse {
total_count: number;
items: Array<{
repository: Repository;
}>;
}
interface CodeSearchRequest { interface CodeSearchRequest {
request: { request: {
kind: RequestKind.CodeSearch; kind: RequestKind.CodeSearch;
@@ -81,16 +88,14 @@ interface CodeSearchRequest {
}; };
response: { response: {
status: number; status: number;
body?: { body?: CodeSearchResponse | BasicErorResponse;
total_count?: number;
items?: Array<{
repository: Repository;
}>;
};
message?: string;
}; };
} }
interface AutoModelResponse {
models: string;
}
interface AutoModelRequest { interface AutoModelRequest {
request: { request: {
kind: RequestKind.AutoModel; kind: RequestKind.AutoModel;
@@ -100,10 +105,7 @@ interface AutoModelRequest {
}; };
response: { response: {
status: number; status: number;
body?: { body?: AutoModelResponse | BasicErorResponse;
models: string;
};
message?: string;
}; };
} }

View File

@@ -12,18 +12,31 @@ import { getDirectoryNamesInsidePath } from "../files";
* Enables mocking of the GitHub API server via HTTP interception, using msw. * Enables mocking of the GitHub API server via HTTP interception, using msw.
*/ */
export class MockGitHubApiServer extends DisposableObject { export class MockGitHubApiServer extends DisposableObject {
private _isListening: boolean;
private readonly server: SetupServer; private readonly server: SetupServer;
private readonly recorder: Recorder; private readonly recorder: Recorder;
constructor() { constructor() {
super(); super();
this._isListening = false;
this.server = setupServer(); this.server = setupServer();
this.recorder = this.push(new Recorder(this.server)); this.recorder = this.push(new Recorder(this.server));
} }
public startServer(): void {
if (this._isListening) {
return;
}
this.server.listen({ onUnhandledRequest: "bypass" });
this._isListening = true;
}
public stopServer(): void { public stopServer(): void {
this.server.close(); this.server.close();
this._isListening = false;
} }
public async loadScenario( public async loadScenario(
@@ -42,7 +55,6 @@ export class MockGitHubApiServer extends DisposableObject {
const handlers = await createRequestHandlers(scenarioPath); const handlers = await createRequestHandlers(scenarioPath);
this.server.resetHandlers(); this.server.resetHandlers();
this.server.use(...handlers); this.server.use(...handlers);
this.server.listen({ onUnhandledRequest: "bypass" });
} }
public async saveScenario( public async saveScenario(
@@ -99,6 +111,10 @@ export class MockGitHubApiServer extends DisposableObject {
return await getDirectoryNamesInsidePath(scenariosPath); return await getDirectoryNamesInsidePath(scenariosPath);
} }
public get isListening(): boolean {
return this._isListening;
}
public get isRecording(): boolean { public get isRecording(): boolean {
return this.recorder.isRecording; return this.recorder.isRecording;
} }

View File

@@ -3,8 +3,6 @@ import { join } from "path";
import { SetupServer } from "msw/node"; import { SetupServer } from "msw/node";
import fetch from "node-fetch";
import { DisposableObject } from "../disposable-object"; import { DisposableObject } from "../disposable-object";
import { import {
@@ -14,14 +12,12 @@ import {
} from "./gh-api-request"; } from "./gh-api-request";
export class Recorder extends DisposableObject { export class Recorder extends DisposableObject {
private readonly allRequests = new Map<string, Request>();
private currentRecordedScenario: GitHubApiRequest[] = []; private currentRecordedScenario: GitHubApiRequest[] = [];
private _isRecording = false; private _isRecording = false;
constructor(private readonly server: SetupServer) { constructor(private readonly server: SetupServer) {
super(); super();
this.onRequestStart = this.onRequestStart.bind(this);
this.onResponseBypass = this.onResponseBypass.bind(this); this.onResponseBypass = this.onResponseBypass.bind(this);
} }
@@ -42,7 +38,6 @@ export class Recorder extends DisposableObject {
this.clear(); this.clear();
this.server.events.on("request:start", this.onRequestStart);
this.server.events.on("response:bypass", this.onResponseBypass); this.server.events.on("response:bypass", this.onResponseBypass);
} }
@@ -53,13 +48,11 @@ export class Recorder extends DisposableObject {
this._isRecording = false; this._isRecording = false;
this.server.events.removeListener("request:start", this.onRequestStart);
this.server.events.removeListener("response:bypass", this.onResponseBypass); this.server.events.removeListener("response:bypass", this.onResponseBypass);
} }
public clear() { public clear() {
this.currentRecordedScenario = []; this.currentRecordedScenario = [];
this.allRequests.clear();
} }
public async save(scenariosPath: string, name: string): Promise<string> { public async save(scenariosPath: string, name: string): Promise<string> {
@@ -109,34 +102,14 @@ export class Recorder extends DisposableObject {
return scenarioDirectory; return scenarioDirectory;
} }
private onRequestStart(request: Request, requestId: string): void {
if (request.headers.has("x-vscode-codeql-msw-bypass")) {
return;
}
this.allRequests.set(requestId, request);
}
private async onResponseBypass( private async onResponseBypass(
response: Response, response: Response,
_: Request, request: Request,
requestId: string, _requestId: string,
): Promise<void> { ): Promise<void> {
const request = this.allRequests.get(requestId);
this.allRequests.delete(requestId);
if (!request) {
return;
}
if (response.body === undefined) {
return;
}
const gitHubApiRequest = await createGitHubApiRequest( const gitHubApiRequest = await createGitHubApiRequest(
request.url.toString(), request.url,
response.status, response,
response.body?.toString() || "",
response.headers,
); );
if (!gitHubApiRequest) { if (!gitHubApiRequest) {
return; return;
@@ -148,14 +121,15 @@ export class Recorder extends DisposableObject {
async function createGitHubApiRequest( async function createGitHubApiRequest(
url: string, url: string,
status: number, response: Response,
body: string,
headers: globalThis.Headers,
): Promise<GitHubApiRequest | undefined> { ): Promise<GitHubApiRequest | undefined> {
if (!url) { if (!url) {
return undefined; return undefined;
} }
const status = response.status;
const headers = response.headers;
if (url.match(/\/repos\/[a-zA-Z0-9-_.]+\/[a-zA-Z0-9-_.]+$/)) { if (url.match(/\/repos\/[a-zA-Z0-9-_.]+\/[a-zA-Z0-9-_.]+$/)) {
return { return {
request: { request: {
@@ -163,7 +137,7 @@ async function createGitHubApiRequest(
}, },
response: { response: {
status, status,
body: JSON.parse(body), body: await response.json(),
}, },
}; };
} }
@@ -177,7 +151,7 @@ async function createGitHubApiRequest(
}, },
response: { response: {
status, status,
body: JSON.parse(body), body: await response.json(),
}, },
}; };
} }
@@ -193,7 +167,7 @@ async function createGitHubApiRequest(
}, },
response: { response: {
status, status,
body: JSON.parse(body), body: await response.json(),
}, },
}; };
} }
@@ -209,7 +183,7 @@ async function createGitHubApiRequest(
}, },
response: { response: {
status, status,
body: JSON.parse(body), body: await response.json(),
}, },
}; };
} }
@@ -219,17 +193,6 @@ async function createGitHubApiRequest(
/objects-origin\.githubusercontent\.com\/codeql-query-console\/codeql-variant-analysis-repo-tasks\/\d+\/(?<repositoryId>\d+)/, /objects-origin\.githubusercontent\.com\/codeql-query-console\/codeql-variant-analysis-repo-tasks\/\d+\/(?<repositoryId>\d+)/,
); );
if (repoDownloadMatch?.groups?.repositoryId) { if (repoDownloadMatch?.groups?.repositoryId) {
// msw currently doesn't support binary response bodies, so we need to download this separately
// see https://github.com/mswjs/interceptors/blob/15eafa6215a328219999403e3ff110e71699b016/src/interceptors/ClientRequest/utils/getIncomingMessageBody.ts#L24-L33
// Essentially, mws is trying to decode a ZIP file as UTF-8 which changes the bytes and corrupts the file.
const response = await fetch(url, {
headers: {
// We need to ensure we don't end up in an infinite loop, since this request will also be intercepted
"x-vscode-codeql-msw-bypass": "true",
},
});
const responseBuffer = await response.buffer();
return { return {
request: { request: {
kind: RequestKind.GetVariantAnalysisRepoResult, kind: RequestKind.GetVariantAnalysisRepoResult,
@@ -237,7 +200,7 @@ async function createGitHubApiRequest(
}, },
response: { response: {
status, status,
body: responseBuffer, body: Buffer.from(await response.arrayBuffer()),
contentType: headers.get("content-type") ?? "application/octet-stream", contentType: headers.get("content-type") ?? "application/octet-stream",
}, },
}; };
@@ -252,7 +215,7 @@ async function createGitHubApiRequest(
}, },
response: { response: {
status, status,
body: JSON.parse(body), body: await response.json(),
}, },
}; };
} }
@@ -267,7 +230,7 @@ async function createGitHubApiRequest(
}, },
response: { response: {
status, status,
body: JSON.parse(body), body: await response.json(),
}, },
}; };
} }

View File

@@ -42,6 +42,10 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
}; };
} }
public async startServer(): Promise<void> {
this.server.startServer();
}
public async stopServer(): Promise<void> { public async stopServer(): Promise<void> {
this.server.stopServer(); this.server.stopServer();
@@ -252,7 +256,9 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
} }
private async onConfigChange(): Promise<void> { private async onConfigChange(): Promise<void> {
if (!this.config.mockServerEnabled) { if (this.config.mockServerEnabled && !this.server.isListening) {
await this.startServer();
} else if (!this.config.mockServerEnabled && this.server.isListening) {
await this.stopServer(); await this.stopServer();
} }
} }

View File

@@ -13,6 +13,7 @@ import { response as variantAnalysisRepoJson_response } from "../../../../src/co
import { testCredentialsWithRealOctokit } from "../../../factories/authentication"; import { testCredentialsWithRealOctokit } from "../../../factories/authentication";
const mockServer = new MockGitHubApiServer(); const mockServer = new MockGitHubApiServer();
beforeAll(() => mockServer.startServer());
afterEach(() => mockServer.unloadScenario()); afterEach(() => mockServer.unloadScenario());
afterAll(() => mockServer.stopServer()); afterAll(() => mockServer.stopServer());

View File

@@ -16,6 +16,7 @@ import { createVSCodeCommandManager } from "../../../../src/common/vscode/comman
import { AllCommands } from "../../../../src/common/commands"; import { AllCommands } from "../../../../src/common/commands";
const mockServer = new MockGitHubApiServer(); const mockServer = new MockGitHubApiServer();
beforeAll(() => mockServer.startServer());
afterEach(() => mockServer.unloadScenario()); afterEach(() => mockServer.unloadScenario());
afterAll(() => mockServer.stopServer()); afterAll(() => mockServer.stopServer());