From 0eff045c56201f72cbe6709ecefc180e45f97b4b Mon Sep 17 00:00:00 2001 From: nycomp Date: Sun, 4 Oct 2026 00:01:10 +0800 Subject: [PATCH] fix(api): CircleMembers targets /members/add|remove with access_value; list returns flat mapping CircleMembers.add()/remove() POSTed/DELETEed the bare /members path, which werkzeug redirects to GET-only /members/ (405), and add() sent the payload key "access" where the API expects "access_value". list() unwrapped a "members" key that the API does not send (KeyError); the GET now targets the canonical /members/ route and returns the flat {member_id: access_value} mapping directly. Fixes #42 --- campus_python/api/v1/circles.py | 13 +++++++------ tests/unit/test_nested_resources.py | 27 +++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/campus_python/api/v1/circles.py b/campus_python/api/v1/circles.py index 3302498..2eb243f 100644 --- a/campus_python/api/v1/circles.py +++ b/campus_python/api/v1/circles.py @@ -73,22 +73,23 @@ class CircleMembers(Resource): """Campus API Circle Members resource.""" path = "members" - def list(self) -> "list[dict[str, int]]": - resp = self.client.get(self.make_path()) + def list(self) -> dict[str, int]: + """Return the circle's {member_id: access_value} mapping.""" + resp = self.client.get(self.make_path(end_slash=True)) resp.raise_for_status() - return resp.json()["members"] + return resp.json() def add(self, member_id: str, access_value: int) -> None: resp = self.client.post( - self.make_path(), - json={"member_id": member_id, "access": access_value} + self.make_path("add"), + json={"member_id": member_id, "access_value": access_value} ) resp.raise_for_status() return None def remove(self, member_id: str) -> None: resp = self.client.delete( - self.make_path(), + self.make_path("remove"), json={"member_id": member_id} ) resp.raise_for_status() diff --git a/tests/unit/test_nested_resources.py b/tests/unit/test_nested_resources.py index 732b67c..6049345 100644 --- a/tests/unit/test_nested_resources.py +++ b/tests/unit/test_nested_resources.py @@ -1,4 +1,4 @@ -"""Regression tests for nested resource paths (issue #38). +"""Regression tests for nested resource paths (issues #38, #42). Nested resources must be constructed with their path part so that their paths extend the parent resource's path instead of collapsing onto it. @@ -11,7 +11,9 @@ - POST /api/v1/assignments//links - GET /api/v1/timetable//entries - GET /api/v1/timetable//metadata -- GET /api/v1/circles//members +- GET /api/v1/circles//members/ +- POST /api/v1/circles//members/add +- DELETE /api/v1/circles//members/remove """ import unittest @@ -139,10 +141,27 @@ def test_metadata_get_gets_metadata_endpoint(self): ) def test_members_list_gets_members_endpoint(self): - self.client.get.return_value.json.return_value = {"members": {}} + """members.list() must GET the canonical /members/ route (#42).""" + self.client.get.return_value.json.return_value = {"cir-2": 7} self.api.circles["cir-1"].members.list() self.client.get.assert_called_once_with( - "/api/v1/circles/cir-1/members" + "/api/v1/circles/cir-1/members/" + ) + + def test_members_add_posts_to_members_add_endpoint(self): + """members.add() must POST the /members/add route with access_value (#42).""" + self.api.circles["cir-1"].members.add(member_id="cir-2", access_value=7) + self.client.post.assert_called_once_with( + "/api/v1/circles/cir-1/members/add", + json={"member_id": "cir-2", "access_value": 7}, + ) + + def test_members_remove_deletes_members_remove_endpoint(self): + """members.remove() must DELETE the /members/remove route (#42).""" + self.api.circles["cir-1"].members.remove(member_id="cir-2") + self.client.delete.assert_called_once_with( + "/api/v1/circles/cir-1/members/remove", + json={"member_id": "cir-2"}, ) def test_assignments_list_passes_query_argument(self):