Merge pull request #3607 from github/koesie10/ghec-dr-variant-analysis

Add GHEC-DR support
This commit is contained in:
Koen Vlaswinkel
2024-05-14 10:40:28 +02:00
committed by GitHub
25 changed files with 345 additions and 101 deletions

View File

@@ -31,4 +31,9 @@ export interface Credentials {
* @returns An OAuth access token, or undefined.
*/
getExistingAccessToken(): Promise<string | undefined>;
/**
* Returns the ID of the authentication provider to use.
*/
authProviderId: string;
}

View File

@@ -29,37 +29,45 @@ function validGitHubNwoOrOwner(
/**
* Extracts an NWO from a GitHub URL.
* @param githubUrl The GitHub repository URL
* @param repositoryUrl The GitHub repository URL
* @param githubUrl The URL of the GitHub instance
* @return The corresponding NWO, or undefined if the URL is not valid
*/
export function getNwoFromGitHubUrl(githubUrl: string): string | undefined {
return getNwoOrOwnerFromGitHubUrl(githubUrl, "nwo");
export function getNwoFromGitHubUrl(
repositoryUrl: string,
githubUrl: URL,
): string | undefined {
return getNwoOrOwnerFromGitHubUrl(repositoryUrl, githubUrl, "nwo");
}
/**
* Extracts an owner from a GitHub URL.
* @param githubUrl The GitHub repository URL
* @param repositoryUrl The GitHub repository URL
* @param githubUrl The URL of the GitHub instance
* @return The corresponding Owner, or undefined if the URL is not valid
*/
export function getOwnerFromGitHubUrl(githubUrl: string): string | undefined {
return getNwoOrOwnerFromGitHubUrl(githubUrl, "owner");
export function getOwnerFromGitHubUrl(
repositoryUrl: string,
githubUrl: URL,
): string | undefined {
return getNwoOrOwnerFromGitHubUrl(repositoryUrl, githubUrl, "owner");
}
function getNwoOrOwnerFromGitHubUrl(
githubUrl: string,
repositoryUrl: string,
githubUrl: URL,
kind: "owner" | "nwo",
): string | undefined {
const validHostnames = [githubUrl.hostname, `www.${githubUrl.hostname}`];
try {
let paths: string[];
const urlElements = githubUrl.split("/");
if (
urlElements[0] === "github.com" ||
urlElements[0] === "www.github.com"
) {
paths = githubUrl.split("/").slice(1);
const urlElements = repositoryUrl.split("/");
if (validHostnames.includes(urlElements[0])) {
paths = repositoryUrl.split("/").slice(1);
} else {
const uri = new URL(githubUrl);
if (uri.hostname !== "github.com" && uri.hostname !== "www.github.com") {
const uri = new URL(repositoryUrl);
if (!validHostnames.includes(uri.hostname)) {
return;
}
paths = uri.pathname.split("/").filter((segment: string) => segment);

View File

@@ -2,8 +2,8 @@ import { authentication } from "vscode";
import type { Octokit } from "@octokit/rest";
import type { Credentials } from "../authentication";
import { AppOctokit } from "../octokit";
export const GITHUB_AUTH_PROVIDER_ID = "github";
import { hasGhecDrUri } from "../../config";
import { getOctokitBaseUrl } from "./octokit";
// We need 'repo' scope for triggering workflows, 'gist' scope for exporting results to Gist,
// and 'read:packages' for reading private CodeQL packages.
@@ -16,30 +16,24 @@ const SCOPES = ["repo", "gist", "read:packages"];
*/
export class VSCodeCredentials implements Credentials {
/**
* A specific octokit to return, otherwise a new authenticated octokit will be created when needed.
*/
private octokit: Octokit | undefined;
/**
* Creates or returns an instance of Octokit.
* Creates or returns an instance of Octokit. The returned instance should
* not be stored and reused, as it may become out-of-date with the current
* authentication session.
*
* @returns An instance of Octokit.
*/
async getOctokit(): Promise<Octokit> {
if (this.octokit) {
return this.octokit;
}
const accessToken = await this.getAccessToken();
return new AppOctokit({
auth: accessToken,
baseUrl: getOctokitBaseUrl(),
});
}
async getAccessToken(): Promise<string> {
const session = await authentication.getSession(
GITHUB_AUTH_PROVIDER_ID,
this.authProviderId,
SCOPES,
{ createIfNone: true },
);
@@ -49,11 +43,18 @@ export class VSCodeCredentials implements Credentials {
async getExistingAccessToken(): Promise<string | undefined> {
const session = await authentication.getSession(
GITHUB_AUTH_PROVIDER_ID,
this.authProviderId,
SCOPES,
{ createIfNone: false },
);
return session?.accessToken;
}
public get authProviderId(): string {
if (hasGhecDrUri()) {
return "github-enterprise";
}
return "github";
}
}

View File

@@ -0,0 +1,15 @@
import { getGitHubInstanceApiUrl } from "../../config";
/**
* Returns the Octokit base URL to use based on the GitHub instance URL.
*
* This is necessary because the Octokit base URL should not have a trailing
* slash, but this is included by default in a URL.
*/
export function getOctokitBaseUrl(): string {
let apiUrl = getGitHubInstanceApiUrl().toString();
if (apiUrl.endsWith("/")) {
apiUrl = apiUrl.slice(0, -1);
}
return apiUrl;
}

View File

@@ -108,12 +108,55 @@ export function hasEnterpriseUri(): boolean {
return getEnterpriseUri() !== undefined;
}
/**
* Does the uri look like GHEC-DR?
*/
function isGhecDrUri(uri: Uri | undefined): boolean {
return uri !== undefined && uri.authority.toLowerCase().endsWith(".ghe.com");
}
/**
* Is the GitHub Enterprise URI set to something that looks like GHEC-DR?
*/
export function hasGhecDrUri(): boolean {
const uri = getEnterpriseUri();
return uri !== undefined && uri.authority.toLowerCase().endsWith(".ghe.com");
return isGhecDrUri(uri);
}
/**
* The URI for GitHub.com.
*/
export const GITHUB_URL = new URL("https://github.com");
export const GITHUB_API_URL = new URL("https://api.github.com");
/**
* If the GitHub Enterprise URI is set to something that looks like GHEC-DR, return it.
*/
export function getGhecDrUri(): Uri | undefined {
const uri = getEnterpriseUri();
if (isGhecDrUri(uri)) {
return uri;
} else {
return undefined;
}
}
export function getGitHubInstanceUrl(): URL {
const ghecDrUri = getGhecDrUri();
if (ghecDrUri) {
return new URL(ghecDrUri.toString());
}
return GITHUB_URL;
}
export function getGitHubInstanceApiUrl(): URL {
const ghecDrUri = getGhecDrUri();
if (ghecDrUri) {
const url = new URL(ghecDrUri.toString());
url.hostname = `api.${url.hostname}`;
return url;
}
return GITHUB_API_URL;
}
const ROOT_SETTING = new Setting("codeQL");
@@ -570,6 +613,11 @@ export async function setRemoteControllerRepo(repo: string | undefined) {
export interface VariantAnalysisConfig {
controllerRepo: string | undefined;
showSystemDefinedRepositoryLists: boolean;
/**
* This uses a URL instead of a URI because the URL class is available in
* unit tests and is fully browser-compatible.
*/
githubUrl: URL;
onDidChangeConfiguration?: Event<void>;
}
@@ -591,6 +639,10 @@ export class VariantAnalysisConfigListener
public get showSystemDefinedRepositoryLists(): boolean {
return !hasEnterpriseUri();
}
public get githubUrl(): URL {
return getGitHubInstanceUrl();
}
}
const VARIANT_ANALYSIS_FILTER_RESULTS = new Setting(

View File

@@ -7,6 +7,7 @@ import { AppOctokit } from "../common/octokit";
import type { ProgressCallback } from "../common/vscode/progress";
import { UserCancellationException } from "../common/vscode/progress";
import type { EndpointDefaults } from "@octokit/types";
import { getOctokitBaseUrl } from "../common/vscode/octokit";
export async function getCodeSearchRepositories(
query: string,
@@ -54,6 +55,7 @@ async function provideOctokitWithThrottling(
const octokit = new MyOctokit({
auth,
baseUrl: getOctokitBaseUrl(),
throttle: {
onRateLimit: (retryAfter: number, options: EndpointDefaults): boolean => {
void logger.log(

View File

@@ -29,6 +29,8 @@ import {
addDatabaseSourceToWorkspace,
allowHttp,
downloadTimeout,
getGitHubInstanceUrl,
hasGhecDrUri,
isCanary,
} from "../config";
import { showAndLogInformationMessage } from "../common/logging";
@@ -150,10 +152,11 @@ export class DatabaseFetcher {
maxStep: 2,
});
const instanceUrl = getGitHubInstanceUrl();
const options: InputBoxOptions = {
title:
'Enter a GitHub repository URL or "name with owner" (e.g. https://github.com/github/codeql or github/codeql)',
placeHolder: "https://github.com/<owner>/<repo> or <owner>/<repo>",
title: `Enter a GitHub repository URL or "name with owner" (e.g. ${new URL("/github/codeql", instanceUrl).toString()} or github/codeql)`,
placeHolder: `${new URL("/", instanceUrl).toString()}<owner>/<repo> or <owner>/<repo>`,
ignoreFocusOut: true,
};
@@ -180,12 +183,14 @@ export class DatabaseFetcher {
makeSelected = true,
addSourceArchiveFolder = addDatabaseSourceToWorkspace(),
): Promise<DatabaseItem | undefined> {
const nwo = getNwoFromGitHubUrl(githubRepo) || githubRepo;
const nwo =
getNwoFromGitHubUrl(githubRepo, getGitHubInstanceUrl()) || githubRepo;
if (!isValidGitHubNwo(nwo)) {
throw new Error(`Invalid GitHub repository: ${githubRepo}`);
}
const credentials = isCanary() ? this.app.credentials : undefined;
const credentials =
isCanary() || hasGhecDrUri() ? this.app.credentials : undefined;
const octokit = credentials
? await credentials.getOctokit()

View File

@@ -3,6 +3,7 @@ import type { Octokit } from "@octokit/rest";
import type { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
import { showNeverAskAgainDialog } from "../../common/vscode/dialog";
import type { GitHubDatabaseConfig } from "../../config";
import { hasGhecDrUri } from "../../config";
import type { Credentials } from "../../common/authentication";
import { AppOctokit } from "../../common/octokit";
import type { ProgressCallback } from "../../common/vscode/progress";
@@ -67,7 +68,10 @@ export async function listDatabases(
credentials: Credentials,
config: GitHubDatabaseConfig,
): Promise<ListDatabasesResult | undefined> {
const hasAccessToken = !!(await credentials.getExistingAccessToken());
// On GHEC-DR, unauthenticated requests will never work, so we should always ask
// for authentication.
const hasAccessToken =
!!(await credentials.getExistingAccessToken()) || hasGhecDrUri();
let octokit = hasAccessToken
? await credentials.getOctokit()

View File

@@ -25,6 +25,7 @@ import type { App } from "../../common/app";
import { QueryLanguage } from "../../common/query-language";
import { getCodeSearchRepositories } from "../code-search-api";
import { showAndLogErrorMessage } from "../../common/logging";
import { getGitHubInstanceUrl } from "../../config";
export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
remoteDatabaseKind: string;
@@ -146,16 +147,19 @@ export class DbPanel extends DisposableObject {
}
private async addNewRemoteRepo(parentList?: string): Promise<void> {
const instanceUrl = getGitHubInstanceUrl();
const repoName = await window.showInputBox({
title: "Add a repository",
prompt: "Insert a GitHub repository URL or name with owner",
placeHolder: "<owner>/<repo> or https://github.com/<owner>/<repo>",
placeHolder: `<owner>/<repo> or ${new URL("/", instanceUrl).toString()}<owner>/<repo>`,
});
if (!repoName) {
return;
}
const nwo = getNwoFromGitHubUrl(repoName) || repoName;
const nwo =
getNwoFromGitHubUrl(repoName, getGitHubInstanceUrl()) || repoName;
if (!isValidGitHubNwo(nwo)) {
void showAndLogErrorMessage(
this.app.logger,
@@ -176,17 +180,20 @@ export class DbPanel extends DisposableObject {
}
private async addNewRemoteOwner(): Promise<void> {
const instanceUrl = getGitHubInstanceUrl();
const ownerName = await window.showInputBox({
title: "Add all repositories of a GitHub org or owner",
prompt: "Insert a GitHub organization or owner name",
placeHolder: "<owner> or https://github.com/<owner>",
placeHolder: `<owner> or ${new URL("/", instanceUrl).toString()}<owner>`,
});
if (!ownerName) {
return;
}
const owner = getOwnerFromGitHubUrl(ownerName) || ownerName;
const owner =
getOwnerFromGitHubUrl(ownerName, getGitHubInstanceUrl()) || ownerName;
if (!isValidGitHubOwner(owner)) {
void showAndLogErrorMessage(
this.app.logger,
@@ -411,7 +418,7 @@ export class DbPanel extends DisposableObject {
if (treeViewItem.dbItem === undefined) {
throw new Error("Unable to open on GitHub. Please select a valid item.");
}
const githubUrl = getGitHubUrl(treeViewItem.dbItem);
const githubUrl = getGitHubUrl(treeViewItem.dbItem, getGitHubInstanceUrl());
if (!githubUrl) {
throw new Error(
"Unable to open on GitHub. Please select a variant analysis repository or owner.",

View File

@@ -62,12 +62,15 @@ function canImportCodeSearch(dbItem: DbItem): boolean {
return DbItemKind.RemoteUserDefinedList === dbItem.kind;
}
export function getGitHubUrl(dbItem: DbItem): string | undefined {
export function getGitHubUrl(
dbItem: DbItem,
githubUrl: URL,
): string | undefined {
switch (dbItem.kind) {
case DbItemKind.RemoteOwner:
return `https://github.com/${dbItem.ownerName}`;
return new URL(`/${dbItem.ownerName}`, githubUrl).toString();
case DbItemKind.RemoteRepo:
return `https://github.com/${dbItem.repoFullName}`;
return new URL(`/${dbItem.repoFullName}`, githubUrl).toString();
default:
return undefined;
}

View File

@@ -31,6 +31,7 @@ import {
joinOrderWarningThreshold,
QueryHistoryConfigListener,
QueryServerConfigListener,
VariantAnalysisConfigListener,
} from "./config";
import {
AstViewer,
@@ -865,8 +866,10 @@ async function activateWithInstalledDistribution(
"variant-analyses",
);
await ensureDir(variantAnalysisStorageDir);
const variantAnalysisConfig = new VariantAnalysisConfigListener();
const variantAnalysisResultsManager = new VariantAnalysisResultsManager(
cliServer,
variantAnalysisConfig,
extLogger,
);
@@ -876,6 +879,7 @@ async function activateWithInstalledDistribution(
variantAnalysisStorageDir,
variantAnalysisResultsManager,
dbModule.dbManager,
variantAnalysisConfig,
);
ctx.subscriptions.push(variantAnalysisManager);
ctx.subscriptions.push(variantAnalysisResultsManager);

View File

@@ -7,6 +7,7 @@ import {
getActionsWorkflowRunUrl as getVariantAnalysisActionsWorkflowRunUrl,
} from "../variant-analysis/shared/variant-analysis";
import type { QueryLanguage } from "../common/query-language";
import { getGitHubInstanceUrl } from "../config";
export type QueryHistoryInfo = LocalQueryInfo | VariantAnalysisHistoryItem;
@@ -79,5 +80,8 @@ export function buildRepoLabel(item: VariantAnalysisHistoryItem): string {
export function getActionsWorkflowRunUrl(
item: VariantAnalysisHistoryItem,
): string {
return getVariantAnalysisActionsWorkflowRunUrl(item.variantAnalysis);
return getVariantAnalysisActionsWorkflowRunUrl(
item.variantAnalysis,
getGitHubInstanceUrl(),
);
}

View File

@@ -15,6 +15,7 @@ type ErrorResponse = {
export function handleRequestError(
e: RequestError,
githubUrl: URL,
logger: NotificationLogger,
): boolean {
if (e.status !== 422) {
@@ -60,9 +61,12 @@ export function handleRequestError(
return false;
}
const createBranchURL = `https://github.com/${
missingDefaultBranchError.repository
}/new/${encodeURIComponent(missingDefaultBranchError.default_branch)}`;
const createBranchURL = new URL(
`/${
missingDefaultBranchError.repository
}/new/${encodeURIComponent(missingDefaultBranchError.default_branch)}`,
githubUrl,
).toString();
void showAndLogErrorMessage(
logger,

View File

@@ -295,10 +295,14 @@ export function getSkippedRepoCount(
export function getActionsWorkflowRunUrl(
variantAnalysis: VariantAnalysis,
githubUrl: URL,
): string {
const {
actionsWorkflowRunId,
controllerRepo: { fullName },
} = variantAnalysis;
return `https://github.com/${fullName}/actions/runs/${actionsWorkflowRunId}`;
return new URL(
`/${fullName}/actions/runs/${actionsWorkflowRunId}`,
githubUrl,
).toString();
}

View File

@@ -78,7 +78,6 @@ import {
REPO_STATES_FILENAME,
writeRepoStates,
} from "./repo-states-store";
import { GITHUB_AUTH_PROVIDER_ID } from "../common/vscode/authentication";
import { FetchError } from "node-fetch";
import {
showAndLogExceptionWithTelemetry,
@@ -98,6 +97,7 @@ import { findVariantAnalysisQlPackRoot } from "./ql";
import { resolveCodeScanningQueryPack } from "./code-scanning-pack";
import { isSarifResultsQueryKind } from "../common/query-metadata";
import { isVariantAnalysisEnabledForGitHubHost } from "./ghec-dr";
import type { VariantAnalysisConfig } from "../config";
import { getEnterpriseUri } from "../config";
const maxRetryCount = 3;
@@ -158,6 +158,7 @@ export class VariantAnalysisManager
private readonly storagePath: string,
private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager,
private readonly dbManager: DbManager,
private readonly config: VariantAnalysisConfig,
) {
super();
this.variantAnalysisMonitor = this.push(
@@ -426,7 +427,10 @@ export class VariantAnalysisManager
);
} catch (e: unknown) {
// If the error is handled by the handleRequestError function, we don't need to throw
if (e instanceof RequestError && handleRequestError(e, this.app.logger)) {
if (
e instanceof RequestError &&
handleRequestError(e, this.config.githubUrl, this.app.logger)
) {
return undefined;
}
@@ -745,7 +749,7 @@ export class VariantAnalysisManager
private async onDidChangeSessions(
event: AuthenticationSessionsChangeEvent,
): Promise<void> {
if (event.provider.id !== GITHUB_AUTH_PROVIDER_ID) {
if (event.provider.id !== this.app.credentials.authProviderId) {
return;
}
@@ -951,7 +955,10 @@ export class VariantAnalysisManager
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
}
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(variantAnalysis);
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(
variantAnalysis,
this.config.githubUrl,
);
await this.app.commands.execute(
"vscode.open",

View File

@@ -23,6 +23,7 @@ import { DisposableObject } from "../common/disposable-object";
import { EventEmitter } from "vscode";
import { unzipToDirectoryConcurrently } from "../common/unzip-concurrently";
import { readRepoTask, writeRepoTask } from "./repo-tasks-store";
import type { VariantAnalysisConfig } from "../config";
type CacheKey = `${number}/${string}`;
@@ -62,6 +63,7 @@ export class VariantAnalysisResultsManager extends DisposableObject {
constructor(
private readonly cliServer: CodeQLCliServer,
private readonly config: VariantAnalysisConfig,
private readonly logger: Logger,
) {
super();
@@ -192,7 +194,7 @@ export class VariantAnalysisResultsManager extends DisposableObject {
throw new Error("Missing database commit SHA");
}
const fileLinkPrefix = this.createGitHubDotcomFileLinkPrefix(
const fileLinkPrefix = this.createGitHubFileLinkPrefix(
repoTask.repository.fullName,
repoTask.databaseCommitSha,
);
@@ -283,11 +285,11 @@ export class VariantAnalysisResultsManager extends DisposableObject {
return join(variantAnalysisStoragePath, fullName);
}
private createGitHubDotcomFileLinkPrefix(
fullName: string,
sha: string,
): string {
return `https://github.com/${fullName}/blob/${sha}`;
private createGitHubFileLinkPrefix(fullName: string, sha: string): string {
return new URL(
`/${fullName}/blob/${sha}`,
this.config.githubUrl,
).toString();
}
public removeAnalysisResults(variantAnalysis: VariantAnalysis) {

View File

@@ -15,6 +15,7 @@ function makeTestOctokit(octokit: Octokit): Credentials {
"getExistingAccessToken not supported by test credentials",
);
},
authProviderId: "github",
};
}

View File

@@ -4,6 +4,7 @@ export function createMockVariantAnalysisConfig(): VariantAnalysisConfig {
return {
controllerRepo: "foo/bar",
showSystemDefinedRepositoryLists: true,
githubUrl: new URL("https://github.com"),
onDidChangeConfiguration: jest.fn(),
};
}

View File

@@ -6,6 +6,8 @@ import {
} from "../../../src/common/github-url-identifier-helper";
describe("github url identifier helper", () => {
const githubUrl = new URL("https://github.com");
describe("valid GitHub Nwo Or Owner method", () => {
it("should return true for valid owner", () => {
expect(isValidGitHubOwner("github")).toBe(true);
@@ -23,51 +25,96 @@ describe("github url identifier helper", () => {
describe("getNwoFromGitHubUrl method", () => {
it("should handle invalid urls", () => {
expect(getNwoFromGitHubUrl("")).toBe(undefined);
expect(getNwoFromGitHubUrl("https://ww.github.com/foo/bar")).toBe(
expect(getNwoFromGitHubUrl("", githubUrl)).toBe(undefined);
expect(
getNwoFromGitHubUrl("https://ww.github.com/foo/bar", githubUrl),
).toBe(undefined);
expect(
getNwoFromGitHubUrl("https://tenant.ghe.com/foo/bar", githubUrl),
).toBe(undefined);
expect(getNwoFromGitHubUrl("https://www.github.com/foo", githubUrl)).toBe(
undefined,
);
expect(getNwoFromGitHubUrl("https://www.github.com/foo")).toBe(undefined);
expect(getNwoFromGitHubUrl("foo")).toBe(undefined);
expect(getNwoFromGitHubUrl("foo/bar")).toBe(undefined);
expect(getNwoFromGitHubUrl("foo", githubUrl)).toBe(undefined);
expect(getNwoFromGitHubUrl("foo/bar", githubUrl)).toBe(undefined);
});
it("should handle valid urls", () => {
expect(getNwoFromGitHubUrl("github.com/foo/bar")).toBe("foo/bar");
expect(getNwoFromGitHubUrl("www.github.com/foo/bar")).toBe("foo/bar");
expect(getNwoFromGitHubUrl("https://github.com/foo/bar")).toBe("foo/bar");
expect(getNwoFromGitHubUrl("http://github.com/foo/bar")).toBe("foo/bar");
expect(getNwoFromGitHubUrl("https://www.github.com/foo/bar")).toBe(
expect(getNwoFromGitHubUrl("github.com/foo/bar", githubUrl)).toBe(
"foo/bar",
);
expect(getNwoFromGitHubUrl("https://github.com/foo/bar/sub/pages")).toBe(
expect(getNwoFromGitHubUrl("www.github.com/foo/bar", githubUrl)).toBe(
"foo/bar",
);
expect(getNwoFromGitHubUrl("https://github.com/foo/bar", githubUrl)).toBe(
"foo/bar",
);
expect(getNwoFromGitHubUrl("http://github.com/foo/bar", githubUrl)).toBe(
"foo/bar",
);
expect(
getNwoFromGitHubUrl("https://www.github.com/foo/bar", githubUrl),
).toBe("foo/bar");
expect(
getNwoFromGitHubUrl("https://github.com/foo/bar/sub/pages", githubUrl),
).toBe("foo/bar");
expect(
getNwoFromGitHubUrl(
"https://tenant.ghe.com/foo/bar",
new URL("https://tenant.ghe.com"),
),
).toBe("foo/bar");
});
});
describe("getOwnerFromGitHubUrl method", () => {
it("should handle invalid urls", () => {
expect(getOwnerFromGitHubUrl("")).toBe(undefined);
expect(getOwnerFromGitHubUrl("https://ww.github.com/foo/bar")).toBe(
undefined,
);
expect(getOwnerFromGitHubUrl("foo")).toBe(undefined);
expect(getOwnerFromGitHubUrl("foo/bar")).toBe(undefined);
expect(getOwnerFromGitHubUrl("", githubUrl)).toBe(undefined);
expect(
getOwnerFromGitHubUrl("https://ww.github.com/foo/bar", githubUrl),
).toBe(undefined);
expect(
getOwnerFromGitHubUrl("https://tenant.ghe.com/foo/bar", githubUrl),
).toBe(undefined);
expect(getOwnerFromGitHubUrl("foo", githubUrl)).toBe(undefined);
expect(getOwnerFromGitHubUrl("foo/bar", githubUrl)).toBe(undefined);
});
it("should handle valid urls", () => {
expect(getOwnerFromGitHubUrl("http://github.com/foo/bar")).toBe("foo");
expect(getOwnerFromGitHubUrl("https://github.com/foo/bar")).toBe("foo");
expect(getOwnerFromGitHubUrl("https://www.github.com/foo/bar")).toBe(
expect(
getOwnerFromGitHubUrl("http://github.com/foo/bar", githubUrl),
).toBe("foo");
expect(
getOwnerFromGitHubUrl("https://github.com/foo/bar", githubUrl),
).toBe("foo");
expect(
getOwnerFromGitHubUrl("https://www.github.com/foo/bar", githubUrl),
).toBe("foo");
expect(
getOwnerFromGitHubUrl(
"https://github.com/foo/bar/sub/pages",
githubUrl,
),
).toBe("foo");
expect(
getOwnerFromGitHubUrl("https://www.github.com/foo", githubUrl),
).toBe("foo");
expect(getOwnerFromGitHubUrl("github.com/foo", githubUrl)).toBe("foo");
expect(getOwnerFromGitHubUrl("www.github.com/foo", githubUrl)).toBe(
"foo",
);
expect(
getOwnerFromGitHubUrl("https://github.com/foo/bar/sub/pages"),
getOwnerFromGitHubUrl(
"https://tenant.ghe.com/foo/bar",
new URL("https://tenant.ghe.com"),
),
).toBe("foo");
expect(
getOwnerFromGitHubUrl(
"https://tenant.ghe.com/foo",
new URL("https://tenant.ghe.com"),
),
).toBe("foo");
expect(getOwnerFromGitHubUrl("https://www.github.com/foo")).toBe("foo");
expect(getOwnerFromGitHubUrl("github.com/foo")).toBe("foo");
expect(getOwnerFromGitHubUrl("www.github.com/foo")).toBe("foo");
});
});
});

View File

@@ -92,32 +92,52 @@ describe("getDbItemActions", () => {
});
describe("getGitHubUrl", () => {
it("should return the correct url for a remote owner", () => {
const githubUrl = new URL("https://github.com");
it("should return the correct url for a remote owner with github.com", () => {
const dbItem = createRemoteOwnerDbItem();
const actualUrl = getGitHubUrl(dbItem);
const actualUrl = getGitHubUrl(dbItem, githubUrl);
const expectedUrl = `https://github.com/${dbItem.ownerName}`;
expect(actualUrl).toEqual(expectedUrl);
});
it("should return the correct url for a remote repo", () => {
it("should return the correct url for a remote owner with GHEC-DR", () => {
const dbItem = createRemoteOwnerDbItem();
const actualUrl = getGitHubUrl(dbItem, new URL("https://tenant.ghe.com"));
const expectedUrl = `https://tenant.ghe.com/${dbItem.ownerName}`;
expect(actualUrl).toEqual(expectedUrl);
});
it("should return the correct url for a remote repo with github.com", () => {
const dbItem = createRemoteRepoDbItem();
const actualUrl = getGitHubUrl(dbItem);
const actualUrl = getGitHubUrl(dbItem, githubUrl);
const expectedUrl = `https://github.com/${dbItem.repoFullName}`;
expect(actualUrl).toEqual(expectedUrl);
});
it("should return the correct url for a remote repo with GHEC-DR", () => {
const dbItem = createRemoteRepoDbItem();
const actualUrl = getGitHubUrl(dbItem, new URL("https://tenant.ghe.com"));
const expectedUrl = `https://tenant.ghe.com/${dbItem.repoFullName}`;
expect(actualUrl).toEqual(expectedUrl);
});
it("should return undefined for other remote db items", () => {
const dbItem0 = createRootRemoteDbItem();
const dbItem1 = createRemoteSystemDefinedListDbItem();
const dbItem2 = createRemoteUserDefinedListDbItem();
const actualUrl0 = getGitHubUrl(dbItem0);
const actualUrl1 = getGitHubUrl(dbItem1);
const actualUrl2 = getGitHubUrl(dbItem2);
const actualUrl0 = getGitHubUrl(dbItem0, githubUrl);
const actualUrl1 = getGitHubUrl(dbItem1, githubUrl);
const actualUrl2 = getGitHubUrl(dbItem2, githubUrl);
expect(actualUrl0).toBeUndefined();
expect(actualUrl1).toBeUndefined();

View File

@@ -151,13 +151,29 @@ describe("isVariantAnalysisComplete", () => {
});
describe("getActionsWorkflowRunUrl", () => {
it("should get the run url", () => {
it("should get the run url on github.com", () => {
const variantAnalysis = createMockVariantAnalysis({});
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(variantAnalysis);
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(
variantAnalysis,
new URL("https://github.com"),
);
expect(actionsWorkflowRunUrl).toBe(
`https://github.com/${variantAnalysis.controllerRepo.fullName}/actions/runs/${variantAnalysis.actionsWorkflowRunId}`,
);
});
it("should get the run url on GHEC-DR", () => {
const variantAnalysis = createMockVariantAnalysis({});
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(
variantAnalysis,
new URL("https://tenant.ghe.com"),
);
expect(actionsWorkflowRunUrl).toBe(
`https://tenant.ghe.com/${variantAnalysis.controllerRepo.fullName}/actions/runs/${variantAnalysis.actionsWorkflowRunId}`,
);
});
});

View File

@@ -4,13 +4,14 @@ import { handleRequestError } from "../../../src/variant-analysis/custom-errors"
import { faker } from "@faker-js/faker";
describe("handleRequestError", () => {
const githubUrl = new URL("https://github.com");
const logger = createMockLogger();
it("returns false when handling a non-422 error", () => {
const e = mockRequestError(404, {
message: "Not Found",
});
expect(handleRequestError(e, logger)).toBe(false);
expect(handleRequestError(e, githubUrl, logger)).toBe(false);
expect(logger.showErrorMessage).not.toHaveBeenCalled();
});
@@ -19,13 +20,13 @@ describe("handleRequestError", () => {
message:
"Unable to trigger a variant analysis. None of the requested repositories could be found.",
});
expect(handleRequestError(e, logger)).toBe(false);
expect(handleRequestError(e, githubUrl, logger)).toBe(false);
expect(logger.showErrorMessage).not.toHaveBeenCalled();
});
it("returns false when handling an error without response body", () => {
const e = mockRequestError(422, undefined);
expect(handleRequestError(e, logger)).toBe(false);
expect(handleRequestError(e, githubUrl, logger)).toBe(false);
expect(logger.showErrorMessage).not.toHaveBeenCalled();
});
@@ -42,7 +43,7 @@ describe("handleRequestError", () => {
},
},
});
expect(handleRequestError(e, logger)).toBe(false);
expect(handleRequestError(e, githubUrl, logger)).toBe(false);
expect(logger.showErrorMessage).not.toHaveBeenCalled();
});
@@ -58,7 +59,7 @@ describe("handleRequestError", () => {
},
],
});
expect(handleRequestError(e, logger)).toBe(false);
expect(handleRequestError(e, githubUrl, logger)).toBe(false);
expect(logger.showErrorMessage).not.toHaveBeenCalled();
});
@@ -75,7 +76,7 @@ describe("handleRequestError", () => {
},
],
});
expect(handleRequestError(e, logger)).toBe(false);
expect(handleRequestError(e, githubUrl, logger)).toBe(false);
expect(logger.showErrorMessage).not.toHaveBeenCalled();
});
@@ -92,11 +93,11 @@ describe("handleRequestError", () => {
},
],
});
expect(handleRequestError(e, logger)).toBe(false);
expect(handleRequestError(e, githubUrl, logger)).toBe(false);
expect(logger.showErrorMessage).not.toHaveBeenCalled();
});
it("shows notification when handling a missing default branch error", () => {
it("shows notification when handling a missing default branch error with github.com URL", () => {
const e = mockRequestError(422, {
message:
"Variant analysis failed because controller repository github/pickles does not have a branch 'main'. Please create a 'main' branch in the repository and re-run the variant analysis.",
@@ -110,11 +111,33 @@ describe("handleRequestError", () => {
},
],
});
expect(handleRequestError(e, logger)).toBe(true);
expect(handleRequestError(e, githubUrl, logger)).toBe(true);
expect(logger.showErrorMessage).toHaveBeenCalledWith(
"Variant analysis failed because the controller repository github/pickles does not have a branch 'main'. Please create a 'main' branch by clicking [here](https://github.com/github/pickles/new/main) and re-run the variant analysis query.",
);
});
it("shows notification when handling a missing default branch error with GHEC-DR URL", () => {
const e = mockRequestError(422, {
message:
"Variant analysis failed because controller repository github/pickles does not have a branch 'main'. Please create a 'main' branch in the repository and re-run the variant analysis.",
errors: [
{
resource: "Repository",
field: "default_branch",
code: "missing",
repository: "github/pickles",
default_branch: "main",
},
],
});
expect(
handleRequestError(e, new URL("https://tenant.ghe.com"), logger),
).toBe(true);
expect(logger.showErrorMessage).toHaveBeenCalledWith(
"Variant analysis failed because the controller repository github/pickles does not have a branch 'main'. Please create a 'main' branch by clicking [here](https://tenant.ghe.com/github/pickles/new/main) and re-run the variant analysis query.",
);
});
});
function mockRequestError(status: number, body: any): RequestError {

View File

@@ -78,8 +78,10 @@ describe("Variant Analysis Manager", () => {
new DbConfigStore(app),
createMockVariantAnalysisConfig(),
);
const variantAnalysisConfig = createMockVariantAnalysisConfig();
variantAnalysisResultsManager = new VariantAnalysisResultsManager(
cli,
variantAnalysisConfig,
extLogger,
);
variantAnalysisManager = new VariantAnalysisManager(
@@ -88,6 +90,7 @@ describe("Variant Analysis Manager", () => {
storagePath,
variantAnalysisResultsManager,
dbManager,
variantAnalysisConfig,
);
});

View File

@@ -16,6 +16,7 @@ import type {
VariantAnalysisScannedRepositoryResult,
} from "../../../../src/variant-analysis/shared/variant-analysis";
import { mockedObject } from "../../utils/mocking.helpers";
import { createMockVariantAnalysisConfig } from "../../../factories/config";
jest.setTimeout(10_000);
@@ -27,8 +28,10 @@ describe(VariantAnalysisResultsManager.name, () => {
variantAnalysisId = faker.number.int();
const cli = mockedObject<CodeQLCliServer>({});
const variantAnalysisConfig = createMockVariantAnalysisConfig();
variantAnalysisResultsManager = new VariantAnalysisResultsManager(
cli,
variantAnalysisConfig,
extLogger,
);
});

View File

@@ -56,8 +56,10 @@ describe("Variant Analysis Manager", () => {
new DbConfigStore(app),
createMockVariantAnalysisConfig(),
);
const variantAnalysisConfig = createMockVariantAnalysisConfig();
const variantAnalysisResultsManager = new VariantAnalysisResultsManager(
cli,
variantAnalysisConfig,
extLogger,
);
variantAnalysisManager = new VariantAnalysisManager(
@@ -66,6 +68,7 @@ describe("Variant Analysis Manager", () => {
storagePath,
variantAnalysisResultsManager,
dbManager,
variantAnalysisConfig,
);
});