mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-14 00:07:00 +01:00
fix: persist pull request conferences and reconcile runtime tests
This commit is contained in:
1 parent
fd0dd0adbd
commit
b09ef0e5b0
7 files changed
+279
-1
No files matched your search
@@ -0,0 +1,5 @@
|
||||
export function joinUrl(base: string, pathname: string): string {
|
||||
const normalizedBase = base.endsWith("/") ? base : `${base}/`;
|
||||
const normalizedPath = pathname.replace(/^\/+/, "");
|
||||
return new URL(normalizedPath, normalizedBase).toString();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
/* global module, require */
|
||||
require("ts-node/register/transpile-only");
|
||||
module.exports = require("./downloadRepository.ts").default;
|
||||
@@ -0,0 +1,3 @@
|
||||
/* global module, require */
|
||||
require("ts-node/register/transpile-only");
|
||||
module.exports = require("./removeCache.ts").default;
|
||||
@@ -0,0 +1,3 @@
|
||||
/* global module, require */
|
||||
require("ts-node/register/transpile-only");
|
||||
module.exports = require("./removeRepository.ts").default;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { existsSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
|
||||
const PROCESSOR_PATHS = {
|
||||
downloadRepository: {
|
||||
build: resolve("build/queue/processes/downloadRepository.js"),
|
||||
source: resolve("src/queue/processes/downloadRepository.worker.js"),
|
||||
},
|
||||
removeCache: {
|
||||
build: resolve("build/queue/processes/removeCache.js"),
|
||||
source: resolve("src/queue/processes/removeCache.worker.js"),
|
||||
},
|
||||
removeRepository: {
|
||||
build: resolve("build/queue/processes/removeRepository.js"),
|
||||
source: resolve("src/queue/processes/removeRepository.worker.js"),
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type WorkerProcessorName = keyof typeof PROCESSOR_PATHS;
|
||||
|
||||
export function resolveWorkerProcessorPath(
|
||||
name: WorkerProcessorName,
|
||||
fileExists: (path: string) => boolean = existsSync
|
||||
): string {
|
||||
const processor = PROCESSOR_PATHS[name];
|
||||
return fileExists(processor.build) ? processor.build : processor.source;
|
||||
}
|
||||
@@ -275,7 +275,7 @@ router.post("/", async (req, res) => {
|
||||
pullRequest.source.repositoryFullName =
|
||||
pullRequestUpdate.source.repositoryFullName;
|
||||
|
||||
pullRequest.conference = pullRequestUpdate.conference;
|
||||
pullRequest.model.conference = pullRequestUpdate.conference;
|
||||
|
||||
await pullRequest.model.save();
|
||||
await pullRequest.anonymize();
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
require("ts-node/register/transpile-only");
|
||||
|
||||
const path = require("path");
|
||||
const { expect } = require("chai");
|
||||
|
||||
const Repository = require("../src/core/Repository").default;
|
||||
const PullRequest = require("../src/core/PullRequest").default;
|
||||
const PullRequestModel = require("../src/core/model/anonymizedPullRequests/anonymizedPullRequests.model").default;
|
||||
const github = require("../src/core/GitHubUtils");
|
||||
const githubApp = require("../src/core/github-app");
|
||||
const routeUtils = require("../src/server/routes/route-utils");
|
||||
const GitHubStream = require("../src/core/source/GitHubStream").default;
|
||||
const {
|
||||
RepositoryStatus,
|
||||
} = require("../src/core/types");
|
||||
const { joinUrl } = require("../src/core/url-utils");
|
||||
const pullRequestRouter = require("../src/server/routes/pullRequest-private").default;
|
||||
const {
|
||||
resolveWorkerProcessorPath,
|
||||
} = require("../src/queue/worker-path");
|
||||
|
||||
describe("Runtime regression fixes", function () {
|
||||
const restores = [];
|
||||
function stub(object, key, value) {
|
||||
const original = object[key];
|
||||
restores.push(() => { object[key] = original; });
|
||||
object[key] = value;
|
||||
}
|
||||
afterEach(() => { while (restores.length) restores.pop()(); });
|
||||
|
||||
describe("joinUrl", function () {
|
||||
it("preserves the double slash in https URLs", function () {
|
||||
expect(joinUrl("https://streamer.example.com", "api/download")).to.equal(
|
||||
"https://streamer.example.com/api/download"
|
||||
);
|
||||
});
|
||||
|
||||
it("handles base URLs that already end with a slash", function () {
|
||||
expect(joinUrl("http://localhost:3000/", "/api")).to.equal(
|
||||
"http://localhost:3000/api"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PullRequest.download()", function () {
|
||||
it("uses the authenticated GitHub API diff endpoint", async function () {
|
||||
const pullRequest = new PullRequest(new PullRequestModel({
|
||||
source: { repositoryFullName: "owner/repo", pullRequestId: 42 },
|
||||
}));
|
||||
pullRequest.getToken = async () => "secret-token";
|
||||
const requests = [];
|
||||
const tokens = [];
|
||||
stub(github, "octokit", token => {
|
||||
tokens.push(token);
|
||||
return {
|
||||
rest: { pulls: { get: async options => {
|
||||
requests.push(options);
|
||||
return { data: options.mediaType?.format === "diff" ? "private diff" : {
|
||||
title: "Private PR", base: { repo: { full_name: "owner/repo" } }, head: {},
|
||||
} };
|
||||
} } },
|
||||
paginate: async () => [],
|
||||
};
|
||||
});
|
||||
|
||||
await pullRequest.download();
|
||||
|
||||
expect(tokens).to.deep.equal(["secret-token"]);
|
||||
expect(requests).to.deep.equal([
|
||||
{ owner: "owner", repo: "repo", pull_number: 42 },
|
||||
{ owner: "owner", repo: "repo", pull_number: 42, mediaType: { format: "diff" } },
|
||||
]);
|
||||
expect(pullRequest.model.pullRequest.diff).to.equal("private diff");
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST / pull request creation", function () {
|
||||
it("stores the conference on the persisted model", async function () {
|
||||
const update = {
|
||||
pullRequestId: "anon-pr-1",
|
||||
conference: "conf-2026",
|
||||
source: {
|
||||
repositoryFullName: "owner/repo",
|
||||
pullRequestId: 123,
|
||||
},
|
||||
options: {
|
||||
expirationMode: "never",
|
||||
},
|
||||
terms: [],
|
||||
};
|
||||
const user = {
|
||||
id: "507f191e810c19729de860ea",
|
||||
};
|
||||
const saved = [];
|
||||
let response;
|
||||
let anonymized = false;
|
||||
stub(routeUtils, "getUser", async () => user);
|
||||
stub(routeUtils, "handleError", error => { throw error; });
|
||||
stub(githubApp, "selectRepositoryAccess", async () => ({ binding: { kind: "oauth" } }));
|
||||
stub(PullRequestModel.prototype, "save", async function () {
|
||||
saved.push(this.toObject());
|
||||
return this;
|
||||
});
|
||||
stub(PullRequest.prototype, "anonymize", async function () {
|
||||
expect(saved).to.have.length(1);
|
||||
anonymized = true;
|
||||
});
|
||||
const route = pullRequestRouter.stack.find(layer =>
|
||||
layer.route?.path === "/" && layer.route.methods.post).route;
|
||||
|
||||
await route.stack[0].handle({ body: update }, { send: value => { response = value; } });
|
||||
|
||||
expect(saved).to.have.length(1);
|
||||
expect(saved[0].conference).to.equal("conf-2026");
|
||||
expect(saved[0].source.repositoryFullName).to.equal("owner/repo");
|
||||
expect(saved[0].source.pullRequestId).to.equal(123);
|
||||
expect(response.conference).to.equal("conf-2026");
|
||||
expect(anonymized).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Repository.check()", function () {
|
||||
it("records logical expiration before throwing repository_expired", async function () {
|
||||
const repo = new Repository({
|
||||
owner: "507f191e810c19729de860ea",
|
||||
options: {
|
||||
expirationMode: "date",
|
||||
expirationDate: new Date(Date.now() - 1000),
|
||||
},
|
||||
status: RepositoryStatus.READY,
|
||||
statusDate: new Date(),
|
||||
source: {
|
||||
repositoryName: "owner/repo",
|
||||
type: "GitHubStream",
|
||||
},
|
||||
});
|
||||
|
||||
let expired = false;
|
||||
repo.markExpired = async function () {
|
||||
await Promise.resolve();
|
||||
expired = true;
|
||||
this.model.status = RepositoryStatus.EXPIRED;
|
||||
};
|
||||
|
||||
try {
|
||||
await repo.check();
|
||||
throw new Error("expected Repository.check() to throw");
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal("repository_expired");
|
||||
}
|
||||
|
||||
expect(expired).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubStream.getTruncatedTree()", function () {
|
||||
it("recursively refetches truncated subtrees", async function () {
|
||||
const source = new GitHubStream({
|
||||
repoId: "repo-1",
|
||||
organization: "owner",
|
||||
repoName: "repo",
|
||||
commit: "head",
|
||||
getToken: async () => "token",
|
||||
});
|
||||
|
||||
const calls = [];
|
||||
source.tree2Tree = function (tree, parentPath = "") {
|
||||
return tree.map((entry) =>
|
||||
path.posix.join(parentPath, entry.path || "")
|
||||
);
|
||||
};
|
||||
source.getGHTree = async function (_oct, _token, sha, _count, opt) {
|
||||
calls.push({ sha, recursive: opt.recursive === true });
|
||||
if (sha === "head" && opt.recursive) return { tree: [], truncated: true };
|
||||
if (sha === "head") {
|
||||
return {
|
||||
tree: [{ type: "tree", path: "src", sha: "src-sha" }],
|
||||
truncated: false,
|
||||
};
|
||||
}
|
||||
if (sha === "src-sha" && opt.recursive === true) {
|
||||
return {
|
||||
tree: [],
|
||||
truncated: true,
|
||||
};
|
||||
}
|
||||
if (sha === "src-sha" && opt.recursive === false) {
|
||||
return {
|
||||
tree: [{ type: "tree", path: "deep", sha: "deep-sha" }],
|
||||
truncated: false,
|
||||
};
|
||||
}
|
||||
if (sha === "deep-sha" && opt.recursive === true) {
|
||||
return {
|
||||
tree: [{ type: "blob", path: "file.ts", sha: "file-sha" }],
|
||||
truncated: false,
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected request: ${sha}:${opt.recursive}`);
|
||||
};
|
||||
|
||||
const files = await source.getTruncatedTree("head");
|
||||
|
||||
expect(files).to.deep.equal(["src", "src/deep", "src/deep/file.ts"]);
|
||||
expect(calls).to.deep.equal([
|
||||
{ sha: "head", recursive: true },
|
||||
{ sha: "head", recursive: false },
|
||||
{ sha: "src-sha", recursive: true },
|
||||
{ sha: "src-sha", recursive: false },
|
||||
{ sha: "deep-sha", recursive: true },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveWorkerProcessorPath", function () {
|
||||
it("falls back to the source-mode worker wrapper when build output is missing", function () {
|
||||
const resolved = resolveWorkerProcessorPath(
|
||||
"downloadRepository",
|
||||
() => false
|
||||
);
|
||||
|
||||
expect(resolved).to.equal(
|
||||
path.resolve("src/queue/processes/downloadRepository.worker.js")
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers the built worker script when it exists", function () {
|
||||
const resolved = resolveWorkerProcessorPath("removeCache", (candidate) =>
|
||||
candidate.endsWith(path.join("build", "queue", "processes", "removeCache.js"))
|
||||
);
|
||||
|
||||
expect(resolved).to.equal(
|
||||
path.resolve("build/queue/processes/removeCache.js")
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user