- 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
260 lines
12 KiB
Python
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)
|