Files
grm/tests/unit/test_post_merge.py
T
Emil Simeonov fefddda715 GRM-20: refactor(scripts): centralize constants, API clients, and HTTP status codes
- Add shared config.py with API URLs, regexes, timeouts, pagination
- Add GiteaClient and VikunjaClient in api_clients.py with pooled sessions
- Add APIError exception for unified HTTP error handling
- Refactor all scripts to use shared modules and http.HTTPStatus
- Rewrite unit tests to mock clients and use HTTPStatus constants
- Add tests for api_clients and config modules
- Achieve 100% test coverage
2026-06-19 21:00:21 +02:00

164 lines
6.4 KiB
Python

"""Unit tests for scripts/post_merge.py."""
import http
from unittest.mock import MagicMock, patch
import click
import pytest
from click.testing import CliRunner
from gitea_runner_manager.config import VIKUNJA_PROJECT_ID
from gitea_runner_manager.exceptions import APIError
from scripts.post_merge import (
build_comment,
extract_conventional_msg,
extract_task_id,
main,
resolve_task_id,
)
class TestExtractTaskId:
def test_extracts_from_first_line(self) -> None:
assert extract_task_id("GRM-19: fix: resolve bug\n\nBody") == "GRM-19"
def test_returns_empty_when_missing(self) -> None:
assert extract_task_id("fix: resolve bug") == ""
class TestExtractConventionalMsg:
def test_strips_task_id_prefix(self) -> None:
assert extract_conventional_msg("GRM-19: fix: resolve bug") == "fix: resolve bug"
def test_returns_unchanged_without_prefix(self) -> None:
assert extract_conventional_msg("fix: resolve bug") == "fix: resolve bug"
class TestBuildComment:
def test_html_format(self) -> None:
html = build_comment("GRM-19", "fix: bug", "abc123")
assert "<strong>GRM-19</strong>" in html
assert "fix: bug" in html
assert "<code>abc123</code>" in html
class TestResolveTaskId:
def test_found(self) -> None:
mock_client = MagicMock()
mock_client.list_tasks.return_value = [
{"id": 42, "project_id": VIKUNJA_PROJECT_ID, "identifier": "GRM-19"},
]
assert resolve_task_id(mock_client, "GRM-19") == 42
mock_client.list_tasks.assert_called_once()
def test_not_found_raises(self) -> None:
mock_client = MagicMock()
mock_client.list_tasks.return_value = []
with pytest.raises(click.ClickException) as exc:
resolve_task_id(mock_client, "GRM-99")
assert "Could not find" in str(exc.value)
def test_wrong_project_filtered(self) -> None:
mock_client = MagicMock()
mock_client.list_tasks.return_value = [
{"id": 42, "project_id": 999, "identifier": "GRM-19"},
]
with pytest.raises(click.ClickException) as exc:
resolve_task_id(mock_client, "GRM-19")
assert "Could not find" in str(exc.value)
def test_http_error_propagates(self) -> None:
mock_client = MagicMock()
mock_client.list_tasks.side_effect = APIError(http.HTTPStatus.INTERNAL_SERVER_ERROR, "server error")
with pytest.raises(APIError):
resolve_task_id(mock_client, "GRM-19")
class TestMain:
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
@patch("scripts.post_merge.VikunjaClient")
def test_full_flow(self, mock_client_cls: MagicMock) -> None:
mock_client = MagicMock()
mock_client.list_tasks.return_value = [
{"id": 267, "project_id": VIKUNJA_PROJECT_ID, "identifier": "GRM-20"},
]
mock_client_cls.return_value = mock_client
runner = CliRunner()
result = runner.invoke(
main,
["GRM-20: fix: resolve bug\n\nBody", "--commit-sha", "abc123"],
)
assert result.exit_code == 0
assert "updated and marked done" in result.output
mock_client.post_comment.assert_called_once()
mock_client.update_task.assert_called_once_with(267, done=True)
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
@patch("scripts.post_merge.VikunjaClient")
def test_no_commit_sha(self, mock_client_cls: MagicMock) -> None:
mock_client = MagicMock()
mock_client.list_tasks.return_value = [
{"id": 267, "project_id": VIKUNJA_PROJECT_ID, "identifier": "GRM-20"},
]
mock_client_cls.return_value = mock_client
runner = CliRunner()
result = runner.invoke(main, ["GRM-20: fix: resolve bug"])
assert result.exit_code == 0
mock_client.post_comment.assert_called_once()
args, _ = mock_client.post_comment.call_args
assert "unknown" in args[1]
@patch.dict("os.environ", {"VIKUNJA_TOKEN": ""}, clear=True)
def test_missing_token_exits(self) -> None:
runner = CliRunner()
result = runner.invoke(main, ["GRM-20: fix: bug"])
assert result.exit_code == 1
assert "VIKUNJA_TOKEN" in result.output
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
def test_no_task_id_skips(self) -> None:
runner = CliRunner()
result = runner.invoke(main, ["fix: resolve bug"])
assert result.exit_code == 0
assert "skipping" in result.output
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
@patch("scripts.post_merge.VikunjaClient")
def test_resolve_failure_propagates(self, mock_client_cls: MagicMock) -> None:
mock_client = MagicMock()
mock_client.list_tasks.return_value = []
mock_client_cls.return_value = mock_client
runner = CliRunner()
result = runner.invoke(main, ["GRM-20: fix: bug"])
assert result.exit_code == 1
assert "Could not find" in result.output
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
@patch("scripts.post_merge.VikunjaClient")
def test_post_comment_failure_raises_click(self, mock_client_cls: MagicMock) -> None:
mock_client = MagicMock()
mock_client.list_tasks.return_value = [
{"id": 267, "project_id": VIKUNJA_PROJECT_ID, "identifier": "GRM-20"},
]
mock_client.post_comment.side_effect = APIError(http.HTTPStatus.INTERNAL_SERVER_ERROR, "server error")
mock_client_cls.return_value = mock_client
runner = CliRunner()
result = runner.invoke(main, ["GRM-20: fix: bug"])
assert result.exit_code == 1
assert "HTTP" in result.output
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
@patch("scripts.post_merge.VikunjaClient")
def test_mark_done_failure_raises_click(self, mock_client_cls: MagicMock) -> None:
mock_client = MagicMock()
mock_client.list_tasks.return_value = [
{"id": 267, "project_id": VIKUNJA_PROJECT_ID, "identifier": "GRM-20"},
]
mock_client.post_comment.return_value = None
mock_client.update_task.side_effect = APIError(http.HTTPStatus.INTERNAL_SERVER_ERROR, "server error")
mock_client_cls.return_value = mock_client
runner = CliRunner()
result = runner.invoke(main, ["GRM-20: fix: bug"])
assert result.exit_code == 1
assert "HTTP" in result.output