"""Offline Gitea API and issue-reminder regression tests.""" import importlib.util import json from pathlib import Path import tempfile import unittest from unittest.mock import Mock from urllib.error import HTTPError SPEC = importlib.util.spec_from_file_location("gitea_actions", Path(__file__).with_name("gitea_actions.py")) ACTIONS = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(ACTIONS) class APITests(unittest.TestCase): def test_instance_url_token_and_raw_asset_request(self): api = ACTIONS.GiteaAPI("https://git.example/subpath/", "owner/repo", "test-token") response = Mock() response.__enter__ = Mock(return_value=response) response.__exit__ = Mock(return_value=False) response.read.return_value = b'{"id": 1}' api.opener = Mock() api.opener.open.return_value = response self.assertEqual({"id": 1}, api.request("POST", "/releases/1/assets?name=test.zip", b"archive", "application/octet-stream")) request = api.opener.open.call_args.args[0] self.assertEqual("https://git.example/subpath/api/v1/repos/owner/repo/releases/1/assets?name=test.zip", request.full_url) self.assertEqual("token test-token", request.get_header("Authorization")) self.assertEqual(b"archive", request.data) self.assertEqual("application/octet-stream", request.get_header("Content-type")) def test_only_explicit_404_is_missing_and_redirects_are_refused(self): api = ACTIONS.GiteaAPI("https://git.example", "owner/repo", "test-token") api.opener = Mock() for status in (401, 403, 500, 302): api.opener.open.side_effect = HTTPError(api.base, status, "error", {}, None) with self.assertRaisesRegex(RuntimeError, f"HTTP {status}"): api.request("GET", "/releases/tags/v28.0.0", allow_missing=True) api.opener.open.side_effect = HTTPError(api.base, 404, "missing", {}, None) self.assertIsNone(api.request("GET", "/releases/tags/v28.0.0", allow_missing=True)) self.assertIsNone(ACTIONS.NoRedirects().redirect_request(None, None, 302, "", {}, "https://elsewhere.example")) def test_rejects_invalid_configuration(self): for server, repository, token in [("http://git.example", "owner/repo", "x"), ("https://user:password@git.example", "owner/repo", "x"), ("https://git.example", "../owner/repo", "x"), ("https://git.example", "owner/repo", "")]: with self.assertRaises(ValueError): ACTIONS.GiteaAPI(server, repository, token) class ReleaseTests(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.root = Path(self.temp.name) (self.root / "gitea.lock.json").write_text(json.dumps({"tag": "v28.0.0"})) notes = self.root / ".github/release-notes/v28.0.0.md" notes.parent.mkdir(parents=True) notes.write_text("Reviewed notes", encoding="utf-8") (self.root / "dist").mkdir() for ext in (".zip", ".tar.gz"): (self.root / "dist" / ("gitea-mail-templates-v28.0.0" + ext)).write_bytes(b"archive") def test_existing_release_is_not_modified(self): api = Mock() api.request.return_value = {"id": 5} with self.assertRaisesRegex(ValueError, "already exists"): ACTIONS.publish_release(api, "v28.0.0", self.root) self.assertEqual(["GET"], [call.args[0] for call in api.request.call_args_list]) def test_publish_only_after_both_uploads_succeed(self): api = Mock() api.request.side_effect = [None, {"id": 5}, {"id": 6}, {"id": 7}, {"html_url": "https://git.example/release"}] ACTIONS.publish_release(api, "v28.0.0", self.root) calls = api.request.call_args_list self.assertEqual(["GET", "POST", "POST", "POST", "PATCH"], [c.args[0] for c in calls]) self.assertTrue(calls[1].args[2]["draft"]) self.assertEqual("Reviewed notes", calls[1].args[2]["body"]) self.assertEqual("/releases/5/assets?name=gitea-mail-templates-v28.0.0.zip", calls[2].args[1]) self.assertEqual("/releases/5/assets?name=gitea-mail-templates-v28.0.0.tar.gz", calls[3].args[1]) self.assertEqual({"draft": False}, calls[4].args[2]) def test_failed_upload_leaves_draft_unpublished(self): api = Mock() api.request.side_effect = [None, {"id": 5}, RuntimeError("upload failed")] with self.assertRaisesRegex(RuntimeError, "upload failed"): ACTIONS.publish_release(api, "v28.0.0", self.root) self.assertNotIn("PATCH", [c.args[0] for c in api.request.call_args_list]) def test_bad_version_or_missing_archive_fails_before_api_call(self): api = Mock() for version in ("v28.0.1", "v28.0.0-rc1", "../../x"): with self.assertRaises(ValueError): ACTIONS.publish_release(api, version, self.root) (self.root / "dist/gitea-mail-templates-v28.0.0.zip").unlink() with self.assertRaisesRegex(ValueError, "archive"): ACTIONS.publish_release(api, "v28.0.0", self.root) api.request.assert_not_called() class IssueTests(unittest.TestCase): def test_creates_release_documentation_issue(self): api = Mock() api.request.side_effect = [[], {"html_url": "https://git.example/issues/1"}] ACTIONS.create_issue(api, "v28.1.0") calls = api.request.call_args_list self.assertEqual(["GET", "POST"], [c.args[0] for c in calls]) self.assertEqual("/issues", calls[-1].args[1]) payload = calls[-1].args[2] self.assertIn("", payload["body"]) self.assertIn("Documentation is maintained manually", payload["body"]) def test_reuses_closed_issue_on_later_page_and_ignores_pull_requests(self): api = Mock() marker = "" api.request.side_effect = [[{"body": marker, "pull_request": {"url": "pr"}}], [{"body": marker, "state": "closed", "title": "Renamed", "html_url": "https://git.example/issues/1"}]] ACTIONS.create_issue(api, "v28.1.0") self.assertEqual(["GET", "GET"], [c.args[0] for c in api.request.call_args_list]) self.assertIn("state=all", api.request.call_args_list[0].args[1]) self.assertIn("page=2", api.request.call_args_list[1].args[1]) def test_other_release_versions_do_not_suppress_reminder(self): api = Mock() api.request.side_effect = [[{"body": ""}], [], {"html_url": "https://git.example/issues/2"}] ACTIONS.create_issue(api, "v28.1.0") payload = api.request.call_args.args[2] self.assertIn("template-release:v28.1.0", payload["body"]) self.assertIn("READMEs", payload["body"]) def test_invalid_versions_fail_before_api_call(self): api = Mock() for version in ("28.1.0", "v28.1.0-rc1", "../../x"): with self.assertRaises(ValueError): ACTIONS.create_issue(api, version) api.request.assert_not_called() if __name__ == "__main__": unittest.main()