Files
anonymous_github/test/app-reinstall-recovery.test.js
T

105 lines
5.9 KiB
JavaScript

const { expect } = require("chai");
require("ts-node/register/transpile-only");
const config = require("../src/config").default;
const app = require("../src/core/github-app");
const utils = require("../src/server/routes/route-utils");
const Repositories = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default;
const PullRequests = require("../src/core/model/anonymizedPullRequests/anonymizedPullRequests.model").default;
const { githubAppRouter } = require("../src/server/routes/github-app");
const migrate = githubAppRouter.stack.find(layer => layer.route?.path === "/connections/migrate").route.stack[0].handle;
for (const type of ["repository", "pull-request"]) {
describe(`${type} App reinstall recovery`, function () {
let restores, model, selected, writes, checks, owner;
function stub(object, key, value) {
const previous = object[key]; restores.push(() => { object[key] = previous; }); object[key] = value;
}
beforeEach(function () {
restores = []; writes = []; checks = []; owner = "owner";
model = { _id: "saved-resource", owner: "owner", status: "ready", statusDate: new Date(),
repoId: "published-url", pullRequestId: "published-url",
source: { repositoryName: "owner/private", repositoryFullName: "owner/private", commit: "saved-sha", pullRequestId: 7 },
githubAccess: { kind: "github-app", repositoryId: 42, installationId: 100, revision: "old" },
};
selected = { token: "fresh-token", binding: { kind: "github-app", repositoryId: 42, installationId: 200, revision: "new" } };
stub(config, "GITHUB_APP_ENABLED", true); stub(config, "GITHUB_APP_NEW_CONNECTIONS", true);
stub(utils, "getUser", async () => ({ id: owner }));
stub(utils, "handleError", error => { throw error; });
stub(app, "selectRepositoryAccess", async (id, name, connection, repositoryId) => {
expect(repositoryId).to.equal(42);
expect([id, name, connection]).to.deep.equal(["owner", "owner/private", "github-app"]);
return selected;
});
stub(app, "githubRequest", async (url, token) => { checks.push({ url, token }); return {}; });
const Model = type === "repository" ? Repositories : PullRequests;
stub(Model, "findOne", async filter => filter.owner === model.owner ? model : null);
stub(Model, "updateOne", async (filter, update) => { writes.push({ filter, update }); return { modifiedCount: 1 }; });
});
afterEach(function () { while (restores.length) restores.pop()(); });
async function run(preview = false) {
let response;
await migrate({ body: { type, id: "published-url", connection: "github-app", preview } }, { json: body => { response = body; } });
return response;
}
async function fails(message) {
let error; try { await run(); } catch (caught) { error = caught; }
expect(error?.message).to.equal(message); expect(writes).to.have.length(0);
}
it("previews fresh installation access without changing the saved binding", async function () {
expect(await run(true)).to.deep.equal({ eligible: true, connection: "github-app" });
expect(writes).to.have.length(0); expect(model.githubAccess.installationId).to.equal(100);
});
it("replaces only the binding while preserving the URL, source and snapshot", async function () {
expect(await run()).to.deep.equal({ connection: "github-app" });
expect(writes).to.have.length(1);
expect(writes[0].update).to.deep.equal({ $set: { githubAccess: selected.binding } });
expect(writes[0].filter.githubAccess).to.deep.equal(model.githubAccess);
expect(writes[0].filter.source).to.deep.equal(model.source);
expect(checks).to.deep.equal([{ url: type === "repository" ? "/repos/owner/private/commits/saved-sha" : "/repos/owner/private/pulls/7", token: "fresh-token" }]);
});
it("saves a resolved rename atomically with the binding after preview", async function () {
selected.fullName = "owner/renamed";
await run(true);
expect(writes).to.have.length(0);
await run();
expect(writes).to.have.length(1);
expect(writes[0].update).to.deep.equal({ $set: {
githubAccess: selected.binding,
[type === "repository" ? "source.repositoryName" : "source.repositoryFullName"]: "owner/renamed",
} });
expect(writes[0].filter.source).to.deep.equal(model.source);
expect(writes[0].filter.githubAccess).to.deep.equal(model.githubAccess);
expect(checks.every(check => check.url.startsWith("/repos/owner/renamed/"))).to.equal(true);
});
it("rejects a replacement repository at the same name", async function () {
selected.binding.repositoryId = 99; await fails("connection_changed"); expect(checks).to.have.length(0);
});
for (const status of ["preparing", "queue", "download", "removing", "expiring"]) {
it("rejects reconnect during " + status, async function () {
model.status = status;
await fails("repository_busy");
expect(checks).to.have.length(0);
});
}
it("requires access to the existing snapshot before saving", async function () {
stub(app, "githubRequest", async () => { throw new Error("github_app_access_required"); });
await fails("github_app_access_required");
});
if (type === "pull-request") {
it("allows stale downloads but guards their timestamp against a new refresh", async function () {
model.status = "download";
model.statusDate = new Date(Date.now() - 6 * 60 * 1000);
await run();
expect(writes[0].filter.statusDate).to.equal(model.statusDate);
});
}
it("does not let another user repair the binding", async function () {
owner = "other"; await fails("repo_not_found");
});
it("reports a concurrent change instead of overwriting it", async function () {
stub(type === "repository" ? Repositories : PullRequests, "updateOne", async () => ({ modifiedCount: 0 }));
await fails("connection_changed");
});
});
}