Initial Commit
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
# testing-integration-oauth-mock
|
||||
|
||||
**Package**: `@suaveplan/testing-integration-oauth-mock`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package
|
||||
|
||||
## Why
|
||||
|
||||
Stage 6.3 (`api-auth-better-auth`) requires an end-to-end GitHub OAuth flow exercised against a controllable IdP. Stage 8's `api-plugin-example` and a future `api-plugin-auth` GitHub flow will need the same fixture. Stand-up belongs once in `testkit/`, reusable across packages and conformance suites.
|
||||
|
||||
## What changes
|
||||
|
||||
Add a new testing-infrastructure package at `packages/testkit/testing/testing-integration-oauth-mock/`. Like its `testing-integration-*` siblings (rule 8), it has zero `@suaveplan/*` runtime deps except types-only consumption.
|
||||
|
||||
The package SHALL expose:
|
||||
|
||||
- `createMockOauthIdp(opts)` — returns a `{ url, port, stop, primeUser, primeError, lastRequest, allRequests }` handle backing a real HTTP server (Bun.serve) implementing OAuth 2.0 + PKCE.
|
||||
- Endpoints: `GET /authorize`, `POST /token`, `GET /userinfo`, `GET /.well-known/openid-configuration` (when `openid: true`), `GET /jwks.json` (when `openid: true`).
|
||||
- Test-control levers: `primeUser({ id, email, ... })` seeds the next callback's user; `primeError(...)` forces a specific RFC-6749 error; `lastRequest()` and `allRequests()` for assertion.
|
||||
- Deterministic state/code generation via injectable `rng` (defaults to `crypto.getRandomValues`).
|
||||
- Per-test isolation: each `createMockOauthIdp` listens on a fresh ephemeral port.
|
||||
- Full GitHub-shaped user payload (`id`, `login`, `email`, `avatar_url`, …) as the default user shape; arbitrary providers via `userShape: "github" | "google" | "generic"` or custom mapper.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Real OAuth provider discovery / token introspection beyond what the spec mandates.
|
||||
- OIDC ID-token signing with arbitrary algorithms (HS256 + RS256 only).
|
||||
- Multi-tenant OAuth app registration; one app per IdP instance.
|
||||
@@ -0,0 +1,117 @@
|
||||
# Capability Spec Delta: testing-integration-oauth-mock
|
||||
|
||||
**Package**: `@suaveplan/testing-integration-oauth-mock`
|
||||
**Status**: PROPOSED
|
||||
|
||||
## ADDED 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 (Bun.serve) — 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:<port>` 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"` 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"` AND `src/control/primer.test.ts::"primeError forces a specific RFC-6749 error"` AND `src/control/primer.test.ts::"queues are FIFO"`
|
||||
|
||||
---
|
||||
|
||||
### 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::"github shape includes canonical fields"`
|
||||
|
||||
---
|
||||
|
||||
### 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 Bun.serve 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 (only catalog deps); `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"`
|
||||
@@ -0,0 +1,39 @@
|
||||
# Tasks — testing-integration-oauth-mock
|
||||
|
||||
## 1. Package scaffold
|
||||
|
||||
- [x] 1.1 Create `packages/testkit/testing/testing-integration-oauth-mock/` with `package.json`, `tsconfig.json`, `vitest.config.ts`, `README.md`.
|
||||
- [x] 1.2 No `@suaveplan/*` runtime deps (rule 8). Types-only `@suaveplan/types` allowed if needed.
|
||||
- [x] 1.3 `package.json` exports: `.` (root), `./testing` (conformance helpers if any), `./package.json`.
|
||||
|
||||
## 2. Implementation (feature folders per CLAUDE.md)
|
||||
|
||||
- [x] 2.1 `src/server/server.ts` — `createMockOauthIdp(opts)` factory; Bun.serve HTTP server; ephemeral port; `stop()` returns `Promise<void>`.
|
||||
- [x] 2.2 `src/endpoints/authorize.ts` — `GET /authorize` (state echo, PKCE challenge capture, redirect with code).
|
||||
- [x] 2.3 `src/endpoints/token.ts` — `POST /token` (code-exchange, PKCE verify, refresh-token flow, configurable expiry).
|
||||
- [x] 2.4 `src/endpoints/userinfo.ts` — `GET /userinfo` (bearer auth; primed user payload).
|
||||
- [x] 2.5 `src/endpoints/openid.ts` — `/.well-known/openid-configuration` + `/jwks.json` (gated on `openid: true`).
|
||||
- [x] 2.6 `src/control/primer.ts` — `primeUser`/`primeError` queues; FIFO consumption.
|
||||
- [x] 2.7 `src/control/audit.ts` — `lastRequest`/`allRequests` snapshot (frozen).
|
||||
- [x] 2.8 `src/shapes/github.ts`, `src/shapes/google.ts`, `src/shapes/generic.ts` — default user payloads.
|
||||
- [x] 2.9 `src/error-codes/` — `OAUTH_MOCK_*` registry.
|
||||
- [x] 2.10 Sibling `.md` for every implementation file (≥200 words, all sections).
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 Each endpoint has a colocated `.test.ts` exercising happy path + every error path. 100% istanbul on touched modules.
|
||||
- [x] 3.2 End-to-end test in `src/server/server.test.ts` drives a full `/authorize` → callback → `/token` → `/userinfo` round-trip via real HTTP.
|
||||
- [x] 3.3 PKCE flow tested with both `plain` and `S256` challenges.
|
||||
- [x] 3.4 RFC-6749 error responses tested (`access_denied`, `invalid_grant`, `invalid_request`).
|
||||
- [x] 3.5 Test files import from `@suaveplan/testing/runner` (rule 11). NOTE: this package is rule-8 testing-infrastructure but NOT on the rule-11 exempt allowlist, so the `@suaveplan/testing/runner` import is required.
|
||||
|
||||
## 4. Pre-publish gate
|
||||
|
||||
- [x] 4.1 `bunx turbo lint typecheck test --filter=@suaveplan/testing-integration-oauth-mock` green.
|
||||
- [x] 4.2 `bunx turbo test:coverage --filter=@suaveplan/testing-integration-oauth-mock` 100%.
|
||||
- [x] 4.3 `bun scripts/folder-reorg/generate-manifest.ts` to refresh `MANIFEST.md`.
|
||||
|
||||
## 5. Archive
|
||||
|
||||
- [x] 5.1 Fold delta into `openspec/specs/testing-integration-oauth-mock/spec.md`.
|
||||
- [x] 5.2 Move folder to `openspec/archive/2026-05-26-testing-integration-oauth-mock/`.
|
||||
Reference in New Issue
Block a user