# testing-integration-oauth-mock ## Purpose Real-HTTP mock OAuth 2.0 Identity Provider fixture for integration tests. Boots a `node:http` listener on an ephemeral `127.0.0.1` port and implements OAuth 2.0 authorization-code + refresh-token grants with PKCE, an OIDC discovery + JWKS pair (opt-in), GitHub/Google/Generic user shapes (or a custom mapper), and FIFO test-control levers (`primeUser`, `primeError`) plus a frozen audit log of every served request. Rule-8 testing-infrastructure package — zero `@suaveplan/*` runtime dependencies; only `node:*` builtins. ## Requirements ### Requirement: Exposes a controllable mock OAuth 2.0 IdP The package SHALL export `createMockOauthIdp(opts)` returning a handle `{ url, port, stop, primeUser, primeError, lastRequest, allRequests }` backed by a real HTTP listener (`node:http` `createServer`) — not a mock library — implementing OAuth 2.0 authorization-code flow with PKCE. #### Scenario: factory boots a real HTTP server on an ephemeral port - **WHEN** `createMockOauthIdp({})` is called - **THEN** `handle.url` is a `http://127.0.0.1:` origin AND a `GET` to `${url}/authorize?...&state=abc` responds with a 302 carrying the same `state` and a generated `code` #### Scenario: stop releases the port - **WHEN** `handle.stop()` is called and awaited - **THEN** the port is free for subsequent binds AND a second `GET` to the URL fails with `ECONNREFUSED` **Verified by:** `src/server/server.test.ts::"factory boots a real HTTP server on an ephemeral port"` AND `src/server/server.test.ts::"stop releases the port"` --- ### Requirement: Implements authorize → token → userinfo round-trip The IdP SHALL accept `GET /authorize` (state + code_challenge), `POST /token` (code + code_verifier exchange OR refresh-token grant), and `GET /userinfo` (Bearer authentication). The full round-trip MUST succeed without external network calls. #### Scenario: full happy-path round-trip - **WHEN** a client primes a user, hits `/authorize`, exchanges the returned code at `/token`, and fetches `/userinfo` with the access token - **THEN** `/userinfo` returns the primed user payload AND `lastRequest()` records every step #### Scenario: PKCE S256 challenge enforced - **WHEN** `/token` is called with a `code_verifier` whose `SHA-256` does not match the original `code_challenge` - **THEN** the response is HTTP 400 with `{ error: "invalid_grant" }` #### Scenario: refresh-token grant rotates the token pair - **WHEN** `/token` is called with `grant_type=refresh_token` and a valid refresh token - **THEN** a new access+refresh pair is returned AND the previous refresh token is invalidated **Verified by:** `src/server/server.test.ts::"full happy-path round-trip"` AND `src/endpoints/token.test.ts::"PKCE S256 challenge enforced — mismatch returns invalid_grant"` AND `src/endpoints/token.test.ts::"refresh-token grant rotates the token pair"` --- ### Requirement: Test-control levers (`primeUser`, `primeError`) Tests SHALL be able to control IdP behaviour without restarting the server. #### Scenario: primeUser seeds the next callback's user payload - **WHEN** `handle.primeUser({ id: "42", login: "alice" })` is called before `/authorize` - **THEN** the following `/userinfo` response carries `{ id: "42", login: "alice", ... }` #### Scenario: primeError forces a specific RFC-6749 error - **WHEN** `handle.primeError("access_denied")` is called and the user hits `/authorize` - **THEN** the redirect carries `?error=access_denied` (no `code`) #### Scenario: queues are FIFO - **WHEN** `primeUser(A)` then `primeUser(B)` is called and two round-trips run - **THEN** the first returns A and the second returns B **Verified by:** `src/control/primer.test.ts::"primeUser seeds the next callback (FIFO)"` AND `src/control/primer.test.ts::"primeError forces a specific RFC-6749 error"` AND `src/control/primer.test.ts::"queues are FIFO across both lanes independently"` --- ### Requirement: Audit surface (`lastRequest`, `allRequests`) The handle SHALL record every HTTP request the IdP served as a frozen snapshot: `{ method, path, headers, query, body, at }`. #### Scenario: lastRequest returns the most recent record - **WHEN** two requests are served and `handle.lastRequest()` is called - **THEN** the returned record matches the second request #### Scenario: allRequests returns a frozen ordered array - **WHEN** `handle.allRequests()` is called - **THEN** the returned array is frozen AND iterating it yields requests in serve-order **Verified by:** `src/control/audit.test.ts::"lastRequest returns the most recent record"` AND `src/control/audit.test.ts::"allRequests returns a frozen ordered array"` --- ### Requirement: Provider-shaped user payloads The factory SHALL accept `userShape: "github" | "google" | "generic"` (default `"github"`) or a custom mapper, producing realistic provider-shaped userinfo responses. #### Scenario: github shape includes GitHub-canonical fields - **WHEN** `userShape: "github"` is set and `primeUser({ id: 42 })` is called - **THEN** `/userinfo` returns `{ id: 42, login, email, avatar_url, name, ... }` **Verified by:** `src/shapes/github.test.ts::"populates canonical GitHub claims by default"` AND `src/shapes/github.test.ts::"honours primed id + login"` --- ### Requirement: No mocks; rule 8 testing-infrastructure compliance The package MUST NOT depend on any `@suaveplan/*` runtime package and MUST NOT use mocking libraries (no `vi.mock`, no `jest.mock`). The server is a real `node:http` listener. Test files import test-runner primitives from `@suaveplan/testing/runner` per rule 11. #### Scenario: no @suaveplan/* runtime deps - **WHEN** `package.json` is inspected - **THEN** the `dependencies` block contains no `@suaveplan/*` entry; `peerDependencies` may declare `@suaveplan/types` types-only #### Scenario: test-runner imports go through @suaveplan/testing/runner - **WHEN** `bun scripts/lint/check-test-runner-imports.ts` is run - **THEN** the exit code is 0 (no direct `vitest` or `bun:test` imports inside `src/`) **Verified by:** `tests/package-shape.test.ts::"package.json declares no @suaveplan/* runtime deps"` AND `tests/package-shape.test.ts::"no direct vitest or bun:test imports in src/"`