Files
devx/tests/unit/test_configure_repo.py
T
emil e796b06a91
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 15s
Post-merge / configure-repo (push) Successful in 19s
Post-merge / release (push) Successful in 45s
Post-merge / sync-wiki (push) Successful in 46s
Post-merge / badges (push) Successful in 46s
Post-merge / publish (push) Successful in 17s
DEVX-117: refactor: remove project-specific references from devx
2026-07-06 06:17:52 +00:00

237 lines
10 KiB
Python

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