GRM-24: feat: bandit integration (#1)

Co-authored-by: Emil Simeonov <emil@theliberatededge.org>
Reviewed-on: https://git.oblachno.oblachno.fyi/oblachno-oss/grm/pulls/1
This commit is contained in:
emil
2026-06-19 21:17:39 +00:00
co-authored by Emil Simeonov
parent d8c31238bd
commit d949bd3444
21 changed files with 325 additions and 75 deletions
+8 -7
View File
@@ -146,13 +146,14 @@ class TestGiteaClient:
def test_update_branch_protection(self) -> None:
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
client._session.request = MagicMock(return_value=_mock_response({"id": 1, "required_approvals": 2}))
resp = {"branch_name": "master", "required_approvals": 2}
client._session.request = MagicMock(return_value=_mock_response(resp))
update = {"required_approvals": 2}
result = client.update_branch_protection(1, update)
result = client.update_branch_protection("master", update)
assert result["required_approvals"] == 2
client._session.request.assert_called_once_with(
"PATCH",
"https://git.example.com/repos/owner/repo/branch_protections/1",
"https://git.example.com/repos/owner/repo/branch_protections/master",
timeout=DEFAULT_TIMEOUT,
json=update,
)
@@ -168,13 +169,13 @@ class TestGiteaClient:
def test_ensure_branch_protection_updates_when_exists(self) -> None:
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
client.list_branch_protections = MagicMock(return_value=[{"id": 5, "branch_name": "master"}])
client.update_branch_protection = MagicMock(return_value={"id": 5, "required_approvals": 1})
client.list_branch_protections = MagicMock(return_value=[{"branch_name": "master", "required_approvals": 0}])
client.update_branch_protection = MagicMock(return_value={"branch_name": "master", "required_approvals": 1})
result = client.ensure_branch_protection("master", BRANCH_PROTECTION_CONFIG)
assert result["id"] == 5
assert result["required_approvals"] == 1
expected_update = {k: v for k, v in BRANCH_PROTECTION_CONFIG.items() if k != "branch_name"}
client.update_branch_protection.assert_called_once_with(5, expected_update)
client.update_branch_protection.assert_called_once_with("master", expected_update)
def test_merge_pr(self) -> None:
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
+7 -7
View File
@@ -54,7 +54,7 @@ class TestValidatePrTitle:
class TestMain:
@patch.dict("os.environ", {"GITEA_TOKEN": "tok"})
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch("scripts.auto_merge.GiteaClient")
def test_successful_flow(self, mock_client_cls: MagicMock) -> None:
mock_client = MagicMock()
@@ -69,28 +69,28 @@ class TestMain:
mock_client_cls.assert_called_once()
mock_client.merge_pr.assert_called_once_with("7", "GRM-19: fix: resolve timeout")
@patch.dict("os.environ", {"GITEA_TOKEN": ""}, clear=True)
@patch.dict("os.environ", {"REPO_TOKEN": ""}, clear=True)
def test_missing_token_exits(self) -> None:
runner = CliRunner()
result = runner.invoke(main, ["branch", "title", "repo", "1"])
assert result.exit_code == 1
assert "GITEA_TOKEN" in result.output
assert "REPO_TOKEN" in result.output
@patch.dict("os.environ", {"GITEA_TOKEN": "tok"})
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
def test_missing_task_id_exits(self) -> None:
runner = CliRunner()
result = runner.invoke(main, ["feature-no-id", "fix: bug", "repo", "1"])
assert result.exit_code == 1
assert "task ID" in result.output
@patch.dict("os.environ", {"GITEA_TOKEN": "tok"})
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
def test_invalid_pr_title_exits(self) -> None:
runner = CliRunner()
result = runner.invoke(main, ["GRM-19-fix", "random title", "repo", "1"])
assert result.exit_code == 1
assert "conventional" in result.output
@patch.dict("os.environ", {"GITEA_TOKEN": "tok"})
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch("scripts.auto_merge.GiteaClient")
def test_merge_pr_failure_raises_click(self, mock_client_cls: MagicMock) -> None:
mock_client = MagicMock()
@@ -101,7 +101,7 @@ class TestMain:
assert result.exit_code == 1
assert "HTTP" in result.output
@patch.dict("os.environ", {"GITEA_TOKEN": "tok"})
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch("scripts.auto_merge.GiteaClient")
def test_merge_pr_json_parse_failure(self, mock_client_cls: MagicMock) -> None:
mock_client = MagicMock()
+137
View File
@@ -0,0 +1,137 @@
"""Unit tests for scripts/check_test_speed.py."""
from unittest.mock import MagicMock, patch
import click
import pytest
from click.testing import CliRunner
from scripts.check_test_speed import (
DEFAULT_MAX_SECONDS,
TEST_COMMAND,
check_speed,
cli,
parse_duration,
run_tests,
)
class TestRunTests:
@patch("scripts.check_test_speed.subprocess.run")
def test_run_tests_returns_stdout_stderr(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(stdout="out", stderr="err", returncode=0)
stdout, stderr = run_tests()
assert stdout == "out"
assert stderr == "err"
mock_run.assert_called_once_with(
TEST_COMMAND,
capture_output=True,
text=True,
check=False,
)
class TestParseDuration:
def test_parses_valid_line(self) -> None:
assert parse_duration("234 passed in 0.70s") == 0.70
def test_parses_multiline_output(self) -> None:
output = "some header\n234 passed in 1.23s\nfooter"
assert parse_duration(output) == 1.23
def test_raises_when_no_timing_line(self) -> None:
with pytest.raises(click.ClickException) as exc:
parse_duration("no timing here")
assert "Could not parse" in str(exc.value)
class TestCheckSpeed:
def test_under_budget_passes(self) -> None:
check_speed(1.0, 2.0) # should not raise
def test_exact_budget_passes(self) -> None:
check_speed(2.0, 2.0) # should not raise
def test_over_budget_raises(self) -> None:
with pytest.raises(click.ClickException) as exc:
check_speed(2.1, 2.0)
msg = str(exc.value)
assert "too slow" in msg.lower()
assert "2.10s" in msg
assert "max allowed: 2.0s" in msg
def test_main_module_block() -> None:
import scripts.check_test_speed as cts
with patch.object(cts, "cli") as mock_cli:
with patch.object(cts, "__name__", "__main__"):
cts.cli([])
mock_cli.assert_called_once_with([])
class TestMain:
@patch("scripts.check_test_speed.run_tests")
@patch("scripts.check_test_speed.parse_duration")
@patch("scripts.check_test_speed.check_speed")
def test_successful_run(
self,
mock_check: MagicMock,
mock_parse: MagicMock,
mock_run: MagicMock,
) -> None:
mock_run.return_value = ("stdout\n", "stderr\n")
mock_parse.return_value = 1.5
runner = CliRunner()
result = runner.invoke(cli, [])
assert result.exit_code == 0
assert "1.50s" in result.output
assert "under 2.0s limit" in result.output
mock_run.assert_called_once()
mock_parse.assert_called_once_with("stdout\n\nstderr\n")
mock_check.assert_called_once_with(1.5, DEFAULT_MAX_SECONDS)
@patch("scripts.check_test_speed.run_tests")
@patch("scripts.check_test_speed.parse_duration")
def test_slow_tests_exit(
self,
mock_parse: MagicMock,
mock_run: MagicMock,
) -> None:
mock_run.return_value = ("out\n", "err\n")
mock_parse.return_value = 3.0
runner = CliRunner()
result = runner.invoke(cli, [])
assert result.exit_code == 1
assert "too slow" in result.output.lower()
@patch("scripts.check_test_speed.run_tests")
def test_parse_failure_exits(
self,
mock_run: MagicMock,
) -> None:
mock_run.return_value = ("bad output\n", "")
runner = CliRunner()
result = runner.invoke(cli, [])
assert result.exit_code == 1
assert "Could not parse" in result.output
@patch("scripts.check_test_speed.run_tests")
@patch("scripts.check_test_speed.parse_duration")
@patch("scripts.check_test_speed.check_speed")
def test_custom_max_seconds(
self,
mock_check: MagicMock,
mock_parse: MagicMock,
mock_run: MagicMock,
) -> None:
mock_run.return_value = ("out\n", "err\n")
mock_parse.return_value = 0.5
runner = CliRunner()
result = runner.invoke(cli, ["--max-seconds", "1.5"])
assert result.exit_code == 0
mock_check.assert_called_once_with(0.5, 1.5)
+5 -5
View File
@@ -19,7 +19,7 @@ class TestCLI:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "GITEA_ADMIN_TOKEN": ""})
runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "REPO_TOKEN": ""})
result = runner.invoke(cli, ["install", "host1", "--user", "ubuntu", "--token", "tok"])
assert result.exit_code == 0
mock_manager.install.assert_called_once_with(
@@ -40,7 +40,7 @@ class TestCLI:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "GITEA_ADMIN_TOKEN": ""})
runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "REPO_TOKEN": ""})
result = runner.invoke(cli, ["install", "host1", "--user", "ubuntu", "--token", "tok", "--no-ask-become-pass"])
assert result.exit_code == 0
mock_manager.install.assert_called_once_with(
@@ -114,7 +114,7 @@ class TestCLI:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "GITEA_ADMIN_TOKEN": ""})
runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "REPO_TOKEN": ""})
result = runner.invoke(
cli,
[
@@ -149,7 +149,7 @@ class TestCLI:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "GITEA_ADMIN_TOKEN": ""})
runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "REPO_TOKEN": ""})
result = runner.invoke(cli, ["install", "host1", "--user", "ubuntu", "--token", "tok", "--ask-become-pass"])
assert result.exit_code == 0
mock_manager.install.assert_called_once_with(
@@ -170,7 +170,7 @@ class TestCLI:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "GITEA_ADMIN_TOKEN": ""})
runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "REPO_TOKEN": ""})
result = runner.invoke(cli, ["install", "host1", "--user", "ubuntu", "--token", "tok", "--mode", "binary"])
assert result.exit_code == 0
mock_manager.install.assert_called_once_with(
+5 -5
View File
@@ -41,10 +41,10 @@ class TestMain:
with patch.dict("os.environ", {}, clear=True):
with pytest.raises(click.ClickException) as exc:
main()
assert "GITEA_ADMIN_TOKEN" in str(exc.value)
assert "REPO_TOKEN" in str(exc.value)
def test_main_success(self) -> None:
with patch.dict("os.environ", {"GITEA_ADMIN_TOKEN": "tok"}, clear=True):
with patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True):
with patch("scripts.configure_repo.GiteaClient") as mock_client_cls:
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
@@ -55,7 +55,7 @@ class TestMain:
mock_client.ensure_label.assert_called_once()
def test_main_label_already_exists(self) -> None:
with patch.dict("os.environ", {"GITEA_ADMIN_TOKEN": "tok"}, clear=True):
with patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True):
with patch("scripts.configure_repo.GiteaClient") as mock_client_cls:
mock_client = MagicMock()
mock_client.ensure_label.return_value = None
@@ -67,7 +67,7 @@ class TestMain:
mock_client.ensure_label.assert_called_once()
def test_main_api_error(self) -> None:
with patch.dict("os.environ", {"GITEA_ADMIN_TOKEN": "tok"}, clear=True):
with patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True):
with patch("scripts.configure_repo.GiteaClient") as mock_client_cls:
mock_client = MagicMock()
mock_client.ensure_branch_protection.side_effect = APIError(http.HTTPStatus.FORBIDDEN, "Forbidden")
@@ -79,7 +79,7 @@ class TestMain:
def test_main_module_block() -> None:
with patch.dict("os.environ", {"GITEA_ADMIN_TOKEN": "tok"}, clear=True):
with patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True):
with patch("scripts.configure_repo.GiteaClient") as mock_client_cls:
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
+9 -9
View File
@@ -49,7 +49,7 @@ class TestPublishToPypi:
class TestMain:
@patch.dict("os.environ", {"GITEA_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch("scripts.publish.GiteaClient")
@patch("scripts.publish.publish_to_pypi")
@patch("scripts.publish.build_package")
@@ -67,7 +67,7 @@ class TestMain:
mock_publish.assert_called_once_with("pypi-tok")
mock_client_cls.return_value.create_release.assert_called_once()
@patch.dict("os.environ", {"GITEA_TOKEN": "gitea-tok"}, clear=True)
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok"}, clear=True)
@patch("scripts.publish.GiteaClient")
@patch("scripts.publish.build_package")
def test_without_pypi(
@@ -82,14 +82,14 @@ class TestMain:
mock_client_cls.return_value.create_release.assert_called_once()
assert "PYPI_TOKEN not set" in result.output
@patch.dict("os.environ", {"GITEA_TOKEN": ""}, clear=True)
def test_missing_gitea_token_exits(self) -> None:
@patch.dict("os.environ", {"REPO_TOKEN": ""}, clear=True)
def test_missing_repo_token_exits(self) -> None:
runner = CliRunner()
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
assert result.exit_code == 1
assert "GITEA_TOKEN" in result.output
assert "REPO_TOKEN" in result.output
@patch.dict("os.environ", {"GITEA_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch("scripts.publish.GiteaClient")
@patch("scripts.publish.publish_to_pypi")
@patch("scripts.publish.build_package")
@@ -102,7 +102,7 @@ class TestMain:
assert result.exit_code == 1
assert "build" in result.output
@patch.dict("os.environ", {"GITEA_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch("scripts.publish.GiteaClient")
@patch("scripts.publish.publish_to_pypi")
@patch("scripts.publish.build_package")
@@ -115,7 +115,7 @@ class TestMain:
assert result.exit_code == 1
assert "publish" in result.output
@patch.dict("os.environ", {"GITEA_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch("scripts.publish.GiteaClient")
@patch("scripts.publish.publish_to_pypi")
@patch("scripts.publish.build_package")
@@ -132,7 +132,7 @@ class TestMain:
assert result.exit_code == 1
assert "HTTP" in result.output
@patch.dict("os.environ", {"GITEA_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch("scripts.publish.GiteaClient")
@patch("scripts.publish.publish_to_pypi")
@patch("scripts.publish.build_package")
+3
View File
@@ -146,6 +146,9 @@ def test_main_module_block() -> None:
source = source.replace('if __name__ == "__main__":\n main()\n', "")
namespace = dict(vcm.__dict__)
exec(compile(source, vcm.__file__, "exec"), namespace)
# exec() redefines get_branch() from source, overwriting the mock.
# Restore the patched mock so main() uses it.
namespace["get_branch"] = vcm.get_branch
namespace["main"]([msg_path], standalone_mode=False)
os.unlink(msg_path)