"""Unit tests for devx.tools.configure_repo.""" from unittest.mock import MagicMock, patch import click import pytest from click.testing import CliRunner from devx.exceptions import APIError from devx.tools.configure_repo import ( _STANDARD_LABELS, _default_branch_protection_config, _default_repo_settings_config, _handle_http_error, configure_repo, main, ) class TestHandleHttpError: def test_forbidden_raises_click_exception(self) -> None: with pytest.raises(click.ClickException, match="Forbidden"): _handle_http_error(APIError(403, "Forbidden")) def test_other_error_raises_click_exception(self) -> None: with pytest.raises(click.ClickException, match="HTTP error"): _handle_http_error(APIError(500, "Server error")) class TestDefaultConfigs: def test_default_branch_protection_config(self) -> None: config = _default_branch_protection_config() assert config["branch_name"] == "master" assert config["enable_push"] is True assert config["enable_push_whitelist"] is False assert config["required_approvals"] == 1 assert isinstance(config["status_check_contexts"], list) assert "CI / quality (pull_request)" in config["status_check_contexts"] assert config["block_admin_merge_override"] is True def test_default_repo_settings_config(self) -> None: config = _default_repo_settings_config() assert config["default_delete_branch_after_merge"] is True def test_status_checks_from_env(self) -> None: with patch.dict("os.environ", {"DEVX_STATUS_CHECKS": "check1, check2, check3"}): config = _default_branch_protection_config() assert config["status_check_contexts"] == ["check1", "check2", "check3"] class TestConfigureRepo: @patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True) @patch("devx.tools.configure_repo.GiteaClient") def test_configure_repo_success(self, mock_client_cls: MagicMock) -> None: mock_client = MagicMock() mock_client_cls.return_value = mock_client configure_repo(token="tok", owner="owner", repo="repo") mock_client.ensure_branch_protection.assert_called_once() mock_client.update_repo_settings.assert_called_once() assert mock_client.ensure_label.call_count == len(_STANDARD_LABELS) @patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True) @patch("devx.tools.configure_repo.GiteaClient") def test_configure_repo_api_error(self, mock_client_cls: MagicMock) -> None: mock_client = MagicMock() mock_client.ensure_branch_protection.side_effect = APIError(403, "Forbidden") mock_client_cls.return_value = mock_client with pytest.raises(click.ClickException, match="Forbidden"): configure_repo(token="tok", owner="owner", repo="repo") def test_configure_repo_no_token(self) -> None: with pytest.raises(click.ClickException, match="CI_GITEA_TOKEN"): configure_repo(token="", owner="owner", repo="repo") @patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True) @patch("devx.tools.configure_repo.GiteaClient") def test_configure_repo_custom_configs(self, mock_client_cls: MagicMock) -> None: mock_client = MagicMock() mock_client_cls.return_value = mock_client custom_bp = { "branch_name": "develop", "enable_push": True, "enable_push_whitelist": True, "push_whitelist_usernames": [], "enable_status_check": True, "status_check_contexts": ["CI / quality (pull_request)"], "required_approvals": 2, } custom_rs = {"default_delete_branch_after_merge": False} configure_repo( token="tok", owner="owner", repo="repo", branch="develop", branch_protection_config=custom_bp, repo_settings_config=custom_rs, ) mock_client.ensure_branch_protection.assert_called_once_with("develop", custom_bp) mock_client.update_repo_settings.assert_called_once_with(custom_rs) # Labels are created regardless of custom configs assert mock_client.ensure_label.call_count == len(_STANDARD_LABELS) @patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True) @patch("devx.tools.configure_repo.GiteaClient") def test_configure_repo_creates_all_standard_labels(self, mock_client_cls: MagicMock) -> None: """Verify all standard labels are ensured with correct names.""" mock_client = MagicMock() mock_client_cls.return_value = mock_client configure_repo(token="tok", owner="owner", repo="repo") created_names = [call.args[0] for call in mock_client.ensure_label.call_args_list] expected_names = [lbl["name"] for lbl in _STANDARD_LABELS] assert created_names == expected_names class TestMain: @patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok", "DEVX_REPO_NAME": "myrepo"}, clear=True) @patch("devx.tools.configure_repo.GiteaClient") def test_main_success_with_env_repo(self, mock_client_cls: MagicMock) -> None: mock_client = MagicMock() mock_client_cls.return_value = mock_client runner = CliRunner() result = runner.invoke(main, []) assert result.exit_code == 0 mock_client.ensure_branch_protection.assert_called_once() mock_client.update_repo_settings.assert_called_once() assert mock_client.ensure_label.call_count == len(_STANDARD_LABELS) @patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True) @patch("devx.tools.configure_repo.GiteaClient") def test_main_success_with_cli_repo(self, mock_client_cls: MagicMock) -> None: mock_client = MagicMock() mock_client_cls.return_value = mock_client runner = CliRunner() result = runner.invoke(main, ["--repo", "myrepo", "--owner", "myorg"]) assert result.exit_code == 0 mock_client.ensure_branch_protection.assert_called_once() assert mock_client.ensure_label.call_count == len(_STANDARD_LABELS) @patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True) @patch("devx.tools.configure_repo.GiteaClient") def test_main_api_error(self, mock_client_cls: MagicMock) -> None: mock_client = MagicMock() mock_client.ensure_branch_protection.side_effect = APIError(403, "Forbidden") mock_client_cls.return_value = mock_client runner = CliRunner() result = runner.invoke(main, ["--repo", "myrepo"]) assert result.exit_code != 0 assert "Forbidden" in result.output @patch.dict("os.environ", {}, clear=True) def test_main_no_token(self) -> None: runner = CliRunner() result = runner.invoke(main, ["--repo", "myrepo"]) assert result.exit_code != 0 assert "CI_GITEA_TOKEN" in result.output @patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True) @patch("devx.tools.configure_repo.REPO_NAME", "") def test_main_no_repo(self) -> None: runner = CliRunner() result = runner.invoke(main, []) assert result.exit_code != 0 assert "Repository name not specified" in result.output @patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True) @patch("devx.tools.configure_repo.GiteaClient") def test_main_custom_branch(self, mock_client_cls: MagicMock) -> None: mock_client = MagicMock() mock_client_cls.return_value = mock_client runner = CliRunner() result = runner.invoke(main, ["--repo", "myrepo", "--branch", "develop"]) assert result.exit_code == 0 mock_client.ensure_branch_protection.assert_called_once() args = mock_client.ensure_branch_protection.call_args assert args[0][0] == "develop" @patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok", "DEVX_REPO_NAME": "my-org/my-repo"}, clear=True) @patch("devx.tools.configure_repo.GiteaClient") def test_main_parses_owner_repo_from_env(self, mock_client_cls: MagicMock) -> None: """DEVX_REPO_NAME with 'owner/repo' format should be split.""" mock_client = MagicMock() mock_client_cls.return_value = mock_client runner = CliRunner() result = runner.invoke(main, []) assert result.exit_code == 0 # Verify GiteaClient was constructed with parsed owner and repo (positional) call_args = mock_client_cls.call_args assert call_args[0][2] == "my-org" # owner is 3rd positional arg assert call_args[0][3] == "my-repo" # repo is 4th positional arg @patch.dict( "os.environ", {"CI_GITEA_TOKEN": "tok", "DEVX_REPO_NAME": "my-repo", "DEVX_REPO_OWNER": "my-org"}, clear=True, ) @patch("devx.tools.configure_repo.REPO_OWNER", "my-org") @patch("devx.tools.configure_repo.GiteaClient") def test_main_no_slash_when_owner_set_separately(self, mock_client_cls: MagicMock) -> None: """When DEVX_REPO_OWNER is set, DEVX_REPO_NAME should not be split.""" mock_client = MagicMock() mock_client_cls.return_value = mock_client runner = CliRunner() result = runner.invoke(main, []) assert result.exit_code == 0 call_args = mock_client_cls.call_args assert call_args[0][2] == "my-org" # owner assert call_args[0][3] == "my-repo" # repo @patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True) @patch("devx.tools.configure_repo.REPO_NAME", "devx") @patch("devx.tools.configure_repo.REPO_OWNER", "my-org") @patch("devx.tools.configure_repo.GiteaClient") def test_main_repo_from_pyproject(self, mock_client_cls: MagicMock) -> None: """When no env var is set, repo name should come from pyproject.toml.""" mock_client = MagicMock() mock_client_cls.return_value = mock_client runner = CliRunner() result = runner.invoke(main, []) assert result.exit_code == 0 call_args = mock_client_cls.call_args assert call_args[0][2] == "my-org" # owner assert call_args[0][3] == "devx" # repo