Files
devx/tests/unit/test_gitea_cli.py
T
emil a9fd1a47af
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / vikunja (push) Successful in 11s
Post-merge / sync-wiki (push) Successful in 18s
Build Images / detect-type (push) Successful in 33s
Post-merge / configure-repo (push) Successful in 10s
Post-merge / badges (push) Successful in 27s
Post-merge / release (push) Successful in 32s
Post-merge / publish (push) Successful in 17s
Build Images / build-and-push (push) Successful in 3m6s
Build Images / cleanup (push) Successful in 54s
DEVX-76: feat: add --auto-login to publish, extract configure_tea_login to gitea_cli
2026-06-27 13:04:06 +00:00

396 lines
16 KiB
Python

"""Unit tests for scripts/gitea_cli.py."""
from __future__ import annotations
import json
from unittest.mock import MagicMock, patch
import pytest
from devx.gitea_cli import TeaCLI, TeaCLIError, _extract_issue_number, _extract_pr_number, configure_tea_login
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_tea_not_found_raises_tea_error(self) -> None:
cli = TeaCLI(tea_bin="tea")
with patch("subprocess.run", side_effect=FileNotFoundError("tea not found")):
with pytest.raises(TeaCLIError, match="tea binary not found"):
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="testuser", stderr="")
with patch("subprocess.run", return_value=mock_result):
assert cli.whoami() == "testuser"
class TestConfigureTeaLogin:
@patch.dict("os.environ", {"REPO_TOKEN": ""}, clear=True)
@patch("devx.gitea_cli.shutil.which", return_value="/usr/bin/tea")
def test_no_token_skips(self, mock_which: MagicMock) -> None:
"""configure_tea_login with no token prints skip message and returns."""
configure_tea_login()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch("devx.gitea_cli.shutil.which", return_value=None)
def test_no_tea_skips(self, mock_which: MagicMock) -> None:
"""configure_tea_login with no tea binary prints skip message and returns."""
configure_tea_login()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch("devx.gitea_cli.shutil.which", return_value="/usr/bin/tea")
@patch("devx.gitea_cli.subprocess.run")
def test_configures_login_when_not_present(self, mock_subprocess: MagicMock, mock_which: MagicMock) -> None:
"""configure_tea_login adds login when not already configured."""
mock_list = MagicMock(returncode=0, stdout="")
mock_subprocess.return_value = mock_list
configure_tea_login()
assert mock_subprocess.call_count >= 2 # login list + login add + login default
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch("devx.gitea_cli.shutil.which", return_value="/usr/bin/tea")
@patch("devx.gitea_cli.subprocess.run")
def test_skips_when_already_configured(self, mock_subprocess: MagicMock, mock_which: MagicMock) -> None:
"""configure_tea_login skips if login already exists."""
mock_list = MagicMock(returncode=0, stdout="devx https://git.example.com")
mock_subprocess.return_value = mock_list
configure_tea_login()
assert mock_subprocess.call_count == 1 # only login list, no add