From 4e9afd92b729f68123a0455a55049e886a544ba2 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 30 Sep 2026 15:55:53 +0800 Subject: [PATCH] refactor(auth): align with scope-only token resources (campus #657) campus #657 (merged to weekly, bdac1f8c) stops dual-emitting `scopes` from token resources (#648 checklist item 4) and deprecates OAuthToken's expiry_seconds with DeprecationWarning at both intake points (constructor InitVar and from_resource payload key). The client's live paths were already compliant: User.update() sends to_resource() output, which the server validates via from_resource (#656) and which now carries `scope` only; all deserialization goes through from_resource, which still accepts both scope forms; and no client code passes expiry_seconds to OAuthToken. Changes: - campus-suite bumped to weekly head bdac1f8c - contract tests re-pinned to the scope-only emission (PATCH body and GET nested-token fixture) - Credentials.Provider.User.new() stub param renamed expiry_seconds -> expires_in, matching the POST /credentials contract (expires_in preferred; expiry_seconds deprecated, still accepted with a warning; neither -> 422) Refs nyjc-computing/campus#648, nyjc-computing/campus#657 --- campus_python/auth/v1/credentials.py | 2 +- poetry.lock | 2 +- tests/unit/test_oauth_token_contract.py | 21 +++++++++++---------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/campus_python/auth/v1/credentials.py b/campus_python/auth/v1/credentials.py index 24b383c..cd3db08 100644 --- a/campus_python/auth/v1/credentials.py +++ b/campus_python/auth/v1/credentials.py @@ -80,7 +80,7 @@ def get(self) -> campus.model.UserCredentials: def new( self, scopes: "list[str]", - expiry_seconds: int, + expires_in: int, ) -> campus.model.UserCredentials: raise NotImplementedError( "Method not expected to be called on API" diff --git a/poetry.lock b/poetry.lock index 1f7995b..0fe57fe 100644 --- a/poetry.lock +++ b/poetry.lock @@ -142,7 +142,7 @@ werkzeug = "^3.0.0" type = "git" url = "https://github.com/nyjc-computing/campus.git" reference = "weekly" -resolved_reference = "2e46783c9b55be587625aa83156d118419fb730d" +resolved_reference = "bdac1f8c92e6e4ca3e39ee9f32f389b5b39de14e" [[package]] name = "certifi" diff --git a/tests/unit/test_oauth_token_contract.py b/tests/unit/test_oauth_token_contract.py index 38ab073..86fd31f 100644 --- a/tests/unit/test_oauth_token_contract.py +++ b/tests/unit/test_oauth_token_contract.py @@ -9,11 +9,12 @@ - POST /auth/v1/token responses carry standard RFC keys (access_token, token_type, expires_in, scope) which OAuthToken.from_resource maps to campus names; -- GET /credentials/... resources nest the token with both `scope` - (string) and `scopes` (list) during the compat window; +- GET /credentials/... resources nest the token carrying the RFC 6749 + `scope` string only (scope-only emission since campus #657); + from_resource still accepts legacy `scopes` lists; - PATCH /credentials/... bodies are validated server-side via - OAuthToken.from_resource() (campus #656), which accepts the - dual-emitted `scope` string alias, legacy `expiry_seconds`, and bags + OAuthToken.from_resource() (campus #656), which maps the RFC 6749 + `scope` string to `scopes`, accepts legacy `expiry_seconds`, and bags unknown provider keys into provider_fields; User.update() sends the full to_resource() output. """ @@ -38,7 +39,8 @@ } # Exact credentials-resource shape emitted by campus weekly -# (UserCredentials.to_resource() with its nested OAuthToken token). +# (UserCredentials.to_resource() with its nested OAuthToken token; +# scope-only token emission since campus #657). CREDENTIALS_RESOURCE = { "id": "cred1", "created_at": "2026-09-30T06:31:24.582763+00:00", @@ -53,7 +55,6 @@ "token_type": "Bearer", "refresh_token": "rt123", "refresh_token_expires_at": None, - "scopes": ["campus.profile", "campus.identities"], "scope": "campus.profile campus.identities", }, } @@ -138,16 +139,16 @@ def test_update_patches_credentials_endpoint(self): def test_patch_body_passes_server_validation(self): """The sent token payload must pass OAuthToken.from_resource(). - Mirrors the server-side validation (campus #656), which accepts - the RFC 6749 `scope` string alias dual-emitted by to_resource() - alongside `scopes` (campus #650). + Mirrors the server-side validation (campus #656). Token + resources emit `scope` only since campus #657 (deprecation + checklist item 4); from_resource maps it back to `scopes`. """ with patch.dict(os.environ, {"CLIENT_ID": "cid123"}): self.auth.credentials["campus"]["user1"].update(self.token) body = self.client.patch.call_args.kwargs["json"] sent_token = body["token"] self.assertIn("scope", sent_token) - self.assertIn("scopes", sent_token) + self.assertNotIn("scopes", sent_token) validated = campus.model.OAuthToken.from_resource(sent_token) self.assertEqual(validated.id, "tok123") self.assertEqual(validated.scopes, ["campus.profile", "campus.identities"])