Public Access
197 lines
8.5 KiB
Python
197 lines
8.5 KiB
Python
"""Unit tests for devx.tools.create_pr."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import click
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
from devx.tools.create_pr import (
|
|
cli,
|
|
create_pr,
|
|
extract_task_id,
|
|
find_existing_pr,
|
|
get_repo_name,
|
|
get_vikunja_task_title,
|
|
)
|
|
|
|
|
|
class TestExtractTaskId:
|
|
def test_valid(self) -> None:
|
|
assert extract_task_id("DEVX-42-fix") == "DEVX-42"
|
|
|
|
def test_invalid(self) -> None:
|
|
assert extract_task_id("feature") == ""
|
|
|
|
|
|
class TestGetRepoName:
|
|
@patch.dict("os.environ", {"DEVX_REPO_NAME": "my-repo"})
|
|
def test_from_env(self) -> None:
|
|
assert get_repo_name() == "my-repo"
|
|
|
|
@patch("devx.tools.create_pr.REPO_NAME", "devx")
|
|
@patch.dict("os.environ", {"GITHUB_REPOSITORY": "my-org/my-repo"}, clear=True)
|
|
def test_env_overrides_pyproject(self) -> None:
|
|
assert get_repo_name() == "my-repo"
|
|
|
|
@patch("devx.tools.create_pr.REPO_NAME", "devx")
|
|
@patch.dict("os.environ", {}, clear=True)
|
|
def test_from_pyproject(self) -> None:
|
|
assert get_repo_name() == "devx"
|
|
|
|
@patch("devx.tools.create_pr.REPO_NAME", "")
|
|
@patch.dict("os.environ", {"GITHUB_REPOSITORY": "my-org/my-repo"}, clear=True)
|
|
def test_from_github(self) -> None:
|
|
assert get_repo_name() == "my-repo"
|
|
|
|
@patch("devx.tools.create_pr.REPO_NAME", "")
|
|
@patch.dict("os.environ", {}, clear=True)
|
|
def test_missing_raises(self) -> None:
|
|
with pytest.raises(click.ClickException, match="Repository name"):
|
|
get_repo_name()
|
|
|
|
|
|
class TestGetVikunjaTaskTitle:
|
|
@patch("devx.tools.create_pr.VikunjaClient")
|
|
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
|
def test_found(self, mock_client_cls: MagicMock) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.find_task_by_identifier.return_value = {"identifier": "DEVX-42", "title": "Add feature"}
|
|
mock_client_cls.return_value = mock_client
|
|
assert get_vikunja_task_title("DEVX-42") == "Add feature"
|
|
|
|
@patch.dict("os.environ", {}, clear=True)
|
|
def test_no_token(self) -> None:
|
|
with pytest.raises(click.ClickException, match="VIKUNJA_TOKEN"):
|
|
get_vikunja_task_title("DEVX-42")
|
|
|
|
@patch("devx.tools.create_pr.VikunjaClient")
|
|
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
|
def test_not_found(self, mock_client_cls: MagicMock) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.find_task_by_identifier.return_value = None
|
|
mock_client_cls.return_value = mock_client
|
|
with pytest.raises(click.ClickException, match="Could not find"):
|
|
get_vikunja_task_title("DEVX-42")
|
|
|
|
|
|
class TestFindExistingPr:
|
|
def test_found(self) -> None:
|
|
client = MagicMock()
|
|
client.list_prs.return_value = [{"head": {"ref": "DEVX-42-fix"}, "number": 10}]
|
|
result = find_existing_pr(client, "DEVX-42-fix")
|
|
assert result is not None
|
|
assert result["number"] == 10
|
|
|
|
def test_not_found(self) -> None:
|
|
client = MagicMock()
|
|
client.list_prs.return_value = [{"head": {"ref": "other"}, "number": 10}]
|
|
result = find_existing_pr(client, "DEVX-42-fix")
|
|
assert result is None
|
|
|
|
|
|
class TestCreatePr:
|
|
@patch("devx.tools.create_pr.GiteaClient")
|
|
@patch("devx.tools.create_pr.get_vikunja_task_title", return_value="Add feature")
|
|
@patch("devx.tools.create_pr.find_existing_pr", return_value=None)
|
|
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
|
|
def test_creates_new_pr(self, mock_find: MagicMock, mock_title: MagicMock, mock_gitea: MagicMock) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.create_pr.return_value = {"number": 15, "html_url": "https://git.example.com/pr/15"}
|
|
mock_gitea.return_value = mock_client
|
|
result = create_pr("DEVX-42-fix", "master", "body", "owner", "repo")
|
|
assert result["number"] == 15
|
|
mock_client.create_pr.assert_called_once_with(
|
|
title="DEVX-42: Add feature",
|
|
head="DEVX-42-fix",
|
|
base="master",
|
|
body="body",
|
|
)
|
|
|
|
@patch("devx.tools.create_pr.GiteaClient")
|
|
@patch("devx.tools.create_pr.get_vikunja_task_title", return_value="Add feature")
|
|
@patch("devx.tools.create_pr.find_existing_pr")
|
|
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
|
|
def test_existing_pr_idempotent(self, mock_find: MagicMock, mock_title: MagicMock, mock_gitea: MagicMock) -> None:
|
|
mock_find.return_value = {"number": 10, "html_url": "https://git.example.com/pr/10"}
|
|
mock_client = MagicMock()
|
|
mock_gitea.return_value = mock_client
|
|
result = create_pr("DEVX-42-fix", "master", "", "owner", "repo")
|
|
assert result["number"] == 10
|
|
mock_client.create_pr.assert_not_called()
|
|
|
|
@patch.dict("os.environ", {}, clear=True)
|
|
def test_no_repo_token(self) -> None:
|
|
with pytest.raises(click.ClickException, match="CI_GITEA_TOKEN"):
|
|
create_pr("DEVX-42-fix", "master", "", "owner", "repo")
|
|
|
|
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
|
|
def test_no_task_id_in_branch(self) -> None:
|
|
with pytest.raises(click.ClickException, match="does not contain a task ID"):
|
|
create_pr("feature-branch", "master", "", "owner", "repo")
|
|
|
|
|
|
class TestCli:
|
|
@patch("devx.tools.create_pr.create_pr")
|
|
@patch("devx.tools.create_pr.subprocess.run")
|
|
@patch("devx.tools.create_pr.REPO_OWNER", "owner")
|
|
@patch("devx.tools.create_pr.get_repo_name", return_value="repo")
|
|
def test_auto_detect_branch(self, mock_repo: MagicMock, mock_run: MagicMock, mock_create: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(stdout="DEVX-42-fix\n", returncode=0)
|
|
mock_create.return_value = {"number": 1}
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, [])
|
|
assert result.exit_code == 0
|
|
mock_create.assert_called_once_with("DEVX-42-fix", "master", "", "owner", "repo")
|
|
|
|
@patch("devx.tools.create_pr.subprocess.run")
|
|
@patch("devx.tools.create_pr.create_pr")
|
|
@patch("devx.tools.create_pr.REPO_OWNER", "owner")
|
|
@patch("devx.tools.create_pr.get_repo_name", return_value="repo")
|
|
def test_explicit_branch(self, mock_repo: MagicMock, mock_create: MagicMock, mock_subproc: MagicMock) -> None:
|
|
mock_create.return_value = {"number": 1}
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["--branch", "DEVX-42-fix"])
|
|
assert result.exit_code == 0
|
|
|
|
@patch("devx.tools.create_pr.subprocess.run")
|
|
@patch("devx.tools.create_pr.create_pr")
|
|
@patch("devx.tools.create_pr.REPO_OWNER", "owner")
|
|
@patch("devx.tools.create_pr.get_repo_name", return_value="repo")
|
|
def test_body_from_stdin(self, mock_repo: MagicMock, mock_create: MagicMock, mock_subproc: MagicMock) -> None:
|
|
mock_create.return_value = {"number": 1}
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["--branch", "DEVX-42-fix", "--body", "-"], input="PR body text")
|
|
assert result.exit_code == 0
|
|
mock_create.assert_called_once()
|
|
assert mock_create.call_args.args[2] == "PR body text"
|
|
|
|
@patch("devx.tools.create_pr.subprocess.run")
|
|
@patch("devx.tools.create_pr.REPO_OWNER", "")
|
|
@patch("devx.tools.create_pr.get_repo_name", return_value="repo")
|
|
def test_missing_owner(self, mock_repo: MagicMock, mock_subproc: MagicMock) -> None:
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["--branch", "DEVX-42-fix"])
|
|
assert result.exit_code != 0
|
|
assert "owner" in result.output.lower()
|
|
|
|
@patch("devx.tools.create_pr.subprocess.run")
|
|
@patch("devx.tools.create_pr.create_pr")
|
|
@patch("devx.tools.create_pr.get_repo_name", return_value="repo")
|
|
def test_explicit_owner(self, mock_repo: MagicMock, mock_create: MagicMock, mock_subproc: MagicMock) -> None:
|
|
mock_create.return_value = {"number": 1}
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["--branch", "DEVX-42-fix", "--owner", "custom"])
|
|
assert result.exit_code == 0
|
|
mock_create.assert_called_once_with("DEVX-42-fix", "master", "", "custom", "repo")
|
|
|
|
@patch("devx.tools.create_pr.subprocess.run")
|
|
@patch("devx.tools.create_pr.REPO_OWNER", "owner")
|
|
@patch("devx.tools.create_pr.get_repo_name", return_value="repo")
|
|
def test_git_detect_failure(self, mock_repo: MagicMock, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(stdout="", stderr="fatal: not a git repository", returncode=128)
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, [])
|
|
assert result.exit_code != 0
|
|
assert "Could not detect" in result.output
|