6.1 KiB
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.urlis ahttp://127.0.0.1:<port>origin AND aGETto${url}/authorize?...&state=abcresponds with a 302 carrying the samestateand a generatedcode
Scenario: stop releases the port
- WHEN
handle.stop()is called and awaited - THEN the port is free for subsequent binds AND a second
GETto the URL fails withECONNREFUSED
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/userinfowith the access token - THEN
/userinforeturns the primed user payload ANDlastRequest()records every step
Scenario: PKCE S256 challenge enforced
- WHEN
/tokenis called with acode_verifierwhoseSHA-256does not match the originalcode_challenge - THEN the response is HTTP 400 with
{ error: "invalid_grant" }
Scenario: refresh-token grant rotates the token pair
- WHEN
/tokenis called withgrant_type=refresh_tokenand 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
/userinforesponse 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(nocode)
Scenario: queues are FIFO
- WHEN
primeUser(A)thenprimeUser(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 andprimeUser({ id: 42 })is called - THEN
/userinforeturns{ 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.jsonis inspected - THEN the
dependenciesblock contains no@suaveplan/*entry;peerDependenciesmay declare@suaveplan/typestypes-only
Scenario: test-runner imports go through @suaveplan/testing/runner
- WHEN
bun scripts/lint/check-test-runner-imports.tsis run - THEN the exit code is 0 (no direct
vitestorbun:testimports insidesrc/)
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/"