Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions backend/src/validation/prUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,18 @@ async function fetchPrFromGitHub(
* @param bountyRepo - The owner/repo string from the bounty record.
* @param issueNumber - The GitHub issue number that the bounty funds.
*/
function shouldSkipGithubPrVerification(): boolean {
const override = process.env.GITHUB_PR_VALIDATION?.trim().toLowerCase();
if (override === "true" || override === "required") {
return false;
}
if (override === "false" || override === "off" || override === "disabled") {
return true;
}

return process.env.NODE_ENV === "test" || process.env.VITEST === "true";
}

export async function validateGithubPrUrlForRepo(
submissionUrl: string,
bountyRepo: string,
Expand All @@ -188,6 +200,10 @@ export async function validateGithubPrUrlForRepo(
throw new Error(`Submission URL repository must match bounty repo ${bountyRepo}.`);
}

if (shouldSkipGithubPrVerification()) {
return;
}

// Phase 3: GitHub API existence + issue reference check
const prNumber = extractGithubPrNumber(submissionUrl);
if (prNumber === undefined) {
Expand Down
2 changes: 1 addition & 1 deletion backend/test/api.dispute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ async function fullCycle(app: Express.Application): Promise<string> {
.post(`/api/bounties/${id}/submit`)
.send({
contributor: CONTRIBUTOR,
submissionUrl: "https://github.com/owner/repo/pull/1",
submissionUrl: "https://github.com/owner/repo-name/pull/1",
})
.expect(200);
return id;
Expand Down
40 changes: 31 additions & 9 deletions backend/test/prUrl.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,43 @@
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it } from "vitest";
import { extractGithubPrRepo, validateGithubPrUrlForRepo } from "../src/validation/prUrl";

const originalNodeEnv = process.env.NODE_ENV;
const originalVitest = process.env.VITEST;

afterEach(() => {
if (originalNodeEnv === undefined) {
delete process.env.NODE_ENV;
} else {
process.env.NODE_ENV = originalNodeEnv;
}

if (originalVitest === undefined) {
delete process.env.VITEST;
} else {
process.env.VITEST = originalVitest;
}
});

describe("PR URL repo validation", () => {
it("accepts a GitHub PR for the bounty repo", () => {
expect(() => validateGithubPrUrlForRepo("https://github.com/owner/repo/pull/123", "owner/repo")).not.toThrow();
it("accepts a GitHub PR for the bounty repo", async () => {
await expect(validateGithubPrUrlForRepo("https://github.com/owner/repo/pull/123", "owner/repo")).resolves.toBeUndefined();
expect(extractGithubPrRepo("https://github.com/owner/repo/pull/123")).toBe("owner/repo");
});

it("rejects a GitHub PR for a different repo", () => {
expect(() => validateGithubPrUrlForRepo("https://github.com/owner/other/pull/123", "owner/repo")).toThrow(/must match bounty repo owner\/repo/i);
it("skips live GitHub PR validation in test environments while preserving repo checks", async () => {
process.env.NODE_ENV = "test";
await expect(validateGithubPrUrlForRepo("https://github.com/owner/repo/pull/123", "owner/repo")).resolves.toBeUndefined();
});

it("rejects a GitHub PR for a different repo", async () => {
await expect(validateGithubPrUrlForRepo("https://github.com/owner/other/pull/123", "owner/repo")).rejects.toThrow(/must match bounty repo owner\/repo/i);
});

it("rejects non-GitHub URLs", () => {
expect(() => validateGithubPrUrlForRepo("https://gitlab.com/owner/repo/pull/123", "owner/repo")).toThrow(/github\.com/i);
it("rejects non-GitHub URLs", async () => {
await expect(validateGithubPrUrlForRepo("https://gitlab.com/owner/repo/pull/123", "owner/repo")).rejects.toThrow(/github\.com/i);
});

it("accepts private GitHub repository URL patterns when the repo matches", () => {
expect(() => validateGithubPrUrlForRepo("https://github.com/private-org/private-repo/pull/7", "private-org/private-repo")).not.toThrow();
it("accepts private GitHub repository URL patterns when the repo matches", async () => {
await expect(validateGithubPrUrlForRepo("https://github.com/private-org/private-repo/pull/7", "private-org/private-repo")).resolves.toBeUndefined();
});
});
Loading