Files
codeql-action/src/database-upload.test.ts
2025-11-18 18:43:19 +00:00

309 lines
8.3 KiB
TypeScript

import * as fs from "fs";
import * as github from "@actions/github";
import test from "ava";
import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import { AnalysisKind } from "./analyses";
import { GitHubApiDetails } from "./api-client";
import * as apiClient from "./api-client";
import { createStubCodeQL } from "./codeql";
import { Config } from "./config-utils";
import { cleanupAndUploadDatabases } from "./database-upload";
import * as gitUtils from "./git-utils";
import { KnownLanguage } from "./languages";
import { RepositoryNwo } from "./repository";
import {
createFeatures,
createTestConfig,
getRecordingLogger,
LoggedMessage,
setupActionsVars,
setupTests,
} from "./testing-utils";
import {
GitHubVariant,
HTTPError,
initializeEnvironment,
withTmpDir,
} from "./util";
setupTests(test);
test.beforeEach(() => {
initializeEnvironment("1.2.3");
});
const testRepoName: RepositoryNwo = { owner: "github", repo: "example" };
const testApiDetails: GitHubApiDetails = {
auth: "1234",
url: "https://github.com",
apiURL: undefined,
};
function getTestConfig(tmpDir: string): Config {
return createTestConfig({
languages: [KnownLanguage.javascript],
dbLocation: tmpDir,
});
}
async function mockHttpRequests(databaseUploadStatusCode: number) {
// Passing an auth token is required, so we just use a dummy value
const client = github.getOctokit("123");
const requestSpy = sinon.stub(client, "request");
const url =
"POST /repos/:owner/:repo/code-scanning/codeql/databases/:language?name=:name&commit_oid=:commit_oid";
const databaseUploadSpy = requestSpy.withArgs(url);
if (databaseUploadStatusCode < 300) {
databaseUploadSpy.resolves(undefined);
} else {
databaseUploadSpy.throws(
new HTTPError("some error message", databaseUploadStatusCode),
);
}
sinon.stub(apiClient, "getApiClient").value(() => client);
return databaseUploadSpy;
}
function getCodeQL() {
return createStubCodeQL({
async databaseBundle(_: string, outputFilePath: string) {
fs.writeFileSync(outputFilePath, "");
},
async databaseCleanupCluster() {
// Do nothing, as we are not testing cleanup here.
},
});
}
test("Abort database upload if 'upload-database' input set to false", async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("false");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
const loggedMessages = [];
await cleanupAndUploadDatabases(
testRepoName,
getCodeQL(),
getTestConfig(tmpDir),
testApiDetails,
createFeatures([]),
getRecordingLogger(loggedMessages),
);
t.assert(
loggedMessages.find(
(v: LoggedMessage) =>
v.type === "debug" &&
v.message ===
"Database upload disabled in workflow. Skipping upload.",
) !== undefined,
);
});
});
test("Abort database upload if 'analysis-kinds: code-scanning' is not enabled", async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("true");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
await mockHttpRequests(201);
const loggedMessages = [];
await cleanupAndUploadDatabases(
testRepoName,
getCodeQL(),
{
...getTestConfig(tmpDir),
analysisKinds: [AnalysisKind.CodeQuality],
},
testApiDetails,
createFeatures([]),
getRecordingLogger(loggedMessages),
);
t.assert(
loggedMessages.find(
(v: LoggedMessage) =>
v.type === "debug" &&
v.message ===
"Not uploading database because 'analysis-kinds: code-scanning' is not enabled.",
) !== undefined,
);
});
});
test("Abort database upload if running against GHES", async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("true");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
const config = getTestConfig(tmpDir);
config.gitHubVersion = { type: GitHubVariant.GHES, version: "3.0" };
const loggedMessages = [];
await cleanupAndUploadDatabases(
testRepoName,
getCodeQL(),
config,
testApiDetails,
createFeatures([]),
getRecordingLogger(loggedMessages),
);
t.assert(
loggedMessages.find(
(v: LoggedMessage) =>
v.type === "debug" &&
v.message ===
"Not running against github.com or GHEC-DR. Skipping upload.",
) !== undefined,
);
});
});
test("Abort database upload if not analyzing default branch", async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("true");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(false);
const loggedMessages = [];
await cleanupAndUploadDatabases(
testRepoName,
getCodeQL(),
getTestConfig(tmpDir),
testApiDetails,
createFeatures([]),
getRecordingLogger(loggedMessages),
);
t.assert(
loggedMessages.find(
(v: LoggedMessage) =>
v.type === "debug" &&
v.message === "Not analyzing default branch. Skipping upload.",
) !== undefined,
);
});
});
test("Don't crash if uploading a database fails", async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("true");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
await mockHttpRequests(500);
const loggedMessages = [] as LoggedMessage[];
await cleanupAndUploadDatabases(
testRepoName,
getCodeQL(),
getTestConfig(tmpDir),
testApiDetails,
createFeatures([]),
getRecordingLogger(loggedMessages),
);
t.assert(
loggedMessages.find(
(v) =>
v.type === "warning" &&
v.message ===
"Failed to upload database for javascript: Error: some error message",
) !== undefined,
);
});
});
test("Successfully uploading a database to github.com", async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("true");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
await mockHttpRequests(201);
const loggedMessages = [] as LoggedMessage[];
await cleanupAndUploadDatabases(
testRepoName,
getCodeQL(),
getTestConfig(tmpDir),
testApiDetails,
createFeatures([]),
getRecordingLogger(loggedMessages),
);
t.assert(
loggedMessages.find(
(v) =>
v.type === "debug" &&
v.message === "Successfully uploaded database for javascript",
) !== undefined,
);
});
});
test("Successfully uploading a database to GHEC-DR", async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("true");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
const databaseUploadSpy = await mockHttpRequests(201);
const loggedMessages = [] as LoggedMessage[];
await cleanupAndUploadDatabases(
testRepoName,
getCodeQL(),
getTestConfig(tmpDir),
{
auth: "1234",
url: "https://tenant.ghe.com",
apiURL: undefined,
},
createFeatures([]),
getRecordingLogger(loggedMessages),
);
t.assert(
loggedMessages.find(
(v) =>
v.type === "debug" &&
v.message === "Successfully uploaded database for javascript",
) !== undefined,
);
t.assert(
databaseUploadSpy.calledOnceWith(
sinon.match.string,
sinon.match.has("baseUrl", "https://uploads.tenant.ghe.com"),
),
);
});
});