Files
devx/tests/unit/test_fix_pr_title.py
T
emil ddfbdec956
Post-merge / detect-and-configure (push) Successful in 12s
Post-merge / release-and-maintain (push) Successful in 1m0s
DEVX-136: feat: add fix_pr_title module and update_pr API method
2026-07-13 23:55:11 +00:00

196 lines
7.3 KiB
Python

"""Tests for devx.ci.fix_pr_title."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from click.testing import CliRunner
from devx.ci.fix_pr_title import cli
@pytest.fixture(autouse=True)
def _obl_infra_prefix(monkeypatch: pytest.MonkeyPatch) -> None:
"""Use OBL-INFRA prefix to match infra repo conventions."""
import re
monkeypatch.setattr("devx.ci.fix_pr_title.TASK_PREFIX", "OBL-INFRA")
monkeypatch.setattr("devx.ci.auto_merge.TASK_PREFIX", "OBL-INFRA")
monkeypatch.setattr("devx.ci.auto_merge.PR_TITLE_RE", re.compile(r"^OBL-INFRA-\d+:\s+.+"))
monkeypatch.setattr("devx.ci.auto_merge._TASK_ID_PREFIX_RE", re.compile(r"^OBL-INFRA-\d+:\s*"))
monkeypatch.setattr("devx.ci._shared.TASK_ID_RE", re.compile(r"OBL-INFRA-\d+"))
class TestFixPrTitle:
@patch("devx.ci.fix_pr_title.get_ci_token")
@patch("devx.ci.fix_pr_title.GiteaClient")
@patch("devx.ci.fix_pr_title.get_vikunja_title_optional")
def test_fixes_title_with_vikunja(
self,
mock_vikunja: MagicMock,
mock_gitea_cls: MagicMock,
mock_ci_token: MagicMock,
) -> None:
"""PR title is updated to match task ID + Vikunja title."""
mock_client = MagicMock()
mock_gitea_cls.return_value = mock_client
mock_client.get_pr.return_value = {
"number": 42,
"title": "Fix blackbox exporter",
"head": {"ref": "OBL-INFRA-458-blackbox-ipv4"},
}
mock_vikunja.return_value = "Fix blackbox exporter IPv4 config"
mock_ci_token.return_value = "token"
runner = CliRunner()
result = runner.invoke(cli, ["--repo", "oblachno/infra", "--pr-number", "42"])
assert result.exit_code == 0
mock_client.update_pr.assert_called_once_with(42, {"title": "OBL-INFRA-458: Fix blackbox exporter IPv4 config"})
@patch("devx.ci.fix_pr_title.get_ci_token")
@patch("devx.ci.fix_pr_title.GiteaClient")
@patch("devx.ci.fix_pr_title.get_vikunja_title_optional")
def test_strips_conventional_commit_prefix_when_no_vikunja(
self,
mock_vikunja: MagicMock,
mock_gitea_cls: MagicMock,
mock_ci_token: MagicMock,
) -> None:
"""When Vikunja task not found, strips conventional-commit prefix from current title."""
mock_client = MagicMock()
mock_gitea_cls.return_value = mock_client
mock_client.get_pr.return_value = {
"number": 10,
"title": "fix: platform self-monitoring and fixes",
"head": {"ref": "OBL-INFRA-456-platform-fixes"},
}
mock_vikunja.return_value = None
mock_ci_token.return_value = "token"
runner = CliRunner()
result = runner.invoke(cli, ["--repo", "oblachno/infra", "--pr-number", "10"])
assert result.exit_code == 0
mock_client.update_pr.assert_called_once_with(
10, {"title": "OBL-INFRA-456: platform self-monitoring and fixes"}
)
@patch("devx.ci.fix_pr_title.get_ci_token")
@patch("devx.ci.fix_pr_title.GiteaClient")
@patch("devx.ci.fix_pr_title.get_vikunja_title_optional")
def test_already_correct_title_no_update(
self,
mock_vikunja: MagicMock,
mock_gitea_cls: MagicMock,
mock_ci_token: MagicMock,
) -> None:
"""When PR title is already correct, no update is made."""
mock_client = MagicMock()
mock_gitea_cls.return_value = mock_client
mock_client.get_pr.return_value = {
"number": 5,
"title": "OBL-INFRA-100: Fix bug",
"head": {"ref": "OBL-INFRA-100-fix-bug"},
}
mock_vikunja.return_value = "Fix bug"
mock_ci_token.return_value = "token"
runner = CliRunner()
result = runner.invoke(cli, ["--repo", "oblachno/infra", "--pr-number", "5"])
assert result.exit_code == 0
mock_client.update_pr.assert_not_called()
@patch("devx.ci.fix_pr_title.get_ci_token")
@patch("devx.ci.fix_pr_title.GiteaClient")
@patch("devx.ci.fix_pr_title.get_vikunja_title_optional")
def test_dry_run_no_update(
self,
mock_vikunja: MagicMock,
mock_gitea_cls: MagicMock,
mock_ci_token: MagicMock,
) -> None:
"""Dry run shows what would change without updating."""
mock_client = MagicMock()
mock_gitea_cls.return_value = mock_client
mock_client.get_pr.return_value = {
"number": 7,
"title": "Fix thing",
"head": {"ref": "OBL-INFRA-7-fix-thing"},
}
mock_vikunja.return_value = "Fix thing"
mock_ci_token.return_value = "token"
runner = CliRunner()
result = runner.invoke(cli, ["--repo", "oblachno/infra", "--pr-number", "7", "--dry-run"])
assert result.exit_code == 0
mock_client.update_pr.assert_not_called()
@patch("devx.ci.fix_pr_title.get_ci_token")
@patch("devx.ci.fix_pr_title.GiteaClient")
def test_no_task_id_in_branch_exits_error(
self,
mock_gitea_cls: MagicMock,
mock_ci_token: MagicMock,
) -> None:
"""When branch has no task ID, exits with error."""
mock_client = MagicMock()
mock_gitea_cls.return_value = mock_client
mock_client.get_pr.return_value = {
"number": 1,
"title": "Some title",
"head": {"ref": "just-a-branch"},
}
mock_ci_token.return_value = "token"
runner = CliRunner()
result = runner.invoke(cli, ["--repo", "oblachno/infra", "--pr-number", "1"])
assert result.exit_code != 0
mock_client.update_pr.assert_not_called()
@patch("devx.ci.fix_pr_title.get_ci_token")
def test_no_token_exits_error(self, mock_ci_token: MagicMock) -> None:
"""When CI token is not set, exits with error."""
mock_ci_token.side_effect = Exception("no token")
runner = CliRunner()
result = runner.invoke(cli, ["--repo", "oblachno/infra", "--pr-number", "1"])
assert result.exit_code != 0
@patch("devx.ci.fix_pr_title.get_ci_token")
@patch("devx.ci.fix_pr_title.GiteaClient")
@patch("devx.ci.fix_pr_title.get_vikunja_title_optional")
def test_strips_task_id_prefix_from_vikunja_title(
self,
mock_vikunja: MagicMock,
mock_gitea_cls: MagicMock,
mock_ci_token: MagicMock,
) -> None:
"""When Vikunja title already has task ID prefix, it's stripped to avoid double prefix."""
mock_client = MagicMock()
mock_gitea_cls.return_value = mock_client
mock_client.get_pr.return_value = {
"number": 99,
"title": "Fix thing",
"head": {"ref": "OBL-INFRA-99-fix-thing"},
}
mock_vikunja.return_value = "OBL-INFRA-99: Fix thing"
mock_ci_token.return_value = "token"
runner = CliRunner()
result = runner.invoke(cli, ["--repo", "oblachno/infra", "--pr-number", "99"])
assert result.exit_code == 0
mock_client.update_pr.assert_called_once_with(99, {"title": "OBL-INFRA-99: Fix thing"})
def test_invalid_repo_format_exits_error(self) -> None:
"""When repo is not in owner/name format, exits with error."""
runner = CliRunner()
result = runner.invoke(cli, ["--repo", "invalid", "--pr-number", "1"])
assert result.exit_code != 0