diff --git a/campus_python/auth/v1/users.py b/campus_python/auth/v1/users.py index 678007b..1c4217a 100644 --- a/campus_python/auth/v1/users.py +++ b/campus_python/auth/v1/users.py @@ -4,7 +4,7 @@ This mirrors the style used in `clients.py` and `credentials.py` and implements the minimal methods used by the Flask routes: list, new, -and per-user activate/delete/get. +and per-user activate/delete/get/update. """ import campus.model @@ -53,3 +53,20 @@ def get(self) -> campus.model.User: resp = self.client.get(self.make_path()) resp.raise_for_status() return campus.model.User.from_resource(resp.json()) + + def update(self, *, name: str) -> campus.model.User: + """Update the user's display name (campus-cli#42). + + The server PATCH accepts name only: a user's id IS its + email (identity), and activation runs through the + /activate endpoint, so no other field is patchable. + + Args: + name: New display name + + Returns: + The updated User + """ + resp = self.client.patch(self.make_path(), json={"name": name}) + resp.raise_for_status() + return campus.model.User.from_resource(resp.json()) diff --git a/tests/unit/test_auth_users.py b/tests/unit/test_auth_users.py new file mode 100644 index 0000000..3e08720 --- /dev/null +++ b/tests/unit/test_auth_users.py @@ -0,0 +1,62 @@ +"""Contract tests for the auth users resource (campus-cli#42). + +The campus auth server gates user management behind the users:* +management scopes and implements PATCH /users/{id} as rename-only +(a user's id IS its email), so Users.User.update() must send PATCH +with exactly {"name": ...} and no other field. +""" + +import unittest +from unittest.mock import Mock + +import campus.model + +from campus_python.auth.v1 import AuthRoot + +# User resource shape emitted by campus weekly +# (campus/auth/routes/users.py responses). +USER_RESOURCE = { + "id": "user@example.com", + "created_at": "2026-09-30T06:31:24.582763+00:00", + "email": "user@example.com", + "name": "Test User", + "activated_at": None, +} + + +def make_auth() -> tuple[AuthRoot, Mock]: + """Create an AuthRoot backed by a mock JSON client.""" + client = Mock() + return AuthRoot(json_client=client), client + + +class TestUsersUpdatePatchVerb(unittest.TestCase): + """Users.User.update() must send PATCH carrying only the name.""" + + def setUp(self): + self.auth, self.client = make_auth() + response = Mock() + response.json.return_value = USER_RESOURCE + self.client.patch.return_value = response + + def test_update_sends_patch_with_name(self): + user = self.auth.users["user@example.com"].update(name="New Name") + self.client.patch.assert_called_once() + self.client.put.assert_not_called() + args, kwargs = self.client.patch.call_args + self.assertEqual(args[0], "/auth/v1/users/user@example.com/") + self.assertEqual(kwargs["json"], {"name": "New Name"}) + self.assertIsInstance(user, campus.model.User) + self.assertEqual(user.id, "user@example.com") + self.assertEqual(user.name, "Test User") + + def test_update_returns_updated_user(self): + response = Mock() + response.json.return_value = {**USER_RESOURCE, "name": "Renamed"} + self.client.patch.return_value = response + user = self.auth.users["user@example.com"].update(name="Renamed") + self.assertEqual(user.name, "Renamed") + + +if __name__ == "__main__": + unittest.main()