Merge pull request #1892 from github/robertbrignull/undefined_credentials

Simplify the credentials class, and clear up impossible error cases
This commit is contained in:
Robert 2022-12-22 11:20:09 +00:00 коммит произвёл GitHub
Родитель be79d68271 6285ba7632
Коммит 25dd679b7d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
14 изменённых файлов: 520 добавлений и 694 удалений

Просмотреть файл

@ -13,102 +13,59 @@ const SCOPES = ["repo", "gist"];
* Handles authentication to GitHub, using the VS Code [authentication API](https://code.visualstudio.com/api/references/vscode-api#authentication). * Handles authentication to GitHub, using the VS Code [authentication API](https://code.visualstudio.com/api/references/vscode-api#authentication).
*/ */
export class Credentials { export class Credentials {
/**
* A specific octokit to return, otherwise a new authenticated octokit will be created when needed.
*/
private octokit: Octokit.Octokit | undefined; private octokit: Octokit.Octokit | undefined;
// Explicitly make the constructor private, so that we can't accidentally call the constructor from outside the class // Explicitly make the constructor private, so that we can't accidentally call the constructor from outside the class
// without also initializing the class. // without also initializing the class.
// eslint-disable-next-line @typescript-eslint/no-empty-function private constructor(octokit?: Octokit.Octokit) {
private constructor() {} this.octokit = octokit;
}
/** /**
* Initializes an instance of credentials with an octokit instance. * Initializes a Credentials instance. This will generate octokit instances
* authenticated as the user. If there is not already an authenticated GitHub
* session available then the user will be prompted to log in.
* *
* Do not call this method until you know you actually need an instance of credentials.
* since calling this method will require the user to log in.
*
* @param context The extension context.
* @returns An instance of credentials. * @returns An instance of credentials.
*/ */
static async initialize( static async initialize(): Promise<Credentials> {
context: vscode.ExtensionContext, return new Credentials();
): Promise<Credentials> {
const c = new Credentials();
c.registerListeners(context);
c.octokit = await c.createOctokit(false);
return c;
} }
/** /**
* Initializes an instance of credentials with an octokit instance using * Initializes an instance of credentials with an octokit instance using
* a token from the user's GitHub account. This method is meant to be * a specific known token. This method is meant to be used in
* used non-interactive environments such as tests. * non-interactive environments such as tests.
* *
* @param overrideToken The GitHub token to use for authentication. * @param overrideToken The GitHub token to use for authentication.
* @returns An instance of credentials. * @returns An instance of credentials.
*/ */
static async initializeWithToken(overrideToken: string) { static async initializeWithToken(overrideToken: string) {
const c = new Credentials(); return new Credentials(new Octokit.Octokit({ auth: overrideToken, retry }));
c.octokit = await c.createOctokit(false, overrideToken);
return c;
}
private async createOctokit(
createIfNone: boolean,
overrideToken?: string,
): Promise<Octokit.Octokit | undefined> {
if (overrideToken) {
return new Octokit.Octokit({ auth: overrideToken, retry });
}
const session = await vscode.authentication.getSession(
GITHUB_AUTH_PROVIDER_ID,
SCOPES,
{ createIfNone },
);
if (session) {
return new Octokit.Octokit({
auth: session.accessToken,
retry,
});
} else {
return undefined;
}
}
registerListeners(context: vscode.ExtensionContext): void {
// Sessions are changed when a user logs in or logs out.
context.subscriptions.push(
vscode.authentication.onDidChangeSessions(async (e) => {
if (e.provider.id === GITHUB_AUTH_PROVIDER_ID) {
this.octokit = await this.createOctokit(false);
}
}),
);
} }
/** /**
* Creates or returns an instance of Octokit. * Creates or returns an instance of Octokit.
* *
* @param requireAuthentication Whether the Octokit instance needs to be authenticated as user.
* @returns An instance of Octokit. * @returns An instance of Octokit.
*/ */
async getOctokit(requireAuthentication = true): Promise<Octokit.Octokit> { async getOctokit(): Promise<Octokit.Octokit> {
if (this.octokit) { if (this.octokit) {
return this.octokit; return this.octokit;
} }
this.octokit = await this.createOctokit(requireAuthentication); const session = await vscode.authentication.getSession(
GITHUB_AUTH_PROVIDER_ID,
SCOPES,
{ createIfNone: true },
);
if (!this.octokit) { return new Octokit.Octokit({
if (requireAuthentication) { auth: session.accessToken,
throw new Error("Did not initialize Octokit."); retry,
} });
// We don't want to set this in this.octokit because that would prevent
// authenticating when requireCredentials is true.
return new Octokit.Octokit({ retry });
}
return this.octokit;
} }
} }

Просмотреть файл

@ -106,7 +106,7 @@ export async function promptImportGithubDatabase(
} }
const octokit = credentials const octokit = credentials
? await credentials.getOctokit(true) ? await credentials.getOctokit()
: new Octokit.Octokit({ retry }); : new Octokit.Octokit({ retry });
const result = await convertGithubNwoToDatabaseUrl(nwo, octokit, progress); const result = await convertGithubNwoToDatabaseUrl(nwo, octokit, progress);

Просмотреть файл

@ -591,7 +591,7 @@ async function activateWithInstalledDistribution(
qs, qs,
getContextStoragePath(ctx), getContextStoragePath(ctx),
ctx.extensionPath, ctx.extensionPath,
() => Credentials.initialize(ctx), () => Credentials.initialize(),
); );
databaseUI.init(); databaseUI.init();
ctx.subscriptions.push(databaseUI); ctx.subscriptions.push(databaseUI);
@ -1236,7 +1236,7 @@ async function activateWithInstalledDistribution(
commandRunner( commandRunner(
"codeQL.exportRemoteQueryResults", "codeQL.exportRemoteQueryResults",
async (queryId: string) => { async (queryId: string) => {
await exportRemoteQueryResults(qhm, rqm, ctx, queryId); await exportRemoteQueryResults(qhm, rqm, queryId);
}, },
), ),
); );
@ -1251,7 +1251,6 @@ async function activateWithInstalledDistribution(
filterSort?: RepositoriesFilterSortStateWithIds, filterSort?: RepositoriesFilterSortStateWithIds,
) => { ) => {
await exportVariantAnalysisResults( await exportVariantAnalysisResults(
ctx,
variantAnalysisManager, variantAnalysisManager,
variantAnalysisId, variantAnalysisId,
filterSort, filterSort,
@ -1356,7 +1355,7 @@ async function activateWithInstalledDistribution(
"codeQL.chooseDatabaseGithub", "codeQL.chooseDatabaseGithub",
async (progress: ProgressCallback, token: CancellationToken) => { async (progress: ProgressCallback, token: CancellationToken) => {
const credentials = isCanary() const credentials = isCanary()
? await Credentials.initialize(ctx) ? await Credentials.initialize()
: undefined; : undefined;
await databaseUI.handleChooseDatabaseGithub( await databaseUI.handleChooseDatabaseGithub(
credentials, credentials,
@ -1411,7 +1410,7 @@ async function activateWithInstalledDistribution(
* Credentials for authenticating to GitHub. * Credentials for authenticating to GitHub.
* These are used when making API calls. * These are used when making API calls.
*/ */
const credentials = await Credentials.initialize(ctx); const credentials = await Credentials.initialize();
const octokit = await credentials.getOctokit(); const octokit = await credentials.getOctokit();
const userInfo = await octokit.users.getAuthenticated(); const userInfo = await octokit.users.getAuthenticated();
void showAndLogInformationMessage( void showAndLogInformationMessage(

Просмотреть файл

@ -397,7 +397,7 @@ export class QueryHistoryManager extends DisposableObject {
private readonly variantAnalysisManager: VariantAnalysisManager, private readonly variantAnalysisManager: VariantAnalysisManager,
private readonly evalLogViewer: EvalLogViewer, private readonly evalLogViewer: EvalLogViewer,
private readonly queryStorageDir: string, private readonly queryStorageDir: string,
private readonly ctx: ExtensionContext, ctx: ExtensionContext,
private readonly queryHistoryConfigListener: QueryHistoryConfig, private readonly queryHistoryConfigListener: QueryHistoryConfig,
private readonly labelProvider: HistoryItemLabelProvider, private readonly labelProvider: HistoryItemLabelProvider,
private readonly doCompareCallback: ( private readonly doCompareCallback: (
@ -633,7 +633,7 @@ export class QueryHistoryManager extends DisposableObject {
} }
private getCredentials() { private getCredentials() {
return Credentials.initialize(this.ctx); return Credentials.initialize();
} }
/** /**

Просмотреть файл

@ -1,7 +1,7 @@
import { pathExists } from "fs-extra"; import { pathExists } from "fs-extra";
import { EOL } from "os"; import { EOL } from "os";
import { extname } from "path"; import { extname } from "path";
import { CancellationToken, ExtensionContext } from "vscode"; import { CancellationToken } from "vscode";
import { Credentials } from "../authentication"; import { Credentials } from "../authentication";
import { Logger } from "../common"; import { Logger } from "../common";
@ -26,7 +26,6 @@ export class AnalysesResultsManager {
private readonly analysesResults: Map<string, AnalysisResults[]>; private readonly analysesResults: Map<string, AnalysisResults[]>;
constructor( constructor(
private readonly ctx: ExtensionContext,
private readonly cliServer: CodeQLCliServer, private readonly cliServer: CodeQLCliServer,
readonly storagePath: string, readonly storagePath: string,
private readonly logger: Logger, private readonly logger: Logger,
@ -43,7 +42,7 @@ export class AnalysesResultsManager {
return; return;
} }
const credentials = await Credentials.initialize(this.ctx); const credentials = await Credentials.initialize();
void this.logger.log( void this.logger.log(
`Downloading and processing results for ${analysisSummary.nwo}`, `Downloading and processing results for ${analysisSummary.nwo}`,
@ -77,7 +76,7 @@ export class AnalysesResultsManager {
(x) => !this.isAnalysisInMemory(x), (x) => !this.isAnalysisInMemory(x),
); );
const credentials = await Credentials.initialize(this.ctx); const credentials = await Credentials.initialize();
void this.logger.log("Downloading and processing analyses results"); void this.logger.log("Downloading and processing analyses results");

Просмотреть файл

@ -4,7 +4,6 @@ import { ensureDir, writeFile } from "fs-extra";
import { import {
commands, commands,
CancellationToken, CancellationToken,
ExtensionContext,
Uri, Uri,
ViewColumn, ViewColumn,
window, window,
@ -74,7 +73,6 @@ export async function exportSelectedRemoteQueryResults(
export async function exportRemoteQueryResults( export async function exportRemoteQueryResults(
queryHistoryManager: QueryHistoryManager, queryHistoryManager: QueryHistoryManager,
remoteQueriesManager: RemoteQueriesManager, remoteQueriesManager: RemoteQueriesManager,
ctx: ExtensionContext,
queryId: string, queryId: string,
): Promise<void> { ): Promise<void> {
const queryHistoryItem = queryHistoryManager.getRemoteQueryById(queryId); const queryHistoryItem = queryHistoryManager.getRemoteQueryById(queryId);
@ -107,7 +105,6 @@ export async function exportRemoteQueryResults(
const exportedResultsDirectory = join(exportDirectory, "exported-results"); const exportedResultsDirectory = join(exportDirectory, "exported-results");
await exportRemoteQueryAnalysisResults( await exportRemoteQueryAnalysisResults(
ctx,
exportedResultsDirectory, exportedResultsDirectory,
query, query,
analysesResults, analysesResults,
@ -116,7 +113,6 @@ export async function exportRemoteQueryResults(
} }
export async function exportRemoteQueryAnalysisResults( export async function exportRemoteQueryAnalysisResults(
ctx: ExtensionContext,
exportedResultsPath: string, exportedResultsPath: string,
query: RemoteQuery, query: RemoteQuery,
analysesResults: AnalysisResults[], analysesResults: AnalysisResults[],
@ -126,7 +122,6 @@ export async function exportRemoteQueryAnalysisResults(
const markdownFiles = generateMarkdown(query, analysesResults, exportFormat); const markdownFiles = generateMarkdown(query, analysesResults, exportFormat);
await exportResults( await exportResults(
ctx,
exportedResultsPath, exportedResultsPath,
description, description,
markdownFiles, markdownFiles,
@ -141,7 +136,6 @@ const MAX_VARIANT_ANALYSIS_EXPORT_PROGRESS_STEPS = 2;
* The user is prompted to select the export format. * The user is prompted to select the export format.
*/ */
export async function exportVariantAnalysisResults( export async function exportVariantAnalysisResults(
ctx: ExtensionContext,
variantAnalysisManager: VariantAnalysisManager, variantAnalysisManager: VariantAnalysisManager,
variantAnalysisId: number, variantAnalysisId: number,
filterSort: RepositoriesFilterSortStateWithIds | undefined, filterSort: RepositoriesFilterSortStateWithIds | undefined,
@ -239,7 +233,6 @@ export async function exportVariantAnalysisResults(
); );
await exportVariantAnalysisAnalysisResults( await exportVariantAnalysisAnalysisResults(
ctx,
exportedResultsDirectory, exportedResultsDirectory,
variantAnalysis, variantAnalysis,
getAnalysesResults(), getAnalysesResults(),
@ -251,7 +244,6 @@ export async function exportVariantAnalysisResults(
} }
export async function exportVariantAnalysisAnalysisResults( export async function exportVariantAnalysisAnalysisResults(
ctx: ExtensionContext,
exportedResultsPath: string, exportedResultsPath: string,
variantAnalysis: VariantAnalysis, variantAnalysis: VariantAnalysis,
analysesResults: AsyncIterable< analysesResults: AsyncIterable<
@ -284,7 +276,6 @@ export async function exportVariantAnalysisAnalysisResults(
); );
await exportResults( await exportResults(
ctx,
exportedResultsPath, exportedResultsPath,
description, description,
markdownFiles, markdownFiles,
@ -328,7 +319,6 @@ async function determineExportFormat(): Promise<"gist" | "local" | undefined> {
} }
export async function exportResults( export async function exportResults(
ctx: ExtensionContext,
exportedResultsPath: string, exportedResultsPath: string,
description: string, description: string,
markdownFiles: MarkdownFile[], markdownFiles: MarkdownFile[],
@ -341,7 +331,7 @@ export async function exportResults(
} }
if (exportFormat === "gist") { if (exportFormat === "gist") {
await exportToGist(ctx, description, markdownFiles, progress, token); await exportToGist(description, markdownFiles, progress, token);
} else if (exportFormat === "local") { } else if (exportFormat === "local") {
await exportToLocalMarkdown( await exportToLocalMarkdown(
exportedResultsPath, exportedResultsPath,
@ -353,7 +343,6 @@ export async function exportResults(
} }
export async function exportToGist( export async function exportToGist(
ctx: ExtensionContext,
description: string, description: string,
markdownFiles: MarkdownFile[], markdownFiles: MarkdownFile[],
progress?: ProgressCallback, progress?: ProgressCallback,
@ -365,7 +354,7 @@ export async function exportToGist(
message: "Creating Gist", message: "Creating Gist",
}); });
const credentials = await Credentials.initialize(ctx); const credentials = await Credentials.initialize();
if (token?.isCancellationRequested) { if (token?.isCancellationRequested) {
throw new UserCancellationException("Cancelled"); throw new UserCancellationException("Cancelled");

Просмотреть файл

@ -81,20 +81,19 @@ export class RemoteQueriesManager extends DisposableObject {
private readonly view: RemoteQueriesView; private readonly view: RemoteQueriesView;
constructor( constructor(
private readonly ctx: ExtensionContext, ctx: ExtensionContext,
private readonly cliServer: CodeQLCliServer, private readonly cliServer: CodeQLCliServer,
private readonly storagePath: string, private readonly storagePath: string,
logger: Logger, logger: Logger,
) { ) {
super(); super();
this.analysesResultsManager = new AnalysesResultsManager( this.analysesResultsManager = new AnalysesResultsManager(
ctx,
cliServer, cliServer,
storagePath, storagePath,
logger, logger,
); );
this.view = new RemoteQueriesView(ctx, logger, this.analysesResultsManager); this.view = new RemoteQueriesView(ctx, logger, this.analysesResultsManager);
this.remoteQueriesMonitor = new RemoteQueriesMonitor(ctx, logger); this.remoteQueriesMonitor = new RemoteQueriesMonitor(logger);
this.remoteQueryAddedEventEmitter = this.push( this.remoteQueryAddedEventEmitter = this.push(
new EventEmitter<NewQueryEvent>(), new EventEmitter<NewQueryEvent>(),
@ -160,7 +159,7 @@ export class RemoteQueriesManager extends DisposableObject {
progress: ProgressCallback, progress: ProgressCallback,
token: CancellationToken, token: CancellationToken,
): Promise<void> { ): Promise<void> {
const credentials = await Credentials.initialize(this.ctx); const credentials = await Credentials.initialize();
const { const {
actionBranch, actionBranch,
@ -218,7 +217,7 @@ export class RemoteQueriesManager extends DisposableObject {
remoteQuery: RemoteQuery, remoteQuery: RemoteQuery,
cancellationToken: CancellationToken, cancellationToken: CancellationToken,
): Promise<void> { ): Promise<void> {
const credentials = await Credentials.initialize(this.ctx); const credentials = await Credentials.initialize();
const queryWorkflowResult = await this.remoteQueriesMonitor.monitorQuery( const queryWorkflowResult = await this.remoteQueriesMonitor.monitorQuery(
remoteQuery, remoteQuery,

Просмотреть файл

@ -16,20 +16,13 @@ export class RemoteQueriesMonitor {
private static readonly maxAttemptCount = 17280; private static readonly maxAttemptCount = 17280;
private static readonly sleepTime = 5000; private static readonly sleepTime = 5000;
constructor( constructor(private readonly logger: Logger) {}
private readonly extensionContext: vscode.ExtensionContext,
private readonly logger: Logger,
) {}
public async monitorQuery( public async monitorQuery(
remoteQuery: RemoteQuery, remoteQuery: RemoteQuery,
cancellationToken: vscode.CancellationToken, cancellationToken: vscode.CancellationToken,
): Promise<RemoteQueryWorkflowResult> { ): Promise<RemoteQueryWorkflowResult> {
const credentials = await Credentials.initialize(this.extensionContext); const credentials = await Credentials.initialize();
if (!credentials) {
throw Error("Error authenticating with GitHub");
}
let attemptCount = 0; let attemptCount = 0;

Просмотреть файл

@ -106,7 +106,6 @@ export class VariantAnalysisManager
super(); super();
this.variantAnalysisMonitor = this.push( this.variantAnalysisMonitor = this.push(
new VariantAnalysisMonitor( new VariantAnalysisMonitor(
ctx,
this.shouldCancelMonitorVariantAnalysis.bind(this), this.shouldCancelMonitorVariantAnalysis.bind(this),
), ),
); );
@ -125,7 +124,7 @@ export class VariantAnalysisManager
progress: ProgressCallback, progress: ProgressCallback,
token: CancellationToken, token: CancellationToken,
): Promise<void> { ): Promise<void> {
const credentials = await Credentials.initialize(this.ctx); const credentials = await Credentials.initialize();
const { const {
actionBranch, actionBranch,
@ -479,10 +478,7 @@ export class VariantAnalysisManager
await this.onRepoStateUpdated(variantAnalysis.id, repoState); await this.onRepoStateUpdated(variantAnalysis.id, repoState);
const credentials = await Credentials.initialize(this.ctx); const credentials = await Credentials.initialize();
if (!credentials) {
throw Error("Error authenticating with GitHub");
}
if (cancellationToken && cancellationToken.isCancellationRequested) { if (cancellationToken && cancellationToken.isCancellationRequested) {
repoState.downloadStatus = repoState.downloadStatus =
@ -580,10 +576,7 @@ export class VariantAnalysisManager
); );
} }
const credentials = await Credentials.initialize(this.ctx); const credentials = await Credentials.initialize();
if (!credentials) {
throw Error("Error authenticating with GitHub");
}
void showAndLogInformationMessage( void showAndLogInformationMessage(
"Cancelling variant analysis. This may take a while.", "Cancelling variant analysis. This may take a while.",

Просмотреть файл

@ -1,9 +1,4 @@
import { import { CancellationToken, commands, EventEmitter } from "vscode";
CancellationToken,
commands,
EventEmitter,
ExtensionContext,
} from "vscode";
import { Credentials } from "../authentication"; import { Credentials } from "../authentication";
import { getVariantAnalysis } from "./gh-api/gh-api-client"; import { getVariantAnalysis } from "./gh-api/gh-api-client";
@ -32,7 +27,6 @@ export class VariantAnalysisMonitor extends DisposableObject {
readonly onVariantAnalysisChange = this._onVariantAnalysisChange.event; readonly onVariantAnalysisChange = this._onVariantAnalysisChange.event;
constructor( constructor(
private readonly extensionContext: ExtensionContext,
private readonly shouldCancelMonitor: ( private readonly shouldCancelMonitor: (
variantAnalysisId: number, variantAnalysisId: number,
) => Promise<boolean>, ) => Promise<boolean>,
@ -44,10 +38,7 @@ export class VariantAnalysisMonitor extends DisposableObject {
variantAnalysis: VariantAnalysis, variantAnalysis: VariantAnalysis,
cancellationToken: CancellationToken, cancellationToken: CancellationToken,
): Promise<void> { ): Promise<void> {
const credentials = await Credentials.initialize(this.extensionContext); const credentials = await Credentials.initialize();
if (!credentials) {
throw Error("Error authenticating with GitHub");
}
let attemptCount = 0; let attemptCount = 0;
const scannedReposDownloaded: number[] = []; const scannedReposDownloaded: number[] = [];

Просмотреть файл

@ -142,6 +142,14 @@ describe("Variant Analysis Manager", () => {
beforeEach(async () => { beforeEach(async () => {
writeFileStub.mockRestore(); writeFileStub.mockRestore();
const mockCredentials = {
getOctokit: () =>
Promise.resolve({
request: jest.fn(),
}),
} as unknown as Credentials;
jest.spyOn(Credentials, "initialize").mockResolvedValue(mockCredentials);
// Should not have asked for a language // Should not have asked for a language
showQuickPickSpy = jest showQuickPickSpy = jest
.spyOn(window, "showQuickPick") .spyOn(window, "showQuickPick")
@ -367,269 +375,267 @@ describe("Variant Analysis Manager", () => {
}); });
describe("autoDownloadVariantAnalysisResult", () => { describe("autoDownloadVariantAnalysisResult", () => {
describe("when credentials are invalid", () => { let arrayBuffer: ArrayBuffer;
let getVariantAnalysisRepoStub: jest.SpiedFunction<
typeof ghApiClient.getVariantAnalysisRepo
>;
let getVariantAnalysisRepoResultStub: jest.SpiedFunction<
typeof ghApiClient.getVariantAnalysisRepoResult
>;
beforeEach(async () => {
const mockCredentials = {
getOctokit: () =>
Promise.resolve({
request: jest.fn(),
}),
} as unknown as Credentials;
jest.spyOn(Credentials, "initialize").mockResolvedValue(mockCredentials);
const sourceFilePath = join(
__dirname,
"../../../../src/vscode-tests/cli-integration/data/variant-analysis-results.zip",
);
arrayBuffer = fs.readFileSync(sourceFilePath).buffer;
getVariantAnalysisRepoStub = jest.spyOn(
ghApiClient,
"getVariantAnalysisRepo",
);
getVariantAnalysisRepoResultStub = jest.spyOn(
ghApiClient,
"getVariantAnalysisRepoResult",
);
});
describe("when the artifact_url is missing", () => {
beforeEach(async () => { beforeEach(async () => {
jest const dummyRepoTask = createMockVariantAnalysisRepoTask();
.spyOn(Credentials, "initialize") delete dummyRepoTask.artifact_url;
.mockResolvedValue(undefined as unknown as Credentials);
getVariantAnalysisRepoStub.mockResolvedValue(dummyRepoTask);
getVariantAnalysisRepoResultStub.mockResolvedValue(arrayBuffer);
}); });
it("should return early if credentials are wrong", async () => { it("should not try to download the result", async () => {
try { await variantAnalysisManager.autoDownloadVariantAnalysisResult(
await variantAnalysisManager.autoDownloadVariantAnalysisResult( scannedRepos[0],
scannedRepos[0], variantAnalysis,
variantAnalysis, cancellationTokenSource.token,
cancellationTokenSource.token, );
);
} catch (error: any) { expect(getVariantAnalysisRepoResultStub).not.toHaveBeenCalled();
expect(error.message).toBe("Error authenticating with GitHub");
}
}); });
}); });
describe("when credentials are valid", () => { describe("when the artifact_url is present", () => {
let arrayBuffer: ArrayBuffer; let dummyRepoTask: VariantAnalysisRepoTask;
let getVariantAnalysisRepoStub: jest.SpiedFunction<
typeof ghApiClient.getVariantAnalysisRepo
>;
let getVariantAnalysisRepoResultStub: jest.SpiedFunction<
typeof ghApiClient.getVariantAnalysisRepoResult
>;
beforeEach(async () => { beforeEach(async () => {
const mockCredentials = { dummyRepoTask = createMockVariantAnalysisRepoTask();
getOctokit: () =>
Promise.resolve({
request: jest.fn(),
}),
} as unknown as Credentials;
jest
.spyOn(Credentials, "initialize")
.mockResolvedValue(mockCredentials);
const sourceFilePath = join( getVariantAnalysisRepoStub.mockResolvedValue(dummyRepoTask);
__dirname, getVariantAnalysisRepoResultStub.mockResolvedValue(arrayBuffer);
"../../../../src/vscode-tests/cli-integration/data/variant-analysis-results.zip", });
);
arrayBuffer = fs.readFileSync(sourceFilePath).buffer;
getVariantAnalysisRepoStub = jest.spyOn( it("should return early if variant analysis is cancelled", async () => {
ghApiClient, cancellationTokenSource.cancel();
"getVariantAnalysisRepo",
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
); );
getVariantAnalysisRepoResultStub = jest.spyOn(
ghApiClient, expect(getVariantAnalysisRepoStub).not.toHaveBeenCalled();
"getVariantAnalysisRepoResult", });
it("should fetch a repo task", async () => {
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
);
expect(getVariantAnalysisRepoStub).toHaveBeenCalled();
});
it("should fetch a repo result", async () => {
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
);
expect(getVariantAnalysisRepoResultStub).toHaveBeenCalled();
});
it("should skip the download if the repository has already been downloaded", async () => {
// First, do a download so it is downloaded. This avoids having to mock the repo states.
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
);
getVariantAnalysisRepoStub.mockClear();
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
);
expect(getVariantAnalysisRepoStub).not.toHaveBeenCalled();
});
it("should write the repo state when the download is successful", async () => {
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
);
expect(outputJsonStub).toHaveBeenCalledWith(
join(storagePath, variantAnalysis.id.toString(), "repo_states.json"),
{
[scannedRepos[0].repository.id]: {
repositoryId: scannedRepos[0].repository.id,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
},
},
); );
}); });
describe("when the artifact_url is missing", () => { it("should not write the repo state when the download fails", async () => {
beforeEach(async () => { getVariantAnalysisRepoResultStub.mockRejectedValue(
const dummyRepoTask = createMockVariantAnalysisRepoTask(); new Error("Failed to download"),
delete dummyRepoTask.artifact_url; );
getVariantAnalysisRepoStub.mockResolvedValue(dummyRepoTask); await expect(
getVariantAnalysisRepoResultStub.mockResolvedValue(arrayBuffer); variantAnalysisManager.autoDownloadVariantAnalysisResult(
});
it("should not try to download the result", async () => {
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0], scannedRepos[0],
variantAnalysis, variantAnalysis,
cancellationTokenSource.token, cancellationTokenSource.token,
); ),
).rejects.toThrow();
expect(getVariantAnalysisRepoResultStub).not.toHaveBeenCalled(); expect(outputJsonStub).not.toHaveBeenCalled();
});
}); });
describe("when the artifact_url is present", () => { it("should have a failed repo state when the repo task API fails", async () => {
let dummyRepoTask: VariantAnalysisRepoTask; getVariantAnalysisRepoStub.mockRejectedValueOnce(
new Error("Failed to download"),
);
beforeEach(async () => { await expect(
dummyRepoTask = createMockVariantAnalysisRepoTask(); variantAnalysisManager.autoDownloadVariantAnalysisResult(
getVariantAnalysisRepoStub.mockResolvedValue(dummyRepoTask);
getVariantAnalysisRepoResultStub.mockResolvedValue(arrayBuffer);
});
it("should return early if variant analysis is cancelled", async () => {
cancellationTokenSource.cancel();
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0], scannedRepos[0],
variantAnalysis, variantAnalysis,
cancellationTokenSource.token, cancellationTokenSource.token,
); ),
).rejects.toThrow();
expect(getVariantAnalysisRepoStub).not.toHaveBeenCalled(); expect(outputJsonStub).not.toHaveBeenCalled();
});
it("should fetch a repo task", async () => { await variantAnalysisManager.autoDownloadVariantAnalysisResult(
await variantAnalysisManager.autoDownloadVariantAnalysisResult( scannedRepos[1],
scannedRepos[0], variantAnalysis,
variantAnalysis, cancellationTokenSource.token,
cancellationTokenSource.token, );
);
expect(getVariantAnalysisRepoStub).toHaveBeenCalled(); expect(outputJsonStub).toHaveBeenCalledWith(
}); join(storagePath, variantAnalysis.id.toString(), "repo_states.json"),
{
it("should fetch a repo result", async () => { [scannedRepos[0].repository.id]: {
await variantAnalysisManager.autoDownloadVariantAnalysisResult( repositoryId: scannedRepos[0].repository.id,
scannedRepos[0], downloadStatus:
variantAnalysis, VariantAnalysisScannedRepositoryDownloadStatus.Failed,
cancellationTokenSource.token,
);
expect(getVariantAnalysisRepoResultStub).toHaveBeenCalled();
});
it("should skip the download if the repository has already been downloaded", async () => {
// First, do a download so it is downloaded. This avoids having to mock the repo states.
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
);
getVariantAnalysisRepoStub.mockClear();
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
);
expect(getVariantAnalysisRepoStub).not.toHaveBeenCalled();
});
it("should write the repo state when the download is successful", async () => {
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
);
expect(outputJsonStub).toHaveBeenCalledWith(
join(
storagePath,
variantAnalysis.id.toString(),
"repo_states.json",
),
{
[scannedRepos[0].repository.id]: {
repositoryId: scannedRepos[0].repository.id,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
},
}, },
); [scannedRepos[1].repository.id]: {
}); repositoryId: scannedRepos[1].repository.id,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
},
},
);
});
it("should not write the repo state when the download fails", async () => { it("should have a failed repo state when the download fails", async () => {
getVariantAnalysisRepoResultStub.mockRejectedValue( getVariantAnalysisRepoResultStub.mockRejectedValueOnce(
new Error("Failed to download"), new Error("Failed to download"),
); );
await expect( await expect(
variantAnalysisManager.autoDownloadVariantAnalysisResult( variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0], scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
),
).rejects.toThrow();
expect(outputJsonStub).not.toHaveBeenCalled();
});
it("should have a failed repo state when the repo task API fails", async () => {
getVariantAnalysisRepoStub.mockRejectedValueOnce(
new Error("Failed to download"),
);
await expect(
variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
),
).rejects.toThrow();
expect(outputJsonStub).not.toHaveBeenCalled();
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[1],
variantAnalysis, variantAnalysis,
cancellationTokenSource.token, cancellationTokenSource.token,
); ),
).rejects.toThrow();
expect(outputJsonStub).toHaveBeenCalledWith( expect(outputJsonStub).not.toHaveBeenCalled();
join(
storagePath, await variantAnalysisManager.autoDownloadVariantAnalysisResult(
variantAnalysis.id.toString(), scannedRepos[1],
"repo_states.json", variantAnalysis,
), cancellationTokenSource.token,
{ );
[scannedRepos[0].repository.id]: {
repositoryId: scannedRepos[0].repository.id, expect(outputJsonStub).toHaveBeenCalledWith(
downloadStatus: join(storagePath, variantAnalysis.id.toString(), "repo_states.json"),
VariantAnalysisScannedRepositoryDownloadStatus.Failed, {
}, [scannedRepos[0].repository.id]: {
[scannedRepos[1].repository.id]: { repositoryId: scannedRepos[0].repository.id,
repositoryId: scannedRepos[1].repository.id, downloadStatus:
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Failed,
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
},
}, },
); [scannedRepos[1].repository.id]: {
repositoryId: scannedRepos[1].repository.id,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
},
},
);
});
it("should update the repo state correctly", async () => {
mockRepoStates({
[scannedRepos[1].repository.id]: {
repositoryId: scannedRepos[1].repository.id,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
},
[scannedRepos[2].repository.id]: {
repositoryId: scannedRepos[2].repository.id,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
},
}); });
it("should have a failed repo state when the download fails", async () => { await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis);
getVariantAnalysisRepoResultStub.mockRejectedValueOnce(
new Error("Failed to download"),
);
await expect( expect(pathExistsStub).toBeCalledWith(
variantAnalysisManager.autoDownloadVariantAnalysisResult( join(storagePath, variantAnalysis.id.toString()),
scannedRepos[0], );
variantAnalysis, expect(readJsonStub).toHaveBeenCalledTimes(1);
cancellationTokenSource.token, expect(readJsonStub).toHaveBeenCalledWith(
), join(storagePath, variantAnalysis.id.toString(), "repo_states.json"),
).rejects.toThrow(); );
expect(outputJsonStub).not.toHaveBeenCalled(); pathExistsStub.mockRestore();
await variantAnalysisManager.autoDownloadVariantAnalysisResult( await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[1], scannedRepos[0],
variantAnalysis, variantAnalysis,
cancellationTokenSource.token, cancellationTokenSource.token,
); );
expect(outputJsonStub).toHaveBeenCalledWith( expect(outputJsonStub).toHaveBeenCalledWith(
join( join(storagePath, variantAnalysis.id.toString(), "repo_states.json"),
storagePath, {
variantAnalysis.id.toString(),
"repo_states.json",
),
{
[scannedRepos[0].repository.id]: {
repositoryId: scannedRepos[0].repository.id,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.Failed,
},
[scannedRepos[1].repository.id]: {
repositoryId: scannedRepos[1].repository.id,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
},
},
);
});
it("should update the repo state correctly", async () => {
mockRepoStates({
[scannedRepos[1].repository.id]: { [scannedRepos[1].repository.id]: {
repositoryId: scannedRepos[1].repository.id, repositoryId: scannedRepos[1].repository.id,
downloadStatus: downloadStatus:
@ -640,66 +646,22 @@ describe("Variant Analysis Manager", () => {
downloadStatus: downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.InProgress, VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
}, },
}); [scannedRepos[0].repository.id]: {
repositoryId: scannedRepos[0].repository.id,
await variantAnalysisManager.rehydrateVariantAnalysis( downloadStatus:
variantAnalysis, VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
);
expect(pathExistsStub).toBeCalledWith(
join(storagePath, variantAnalysis.id.toString()),
);
expect(readJsonStub).toHaveBeenCalledTimes(1);
expect(readJsonStub).toHaveBeenCalledWith(
join(
storagePath,
variantAnalysis.id.toString(),
"repo_states.json",
),
);
pathExistsStub.mockRestore();
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
);
expect(outputJsonStub).toHaveBeenCalledWith(
join(
storagePath,
variantAnalysis.id.toString(),
"repo_states.json",
),
{
[scannedRepos[1].repository.id]: {
repositoryId: scannedRepos[1].repository.id,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
},
[scannedRepos[2].repository.id]: {
repositoryId: scannedRepos[2].repository.id,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
},
[scannedRepos[0].repository.id]: {
repositoryId: scannedRepos[0].repository.id,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
},
}, },
); },
}); );
function mockRepoStates(
repoStates: Record<number, VariantAnalysisScannedRepositoryState>,
) {
pathExistsStub.mockImplementation(() => true);
// This will read in the correct repo states
readJsonStub.mockImplementation(() => Promise.resolve(repoStates));
}
}); });
function mockRepoStates(
repoStates: Record<number, VariantAnalysisScannedRepositoryState>,
) {
pathExistsStub.mockImplementation(() => true);
// This will read in the correct repo states
readJsonStub.mockImplementation(() => Promise.resolve(repoStates));
}
}); });
}); });
@ -876,6 +838,8 @@ describe("Variant Analysis Manager", () => {
let variantAnalysisStorageLocation: string; let variantAnalysisStorageLocation: string;
let mockCredentials: Credentials;
beforeEach(async () => { beforeEach(async () => {
variantAnalysis = createMockVariantAnalysis({}); variantAnalysis = createMockVariantAnalysis({});
@ -889,82 +853,54 @@ describe("Variant Analysis Manager", () => {
); );
await createTimestampFile(variantAnalysisStorageLocation); await createTimestampFile(variantAnalysisStorageLocation);
await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis);
mockCredentials = {
getOctokit: () =>
Promise.resolve({
request: jest.fn(),
}),
} as unknown as Credentials;
jest.spyOn(Credentials, "initialize").mockResolvedValue(mockCredentials);
}); });
afterEach(() => { afterEach(() => {
fs.rmSync(variantAnalysisStorageLocation, { recursive: true }); fs.rmSync(variantAnalysisStorageLocation, { recursive: true });
}); });
describe("when the credentials are invalid", () => { it("should return early if the variant analysis is not found", async () => {
beforeEach(async () => { try {
jest await variantAnalysisManager.cancelVariantAnalysis(
.spyOn(Credentials, "initialize") variantAnalysis.id + 100,
.mockResolvedValue(undefined as unknown as Credentials); );
}); } catch (error: any) {
expect(error.message).toBe(
it("should return early", async () => { `No variant analysis with id: ${variantAnalysis.id + 100}`,
try { );
await variantAnalysisManager.cancelVariantAnalysis( }
variantAnalysis.id,
);
} catch (error: any) {
expect(error.message).toBe("Error authenticating with GitHub");
}
});
}); });
describe("when the credentials are valid", () => { it("should return early if the variant analysis does not have an actions workflow run id", async () => {
let mockCredentials: Credentials; await variantAnalysisManager.onVariantAnalysisUpdated({
...variantAnalysis,
beforeEach(async () => { actionsWorkflowRunId: undefined,
mockCredentials = {
getOctokit: () =>
Promise.resolve({
request: jest.fn(),
}),
} as unknown as Credentials;
jest
.spyOn(Credentials, "initialize")
.mockResolvedValue(mockCredentials);
}); });
it("should return early if the variant analysis is not found", async () => { try {
try {
await variantAnalysisManager.cancelVariantAnalysis(
variantAnalysis.id + 100,
);
} catch (error: any) {
expect(error.message).toBe(
`No variant analysis with id: ${variantAnalysis.id + 100}`,
);
}
});
it("should return early if the variant analysis does not have an actions workflow run id", async () => {
await variantAnalysisManager.onVariantAnalysisUpdated({
...variantAnalysis,
actionsWorkflowRunId: undefined,
});
try {
await variantAnalysisManager.cancelVariantAnalysis(
variantAnalysis.id,
);
} catch (error: any) {
expect(error.message).toBe(
`No workflow run id for variant analysis with id: ${variantAnalysis.id}`,
);
}
});
it("should return cancel if valid", async () => {
await variantAnalysisManager.cancelVariantAnalysis(variantAnalysis.id); await variantAnalysisManager.cancelVariantAnalysis(variantAnalysis.id);
} catch (error: any) {
expect(mockCancelVariantAnalysis).toBeCalledWith( expect(error.message).toBe(
mockCredentials, `No workflow run id for variant analysis with id: ${variantAnalysis.id}`,
variantAnalysis,
); );
}); }
});
it("should return cancel if valid", async () => {
await variantAnalysisManager.cancelVariantAnalysis(variantAnalysis.id);
expect(mockCancelVariantAnalysis).toBeCalledWith(
mockCredentials,
variantAnalysis,
);
}); });
}); });

Просмотреть файл

@ -61,10 +61,7 @@ describe("Variant Analysis Monitor", () => {
"GitHub.vscode-codeql", "GitHub.vscode-codeql",
)! )!
.activate(); .activate();
variantAnalysisMonitor = new VariantAnalysisMonitor( variantAnalysisMonitor = new VariantAnalysisMonitor(shouldCancelMonitor);
extension.ctx,
shouldCancelMonitor,
);
variantAnalysisMonitor.onVariantAnalysisChange(onVariantAnalysisChangeSpy); variantAnalysisMonitor.onVariantAnalysisChange(onVariantAnalysisChangeSpy);
variantAnalysisManager = extension.variantAnalysisManager; variantAnalysisManager = extension.variantAnalysisManager;
@ -77,260 +74,237 @@ describe("Variant Analysis Monitor", () => {
.mockRejectedValue(new Error("Not mocked")); .mockRejectedValue(new Error("Not mocked"));
limitNumberOfAttemptsToMonitor(); limitNumberOfAttemptsToMonitor();
const mockCredentials = {
getOctokit: () =>
Promise.resolve({
request: jest.fn(),
}),
} as unknown as Credentials;
jest.spyOn(Credentials, "initialize").mockResolvedValue(mockCredentials);
}); });
describe("when credentials are invalid", () => { it("should return early if variant analysis is cancelled", async () => {
cancellationTokenSource.cancel();
await variantAnalysisMonitor.monitorVariantAnalysis(
variantAnalysis,
cancellationTokenSource.token,
);
expect(onVariantAnalysisChangeSpy).not.toHaveBeenCalled();
});
it("should return early if variant analysis should be cancelled", async () => {
shouldCancelMonitor.mockResolvedValue(true);
await variantAnalysisMonitor.monitorVariantAnalysis(
variantAnalysis,
cancellationTokenSource.token,
);
expect(onVariantAnalysisChangeSpy).not.toHaveBeenCalled();
});
describe("when the variant analysis fails", () => {
let mockFailedApiResponse: VariantAnalysisApiResponse;
beforeEach(async () => { beforeEach(async () => {
jest mockFailedApiResponse = createFailedMockApiResponse();
.spyOn(Credentials, "initialize") mockGetVariantAnalysis.mockResolvedValue(mockFailedApiResponse);
.mockResolvedValue(undefined as unknown as Credentials);
}); });
it("should return early if credentials are wrong", async () => { it("should mark as failed and stop monitoring", async () => {
try { await variantAnalysisMonitor.monitorVariantAnalysis(
variantAnalysis,
cancellationTokenSource.token,
);
expect(mockGetVariantAnalysis).toHaveBeenCalledTimes(1);
expect(onVariantAnalysisChangeSpy).toHaveBeenCalledWith(
expect.objectContaining({
status: VariantAnalysisStatus.Failed,
failureReason: processFailureReason(
mockFailedApiResponse.failure_reason as VariantAnalysisFailureReason,
),
}),
);
});
});
describe("when the variant analysis is in progress", () => {
let mockApiResponse: VariantAnalysisApiResponse;
let scannedRepos: ApiVariantAnalysisScannedRepository[];
let succeededRepos: ApiVariantAnalysisScannedRepository[];
describe("when there are successfully scanned repos", () => {
beforeEach(async () => {
scannedRepos = createMockScannedRepos([
"pending",
"pending",
"in_progress",
"in_progress",
"succeeded",
"succeeded",
"succeeded",
]);
mockApiResponse = createMockApiResponse("succeeded", scannedRepos);
mockGetVariantAnalysis.mockResolvedValue(mockApiResponse);
succeededRepos = scannedRepos.filter(
(r) => r.analysis_status === "succeeded",
);
});
it("should trigger a download extension command for each repo", async () => {
const succeededRepos = scannedRepos.filter(
(r) => r.analysis_status === "succeeded",
);
const commandSpy = jest
.spyOn(commands, "executeCommand")
.mockResolvedValue(undefined);
await variantAnalysisMonitor.monitorVariantAnalysis( await variantAnalysisMonitor.monitorVariantAnalysis(
variantAnalysis, variantAnalysis,
cancellationTokenSource.token, cancellationTokenSource.token,
); );
} catch (error: any) {
expect(error.message).toBe("Error authenticating with GitHub");
}
});
});
describe("when credentials are valid", () => { expect(commandSpy).toBeCalledTimes(succeededRepos.length);
beforeEach(async () => {
const mockCredentials = { succeededRepos.forEach((succeededRepo, index) => {
getOctokit: () => expect(commandSpy).toHaveBeenNthCalledWith(
Promise.resolve({ index + 1,
request: jest.fn(), "codeQL.autoDownloadVariantAnalysisResult",
}), processScannedRepository(succeededRepo),
} as unknown as Credentials; processUpdatedVariantAnalysis(variantAnalysis, mockApiResponse),
jest.spyOn(Credentials, "initialize").mockResolvedValue(mockCredentials); );
});
});
it("should download all available results", async () => {
await variantAnalysisMonitor.monitorVariantAnalysis(
variantAnalysis,
cancellationTokenSource.token,
);
expect(mockGetDownloadResult).toBeCalledTimes(succeededRepos.length);
succeededRepos.forEach((succeededRepo, index) => {
expect(mockGetDownloadResult).toHaveBeenNthCalledWith(
index + 1,
processScannedRepository(succeededRepo),
processUpdatedVariantAnalysis(variantAnalysis, mockApiResponse),
undefined,
);
});
});
}); });
it("should return early if variant analysis is cancelled", async () => { describe("when there are only in progress repos", () => {
cancellationTokenSource.cancel(); let scannedRepos: ApiVariantAnalysisScannedRepository[];
await variantAnalysisMonitor.monitorVariantAnalysis(
variantAnalysis,
cancellationTokenSource.token,
);
expect(onVariantAnalysisChangeSpy).not.toHaveBeenCalled();
});
it("should return early if variant analysis should be cancelled", async () => {
shouldCancelMonitor.mockResolvedValue(true);
await variantAnalysisMonitor.monitorVariantAnalysis(
variantAnalysis,
cancellationTokenSource.token,
);
expect(onVariantAnalysisChangeSpy).not.toHaveBeenCalled();
});
describe("when the variant analysis fails", () => {
let mockFailedApiResponse: VariantAnalysisApiResponse;
beforeEach(async () => { beforeEach(async () => {
mockFailedApiResponse = createFailedMockApiResponse(); scannedRepos = createMockScannedRepos(["pending", "in_progress"]);
mockGetVariantAnalysis.mockResolvedValue(mockFailedApiResponse); mockApiResponse = createMockApiResponse("in_progress", scannedRepos);
mockGetVariantAnalysis.mockResolvedValue(mockApiResponse);
}); });
it("should mark as failed and stop monitoring", async () => { it("should succeed and not download any repos via a command", async () => {
const commandSpy = jest
.spyOn(commands, "executeCommand")
.mockResolvedValue(undefined);
await variantAnalysisMonitor.monitorVariantAnalysis( await variantAnalysisMonitor.monitorVariantAnalysis(
variantAnalysis, variantAnalysis,
cancellationTokenSource.token, cancellationTokenSource.token,
); );
expect(mockGetVariantAnalysis).toHaveBeenCalledTimes(1); expect(commandSpy).not.toHaveBeenCalled();
});
expect(onVariantAnalysisChangeSpy).toHaveBeenCalledWith( it("should not try to download any repos", async () => {
expect.objectContaining({ await variantAnalysisMonitor.monitorVariantAnalysis(
status: VariantAnalysisStatus.Failed, variantAnalysis,
failureReason: processFailureReason( cancellationTokenSource.token,
mockFailedApiResponse.failure_reason as VariantAnalysisFailureReason,
),
}),
); );
expect(mockGetDownloadResult).not.toBeCalled();
}); });
}); });
describe("when the variant analysis is in progress", () => { describe("when the responses change", () => {
let mockApiResponse: VariantAnalysisApiResponse;
let scannedRepos: ApiVariantAnalysisScannedRepository[]; let scannedRepos: ApiVariantAnalysisScannedRepository[];
let succeededRepos: ApiVariantAnalysisScannedRepository[];
describe("when there are successfully scanned repos", () => { beforeEach(async () => {
beforeEach(async () => { scannedRepos = createMockScannedRepos([
scannedRepos = createMockScannedRepos([ "pending",
"pending", "in_progress",
"pending", "in_progress",
"in_progress", "in_progress",
"in_progress", "pending",
"succeeded", "pending",
"succeeded", ]);
"succeeded", mockApiResponse = createMockApiResponse("in_progress", scannedRepos);
]); mockGetVariantAnalysis.mockResolvedValueOnce(mockApiResponse);
mockApiResponse = createMockApiResponse("succeeded", scannedRepos);
mockGetVariantAnalysis.mockResolvedValue(mockApiResponse);
succeededRepos = scannedRepos.filter(
(r) => r.analysis_status === "succeeded",
);
});
it("should trigger a download extension command for each repo", async () => { let nextApiResponse = {
const succeededRepos = scannedRepos.filter( ...mockApiResponse,
(r) => r.analysis_status === "succeeded", scanned_repositories: [...scannedRepos.map((r) => ({ ...r }))],
); };
const commandSpy = jest nextApiResponse.scanned_repositories[0].analysis_status = "succeeded";
.spyOn(commands, "executeCommand") nextApiResponse.scanned_repositories[1].analysis_status = "succeeded";
.mockResolvedValue(undefined); mockGetVariantAnalysis.mockResolvedValueOnce(nextApiResponse);
await variantAnalysisMonitor.monitorVariantAnalysis( nextApiResponse = {
variantAnalysis, ...mockApiResponse,
cancellationTokenSource.token, scanned_repositories: [
); ...nextApiResponse.scanned_repositories.map((r) => ({ ...r })),
],
};
nextApiResponse.scanned_repositories[2].analysis_status = "succeeded";
nextApiResponse.scanned_repositories[5].analysis_status = "succeeded";
mockGetVariantAnalysis.mockResolvedValueOnce(nextApiResponse);
expect(commandSpy).toBeCalledTimes(succeededRepos.length); nextApiResponse = {
...mockApiResponse,
succeededRepos.forEach((succeededRepo, index) => { scanned_repositories: [
expect(commandSpy).toHaveBeenNthCalledWith( ...nextApiResponse.scanned_repositories.map((r) => ({ ...r })),
index + 1, ],
"codeQL.autoDownloadVariantAnalysisResult", };
processScannedRepository(succeededRepo), nextApiResponse.scanned_repositories[3].analysis_status = "succeeded";
processUpdatedVariantAnalysis(variantAnalysis, mockApiResponse), nextApiResponse.scanned_repositories[4].analysis_status = "failed";
); mockGetVariantAnalysis.mockResolvedValueOnce(nextApiResponse);
});
});
it("should download all available results", async () => {
await variantAnalysisMonitor.monitorVariantAnalysis(
variantAnalysis,
cancellationTokenSource.token,
);
expect(mockGetDownloadResult).toBeCalledTimes(succeededRepos.length);
succeededRepos.forEach((succeededRepo, index) => {
expect(mockGetDownloadResult).toHaveBeenNthCalledWith(
index + 1,
processScannedRepository(succeededRepo),
processUpdatedVariantAnalysis(variantAnalysis, mockApiResponse),
undefined,
);
});
});
}); });
describe("when there are only in progress repos", () => { it("should trigger a download extension command for each repo", async () => {
let scannedRepos: ApiVariantAnalysisScannedRepository[]; const commandSpy = jest
.spyOn(commands, "executeCommand")
.mockResolvedValue(undefined);
beforeEach(async () => { await variantAnalysisMonitor.monitorVariantAnalysis(
scannedRepos = createMockScannedRepos(["pending", "in_progress"]); variantAnalysis,
mockApiResponse = createMockApiResponse("in_progress", scannedRepos); cancellationTokenSource.token,
mockGetVariantAnalysis.mockResolvedValue(mockApiResponse); );
});
it("should succeed and not download any repos via a command", async () => { expect(mockGetVariantAnalysis).toBeCalledTimes(4);
const commandSpy = jest expect(commandSpy).toBeCalledTimes(5);
.spyOn(commands, "executeCommand") });
.mockResolvedValue(undefined); });
await variantAnalysisMonitor.monitorVariantAnalysis( describe("when there are no repos to scan", () => {
variantAnalysis, beforeEach(async () => {
cancellationTokenSource.token, scannedRepos = [];
); mockApiResponse = createMockApiResponse("succeeded", scannedRepos);
mockGetVariantAnalysis.mockResolvedValue(mockApiResponse);
expect(commandSpy).not.toHaveBeenCalled();
});
it("should not try to download any repos", async () => {
await variantAnalysisMonitor.monitorVariantAnalysis(
variantAnalysis,
cancellationTokenSource.token,
);
expect(mockGetDownloadResult).not.toBeCalled();
});
}); });
describe("when the responses change", () => { it("should not try to download any repos", async () => {
let scannedRepos: ApiVariantAnalysisScannedRepository[]; await variantAnalysisMonitor.monitorVariantAnalysis(
variantAnalysis,
cancellationTokenSource.token,
);
beforeEach(async () => { expect(mockGetDownloadResult).not.toBeCalled();
scannedRepos = createMockScannedRepos([
"pending",
"in_progress",
"in_progress",
"in_progress",
"pending",
"pending",
]);
mockApiResponse = createMockApiResponse("in_progress", scannedRepos);
mockGetVariantAnalysis.mockResolvedValueOnce(mockApiResponse);
let nextApiResponse = {
...mockApiResponse,
scanned_repositories: [...scannedRepos.map((r) => ({ ...r }))],
};
nextApiResponse.scanned_repositories[0].analysis_status = "succeeded";
nextApiResponse.scanned_repositories[1].analysis_status = "succeeded";
mockGetVariantAnalysis.mockResolvedValueOnce(nextApiResponse);
nextApiResponse = {
...mockApiResponse,
scanned_repositories: [
...nextApiResponse.scanned_repositories.map((r) => ({ ...r })),
],
};
nextApiResponse.scanned_repositories[2].analysis_status = "succeeded";
nextApiResponse.scanned_repositories[5].analysis_status = "succeeded";
mockGetVariantAnalysis.mockResolvedValueOnce(nextApiResponse);
nextApiResponse = {
...mockApiResponse,
scanned_repositories: [
...nextApiResponse.scanned_repositories.map((r) => ({ ...r })),
],
};
nextApiResponse.scanned_repositories[3].analysis_status = "succeeded";
nextApiResponse.scanned_repositories[4].analysis_status = "failed";
mockGetVariantAnalysis.mockResolvedValueOnce(nextApiResponse);
});
it("should trigger a download extension command for each repo", async () => {
const commandSpy = jest
.spyOn(commands, "executeCommand")
.mockResolvedValue(undefined);
await variantAnalysisMonitor.monitorVariantAnalysis(
variantAnalysis,
cancellationTokenSource.token,
);
expect(mockGetVariantAnalysis).toBeCalledTimes(4);
expect(commandSpy).toBeCalledTimes(5);
});
});
describe("when there are no repos to scan", () => {
beforeEach(async () => {
scannedRepos = [];
mockApiResponse = createMockApiResponse("succeeded", scannedRepos);
mockGetVariantAnalysis.mockResolvedValue(mockApiResponse);
});
it("should not try to download any repos", async () => {
await variantAnalysisMonitor.monitorVariantAnalysis(
variantAnalysis,
cancellationTokenSource.token,
);
expect(mockGetDownloadResult).not.toBeCalled();
});
}); });
}); });
}); });

Просмотреть файл

@ -1,6 +1,5 @@
import { join } from "path"; import { join } from "path";
import { readFile } from "fs-extra"; import { readFile } from "fs-extra";
import { createMockExtensionContext } from "../index";
import { Credentials } from "../../../authentication"; import { Credentials } from "../../../authentication";
import * as markdownGenerator from "../../../remote-queries/remote-queries-markdown-generation"; import * as markdownGenerator from "../../../remote-queries/remote-queries-markdown-generation";
import * as ghApiClient from "../../../remote-queries/gh-api/gh-api-client"; import * as ghApiClient from "../../../remote-queries/gh-api/gh-api-client";
@ -20,7 +19,6 @@ describe("export results", () => {
.spyOn(ghApiClient, "createGist") .spyOn(ghApiClient, "createGist")
.mockResolvedValue(undefined); .mockResolvedValue(undefined);
const ctx = createMockExtensionContext();
const query = JSON.parse( const query = JSON.parse(
await readFile( await readFile(
join( join(
@ -41,7 +39,6 @@ describe("export results", () => {
); );
await exportRemoteQueryAnalysisResults( await exportRemoteQueryAnalysisResults(
ctx,
"", "",
query, query,
analysesResults, analysesResults,

Просмотреть файл

@ -279,7 +279,6 @@ describe("Remote queries and query history manager", () => {
jest.spyOn(Credentials, "initialize").mockResolvedValue(mockCredentials); jest.spyOn(Credentials, "initialize").mockResolvedValue(mockCredentials);
arm = new AnalysesResultsManager( arm = new AnalysesResultsManager(
{} as ExtensionContext,
mockCliServer, mockCliServer,
join(STORAGE_DIR, "queries"), join(STORAGE_DIR, "queries"),
mockLogger, mockLogger,