f211ece6bf
CI runs `pyright .` over the whole repo including tests; the earlier run only checked the source paths. The test helpers used `**over` dict-splat into typed constructors, which pyright strict rejects. - forge_state: build a typed ForgeState base and dataclasses.replace(**over) - provenance: explicit typed keyword params instead of a **over dict - resume: _launch_kwargs returns dict[str, Any] (copy call_args.kwargs) - forge_base: assert PermissionError in __mro__ (avoids always-true issubclass) - client: annotate _resp body param; type: ignore the mock __enter__ lambda pyright . now 0 errors; 47 tests still pass; pylint 9.97/10. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WL77TgFxKbs3cidGMG9dz7
132 lines
5.1 KiB
Python
132 lines
5.1 KiB
Python
"""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))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|