Public Access
314 lines
14 KiB
Python
314 lines
14 KiB
Python
"""Unit tests for devx.ci.check_auto_merge_ready."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from click.testing import CliRunner
|
|
|
|
from devx.ci.check_auto_merge_ready import (
|
|
cli,
|
|
get_pr_title_from_gitea,
|
|
get_vikunja_title_optional,
|
|
is_branch_behind_master,
|
|
)
|
|
|
|
|
|
class TestIsBranchBehindMaster:
|
|
@patch("devx.ci.check_auto_merge_ready.subprocess.run")
|
|
def test_returns_false_when_ahead(self, mock_run: MagicMock) -> None:
|
|
# First: fetch (ok), second: ahead count (ok), third: behind count = 0
|
|
mock_run.side_effect = [
|
|
MagicMock(returncode=0, stdout="", stderr=""),
|
|
MagicMock(returncode=0, stdout="3\n", stderr=""),
|
|
MagicMock(returncode=0, stdout="0\n", stderr=""),
|
|
]
|
|
assert is_branch_behind_master("feature") is False
|
|
|
|
@patch("devx.ci.check_auto_merge_ready.subprocess.run")
|
|
def test_returns_true_when_behind(self, mock_run: MagicMock) -> None:
|
|
mock_run.side_effect = [
|
|
MagicMock(returncode=0, stdout="", stderr=""),
|
|
MagicMock(returncode=0, stdout="0\n", stderr=""),
|
|
MagicMock(returncode=0, stdout="5\n", stderr=""),
|
|
]
|
|
assert is_branch_behind_master("feature") is True
|
|
|
|
@patch("devx.ci.check_auto_merge_ready.subprocess.run")
|
|
def test_returns_false_on_git_error(self, mock_run: MagicMock) -> None:
|
|
mock_run.side_effect = [
|
|
MagicMock(returncode=0, stdout="", stderr=""),
|
|
MagicMock(returncode=1, stdout="", stderr="error"),
|
|
]
|
|
assert is_branch_behind_master("feature") is False
|
|
|
|
@patch("devx.ci.check_auto_merge_ready.subprocess.run")
|
|
def test_returns_false_on_timeout(self, mock_run: MagicMock) -> None:
|
|
import subprocess
|
|
|
|
mock_run.side_effect = subprocess.TimeoutExpired(cmd="git", timeout=30)
|
|
assert is_branch_behind_master("feature") is False
|
|
|
|
@patch("devx.ci.check_auto_merge_ready.subprocess.run")
|
|
def test_returns_false_on_value_error(self, mock_run: MagicMock) -> None:
|
|
mock_run.side_effect = [
|
|
MagicMock(returncode=0, stdout="", stderr=""),
|
|
MagicMock(returncode=0, stdout="3\n", stderr=""),
|
|
MagicMock(returncode=0, stdout="not_a_number\n", stderr=""),
|
|
]
|
|
assert is_branch_behind_master("feature") is False
|
|
|
|
@patch("devx.ci.check_auto_merge_ready.subprocess.run")
|
|
def test_returns_false_on_file_not_found(self, mock_run: MagicMock) -> None:
|
|
mock_run.side_effect = FileNotFoundError("git not found")
|
|
assert is_branch_behind_master("feature") is False
|
|
|
|
@patch("devx.ci.check_auto_merge_ready.subprocess.run")
|
|
def test_returns_false_when_behind_check_fails(self, mock_run: MagicMock) -> None:
|
|
# fetch ok, ahead count ok, behind count command fails
|
|
mock_run.side_effect = [
|
|
MagicMock(returncode=0, stdout="", stderr=""),
|
|
MagicMock(returncode=0, stdout="3\n", stderr=""),
|
|
MagicMock(returncode=1, stdout="", stderr="error"),
|
|
]
|
|
assert is_branch_behind_master("feature") is False
|
|
|
|
|
|
class TestGetPrTitleFromGitea:
|
|
def test_returns_none_without_token(self) -> None:
|
|
with patch.dict("os.environ", {}, clear=True):
|
|
assert get_pr_title_from_gitea("owner/repo", 1) is None
|
|
|
|
def test_returns_none_with_invalid_repo(self) -> None:
|
|
with patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True):
|
|
assert get_pr_title_from_gitea("invalid", 1) is None
|
|
|
|
@patch("devx.ci.check_auto_merge_ready.GiteaClient")
|
|
def test_fetches_title(self, mock_client_cls: MagicMock) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.get_pr.return_value = {"title": "DEVX-1: Fix bug"}
|
|
mock_client_cls.return_value = mock_client
|
|
with patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True):
|
|
result = get_pr_title_from_gitea("owner/repo", 1)
|
|
assert result == "DEVX-1: Fix bug"
|
|
|
|
@patch("devx.ci.check_auto_merge_ready.GiteaClient")
|
|
def test_returns_none_on_exception(self, mock_client_cls: MagicMock) -> None:
|
|
from devx.exceptions import APIError
|
|
|
|
mock_client = MagicMock()
|
|
mock_client.get_pr.side_effect = APIError(500, "API error")
|
|
mock_client_cls.return_value = mock_client
|
|
with patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True):
|
|
result = get_pr_title_from_gitea("owner/repo", 1)
|
|
assert result is None
|
|
|
|
|
|
class TestGetVikunjaTitleOptional:
|
|
def test_returns_none_without_token(self) -> None:
|
|
with patch.dict("os.environ", {}, clear=True):
|
|
assert get_vikunja_title_optional("DEVX-1") is None
|
|
|
|
@patch("devx.ci.check_auto_merge_ready.VikunjaClient")
|
|
def test_returns_title_when_found(self, mock_client_cls: MagicMock) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.list_project_tasks.return_value = [{"identifier": "DEVX-1", "title": "Fix bug"}]
|
|
mock_client_cls.return_value = mock_client
|
|
with patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"}, clear=True):
|
|
result = get_vikunja_title_optional("DEVX-1")
|
|
assert result == "Fix bug"
|
|
|
|
@patch("devx.ci.check_auto_merge_ready.VikunjaClient")
|
|
def test_returns_none_when_not_found(self, mock_client_cls: MagicMock) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.list_project_tasks.return_value = [{"identifier": "DEVX-2", "title": "Other task"}]
|
|
mock_client_cls.return_value = mock_client
|
|
with patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"}, clear=True):
|
|
result = get_vikunja_title_optional("DEVX-1")
|
|
assert result is None
|
|
|
|
@patch("devx.ci.check_auto_merge_ready.VikunjaClient")
|
|
def test_paginates_until_found(self, mock_client_cls: MagicMock) -> None:
|
|
from devx.config import DEFAULT_PER_PAGE
|
|
|
|
mock_client = MagicMock()
|
|
# First page: full page of non-matching tasks, second page: match
|
|
page1 = [{"identifier": f"DEVX-{i}", "title": f"Task {i}"} for i in range(DEFAULT_PER_PAGE)]
|
|
page2 = [{"identifier": "DEVX-99", "title": "Found it"}]
|
|
mock_client.list_project_tasks.side_effect = [page1, page2]
|
|
mock_client_cls.return_value = mock_client
|
|
with patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"}, clear=True):
|
|
result = get_vikunja_title_optional("DEVX-99")
|
|
assert result == "Found it"
|
|
|
|
@patch("devx.ci.check_auto_merge_ready.VikunjaClient")
|
|
def test_returns_none_when_empty_first_page(self, mock_client_cls: MagicMock) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.list_project_tasks.return_value = []
|
|
mock_client_cls.return_value = mock_client
|
|
with patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"}, clear=True):
|
|
result = get_vikunja_title_optional("DEVX-1")
|
|
assert result is None
|
|
|
|
|
|
class TestCli:
|
|
def test_fails_without_task_id(self) -> None:
|
|
runner = CliRunner()
|
|
with (
|
|
patch.dict("os.environ", {"DEVX_TASK_PREFIX": "DEVX"}, clear=True),
|
|
patch("devx.ci.check_auto_merge_ready.is_branch_behind_master", return_value=False),
|
|
):
|
|
result = runner.invoke(cli, ["--branch", "no-task-id-here"])
|
|
assert result.exit_code != 0
|
|
|
|
def test_local_mode_no_pr_title(self) -> None:
|
|
runner = CliRunner()
|
|
with (
|
|
patch.dict("os.environ", {"DEVX_TASK_PREFIX": "DEVX", "VIKUNJA_TOKEN": ""}, clear=True),
|
|
patch("devx.ci.check_auto_merge_ready.is_branch_behind_master", return_value=False),
|
|
):
|
|
result = runner.invoke(cli, ["--branch", "DEVX-1-fix-foo"])
|
|
assert result.exit_code == 0
|
|
assert "local mode" in result.output
|
|
|
|
def test_validates_pr_title_format(self) -> None:
|
|
runner = CliRunner()
|
|
with (
|
|
patch.dict("os.environ", {"DEVX_TASK_PREFIX": "DEVX", "VIKUNJA_TOKEN": ""}, clear=True),
|
|
patch("devx.ci.check_auto_merge_ready.is_branch_behind_master", return_value=False),
|
|
):
|
|
result = runner.invoke(cli, ["--branch", "DEVX-1-fix-foo", "--pr-title", "Bad title"])
|
|
assert result.exit_code != 0
|
|
assert "format" in result.output.lower() or "mismatch" in result.output.lower()
|
|
|
|
def test_passes_with_valid_title(self) -> None:
|
|
runner = CliRunner()
|
|
with (
|
|
patch.dict("os.environ", {"DEVX_TASK_PREFIX": "DEVX", "VIKUNJA_TOKEN": ""}, clear=True),
|
|
patch("devx.ci.check_auto_merge_ready.is_branch_behind_master", return_value=False),
|
|
):
|
|
result = runner.invoke(cli, ["--branch", "DEVX-1-fix-foo", "--pr-title", "DEVX-1: Fix foo"])
|
|
assert result.exit_code == 0
|
|
assert "satisfied" in result.output
|
|
|
|
def test_skip_behind_check(self) -> None:
|
|
runner = CliRunner()
|
|
with (
|
|
patch.dict("os.environ", {"DEVX_TASK_PREFIX": "DEVX", "VIKUNJA_TOKEN": ""}, clear=True),
|
|
patch("devx.ci.check_auto_merge_ready.is_branch_behind_master", return_value=True),
|
|
):
|
|
result = runner.invoke(
|
|
cli,
|
|
["--branch", "DEVX-1-fix-foo", "--pr-title", "DEVX-1: Fix foo", "--skip-behind-check"],
|
|
)
|
|
assert result.exit_code == 0
|
|
|
|
def test_fails_when_behind_master(self) -> None:
|
|
runner = CliRunner()
|
|
with (
|
|
patch.dict("os.environ", {"DEVX_TASK_PREFIX": "DEVX", "VIKUNJA_TOKEN": ""}, clear=True),
|
|
patch("devx.ci.check_auto_merge_ready.is_branch_behind_master", return_value=True),
|
|
):
|
|
result = runner.invoke(
|
|
cli,
|
|
["--branch", "DEVX-1-fix-foo", "--pr-title", "DEVX-1: Fix foo"],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "behind" in result.output.lower()
|
|
|
|
def test_skip_vikunja(self) -> None:
|
|
runner = CliRunner()
|
|
with (
|
|
patch.dict("os.environ", {"DEVX_TASK_PREFIX": "DEVX", "VIKUNJA_TOKEN": "tok"}, clear=True),
|
|
patch("devx.ci.check_auto_merge_ready.is_branch_behind_master", return_value=False),
|
|
patch("devx.ci.check_auto_merge_ready.get_vikunja_title_optional", return_value="Different title"),
|
|
):
|
|
result = runner.invoke(
|
|
cli,
|
|
["--branch", "DEVX-1-fix-foo", "--pr-title", "DEVX-1: Fix foo", "--skip-vikunja"],
|
|
)
|
|
assert result.exit_code == 0
|
|
|
|
def test_fetches_pr_title_from_gitea(self) -> None:
|
|
runner = CliRunner()
|
|
with (
|
|
patch.dict("os.environ", {"DEVX_TASK_PREFIX": "DEVX", "VIKUNJA_TOKEN": ""}, clear=True),
|
|
patch("devx.ci.check_auto_merge_ready.is_branch_behind_master", return_value=False),
|
|
patch("devx.ci.check_auto_merge_ready.get_pr_title_from_gitea", return_value="DEVX-1: Fix foo"),
|
|
):
|
|
result = runner.invoke(
|
|
cli,
|
|
["--branch", "DEVX-1-fix-foo", "--repo", "owner/repo", "--pr-number", "1"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "from Gitea" in result.output
|
|
|
|
def test_fails_when_pr_number_but_no_title(self) -> None:
|
|
runner = CliRunner()
|
|
with (
|
|
patch.dict("os.environ", {"DEVX_TASK_PREFIX": "DEVX", "VIKUNJA_TOKEN": ""}, clear=True),
|
|
patch("devx.ci.check_auto_merge_ready.is_branch_behind_master", return_value=False),
|
|
patch("devx.ci.check_auto_merge_ready.get_pr_title_from_gitea", return_value=None),
|
|
):
|
|
result = runner.invoke(
|
|
cli,
|
|
["--branch", "DEVX-1-fix-foo", "--repo", "owner/repo", "--pr-number", "1"],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "Could not fetch" in result.output
|
|
|
|
def test_fails_when_vikunja_token_set_but_task_not_found(self) -> None:
|
|
runner = CliRunner()
|
|
with (
|
|
patch.dict("os.environ", {"DEVX_TASK_PREFIX": "DEVX", "VIKUNJA_TOKEN": "tok"}, clear=True),
|
|
patch("devx.ci.check_auto_merge_ready.is_branch_behind_master", return_value=False),
|
|
patch("devx.ci.check_auto_merge_ready.get_vikunja_title_optional", return_value=None),
|
|
):
|
|
result = runner.invoke(
|
|
cli,
|
|
["--branch", "DEVX-1-fix-foo", "--pr-title", "DEVX-1: Fix foo"],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "Could not find Vikunja task" in result.output
|
|
|
|
def test_passes_with_vikunja_title_match(self) -> None:
|
|
runner = CliRunner()
|
|
with (
|
|
patch.dict("os.environ", {"DEVX_TASK_PREFIX": "DEVX", "VIKUNJA_TOKEN": "tok"}, clear=True),
|
|
patch("devx.ci.check_auto_merge_ready.is_branch_behind_master", return_value=False),
|
|
patch("devx.ci.check_auto_merge_ready.get_vikunja_title_optional", return_value="Fix foo"),
|
|
):
|
|
result = runner.invoke(
|
|
cli,
|
|
["--branch", "DEVX-1-fix-foo", "--pr-title", "DEVX-1: Fix foo"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "Vikunja title match OK" in result.output
|
|
|
|
def test_fails_with_vikunja_title_mismatch(self) -> None:
|
|
runner = CliRunner()
|
|
with (
|
|
patch.dict("os.environ", {"DEVX_TASK_PREFIX": "DEVX", "VIKUNJA_TOKEN": "tok"}, clear=True),
|
|
patch("devx.ci.check_auto_merge_ready.is_branch_behind_master", return_value=False),
|
|
patch("devx.ci.check_auto_merge_ready.get_vikunja_title_optional", return_value="Different title"),
|
|
):
|
|
result = runner.invoke(
|
|
cli,
|
|
["--branch", "DEVX-1-fix-foo", "--pr-title", "DEVX-1: Fix foo"],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "does not match Vikunja" in result.output
|
|
|
|
def test_fails_with_double_prefix_in_vikunja_title(self) -> None:
|
|
"""Vikunja title with task ID prefix causes double-prefix in PR title."""
|
|
runner = CliRunner()
|
|
with (
|
|
patch.dict("os.environ", {"DEVX_TASK_PREFIX": "DEVX", "VIKUNJA_TOKEN": "tok"}, clear=True),
|
|
patch("devx.ci.check_auto_merge_ready.is_branch_behind_master", return_value=False),
|
|
patch("devx.ci.check_auto_merge_ready.get_vikunja_title_optional", return_value="DEVX-1: Fix foo"),
|
|
):
|
|
result = runner.invoke(
|
|
cli,
|
|
["--branch", "DEVX-1-fix-foo", "--pr-title", "DEVX-1: Fix foo"],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "should NOT include" in result.output
|