Skip to content

fix: bug sweep — timetable envelopes, session cleanup, oauth device flow - #70

Merged
nycomp merged 1 commit into
mainfrom
fix/bug-sweep-oct
Oct 3, 2026
Merged

nycomp merged 1 commit into
mainfrom
fix/bug-sweep-oct

Conversation

@nycomp

@nycomp nycomp commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Self-contained bug sweep verified against campus weekly (74738e1). No API redesign, no model changes, no impact on other services (campus-cli has its own device-flow implementation and does not import campus_python.auth.v1.oauth).

Bugs fixed

api (timetable) — closes #54

  • Timetables.list() unwraps the {"data": [...]} envelope; previously iterated the envelope dict and crashed with TypeError: string indices must be integers.
  • Timetables.new() unwraps the {"data": <resource>} create envelope (route returns 200 + envelope, not 201).
  • Timetable.get() unwraps the {"timetable": <resource>} envelope and parses into campus.model.Timetable; previously returned the raw envelope while declaring campus.model.Timetable.
  • Timetable.Metadata.get() parses TimetableMetadata (was Timetable.from_resource against its declared return type).
  • Timetable.Metadata.update() implemented — the route is live server-side and requires start_date + end_date together; was a NotImplementedError stub.

auth — closes #59

  • CampusSessions._session_key now yields campus_session_id; path.split("/")[-1] on "sessions/campus/" produced "", storing login state under the generic _session_id key (server provider convention: f"{PROVIDER}_session_id", campus/auth/provider.py:62).
  • Session.finalize() / Login.revoke() clear the Flask key with pop(key, None) — a missing key (expired cookie, worker restart, lost session) no longer turns an already-successful remote op into a 500 KeyError.
  • OAuth device flow posts the real /auth/v1/oauth/device_authorize, /auth/v1/oauth/token, /auth/v1/oauth/device/authorize routes; the old absolute /oauth/... paths missed the /auth/v1 prefix and 404 (auth app sets strict_slashes = True).
  • poll_for_token() maps RFC 8628 error responses to AuthenticationError(details={"oauth_error": ...}) — readable via the existing APIError.oauth_error property. The old call passed error_code=, which is not an APIError.__init__ parameter, so the intended error type raised TypeError instead.
  • ClientAccess.get() requests /access/ with its trailing slash (rule /<client_id>/access/ + strict_slashes; the slash-less path 404'd on real deployments).
  • Auth.finalize() step 4 ("Ensure user exists") actually calls .get() now — it previously constructed the resource object as a no-op. Safe: the server provisions the user during verify_login before the callback.
  • LoginSessions.new() parses the create response once instead of twice.

Tests

  • New: test_timetable.py (envelopes + metadata update), test_oauth_device_flow.py (paths + RFC 8628 error mapping), test_sessions.py (campus_session_id key, finalize/revoke without session key), test_token_refresh.py (refreshed token is returned, not the stale one).
  • Updated: test_auth_clients.py (access trailing slash), test_nested_resources.py (metadata-get pins TimetableMetadata).

146 passed (was 124).

Also fixed in passing

Campus._get_token_from_session now returns the refreshed token — the refresh-token grant rotates server-side, but the method handed back the stale user_creds.token it had just replaced.

Not in scope (filed separately / existing issues)

Self-contained fixes verified against campus weekly (74738e1); no API
or model changes, no other-service impact.

api:
- Timetables.list() unwraps the {"data": [...]} envelope instead of
  iterating it (crash: TypeError on string keys) — closes #54
- Timetables.new() unwraps the {"data": ...} create envelope
- Timetable.get() unwraps the {"timetable": ...} envelope and parses
  into campus.model.Timetable (was returning the raw envelope dict)
- Timetable.Metadata.get() parses TimetableMetadata (was Timetable)
- Timetable.Metadata.update() implemented: PATCH start_date+end_date
  (both required by the route); was a NotImplementedError stub

auth:
- CampusSessions._session_key() yields "campus_session_id", not
  "_session_id" (trailing-slash path split gave an empty provider;
  matches the server-side provider session-key convention)
- Session.finalize() and Login.revoke() clear the Flask session key
  with pop(key, None) — a missing key no longer raises KeyError/500
  after the remote op succeeded — closes #59
- OAuth device flow posts the real /auth/v1/oauth/... routes
  (device_authorize, token, device/authorize); absolute /oauth/...
  paths missed the /auth/v1 prefix and 404 (auth sets strict_slashes)
- poll_for_token() maps RFC 8628 errors to AuthenticationError with
  details={"oauth_error": ...} — the old error_code= kwarg was not an
  APIError parameter and raised TypeError instead
- ClientAccess.get() requests /access/ with its trailing slash (route
  rule + strict_slashes; slash-less path 404'd)
- Auth.finalize() step 4 actually verifies the user exists (.get());
  it previously constructed the resource object as a no-op
- LoginSessions.new() parses the create response once

Regression tests: test_timetable.py, test_oauth_device_flow.py,
test_sessions.py, test_token_refresh.py; access-slash cases added to
test_auth_clients.py; metadata-get test moved to TimetableMetadata.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants