Skip to content

Commit

Permalink
Improve scenario recording
Browse files Browse the repository at this point in the history
  • Loading branch information
koesie10 committed Sep 29, 2023
1 parent ae2d6ce commit 56b4243
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 65 deletions.
24 changes: 13 additions & 11 deletions extensions/ql-vscode/src/common/mock-gh-api/gh-api-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,23 +74,28 @@ export interface GetVariantAnalysisRepoResultRequest {
};
}

interface CodeSearchResponse {
total_count: number;
items: Array<{
repository: Repository;
}>;
}

interface CodeSearchRequest {
request: {
kind: RequestKind.CodeSearch;
query: string;
};
response: {
status: number;
body?: {
total_count?: number;
items?: Array<{
repository: Repository;
}>;
};
message?: string;
body?: CodeSearchResponse | BasicErorResponse;
};
}

interface AutoModelResponse {
models: string;
}

interface AutoModelRequest {
request: {
kind: RequestKind.AutoModel;
Expand All @@ -100,10 +105,7 @@ interface AutoModelRequest {
};
response: {
status: number;
body?: {
models: string;
};
message?: string;
body?: AutoModelResponse | BasicErorResponse;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,31 @@ import { getDirectoryNamesInsidePath } from "../files";
* Enables mocking of the GitHub API server via HTTP interception, using msw.
*/
export class MockGitHubApiServer extends DisposableObject {
private _isListening: boolean;

private readonly server: SetupServer;
private readonly recorder: Recorder;

constructor() {
super();
this._isListening = false;

this.server = setupServer();
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 {
this.server.close();
this._isListening = false;
}

public async loadScenario(
Expand All @@ -42,7 +55,6 @@ export class MockGitHubApiServer extends DisposableObject {
const handlers = await createRequestHandlers(scenarioPath);
this.server.resetHandlers();
this.server.use(...handlers);
this.server.listen({ onUnhandledRequest: "bypass" });
}

public async saveScenario(
Expand Down Expand Up @@ -99,6 +111,10 @@ export class MockGitHubApiServer extends DisposableObject {
return await getDirectoryNamesInsidePath(scenariosPath);
}

public get isListening(): boolean {
return this._isListening;
}

public get isRecording(): boolean {
return this.recorder.isRecording;
}
Expand Down
67 changes: 15 additions & 52 deletions extensions/ql-vscode/src/common/mock-gh-api/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { join } from "path";

import { SetupServer } from "msw/node";

import fetch from "node-fetch";

import { DisposableObject } from "../disposable-object";

import {
Expand All @@ -14,14 +12,12 @@ import {
} from "./gh-api-request";

export class Recorder extends DisposableObject {
private readonly allRequests = new Map<string, Request>();
private currentRecordedScenario: GitHubApiRequest[] = [];

private _isRecording = false;

constructor(private readonly server: SetupServer) {
super();
this.onRequestStart = this.onRequestStart.bind(this);
this.onResponseBypass = this.onResponseBypass.bind(this);
}

Expand All @@ -42,7 +38,6 @@ export class Recorder extends DisposableObject {

this.clear();

this.server.events.on("request:start", this.onRequestStart);
this.server.events.on("response:bypass", this.onResponseBypass);
}

Expand All @@ -53,13 +48,11 @@ export class Recorder extends DisposableObject {

this._isRecording = false;

this.server.events.removeListener("request:start", this.onRequestStart);
this.server.events.removeListener("response:bypass", this.onResponseBypass);
}

public clear() {
this.currentRecordedScenario = [];
this.allRequests.clear();
}

public async save(scenariosPath: string, name: string): Promise<string> {
Expand Down Expand Up @@ -109,34 +102,14 @@ export class Recorder extends DisposableObject {
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(
response: Response,
_: Request,
requestId: string,
request: Request,
_requestId: string,
): Promise<void> {
const request = this.allRequests.get(requestId);
this.allRequests.delete(requestId);
if (!request) {
return;
}

if (response.body === undefined) {
return;
}

const gitHubApiRequest = await createGitHubApiRequest(
request.url.toString(),
response.status,
response.body?.toString() || "",
response.headers,
request.url,
response,
);
if (!gitHubApiRequest) {
return;
Expand All @@ -148,22 +121,23 @@ export class Recorder extends DisposableObject {

async function createGitHubApiRequest(
url: string,
status: number,
body: string,
headers: globalThis.Headers,
response: Response,
): Promise<GitHubApiRequest | undefined> {
if (!url) {
return undefined;
}

const status = response.status;
const headers = response.headers;

if (url.match(/\/repos\/[a-zA-Z0-9-_.]+\/[a-zA-Z0-9-_.]+$/)) {
return {
request: {
kind: RequestKind.GetRepo,
},
response: {
status,
body: JSON.parse(body),
body: await response.json(),
},
};
}
Expand All @@ -177,7 +151,7 @@ async function createGitHubApiRequest(
},
response: {
status,
body: JSON.parse(body),
body: await response.json(),
},
};
}
Expand All @@ -193,7 +167,7 @@ async function createGitHubApiRequest(
},
response: {
status,
body: JSON.parse(body),
body: await response.json(),
},
};
}
Expand All @@ -209,7 +183,7 @@ async function createGitHubApiRequest(
},
response: {
status,
body: JSON.parse(body),
body: await response.json(),
},
};
}
Expand All @@ -219,25 +193,14 @@ async function createGitHubApiRequest(
/objects-origin\.githubusercontent\.com\/codeql-query-console\/codeql-variant-analysis-repo-tasks\/\d+\/(?<repositoryId>\d+)/,
);
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 {
request: {
kind: RequestKind.GetVariantAnalysisRepoResult,
repositoryId: parseInt(repoDownloadMatch.groups.repositoryId, 10),
},
response: {
status,
body: responseBuffer,
body: Buffer.from(await response.arrayBuffer()),
contentType: headers.get("content-type") ?? "application/octet-stream",
},
};
Expand All @@ -252,7 +215,7 @@ async function createGitHubApiRequest(
},
response: {
status,
body: JSON.parse(body),
body: await response.json(),
},
};
}
Expand All @@ -267,7 +230,7 @@ async function createGitHubApiRequest(
},
response: {
status,
body: JSON.parse(body),
body: await response.json(),
},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
};
}

public async startServer(): Promise<void> {
this.server.startServer();
}

public async stopServer(): Promise<void> {
this.server.stopServer();

Expand Down Expand Up @@ -252,7 +256,9 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
}

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();
}
}
Expand Down

0 comments on commit 56b4243

Please sign in to comment.