"""Unit tests for devx.ci.create_dependency_pr.""" from pathlib import Path from unittest.mock import MagicMock, patch import click from click.testing import CliRunner from devx.ci.create_dependency_pr import ( cli, create_vikunja_task, find_existing_pr, find_pinned_version, update_pinned_version, ) class TestFindPinnedVersion: def test_finds_pip_git_pin(self, tmp_path: Path) -> None: content = "grm @ git+https://git.example.com/repo.git@v0.5.1" path = tmp_path / "pyproject.toml" path.write_text(content) version = find_pinned_version("grm", str(path)) assert version == "0.5.1" def test_finds_pyproject_pin(self, tmp_path: Path) -> None: content = 'grm = "0.5.1"' path = tmp_path / "pyproject.toml" path.write_text(content) version = find_pinned_version("grm", str(path)) assert version == "0.5.1" def test_finds_ansible_var_pin(self, tmp_path: Path) -> None: content = 'grm_version: "0.5.1"' path = tmp_path / "images.yml" path.write_text(content) version = find_pinned_version("grm", str(path)) assert version == "0.5.1" def test_finds_image_version_pin(self, tmp_path: Path) -> None: content = 'sso_bridge_image_version: "1.2.3"' path = tmp_path / "images.yml" path.write_text(content) version = find_pinned_version("sso_bridge", str(path)) assert version == "1.2.3" def test_returns_none_when_not_found(self, tmp_path: Path) -> None: path = tmp_path / "pyproject.toml" path.write_text('other = "1.0.0"') assert find_pinned_version("grm", str(path)) is None def test_returns_none_when_file_missing(self, tmp_path: Path) -> None: assert find_pinned_version("grm", str(tmp_path / "nonexistent.toml")) is None class TestUpdatePinnedVersion: def test_updates_pip_git_pin(self, tmp_path: Path) -> None: content = "grm @ git+https://git.example.com/repo.git@v0.5.1" path = tmp_path / "pyproject.toml" path.write_text(content) changed = update_pinned_version(str(path), "grm", "0.5.1", "0.5.2") assert changed is True assert "0.5.2" in path.read_text() assert "0.5.1" not in path.read_text() def test_updates_pyproject_pin(self, tmp_path: Path) -> None: content = 'grm = "0.5.1"' path = tmp_path / "pyproject.toml" path.write_text(content) changed = update_pinned_version(str(path), "grm", "0.5.1", "0.5.2") assert changed is True assert 'grm = "0.5.2"' in path.read_text() def test_no_change_when_version_not_found(self, tmp_path: Path) -> None: content = 'other = "1.0.0"' path = tmp_path / "pyproject.toml" path.write_text(content) changed = update_pinned_version(str(path), "grm", "0.5.1", "0.5.2") assert changed is False def test_no_change_when_file_missing(self, tmp_path: Path) -> None: changed = update_pinned_version(str(tmp_path / "nonexistent"), "grm", "0.5.1", "0.5.2") assert changed is False class TestFindExistingPr: @patch("devx.tools.create_pr.GiteaClient") def test_returns_pr_when_found(self, mock_client_cls: MagicMock) -> None: mock_client = mock_client_cls.return_value mock_client.list_prs.return_value = [ {"head": {"ref": "deps/grm-0.5.2"}, "number": 42}, {"head": {"ref": "other-branch"}, "number": 43}, ] result = find_existing_pr(mock_client, "deps/grm-0.5.2") assert result is not None assert result["number"] == 42 @patch("devx.tools.create_pr.GiteaClient") def test_returns_none_when_not_found(self, mock_client_cls: MagicMock) -> None: mock_client = mock_client_cls.return_value mock_client.list_prs.return_value = [] result = find_existing_pr(mock_client, "deps/grm-0.5.2") assert result is None class TestCli: @patch("devx.ci.create_dependency_pr.find_pinned_version") @patch("devx.ci.create_dependency_pr.get_ci_token") def test_same_version_no_pr(self, mock_token: MagicMock, mock_find: MagicMock) -> None: mock_token.return_value = "fake-token" mock_find.return_value = "0.5.2" runner = CliRunner() result = runner.invoke( cli, [ "--package", "grm", "--new-version", "0.5.2", "--source-repo", "oblachno/grm", ], ) assert result.exit_code == 0 assert "no pr needed" in result.output.lower() @patch("devx.ci.create_dependency_pr.find_pinned_version") @patch("devx.ci.create_dependency_pr.get_ci_token") def test_dry_run(self, mock_token: MagicMock, mock_find: MagicMock) -> None: mock_token.return_value = "fake-token" mock_find.return_value = "0.5.1" runner = CliRunner() result = runner.invoke( cli, [ "--package", "grm", "--new-version", "0.5.2", "--source-repo", "oblachno/grm", "--dry-run", ], ) assert result.exit_code == 0 assert "DRY RUN" in result.output @patch("devx.ci.create_dependency_pr.find_pinned_version") @patch("devx.ci.create_dependency_pr.get_ci_token") def test_version_not_found_fails(self, mock_token: MagicMock, mock_find: MagicMock) -> None: mock_token.return_value = "fake-token" mock_find.return_value = None runner = CliRunner() result = runner.invoke( cli, [ "--package", "nonexistent", "--new-version", "1.0.0", "--source-repo", "oblachno/test", ], ) assert result.exit_code != 0 class TestCreateVikunjaTask: def test_returns_none_when_no_token(self) -> None: with patch("devx.ci.create_dependency_pr.get_vikunja_token", side_effect=click.ClickException("no token")): result = create_vikunja_task("Test", "desc") assert result is None def test_returns_identifier_on_success(self) -> None: with ( patch("devx.ci.create_dependency_pr.get_vikunja_token", return_value="fake-token"), patch("devx.api_clients.VikunjaClient") as mock_client_cls, ): mock_client = mock_client_cls.return_value mock_client.create_task.return_value = {"identifier": "OBL-INFRA-999"} result = create_vikunja_task("Test", "desc") assert result == "OBL-INFRA-999"