feat(system): fix(lint): ruff format test_git_gate.py
Co-Authored-By: @devpulse <devpulse@aipass>
This commit is contained in:
@@ -38,71 +38,86 @@ BLOCKED_EDIT_PATTERNS = _mod.BLOCKED_EDIT_PATTERNS
|
||||
class TestGitWriteBlocking:
|
||||
"""Core git write verbs must be blocked."""
|
||||
|
||||
@pytest.mark.parametrize("cmd", [
|
||||
"git commit -m 'test'",
|
||||
"git push origin main",
|
||||
"git pull",
|
||||
"git merge main",
|
||||
"git rebase main",
|
||||
"git reset HEAD~1",
|
||||
"git checkout -b new-branch",
|
||||
"git switch -c new-branch",
|
||||
"git cherry-pick abc123",
|
||||
"git revert HEAD",
|
||||
"git rm file.txt",
|
||||
"git mv old.py new.py",
|
||||
"git restore --staged file.py",
|
||||
"git clean -fd",
|
||||
"git config user.name 'x'",
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"cmd",
|
||||
[
|
||||
"git commit -m 'test'",
|
||||
"git push origin main",
|
||||
"git pull",
|
||||
"git merge main",
|
||||
"git rebase main",
|
||||
"git reset HEAD~1",
|
||||
"git checkout -b new-branch",
|
||||
"git switch -c new-branch",
|
||||
"git cherry-pick abc123",
|
||||
"git revert HEAD",
|
||||
"git rm file.txt",
|
||||
"git mv old.py new.py",
|
||||
"git restore --staged file.py",
|
||||
"git clean -fd",
|
||||
"git config user.name 'x'",
|
||||
],
|
||||
)
|
||||
def test_blocks_write_verbs(self, cmd):
|
||||
"""Each blocked git verb triggers the regex."""
|
||||
assert BLOCKED_GIT_RE.search(cmd), f"Should block: {cmd}"
|
||||
|
||||
@pytest.mark.parametrize("cmd", [
|
||||
"git stash drop",
|
||||
"git stash clear",
|
||||
"git stash pop",
|
||||
"git stash apply",
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"cmd",
|
||||
[
|
||||
"git stash drop",
|
||||
"git stash clear",
|
||||
"git stash pop",
|
||||
"git stash apply",
|
||||
],
|
||||
)
|
||||
def test_blocks_stash_destructive(self, cmd):
|
||||
"""Destructive stash subcommands are blocked."""
|
||||
assert BLOCKED_GIT_STASH_RE.search(cmd), f"Should block: {cmd}"
|
||||
|
||||
@pytest.mark.parametrize("cmd", [
|
||||
"git branch -D old",
|
||||
"git branch -d old",
|
||||
"git branch -m old new",
|
||||
"git branch -M old new",
|
||||
"git branch -c old new",
|
||||
"git branch --delete old",
|
||||
"git branch --move old new",
|
||||
"git branch --copy old new",
|
||||
"git branch --force old",
|
||||
"git branch --set-upstream-to=origin/main",
|
||||
"git branch --unset-upstream",
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"cmd",
|
||||
[
|
||||
"git branch -D old",
|
||||
"git branch -d old",
|
||||
"git branch -m old new",
|
||||
"git branch -M old new",
|
||||
"git branch -c old new",
|
||||
"git branch --delete old",
|
||||
"git branch --move old new",
|
||||
"git branch --copy old new",
|
||||
"git branch --force old",
|
||||
"git branch --set-upstream-to=origin/main",
|
||||
"git branch --unset-upstream",
|
||||
],
|
||||
)
|
||||
def test_blocks_branch_destructive(self, cmd):
|
||||
"""Destructive branch subcommands are blocked."""
|
||||
assert BLOCKED_GIT_BRANCH_RE.search(cmd), f"Should block: {cmd}"
|
||||
|
||||
@pytest.mark.parametrize("cmd", [
|
||||
"git tag -d v1.0",
|
||||
"git tag --delete v1.0",
|
||||
"git tag -f v1.0",
|
||||
"git tag --force v1.0",
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"cmd",
|
||||
[
|
||||
"git tag -d v1.0",
|
||||
"git tag --delete v1.0",
|
||||
"git tag -f v1.0",
|
||||
"git tag --force v1.0",
|
||||
],
|
||||
)
|
||||
def test_blocks_tag_destructive(self, cmd):
|
||||
"""Destructive tag operations are blocked."""
|
||||
assert BLOCKED_GIT_TAG_RE.search(cmd), f"Should block: {cmd}"
|
||||
|
||||
@pytest.mark.parametrize("cmd", [
|
||||
"git remote add origin url",
|
||||
"git remote remove origin",
|
||||
"git remote rename old new",
|
||||
"git remote set-url origin url",
|
||||
"git remote prune origin",
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"cmd",
|
||||
[
|
||||
"git remote add origin url",
|
||||
"git remote remove origin",
|
||||
"git remote rename old new",
|
||||
"git remote set-url origin url",
|
||||
"git remote prune origin",
|
||||
],
|
||||
)
|
||||
def test_blocks_remote_destructive(self, cmd):
|
||||
"""Destructive remote operations are blocked."""
|
||||
assert BLOCKED_GIT_REMOTE_RE.search(cmd), f"Should block: {cmd}"
|
||||
@@ -111,17 +126,20 @@ class TestGitWriteBlocking:
|
||||
class TestLongFormFlagBypass:
|
||||
"""DPLAN-0163 Finding 1: long-form flags must not bypass detection."""
|
||||
|
||||
@pytest.mark.parametrize("cmd", [
|
||||
"git --config core.hooksPath=/dev/null commit",
|
||||
"git --no-pager push",
|
||||
"git -c x=y commit",
|
||||
"git --git-dir=/x checkout",
|
||||
"git --config=core.hooksPath=/dev/null commit",
|
||||
"git --no-pager -c x=y push",
|
||||
"git --work-tree=/tmp commit -m 'x'",
|
||||
"git -C /some/path commit",
|
||||
"git --bare push origin main",
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"cmd",
|
||||
[
|
||||
"git --config core.hooksPath=/dev/null commit",
|
||||
"git --no-pager push",
|
||||
"git -c x=y commit",
|
||||
"git --git-dir=/x checkout",
|
||||
"git --config=core.hooksPath=/dev/null commit",
|
||||
"git --no-pager -c x=y push",
|
||||
"git --work-tree=/tmp commit -m 'x'",
|
||||
"git -C /some/path commit",
|
||||
"git --bare push origin main",
|
||||
],
|
||||
)
|
||||
def test_blocks_long_form_flag_bypass(self, cmd):
|
||||
"""Long-form flags before the verb must not hide the write verb."""
|
||||
assert BLOCKED_GIT_RE.search(cmd), f"Should block: {cmd}"
|
||||
@@ -135,58 +153,63 @@ class TestEscapedQuoteBypass:
|
||||
inside those strings must not break the stripping.
|
||||
"""
|
||||
|
||||
@pytest.mark.parametrize("cmd,should_block", [
|
||||
('echo "git commit inside quotes"', False),
|
||||
("echo 'git push inside single quotes'", False),
|
||||
('echo "msg \\"escaped\\" inner"', False),
|
||||
("echo 'msg \\'escaped\\' inner'", False),
|
||||
('drone @git pr "fix: git commit msg"', False),
|
||||
('git commit -m "msg \\"escaped\\""', True),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"cmd,should_block",
|
||||
[
|
||||
('echo "git commit inside quotes"', False),
|
||||
("echo 'git push inside single quotes'", False),
|
||||
('echo "msg \\"escaped\\" inner"', False),
|
||||
("echo 'msg \\'escaped\\' inner'", False),
|
||||
('drone @git pr "fix: git commit msg"', False),
|
||||
('git commit -m "msg \\"escaped\\""', True),
|
||||
],
|
||||
)
|
||||
def test_escaped_quote_stripping(self, cmd, should_block):
|
||||
"""Escaped quotes inside strings must not leak verb matches."""
|
||||
scan = re.sub(r'"(?:[^"\\]|\\.)*"', '""', cmd)
|
||||
scan = re.sub(r"'(?:[^'\\]|\\.)*'", "''", scan)
|
||||
matched = bool(BLOCKED_GIT_RE.search(scan))
|
||||
assert matched == should_block, (
|
||||
f"{'Should block' if should_block else 'Should allow'}: {cmd}\n"
|
||||
f" After strip: {scan}"
|
||||
f"{'Should block' if should_block else 'Should allow'}: {cmd}\n After strip: {scan}"
|
||||
)
|
||||
|
||||
|
||||
class TestReadOnlyAllowed:
|
||||
"""Read-only git commands must not be blocked."""
|
||||
|
||||
@pytest.mark.parametrize("cmd", [
|
||||
"git status",
|
||||
"git log --oneline",
|
||||
"git diff",
|
||||
"git diff --staged",
|
||||
"git show HEAD",
|
||||
"git fetch",
|
||||
"git fetch origin",
|
||||
"git ls-files",
|
||||
"git log --graph --all",
|
||||
"git stash list",
|
||||
"git stash show",
|
||||
"git rev-parse HEAD",
|
||||
"git describe --tags",
|
||||
"git remote -v",
|
||||
"git blame file.py",
|
||||
"git shortlog -sn",
|
||||
"git branch",
|
||||
"git branch -r",
|
||||
"git branch -a",
|
||||
"git branch --list",
|
||||
"git branch -v",
|
||||
"git branch --show-current",
|
||||
"git branch --contains abc123",
|
||||
"git tag",
|
||||
"git tag -l",
|
||||
"git tag --list",
|
||||
"git remote",
|
||||
"git remote show origin",
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"cmd",
|
||||
[
|
||||
"git status",
|
||||
"git log --oneline",
|
||||
"git diff",
|
||||
"git diff --staged",
|
||||
"git show HEAD",
|
||||
"git fetch",
|
||||
"git fetch origin",
|
||||
"git ls-files",
|
||||
"git log --graph --all",
|
||||
"git stash list",
|
||||
"git stash show",
|
||||
"git rev-parse HEAD",
|
||||
"git describe --tags",
|
||||
"git remote -v",
|
||||
"git blame file.py",
|
||||
"git shortlog -sn",
|
||||
"git branch",
|
||||
"git branch -r",
|
||||
"git branch -a",
|
||||
"git branch --list",
|
||||
"git branch -v",
|
||||
"git branch --show-current",
|
||||
"git branch --contains abc123",
|
||||
"git tag",
|
||||
"git tag -l",
|
||||
"git tag --list",
|
||||
"git remote",
|
||||
"git remote show origin",
|
||||
],
|
||||
)
|
||||
def test_allows_read_only(self, cmd):
|
||||
"""Read-only git subcommands must pass through."""
|
||||
assert not BLOCKED_GIT_RE.search(cmd), f"Should allow: {cmd}"
|
||||
@@ -199,14 +222,17 @@ class TestReadOnlyAllowed:
|
||||
class TestDroneNotBlocked:
|
||||
"""Drone commands must never be blocked."""
|
||||
|
||||
@pytest.mark.parametrize("cmd", [
|
||||
'drone @git pr "description"',
|
||||
'drone @git system-pr "fix"',
|
||||
"drone @git smart-sync",
|
||||
"drone @git sync",
|
||||
"drone @git status",
|
||||
"drone @git merge 42",
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"cmd",
|
||||
[
|
||||
'drone @git pr "description"',
|
||||
'drone @git system-pr "fix"',
|
||||
"drone @git smart-sync",
|
||||
"drone @git sync",
|
||||
"drone @git status",
|
||||
"drone @git merge 42",
|
||||
],
|
||||
)
|
||||
def test_allows_drone(self, cmd):
|
||||
"""Drone-wrapped git ops are not raw git — must pass."""
|
||||
assert not BLOCKED_GIT_RE.search(cmd), f"Should allow: {cmd}"
|
||||
@@ -215,31 +241,37 @@ class TestDroneNotBlocked:
|
||||
class TestGhBlocking:
|
||||
"""gh write subcommands blocked, read-only allowed."""
|
||||
|
||||
@pytest.mark.parametrize("cmd", [
|
||||
"gh pr create --title x",
|
||||
"gh pr merge 42",
|
||||
"gh pr close 42",
|
||||
"gh issue create",
|
||||
"gh issue close 5",
|
||||
"gh release create v1",
|
||||
"gh repo create x",
|
||||
"gh api repos/x/pulls",
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"cmd",
|
||||
[
|
||||
"gh pr create --title x",
|
||||
"gh pr merge 42",
|
||||
"gh pr close 42",
|
||||
"gh issue create",
|
||||
"gh issue close 5",
|
||||
"gh release create v1",
|
||||
"gh repo create x",
|
||||
"gh api repos/x/pulls",
|
||||
],
|
||||
)
|
||||
def test_blocks_gh_writes(self, cmd):
|
||||
"""State-changing gh subcommands are blocked."""
|
||||
blocked = BLOCKED_GH_RE.search(cmd) or BLOCKED_GH_API_RE.search(cmd)
|
||||
assert blocked, f"Should block: {cmd}"
|
||||
|
||||
@pytest.mark.parametrize("cmd", [
|
||||
"gh pr list",
|
||||
"gh pr view 42",
|
||||
"gh pr status",
|
||||
"gh pr diff 42",
|
||||
"gh pr checks 42",
|
||||
"gh issue list",
|
||||
"gh issue view 5",
|
||||
"gh issue status",
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"cmd",
|
||||
[
|
||||
"gh pr list",
|
||||
"gh pr view 42",
|
||||
"gh pr status",
|
||||
"gh pr diff 42",
|
||||
"gh pr checks 42",
|
||||
"gh issue list",
|
||||
"gh issue view 5",
|
||||
"gh issue status",
|
||||
],
|
||||
)
|
||||
def test_allows_gh_reads(self, cmd):
|
||||
"""Read-only gh subcommands must pass through."""
|
||||
assert not BLOCKED_GH_RE.search(cmd), f"Should allow: {cmd}"
|
||||
@@ -249,24 +281,30 @@ class TestGhBlocking:
|
||||
class TestEditBlocking:
|
||||
"""Protected file paths must be blocked by edit patterns."""
|
||||
|
||||
@pytest.mark.parametrize("path", [
|
||||
"/home/user/.claude/settings.json",
|
||||
"/home/user/.claude/settings.local.json",
|
||||
"/home/user/.claude/hooks/git_gate.py",
|
||||
"/home/user/.claude/hooks/pre_edit_gate.py",
|
||||
"/repo/.git/hooks/pre-commit",
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/home/user/.claude/settings.json",
|
||||
"/home/user/.claude/settings.local.json",
|
||||
"/home/user/.claude/hooks/git_gate.py",
|
||||
"/home/user/.claude/hooks/pre_edit_gate.py",
|
||||
"/repo/.git/hooks/pre-commit",
|
||||
],
|
||||
)
|
||||
def test_blocks_protected_paths(self, path):
|
||||
"""Enforcement-layer files are protected from edits."""
|
||||
matched = any(p.search(path) for p in BLOCKED_EDIT_PATTERNS)
|
||||
assert matched, f"Should block edit: {path}"
|
||||
|
||||
@pytest.mark.parametrize("path", [
|
||||
"/home/user/project/src/main.py",
|
||||
"/home/user/.claude/CLAUDE.md",
|
||||
"/home/user/project/.git/config",
|
||||
"/home/user/project/src/aipass/devpulse/apps/handler.py",
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/home/user/project/src/main.py",
|
||||
"/home/user/.claude/CLAUDE.md",
|
||||
"/home/user/project/.git/config",
|
||||
"/home/user/project/src/aipass/devpulse/apps/handler.py",
|
||||
],
|
||||
)
|
||||
def test_allows_normal_paths(self, path):
|
||||
"""Normal project files are not blocked."""
|
||||
matched = any(p.search(path) for p in BLOCKED_EDIT_PATTERNS)
|
||||
|
||||
Reference in New Issue
Block a user