"""Unit: GiteaClient + GiteaForge (PRD forge-native-integration).""" from __future__ import annotations import json import unittest import urllib.error from io import BytesIO from unittest.mock import MagicMock, patch from bot_bottle.contrib.gitea.client import GiteaClient, GiteaForge def _client() -> GiteaClient: return GiteaClient( api_url="https://gitea.example.com/api/v1", owner="didericis", repo="bot-bottle", token="test-token", ) def _resp(body: object, status: int = 200) -> MagicMock: resp = MagicMock() resp.read.return_value = json.dumps(body).encode() if body is not None else b"" resp.status = status resp.__enter__ = lambda s: s # type: ignore resp.__exit__ = MagicMock(return_value=False) return resp def _http_error(code: int, body: str = "") -> urllib.error.HTTPError: return urllib.error.HTTPError( url="http://x", code=code, msg="err", hdrs=None, # type: ignore[arg-type] fp=BytesIO(body.encode()), ) _URLOPEN = "bot_bottle.contrib.gitea.client.urllib.request.urlopen" class TestOrgMembership(unittest.TestCase): def test_member_returns_true_on_2xx(self): with patch(_URLOPEN, return_value=_resp(None, 204)) as m: self.assertTrue(_client().is_org_member("bot-bottle", "alice")) req = m.call_args.args[0] self.assertIn("/orgs/bot-bottle/members/alice", req.full_url) def test_nonmember_returns_false_on_404(self): with patch(_URLOPEN, side_effect=_http_error(404)): self.assertFalse(_client().is_org_member("bot-bottle", "stranger")) def test_other_http_error_raises(self): with patch(_URLOPEN, side_effect=_http_error(403, "forbidden")): with self.assertRaises(RuntimeError) as ctx: _client().is_org_member("bot-bottle", "alice") self.assertIn("403", str(ctx.exception)) class TestForgeReads(unittest.TestCase): def test_read_issue_maps_fields(self): raw = {"number": 17, "title": "Bug", "body": "broken", "state": "open"} with patch(_URLOPEN, return_value=_resp(raw)) as m: issue = GiteaForge(_client()).read_issue(17) self.assertEqual((17, "Bug", "broken", "open"), (issue.number, issue.title, issue.body, issue.state)) self.assertIn("/repos/didericis/bot-bottle/issues/17", m.call_args.args[0].full_url) def test_read_issue_tolerates_null_body(self): raw = {"number": 17, "title": "T", "body": None, "state": "open"} with patch(_URLOPEN, return_value=_resp(raw)): self.assertEqual("", GiteaForge(_client()).read_issue(17).body) def test_read_comments_maps_user_login(self): raw = [ {"id": 1, "user": {"login": "alice"}, "body": "hi"}, {"id": 2, "user": {"login": "bob"}, "body": "yo"}, ] with patch(_URLOPEN, return_value=_resp(raw)): comments = GiteaForge(_client()).read_comments(17) self.assertEqual(["alice", "bob"], [c.user for c in comments]) self.assertEqual([1, 2], [c.id for c in comments]) class TestForgeWrites(unittest.TestCase): def test_post_comment_payload_and_url(self): with patch(_URLOPEN, return_value=_resp(None, 201)) as m: GiteaForge(_client()).post_comment(17, "done ✓") req = m.call_args.args[0] self.assertEqual("POST", req.method) self.assertIn("/repos/didericis/bot-bottle/issues/17/comments", req.full_url) self.assertEqual("done ✓", json.loads(req.data)["body"]) def test_update_description_patches_issue(self): with patch(_URLOPEN, return_value=_resp(None, 200)) as m: GiteaForge(_client()).update_description(17, "edited") req = m.call_args.args[0] self.assertEqual("PATCH", req.method) self.assertTrue(req.full_url.endswith("/issues/17")) self.assertEqual("edited", json.loads(req.data)["body"]) def test_auth_header_sent(self): with patch(_URLOPEN, return_value=_resp(None, 201)) as m: GiteaForge(_client()).post_comment(17, "x") self.assertEqual("token test-token", m.call_args.args[0].headers["Authorization"]) class TestPRHelpers(unittest.TestCase): def test_get_pr_for_issue_returns_number_when_issue_is_pr(self): raw = {"number": 18, "pull_request": {"merged": False}} with patch(_URLOPEN, return_value=_resp(raw)): self.assertEqual(18, GiteaForge(_client()).get_pr_for_issue(18)) def test_get_pr_for_issue_none_for_plain_issue(self): raw = {"number": 17, "pull_request": None} with patch(_URLOPEN, return_value=_resp(raw)): self.assertIsNone(GiteaForge(_client()).get_pr_for_issue(17)) def test_is_pr_open_true_when_state_open(self): with patch(_URLOPEN, return_value=_resp({"state": "open"})): self.assertTrue(GiteaForge(_client()).is_pr_open(18)) def test_is_pr_open_false_when_closed(self): with patch(_URLOPEN, return_value=_resp({"state": "closed"})): self.assertFalse(GiteaForge(_client()).is_pr_open(18)) def test_read_pr_maps_fields_including_merged(self): raw = {"number": 18, "title": "Fix", "body": "patch", "state": "closed", "merged": True} with patch(_URLOPEN, return_value=_resp(raw)) as m: pr = GiteaForge(_client()).read_pr(18) self.assertEqual((18, "Fix", "patch", "closed", True), (pr.number, pr.title, pr.body, pr.state, pr.merged)) self.assertIn("/repos/didericis/bot-bottle/pulls/18", m.call_args.args[0].full_url) def test_read_pr_merged_defaults_false(self): with patch(_URLOPEN, return_value=_resp({"number": 18, "state": "open"})): self.assertFalse(GiteaForge(_client()).read_pr(18).merged) if __name__ == "__main__": unittest.main()