Move convertGithubNwoToDatabaseUrl to databases/github-databases/api.ts
This commit is contained in:
@@ -33,11 +33,11 @@ import {
|
||||
} from "../config";
|
||||
import { showAndLogInformationMessage } from "../common/logging";
|
||||
import { AppOctokit } from "../common/octokit";
|
||||
import { getLanguageDisplayName } from "../common/query-language";
|
||||
import type { DatabaseOrigin } from "./local-databases/database-origin";
|
||||
import { createTimeoutSignal } from "../common/fetch-stream";
|
||||
import type { App } from "../common/app";
|
||||
import { findDirWithFile } from "../common/files";
|
||||
import { convertGithubNwoToDatabaseUrl } from "./github-databases/api";
|
||||
|
||||
/**
|
||||
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
|
||||
@@ -587,95 +587,6 @@ function isFile(databaseUrl: string) {
|
||||
return Uri.parse(databaseUrl).scheme === "file";
|
||||
}
|
||||
|
||||
export async function convertGithubNwoToDatabaseUrl(
|
||||
nwo: string,
|
||||
octokit: Octokit,
|
||||
progress: ProgressCallback,
|
||||
language?: string,
|
||||
): Promise<
|
||||
| {
|
||||
databaseUrl: string;
|
||||
owner: string;
|
||||
name: string;
|
||||
databaseId: number;
|
||||
databaseCreatedAt: string;
|
||||
commitOid: string | null;
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
try {
|
||||
const [owner, repo] = nwo.split("/");
|
||||
|
||||
const response = await octokit.rest.codeScanning.listCodeqlDatabases({
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
||||
const languages = response.data.map((db) => db.language);
|
||||
|
||||
if (!language || !languages.includes(language)) {
|
||||
language = await promptForLanguage(languages, progress);
|
||||
if (!language) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const databaseForLanguage = response.data.find(
|
||||
(db) => db.language === language,
|
||||
);
|
||||
if (!databaseForLanguage) {
|
||||
throw new Error(`No database found for language '${language}'`);
|
||||
}
|
||||
|
||||
return {
|
||||
databaseUrl: databaseForLanguage.url,
|
||||
owner,
|
||||
name: repo,
|
||||
databaseId: databaseForLanguage.id,
|
||||
databaseCreatedAt: databaseForLanguage.created_at,
|
||||
commitOid: databaseForLanguage.commit_oid ?? null,
|
||||
};
|
||||
} catch (e) {
|
||||
void extLogger.log(`Error: ${getErrorMessage(e)}`);
|
||||
throw new Error(`Unable to get database for '${nwo}'`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function promptForLanguage(
|
||||
languages: string[],
|
||||
progress: ProgressCallback | undefined,
|
||||
): Promise<string | undefined> {
|
||||
progress?.({
|
||||
message: "Choose language",
|
||||
step: 2,
|
||||
maxStep: 2,
|
||||
});
|
||||
if (!languages.length) {
|
||||
throw new Error("No databases found");
|
||||
}
|
||||
if (languages.length === 1) {
|
||||
return languages[0];
|
||||
}
|
||||
|
||||
const items = languages
|
||||
.map((language) => ({
|
||||
label: getLanguageDisplayName(language),
|
||||
description: language,
|
||||
language,
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
const selectedItem = await window.showQuickPick(items, {
|
||||
placeHolder: "Select the database language to download:",
|
||||
ignoreFocusOut: true,
|
||||
});
|
||||
if (!selectedItem) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return selectedItem.language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Databases created by the old odasa tool will not have a zipped
|
||||
* source location. However, this extension works better if sources
|
||||
|
||||
@@ -5,6 +5,11 @@ import { showNeverAskAgainDialog } from "../../common/vscode/dialog";
|
||||
import type { GitHubDatabaseConfig } from "../../config";
|
||||
import type { Credentials } from "../../common/authentication";
|
||||
import { AppOctokit } from "../../common/octokit";
|
||||
import type { ProgressCallback } from "../../common/vscode/progress";
|
||||
import { getErrorMessage } from "../../common/helpers-pure";
|
||||
import { getLanguageDisplayName } from "../../common/query-language";
|
||||
import { window } from "vscode";
|
||||
import { extLogger } from "../../common/logging/vscode";
|
||||
|
||||
export type CodeqlDatabase =
|
||||
RestEndpointMethodTypes["codeScanning"]["listCodeqlDatabases"]["response"]["data"][number];
|
||||
@@ -108,3 +113,92 @@ export async function listDatabases(
|
||||
octokit,
|
||||
};
|
||||
}
|
||||
|
||||
export async function convertGithubNwoToDatabaseUrl(
|
||||
nwo: string,
|
||||
octokit: Octokit,
|
||||
progress: ProgressCallback,
|
||||
language?: string,
|
||||
): Promise<
|
||||
| {
|
||||
databaseUrl: string;
|
||||
owner: string;
|
||||
name: string;
|
||||
databaseId: number;
|
||||
databaseCreatedAt: string;
|
||||
commitOid: string | null;
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
try {
|
||||
const [owner, repo] = nwo.split("/");
|
||||
|
||||
const response = await octokit.rest.codeScanning.listCodeqlDatabases({
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
||||
const languages = response.data.map((db) => db.language);
|
||||
|
||||
if (!language || !languages.includes(language)) {
|
||||
language = await promptForLanguage(languages, progress);
|
||||
if (!language) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const databaseForLanguage = response.data.find(
|
||||
(db) => db.language === language,
|
||||
);
|
||||
if (!databaseForLanguage) {
|
||||
throw new Error(`No database found for language '${language}'`);
|
||||
}
|
||||
|
||||
return {
|
||||
databaseUrl: databaseForLanguage.url,
|
||||
owner,
|
||||
name: repo,
|
||||
databaseId: databaseForLanguage.id,
|
||||
databaseCreatedAt: databaseForLanguage.created_at,
|
||||
commitOid: databaseForLanguage.commit_oid ?? null,
|
||||
};
|
||||
} catch (e) {
|
||||
void extLogger.log(`Error: ${getErrorMessage(e)}`);
|
||||
throw new Error(`Unable to get database for '${nwo}'`);
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForLanguage(
|
||||
languages: string[],
|
||||
progress: ProgressCallback | undefined,
|
||||
): Promise<string | undefined> {
|
||||
progress?.({
|
||||
message: "Choose language",
|
||||
step: 2,
|
||||
maxStep: 2,
|
||||
});
|
||||
if (!languages.length) {
|
||||
throw new Error("No databases found");
|
||||
}
|
||||
if (languages.length === 1) {
|
||||
return languages[0];
|
||||
}
|
||||
|
||||
const items = languages
|
||||
.map((language) => ({
|
||||
label: getLanguageDisplayName(language),
|
||||
description: language,
|
||||
language,
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
const selectedItem = await window.showQuickPick(items, {
|
||||
placeHolder: "Select the database language to download:",
|
||||
ignoreFocusOut: true,
|
||||
});
|
||||
if (!selectedItem) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return selectedItem.language;
|
||||
}
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
import { window } from "vscode";
|
||||
|
||||
import { convertGithubNwoToDatabaseUrl } from "../../../../src/databases/database-fetcher";
|
||||
import type { Octokit } from "@octokit/rest";
|
||||
import {
|
||||
mockedObject,
|
||||
mockedOctokitFunction,
|
||||
mockedQuickPickItem,
|
||||
} from "../../utils/mocking.helpers";
|
||||
|
||||
// These tests make API calls and may need extra time to complete.
|
||||
jest.setTimeout(10000);
|
||||
|
||||
describe("database-fetcher", () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,19 @@
|
||||
import {
|
||||
mockedObject,
|
||||
mockedOctokitFunction,
|
||||
mockedQuickPickItem,
|
||||
} from "../../../utils/mocking.helpers";
|
||||
import type { GitHubDatabaseConfig } from "../../../../../src/config";
|
||||
import * as dialog from "../../../../../src/common/vscode/dialog";
|
||||
import { listDatabases } from "../../../../../src/databases/github-databases/api";
|
||||
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", () => ({
|
||||
@@ -349,3 +354,186 @@ describe("listDatabases", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user