Skip to content

feat(games): get a Twitch app token for IGDB - #181

Merged
Bakhomious merged 2 commits into
mainfrom
step/twitch-token
Oct 30, 2026
Merged

Bakhomious merged 2 commits into
mainfrom
step/twitch-token

Conversation

@Bakhomious

Copy link
Copy Markdown
Contributor

Brief

Goal: the games service can get a Twitch app token for IGDB, keep it, and renew it before it expires (decision 68): the fifth of games-exophase's eleven steps.

Scope: a new package, services/games/internal/twitch. It asks Twitch's OAuth server for an app access token with the client credentials grant, keeps the token in memory, and renews it before it expires. It also forgets a token that IGDB refuses. Decision 304 and the step's rows in plan §0 and §8.2 come with it.

Out of scope:

  • the IGDB client that uses the token (igdb-search);
  • the Twitch settings, the secret and their docs (games-covers);
  • the laptop (homelab-games).

Acceptance criteria:

  • Token asks POST /oauth2/token with client_id, client_secret and grant_type=client_credentials in a form body, with nothing in the query. It returns the token from the answer.
  • The token is kept until 9 tenths of its expires_in have passed, counted from when the request began. After that, the next call asks for a new one.
  • Only one call at a time asks Twitch; the others wait for its token. A waiting call whose context ends returns the context's error at once.
  • A failed request changes nothing, so the next call asks again.
  • Drop forgets the token that IGDB refused. A token that is no longer the one kept changes nothing.
  • A status other than 200 gives get a Twitch token: and the status, with Twitch's message when there is one. An answer with no token, a lifetime of 0 seconds or less, a body that isn't JSON, or a body over 64 KiB gives read Twitch's answer:.
  • Each request gives up after 10 seconds.
  • No error shows the secret.
  • Coverage is 70% or more; mise run check is green.

What changed

  • services/games/internal/twitch/tokens.go: Tokens keeps the latest token and takes turns through a one-slot channel. Token returns the kept token or gets a new one, and Drop forgets a refused one.
  • services/games/internal/twitch/app.go: New gives the Tokens of a Twitch app. Its request sends the form, reads the token and its lifetime, and says why Twitch refused.
  • Tests:
    • tokens_internal_test.go: a fake Twitch, in synctest bubbles;
    • app_internal_test.go: an in-memory Twitch for the lifetime and the 10 seconds, in bubbles;
    • app_test.go: an httptest stand-in for the request and the errors.
  • docs/plan.md: decision 304, the §0 row and the §8.2 bullet.

QA

  • Tests:
    • Every criterion has a test, and the tests pass with -race.
    • Coverage: 98%. The one statement left is http.NewRequestWithContext's error, which a parsed URL and a fixed method never hit.
  • Mutants:
    • I ran 26 hand mutants. All 26 are killed.
    • One of them, Drop without the turn, leaves the same token behind. Only the race detector catches it, which mise run test runs.
  • Tests QA added (they killed the mutants that survived at first):
    • a token lives as long as Twitch's expires_in says, in seconds;
    • a request that never gets an answer gives up after 10 seconds;
    • Drop waits for a token that's on its way;
    • a status that isn't 200 is refused, even a 202 with a token;
    • a refusal over 64 KiB gives only the status;
    • the stand-in now records Accept too.
  • Live: with a made-up client ID, the real https://id.twitch.tv gives get a Twitch token: 400 Bad Request, invalid client.
  • Gates: mise run check passed on this branch's tree, merged with main after refactor(notes): keep a note page's state in a class #177 and feat(games): serve the games I played lately #178:
    • golangci-lint: 0 issues;
    • govulncheck: no vulnerabilities;
    • the Go coverage gates: 70% per package, 80% in total, at 96.5% in total;
    • Vitest: 1192 tests;
    • Playwright: 381 passed, 7 skipped.

Review

  • Blocking: none.
  • Non-blocking:
    • Drop takes no context. It waits for a request under way, which takes at most 10 seconds.
    • When a call's context has already ended and the turn is free, Go's select picks either case. So such a call may still return the kept token, or fail its request at once. Both are harmless.
    • A lifetime of more than about 292 years would overflow time.Duration. Twitch gives about 60 days, and Drop forgets any token that IGDB refuses.
    • Nothing uses New or Drop yet: igdb-search and games-covers will.

Merged under the user's standing OK once CI's check is green.

IGDB takes a Twitch app access token, so the games service gets one
with the client credentials grant, keeps it in memory until 9 tenths
of its life, asks Twitch one request at a time, and forgets a token
that IGDB refuses. Nothing uses it yet: igdb-search will. Records
decision 304.
@Bakhomious
Bakhomious merged commit 2aad34d into main Oct 30, 2026
2 checks passed
@Bakhomious
Bakhomious deleted the step/twitch-token branch October 30, 2026 02:31
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