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:
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user