Files
vscode-codeql/extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-manager.test.ts
2023-04-28 10:14:55 +00:00

358 lines
12 KiB
TypeScript

import { CancellationTokenSource, commands, Uri, window } from "vscode";
import { extLogger } from "../../../../src/common";
import { setRemoteControllerRepo } from "../../../../src/config";
import * as ghApiClient from "../../../../src/variant-analysis/gh-api/gh-api-client";
import { join } from "path";
import { VariantAnalysisManager } from "../../../../src/variant-analysis/variant-analysis-manager";
import {
CliVersionConstraint,
CodeQLCliServer,
} from "../../../../src/codeql-cli/cli";
import {
fixWorkspaceReferences,
getActivatedExtension,
restoreWorkspaceReferences,
storagePath,
} from "../../global.helper";
import { VariantAnalysisResultsManager } from "../../../../src/variant-analysis/variant-analysis-results-manager";
import {
VariantAnalysisStatus,
VariantAnalysisSubmission,
} from "../../../../src/variant-analysis/shared/variant-analysis";
import { VariantAnalysis as VariantAnalysisApiResponse } from "../../../../src/variant-analysis/gh-api/variant-analysis";
import { createMockApiResponse } from "../../../factories/variant-analysis/gh-api/variant-analysis-api-response";
import { UserCancellationException } from "../../../../src/common/vscode/progress";
import { Repository } from "../../../../src/variant-analysis/gh-api/repository";
import { DbManager } from "../../../../src/databases/db-manager";
import { ExtensionApp } from "../../../../src/common/vscode/vscode-app";
import { DbConfigStore } from "../../../../src/databases/config/db-config-store";
import { mockedQuickPickItem } from "../../utils/mocking.helpers";
import { QueryLanguage } from "../../../../src/common/query-language";
import { readBundledPack } from "../../utils/bundled-pack-helpers";
import { load } from "js-yaml";
describe("Variant Analysis Manager", () => {
let cli: CodeQLCliServer;
let cancellationTokenSource: CancellationTokenSource;
let variantAnalysisManager: VariantAnalysisManager;
beforeEach(async () => {
jest.spyOn(extLogger, "log").mockResolvedValue(undefined);
cancellationTokenSource = new CancellationTokenSource();
const extension = await getActivatedExtension();
cli = extension.cliServer;
const app = new ExtensionApp(extension.ctx);
const dbManager = new DbManager(app, new DbConfigStore(app));
const variantAnalysisResultsManager = new VariantAnalysisResultsManager(
cli,
extLogger,
);
variantAnalysisManager = new VariantAnalysisManager(
extension.ctx,
app,
cli,
storagePath,
variantAnalysisResultsManager,
dbManager,
);
});
describe("runVariantAnalysis", () => {
const progress = jest.fn();
let mockGetRepositoryFromNwo: jest.SpiedFunction<
typeof ghApiClient.getRepositoryFromNwo
>;
let mockSubmitVariantAnalysis: jest.SpiedFunction<
typeof ghApiClient.submitVariantAnalysis
>;
let mockApiResponse: VariantAnalysisApiResponse;
let originalDeps: Record<string, string> | undefined;
let executeCommandSpy: jest.SpiedFunction<typeof commands.executeCommand>;
const baseDir = join(__dirname, "..");
const qlpackFileWithWorkspaceRefs = getFile(
"data-remote-qlpack/qlpack.yml",
).fsPath;
beforeEach(async () => {
jest
.spyOn(window, "showQuickPick")
.mockResolvedValueOnce(mockedQuickPickItem("javascript"));
cancellationTokenSource = new CancellationTokenSource();
const dummyRepository: Repository = {
id: 123,
name: "vscode-codeql",
full_name: "github/vscode-codeql",
private: false,
};
mockGetRepositoryFromNwo = jest
.spyOn(ghApiClient, "getRepositoryFromNwo")
.mockResolvedValue(dummyRepository);
mockApiResponse = createMockApiResponse("in_progress");
mockSubmitVariantAnalysis = jest
.spyOn(ghApiClient, "submitVariantAnalysis")
.mockResolvedValue(mockApiResponse);
executeCommandSpy = jest.spyOn(commands, "executeCommand");
// always run in the vscode-codeql repo
await setRemoteControllerRepo("github/vscode-codeql");
// Only new version support `${workspace}` in qlpack.yml
originalDeps = await fixWorkspaceReferences(
qlpackFileWithWorkspaceRefs,
cli,
);
});
afterEach(async () => {
await restoreWorkspaceReferences(
qlpackFileWithWorkspaceRefs,
originalDeps,
);
});
it("should run a variant analysis that is part of a qlpack", async () => {
const fileUri = getFile("data-remote-qlpack/in-pack.ql");
await variantAnalysisManager.runVariantAnalysis(
fileUri,
progress,
cancellationTokenSource.token,
);
expect(executeCommandSpy).toBeCalledWith(
"codeQL.monitorNewVariantAnalysis",
expect.objectContaining({
id: mockApiResponse.id,
status: VariantAnalysisStatus.InProgress,
}),
);
expect(mockGetRepositoryFromNwo).toBeCalledTimes(1);
expect(mockSubmitVariantAnalysis).toBeCalledTimes(1);
});
it("should run a remote query that is not part of a qlpack", async () => {
const fileUri = getFile("data-remote-no-qlpack/in-pack.ql");
await variantAnalysisManager.runVariantAnalysis(
fileUri,
progress,
cancellationTokenSource.token,
);
expect(executeCommandSpy).toBeCalledWith(
"codeQL.monitorNewVariantAnalysis",
expect.objectContaining({
id: mockApiResponse.id,
status: VariantAnalysisStatus.InProgress,
}),
);
expect(mockGetRepositoryFromNwo).toBeCalledTimes(1);
expect(mockSubmitVariantAnalysis).toBeCalledTimes(1);
});
it("should run a remote query that is nested inside a qlpack", async () => {
const fileUri = getFile("data-remote-qlpack-nested/subfolder/in-pack.ql");
await variantAnalysisManager.runVariantAnalysis(
fileUri,
progress,
cancellationTokenSource.token,
);
expect(executeCommandSpy).toBeCalledWith(
"codeQL.monitorNewVariantAnalysis",
expect.objectContaining({
id: mockApiResponse.id,
status: VariantAnalysisStatus.InProgress,
}),
);
expect(mockGetRepositoryFromNwo).toBeCalledTimes(1);
expect(mockSubmitVariantAnalysis).toBeCalledTimes(1);
});
it("should cancel a run before uploading", async () => {
const fileUri = getFile("data-remote-no-qlpack/in-pack.ql");
const promise = variantAnalysisManager.runVariantAnalysis(
fileUri,
progress,
cancellationTokenSource.token,
);
cancellationTokenSource.cancel();
await expect(promise).rejects.toThrow(UserCancellationException);
});
describe("check variant analysis generated packs", () => {
beforeEach(() => {
mockSubmitVariantAnalysis = jest
.spyOn(ghApiClient, "submitVariantAnalysis")
.mockResolvedValue({
id: 1,
query_language: QueryLanguage.Javascript,
query_pack_url: "http://example.com",
created_at: "2021-01-01T00:00:00Z",
updated_at: "2021-01-01T00:00:00Z",
status: "in_progress",
controller_repo: {
id: 1,
name: "vscode-codeql",
full_name: "github/vscode-codeql",
private: false,
},
actions_workflow_run_id: 20,
scanned_repositories: [] as any[],
});
executeCommandSpy = jest.spyOn(commands, "executeCommand");
});
it("should run a remote query that is part of a qlpack", async () => {
await doVariantAnalysisTest({
queryPath: "data-remote-qlpack/in-pack.ql",
filesThatExist: ["in-pack.ql", "lib.qll"],
filesThatDoNotExist: [],
qlxFilesThatExist: ["in-pack.qlx"],
});
});
it("should run a remote query that is not part of a qlpack", async () => {
await doVariantAnalysisTest({
queryPath: "data-remote-no-qlpack/in-pack.ql",
filesThatExist: ["in-pack.ql"],
filesThatDoNotExist: ["lib.qll", "not-in-pack.ql"],
qlxFilesThatExist: ["in-pack.qlx"],
});
});
it("should run a remote query that is nested inside a qlpack", async () => {
await doVariantAnalysisTest({
queryPath: "data-remote-qlpack-nested/subfolder/in-pack.ql",
filesThatExist: ["subfolder/in-pack.ql", "otherfolder/lib.qll"],
filesThatDoNotExist: ["subfolder/not-in-pack.ql"],
qlxFilesThatExist: ["subfolder/in-pack.qlx"],
});
});
it("should run a remote query with extension packs inside a qlpack", async () => {
if (!(await cli.cliConstraints.supportsQlpacksKind())) {
console.log(
`Skipping test because qlpacks kind is only suppported in CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND} or later.`,
);
return;
}
await cli.setUseExtensionPacks(true);
await doVariantAnalysisTest({
queryPath: "data-remote-qlpack-nested/subfolder/in-pack.ql",
filesThatExist: [
"subfolder/in-pack.ql",
"otherfolder/lib.qll",
".codeql/libraries/semmle/targets-extension/0.0.0/ext/extension.yml",
],
filesThatDoNotExist: ["subfolder/not-in-pack.ql"],
qlxFilesThatExist: ["subfolder/in-pack.qlx"],
dependenciesToCheck: [
"codeql/javascript-all",
"semmle/targets-extension",
],
});
});
});
async function doVariantAnalysisTest({
queryPath,
filesThatExist,
qlxFilesThatExist,
filesThatDoNotExist,
dependenciesToCheck = ["codeql/javascript-all"],
}: {
queryPath: string;
filesThatExist: string[];
qlxFilesThatExist: string[];
filesThatDoNotExist: string[];
dependenciesToCheck?: string[];
}) {
const fileUri = getFile(queryPath);
await variantAnalysisManager.runVariantAnalysis(
fileUri,
progress,
cancellationTokenSource.token,
);
expect(mockSubmitVariantAnalysis).toBeCalledTimes(1);
expect(executeCommandSpy).toBeCalledWith(
"codeQL.monitorNewVariantAnalysis",
expect.objectContaining({
query: expect.objectContaining({ filePath: fileUri.fsPath }),
}),
);
const request: VariantAnalysisSubmission =
mockSubmitVariantAnalysis.mock.calls[0][1];
const packFS = await readBundledPack(request.query.pack);
filesThatExist.forEach((file) => {
expect(packFS.fileExists(file)).toBe(true);
});
if (await cli.cliConstraints.supportsQlxRemote()) {
qlxFilesThatExist.forEach((file) => {
expect(packFS.fileExists(file)).toBe(true);
});
}
filesThatDoNotExist.forEach((file) => {
expect(packFS.fileExists(file)).toBe(false);
});
expect(
packFS.fileExists("qlpack.yml") || packFS.fileExists("codeql-pack.yml"),
).toBe(true);
// depending on the cli version, we should have one of these files
expect(
packFS.fileExists("qlpack.lock.yml") ||
packFS.fileExists("codeql-pack.lock.yml"),
).toBe(true);
const packFileName = packFS.fileExists("qlpack.yml")
? "qlpack.yml"
: "codeql-pack.yml";
const qlpackContents = load(
packFS.fileContents(packFileName).toString("utf-8"),
);
expect(qlpackContents.name).toEqual("codeql-remote/query");
expect(qlpackContents.version).toEqual("0.0.0");
expect(qlpackContents.dependencies?.["codeql/javascript-all"]).toEqual(
"*",
);
const qlpackLockContents = load(
packFS.fileContents("codeql-pack.lock.yml").toString("utf-8"),
);
const actualLockKeys = Object.keys(qlpackLockContents.dependencies);
// The lock file should contain at least the specified dependencies.
dependenciesToCheck.forEach((dep) =>
expect(actualLockKeys).toContain(dep),
);
}
function getFile(file: string): Uri {
return Uri.file(join(baseDir, file));
}
});
});