diff --git a/src/colony_sdk/client.py b/src/colony_sdk/client.py index 88e31a1..7427c67 100644 --- a/src/colony_sdk/client.py +++ b/src/colony_sdk/client.py @@ -441,6 +441,15 @@ def join_colony(self, colony: str) -> dict: colony_id = COLONIES.get(colony, colony) return self._raw_request("POST", f"/colonies/{colony_id}/join") + def leave_colony(self, colony: str) -> dict: + """Leave a colony. + + Args: + colony: Colony name (e.g. ``"general"``, ``"findings"``) or UUID. + """ + colony_id = COLONIES.get(colony, colony) + return self._raw_request("POST", f"/colonies/{colony_id}/leave") + # ── Unread messages ────────────────────────────────────────────── def get_unread_count(self) -> dict: diff --git a/tests/test_api_methods.py b/tests/test_api_methods.py index db6ef6c..df67aa2 100644 --- a/tests/test_api_methods.py +++ b/tests/test_api_methods.py @@ -803,6 +803,28 @@ def test_join_colony_by_uuid(self, mock_urlopen: MagicMock) -> None: req = _last_request(mock_urlopen) assert req.full_url == f"{BASE}/colonies/{custom_uuid}/join" + @patch("colony_sdk.client.urlopen") + def test_leave_colony_by_name(self, mock_urlopen: MagicMock) -> None: + mock_urlopen.return_value = _mock_response({"left": True}) + client = _authed_client() + + client.leave_colony("general") + + req = _last_request(mock_urlopen) + assert req.get_method() == "POST" + assert req.full_url == f"{BASE}/colonies/{COLONIES['general']}/leave" + + @patch("colony_sdk.client.urlopen") + def test_leave_colony_by_uuid(self, mock_urlopen: MagicMock) -> None: + mock_urlopen.return_value = _mock_response({"left": True}) + client = _authed_client() + custom_uuid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + + client.leave_colony(custom_uuid) + + req = _last_request(mock_urlopen) + assert req.full_url == f"{BASE}/colonies/{custom_uuid}/leave" + # --------------------------------------------------------------------------- # Webhooks diff --git a/tests/test_integration_colonies.py b/tests/test_integration_colonies.py new file mode 100644 index 0000000..77ae958 --- /dev/null +++ b/tests/test_integration_colonies.py @@ -0,0 +1,63 @@ +"""Integration tests for join/leave colony endpoints. + +These tests hit the real Colony API and require a valid API key. + +Run with: + COLONY_TEST_API_KEY=col_xxx pytest tests/test_integration_colonies.py -v + +Skipped automatically when the env var is not set. +""" + +import contextlib +import os +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +from colony_sdk import ColonyAPIError, ColonyClient + +API_KEY = os.environ.get("COLONY_TEST_API_KEY") +# test-posts colony UUID on thecolony.cc +TEST_POSTS_COLONY_ID = "cb4d2ed0-0425-4d26-8755-d4bfd0130c1d" + +pytestmark = pytest.mark.skipif(not API_KEY, reason="set COLONY_TEST_API_KEY to run") + + +@pytest.fixture +def client() -> ColonyClient: + assert API_KEY is not None + return ColonyClient(API_KEY) + + +class TestColoniesIntegration: + def test_join_leave_lifecycle(self, client: ColonyClient) -> None: + """Join a colony, then leave it.""" + # Ensure we start outside the colony + with contextlib.suppress(ColonyAPIError): + client.leave_colony(TEST_POSTS_COLONY_ID) + + # Join + result = client.join_colony(TEST_POSTS_COLONY_ID) + assert "member" in str(result).lower() or result == {} or isinstance(result, dict) + + try: + # Joining again should fail + with pytest.raises(ColonyAPIError) as exc_info: + client.join_colony(TEST_POSTS_COLONY_ID) + assert exc_info.value.status == 409 + finally: + # Leave (cleanup) + client.leave_colony(TEST_POSTS_COLONY_ID) + + def test_leave_not_member_raises(self, client: ColonyClient) -> None: + """Leaving a colony you're not in should raise an error.""" + # Ensure we're not a member + with contextlib.suppress(ColonyAPIError): + client.leave_colony(TEST_POSTS_COLONY_ID) + + with pytest.raises(ColonyAPIError) as exc_info: + client.leave_colony(TEST_POSTS_COLONY_ID) + assert exc_info.value.status in (404, 409)