"""Offline Gitea API and disposable-Git workflow regression tests.""" import importlib.util import json import os from pathlib import Path import subprocess 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 PullRequestTests(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.root = Path(self.temp.name) self.previous = Path.cwd() os.chdir(self.root) self.addCleanup(os.chdir, self.previous) subprocess.run(["git", "init", "--bare", "remote.git"], check=True, capture_output=True) subprocess.run(["git", "init", "-b", "main", "work"], check=True, capture_output=True) os.chdir(self.root / "work") ACTIONS.git("config", "user.name", "test") ACTIONS.git("config", "user.email", "test@example.invalid") ACTIONS.git("remote", "add", "origin", str(self.root / "remote.git")) Path("README.md").write_text("Baseline\n") Path("source.txt").write_text("Source\n") ACTIONS.git("add", ".") ACTIONS.git("commit", "-m", "baseline") ACTIONS.git("push", "origin", "main") Path("README.md").write_text("Pending 28.1.0\n") def test_create_then_reuse_branch_and_paginated_pr(self): api = Mock() api.request.side_effect = [[], {"html_url": "https://git.example/pulls/1"}] ACTIONS.create_pull_request(api, "28.1.0") self.assertEqual("track/gitea-28.1.0", ACTIONS.git("branch", "--show-current")) payload = api.request.call_args.args[2] self.assertEqual("main", payload["base"]) self.assertEqual("track/gitea-28.1.0", payload["head"]) self.assertIn("[PENDING]", payload["body"]) original_head = ACTIONS.git("rev-parse", "HEAD") ACTIONS.git("switch", "main") Path("README.md").write_text("Pending 28.1.0\n") api.reset_mock() api.request.side_effect = [[{"head": {"ref": "unrelated"}}], [{ "head": {"ref": "track/gitea-28.1.0", "repo": {"full_name": "owner/repo"}}, "base": {"ref": "main", "repo": {"full_name": "owner/repo"}}, "html_url": "https://git.example/pulls/1", }]] ACTIONS.create_pull_request(api, "28.1.0") self.assertEqual(["GET", "GET"], [c.args[0] for c in api.request.call_args_list]) self.assertEqual(original_head, ACTIONS.git("rev-parse", "FETCH_HEAD")) Path("README.md").write_text("Different pending content\n") with self.assertRaisesRegex(ValueError, "Existing.*differs"): ACTIONS.create_pull_request(api, "28.1.0") def test_non_documentation_change_is_not_committed(self): Path("source.txt").write_text("Changed source\n") api = Mock() with self.assertRaisesRegex(ValueError, "only Markdown"): ACTIONS.create_pull_request(api, "28.1.0") api.request.assert_not_called() self.assertEqual("baseline", ACTIONS.git("log", "-1", "--format=%s")) if __name__ == "__main__": unittest.main()