"""Unit tests for scripts/post_merge.py.""" from unittest.mock import MagicMock, patch import pytest import requests from scripts.post_merge import ( PROJECT_ID, VIKUNJA_API, build_comment, extract_conventional_msg, extract_task_id, main, mark_task_done, post_comment, 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 "GRM-19" in html assert "fix: bug" in html assert "abc123" in html class TestResolveTaskId: @patch("scripts.post_merge.requests.get") def test_found(self, mock_get: MagicMock) -> None: mock_response = MagicMock() mock_response.json.return_value = [ {"id": 42, "project_id": PROJECT_ID, "identifier": "GRM-19"}, ] mock_get.return_value = mock_response assert resolve_task_id("tok", "GRM-19") == 42 mock_get.assert_called_once() @patch("scripts.post_merge.requests.get") def test_not_found_exits(self, mock_get: MagicMock) -> None: mock_response = MagicMock() mock_response.json.return_value = [] mock_get.return_value = mock_response with pytest.raises(SystemExit) as exc: resolve_task_id("tok", "GRM-99") assert exc.value.code == 1 @patch("scripts.post_merge.requests.get") def test_wrong_project_filtered(self, mock_get: MagicMock) -> None: mock_response = MagicMock() mock_response.json.return_value = [ {"id": 42, "project_id": 999, "identifier": "GRM-19"}, ] mock_get.return_value = mock_response with pytest.raises(SystemExit) as exc: resolve_task_id("tok", "GRM-19") assert exc.value.code == 1 @patch("scripts.post_merge.requests.get") def test_http_error_propagates(self, mock_get: MagicMock) -> None: mock_response = MagicMock() mock_response.raise_for_status.side_effect = requests.HTTPError("500") mock_get.return_value = mock_response with pytest.raises(requests.HTTPError): resolve_task_id("tok", "GRM-19") class TestPostComment: @patch("scripts.post_merge.requests.post") def test_success(self, mock_post: MagicMock) -> None: mock_response = MagicMock() mock_post.return_value = mock_response post_comment("tok", 42, "

hi

") args, kwargs = mock_post.call_args assert VIKUNJA_API in args[0] assert kwargs["json"]["comment"] == "

hi

" @patch("scripts.post_merge.requests.post") def test_http_error_propagates(self, mock_post: MagicMock) -> None: mock_response = MagicMock() mock_response.raise_for_status.side_effect = requests.HTTPError("500") mock_post.return_value = mock_response with pytest.raises(requests.HTTPError): post_comment("tok", 42, "html") class TestMarkTaskDone: @patch("scripts.post_merge.requests.put") def test_success(self, mock_put: MagicMock) -> None: mock_response = MagicMock() mock_put.return_value = mock_response mark_task_done("tok", 42) args, kwargs = mock_put.call_args assert kwargs["json"]["done"] is True @patch("scripts.post_merge.requests.put") def test_http_error_propagates(self, mock_put: MagicMock) -> None: mock_response = MagicMock() mock_response.raise_for_status.side_effect = requests.HTTPError("500") mock_put.return_value = mock_response with pytest.raises(requests.HTTPError): mark_task_done("tok", 42) class TestMain: @patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"}) @patch("scripts.post_merge.resolve_task_id") @patch("scripts.post_merge.post_comment") @patch("scripts.post_merge.mark_task_done") def test_full_flow( self, mock_mark: MagicMock, mock_post: MagicMock, mock_resolve: MagicMock, capsys: pytest.CaptureFixture[str] ) -> None: mock_resolve.return_value = 267 main(["post_merge.py", "GRM-20: fix: resolve bug\n\nBody", "--commit-sha", "abc123"]) mock_resolve.assert_called_once_with("tok", "GRM-20") mock_post.assert_called_once() mock_mark.assert_called_once_with("tok", 267) captured = capsys.readouterr() assert "updated and marked done" in captured.out @patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"}) @patch("scripts.post_merge.resolve_task_id") @patch("scripts.post_merge.post_comment") @patch("scripts.post_merge.mark_task_done") def test_no_commit_sha(self, mock_mark: MagicMock, mock_post: MagicMock, mock_resolve: MagicMock) -> None: mock_resolve.return_value = 267 main(["post_merge.py", "GRM-20: fix: resolve bug"]) mock_post.assert_called_once() args, _ = mock_post.call_args assert "unknown" in args[2] @patch.dict("os.environ", {"VIKUNJA_TOKEN": ""}, clear=True) def test_missing_token_exits(self) -> None: with pytest.raises(SystemExit) as exc: main(["post_merge.py", "GRM-20: fix: bug"]) assert exc.value.code == 1 @patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"}) def test_no_task_id_skips(self, capsys: pytest.CaptureFixture[str]) -> None: main(["post_merge.py", "fix: resolve bug"]) captured = capsys.readouterr() assert "skipping Vikunja update" in captured.out @patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"}) @patch("scripts.post_merge.resolve_task_id") def test_resolve_failure_propagates(self, mock_resolve: MagicMock) -> None: mock_resolve.side_effect = SystemExit(1) with pytest.raises(SystemExit) as exc: main(["post_merge.py", "GRM-20: fix: bug"]) assert exc.value.code == 1 @patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"}) @patch("scripts.post_merge.resolve_task_id") @patch("scripts.post_merge.post_comment") def test_post_comment_failure_propagates(self, mock_post: MagicMock, mock_resolve: MagicMock) -> None: mock_resolve.return_value = 267 mock_post.side_effect = requests.HTTPError("500") with pytest.raises(requests.HTTPError): main(["post_merge.py", "GRM-20: fix: bug"]) @patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"}) @patch("scripts.post_merge.resolve_task_id") @patch("scripts.post_merge.post_comment") @patch("scripts.post_merge.mark_task_done") def test_mark_done_failure_propagates( self, mock_mark: MagicMock, mock_post: MagicMock, mock_resolve: MagicMock ) -> None: mock_resolve.return_value = 267 mock_mark.side_effect = requests.HTTPError("500") with pytest.raises(requests.HTTPError): main(["post_merge.py", "GRM-20: fix: bug"])