Skip to content

Commit c36db6e

Browse files
ColonistOneclaude
andcommitted
test: add unit coverage + focused integration tests for safety wrappers
Addresses CI failures on PR #62: - codecov/patch failed (50% diff hit): the new methods had only callable() smoke tests in test_client.py, which don't execute the method bodies. Add proper request-mocking unit tests in test_api_methods.py (sync) and test_async_client.py (async) matching the existing test_follow / test_unfollow shape. - lint failed (ruff SIM108): collapse the if/else block in tests/integration/test_safety.py::_target_in_blocked into a ternary. Also adds focused integration tests per individual method so each of block_user / list_blocked / unblock_user has its own named test on top of the existing block-then-unblock round-trip: - TestBlockUser::test_block_user_adds_to_blocked_list - TestListBlocked::test_list_blocked_returns_collection - TestUnblockUser::test_unblock_user_removes_from_blocked_list - TestReportSmoke::test_report_methods_are_present_on_live_client Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 40b3676 commit c36db6e

3 files changed

Lines changed: 243 additions & 5 deletions

File tree

tests/integration/test_safety.py

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@ def _target_in_blocked(blocked_response: object, target_id: str) -> bool:
2525
"""
2626
if isinstance(blocked_response, dict):
2727
items = blocked_response.get("items")
28-
if isinstance(items, list):
29-
members = items
30-
else:
31-
members = []
28+
members = items if isinstance(items, list) else []
3229
elif isinstance(blocked_response, list):
3330
members = blocked_response
3431
else:
@@ -41,7 +38,52 @@ def _target_in_blocked(blocked_response: object, target_id: str) -> bool:
4138
return False
4239

4340

44-
class TestBlock:
41+
class TestBlockUser:
42+
"""Focused tests for ``block_user`` against the live API."""
43+
44+
def test_block_user_adds_to_blocked_list(self, client: ColonyClient, second_me: dict) -> None:
45+
target_id = second_me["id"]
46+
47+
# Best-effort cleanup from a previous failed run.
48+
with contextlib.suppress(ColonyAPIError):
49+
client.unblock_user(target_id)
50+
51+
try:
52+
client.block_user(target_id)
53+
assert _target_in_blocked(client.list_blocked(), target_id)
54+
finally:
55+
with contextlib.suppress(ColonyAPIError):
56+
client.unblock_user(target_id)
57+
58+
59+
class TestListBlocked:
60+
"""Focused tests for ``list_blocked`` against the live API."""
61+
62+
def test_list_blocked_returns_collection(self, client: ColonyClient) -> None:
63+
result = client.list_blocked()
64+
# The endpoint should return either {items: [...]} or a list — both
65+
# shapes are accepted by the SDK type. Validate it's one of them.
66+
if isinstance(result, dict):
67+
assert "items" in result or "total" in result
68+
else:
69+
assert isinstance(result, list)
70+
71+
72+
class TestUnblockUser:
73+
"""Focused tests for ``unblock_user`` against the live API."""
74+
75+
def test_unblock_user_removes_from_blocked_list(self, client: ColonyClient, second_me: dict) -> None:
76+
target_id = second_me["id"]
77+
78+
# Make sure the user is currently blocked.
79+
with contextlib.suppress(ColonyAPIError):
80+
client.block_user(target_id)
81+
82+
client.unblock_user(target_id)
83+
assert not _target_in_blocked(client.list_blocked(), target_id)
84+
85+
86+
class TestBlockUnblockRoundTrip:
4587
def test_block_then_unblock(self, client: ColonyClient, second_me: dict) -> None:
4688
target_id = second_me["id"]
4789

@@ -83,3 +125,20 @@ def test_unblock_when_not_blocked_raises(self, client: ColonyClient, second_me:
83125

84126
with raises_status(404, 409):
85127
client.unblock_user(target_id)
128+
129+
130+
class TestReportSmoke:
131+
"""Smoke check that the report_* methods are reachable.
132+
133+
We intentionally do NOT submit a real report against the secondary
134+
account in CI — that would generate operator-side moderation noise
135+
on every run. The unit tests in ``tests/test_api_methods.py``
136+
exercise the request construction; this just confirms the methods
137+
are wired on the live client without invoking them.
138+
"""
139+
140+
def test_report_methods_are_present_on_live_client(self, client: ColonyClient) -> None:
141+
assert callable(client.report_user)
142+
assert callable(client.report_message)
143+
assert callable(client.report_post)
144+
assert callable(client.report_comment)

tests/test_api_methods.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,98 @@ def test_unfollow(self, mock_urlopen: MagicMock) -> None:
10221022
assert req.full_url == f"{BASE}/users/u1/follow"
10231023

10241024

1025+
# ---------------------------------------------------------------------------
1026+
# Safety / Moderation
1027+
# ---------------------------------------------------------------------------
1028+
1029+
1030+
class TestSafety:
1031+
@patch("colony_sdk.client.urlopen")
1032+
def test_block_user(self, mock_urlopen: MagicMock) -> None:
1033+
mock_urlopen.return_value = _mock_response({"blocked": True})
1034+
client = _authed_client()
1035+
1036+
client.block_user("u1")
1037+
1038+
req = _last_request(mock_urlopen)
1039+
assert req.get_method() == "POST"
1040+
assert req.full_url == f"{BASE}/users/u1/block"
1041+
1042+
@patch("colony_sdk.client.urlopen")
1043+
def test_unblock_user(self, mock_urlopen: MagicMock) -> None:
1044+
mock_urlopen.return_value = _mock_response({"blocked": False})
1045+
client = _authed_client()
1046+
1047+
client.unblock_user("u1")
1048+
1049+
req = _last_request(mock_urlopen)
1050+
assert req.get_method() == "DELETE"
1051+
assert req.full_url == f"{BASE}/users/u1/block"
1052+
1053+
@patch("colony_sdk.client.urlopen")
1054+
def test_list_blocked(self, mock_urlopen: MagicMock) -> None:
1055+
mock_urlopen.return_value = _mock_response({"items": [], "total": 0})
1056+
client = _authed_client()
1057+
1058+
client.list_blocked()
1059+
1060+
req = _last_request(mock_urlopen)
1061+
assert req.get_method() == "GET"
1062+
assert req.full_url == f"{BASE}/users/me/blocked"
1063+
1064+
@patch("colony_sdk.client.urlopen")
1065+
def test_report_user(self, mock_urlopen: MagicMock) -> None:
1066+
mock_urlopen.return_value = _mock_response({"id": "r1", "status": "received"})
1067+
client = _authed_client()
1068+
1069+
client.report_user("u1", reason="spam")
1070+
1071+
req = _last_request(mock_urlopen)
1072+
assert req.get_method() == "POST"
1073+
assert req.full_url == f"{BASE}/reports"
1074+
body = _last_body(mock_urlopen)
1075+
assert body == {"target_type": "user", "target_id": "u1", "reason": "spam"}
1076+
1077+
@patch("colony_sdk.client.urlopen")
1078+
def test_report_message(self, mock_urlopen: MagicMock) -> None:
1079+
mock_urlopen.return_value = _mock_response({"id": "r1", "status": "received"})
1080+
client = _authed_client()
1081+
1082+
client.report_message("m1", reason="abuse")
1083+
1084+
req = _last_request(mock_urlopen)
1085+
assert req.get_method() == "POST"
1086+
assert req.full_url == f"{BASE}/reports"
1087+
body = _last_body(mock_urlopen)
1088+
assert body == {"target_type": "message", "target_id": "m1", "reason": "abuse"}
1089+
1090+
@patch("colony_sdk.client.urlopen")
1091+
def test_report_post(self, mock_urlopen: MagicMock) -> None:
1092+
mock_urlopen.return_value = _mock_response({"id": "r1", "status": "received"})
1093+
client = _authed_client()
1094+
1095+
client.report_post("p1", reason="low-effort")
1096+
1097+
req = _last_request(mock_urlopen)
1098+
assert req.get_method() == "POST"
1099+
assert req.full_url == f"{BASE}/reports"
1100+
body = _last_body(mock_urlopen)
1101+
assert body == {"target_type": "post", "target_id": "p1", "reason": "low-effort"}
1102+
1103+
@patch("colony_sdk.client.urlopen")
1104+
def test_report_comment(self, mock_urlopen: MagicMock) -> None:
1105+
mock_urlopen.return_value = _mock_response({"id": "r1", "status": "received"})
1106+
client = _authed_client()
1107+
1108+
client.report_comment("c1", reason="harassment")
1109+
1110+
req = _last_request(mock_urlopen)
1111+
assert req.get_method() == "POST"
1112+
assert req.full_url == f"{BASE}/reports"
1113+
body = _last_body(mock_urlopen)
1114+
assert body == {"target_type": "comment", "target_id": "c1", "reason": "harassment"}
1115+
1116+
10251117
# ---------------------------------------------------------------------------
10261118
# Notifications
10271119
# ---------------------------------------------------------------------------

tests/test_async_client.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,93 @@ def handler(request: httpx.Request) -> httpx.Response:
10091009
await client.unfollow("u2")
10101010
assert seen["method"] == "DELETE"
10111011

1012+
async def test_block_user(self) -> None:
1013+
seen: dict = {}
1014+
1015+
def handler(request: httpx.Request) -> httpx.Response:
1016+
seen["url"] = str(request.url)
1017+
seen["method"] = request.method
1018+
return _json_response({"blocked": True})
1019+
1020+
client = _make_client(handler)
1021+
await client.block_user("u2")
1022+
assert "/users/u2/block" in seen["url"]
1023+
assert seen["method"] == "POST"
1024+
1025+
async def test_unblock_user(self) -> None:
1026+
seen: dict = {}
1027+
1028+
def handler(request: httpx.Request) -> httpx.Response:
1029+
seen["url"] = str(request.url)
1030+
seen["method"] = request.method
1031+
return _json_response({"blocked": False})
1032+
1033+
client = _make_client(handler)
1034+
await client.unblock_user("u2")
1035+
assert "/users/u2/block" in seen["url"]
1036+
assert seen["method"] == "DELETE"
1037+
1038+
async def test_list_blocked(self) -> None:
1039+
seen: dict = {}
1040+
1041+
def handler(request: httpx.Request) -> httpx.Response:
1042+
seen["url"] = str(request.url)
1043+
seen["method"] = request.method
1044+
return _json_response({"items": [], "total": 0})
1045+
1046+
client = _make_client(handler)
1047+
await client.list_blocked()
1048+
assert "/users/me/blocked" in seen["url"]
1049+
assert seen["method"] == "GET"
1050+
1051+
async def test_report_user(self) -> None:
1052+
seen: dict = {}
1053+
1054+
def handler(request: httpx.Request) -> httpx.Response:
1055+
seen["url"] = str(request.url)
1056+
seen["method"] = request.method
1057+
seen["body"] = json.loads(request.content.decode())
1058+
return _json_response({"id": "r1", "status": "received"})
1059+
1060+
client = _make_client(handler)
1061+
await client.report_user("u2", reason="spam")
1062+
assert "/reports" in seen["url"]
1063+
assert seen["method"] == "POST"
1064+
assert seen["body"] == {"target_type": "user", "target_id": "u2", "reason": "spam"}
1065+
1066+
async def test_report_message(self) -> None:
1067+
seen: dict = {}
1068+
1069+
def handler(request: httpx.Request) -> httpx.Response:
1070+
seen["body"] = json.loads(request.content.decode())
1071+
return _json_response({"id": "r1", "status": "received"})
1072+
1073+
client = _make_client(handler)
1074+
await client.report_message("m1", reason="abuse")
1075+
assert seen["body"] == {"target_type": "message", "target_id": "m1", "reason": "abuse"}
1076+
1077+
async def test_report_post(self) -> None:
1078+
seen: dict = {}
1079+
1080+
def handler(request: httpx.Request) -> httpx.Response:
1081+
seen["body"] = json.loads(request.content.decode())
1082+
return _json_response({"id": "r1", "status": "received"})
1083+
1084+
client = _make_client(handler)
1085+
await client.report_post("p1", reason="low-effort")
1086+
assert seen["body"] == {"target_type": "post", "target_id": "p1", "reason": "low-effort"}
1087+
1088+
async def test_report_comment(self) -> None:
1089+
seen: dict = {}
1090+
1091+
def handler(request: httpx.Request) -> httpx.Response:
1092+
seen["body"] = json.loads(request.content.decode())
1093+
return _json_response({"id": "r1", "status": "received"})
1094+
1095+
client = _make_client(handler)
1096+
await client.report_comment("c1", reason="harassment")
1097+
assert seen["body"] == {"target_type": "comment", "target_id": "c1", "reason": "harassment"}
1098+
10121099
async def test_join_colony(self) -> None:
10131100
seen: dict = {}
10141101

0 commit comments

Comments
 (0)