Files
grm/tests/unit/test_configure_repo.py
T
Emil Simeonov 4da724ce53 GRM-21: Implement commit validation, CI/CD workflows and repo automation
- Add scripts/validate_commit_msg.py with conventional commit enforcement
- Add scripts/configure_repo.py for Gitea branch protection and labels
- Add scripts/__init__.py for Python package importability
- Create Gitea Actions workflows: ci, auto-merge, post-merge, publish
- Update .pre-commit-config.yaml with commit-msg hook
- Update pyproject.toml pythonpath and coverage for scripts
- Add comprehensive unit tests for both scripts with 100% coverage
2026-06-19 04:27:43 +02:00

260 lines
12 KiB
Python

"""Unit tests for scripts/configure_repo.py."""
from unittest.mock import MagicMock, patch
import pytest
import requests
from scripts.configure_repo import (
BRANCH_PROTECTION_CONFIG,
LABEL_CONFIG,
GiteaRepoConfig,
main,
)
class TestGiteaRepoConfig:
def test_init_sets_headers(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
assert cfg._base_url == "https://git.example.com"
assert cfg._owner == "owner"
assert cfg._repo == "repo"
assert cfg._session.headers["Authorization"] == "token tok"
assert cfg._session.headers["Content-Type"] == "application/json"
def test_url_constructs_path(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
assert cfg._url("/branch_protections") == ("https://git.example.com/repos/owner/repo/branch_protections")
def test_url_strips_trailing_slash(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com/", "tok", "owner", "repo")
assert cfg._url("/labels") == ("https://git.example.com/repos/owner/repo/labels")
def test_list_branch_protections(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
mock_response = MagicMock()
mock_response.json.return_value = [
{"id": 1, "branch_name": "master"},
{"id": 2, "branch_name": "develop"},
]
cfg._session.get = MagicMock(return_value=mock_response)
result = cfg.list_branch_protections()
assert len(result) == 2
assert result[0]["branch_name"] == "master"
cfg._session.get.assert_called_once_with("https://git.example.com/repos/owner/repo/branch_protections")
def test_list_branch_protections_raises_on_error(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = requests.HTTPError("500")
cfg._session.get = MagicMock(return_value=mock_response)
with pytest.raises(requests.HTTPError):
cfg.list_branch_protections()
def test_create_branch_protection(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
mock_response = MagicMock()
mock_response.json.return_value = {"id": 3, "branch_name": "master"}
cfg._session.post = MagicMock(return_value=mock_response)
result = cfg.create_branch_protection(BRANCH_PROTECTION_CONFIG)
assert result["id"] == 3
cfg._session.post.assert_called_once_with(
"https://git.example.com/repos/owner/repo/branch_protections",
json=BRANCH_PROTECTION_CONFIG,
)
def test_create_branch_protection_raises_on_error(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = requests.HTTPError("403")
cfg._session.post = MagicMock(return_value=mock_response)
with pytest.raises(requests.HTTPError):
cfg.create_branch_protection(BRANCH_PROTECTION_CONFIG)
def test_update_branch_protection(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
mock_response = MagicMock()
mock_response.json.return_value = {"id": 1, "required_approvals": 2}
cfg._session.patch = MagicMock(return_value=mock_response)
update = {"required_approvals": 2}
result = cfg.update_branch_protection(1, update)
assert result["required_approvals"] == 2
cfg._session.patch.assert_called_once_with(
"https://git.example.com/repos/owner/repo/branch_protections/1",
json=update,
)
def test_update_branch_protection_raises_on_error(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = requests.HTTPError("404")
cfg._session.patch = MagicMock(return_value=mock_response)
with pytest.raises(requests.HTTPError):
cfg.update_branch_protection(999, {})
def test_ensure_branch_protection_creates_when_none_exist(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
cfg.list_branch_protections = MagicMock(return_value=[])
cfg.create_branch_protection = MagicMock(return_value={"id": 1, "branch_name": "master"})
result = cfg.ensure_branch_protection("master", BRANCH_PROTECTION_CONFIG)
assert result["id"] == 1
cfg.create_branch_protection.assert_called_once_with(BRANCH_PROTECTION_CONFIG)
def test_ensure_branch_protection_updates_when_exists(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
cfg.list_branch_protections = MagicMock(return_value=[{"id": 5, "branch_name": "master"}])
cfg.update_branch_protection = MagicMock(return_value={"id": 5, "required_approvals": 1})
result = cfg.ensure_branch_protection("master", BRANCH_PROTECTION_CONFIG)
assert result["id"] == 5
# update config should exclude branch_name
expected_update = {k: v for k, v in BRANCH_PROTECTION_CONFIG.items() if k != "branch_name"}
cfg.update_branch_protection.assert_called_once_with(5, expected_update)
def test_ensure_branch_protection_creates_when_other_branches_exist(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
cfg.list_branch_protections = MagicMock(return_value=[{"id": 1, "branch_name": "develop"}])
cfg.create_branch_protection = MagicMock(return_value={"id": 2, "branch_name": "master"})
result = cfg.ensure_branch_protection("master", BRANCH_PROTECTION_CONFIG)
assert result["id"] == 2
cfg.create_branch_protection.assert_called_once_with(BRANCH_PROTECTION_CONFIG)
def test_list_labels(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
mock_response = MagicMock()
mock_response.json.return_value = [
{"name": "bug", "color": "ff0000"},
{"name": "enhancement", "color": "00ff00"},
]
cfg._session.get = MagicMock(return_value=mock_response)
result = cfg.list_labels()
assert len(result) == 2
cfg._session.get.assert_called_once_with("https://git.example.com/repos/owner/repo/labels")
def test_list_labels_raises_on_error(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = requests.HTTPError("500")
cfg._session.get = MagicMock(return_value=mock_response)
with pytest.raises(requests.HTTPError):
cfg.list_labels()
def test_create_label(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
mock_response = MagicMock()
mock_response.json.return_value = {"name": "ready-to-merge", "color": "2ecc71"}
cfg._session.post = MagicMock(return_value=mock_response)
result = cfg.create_label("ready-to-merge", "2ecc71", "Auto-merge label")
assert result["name"] == "ready-to-merge"
cfg._session.post.assert_called_once_with(
"https://git.example.com/repos/owner/repo/labels",
json={"name": "ready-to-merge", "color": "2ecc71", "description": "Auto-merge label"},
)
def test_create_label_raises_on_error(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = requests.HTTPError("422")
cfg._session.post = MagicMock(return_value=mock_response)
with pytest.raises(requests.HTTPError):
cfg.create_label("dup", "ffffff")
def test_ensure_label_creates_when_not_exists(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
cfg.list_labels = MagicMock(return_value=[])
cfg.create_label = MagicMock(return_value={"name": "ready-to-merge", "color": "2ecc71"})
result = cfg.ensure_label("ready-to-merge", "2ecc71", "desc")
assert result is not None
assert result["name"] == "ready-to-merge"
cfg.create_label.assert_called_once_with("ready-to-merge", "2ecc71", "desc")
def test_ensure_label_returns_none_when_exists(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
cfg.list_labels = MagicMock(return_value=[{"name": "ready-to-merge", "color": "2ecc71"}])
cfg.create_label = MagicMock()
result = cfg.ensure_label("ready-to-merge", "2ecc71", "desc")
assert result is None
cfg.create_label.assert_not_called()
def test_ensure_label_creates_when_other_labels_exist(self) -> None:
cfg = GiteaRepoConfig("https://git.example.com", "tok", "owner", "repo")
cfg.list_labels = MagicMock(return_value=[{"name": "bug", "color": "ff0000"}])
cfg.create_label = MagicMock(return_value={"name": "ready-to-merge", "color": "2ecc71"})
result = cfg.ensure_label("ready-to-merge", "2ecc71", "desc")
assert result is not None
cfg.create_label.assert_called_once()
class TestMain:
def test_main_missing_token(self) -> None:
with patch.dict("os.environ", {}, clear=True):
with pytest.raises(SystemExit) as exc:
main()
assert exc.value.code == 1
def test_main_success(self) -> None:
with patch.dict("os.environ", {"GITEA_ADMIN_TOKEN": "tok"}, clear=True):
with patch("scripts.configure_repo.GiteaRepoConfig") as mock_cfg_class:
mock_cfg = MagicMock()
mock_cfg_class.return_value = mock_cfg
main()
mock_cfg.ensure_branch_protection.assert_called_once_with("master", BRANCH_PROTECTION_CONFIG)
mock_cfg.ensure_label.assert_called_once_with(**LABEL_CONFIG)
def test_main_label_already_exists(self) -> None:
with patch.dict("os.environ", {"GITEA_ADMIN_TOKEN": "tok"}, clear=True):
with patch("scripts.configure_repo.GiteaRepoConfig") as mock_cfg_class:
mock_cfg = MagicMock()
mock_cfg.ensure_label.return_value = None
mock_cfg_class.return_value = mock_cfg
main()
mock_cfg.ensure_branch_protection.assert_called_once_with("master", BRANCH_PROTECTION_CONFIG)
mock_cfg.ensure_label.assert_called_once_with(**LABEL_CONFIG)
def test_main_api_error(self) -> None:
with patch.dict("os.environ", {"GITEA_ADMIN_TOKEN": "tok"}, clear=True):
with patch("scripts.configure_repo.GiteaRepoConfig") as mock_cfg_class:
mock_cfg = MagicMock()
mock_cfg.ensure_branch_protection.side_effect = requests.HTTPError("403")
mock_cfg_class.return_value = mock_cfg
with pytest.raises(requests.HTTPError):
main()
def test_main_module_block() -> None:
with patch.dict("os.environ", {"GITEA_ADMIN_TOKEN": "tok"}, clear=True):
with patch("scripts.configure_repo.GiteaRepoConfig") as mock_cfg_class:
mock_cfg = MagicMock()
mock_cfg_class.return_value = mock_cfg
import scripts.configure_repo as cr
with open(cr.__file__) as f:
source = f.read()
# Remove __main__ block so exec doesn't call main() before we inject the mock
source = source.replace('if __name__ == "__main__":\n main()\n', "")
namespace = dict(cr.__dict__)
exec(compile(source, cr.__file__, "exec"), namespace)
namespace["GiteaRepoConfig"] = mock_cfg_class
namespace["main"]()
mock_cfg.ensure_branch_protection.assert_called_once_with("master", BRANCH_PROTECTION_CONFIG)