211 lines
7.9 KiB
TypeScript
211 lines
7.9 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import { validateBranchName } from "../src/github/operations/branch";
|
|
|
|
describe("validateBranchName", () => {
|
|
describe("valid branch names", () => {
|
|
it("should accept simple alphanumeric names", () => {
|
|
expect(() => validateBranchName("main")).not.toThrow();
|
|
expect(() => validateBranchName("feature123")).not.toThrow();
|
|
expect(() => validateBranchName("Branch1")).not.toThrow();
|
|
});
|
|
|
|
it("should accept names with hyphens", () => {
|
|
expect(() => validateBranchName("feature-branch")).not.toThrow();
|
|
expect(() => validateBranchName("fix-bug-123")).not.toThrow();
|
|
});
|
|
|
|
it("should accept names with underscores", () => {
|
|
expect(() => validateBranchName("feature_branch")).not.toThrow();
|
|
expect(() => validateBranchName("fix_bug_123")).not.toThrow();
|
|
});
|
|
|
|
it("should accept names with forward slashes", () => {
|
|
expect(() => validateBranchName("feature/new-thing")).not.toThrow();
|
|
expect(() => validateBranchName("user/feature/branch")).not.toThrow();
|
|
});
|
|
|
|
it("should accept names with periods", () => {
|
|
expect(() => validateBranchName("v1.0.0")).not.toThrow();
|
|
expect(() => validateBranchName("release.1.2.3")).not.toThrow();
|
|
});
|
|
|
|
it("should accept typical branch name formats", () => {
|
|
expect(() =>
|
|
validateBranchName("claude/issue-123-20250101-1234"),
|
|
).not.toThrow();
|
|
expect(() => validateBranchName("refs/heads/main")).not.toThrow();
|
|
expect(() => validateBranchName("bugfix/JIRA-1234")).not.toThrow();
|
|
});
|
|
|
|
it("should accept branch names containing # (git-valid, common in issue-linked branches)", () => {
|
|
// Reported in #1137: branches like "put-back-arm64-#2" were rejected
|
|
expect(() => validateBranchName("put-back-arm64-#2")).not.toThrow();
|
|
expect(() =>
|
|
validateBranchName("feature/#123-description"),
|
|
).not.toThrow();
|
|
expect(() => validateBranchName("fix/issue-#42")).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("command injection attempts", () => {
|
|
it("should reject shell command substitution with $()", () => {
|
|
expect(() => validateBranchName("$(whoami)")).toThrow();
|
|
expect(() => validateBranchName("branch-$(rm -rf /)")).toThrow();
|
|
expect(() => validateBranchName("test$(cat /etc/passwd)")).toThrow();
|
|
});
|
|
|
|
it("should reject shell command substitution with backticks", () => {
|
|
expect(() => validateBranchName("`whoami`")).toThrow();
|
|
expect(() => validateBranchName("branch-`rm -rf /`")).toThrow();
|
|
});
|
|
|
|
it("should reject command chaining with semicolons", () => {
|
|
expect(() => validateBranchName("branch; rm -rf /")).toThrow();
|
|
expect(() => validateBranchName("test;whoami")).toThrow();
|
|
});
|
|
|
|
it("should reject command chaining with &&", () => {
|
|
expect(() => validateBranchName("branch && rm -rf /")).toThrow();
|
|
expect(() => validateBranchName("test&&whoami")).toThrow();
|
|
});
|
|
|
|
it("should reject command chaining with ||", () => {
|
|
expect(() => validateBranchName("branch || rm -rf /")).toThrow();
|
|
expect(() => validateBranchName("test||whoami")).toThrow();
|
|
});
|
|
|
|
it("should reject pipe characters", () => {
|
|
expect(() => validateBranchName("branch | cat")).toThrow();
|
|
expect(() => validateBranchName("test|grep password")).toThrow();
|
|
});
|
|
|
|
it("should reject redirection operators", () => {
|
|
expect(() => validateBranchName("branch > /etc/passwd")).toThrow();
|
|
expect(() => validateBranchName("branch < input")).toThrow();
|
|
expect(() => validateBranchName("branch >> file")).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("option injection attempts", () => {
|
|
it("should reject branch names starting with dash", () => {
|
|
expect(() => validateBranchName("-x")).toThrow(
|
|
/cannot start with a dash/,
|
|
);
|
|
expect(() => validateBranchName("--help")).toThrow(
|
|
/cannot start with a dash/,
|
|
);
|
|
expect(() => validateBranchName("-")).toThrow(/cannot start with a dash/);
|
|
expect(() => validateBranchName("--version")).toThrow(
|
|
/cannot start with a dash/,
|
|
);
|
|
expect(() => validateBranchName("-rf")).toThrow(
|
|
/cannot start with a dash/,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("path traversal attempts", () => {
|
|
it("should reject double dot sequences", () => {
|
|
expect(() => validateBranchName("../../../etc")).toThrow();
|
|
expect(() => validateBranchName("branch/../secret")).toThrow(/'\.\.'$/);
|
|
expect(() => validateBranchName("a..b")).toThrow(/'\.\.'$/);
|
|
});
|
|
});
|
|
|
|
describe("git-specific invalid patterns", () => {
|
|
it("should reject @{ sequence", () => {
|
|
expect(() => validateBranchName("branch@{1}")).toThrow(/@{/);
|
|
expect(() => validateBranchName("HEAD@{yesterday}")).toThrow(/@{/);
|
|
});
|
|
|
|
it("should reject .lock suffix", () => {
|
|
expect(() => validateBranchName("branch.lock")).toThrow(/\.lock/);
|
|
expect(() => validateBranchName("feature.lock")).toThrow(/\.lock/);
|
|
});
|
|
|
|
it("should reject consecutive slashes", () => {
|
|
expect(() => validateBranchName("feature//branch")).toThrow(
|
|
/consecutive slashes/,
|
|
);
|
|
expect(() => validateBranchName("a//b//c")).toThrow(
|
|
/consecutive slashes/,
|
|
);
|
|
});
|
|
|
|
it("should reject trailing slashes", () => {
|
|
expect(() => validateBranchName("feature/")).toThrow(
|
|
/cannot end with a slash/,
|
|
);
|
|
expect(() => validateBranchName("branch/")).toThrow(
|
|
/cannot end with a slash/,
|
|
);
|
|
});
|
|
|
|
it("should reject leading periods", () => {
|
|
expect(() => validateBranchName(".hidden")).toThrow();
|
|
});
|
|
|
|
it("should reject trailing periods", () => {
|
|
expect(() => validateBranchName("branch.")).toThrow(
|
|
/cannot start or end with a period/,
|
|
);
|
|
});
|
|
|
|
it("should reject special git refspec characters", () => {
|
|
expect(() => validateBranchName("branch~1")).toThrow();
|
|
expect(() => validateBranchName("branch^2")).toThrow();
|
|
expect(() => validateBranchName("branch:ref")).toThrow();
|
|
expect(() => validateBranchName("branch?")).toThrow();
|
|
expect(() => validateBranchName("branch*")).toThrow();
|
|
expect(() => validateBranchName("branch[0]")).toThrow();
|
|
expect(() => validateBranchName("branch\\path")).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("control characters and special characters", () => {
|
|
it("should reject null bytes", () => {
|
|
expect(() => validateBranchName("branch\x00name")).toThrow();
|
|
});
|
|
|
|
it("should reject other control characters", () => {
|
|
expect(() => validateBranchName("branch\x01name")).toThrow();
|
|
expect(() => validateBranchName("branch\x1Fname")).toThrow();
|
|
expect(() => validateBranchName("branch\x7Fname")).toThrow();
|
|
});
|
|
|
|
it("should reject spaces", () => {
|
|
expect(() => validateBranchName("branch name")).toThrow();
|
|
expect(() => validateBranchName("feature branch")).toThrow();
|
|
});
|
|
|
|
it("should reject newlines and tabs", () => {
|
|
expect(() => validateBranchName("branch\nname")).toThrow();
|
|
expect(() => validateBranchName("branch\tname")).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("empty and whitespace", () => {
|
|
it("should reject empty strings", () => {
|
|
expect(() => validateBranchName("")).toThrow(/cannot be empty/);
|
|
});
|
|
|
|
it("should reject whitespace-only strings", () => {
|
|
expect(() => validateBranchName(" ")).toThrow();
|
|
expect(() => validateBranchName("\t\n")).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("edge cases", () => {
|
|
it("should accept single alphanumeric character", () => {
|
|
expect(() => validateBranchName("a")).not.toThrow();
|
|
expect(() => validateBranchName("1")).not.toThrow();
|
|
});
|
|
|
|
it("should reject single special characters", () => {
|
|
expect(() => validateBranchName(".")).toThrow();
|
|
expect(() => validateBranchName("/")).toThrow();
|
|
expect(() => validateBranchName("-")).toThrow();
|
|
});
|
|
});
|
|
});
|