GRM-54: Integrate tea Gitea CLI for API interactions (#68)
This commit is contained in:
@@ -9,9 +9,12 @@ import pytest
|
||||
from gitea_runner_manager.config import BRANCH_PROTECTION_CONFIG, REPO_SETTINGS_CONFIG
|
||||
from gitea_runner_manager.exceptions import APIError
|
||||
from scripts.configure_repo import (
|
||||
_ensure_label_via_client,
|
||||
_ensure_label_via_tea,
|
||||
_handle_http_error,
|
||||
main,
|
||||
)
|
||||
from scripts.gitea_cli import TeaCLIError
|
||||
|
||||
|
||||
class TestHandleHttpError:
|
||||
@@ -36,6 +39,50 @@ class TestHandleHttpError:
|
||||
assert str(http.HTTPStatus.BAD_GATEWAY) in str(exc.value)
|
||||
|
||||
|
||||
class TestEnsureLabelViaTea:
|
||||
def test_creates_new_label(self) -> None:
|
||||
mock_tea = MagicMock()
|
||||
mock_tea.list_labels.return_value = [{"name": "bug"}]
|
||||
result = _ensure_label_via_tea(mock_tea, "owner/repo", "ready-to-merge", "2ecc71", "desc")
|
||||
assert result is True
|
||||
mock_tea.create_label.assert_called_once()
|
||||
|
||||
def test_label_already_exists(self) -> None:
|
||||
mock_tea = MagicMock()
|
||||
mock_tea.list_labels.return_value = [{"name": "ready-to-merge"}]
|
||||
result = _ensure_label_via_tea(mock_tea, "owner/repo", "ready-to-merge", "2ecc71", "desc")
|
||||
assert result is False
|
||||
mock_tea.create_label.assert_not_called()
|
||||
|
||||
def test_tea_error_falls_back_to_client(self) -> None:
|
||||
mock_tea = MagicMock()
|
||||
mock_tea.list_labels.side_effect = TeaCLIError("network error")
|
||||
with patch("scripts.configure_repo._ensure_label_via_client", return_value=True) as mock_fallback:
|
||||
result = _ensure_label_via_tea(mock_tea, "owner/repo", "ready-to-merge", "2ecc71", "desc")
|
||||
assert result is True
|
||||
mock_fallback.assert_called_once_with("ready-to-merge", "2ecc71", "desc")
|
||||
|
||||
|
||||
class TestEnsureLabelViaClient:
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch("scripts.configure_repo.GiteaClient")
|
||||
def test_creates_label(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.ensure_label.return_value = {"id": 1}
|
||||
mock_client_cls.return_value = mock_client
|
||||
result = _ensure_label_via_client("bug", "ff0000", "A bug")
|
||||
assert result is True
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch("scripts.configure_repo.GiteaClient")
|
||||
def test_label_already_exists(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.ensure_label.return_value = None
|
||||
mock_client_cls.return_value = mock_client
|
||||
result = _ensure_label_via_client("bug", "ff0000", "A bug")
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestMain:
|
||||
def test_main_missing_token(self) -> None:
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
@@ -43,57 +90,90 @@ class TestMain:
|
||||
main()
|
||||
assert "REPO_TOKEN" in str(exc.value)
|
||||
|
||||
def test_main_success(self) -> None:
|
||||
with patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True):
|
||||
with patch("scripts.configure_repo.GiteaClient") as mock_client_cls:
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch("scripts.configure_repo.TeaCLI")
|
||||
@patch("scripts.configure_repo.GiteaClient")
|
||||
def test_main_success(self, mock_client_cls: MagicMock, mock_tea_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_tea = MagicMock()
|
||||
mock_tea.list_labels.return_value = [] # No existing labels
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
|
||||
main()
|
||||
main()
|
||||
|
||||
mock_client.ensure_branch_protection.assert_called_once_with("master", BRANCH_PROTECTION_CONFIG)
|
||||
mock_client.ensure_label.assert_called_once()
|
||||
mock_client.update_repo_settings.assert_called_once_with(REPO_SETTINGS_CONFIG)
|
||||
mock_client.ensure_branch_protection.assert_called_once_with("master", BRANCH_PROTECTION_CONFIG)
|
||||
mock_tea.create_label.assert_called_once()
|
||||
mock_client.update_repo_settings.assert_called_once_with(REPO_SETTINGS_CONFIG)
|
||||
|
||||
def test_main_label_already_exists(self) -> None:
|
||||
with patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True):
|
||||
with patch("scripts.configure_repo.GiteaClient") as mock_client_cls:
|
||||
mock_client = MagicMock()
|
||||
mock_client.ensure_label.return_value = None
|
||||
mock_client_cls.return_value = mock_client
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch("scripts.configure_repo.TeaCLI")
|
||||
@patch("scripts.configure_repo.GiteaClient")
|
||||
def test_main_label_already_exists(self, mock_client_cls: MagicMock, mock_tea_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_tea = MagicMock()
|
||||
mock_tea.list_labels.return_value = [{"name": "ready-to-merge"}]
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
|
||||
main()
|
||||
main()
|
||||
|
||||
mock_client.ensure_branch_protection.assert_called_once_with("master", BRANCH_PROTECTION_CONFIG)
|
||||
mock_client.ensure_label.assert_called_once()
|
||||
mock_client.update_repo_settings.assert_called_once_with(REPO_SETTINGS_CONFIG)
|
||||
mock_client.ensure_branch_protection.assert_called_once_with("master", BRANCH_PROTECTION_CONFIG)
|
||||
mock_tea.create_label.assert_not_called()
|
||||
mock_client.update_repo_settings.assert_called_once_with(REPO_SETTINGS_CONFIG)
|
||||
|
||||
def test_main_api_error(self) -> None:
|
||||
with patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True):
|
||||
with patch("scripts.configure_repo.GiteaClient") as mock_client_cls:
|
||||
mock_client = MagicMock()
|
||||
mock_client.ensure_branch_protection.side_effect = APIError(http.HTTPStatus.FORBIDDEN, "Forbidden")
|
||||
mock_client_cls.return_value = mock_client
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch("scripts.configure_repo.TeaCLI")
|
||||
@patch("scripts.configure_repo.GiteaClient")
|
||||
def test_main_tea_error_falls_back(self, mock_client_cls: MagicMock, mock_tea_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.ensure_label.return_value = {"id": 1}
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_tea = MagicMock()
|
||||
mock_tea.list_labels.side_effect = TeaCLIError("network error")
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
|
||||
with pytest.raises(click.ClickException) as exc:
|
||||
main()
|
||||
assert "HTTP" in str(exc.value)
|
||||
main()
|
||||
|
||||
mock_client.ensure_branch_protection.assert_called_once()
|
||||
# Fallback to GiteaClient for label creation
|
||||
mock_client.ensure_label.assert_called_once()
|
||||
mock_client.update_repo_settings.assert_called_once()
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch("scripts.configure_repo.TeaCLI")
|
||||
@patch("scripts.configure_repo.GiteaClient")
|
||||
def test_main_api_error(self, mock_client_cls: MagicMock, mock_tea_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.ensure_branch_protection.side_effect = APIError(http.HTTPStatus.FORBIDDEN, "Forbidden")
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_tea = MagicMock()
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
|
||||
with pytest.raises(click.ClickException) as exc:
|
||||
main()
|
||||
assert "HTTP" in str(exc.value)
|
||||
|
||||
|
||||
def test_main_module_block() -> None:
|
||||
with patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True):
|
||||
with patch("scripts.configure_repo.GiteaClient") as mock_client_cls:
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
import scripts.configure_repo as cr
|
||||
with patch("scripts.configure_repo.TeaCLI") as mock_tea_cls:
|
||||
with patch("scripts.configure_repo.GiteaClient") as mock_client_cls:
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_tea = MagicMock()
|
||||
mock_tea.list_labels.return_value = []
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
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["GiteaClient"] = mock_client_cls
|
||||
namespace["main"]()
|
||||
mock_client.ensure_branch_protection.assert_called_once_with("master", BRANCH_PROTECTION_CONFIG)
|
||||
mock_client.update_repo_settings.assert_called_once_with(REPO_SETTINGS_CONFIG)
|
||||
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["GiteaClient"] = mock_client_cls
|
||||
namespace["TeaCLI"] = mock_tea_cls
|
||||
namespace["main"]()
|
||||
mock_client.ensure_branch_protection.assert_called_once_with("master", BRANCH_PROTECTION_CONFIG)
|
||||
mock_client.update_repo_settings.assert_called_once_with(REPO_SETTINGS_CONFIG)
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
"""Unit tests for scripts/gitea_cli.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.gitea_cli import TeaCLI, TeaCLIError, _extract_issue_number, _extract_pr_number
|
||||
|
||||
|
||||
class TestExtractIssueNumber:
|
||||
def test_extract_from_created_issue(self) -> None:
|
||||
assert _extract_issue_number("Created issue #42: Bug title") == 42
|
||||
|
||||
def test_extract_no_hash(self) -> None:
|
||||
assert _extract_issue_number("No issue number here") == 0
|
||||
|
||||
def test_extract_multiple_hashes(self) -> None:
|
||||
assert _extract_issue_number("Issue #5 and PR #10") == 5
|
||||
|
||||
def test_extract_with_colon(self) -> None:
|
||||
assert _extract_issue_number("Created issue #7: title") == 7
|
||||
|
||||
def test_extract_invalid_number(self) -> None:
|
||||
assert _extract_issue_number("Issue #abc: title") == 0
|
||||
|
||||
|
||||
class TestExtractPrNumber:
|
||||
def test_extract_from_created_pr(self) -> None:
|
||||
assert _extract_pr_number("Created PR #128: Feature") == 128
|
||||
|
||||
def test_extract_no_number(self) -> None:
|
||||
assert _extract_pr_number("No PR number") == 0
|
||||
|
||||
|
||||
class TestTeaCLIInit:
|
||||
def test_auto_detect_tea(self) -> None:
|
||||
with patch("shutil.which", return_value="/usr/bin/tea"):
|
||||
cli = TeaCLI()
|
||||
assert cli._tea == "/usr/bin/tea"
|
||||
|
||||
def test_explicit_tea_bin(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/custom/tea")
|
||||
assert cli._tea == "/custom/tea"
|
||||
|
||||
def test_fallback_to_tea(self) -> None:
|
||||
with patch("shutil.which", return_value=None):
|
||||
cli = TeaCLI()
|
||||
assert cli._tea == "tea"
|
||||
|
||||
def test_with_repo(self) -> None:
|
||||
cli = TeaCLI(repo="owner/repo")
|
||||
assert cli._repo == "owner/repo"
|
||||
|
||||
|
||||
class TestTeaCLIRun:
|
||||
def test_run_success_json(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout='[{"id": 1}]', stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
output = cli._run(["labels", "list"])
|
||||
assert output == '[{"id": 1}]'
|
||||
|
||||
def test_run_success_raw(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="Created issue #42", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
output = cli._run_raw(["issues", "create"])
|
||||
assert output == "Created issue #42"
|
||||
|
||||
def test_run_failure_raises(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=1, stdout="", stderr="auth error")
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
with pytest.raises(TeaCLIError, match="auth error"):
|
||||
cli._run(["labels", "list"])
|
||||
|
||||
def test_run_includes_json_flag(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="[]", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||
cli._run(["labels", "list"])
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "--output" in cmd
|
||||
assert "json" in cmd
|
||||
|
||||
def test_run_raw_no_json_flag(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="ok", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||
cli._run_raw(["whoami"])
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "--output" not in cmd
|
||||
|
||||
|
||||
class TestRepoArg:
|
||||
def test_with_repo_arg(self) -> None:
|
||||
cli = TeaCLI(repo="owner/repo")
|
||||
assert cli._repo_arg() == ["--repo", "owner/repo"]
|
||||
|
||||
def test_with_explicit_repo(self) -> None:
|
||||
cli = TeaCLI()
|
||||
assert cli._repo_arg("other/repo") == ["--repo", "other/repo"]
|
||||
|
||||
def test_without_repo(self) -> None:
|
||||
cli = TeaCLI()
|
||||
assert cli._repo_arg() == []
|
||||
|
||||
def test_explicit_overrides_default(self) -> None:
|
||||
cli = TeaCLI(repo="default/repo")
|
||||
assert cli._repo_arg("override/repo") == ["--repo", "override/repo"]
|
||||
|
||||
|
||||
class TestCreateIssue:
|
||||
def test_create_issue_basic(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea", repo="owner/repo")
|
||||
mock_result = MagicMock(returncode=0, stdout="Created issue #42: Bug", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
issue = cli.create_issue("owner/repo", title="Bug", body="Description")
|
||||
assert issue["index"] == 42
|
||||
assert issue["title"] == "Bug"
|
||||
|
||||
def test_create_issue_with_labels(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="Created issue #5: Title", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
issue = cli.create_issue("owner/repo", title="Title", body="Body", labels=["bug"])
|
||||
assert issue["index"] == 5
|
||||
|
||||
|
||||
class TestListLabels:
|
||||
def test_list_labels_with_data(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
labels_json = json.dumps([{"id": 1, "name": "bug"}, {"id": 2, "name": "enhancement"}])
|
||||
mock_result = MagicMock(returncode=0, stdout=labels_json, stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
labels = cli.list_labels("owner/repo")
|
||||
assert len(labels) == 2
|
||||
assert labels[0]["name"] == "bug"
|
||||
|
||||
def test_list_labels_empty(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
labels = cli.list_labels("owner/repo")
|
||||
assert labels == []
|
||||
|
||||
|
||||
class TestCreateLabel:
|
||||
def test_create_label_full(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="Label created", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
label = cli.create_label("owner/repo", name="bug", color="ff0000", description="A bug")
|
||||
assert label["name"] == "bug"
|
||||
assert label["color"] == "ff0000"
|
||||
|
||||
def test_create_label_name_only(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="Label created", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
label = cli.create_label("owner/repo", name="wip")
|
||||
assert label["name"] == "wip"
|
||||
assert label["color"] == ""
|
||||
|
||||
|
||||
class TestAddLabel:
|
||||
def test_add_label_single(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="ok", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||
cli.add_label("owner/repo", 42, ["ready-to-merge"])
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "--add-labels" in cmd
|
||||
assert "ready-to-merge" in cmd
|
||||
assert "42" in cmd
|
||||
|
||||
def test_add_label_multiple(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="ok", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||
cli.add_label("owner/repo", 42, ["bug", "urgent"])
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "--add-labels" in cmd
|
||||
|
||||
def test_add_label_empty_list(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
with patch("subprocess.run") as mock_run:
|
||||
cli.add_label("owner/repo", 42, [])
|
||||
mock_run.assert_not_called()
|
||||
|
||||
|
||||
class TestCreatePR:
|
||||
def test_create_pr_basic(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="Created PR #128: Feature", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
pr = cli.create_pr("owner/repo", title="Feature", head="feature-branch", base="master")
|
||||
assert pr["index"] == 128
|
||||
|
||||
def test_create_pr_with_body(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="Created PR #10: Title", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||
cli.create_pr("owner/repo", title="Title", head="feat", base="master", body="Description")
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "--body" in cmd
|
||||
assert "Description" in cmd
|
||||
|
||||
|
||||
class TestMergePR:
|
||||
def test_merge_pr_squash(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="Merged", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||
cli.merge_pr("owner/repo", 42, style="squash")
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "--style" in cmd
|
||||
assert "squash" in cmd
|
||||
assert "42" in cmd
|
||||
|
||||
def test_merge_pr_default_style(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="Merged", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||
cli.merge_pr("owner/repo", 42)
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "squash" in cmd
|
||||
|
||||
|
||||
class TestReviewPR:
|
||||
def test_review_approve(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="Reviewed", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||
cli.review_pr("owner/repo", 42, event="APPROVE", body="LGTM")
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "--approve" in cmd
|
||||
assert "--comment" in cmd
|
||||
|
||||
def test_review_reject(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="Reviewed", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||
cli.review_pr("owner/repo", 42, event="REQUEST_CHANGES", body="Needs work")
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "--reject" in cmd
|
||||
|
||||
def test_review_comment(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="Reviewed", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||
cli.review_pr("owner/repo", 42, event="COMMENT", body="Note")
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "--approve" not in cmd
|
||||
assert "--reject" not in cmd
|
||||
assert "--comment" in cmd
|
||||
|
||||
def test_review_no_body(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="Reviewed", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||
cli.review_pr("owner/repo", 42, event="COMMENT")
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "--comment" not in cmd
|
||||
|
||||
|
||||
class TestCreateRelease:
|
||||
def test_create_release_full(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="Release created", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||
release = cli.create_release(
|
||||
"owner/repo",
|
||||
tag="v1.0.0",
|
||||
title="Release 1.0.0",
|
||||
body="Notes",
|
||||
target="master",
|
||||
)
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "v1.0.0" in cmd
|
||||
assert "--title" in cmd
|
||||
assert "--note" in cmd
|
||||
assert "--target" in cmd
|
||||
assert release["tag"] == "v1.0.0"
|
||||
|
||||
def test_create_release_draft(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="Release created", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||
cli.create_release("owner/repo", tag="v0.1.0", draft=True)
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "--draft" in cmd
|
||||
|
||||
def test_create_release_prerelease(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="Release created", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||
cli.create_release("owner/repo", tag="v0.1.0-rc1", prerelease=True)
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "--prerelease" in cmd
|
||||
|
||||
def test_create_release_minimal(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="Release created", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||
release = cli.create_release("owner/repo", tag="v1.0.0")
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "--title" not in cmd
|
||||
assert "--note" not in cmd
|
||||
assert release["tag"] == "v1.0.0"
|
||||
|
||||
|
||||
class TestListReleases:
|
||||
def test_list_releases_with_data(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
releases_json = json.dumps([{"tag": "v1.0.0"}, {"tag": "v0.9.0"}])
|
||||
mock_result = MagicMock(returncode=0, stdout=releases_json, stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
releases = cli.list_releases("owner/repo")
|
||||
assert len(releases) == 2
|
||||
|
||||
def test_list_releases_empty(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
releases = cli.list_releases("owner/repo")
|
||||
assert releases == []
|
||||
|
||||
|
||||
class TestListBranches:
|
||||
def test_list_branches_with_data(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
branches_json = json.dumps([{"name": "master"}, {"name": "develop"}])
|
||||
mock_result = MagicMock(returncode=0, stdout=branches_json, stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
branches = cli.list_branches("owner/repo")
|
||||
assert len(branches) == 2
|
||||
|
||||
def test_list_branches_empty(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
branches = cli.list_branches("owner/repo")
|
||||
assert branches == []
|
||||
|
||||
|
||||
class TestWhoami:
|
||||
def test_whoami(self) -> None:
|
||||
cli = TeaCLI(tea_bin="/fake/tea")
|
||||
mock_result = MagicMock(returncode=0, stdout="emil", stderr="")
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
assert cli.whoami() == "emil"
|
||||
@@ -204,6 +204,24 @@ class TestInstallActRunner:
|
||||
assert (tmp_path / "act_runner").exists()
|
||||
|
||||
|
||||
class TestInstallTea:
|
||||
def test_already_installed(self) -> None:
|
||||
with patch.object(install_tools, "_is_installed", return_value=True):
|
||||
assert install_tools.install_tea() is True
|
||||
|
||||
def test_install(self, tmp_path: Path) -> None:
|
||||
def _write_file(url: str, path: Path) -> tuple[str, None]:
|
||||
Path(path).write_bytes(b"binary")
|
||||
return str(path), None
|
||||
|
||||
with patch.object(install_tools, "_is_installed", return_value=False):
|
||||
with patch.object(install_tools, "TARGET_DIR", tmp_path):
|
||||
with patch.object(platform, "machine", return_value="x86_64"):
|
||||
with patch.object(install_tools, "_download", side_effect=_write_file):
|
||||
assert install_tools.install_tea() is True
|
||||
assert (tmp_path / "tea").exists()
|
||||
|
||||
|
||||
class TestListTools:
|
||||
def test_list(self, tmp_path: Path) -> None:
|
||||
with patch.object(install_tools, "TARGET_DIR", tmp_path):
|
||||
@@ -228,6 +246,11 @@ class TestInstallTool:
|
||||
assert install_tools._install_tool("act_runner") is True
|
||||
mock.assert_called_once()
|
||||
|
||||
def test_tea(self) -> None:
|
||||
with patch.object(install_tools, "install_tea", return_value=True) as mock:
|
||||
assert install_tools._install_tool("tea") is True
|
||||
mock.assert_called_once()
|
||||
|
||||
def test_unknown_tool(self) -> None:
|
||||
with pytest.raises(ClickException, match="Unknown tool"):
|
||||
install_tools._install_tool("unknown")
|
||||
@@ -246,7 +269,7 @@ class TestMain:
|
||||
with patch.object(install_tools, "_install_tool", return_value=True) as mock_install:
|
||||
result = runner.invoke(install_tools.main, [])
|
||||
assert result.exit_code == 0
|
||||
assert mock_install.call_count == 3
|
||||
assert mock_install.call_count == 4
|
||||
|
||||
def test_install_specific_tool(self) -> None:
|
||||
runner = CliRunner()
|
||||
@@ -255,6 +278,13 @@ class TestMain:
|
||||
assert result.exit_code == 0
|
||||
mock_install.assert_called_once_with("actionlint")
|
||||
|
||||
def test_install_multiple_specific_tools(self) -> None:
|
||||
runner = CliRunner()
|
||||
with patch.object(install_tools, "_install_tool", return_value=True) as mock_install:
|
||||
result = runner.invoke(install_tools.main, ["--tool", "git-cliff", "--tool", "tea"])
|
||||
assert result.exit_code == 0
|
||||
assert mock_install.call_count == 2
|
||||
|
||||
def test_install_failure(self) -> None:
|
||||
runner = CliRunner()
|
||||
with patch.object(install_tools, "_install_tool", side_effect=Exception("network error")):
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
"""Unit tests for scripts/ci/notify_failure.py."""
|
||||
|
||||
import http
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from gitea_runner_manager.exceptions import APIError
|
||||
from scripts.ci.notify_failure import main
|
||||
from scripts.gitea_cli import TeaCLIError
|
||||
|
||||
|
||||
class TestNotifyFailure:
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.notify_failure.GiteaClient")
|
||||
def test_creates_issue_with_labels(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.list_labels.return_value = [{"id": 5, "name": "bug"}]
|
||||
mock_client.create_issue.return_value = {"id": 42}
|
||||
mock_client_cls.return_value = mock_client
|
||||
@patch("scripts.ci.notify_failure.shutil.which", return_value="/usr/bin/tea")
|
||||
@patch("scripts.gitea_cli.TeaCLI")
|
||||
def test_creates_issue_with_tea(self, mock_tea_cls: MagicMock, mock_which: MagicMock) -> None:
|
||||
mock_tea = MagicMock()
|
||||
mock_tea.list_labels.return_value = [{"id": 5, "name": "bug"}]
|
||||
mock_tea.create_issue.return_value = {"index": 42, "title": "test"}
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
@@ -33,64 +36,127 @@ class TestNotifyFailure:
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "issue #42" in result.output
|
||||
mock_client.create_issue.assert_called_once()
|
||||
call_kwargs = mock_client.create_issue.call_args
|
||||
assert call_kwargs.kwargs["labels"] == [5]
|
||||
mock_tea.create_issue.assert_called_once()
|
||||
mock_tea.add_label.assert_called_once_with("owner/repo", 42, ["bug"])
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.notify_failure.GiteaClient")
|
||||
def test_creates_issue_without_bug_label(self, mock_client_cls: MagicMock) -> None:
|
||||
"""When 'bug' label doesn't exist, create issue without labels."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.list_labels.return_value = [{"id": 1, "name": "enhancement"}]
|
||||
mock_client.create_issue.return_value = {"id": 43}
|
||||
mock_client_cls.return_value = mock_client
|
||||
@patch("scripts.ci.notify_failure.shutil.which", return_value="/usr/bin/tea")
|
||||
@patch("scripts.gitea_cli.TeaCLI")
|
||||
def test_tea_creates_issue_without_bug_label(self, mock_tea_cls: MagicMock, mock_which: MagicMock) -> None:
|
||||
mock_tea = MagicMock()
|
||||
mock_tea.list_labels.return_value = [{"id": 1, "name": "enhancement"}]
|
||||
mock_tea.create_issue.return_value = {"index": 43, "title": "test"}
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
[
|
||||
"--repo",
|
||||
"owner/repo",
|
||||
"--run-id",
|
||||
"124",
|
||||
"--workflow",
|
||||
"publish",
|
||||
"--commit",
|
||||
"def789",
|
||||
],
|
||||
["--repo", "owner/repo", "--run-id", "124", "--workflow", "publish", "--commit", "def789"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "issue #43" in result.output
|
||||
mock_client.create_issue.assert_called_once()
|
||||
call_kwargs = mock_client.create_issue.call_args
|
||||
assert call_kwargs.kwargs.get("labels") is None
|
||||
mock_tea.add_label.assert_not_called()
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.notify_failure.shutil.which", return_value="/usr/bin/tea")
|
||||
@patch("scripts.gitea_cli.TeaCLI")
|
||||
def test_tea_error_falls_back_to_client(self, mock_tea_cls: MagicMock, mock_which: MagicMock) -> None:
|
||||
"""When tea fails, fall back to GiteaClient."""
|
||||
mock_tea = MagicMock()
|
||||
mock_tea.list_labels.side_effect = TeaCLIError("network error")
|
||||
mock_tea.create_issue.side_effect = TeaCLIError("network error")
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
|
||||
with patch("scripts.ci.notify_failure.GiteaClient") as mock_client_cls:
|
||||
mock_client = MagicMock()
|
||||
mock_client.list_labels.return_value = [{"id": 5, "name": "bug"}]
|
||||
mock_client.create_issue.return_value = {"id": 50}
|
||||
mock_client_cls.return_value = mock_client
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["--repo", "owner/repo", "--run-id", "125", "--workflow", "release", "--commit", "abc"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "issue #50" in result.output
|
||||
mock_client.create_issue.assert_called_once()
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.notify_failure.shutil.which", return_value=None)
|
||||
@patch("scripts.ci.notify_failure.GiteaClient")
|
||||
def test_api_error_raises(self, mock_client_cls: MagicMock) -> None:
|
||||
def test_tea_not_installed_uses_client(self, mock_client_cls: MagicMock, mock_which: MagicMock) -> None:
|
||||
"""When tea is not installed, use GiteaClient directly."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.list_labels.return_value = []
|
||||
mock_client.create_issue.side_effect = APIError(403, "forbidden")
|
||||
mock_client.list_labels.return_value = [{"id": 5, "name": "bug"}]
|
||||
mock_client.create_issue.return_value = {"id": 51}
|
||||
mock_client_cls.return_value = mock_client
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
[
|
||||
"--repo",
|
||||
"owner/repo",
|
||||
"--run-id",
|
||||
"125",
|
||||
"--workflow",
|
||||
"release",
|
||||
"--commit",
|
||||
"abc",
|
||||
],
|
||||
["--repo", "owner/repo", "--run-id", "126", "--workflow", "release", "--commit", "abc"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "issue #51" in result.output
|
||||
mock_client.create_issue.assert_called_once()
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.notify_failure.shutil.which", return_value=None)
|
||||
@patch("scripts.ci.notify_failure.GiteaClient")
|
||||
def test_client_api_error_raises(self, mock_client_cls: MagicMock, mock_which: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.list_labels.return_value = []
|
||||
mock_client.create_issue.side_effect = APIError(http.HTTPStatus.FORBIDDEN, "forbidden")
|
||||
mock_client_cls.return_value = mock_client
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["--repo", "owner/repo", "--run-id", "127", "--workflow", "release", "--commit", "abc"],
|
||||
)
|
||||
assert result.exit_code == 1
|
||||
assert "403" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.notify_failure.shutil.which", return_value="/usr/bin/tea")
|
||||
@patch("scripts.gitea_cli.TeaCLI")
|
||||
def test_tea_list_labels_error_continues_without_labels(
|
||||
self, mock_tea_cls: MagicMock, mock_which: MagicMock
|
||||
) -> None:
|
||||
"""If listing labels fails via tea, issue is still created without labels."""
|
||||
mock_tea = MagicMock()
|
||||
mock_tea.list_labels.side_effect = TeaCLIError("network error")
|
||||
mock_tea.create_issue.return_value = {"index": 50, "title": "test"}
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["--repo", "owner/repo", "--run-id", "128", "--workflow", "release", "--commit", "abc"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "issue #50" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.notify_failure.shutil.which", return_value="/usr/bin/tea")
|
||||
@patch("scripts.gitea_cli.TeaCLI")
|
||||
def test_tea_add_label_error_is_ignored(self, mock_tea_cls: MagicMock, mock_which: MagicMock) -> None:
|
||||
"""If adding label fails via tea, issue is still reported as created."""
|
||||
mock_tea = MagicMock()
|
||||
mock_tea.list_labels.return_value = [{"id": 5, "name": "bug"}]
|
||||
mock_tea.create_issue.return_value = {"index": 51, "title": "test"}
|
||||
mock_tea.add_label.side_effect = TeaCLIError("permission denied")
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["--repo", "owner/repo", "--run-id", "129", "--workflow", "release", "--commit", "abc"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "issue #51" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": ""}, clear=True)
|
||||
def test_missing_token_exits(self) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
+23
-42
@@ -1,6 +1,5 @@
|
||||
"""Unit tests for scripts/ci/publish.py."""
|
||||
|
||||
import http
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import click
|
||||
@@ -13,6 +12,7 @@ from scripts.ci.publish import (
|
||||
main,
|
||||
publish_to_pypi,
|
||||
)
|
||||
from scripts.gitea_cli import TeaCLIError
|
||||
|
||||
|
||||
class TestGenerateReleaseNotes:
|
||||
@@ -97,42 +97,45 @@ class TestPublishToPypi:
|
||||
class TestMain:
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
|
||||
@patch("scripts.ci.publish.generate_release_notes", return_value="Release notes")
|
||||
@patch("scripts.ci.publish.GiteaClient")
|
||||
@patch("scripts.ci.publish.TeaCLI")
|
||||
@patch("scripts.ci.publish.publish_to_pypi")
|
||||
@patch("scripts.ci.publish.build_package")
|
||||
def test_full_flow_with_pypi(
|
||||
self,
|
||||
mock_build: MagicMock,
|
||||
mock_publish: MagicMock,
|
||||
mock_client_cls: MagicMock,
|
||||
mock_tea_cls: MagicMock,
|
||||
mock_notes: MagicMock,
|
||||
) -> None:
|
||||
mock_tea = MagicMock()
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
||||
assert result.exit_code == 0
|
||||
assert "Gitea release v1.0.0 created" in result.output
|
||||
mock_build.assert_called_once()
|
||||
mock_publish.assert_called_once_with("pypi-tok")
|
||||
mock_client_cls.return_value.create_release_idempotent.assert_called_once()
|
||||
# Verify release body uses git-cliff notes
|
||||
call_args = mock_client_cls.return_value.create_release_idempotent.call_args
|
||||
assert call_args.kwargs["body"] == "Release notes"
|
||||
mock_tea.create_release.assert_called_once_with(
|
||||
"owner/repo", tag="v1.0.0", title="v1.0.0", body="Release notes"
|
||||
)
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok"}, clear=True)
|
||||
@patch("scripts.ci.publish.generate_release_notes", return_value="Release notes")
|
||||
@patch("scripts.ci.publish.GiteaClient")
|
||||
@patch("scripts.ci.publish.TeaCLI")
|
||||
@patch("scripts.ci.publish.build_package")
|
||||
def test_without_pypi(
|
||||
self,
|
||||
mock_build: MagicMock,
|
||||
mock_client_cls: MagicMock,
|
||||
mock_tea_cls: MagicMock,
|
||||
mock_notes: MagicMock,
|
||||
) -> None:
|
||||
mock_tea = MagicMock()
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
||||
assert result.exit_code == 0
|
||||
mock_build.assert_called_once()
|
||||
mock_client_cls.return_value.create_release_idempotent.assert_called_once()
|
||||
mock_tea.create_release.assert_called_once()
|
||||
assert "PYPI_TOKEN not set" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": ""}, clear=True)
|
||||
@@ -144,11 +147,11 @@ class TestMain:
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
|
||||
@patch("scripts.ci.publish.generate_release_notes", return_value="Release notes")
|
||||
@patch("scripts.ci.publish.GiteaClient")
|
||||
@patch("scripts.ci.publish.TeaCLI")
|
||||
@patch("scripts.ci.publish.publish_to_pypi")
|
||||
@patch("scripts.ci.publish.build_package")
|
||||
def test_build_failure_raises_click(
|
||||
self, mock_build: MagicMock, mock_publish: MagicMock, mock_client_cls: MagicMock, mock_notes: MagicMock
|
||||
self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock
|
||||
) -> None:
|
||||
mock_build.side_effect = click.ClickException("build failed")
|
||||
runner = CliRunner()
|
||||
@@ -158,11 +161,11 @@ class TestMain:
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
|
||||
@patch("scripts.ci.publish.generate_release_notes", return_value="Release notes")
|
||||
@patch("scripts.ci.publish.GiteaClient")
|
||||
@patch("scripts.ci.publish.TeaCLI")
|
||||
@patch("scripts.ci.publish.publish_to_pypi")
|
||||
@patch("scripts.ci.publish.build_package")
|
||||
def test_publish_failure_raises_click(
|
||||
self, mock_build: MagicMock, mock_publish: MagicMock, mock_client_cls: MagicMock, mock_notes: MagicMock
|
||||
self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock
|
||||
) -> None:
|
||||
mock_publish.side_effect = click.ClickException("publish failed")
|
||||
runner = CliRunner()
|
||||
@@ -172,38 +175,16 @@ class TestMain:
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
|
||||
@patch("scripts.ci.publish.generate_release_notes", return_value="Release notes")
|
||||
@patch("scripts.ci.publish.GiteaClient")
|
||||
@patch("scripts.ci.publish.TeaCLI")
|
||||
@patch("scripts.ci.publish.publish_to_pypi")
|
||||
@patch("scripts.ci.publish.build_package")
|
||||
def test_release_failure_raises_click(
|
||||
self, mock_build: MagicMock, mock_publish: MagicMock, mock_client_cls: MagicMock, mock_notes: MagicMock
|
||||
self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock
|
||||
) -> None:
|
||||
mock_client = MagicMock()
|
||||
from gitea_runner_manager.exceptions import APIError
|
||||
|
||||
mock_client.create_release_idempotent.side_effect = APIError(
|
||||
http.HTTPStatus.INTERNAL_SERVER_ERROR, "server error"
|
||||
)
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_tea = MagicMock()
|
||||
mock_tea.create_release.side_effect = TeaCLIError("server error")
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
||||
assert result.exit_code == 1
|
||||
assert "HTTP" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
|
||||
@patch("scripts.ci.publish.generate_release_notes", return_value="Release notes")
|
||||
@patch("scripts.ci.publish.GiteaClient")
|
||||
@patch("scripts.ci.publish.publish_to_pypi")
|
||||
@patch("scripts.ci.publish.build_package")
|
||||
def test_release_json_parse_failure(
|
||||
self, mock_build: MagicMock, mock_publish: MagicMock, mock_client_cls: MagicMock, mock_notes: MagicMock
|
||||
) -> None:
|
||||
mock_client = MagicMock()
|
||||
from gitea_runner_manager.exceptions import APIError
|
||||
|
||||
mock_client.create_release_idempotent.side_effect = APIError(http.HTTPStatus.BAD_GATEWAY, "bad gateway")
|
||||
mock_client_cls.return_value = mock_client
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
||||
assert result.exit_code == 1
|
||||
assert str(http.HTTPStatus.BAD_GATEWAY) in result.output
|
||||
assert "Release creation failed" in result.output
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Unit tests for scripts/ci/review_pr.py."""
|
||||
|
||||
import http
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -8,8 +7,8 @@ import click
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from gitea_runner_manager.exceptions import APIError
|
||||
from scripts.ci.review_pr import main, parse_comments
|
||||
from scripts.gitea_cli import TeaCLIError
|
||||
|
||||
|
||||
class TestParseComments:
|
||||
@@ -60,27 +59,25 @@ class TestParseComments:
|
||||
|
||||
class TestMain:
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.review_pr.GiteaClient")
|
||||
def test_successful_comment_review(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.create_review.return_value = {"id": 42}
|
||||
mock_client_cls.return_value = mock_client
|
||||
@patch("scripts.ci.review_pr.TeaCLI")
|
||||
def test_successful_comment_review(self, mock_tea_cls: MagicMock) -> None:
|
||||
mock_tea = MagicMock()
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["5", "owner/repo", "--event", "COMMENT", "--body", "LGTM"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "Review #42" in result.output
|
||||
mock_client.create_review.assert_called_once_with("5", event="COMMENT", body="LGTM", comments=[])
|
||||
assert "Review posted" in result.output
|
||||
mock_tea.review_pr.assert_called_once_with("owner/repo", 5, event="COMMENT", body="LGTM")
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.review_pr.GiteaClient")
|
||||
def test_successful_approve_review(self, mock_client_cls: MagicMock) -> None:
|
||||
@patch("scripts.ci.review_pr.TeaCLI")
|
||||
def test_successful_approve_review(self, mock_tea_cls: MagicMock) -> None:
|
||||
"""APPROVE requires --checklist-confirmed and substantive body."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.create_review.return_value = {"id": 7}
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_tea = MagicMock()
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
@@ -95,20 +92,19 @@ class TestMain:
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "Review #7" in result.output
|
||||
mock_client.create_review.assert_called_once_with(
|
||||
"5",
|
||||
mock_tea.review_pr.assert_called_once_with(
|
||||
"owner/repo",
|
||||
5,
|
||||
event="APPROVE",
|
||||
body="All 10 checklist categories verified. Architecture OK, tests pass.",
|
||||
comments=[],
|
||||
)
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.review_pr.GiteaClient")
|
||||
def test_approve_without_checklist_confirmed_fails(self, mock_client_cls: MagicMock) -> None:
|
||||
@patch("scripts.ci.review_pr.TeaCLI")
|
||||
def test_approve_without_checklist_confirmed_fails(self, mock_tea_cls: MagicMock) -> None:
|
||||
"""APPROVE without --checklist-confirmed is rejected."""
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_tea = MagicMock()
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
@@ -116,14 +112,14 @@ class TestMain:
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "checklist" in result.output.lower()
|
||||
mock_client.create_review.assert_not_called()
|
||||
mock_tea.review_pr.assert_not_called()
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.review_pr.GiteaClient")
|
||||
def test_approve_with_trivial_body_fails(self, mock_client_cls: MagicMock) -> None:
|
||||
@patch("scripts.ci.review_pr.TeaCLI")
|
||||
def test_approve_with_trivial_body_fails(self, mock_tea_cls: MagicMock) -> None:
|
||||
"""APPROVE with trivial body (< 20 chars) and no comments is rejected."""
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_tea = MagicMock()
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
@@ -131,32 +127,30 @@ class TestMain:
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "substantive" in result.output.lower()
|
||||
mock_client.create_review.assert_not_called()
|
||||
mock_tea.review_pr.assert_not_called()
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.review_pr.GiteaClient")
|
||||
def test_successful_with_inline_comments(self, mock_client_cls: MagicMock, tmp_path) -> None:
|
||||
@patch("scripts.ci.review_pr.TeaCLI")
|
||||
def test_successful_with_inline_comments(self, mock_tea_cls: MagicMock, tmp_path) -> None:
|
||||
comments = [{"path": "a.py", "body": "fix", "new_position": 1}]
|
||||
f = tmp_path / "comments.json"
|
||||
f.write_text(json.dumps(comments))
|
||||
mock_client = MagicMock()
|
||||
mock_client.create_review.return_value = {"id": 9}
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_tea = MagicMock()
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["5", "owner/repo", "--comments-json", str(f)],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
mock_client.create_review.assert_called_once_with("5", event="COMMENT", body="", comments=comments)
|
||||
mock_tea.review_pr.assert_called_once_with("owner/repo", 5, event="COMMENT", body="")
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.review_pr.GiteaClient")
|
||||
def test_successful_with_stdin_comments(self, mock_client_cls: MagicMock) -> None:
|
||||
@patch("scripts.ci.review_pr.TeaCLI")
|
||||
def test_successful_with_stdin_comments(self, mock_tea_cls: MagicMock) -> None:
|
||||
comments = [{"path": "a.py", "body": "fix", "new_position": 1}]
|
||||
mock_client = MagicMock()
|
||||
mock_client.create_review.return_value = {"id": 11}
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_tea = MagicMock()
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
@@ -164,7 +158,7 @@ class TestMain:
|
||||
input=json.dumps(comments),
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
mock_client.create_review.assert_called_once_with("5", event="COMMENT", body="", comments=comments)
|
||||
mock_tea.review_pr.assert_called_once_with("owner/repo", 5, event="COMMENT", body="")
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": ""}, clear=True)
|
||||
def test_missing_token_exits(self) -> None:
|
||||
@@ -174,44 +168,44 @@ class TestMain:
|
||||
assert "REPO_TOKEN" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.review_pr.GiteaClient")
|
||||
def test_no_body_or_comments_for_comment_event(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
@patch("scripts.ci.review_pr.TeaCLI")
|
||||
def test_no_body_or_comments_for_comment_event(self, mock_tea_cls: MagicMock) -> None:
|
||||
mock_tea = MagicMock()
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["5", "owner/repo", "--event", "COMMENT"])
|
||||
assert result.exit_code == 1
|
||||
assert "required" in result.output
|
||||
mock_client.create_review.assert_not_called()
|
||||
mock_tea.review_pr.assert_not_called()
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.review_pr.GiteaClient")
|
||||
def test_no_body_or_comments_for_request_changes(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
@patch("scripts.ci.review_pr.TeaCLI")
|
||||
def test_no_body_or_comments_for_request_changes(self, mock_tea_cls: MagicMock) -> None:
|
||||
mock_tea = MagicMock()
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["5", "owner/repo", "--event", "REQUEST_CHANGES"])
|
||||
assert result.exit_code == 1
|
||||
assert "required" in result.output
|
||||
mock_client.create_review.assert_not_called()
|
||||
mock_tea.review_pr.assert_not_called()
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.review_pr.GiteaClient")
|
||||
def test_api_error_raises_click(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.create_review.side_effect = APIError(http.HTTPStatus.INTERNAL_SERVER_ERROR, "server error")
|
||||
mock_client_cls.return_value = mock_client
|
||||
@patch("scripts.ci.review_pr.TeaCLI")
|
||||
def test_tea_error_raises_click(self, mock_tea_cls: MagicMock) -> None:
|
||||
mock_tea = MagicMock()
|
||||
mock_tea.review_pr.side_effect = TeaCLIError("tea command failed")
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["5", "owner/repo", "--body", "x"])
|
||||
assert result.exit_code == 1
|
||||
assert "HTTP" in result.output
|
||||
assert "Failed to post review" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.ci.review_pr.GiteaClient")
|
||||
def test_invalid_event_choice(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
@patch("scripts.ci.review_pr.TeaCLI")
|
||||
def test_invalid_event_choice(self, mock_tea_cls: MagicMock) -> None:
|
||||
mock_tea = MagicMock()
|
||||
mock_tea_cls.return_value = mock_tea
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["5", "owner/repo", "--event", "Bogus"])
|
||||
assert result.exit_code != 0
|
||||
mock_client.create_review.assert_not_called()
|
||||
mock_tea.review_pr.assert_not_called()
|
||||
|
||||
@@ -80,6 +80,56 @@ class TestVerify:
|
||||
setup._verify(".venv/bin")
|
||||
|
||||
|
||||
class TestConfigureTeaLogin:
|
||||
def test_tea_not_installed(self) -> None:
|
||||
with patch("shutil.which", return_value=None):
|
||||
with patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True):
|
||||
setup._configure_tea_login()
|
||||
|
||||
def test_no_repo_token(self) -> None:
|
||||
with patch("shutil.which", return_value="/usr/bin/tea"):
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
setup._configure_tea_login()
|
||||
|
||||
def test_login_already_exists(self) -> None:
|
||||
import subprocess
|
||||
|
||||
mock_result = subprocess.CompletedProcess(
|
||||
args=["tea", "login", "list"], returncode=0, stdout="grm https://git.example.com", stderr=""
|
||||
)
|
||||
with patch("shutil.which", return_value="/usr/bin/tea"):
|
||||
with patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True):
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
setup._configure_tea_login()
|
||||
|
||||
def test_login_added_successfully(self) -> None:
|
||||
import subprocess
|
||||
|
||||
list_result = subprocess.CompletedProcess(args=["tea", "login", "list"], returncode=0, stdout="", stderr="")
|
||||
add_result = subprocess.CompletedProcess(
|
||||
args=["tea", "login", "add"], returncode=0, stdout="Login added", stderr=""
|
||||
)
|
||||
default_result = subprocess.CompletedProcess(
|
||||
args=["tea", "login", "default"], returncode=0, stdout="", stderr=""
|
||||
)
|
||||
with patch("shutil.which", return_value="/usr/bin/tea"):
|
||||
with patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True):
|
||||
with patch("subprocess.run", side_effect=[list_result, add_result, default_result]):
|
||||
setup._configure_tea_login()
|
||||
|
||||
def test_login_add_failure(self) -> None:
|
||||
import subprocess
|
||||
|
||||
list_result = subprocess.CompletedProcess(args=["tea", "login", "list"], returncode=0, stdout="", stderr="")
|
||||
add_result = subprocess.CompletedProcess(
|
||||
args=["tea", "login", "add"], returncode=1, stdout="", stderr="auth failed"
|
||||
)
|
||||
with patch("shutil.which", return_value="/usr/bin/tea"):
|
||||
with patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True):
|
||||
with patch("subprocess.run", side_effect=[list_result, add_result]):
|
||||
setup._configure_tea_login()
|
||||
|
||||
|
||||
class TestMain:
|
||||
def test_bin_not_found(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
@@ -101,7 +151,8 @@ class TestMain:
|
||||
with patch("scripts.setup._install_python_deps"):
|
||||
with patch("scripts.setup._install_ansible_collections"):
|
||||
with patch("scripts.setup._install_pre_commit_hooks"):
|
||||
with patch("scripts.setup._verify"):
|
||||
result = runner.invoke(setup.main, ["--bin", str(bin_dir)])
|
||||
with patch("scripts.setup._configure_tea_login"):
|
||||
with patch("scripts.setup._verify"):
|
||||
result = runner.invoke(setup.main, ["--bin", str(bin_dir)])
|
||||
assert result.exit_code == 0
|
||||
assert "Setup complete" in result.output
|
||||
|
||||
Reference in New Issue
Block a user