mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-10-20 06:37:23 +02:00
fix: reconnect resources after GitHub App reinstall
This commit is contained in:
1 parent
2fb4b8fabd
commit
3140ad3c45
6 files changed
+92
-11
No files matched your search
@@ -0,0 +1,74 @@
|
||||
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",
|
||||
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) => {
|
||||
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("rejects a replacement repository at the same name", async function () {
|
||||
selected.binding.repositoryId = 99; await fails("connection_changed"); 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");
|
||||
});
|
||||
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");
|
||||
});
|
||||
});
|
||||
}
|
||||
+8
-5
@@ -155,20 +155,22 @@ describe("Vue 3 UI", function () {
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("previews a connection migration before switching and includes CSRF protection", async function () {
|
||||
const resource = { type: "repository", id: "saved", name: "owner/private", connection: "oauth", status: "ready" };
|
||||
for (const connection of ["oauth", "github-app"]) {
|
||||
it(`previews and saves ${connection} access with CSRF protection`, async function () {
|
||||
const action = connection === "github-app" ? "Reconnect read-only access" : "Switch to read-only access";
|
||||
const resource = { type: "repository", id: "saved", name: "owner/private", connection, status: "ready" };
|
||||
ui = await browser("/connections", {
|
||||
"/api/user": { username: "owner" },
|
||||
"/github/connections": { csrf: "csrf-value", appEnabled: true, appConnected: true, oauthEnabled: true, oauthConnected: true,
|
||||
"/github/connections": { csrf: "csrf-value", appEnabled: true, appConnected: true, oauthEnabled: true, oauthConnected: connection === "oauth",
|
||||
installations: [{ id: 4, account: "owner" }], gistCount: 0, resources: [resource] },
|
||||
"/github/connections/migrate": request => request.payload.preview ? { eligible: true } : { connection: "github-app" },
|
||||
});
|
||||
const button = label => [...ui.window.document.querySelectorAll("button")].find(node => node.textContent.includes(label));
|
||||
expect(button("Switch to read-only access")).to.equal(undefined);
|
||||
expect(button(action)).to.equal(undefined);
|
||||
expect(ui.window.document.querySelector('a[href="/github/app/install?installationId=4"]')).not.to.equal(null);
|
||||
button("Check read-only access").click();
|
||||
await delay(30);
|
||||
button("Switch to read-only access").click();
|
||||
button(action).click();
|
||||
await delay(30);
|
||||
const changes = ui.requests.filter(r => r.url.pathname === "/github/connections/migrate");
|
||||
expect(changes).to.have.length(2);
|
||||
@@ -178,6 +180,7 @@ describe("Vue 3 UI", function () {
|
||||
expect(changes[1].headers["X-CSRF-Token"]).to.equal("csrf-value");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
for (const [appConnected, oauthConnected] of [[true, false], [false, true], [false, false], [true, true]]) {
|
||||
|
||||
Reference in new issue
Block a user