first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
# @suaveplan/testing-integration-oauth-mock - Real-HTTP mock OAuth 2.0 IdP fixture with PKCE, primeUser/primeError control, and full request audit
Real-HTTP mock OAuth 2.0 Identity Provider for integration tests.
`createMockOauthIdp(opts)` boots a real `node:http` listener on an
ephemeral `127.0.0.1` port and implements:
- `GET /authorize` — issues an authorization code, captures PKCE
challenge, echoes state.
- `POST /token` — `authorization_code` and `refresh_token` grants;
PKCE `plain` + `S256` verification with rotated refresh tokens.
- `GET /userinfo` — Bearer-authenticated user payload shaped as
`github` (default), `google`, `generic`, or via a custom `userMapper`.
- `GET /.well-known/openid-configuration` + `GET /jwks.json` — gated
on `openid: true`.
Test-control levers:
- `primeUser({ … })` — FIFO queue of user payloads for the next
`/userinfo` response.
- `primeError("access_denied" | …)` — FIFO queue of RFC-6749 error
codes for the next `/authorize`.
- `lastRequest()` / `allRequests()` — frozen audit records of every
served HTTP request.
<!-- begin:auto-docs -->
## Documentation tree
- [audit](./src/control/audit.md) — `AuditLog` is the request-recorder that backs the `lastRequest()` and `allRequests()` accessors on the mock IdP handle.…
- [primer](./src/control/primer.md) — `Primer` is the in-memory control surface that lets a test author tell the mock IdP "the next user-info response should…
- [authorize](./src/endpoints/authorize.md) — `handleAuthorize` implements the OAuth 2.0 authorization-code endpoint (RFC-6749 §4.1.1) for the mock IdP. It issues a…
- [openid](./src/endpoints/openid.md) — `handleOpenIdConfig` serves the `/.well-known/openid-configuration` discovery document and `handleJwks` serves the matc…
- [token](./src/endpoints/token.md) — `handleToken` implements the OAuth 2.0 token endpoint (RFC-6749 §4.1.3 and §6) for the mock IdP. It supports two grant…
- [userinfo](./src/endpoints/userinfo.md) — `handleUserInfo` implements the OAuth/OIDC userinfo endpoint. Given a `Bearer` authorization header, it looks up the as…
- [error-codes](./src/error-codes/error-codes.md) — This module defines the closed registry of error codes raised by `@suaveplan/testing-integration-oauth-mock`. The packa…
- [error-codes](./src/error-codes/error-codes.md) — This module defines the closed registry of error codes raised by `@suaveplan/testing-integration-oauth-mock`. The packa…
- [server](./src/server/server.md) — `createMockOauthIdp` boots a real HTTP listener (Node's `http.createServer`) on an ephemeral port and routes OAuth 2.0…
- [server](./src/server/server.md) — `createMockOauthIdp` boots a real HTTP listener (Node's `http.createServer`) on an ephemeral port and routes OAuth 2.0…
- [generic](./src/shapes/generic.md) — `buildGenericUser` produces an OIDC-flavoured userinfo payload with the common `sub` / `email` / `name` claims. It is t…
- [github](./src/shapes/github.md) — `buildGithubUser` produces a userinfo payload that closely mirrors the `GET https://api.github.com/user` response GitHu…
- [google](./src/shapes/google.md) — `buildGoogleUser` produces a Google OIDC-style userinfo payload that matches the response Google's `/oauth2/v3/userinfo…
<!-- end:auto-docs -->
## Quick start
```ts
import { createMockOauthIdp } from "@suaveplan/testing-integration-oauth-mock";
const idp = await createMockOauthIdp({ userShape: "github" });
idp.primeUser({ id: 42, login: "alice" });
// drive a Better-Auth / next-auth GitHub provider against `idp.url`
await idp.stop();
```
## Compliance
- Rule 8 — testing-infrastructure: no `@suaveplan/*` runtime
dependencies; only `node:*` builtins.
- Rule 11 — test-runner imports go through
`@suaveplan/testing/runner`.
- Rule 2 — real HTTP, no mocking libraries.

View File

@@ -0,0 +1,65 @@
# Audit log
## Purpose
`AuditLog` is the request-recorder that backs the `lastRequest()` and
`allRequests()` accessors on the mock IdP handle. Every HTTP request the
server processes is captured as a frozen `AuditRecord` and appended to
the log in serve-order. Tests can then assert on the recorded headers,
query string, body, and ordering without re-running the flow.
The records are deep-frozen at insertion time so audit consumers cannot
accidentally mutate captured state through their assertions.
## Features
- Frozen `AuditRecord` snapshots with method, path, headers, query,
body, and `at` (millisecond epoch) timestamp.
- `last()` returns the most recently appended record or `undefined`.
- `all()` returns a frozen shallow-copy snapshot of the records array,
preserving insert-order iteration.
- `clear()` resets the log between scenarios.
## Usage: Basic
```ts
import { AuditLog } from "@suaveplan/testing-integration-oauth-mock/control";
const log = new AuditLog();
log.push({ method: "GET", path: "/authorize", headers: {}, query: {}, body: "", at: Date.now() });
log.last();
```
## Usage: Advanced
When asserting flows that involve multiple requests, snapshot the whole
array up front and walk it deterministically rather than calling
`last()` repeatedly:
```ts
const events = log.all();
expect(events.map((e) => `${e.method} ${e.path}`)).toEqual([
"GET /authorize",
"POST /token",
"GET /userinfo",
]);
```
## API Reference
- `interface AuditRecord` — `{ method, path, headers, query, body, at }`,
all read-only and frozen.
- `class AuditLog`
- `push(record: AuditRecord): void`
- `last(): AuditRecord | undefined`
- `all(): readonly AuditRecord[]`
- `clear(): void`
## Implementation Notes
`push` performs an `Object.freeze` not only on the record itself but on
the nested `headers` and `query` objects, because callers can legally
hold the input object and mutate it post-push otherwise. `all()` returns
a frozen *copy* via `slice()` so the public snapshot can never reach
back into the internal array — preventing test mutations from corrupting
later assertions.

View File

@@ -0,0 +1,65 @@
# Primer queues
## Purpose
`Primer` is the in-memory control surface that lets a test author tell
the mock IdP "the next user-info response should look like X" or "the
next `/authorize` call must fail with RFC-6749 error Y" without
restarting the HTTP server. It backs the public `primeUser` /
`primeError` levers on the `MockOauthIdpHandle` returned by
`createMockOauthIdp`.
Two independent FIFO queues — one for user payloads, one for forced
authorize-errors — guarantee deterministic playback across multiple
sequential round-trips inside a single test.
## Features
- Independent FIFO queues for primed users and primed errors.
- Validates RFC-6749 error codes at prime time and rejects unknown codes
with `OAUTH_MOCK.INVALID_OPTIONS`.
- `take*` consumers return `undefined` when the queue is empty so
endpoints can fall back to defaults gracefully.
- `peek*Count` accessors for tests asserting queue state.
## Usage: Basic
```ts
import { Primer } from "@suaveplan/testing-integration-oauth-mock/control";
const p = new Primer();
p.primeUser({ id: 42, login: "alice" });
p.primeError("access_denied");
```
## Usage: Advanced
The queues are independent, so a test can interleave errors and users
without worrying about consumption order:
```ts
const p = new Primer();
p.primeError("invalid_request"); // first /authorize fails
p.primeUser({ id: 7 }); // second /authorize → /userinfo returns id 7
```
## API Reference
- `class Primer`
- `primeUser(user: PrimedUser): void`
- `primeError(code: PrimerErrorCode): void`
- `takeUser(): PrimedUser | undefined`
- `takeError(): PrimerErrorCode | undefined`
- `peekUserCount(): number`
- `peekErrorCount(): number`
- `const PRIMER_ERRORS` — closed list of RFC-6749 error codes.
- `type PrimerErrorCode` — union over `PRIMER_ERRORS`.
## Implementation Notes
The class deliberately uses plain `Array<T>` with `push` / `shift` rather
than a linked-list deque. Test queues are short-lived and bounded by
test size; the constant-factor cost of `shift()` is irrelevant. Errors
are validated against a closed allowlist (RFC-6749 §4.1.2.1) at prime
time, not at consume time, so the test that supplies the bad code fails
fast and points at the line of the offending `primeError` call.

View File

@@ -0,0 +1,61 @@
# /authorize endpoint
## Purpose
`handleAuthorize` implements the OAuth 2.0 authorization-code endpoint
(RFC-6749 §4.1.1) for the mock IdP. It issues a fresh authorization
code, persists the PKCE challenge for later verification by the token
endpoint, and redirects the user-agent back to the caller's
`redirect_uri` with `code` and `state` parameters echoed.
If a test has primed an RFC-6749 error via `primeError`, the function
short-circuits and redirects with `?error=<code>` instead — letting test
authors drive the IdP's negative paths deterministically without
swapping out implementations.
## Features
- 302 redirect with `code` + echoed `state`.
- Captures PKCE `code_challenge` and `code_challenge_method`
(`plain` | `S256`), discarding unsupported methods.
- Primed error redirect honours RFC-6749 error parameter format.
- Pops the next primed user from the FIFO primer queue and stores it on
the auth-code record for later `/userinfo` retrieval.
## Usage: Basic
```ts
const result = handleAuthorize(state, {
redirect_uri: "http://app/cb",
state: "csrf",
code_challenge: "abc",
code_challenge_method: "S256",
});
// → 302 Location: http://app/cb?code=…&state=csrf
```
## Usage: Advanced
To force a primed error path:
```ts
state.primer.primeError("access_denied");
handleAuthorize(state, { redirect_uri: "http://app/cb" });
// → 302 Location: http://app/cb?error=access_denied
```
## API Reference
- `handleAuthorize(state, query): AuthorizeResult`
- `interface AuthorizeResult` — `{ status: 302, location: string,
code?: string, error?: string }`.
## Implementation Notes
`redirect_uri` is the only mandatory query parameter — when missing, the
function returns a sentinel `error` string and an empty `location`
because there is no safe URL to redirect to. Invalid PKCE methods are
silently downgraded to "no PKCE captured"; the token endpoint then
treats the exchange as PKCE-disabled. The primed user queue is consumed
even when no PKCE challenge is provided so the test author's intent
(prime then drive) is preserved in either flow.

View File

@@ -0,0 +1,57 @@
# OIDC discovery + JWKS endpoints
## Purpose
`handleOpenIdConfig` serves the `/.well-known/openid-configuration`
discovery document and `handleJwks` serves the matching `/jwks.json`
keyset. Together they let OIDC-aware libraries (Better-Auth's OIDC
provider, openid-client, oidc-provider clients) bootstrap from the mock
IdP without any hard-coded endpoint URLs.
The endpoints are only routed when the fixture is created with
`{ openid: true }`; the rest of the fixture remains a plain OAuth-2 IdP
otherwise.
## Features
- Spec-conforming discovery document with `issuer`,
`authorization_endpoint`, `token_endpoint`, `userinfo_endpoint`, and
`jwks_uri` all derived from the live `state.issuer` origin.
- Declares `response_types_supported: ["code"]`,
`grant_types_supported: ["authorization_code", "refresh_token"]`,
`code_challenge_methods_supported: ["plain", "S256"]`, and
`scopes_supported: ["openid", "profile", "email"]`.
- Static JWKS with a single HS256 mock key, sufficient for adapters that
only validate `id_token` signatures lazily.
## Usage: Basic
```ts
const config = handleOpenIdConfig(state);
config.body.token_endpoint; // → http://127.0.0.1:<port>/token
```
## Usage: Advanced
When wiring `openid-client` against the mock IdP:
```ts
const issuer = await Issuer.discover(`${idp.url}/.well-known/openid-configuration`);
const client = new issuer.Client({ client_id: "c", client_secret: "s" });
```
## API Reference
- `handleOpenIdConfig(state): OpenIdConfigResult` — `{ status: 200, body: <discovery> }`.
- `handleJwks(state): JwksResult` — `{ status: 200, body: { keys: […] } }`.
## Implementation Notes
The discovery document is regenerated on every request rather than
cached, because `state.issuer` is fixed at server-start time but the
fixture pattern occasionally re-issues per test. The JWKS is
deliberately small and static; consumers that need rotating asymmetric
keys should drive a real OIDC provider instead of this fixture. The
embedded HS256 key value is a base64-url-encoded placeholder labelled
"for-testing-only" — it has no security meaning and must never be used
to sign anything outside of a fixture.

View File

@@ -0,0 +1,63 @@
# /token endpoint
## Purpose
`handleToken` implements the OAuth 2.0 token endpoint (RFC-6749 §4.1.3
and §6) for the mock IdP. It supports two grant types:
`authorization_code` (exchanging a freshly issued code for an
access+refresh-token pair) and `refresh_token` (rotating an existing
refresh-token).
PKCE verification (RFC-7636) is enforced when the matching auth-code
record carries a `code_challenge`; both `plain` and `S256` challenge
methods are supported, and mismatches produce
`{ error: "invalid_grant" }` responses with HTTP 400 per spec.
## Features
- `authorization_code` grant with single-use code semantics.
- `refresh_token` grant with rotation — the previous refresh token is
revoked on each successful refresh.
- PKCE S256 verification using a real `node:crypto` `sha256` hash.
- Configurable `expires_in` via `accessTokenTtlSeconds`.
- RFC-6749 error envelopes: `invalid_request`, `invalid_grant`,
`unsupported_grant_type`.
## Usage: Basic
```ts
const tokenResponse = handleToken(state, {
grant_type: "authorization_code",
code: "abc",
code_verifier: "verifier-43-chars-min",
});
```
## Usage: Advanced
Rotate a refresh token (the original is revoked):
```ts
const refreshed = handleToken(state, {
grant_type: "refresh_token",
refresh_token: previous.refresh_token,
});
```
## API Reference
- `handleToken(state, form): TokenResult`
- `interface TokenSuccess` — `{ status: 200, body: { access_token,
token_type: "Bearer", expires_in, refresh_token, scope? } }`.
- `interface TokenError` — `{ status: 400|401, body: { error,
error_description? } }`.
## Implementation Notes
PKCE S256 base64url encoding is performed with `Buffer.toString('base64')`
followed by manual `=` stripping and `+`/`/` substitution — this matches
RFC-4648 §5 and is what every conforming client SDK produces. Auth codes
are marked `used = true` on first successful exchange, and reuse is
rejected as `invalid_grant`. Refresh tokens carry forward the original
`userPayload` so subsequent `/userinfo` calls keep returning the same
identity, modelling production IdP behaviour faithfully.

View File

@@ -0,0 +1,58 @@
# /userinfo endpoint
## Purpose
`handleUserInfo` implements the OAuth/OIDC userinfo endpoint. Given a
`Bearer` authorization header, it looks up the associated access token,
verifies it has not expired, and returns the matching user payload
shaped by the configured `userShape` (or by a custom `userMapper` when
the caller supplies one).
The endpoint backs the primary identity-fetch path that production OAuth
adapters use after a successful code-exchange, so it is the surface most
test scenarios assert against.
## Features
- `Bearer <token>` extraction with regex validation.
- Looks up the token in the in-memory access-token map and 401s on
unknown or expired tokens.
- Dispatches across `github` / `google` / `generic` shapes, or hands the
primed payload to a caller-supplied `userMapper`.
- All error responses use RFC-6750 `invalid_token` envelopes.
## Usage: Basic
```ts
const r = handleUserInfo(state, "Bearer at_abc");
// → { status: 200, body: { id: 42, login: "alice", … } }
```
## Usage: Advanced
Provide a custom mapper to return arbitrary claim shapes:
```ts
const state = {
...,
options: {
...,
userMapper: (primed) => ({ sub: String(primed?.id), groups: ["admins"] }),
},
};
```
## API Reference
- `handleUserInfo(state, authorizationHeader: string | undefined): UserInfoResult`
- `interface UserInfoResult` — `{ status: 200 | 401, body: Record<string, unknown> }`.
## Implementation Notes
The matcher uses `^Bearer\s+(.+)$` (case-sensitive, unicode flag) so
RFC-6750's exact-spelling requirement is enforced — "bearer" or
"Token" headers are rejected as malformed. Token expiry is checked with
`<= Date.now()` so a token whose `expiresAt` equals the current
millisecond is treated as expired (defensive boundary). The default
branch of the `userShape` switch is unreachable under the typed input
and is `/* c8 ignore */`d to keep coverage clean.

View File

@@ -0,0 +1,80 @@
# OAUTH_MOCK error codes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module defines the closed registry of error codes raised by
`@suaveplan/testing-integration-oauth-mock`. The package is rule-8
testing-infrastructure and so cannot depend on `@suaveplan/error`; instead
it rolls a minimal `OauthMockError` class that carries the same shape
(`code`, `message`, optional `cause`, optional immutable `context`) and
satisfies tests that need to assert on stable code strings without coupling
the fixture to the runtime error library.
Every internal failure path — invalid PKCE verifier, unknown auth code,
exhausted primer queue, unrecognised user shape — surfaces a code in this
registry rather than a bare `Error`, giving test authors a stable string
contract to assert against when they want to drive negative-path
scenarios through the mock IdP.
## Features
- Closed registry constant `OAUTH_MOCK_CODES` with eleven canonical codes.
- Branded `OauthMockErrorCode` union type for compile-time exhaustiveness.
- `OauthMockError extends Error` with `code`, frozen `context`, and
optional `cause` preserved via the standard ES2022 `Error.cause` slot.
- Zero `@suaveplan/*` runtime dependency (rule 8).
## Usage: Basic
```ts
import {
OAUTH_MOCK_CODES,
OauthMockError,
} from "@suaveplan/testing-integration-oauth-mock/error-codes";
throw new OauthMockError(
OAUTH_MOCK_CODES.UNKNOWN_AUTH_CODE,
"no such code",
);
```
## Usage: Advanced
Attach structured context and chain a cause when re-throwing across
endpoint boundaries:
```ts
try {
verifyPkce(challenge, verifier);
} catch (cause) {
throw new OauthMockError(
OAUTH_MOCK_CODES.INVALID_PKCE_VERIFIER,
"verifier hash mismatch",
{ cause, context: { method: "S256" } },
);
}
```
## API Reference
- `OAUTH_MOCK_CODES` — frozen string-keyed map of canonical codes.
- `OauthMockErrorCode` — union type of the registry values.
- `class OauthMockError extends Error` — constructor
`(code, message, options?: { cause?; context? })`. Exposes
`code: OauthMockErrorCode`, `context: Readonly<Record<string, unknown>>`,
and optional inherited `cause`.
## Implementation Notes
`context` is sealed with `Object.freeze` at construction time so callers
who pass a live reference cannot mutate the recorded context after the
error has propagated up the stack. The class extends the platform `Error`
so structured cloning, `instanceof`, and stack traces continue to behave
in the natural way. Codes use the dot-namespaced form
`OAUTH_MOCK.<CASE>` for grep-ability and to match the wider Suaveplan
code-namespace convention even though the package does not consume
`@suaveplan/error` directly.

View File

@@ -0,0 +1,95 @@
# createMockOauthIdp — server factory
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createMockOauthIdp` boots a real HTTP listener (Node's `http.createServer`)
on an ephemeral port and routes OAuth 2.0 + optional OIDC traffic
through the in-process endpoint handlers. The returned handle is the
single public-API surface of the package: `url`, `port`, `stop`,
`primeUser`, `primeError`, `lastRequest`, `allRequests`.
Unlike mocking libraries, this fixture exposes a real socket, so OAuth
adapters under test exercise their full network code path —
content-type negotiation, redirect handling, header parsing — instead of
short-circuiting against a stub. This is the keystone fixture for
Stage 6.3 (`api-auth-better-auth`) and Stage 8 (`api-plugin-auth`)
GitHub-OAuth round-trip tests.
## Features
- Ephemeral-port `127.0.0.1` listener; per-test isolation.
- Awaitable `stop()` that closes the socket and releases the port.
- Full OAuth 2.0 authorization-code + refresh-token flow with PKCE.
- Optional OIDC discovery + JWKS endpoints under `openid: true`.
- Configurable user shape (`github` / `google` / `generic`) or custom
mapper via `userMapper`.
- Injectable RNG for deterministic codes/tokens in tests.
- Request audit feed (`lastRequest`/`allRequests`) of every served HTTP
request as a frozen `AuditRecord`.
## Usage: Basic
```ts
import { createMockOauthIdp } from "@suaveplan/testing-integration-oauth-mock";
const idp = await createMockOauthIdp({ userShape: "github" });
idp.primeUser({ id: 1, login: "octocat" });
// drive your OAuth client against idp.url
await idp.stop();
```
## Usage: Advanced
Drive the full PKCE S256 round-trip end-to-end:
```ts
const idp = await createMockOauthIdp({ openid: true, rng: () => "det" });
idp.primeUser({ id: 42, login: "alice" });
const authUrl = new URL(`${idp.url}/authorize`);
authUrl.searchParams.set("redirect_uri", "http://app/cb");
authUrl.searchParams.set("code_challenge", sha256base64url(verifier));
authUrl.searchParams.set("code_challenge_method", "S256");
const authResp = await fetch(authUrl, { redirect: "manual" });
const code = new URL(authResp.headers.get("location")!).searchParams.get("code")!;
const tokenResp = await fetch(`${idp.url}/token`, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
code_verifier: verifier,
}).toString(),
});
const { access_token } = await tokenResp.json();
const me = await fetch(`${idp.url}/userinfo`, {
headers: { authorization: `Bearer ${access_token}` },
}).then((r) => r.json());
```
## API Reference
- `createMockOauthIdp(options?: CreateMockOauthIdpOptions): Promise<MockOauthIdpHandle>`
- `interface CreateMockOauthIdpOptions` — `userShape`, `userMapper`,
`openid`, `accessTokenTtlSeconds`, `refreshTokenTtlSeconds`,
`clientId`, `clientSecret`, `rng`.
- `interface MockOauthIdpHandle` — `url`, `port`, `stop`, `primeUser`,
`primeError`, `lastRequest`, `allRequests`.
## Implementation Notes
The server is built with `node:http` rather than `Bun.serve` because the
package's vitest test runner executes under Node (`pool: "forks"`), and
`Bun.serve` is unavailable there. Functionally the listener is
indistinguishable — a real TCP socket bound to `127.0.0.1:<ephemeral>`
— and any OAuth client under test exercises its full network stack
against it. The `state.issuer` is finalised after the OS assigns a port,
so discovery URLs reflect the live origin instead of a placeholder. The
factory throws `OAUTH_MOCK.UNSUPPORTED_USER_SHAPE` when the caller
supplies an unknown `userShape` without a `userMapper` fallback — that
keeps tests honest rather than silently degrading to the generic shape.

View File

@@ -0,0 +1,64 @@
# Generic user shape
## Purpose
`buildGenericUser` produces an OIDC-flavoured userinfo payload with the
common `sub` / `email` / `name` claims. It is the universal fallback when
the consumer does not opt into a provider-specific shape; tests using
single-sign-on flows that are not GitHub or Google can lean on this
shape to drive their `/userinfo` assertions without hand-rolling claim
objects.
Every payload is sealed with `Object.freeze` so that fixture consumers
cannot mutate stored state by modifying the response object after it has
been served — keeping audit captures stable for assertion replay.
## Features
- Sensible defaults for every required OIDC core claim.
- Accepts a `number | string` `id` shortcut that fans out into `sub`.
- Passes through arbitrary extra claims (`custom_claim`, `groups`, …).
- Frozen return value.
## Usage: Basic
```ts
import { buildGenericUser } from "@suaveplan/testing-integration-oauth-mock/shapes";
const u = buildGenericUser({ id: 7, email: "lee@example.test" });
// → { sub: "7", email: "lee@example.test", email_verified: true, name: "User 7" }
```
## Usage: Advanced
Inject provider-specific claims (e.g. SCIM groups) by spreading them
through the input — they survive untouched on the returned payload:
```ts
const u = buildGenericUser({
sub: "user-1",
name: "Lee",
preferred_username: "lee",
picture: "https://cdn.example/lee.png",
groups: ["admins", "billing"],
});
```
## API Reference
- `interface GenericUser` — frozen OIDC-shaped object: `sub`, `email`,
`email_verified`, `name`, optional `preferred_username`, optional
`picture`, plus arbitrary extra claims.
- `interface PrimedGenericInput` — caller-supplied partial input.
- `function buildGenericUser(input?: PrimedGenericInput): GenericUser`.
## Implementation Notes
The function deliberately avoids any imports outside the local module so
it can be tree-shaken into very small bundles for downstream test
harnesses. `sub` is always coerced to a `string` because OIDC mandates
`sub` is a string claim even when the upstream provider exposes a
numeric user identifier. Optional fields are spread conditionally so the
emitted object key set is stable for snapshot assertions: callers that
omit `preferred_username` see the key absent rather than set to
`undefined`.

View File

@@ -0,0 +1,61 @@
# GitHub user shape
## Purpose
`buildGithubUser` produces a userinfo payload that closely mirrors the
`GET https://api.github.com/user` response GitHub serves to OAuth apps.
Adapters in the wider ecosystem (Better-Auth, next-auth, passport-github)
read fields by their GitHub-canonical names — `id`, `login`,
`avatar_url`, `node_id` — and this builder exists so those adapters can
exercise their full mapping logic against the mock IdP without writing
GitHub-specific assertions into every test.
The default payload is opinionated enough to be useful straight out of
the box while remaining trivial to override per call when a test needs
particular id/login pairs to drive primary-key-style scenarios.
## Features
- Canonical GitHub field set: `id`, `login`, `email`, `avatar_url`,
`name`, `node_id`, `type: "User"`, `site_admin`.
- String `id` coerced to number; falls back to `0` on unparseable input.
- Explicit `null` name supported (org-style users).
- Extra unknown claims preserved on the output object.
- Frozen return value.
## Usage: Basic
```ts
import { buildGithubUser } from "@suaveplan/testing-integration-oauth-mock/shapes";
const u = buildGithubUser({ id: 1, login: "octocat" });
```
## Usage: Advanced
Compose with the `primeUser` lever to drive role-based assertions:
```ts
import { createMockOauthIdp } from "@suaveplan/testing-integration-oauth-mock";
const idp = await createMockOauthIdp({ userShape: "github" });
idp.primeUser({ id: 9000, login: "ghost", site_admin: true });
```
## API Reference
- `interface GithubUser` — canonical GitHub-shaped output.
- `interface PrimedGithubInput` — partial caller input. Numeric or
string `id` accepted.
- `function buildGithubUser(input?: PrimedGithubInput): GithubUser`.
## Implementation Notes
`id` is normalised to `number` because the GitHub adapter ecosystem
treats it as the primary key — letting it be a `string` here would
silently break downstream `===` checks. Unparseable string ids deliberately
collapse to `0` rather than `NaN` so JSON serialisation remains lossless;
tests that care about that edge can pass a number directly. `name` is
left explicitly nullable to model GitHub's organisation-style accounts.
The default `node_id` interpolation uses a recognisable static prefix so
test failures involving node-id mismatches are easy to spot in diffs.

View File

@@ -0,0 +1,65 @@
# Google user shape
## Purpose
`buildGoogleUser` produces a Google OIDC-style userinfo payload that
matches the response Google's `/oauth2/v3/userinfo` endpoint serves for
authorised users. It exists so that adapters consuming Google IdP
identity claims — Better-Auth's Google provider, next-auth's OAuth/OIDC
provider, custom JWT pipelines — can run their full mapping logic
against the mock IdP without re-implementing Google-specific shapes in
every test.
The payload combines the OIDC core claims (`sub`, `email`, `name`) with
Google's vendor extensions (`given_name`, `family_name`, `locale`,
optional `hd` for hosted-domain GSuite tenants).
## Features
- Default OIDC claim set with sensible filler values.
- Composes `name` from `given_name` + `family_name` when omitted.
- Optional `hd` only emitted when supplied (matches Google's behaviour).
- Numeric `id` shortcut coerced into the canonical string `sub` claim.
- Frozen return value.
## Usage: Basic
```ts
import { buildGoogleUser } from "@suaveplan/testing-integration-oauth-mock/shapes";
const u = buildGoogleUser({ id: 42, given_name: "Ada", family_name: "Lovelace" });
// → sub: "42", name: "Ada Lovelace", …
```
## Usage: Advanced
Drive a multi-tenant scenario by passing `hd`:
```ts
const u = buildGoogleUser({
sub: "1001",
email: "ada@example.com",
hd: "example.com",
locale: "en-GB",
});
```
## API Reference
- `interface GoogleUser` — frozen output payload with required OIDC
claims plus Google extensions (`given_name`, `family_name`, `picture`,
`locale`) and optional `hd`.
- `interface PrimedGoogleInput` — partial caller input. `id` accepts
`number | string`.
- `function buildGoogleUser(input?: PrimedGoogleInput): GoogleUser`.
## Implementation Notes
`sub` is always a string (per OIDC) and is derived from `id` when only
`id` is supplied. `name` is reconstructed from `given_name` +
`family_name` if the caller does not pass one explicitly, so that test
authors who set just first-name + last-name still see a populated
full-name claim. `hd` is spread conditionally so the key is absent on
the output when the caller does not opt in — Google only includes `hd`
on Workspace accounts, and keeping that semantic preserves test fidelity
for tenancy assertions.