Skip to content

refactor(identity): layer the module into router/service/repository/github - #61

Merged
mariuspruvot merged 1 commit into
mainfrom
refactor/identity-layering
Jul 31, 2026
Merged

refactor(identity): layer the module into router/service/repository/github#61
mariuspruvot merged 1 commit into
mainfrom
refactor/identity-layering

Conversation

@mariuspruvot

Copy link
Copy Markdown
Owner

First step of the layering work: identity becomes the reference shape for every module.

identity/
  router.py      thin — validate, call one use case, shape the response
  service.py     use cases, no SQL, no HTTP
  repository.py  every query against github_users
  github.py      typed boundary (GitHubOAuthToken, GitHubUserProfile)

What changed

  • No more raw JSON crossing the domain. exchange_code_for_token and fetch_github_user returned bare dict; callers indexed them blindly (token_data["access_token"]). They now return validated models, so a GitHub response change surfaces as ExternalServiceError at the boundary instead of a KeyError three layers in.
  • get_user_stats returned a dict built from SQL over ContainerSession — a table identity does not own. The queries moved to a new container/repository.py (typed StatusCounts/DailyCount results) and the service returns UserStatsResponse.
  • create_token_pair returned an untyped 2-tuple; it now returns a TokenPair.
  • Router is thin: the OAuth callback went from inline orchestration (exchange → fetch → upsert → mint) to one authenticate_with_code call. Inline UnauthorizedError imports hoisted; cookie policy in one helper.

Bug fixed along the way

logout hardcoded secure=True while every other cookie call computes it from the environment — over plain HTTP in local dev the browser kept the refresh cookie, so logout did nothing. It also had no rate limit, unlike every other route in the file.

Verification

  • 317 tests pass (was 308); identity tests rewritten off unittest.mock onto httpx.MockTransport, which exercises real request building — the new tests assert on the URL, headers and body actually sent.
  • ruff + mypy clean. mypy caught a real latent type error during the refactor (a date flowing into a datetime field, previously hidden by the untyped dict).

…ithub

- repository.py owns every query against github_users; the service no longer builds SQL
- github.py is a typed boundary: GitHubOAuthToken and GitHubUserProfile replace the bare dicts that used to cross the domain, so a GitHub shape change fails at the edge instead of as a KeyError in a service
- service.py exposes use cases (authenticate_with_code, sync_user, refresh_tokens) and returns a TokenPair object instead of an untyped 2-tuple; get_user_stats returns UserStatsResponse instead of a dict
- container/repository.py is introduced to own the ContainerSession aggregates the dashboard needs, so identity stops writing SQL over another module's tables
- router.py is thin: validate, call one use case, shape the response. Inline UnauthorizedError imports hoisted, cookie policy factored into one helper
- logout now clears the cookie with the same attributes used to set it (it was hardcoded secure=True, so it silently failed over plain HTTP) and is rate-limited like its neighbours
- dead RefreshRequest schema removed
- tests rewritten off unittest.mock onto httpx.MockTransport: 17 -> 26 tests covering the boundary parsing, token claims and the stats path

317 tests pass, ruff and mypy clean.
@helprs-prod

helprs-prod Bot commented Jul 31, 2026

Copy link
Copy Markdown

helPRs session created for this PR.

Skill: challenge-me | Open session

@mariuspruvot
mariuspruvot merged commit a713e41 into main Jul 31, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant