"""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)