293 lines
13 KiB
Python
293 lines
13 KiB
Python
"""Unit tests for scripts/configure_repo.py."""
|
|
|
|
import http
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import click
|
|
import pytest
|
|
import requests
|
|
|
|
from scripts.configure_repo import (
|
|
BRANCH_PROTECTION_CONFIG,
|
|
LABEL_CONFIG,
|
|
GiteaRepoConfig,
|
|
_handle_http_error,
|
|
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(click.ClickException) as exc:
|
|
main()
|
|
assert "GITEA_ADMIN_TOKEN" in str(exc.value)
|
|
|
|
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(click.ClickException) as exc:
|
|
main()
|
|
assert "HTTP" in str(exc.value)
|
|
|
|
def test_handle_http_error_403(self) -> None:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = http.HTTPStatus.FORBIDDEN
|
|
mock_response.json.return_value = {"message": "Forbidden"}
|
|
err = requests.HTTPError("403", response=mock_response)
|
|
with pytest.raises(click.ClickException) as exc:
|
|
_handle_http_error(err)
|
|
msg = str(exc.value)
|
|
assert "admin rights" in msg
|
|
assert "Settings → Branches" in msg
|
|
|
|
def test_handle_http_error_other(self) -> None:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = http.HTTPStatus.INTERNAL_SERVER_ERROR
|
|
mock_response.json.return_value = {"message": "Internal Server Error"}
|
|
err = requests.HTTPError("500", response=mock_response)
|
|
with pytest.raises(click.ClickException) as exc:
|
|
_handle_http_error(err)
|
|
assert "500" in str(exc.value)
|
|
|
|
def test_handle_http_error_json_parse_fails(self) -> None:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = http.HTTPStatus.BAD_GATEWAY
|
|
mock_response.json.side_effect = ValueError("not json")
|
|
err = requests.HTTPError("502", response=mock_response)
|
|
with pytest.raises(click.ClickException) as exc:
|
|
_handle_http_error(err)
|
|
assert "502" in str(exc.value)
|
|
|
|
|
|
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)
|