Files
vscode-codeql/extensions/ql-vscode/test/vscode-tests/no-workspace/databases/github-databases/api.test.ts

540 lines
16 KiB
TypeScript

import {
mockedObject,
mockedOctokitFunction,
mockedQuickPickItem,
} from "../../../utils/mocking.helpers";
import type { GitHubDatabaseConfig } from "../../../../../src/config";
import * as dialog from "../../../../../src/common/vscode/dialog";
import {
convertGithubNwoToDatabaseUrl,
listDatabases,
} from "../../../../../src/databases/github-databases/api";
import type { Credentials } from "../../../../../src/common/authentication";
import type { Octokit } from "@octokit/rest";
import { AppOctokit } from "../../../../../src/common/octokit";
import { RequestError } from "@octokit/request-error";
import { window } from "vscode";
// Mock the AppOctokit constructor to ensure we aren't making any network requests
jest.mock("../../../../../src/common/octokit", () => ({
AppOctokit: jest.fn(),
}));
const appMockListCodeqlDatabases = mockedOctokitFunction<
"codeScanning",
"listCodeqlDatabases"
>();
const appOctokit = mockedObject<Octokit>({
rest: {
codeScanning: {
listCodeqlDatabases: appMockListCodeqlDatabases,
},
},
});
beforeEach(() => {
(AppOctokit as unknown as jest.Mock).mockImplementation(() => appOctokit);
});
describe("listDatabases", () => {
const owner = "github";
const repo = "codeql";
const setDownload = jest.fn();
let config: GitHubDatabaseConfig;
let credentials: Credentials;
const mockListCodeqlDatabases = mockedOctokitFunction<
"codeScanning",
"listCodeqlDatabases"
>();
const octokit = mockedObject<Octokit>({
rest: {
codeScanning: {
listCodeqlDatabases: mockListCodeqlDatabases,
},
},
});
const databases = [
{
id: 1495869,
name: "csharp-database",
language: "csharp",
uploader: {},
content_type: "application/zip",
state: "uploaded",
size: 55599715,
created_at: "2022-03-24T10:46:24Z",
updated_at: "2022-03-24T10:46:27Z",
url: "https://api.github.com/repositories/143040428/code-scanning/codeql/databases/csharp",
},
];
const successfulMockApiResponse = {
data: databases,
};
let showNeverAskAgainDialogSpy: jest.SpiedFunction<
typeof dialog.showNeverAskAgainDialog
>;
beforeEach(() => {
config = mockedObject<GitHubDatabaseConfig>({
setDownload,
});
mockListCodeqlDatabases.mockResolvedValue(successfulMockApiResponse);
showNeverAskAgainDialogSpy = jest
.spyOn(dialog, "showNeverAskAgainDialog")
.mockResolvedValue("Connect");
});
describe("when the user has an access token", () => {
beforeEach(() => {
credentials = mockedObject<Credentials>({
getExistingAccessToken: () => "ghp_xxx",
getOctokit: () => octokit,
});
});
it("returns the databases", async () => {
expect(await listDatabases(owner, repo, credentials, config)).toEqual({
databases,
promptedForCredentials: false,
octokit,
});
});
describe("when the request fails with a 404", () => {
beforeEach(() => {
mockListCodeqlDatabases.mockRejectedValue(
new RequestError("Not found", 404, {
request: {
method: "GET",
url: "",
headers: {},
},
response: {
status: 404,
headers: {},
url: "",
data: {},
retryCount: 0,
},
}),
);
});
it("throws an error", async () => {
await expect(
listDatabases(owner, repo, credentials, config),
).rejects.toThrow("Not found");
});
});
describe("when the request fails with a 500", () => {
beforeEach(() => {
mockListCodeqlDatabases.mockRejectedValue(
new RequestError("Internal server error", 500, {
request: {
method: "GET",
url: "",
headers: {},
},
response: {
status: 500,
headers: {},
url: "",
data: {},
retryCount: 0,
},
}),
);
});
it("throws an error", async () => {
await expect(
listDatabases(owner, repo, credentials, config),
).rejects.toThrow("Internal server error");
});
});
});
describe("when the user does not have an access token", () => {
describe("when the repo is public", () => {
beforeEach(() => {
credentials = mockedObject<Credentials>({
getExistingAccessToken: () => undefined,
});
mockListCodeqlDatabases.mockResolvedValue(undefined);
appMockListCodeqlDatabases.mockResolvedValue(successfulMockApiResponse);
});
it("returns the databases", async () => {
const result = await listDatabases(owner, repo, credentials, config);
expect(result).toEqual({
databases,
promptedForCredentials: false,
octokit: appOctokit,
});
expect(showNeverAskAgainDialogSpy).not.toHaveBeenCalled();
});
describe("when the request fails with a 500", () => {
beforeEach(() => {
appMockListCodeqlDatabases.mockRejectedValue(
new RequestError("Internal server error", 500, {
request: {
method: "GET",
url: "",
headers: {},
},
response: {
status: 500,
headers: {},
url: "",
data: {},
retryCount: 0,
},
}),
);
});
it("throws an error", async () => {
await expect(
listDatabases(owner, repo, credentials, config),
).rejects.toThrow("Internal server error");
expect(mockListCodeqlDatabases).not.toHaveBeenCalled();
});
});
});
describe("when the repo is private", () => {
beforeEach(() => {
credentials = mockedObject<Credentials>({
getExistingAccessToken: () => undefined,
getOctokit: () => octokit,
});
appMockListCodeqlDatabases.mockRejectedValue(
new RequestError("Not found", 404, {
request: {
method: "GET",
url: "",
headers: {},
},
response: {
status: 404,
headers: {},
url: "",
data: {},
retryCount: 0,
},
}),
);
});
describe("when answering connect to prompt", () => {
beforeEach(() => {
showNeverAskAgainDialogSpy.mockResolvedValue("Connect");
});
it("returns the databases", async () => {
const result = await listDatabases(owner, repo, credentials, config);
expect(result).toEqual({
databases,
promptedForCredentials: true,
octokit,
});
expect(showNeverAskAgainDialogSpy).toHaveBeenCalled();
expect(appMockListCodeqlDatabases).toHaveBeenCalled();
expect(mockListCodeqlDatabases).toHaveBeenCalled();
});
describe("when the request fails with a 404", () => {
beforeEach(() => {
mockListCodeqlDatabases.mockRejectedValue(
new RequestError("Not found", 404, {
request: {
method: "GET",
url: "",
headers: {},
},
response: {
status: 404,
headers: {},
url: "",
data: {},
retryCount: 0,
},
}),
);
});
it("throws an error", async () => {
await expect(
listDatabases(owner, repo, credentials, config),
).rejects.toThrow("Not found");
});
});
describe("when the request fails with a 500", () => {
beforeEach(() => {
mockListCodeqlDatabases.mockRejectedValue(
new RequestError("Internal server error", 500, {
request: {
method: "GET",
url: "",
headers: {},
},
response: {
status: 500,
headers: {},
url: "",
data: {},
retryCount: 0,
},
}),
);
});
it("throws an error", async () => {
await expect(
listDatabases(owner, repo, credentials, config),
).rejects.toThrow("Internal server error");
});
});
});
describe("when cancelling prompt", () => {
beforeEach(() => {
showNeverAskAgainDialogSpy.mockResolvedValue(undefined);
});
it("returns undefined", async () => {
const result = await listDatabases(owner, repo, credentials, config);
expect(result).toEqual(undefined);
expect(showNeverAskAgainDialogSpy).toHaveBeenCalled();
expect(appMockListCodeqlDatabases).toHaveBeenCalled();
expect(mockListCodeqlDatabases).not.toHaveBeenCalled();
expect(setDownload).not.toHaveBeenCalled();
});
});
describe("when answering not now to prompt", () => {
beforeEach(() => {
showNeverAskAgainDialogSpy.mockResolvedValue("Not now");
});
it("returns undefined", async () => {
const result = await listDatabases(owner, repo, credentials, config);
expect(result).toEqual(undefined);
expect(showNeverAskAgainDialogSpy).toHaveBeenCalled();
expect(appMockListCodeqlDatabases).toHaveBeenCalled();
expect(mockListCodeqlDatabases).not.toHaveBeenCalled();
expect(setDownload).not.toHaveBeenCalled();
});
});
describe("when answering never to prompt", () => {
beforeEach(() => {
showNeverAskAgainDialogSpy.mockResolvedValue("Never");
});
it("returns undefined and sets the config to 'never'", async () => {
const result = await listDatabases(owner, repo, credentials, config);
expect(result).toEqual(undefined);
expect(showNeverAskAgainDialogSpy).toHaveBeenCalled();
expect(appMockListCodeqlDatabases).toHaveBeenCalled();
expect(mockListCodeqlDatabases).not.toHaveBeenCalled();
expect(setDownload).toHaveBeenCalledWith("never");
});
});
});
});
});
describe("convertGithubNwoToDatabaseUrl", () => {
let quickPickSpy: jest.SpiedFunction<typeof window.showQuickPick>;
const progressSpy = jest.fn();
const mockListCodeqlDatabases = mockedOctokitFunction<
"codeScanning",
"listCodeqlDatabases"
>();
const octokit = mockedObject<Octokit>({
rest: {
codeScanning: {
listCodeqlDatabases: mockListCodeqlDatabases,
},
},
});
// We can't make the real octokit request (since we need credentials), so we mock the response.
const successfullMockApiResponse = {
data: [
{
id: 1495869,
name: "csharp-database",
language: "csharp",
uploader: {},
content_type: "application/zip",
state: "uploaded",
size: 55599715,
created_at: "2022-03-24T10:46:24Z",
updated_at: "2022-03-24T10:46:27Z",
url: "https://api.github.com/repositories/143040428/code-scanning/codeql/databases/csharp",
},
{
id: 1100671,
name: "database.zip",
language: "javascript",
uploader: {},
content_type: "application/zip",
state: "uploaded",
size: 29294434,
created_at: "2022-03-01T16:00:04Z",
updated_at: "2022-03-01T16:00:06Z",
url: "https://api.github.com/repositories/143040428/code-scanning/codeql/databases/javascript",
},
{
id: 648738,
name: "ql-database",
language: "ql",
uploader: {},
content_type: "application/json; charset=utf-8",
state: "uploaded",
size: 39735500,
created_at: "2022-02-02T09:38:50Z",
updated_at: "2022-02-02T09:38:51Z",
url: "https://api.github.com/repositories/143040428/code-scanning/codeql/databases/ql",
},
],
};
beforeEach(() => {
quickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockResolvedValue(undefined);
});
it("should convert a GitHub nwo to a database url", async () => {
mockListCodeqlDatabases.mockResolvedValue(successfullMockApiResponse);
quickPickSpy.mockResolvedValue(
mockedQuickPickItem({
label: "JavaScript",
language: "javascript",
}),
);
const githubRepo = "github/codeql";
const result = await convertGithubNwoToDatabaseUrl(
githubRepo,
octokit,
progressSpy,
);
expect(result).toBeDefined();
if (result === undefined) {
return;
}
const { databaseUrl, name, owner } = result;
expect(databaseUrl).toBe(
"https://api.github.com/repositories/143040428/code-scanning/codeql/databases/javascript",
);
expect(name).toBe("codeql");
expect(owner).toBe("github");
expect(quickPickSpy).toHaveBeenNthCalledWith(
1,
[
expect.objectContaining({
label: "C#",
description: "csharp",
language: "csharp",
}),
expect.objectContaining({
label: "JavaScript",
description: "javascript",
language: "javascript",
}),
expect.objectContaining({
label: "ql",
description: "ql",
language: "ql",
}),
],
expect.anything(),
);
});
// Repository doesn't exist, or the user has no access to the repository.
it("should fail on an invalid/inaccessible repository", async () => {
const mockApiResponse = {
data: {
message: "Not Found",
},
status: 404,
};
mockListCodeqlDatabases.mockResolvedValue(mockApiResponse);
const githubRepo = "foo/bar-not-real";
await expect(
convertGithubNwoToDatabaseUrl(githubRepo, octokit, progressSpy),
).rejects.toThrow(/Unable to get database/);
expect(progressSpy).toHaveBeenCalledTimes(0);
});
// User has access to the repository, but there are no databases for any language.
it("should fail on a repository with no databases", async () => {
const mockApiResponse = {
data: [],
};
mockListCodeqlDatabases.mockResolvedValue(mockApiResponse);
const githubRepo = "foo/bar-with-no-dbs";
await expect(
convertGithubNwoToDatabaseUrl(githubRepo, octokit, progressSpy),
).rejects.toThrow(/Unable to get database/);
expect(progressSpy).toHaveBeenCalledTimes(1);
});
describe("when language is already provided", () => {
describe("when language is valid", () => {
it("should not prompt the user", async () => {
mockListCodeqlDatabases.mockResolvedValue(successfullMockApiResponse);
const githubRepo = "github/codeql";
await convertGithubNwoToDatabaseUrl(
githubRepo,
octokit,
progressSpy,
"javascript",
);
expect(quickPickSpy).not.toHaveBeenCalled();
});
});
describe("when language is invalid", () => {
it("should prompt for language", async () => {
mockListCodeqlDatabases.mockResolvedValue(successfullMockApiResponse);
const githubRepo = "github/codeql";
await convertGithubNwoToDatabaseUrl(
githubRepo,
octokit,
progressSpy,
"invalid-language",
);
expect(quickPickSpy).toHaveBeenCalled();
});
});
});
describe("when language is not provided", () => {
it("should prompt for language", async () => {
mockListCodeqlDatabases.mockResolvedValue(successfullMockApiResponse);
const githubRepo = "github/codeql";
await convertGithubNwoToDatabaseUrl(githubRepo, octokit, progressSpy);
expect(quickPickSpy).toHaveBeenCalled();
});
});
});