## ADDED Requirements ### Requirement: Layer 0 dep-graph isolation The `@suaveplan/testing-integration-oidc` package's `dependencies` MUST be exactly `@suaveplan/testing-integration` (`workspace:^`) and `testcontainers` (`catalog:integration`). No other `@suaveplan/*` runtime dep is permitted. `devDependencies` MAY include `@suaveplan/testing` for the package's own tests. #### Scenario: Runtime closure clean - **WHEN** `bunx turbo ls --filter=@suaveplan/testing-integration-oidc...` is run - **THEN** the only `@suaveplan/*` package in the runtime closure is `@suaveplan/testing-integration` ### Requirement: Pluggable provider selection The fixture SHALL accept a `provider: 'keycloak' | 'oauth2-mock-server'` config field. Both providers MUST satisfy the same `ServiceFixture` contract — only the underlying image, init sequence, and admin-API endpoints differ. #### Scenario: Keycloak provider selected - **WHEN** `start({ provider: 'keycloak', realm: 'test' })` is called - **THEN** the fixture pulls `quay.io/keycloak/keycloak:26.0` AND seeds the `test` realm via the Keycloak admin REST API before resolving #### Scenario: oauth2-mock-server provider selected - **WHEN** `start({ provider: 'oauth2-mock-server' })` is called - **THEN** the fixture pulls `ghcr.io/navikt/mock-oauth2-server:2` AND seeds clients/users via its JSON config-file mount before resolving ### Requirement: Image-pinning policy Default images MUST be pinned to a minor tag (`quay.io/keycloak/keycloak:26.0`, `ghcr.io/navikt/mock-oauth2-server:2`). The `constants/index.ts` file MUST carry a `# TODO: replace with sha256:` comment for each image until the digest is recorded. #### Scenario: Pinned tag - **WHEN** the constants file is read - **THEN** both image strings include an explicit minor tag AND a sha256 TODO line is present ### Requirement: Env-bridge passthrough The fixture MUST consult `OIDC_ISSUER_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_REDIRECT_URI`, and optional `OIDC_USERS_FILE`. When `OIDC_ISSUER_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, and `OIDC_REDIRECT_URI` are all present and non-empty, `start()` MUST skip container creation and return a handle whose config points at the live issuer. #### Scenario: Live issuer present - **WHEN** all four required env vars are set - **THEN** `start()` returns without invoking `containerFactory` AND the returned config's `issuerUrl` equals `OIDC_ISSUER_URL` #### Scenario: Partial env triggers container - **WHEN** `OIDC_ISSUER_URL` is set but `OIDC_CLIENT_SECRET` is empty - **THEN** `start()` falls back to container mode ### Requirement: Healthcheck The fixture MUST poll `HTTP GET /.well-known/openid-configuration` and treat a `200` JSON response containing `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri` as ready. The default `healthTimeoutMs` MUST be `90000` (Keycloak slow-start). Callers MAY override. #### Scenario: Healthcheck succeeds - **WHEN** `/.well-known/openid-configuration` returns 200 with the four required fields - **THEN** `start()` resolves #### Scenario: Healthcheck times out - **WHEN** the discovery endpoint never returns within `healthTimeoutMs` - **THEN** `start()` rejects with code `INTEGRATION.OIDC.HEALTHCHECK_TIMEOUT` ### Requirement: Config shape The exported `OidcConfig` interface MUST include `provider`, optional `realm`, optional `users: Array<{ username: string; password: string; email: string }>`, optional `clients: Array<{ id: string; secret: string; redirectUris: string[]; grantTypes: string[] }>`, and the resolved `issuerUrl`. A sibling `oidc-fixture.schemas.ts` MUST export a Zod schema (`OidcConfigSchema`) that validates the same shape. #### Scenario: Schema parity - **WHEN** `OidcConfigSchema.safeParse` is run against an `OidcConfig` value - **THEN** the parse succeeds for valid configs AND fails with a `ZodError` for missing required fields ### Requirement: Error-code prefix All errors thrown from the fixture MUST be subclasses of `TestingIntegrationError` carrying `{ code, cause?, context? }` where `code` matches `^INTEGRATION\.OIDC\.[A-Z_]+$`. #### Scenario: Coded error - **WHEN** any fixture method throws - **THEN** the thrown value's `code` starts with `INTEGRATION.OIDC.` ### Requirement: Injectable fakes The fixture constructor MUST accept `containerFactory?: () => GenericContainer` and `fetchFn?: typeof fetch`. Unit tests MUST be able to drive 100% Istanbul branch coverage without invoking Docker or the network. #### Scenario: Fake injection - **WHEN** the fixture is constructed with both fakes - **THEN** real Docker / real `fetch` are never called during the test ### Requirement: Plugin scenarios The fixture MUST support, end-to-end against a real container, the following flows used by `@suaveplan/oauth-client` and exercised in the plugin's smoke test: #### Scenario: Authorization Code + PKCE returns valid tokens - **WHEN** `@suaveplan/oauth-client` runs an Authorization Code + PKCE flow against the fixture's seeded client - **THEN** the issuer returns an `id_token` AND `access_token` whose JWT signature verifies against the `jwks_uri` #### Scenario: Client Credentials grant succeeds - **WHEN** a `grant_type=client_credentials` request is made with the seeded client id and secret - **THEN** the response is `200 OK` with an `access_token` JWT #### Scenario: Revoked user introspection - **WHEN** a seeded user is removed (Keycloak admin REST or oauth2-mock-server config reload) and a previously issued token is introspected - **THEN** the introspection response has `active: false`