From ecdc485e794d3c961a1b754b96785ff9bdd7a296 Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Thu, 27 Oct 2022 10:29:19 +0200 Subject: [PATCH 1/4] Add msw tests for gh-api-client This adds some really simple tests for the `gh-api-client` file to ensure that we can use msw mocks in pure tests. --- .../gh-api/gh-api-client.test.ts | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 extensions/ql-vscode/test/pure-tests/remote-queries/gh-api/gh-api-client.test.ts diff --git a/extensions/ql-vscode/test/pure-tests/remote-queries/gh-api/gh-api-client.test.ts b/extensions/ql-vscode/test/pure-tests/remote-queries/gh-api/gh-api-client.test.ts new file mode 100644 index 00000000000..b0d47c8362d --- /dev/null +++ b/extensions/ql-vscode/test/pure-tests/remote-queries/gh-api/gh-api-client.test.ts @@ -0,0 +1,93 @@ +import { expect } from 'chai'; +import * as path from 'path'; + +import * as Octokit from '@octokit/rest'; +import { retry } from '@octokit/plugin-retry'; + +import { setupServer } from 'msw/node'; + +import { + getRepositoryFromNwo, + getVariantAnalysis, + getVariantAnalysisRepo, getVariantAnalysisRepoResult, + submitVariantAnalysis +} from '../../../../src/remote-queries/gh-api/gh-api-client'; +import { Credentials } from '../../../../src/authentication'; +import { + createMockSubmission +} from '../../../../src/vscode-tests/factories/remote-queries/shared/variant-analysis-submission'; +import { createRequestHandlers } from '../../../../src/mocks/request-handlers'; + +const mockCredentials = { + getOctokit: () => Promise.resolve(new Octokit.Octokit({ retry })) +} as unknown as Credentials; + +const server = setupServer(); + +before(() => server.listen()); + +afterEach(() => server.resetHandlers()); + +after(() => server.close()); + +async function loadScenario(scenarioName: string) { + const handlers = await createRequestHandlers(path.join(__dirname, '../../../../src/mocks/scenarios', scenarioName)); + + server.use(...handlers); +} + +describe('submitVariantAnalysis', () => { + it('returns the submitted variant analysis', async () => { + await loadScenario('problem-query-success'); + + const result = await submitVariantAnalysis(mockCredentials, createMockSubmission()); + + expect(result).not.to.be.undefined; + expect(result.id).to.eq(146); + }); +}); + +describe('getVariantAnalysis', () => { + it('returns the variant analysis', async () => { + await loadScenario('problem-query-success'); + + const result = await getVariantAnalysis(mockCredentials, 557804416, 146); + + expect(result).not.to.be.undefined; + expect(result.status).not.to.be.undefined; + }); +}); + +describe('getVariantAnalysisRepo', () => { + it('returns the variant analysis repo task', async () => { + await loadScenario('problem-query-success'); + + const result = await getVariantAnalysisRepo(mockCredentials, 557804416, 146, 206444); + + expect(result).not.to.be.undefined; + expect(result.repository.id).to.eq(206444); + }); +}); + +describe('getVariantAnalysisRepoResult', () => { + it('returns the variant analysis repo result', async () => { + await loadScenario('problem-query-success'); + + const result = await getVariantAnalysisRepoResult(mockCredentials, 'https://objects-origin.githubusercontent.com/codeql-query-console/codeql-variant-analysis-repo-tasks/146/206444/f6752c5c-ad60-46ba-b8dc-977546108458'); + + expect(result).not.to.be.undefined; + expect(result).to.be.an('ArrayBuffer'); + expect(result.byteLength).to.eq(81841); + }); +}); + +describe('getRepositoryFromNwo', () => { + it('returns the repository', async () => { + await loadScenario('problem-query-success'); + + const result = await getRepositoryFromNwo(mockCredentials, 'github', 'mrva-demo-controller-repo'); + + expect(result).not.to.be.undefined; + expect(result.id).to.eq(557804416); + }); +}); From a9e49f2d7240fbfc01aff900c6deb427fc278537 Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Fri, 28 Oct 2022 14:59:18 +0200 Subject: [PATCH 2/4] Split mock GitHub API server into VSCode and non-VSCode This splits the mock GitHub API server class into two parts: one for the interactive, VSCode parts and one for the non-VSCode parts. This allows us to use the non-VSCode part in tests. --- extensions/ql-vscode/src/extension.ts | 4 +- .../ql-vscode/src/mocks/mock-gh-api-server.ts | 197 +++++------------ .../src/mocks/vscode-mock-gh-api-server.ts | 201 ++++++++++++++++++ .../gh-api/gh-api-client.test.ts | 32 +-- 4 files changed, 269 insertions(+), 165 deletions(-) create mode 100644 extensions/ql-vscode/src/mocks/vscode-mock-gh-api-server.ts diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 34d87f4f348..6a4bc32f417 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -116,7 +116,7 @@ import { } from './remote-queries/gh-api/variant-analysis'; import { VariantAnalysisManager } from './remote-queries/variant-analysis-manager'; import { createVariantAnalysisContentProvider } from './remote-queries/variant-analysis-content-provider'; -import { MockGitHubApiServer } from './mocks/mock-gh-api-server'; +import { VSCodeMockGitHubApiServer } from './mocks/vscode-mock-gh-api-server'; import { VariantAnalysisResultsManager } from './remote-queries/variant-analysis-results-manager'; /** @@ -1194,7 +1194,7 @@ async function activateWithInstalledDistribution( ) ); - const mockServer = new MockGitHubApiServer(ctx); + const mockServer = new VSCodeMockGitHubApiServer(ctx); ctx.subscriptions.push(mockServer); ctx.subscriptions.push( commandRunner( diff --git a/extensions/ql-vscode/src/mocks/mock-gh-api-server.ts b/extensions/ql-vscode/src/mocks/mock-gh-api-server.ts index dc7afaf48c2..1bab9256ea4 100644 --- a/extensions/ql-vscode/src/mocks/mock-gh-api-server.ts +++ b/extensions/ql-vscode/src/mocks/mock-gh-api-server.ts @@ -1,9 +1,7 @@ import * as path from 'path'; import * as fs from 'fs-extra'; -import { commands, env, ExtensionContext, ExtensionMode, QuickPickItem, Uri, window } from 'vscode'; import { setupServer, SetupServerApi } from 'msw/node'; -import { getMockGitHubApiServerScenariosPath, MockGitHubApiConfigListener } from '../config'; import { DisposableObject } from '../pure/disposable-object'; import { Recorder } from './recorder'; @@ -14,211 +12,128 @@ import { getDirectoryNamesInsidePath } from '../pure/files'; * Enables mocking of the GitHub API server via HTTP interception, using msw. */ export class MockGitHubApiServer extends DisposableObject { - private isListening: boolean; - private config: MockGitHubApiConfigListener; + private _isListening: boolean; private readonly server: SetupServerApi; private readonly recorder: Recorder; - constructor( - private readonly ctx: ExtensionContext, - ) { + constructor() { super(); - this.isListening = false; - this.config = new MockGitHubApiConfigListener(); + this._isListening = false; this.server = setupServer(); this.recorder = this.push(new Recorder(this.server)); - - this.setupConfigListener(); } public startServer(): void { - if (this.isListening) { + if (this._isListening) { return; } this.server.listen(); - this.isListening = true; + this._isListening = true; } public stopServer(): void { this.server.close(); - this.isListening = false; + this._isListening = false; } - public async loadScenario(): Promise { - const scenariosPath = await this.getScenariosPath(); + public async loadScenario(scenarioName: string, scenariosPath?: string): Promise { if (!scenariosPath) { - return; - } - - const scenarioNames = await getDirectoryNamesInsidePath(scenariosPath); - const scenarioQuickPickItems = scenarioNames.map(s => ({ label: s })); - const quickPickOptions = { - placeHolder: 'Select a scenario to load', - }; - const selectedScenario = await window.showQuickPick( - scenarioQuickPickItems, - quickPickOptions); - if (!selectedScenario) { - return; + scenariosPath = await this.getDefaultScenariosPath(); + if (!scenariosPath) { + return; + } } - const scenarioName = selectedScenario.label; const scenarioPath = path.join(scenariosPath, scenarioName); const handlers = await createRequestHandlers(scenarioPath); this.server.resetHandlers(); this.server.use(...handlers); + } + + public async saveScenario(scenarioName: string, scenariosPath?: string): Promise { + if (!scenariosPath) { + scenariosPath = await this.getDefaultScenariosPath(); + if (!scenariosPath) { + throw new Error('Could not find scenarios path'); + } + } + + const filePath = await this.recorder.save(scenariosPath, scenarioName); - // Set a value in the context to track whether we have a scenario loaded. - // This allows us to use this to show/hide commands (see package.json) - await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.scenarioLoaded', true); + await this.stopRecording(); - await window.showInformationMessage(`Loaded scenario '${scenarioName}'`); + return filePath; } public async unloadScenario(): Promise { - if (!this.isScenarioLoaded()) { - await window.showInformationMessage('No scenario currently loaded'); - } - else { - await this.unloadAllScenarios(); - await window.showInformationMessage('Unloaded scenario'); + if (!this.isScenarioLoaded) { + return; } + + await this.unloadAllScenarios(); } public async startRecording(): Promise { if (this.recorder.isRecording) { - void window.showErrorMessage('A scenario is already being recorded. Use the "Save Scenario" or "Cancel Scenario" commands to finish recording.'); return; } - if (this.isScenarioLoaded()) { + if (this.isScenarioLoaded) { await this.unloadAllScenarios(); - void window.showInformationMessage('A scenario was loaded so it has been unloaded'); } this.recorder.start(); - // Set a value in the context to track whether we are recording. This allows us to use this to show/hide commands (see package.json) - await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', true); + } - await window.showInformationMessage('Recording scenario. To save the scenario, use the "CodeQL Mock GitHub API Server: Save Scenario" command.'); + public async stopRecording(): Promise { + await this.recorder.stop(); + await this.recorder.clear(); } - public async saveScenario(): Promise { - const scenariosPath = await this.getScenariosPath(); + public async getScenarioNames(scenariosPath?: string): Promise { if (!scenariosPath) { - return; - } - - // Set a value in the context to track whether we are recording. This allows us to use this to show/hide commands (see package.json) - await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', false); - - if (!this.recorder.isRecording) { - void window.showErrorMessage('No scenario is currently being recorded.'); - return; - } - if (!this.recorder.anyRequestsRecorded) { - void window.showWarningMessage('No requests were recorded. Cancelling scenario.'); - - await this.stopRecording(); - - return; - } - - const name = await window.showInputBox({ - title: 'Save scenario', - prompt: 'Enter a name for the scenario.', - placeHolder: 'successful-run', - }); - if (!name) { - return; + scenariosPath = await this.getDefaultScenariosPath(); + if (!scenariosPath) { + return []; + } } - const filePath = await this.recorder.save(scenariosPath, name); - - await this.stopRecording(); - - const action = await window.showInformationMessage(`Scenario saved to ${filePath}`, 'Open directory'); - if (action === 'Open directory') { - await env.openExternal(Uri.file(filePath)); - } + return await getDirectoryNamesInsidePath(scenariosPath); } - public async cancelRecording(): Promise { - if (!this.recorder.isRecording) { - void window.showErrorMessage('No scenario is currently being recorded.'); - return; - } - - await this.stopRecording(); - - void window.showInformationMessage('Recording cancelled.'); + public get isListening(): boolean { + return this._isListening; } - private async stopRecording(): Promise { - // Set a value in the context to track whether we are recording. This allows us to use this to show/hide commands (see package.json) - await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', false); + public get isRecording(): boolean { + return this.recorder.isRecording; + } - await this.recorder.stop(); - await this.recorder.clear(); + public get anyRequestsRecorded(): boolean { + return this.recorder.anyRequestsRecorded; } - private async getScenariosPath(): Promise { - const scenariosPath = getMockGitHubApiServerScenariosPath(); - if (scenariosPath) { - return scenariosPath; - } + public get isScenarioLoaded(): boolean { + return this.server.listHandlers().length > 0; + } - if (this.ctx.extensionMode === ExtensionMode.Development) { - const developmentScenariosPath = Uri.joinPath(this.ctx.extensionUri, 'src/mocks/scenarios').fsPath.toString(); - if (await fs.pathExists(developmentScenariosPath)) { - return developmentScenariosPath; - } - } + public async getDefaultScenariosPath(): Promise { + // This should be the directory where package.json is located + const rootDirectory = path.resolve(__dirname, '../..'); - const directories = await window.showOpenDialog({ - canSelectFolders: true, - canSelectFiles: false, - canSelectMany: false, - openLabel: 'Select scenarios directory', - title: 'Select scenarios directory', - }); - if (directories === undefined || directories.length === 0) { - void window.showErrorMessage('No scenarios directory selected.'); - return undefined; + const scenariosPath = path.resolve(rootDirectory, 'src/mocks/scenarios'); + if (await fs.pathExists(scenariosPath)) { + return scenariosPath; } - // Unfortunately, we cannot save the directory in the configuration because that requires - // the configuration to be registered. If we do that, it would be visible to all users; there - // is no "when" clause that would allow us to only show it to users who have enabled the feature flag. - - return directories[0].fsPath; - } - - private isScenarioLoaded(): boolean { - return this.server.listHandlers().length > 0; + return undefined; } private async unloadAllScenarios(): Promise { this.server.resetHandlers(); - await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.scenarioLoaded', false); - } - - private setupConfigListener(): void { - // The config "changes" from the default at startup, so we need to call onConfigChange() to ensure the server is - // started if required. - this.onConfigChange(); - this.config.onDidChangeConfiguration(() => this.onConfigChange()); - } - - private onConfigChange(): void { - if (this.config.mockServerEnabled && !this.isListening) { - this.startServer(); - } else if (!this.config.mockServerEnabled && this.isListening) { - this.stopServer(); - } } } diff --git a/extensions/ql-vscode/src/mocks/vscode-mock-gh-api-server.ts b/extensions/ql-vscode/src/mocks/vscode-mock-gh-api-server.ts new file mode 100644 index 00000000000..0dd7d777c73 --- /dev/null +++ b/extensions/ql-vscode/src/mocks/vscode-mock-gh-api-server.ts @@ -0,0 +1,201 @@ +import * as fs from 'fs-extra'; +import { commands, env, ExtensionContext, ExtensionMode, QuickPickItem, Uri, window } from 'vscode'; + +import { getMockGitHubApiServerScenariosPath, MockGitHubApiConfigListener } from '../config'; +import { DisposableObject } from '../pure/disposable-object'; +import { MockGitHubApiServer } from './mock-gh-api-server'; + +/** + * "Interface" to the mock GitHub API server which implements VSCode interactions, such as + * listening for config changes, asking for scenario names, etc. + * + * This should not be used in tests. For tests, use the `MockGitHubApiServer` class directly. + */ +export class VSCodeMockGitHubApiServer extends DisposableObject { + private readonly server: MockGitHubApiServer; + private readonly config: MockGitHubApiConfigListener; + + constructor( + private readonly ctx: ExtensionContext, + ) { + super(); + this.server = new MockGitHubApiServer(); + this.config = new MockGitHubApiConfigListener(); + + this.setupConfigListener(); + } + + public async startServer(): Promise { + await this.server.startServer(); + } + + public async stopServer(): Promise { + await this.server.stopServer(); + + await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.scenarioLoaded', false); + await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', false); + } + + public async loadScenario(): Promise { + const scenariosPath = await this.getScenariosPath(); + if (!scenariosPath) { + return; + } + + const scenarioNames = await this.server.getScenarioNames(scenariosPath); + const scenarioQuickPickItems = scenarioNames.map(s => ({ label: s })); + const quickPickOptions = { + placeHolder: 'Select a scenario to load', + }; + const selectedScenario = await window.showQuickPick( + scenarioQuickPickItems, + quickPickOptions); + if (!selectedScenario) { + return; + } + + const scenarioName = selectedScenario.label; + + await this.server.loadScenario(scenarioName, scenariosPath); + + // Set a value in the context to track whether we have a scenario loaded. + // This allows us to use this to show/hide commands (see package.json) + await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.scenarioLoaded', true); + + await window.showInformationMessage(`Loaded scenario '${scenarioName}'`); + } + + public async unloadScenario(): Promise { + if (!this.server.isScenarioLoaded) { + await window.showInformationMessage('No scenario currently loaded'); + } else { + await this.server.unloadScenario(); + await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.scenarioLoaded', false); + await window.showInformationMessage('Unloaded scenario'); + } + } + + public async startRecording(): Promise { + if (this.server.isRecording) { + void window.showErrorMessage('A scenario is already being recorded. Use the "Save Scenario" or "Cancel Scenario" commands to finish recording.'); + return; + } + + if (this.server.isScenarioLoaded) { + await this.server.unloadScenario(); + await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.scenarioLoaded', false); + void window.showInformationMessage('A scenario was loaded so it has been unloaded'); + } + + await this.server.startRecording(); + // Set a value in the context to track whether we are recording. This allows us to use this to show/hide commands (see package.json) + await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', true); + + await window.showInformationMessage('Recording scenario. To save the scenario, use the "CodeQL Mock GitHub API Server: Save Scenario" command.'); + } + + public async saveScenario(): Promise { + const scenariosPath = await this.getScenariosPath(); + if (!scenariosPath) { + return; + } + + // Set a value in the context to track whether we are recording. This allows us to use this to show/hide commands (see package.json) + await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', false); + + if (!this.server.isRecording) { + void window.showErrorMessage('No scenario is currently being recorded.'); + return; + } + if (!this.server.anyRequestsRecorded) { + void window.showWarningMessage('No requests were recorded. Cancelling scenario.'); + + await this.stopRecording(); + + return; + } + + const name = await window.showInputBox({ + title: 'Save scenario', + prompt: 'Enter a name for the scenario.', + placeHolder: 'successful-run', + }); + if (!name) { + return; + } + + const filePath = await this.server.saveScenario(name, scenariosPath); + + await this.stopRecording(); + + const action = await window.showInformationMessage(`Scenario saved to ${filePath}`, 'Open directory'); + if (action === 'Open directory') { + await env.openExternal(Uri.file(filePath)); + } + } + + public async cancelRecording(): Promise { + if (!this.server.isRecording) { + void window.showErrorMessage('No scenario is currently being recorded.'); + return; + } + + await this.stopRecording(); + + void window.showInformationMessage('Recording cancelled.'); + } + + private async stopRecording(): Promise { + // Set a value in the context to track whether we are recording. This allows us to use this to show/hide commands (see package.json) + await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', false); + + await this.server.stopRecording(); + } + + private async getScenariosPath(): Promise { + const scenariosPath = getMockGitHubApiServerScenariosPath(); + if (scenariosPath) { + return scenariosPath; + } + + if (this.ctx.extensionMode === ExtensionMode.Development) { + const developmentScenariosPath = Uri.joinPath(this.ctx.extensionUri, 'src/mocks/scenarios').fsPath.toString(); + if (await fs.pathExists(developmentScenariosPath)) { + return developmentScenariosPath; + } + } + + const directories = await window.showOpenDialog({ + canSelectFolders: true, + canSelectFiles: false, + canSelectMany: false, + openLabel: 'Select scenarios directory', + title: 'Select scenarios directory', + }); + if (directories === undefined || directories.length === 0) { + void window.showErrorMessage('No scenarios directory selected.'); + return undefined; + } + + // Unfortunately, we cannot save the directory in the configuration because that requires + // the configuration to be registered. If we do that, it would be visible to all users; there + // is no "when" clause that would allow us to only show it to users who have enabled the feature flag. + + return directories[0].fsPath; + } + + private setupConfigListener(): void { + // The config "changes" from the default at startup, so we need to call onConfigChange() to ensure the server is + // started if required. + void this.onConfigChange(); + this.config.onDidChangeConfiguration(() => void this.onConfigChange()); + } + + private async onConfigChange(): Promise { + if (this.config.mockServerEnabled && !this.server.isListening) { + await this.startServer(); + } else if (!this.config.mockServerEnabled && this.server.isListening) { + await this.stopServer(); + } + } +} diff --git a/extensions/ql-vscode/test/pure-tests/remote-queries/gh-api/gh-api-client.test.ts b/extensions/ql-vscode/test/pure-tests/remote-queries/gh-api/gh-api-client.test.ts index b0d47c8362d..2b2480c282d 100644 --- a/extensions/ql-vscode/test/pure-tests/remote-queries/gh-api/gh-api-client.test.ts +++ b/extensions/ql-vscode/test/pure-tests/remote-queries/gh-api/gh-api-client.test.ts @@ -1,11 +1,8 @@ import { expect } from 'chai'; -import * as path from 'path'; import * as Octokit from '@octokit/rest'; import { retry } from '@octokit/plugin-retry'; -import { setupServer } from 'msw/node'; - import { getRepositoryFromNwo, getVariantAnalysis, @@ -16,29 +13,20 @@ import { Credentials } from '../../../../src/authentication'; import { createMockSubmission } from '../../../../src/vscode-tests/factories/remote-queries/shared/variant-analysis-submission'; -import { createRequestHandlers } from '../../../../src/mocks/request-handlers'; +import { MockGitHubApiServer } from '../../../../src/mocks/mock-gh-api-server'; const mockCredentials = { getOctokit: () => Promise.resolve(new Octokit.Octokit({ retry })) } as unknown as Credentials; -const server = setupServer(); - -before(() => server.listen()); - -afterEach(() => server.resetHandlers()); - -after(() => server.close()); - -async function loadScenario(scenarioName: string) { - const handlers = await createRequestHandlers(path.join(__dirname, '../../../../src/mocks/scenarios', scenarioName)); - - server.use(...handlers); -} +const mockServer = new MockGitHubApiServer(); +before(() => mockServer.startServer()); +afterEach(() => mockServer.unloadScenario()); +after(() => mockServer.stopServer()); describe('submitVariantAnalysis', () => { it('returns the submitted variant analysis', async () => { - await loadScenario('problem-query-success'); + await mockServer.loadScenario('problem-query-success'); const result = await submitVariantAnalysis(mockCredentials, createMockSubmission()); @@ -49,7 +37,7 @@ describe('submitVariantAnalysis', () => { describe('getVariantAnalysis', () => { it('returns the variant analysis', async () => { - await loadScenario('problem-query-success'); + await mockServer.loadScenario('problem-query-success'); const result = await getVariantAnalysis(mockCredentials, 557804416, 146); @@ -60,7 +48,7 @@ describe('getVariantAnalysis', () => { describe('getVariantAnalysisRepo', () => { it('returns the variant analysis repo task', async () => { - await loadScenario('problem-query-success'); + await mockServer.loadScenario('problem-query-success'); const result = await getVariantAnalysisRepo(mockCredentials, 557804416, 146, 206444); @@ -71,7 +59,7 @@ describe('getVariantAnalysisRepo', () => { describe('getVariantAnalysisRepoResult', () => { it('returns the variant analysis repo result', async () => { - await loadScenario('problem-query-success'); + await mockServer.loadScenario('problem-query-success'); const result = await getVariantAnalysisRepoResult(mockCredentials, 'https://objects-origin.githubusercontent.com/codeql-query-console/codeql-variant-analysis-repo-tasks/146/206444/f6752c5c-ad60-46ba-b8dc-977546108458'); @@ -83,7 +71,7 @@ describe('getVariantAnalysisRepoResult', () => { describe('getRepositoryFromNwo', () => { it('returns the repository', async () => { - await loadScenario('problem-query-success'); + await mockServer.loadScenario('problem-query-success'); const result = await getRepositoryFromNwo(mockCredentials, 'github', 'mrva-demo-controller-repo'); From c4d9ef26a8d9709050e2e4891f9c6dc289f0386b Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Fri, 28 Oct 2022 16:34:36 +0200 Subject: [PATCH 3/4] Use correct `tsconfig.json` in pure tests This will change the pure tests Mocha setup to actually use the `tsconfig.json` located in the `test` directory. Before, it was using the root-level `tsconfig.json`. To ensure we are still using mostly the same settings, this will extend the `test/tsconfig.json` from the root-level `tsconfig.json`. --- .vscode/launch.json | 6 ++---- extensions/ql-vscode/.mocharc.json | 6 ++++++ extensions/ql-vscode/package.json | 2 +- extensions/ql-vscode/test/mocha.setup.js | 6 ++++++ extensions/ql-vscode/test/tsconfig.json | 9 +++++---- 5 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 extensions/ql-vscode/.mocharc.json diff --git a/.vscode/launch.json b/.vscode/launch.json index aaf60d06ee9..31a8338277f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -44,10 +44,8 @@ "bdd", "--colors", "--diff", - "-r", - "ts-node/register", - "-r", - "test/mocha.setup.js", + "--config", + ".mocharc.json", "test/pure-tests/**/*.ts" ], "stopOnEntry": false, diff --git a/extensions/ql-vscode/.mocharc.json b/extensions/ql-vscode/.mocharc.json new file mode 100644 index 00000000000..c5b75cac23a --- /dev/null +++ b/extensions/ql-vscode/.mocharc.json @@ -0,0 +1,6 @@ +{ + "exit": true, + "require": [ + "test/mocha.setup.js" + ] +} diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index b5a0da12e45..d0e0e205ce7 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -1248,7 +1248,7 @@ "watch:extension": "tsc --watch", "watch:webpack": "gulp watchView", "test": "npm-run-all -p test:*", - "test:unit": "mocha --exit -r ts-node/register -r test/mocha.setup.js test/pure-tests/**/*.ts", + "test:unit": "mocha --config .mocharc.json test/pure-tests/**/*.ts", "test:view": "jest", "preintegration": "rm -rf ./out/vscode-tests && gulp", "integration": "node ./out/vscode-tests/run-integration-tests.js no-workspace,minimal-workspace", diff --git a/extensions/ql-vscode/test/mocha.setup.js b/extensions/ql-vscode/test/mocha.setup.js index e4716855209..392e00d79c6 100644 --- a/extensions/ql-vscode/test/mocha.setup.js +++ b/extensions/ql-vscode/test/mocha.setup.js @@ -1,2 +1,8 @@ +const path = require('path'); + +require('ts-node').register({ + project: path.resolve(__dirname, 'tsconfig.json') +}) + process.env.TZ = 'UTC'; process.env.LANG = 'en-US'; diff --git a/extensions/ql-vscode/test/tsconfig.json b/extensions/ql-vscode/test/tsconfig.json index 70b548578ab..9e8233e9345 100644 --- a/extensions/ql-vscode/test/tsconfig.json +++ b/extensions/ql-vscode/test/tsconfig.json @@ -1,8 +1,9 @@ { - "include": [ - "**/*.ts" - ], + "extends": "../tsconfig.json", + "include": ["**/*.ts"], + "exclude": [], "compilerOptions": { - "noEmit": true + "noEmit": true, + "resolveJsonModule": true } } From 562986546d1bf9bc175bed01197fb0d3b9483d93 Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Fri, 28 Oct 2022 16:34:51 +0200 Subject: [PATCH 4/4] Use scenario JSON files in tests This will check that the data returned matches the data in the JSON files, rather than checking against constants/magic values. --- .../gh-api/gh-api-client.test.ts | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/extensions/ql-vscode/test/pure-tests/remote-queries/gh-api/gh-api-client.test.ts b/extensions/ql-vscode/test/pure-tests/remote-queries/gh-api/gh-api-client.test.ts index 2b2480c282d..c4f4b4797ae 100644 --- a/extensions/ql-vscode/test/pure-tests/remote-queries/gh-api/gh-api-client.test.ts +++ b/extensions/ql-vscode/test/pure-tests/remote-queries/gh-api/gh-api-client.test.ts @@ -3,6 +3,8 @@ import { expect } from 'chai'; import * as Octokit from '@octokit/rest'; import { retry } from '@octokit/plugin-retry'; +import { faker } from '@faker-js/faker'; + import { getRepositoryFromNwo, getVariantAnalysis, @@ -15,6 +17,10 @@ import { } from '../../../../src/vscode-tests/factories/remote-queries/shared/variant-analysis-submission'; import { MockGitHubApiServer } from '../../../../src/mocks/mock-gh-api-server'; +import * as getRepoJson from '../../../../src/mocks/scenarios/problem-query-success/0-getRepo.json'; +import * as variantAnalysisJson from '../../../../src/mocks/scenarios/problem-query-success/1-submitVariantAnalysis.json'; +import * as variantAnalysisRepoJson from '../../../../src/mocks/scenarios/problem-query-success/9-getVariantAnalysisRepo.json'; + const mockCredentials = { getOctokit: () => Promise.resolve(new Octokit.Octokit({ retry })) } as unknown as Credentials; @@ -24,6 +30,10 @@ before(() => mockServer.startServer()); afterEach(() => mockServer.unloadScenario()); after(() => mockServer.stopServer()); +const controllerRepoId = variantAnalysisJson.response.body.controller_repo.id; +const variantAnalysisId = variantAnalysisJson.response.body.id; +const repoTaskId = variantAnalysisRepoJson.response.body.repository.id; + describe('submitVariantAnalysis', () => { it('returns the submitted variant analysis', async () => { await mockServer.loadScenario('problem-query-success'); @@ -31,7 +41,7 @@ describe('submitVariantAnalysis', () => { const result = await submitVariantAnalysis(mockCredentials, createMockSubmission()); expect(result).not.to.be.undefined; - expect(result.id).to.eq(146); + expect(result.id).to.eq(variantAnalysisId); }); }); @@ -39,7 +49,7 @@ describe('getVariantAnalysis', () => { it('returns the variant analysis', async () => { await mockServer.loadScenario('problem-query-success'); - const result = await getVariantAnalysis(mockCredentials, 557804416, 146); + const result = await getVariantAnalysis(mockCredentials, controllerRepoId, variantAnalysisId); expect(result).not.to.be.undefined; expect(result.status).not.to.be.undefined; @@ -50,10 +60,10 @@ describe('getVariantAnalysisRepo', () => { it('returns the variant analysis repo task', async () => { await mockServer.loadScenario('problem-query-success'); - const result = await getVariantAnalysisRepo(mockCredentials, 557804416, 146, 206444); + const result = await getVariantAnalysisRepo(mockCredentials, controllerRepoId, variantAnalysisId, repoTaskId); expect(result).not.to.be.undefined; - expect(result.repository.id).to.eq(206444); + expect(result.repository.id).to.eq(repoTaskId); }); }); @@ -61,11 +71,11 @@ describe('getVariantAnalysisRepoResult', () => { it('returns the variant analysis repo result', async () => { await mockServer.loadScenario('problem-query-success'); - const result = await getVariantAnalysisRepoResult(mockCredentials, 'https://objects-origin.githubusercontent.com/codeql-query-console/codeql-variant-analysis-repo-tasks/146/206444/f6752c5c-ad60-46ba-b8dc-977546108458'); + const result = await getVariantAnalysisRepoResult(mockCredentials, `https://objects-origin.githubusercontent.com/codeql-query-console/codeql-variant-analysis-repo-tasks/${variantAnalysisId}/${repoTaskId}/${faker.datatype.uuid()}`); expect(result).not.to.be.undefined; expect(result).to.be.an('ArrayBuffer'); - expect(result.byteLength).to.eq(81841); + expect(result.byteLength).to.eq(variantAnalysisRepoJson.response.body.artifact_size_in_bytes); }); }); @@ -76,6 +86,6 @@ describe('getRepositoryFromNwo', () => { const result = await getRepositoryFromNwo(mockCredentials, 'github', 'mrva-demo-controller-repo'); expect(result).not.to.be.undefined; - expect(result.id).to.eq(557804416); + expect(result.id).to.eq(getRepoJson.response.body.id); }); });