Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
## ADDED Requirements
### Requirement: Layer 0 dep-graph isolation
The `@suaveplan/testing-integration-minio` 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-minio...` is run
- **THEN** the only `@suaveplan/*` package in the runtime closure is `@suaveplan/testing-integration`
### Requirement: Coexistence with Garage S3 fixture
The MinIO fixture MUST exist alongside `@suaveplan/testing-integration-garage-s3` rather than replacing it. The two fixtures expose deliberately different surfaces: Garage masks several AWS-S3 quirks (admin-port health, layout-assignment init), while MinIO mirrors AWS-S3 surface much more closely. Tests that need AWS-API parity MUST target the MinIO fixture; tests covering the Garage init dance MUST target the Garage fixture.
#### Scenario: Both fixtures coexist
- **WHEN** a test environment registers both `MinioFixture` and `GarageS3Fixture`
- **THEN** both fixtures start in parallel with no shared port or env-var conflicts
### Requirement: Image-pinning policy
The default image MUST be a pinned `minio/minio:RELEASE.<date>` tag (latest stable RELEASE at implementation time). The `constants/index.ts` file MUST carry a `# TODO: replace with sha256:<digest>` comment until the digest is recorded.
#### Scenario: Pinned RELEASE tag
- **WHEN** the constants file is read
- **THEN** the image string starts with `minio/minio:RELEASE.` AND a sha256 TODO line is present
### Requirement: Env-bridge passthrough
The fixture MUST consult `MINIO_S3_ENDPOINT`, `MINIO_S3_ACCESS_KEY_ID`, `MINIO_S3_SECRET_ACCESS_KEY`, `MINIO_S3_REGION`, and `MINIO_S3_BUCKET`. When the first four are present and non-empty, `start()` MUST skip container creation and return a handle whose config points at the live MinIO. `MINIO_S3_BUCKET` is optional in passthrough mode.
#### Scenario: Live MinIO present
- **WHEN** the four required env vars are set
- **THEN** `start()` returns without invoking `containerFactory` AND the returned config's `endpoint` equals `MINIO_S3_ENDPOINT`
#### Scenario: Partial env triggers container
- **WHEN** `MINIO_S3_ENDPOINT` is set but `MINIO_S3_SECRET_ACCESS_KEY` is empty
- **THEN** `start()` falls back to container mode
### Requirement: Healthcheck
The fixture MUST poll `HTTP GET <endpoint>/minio/health/live` and treat a `200` response as ready. The default `healthTimeoutMs` MUST be `60000`. Callers MAY override.
#### Scenario: Healthcheck succeeds
- **WHEN** `/minio/health/live` returns 200
- **THEN** `start()` resolves
#### Scenario: Healthcheck times out
- **WHEN** the endpoint never returns 200 within `healthTimeoutMs`
- **THEN** `start()` rejects with code `INTEGRATION.MINIO.HEALTHCHECK_TIMEOUT`
### Requirement: Random keys per run
In container mode, when `accessKeyId`/`secretAccessKey` are not supplied, the fixture MUST generate cryptographically random keys for each run (≥ 20 chars, alphanumeric) and pass them as `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD` env vars to the container.
#### Scenario: Random keys
- **WHEN** the fixture starts twice without explicit keys
- **THEN** the two runs use different `accessKeyId` values
### Requirement: Bucket pre-seeding
When `bucket` is supplied, the fixture MUST create the bucket via the MinIO admin API after the healthcheck passes, using the injectable `fetchFn`. The fixture MUST NOT resolve `start()` until the bucket exists (verified via `HEAD <endpoint>/<bucket>` returning `200`).
#### Scenario: Bucket created
- **WHEN** `start({ bucket: 'app-uploads' })` resolves
- **THEN** `HEAD /app-uploads` returns 200 against the returned endpoint
### Requirement: Config shape
The exported `MinioConfig` interface MUST include `accessKeyId`, `secretAccessKey`, `region`, optional `bucket?: string`, optional `console?: boolean`, and the resolved `endpoint`. A sibling `minio-fixture.schemas.ts` MUST export a Zod `MinioConfigSchema` that validates the same shape.
#### Scenario: Schema parity
- **WHEN** `MinioConfigSchema.safeParse` runs against a `MinioConfig` value
- **THEN** valid configs parse OK AND missing required fields fail with `ZodError`
### Requirement: Error-code prefix
All errors thrown from the fixture MUST carry a `code` matching `^INTEGRATION\.MINIO\.[A-Z_]+$`.
#### Scenario: Coded error
- **WHEN** any fixture method throws
- **THEN** `code` starts with `INTEGRATION.MINIO.`
### Requirement: Injectable fakes
The fixture constructor MUST accept `containerFactory?: () => GenericContainer` and `fetchFn?: typeof fetch`. Unit tests MUST drive 100% Istanbul branch coverage without Docker or 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 — AWS-S3 parity
The fixture MUST support, end-to-end against a real container with an AWS S3 SDK client (`@aws-sdk/client-s3`), the following flows in its smoke test:
#### Scenario: PutObject and GetObject round-trip
- **WHEN** an SDK client puts an object then gets it back
- **THEN** the retrieved body bytes equal the put body bytes AND the `ETag` header is present
#### Scenario: Presigned GET URL works
- **WHEN** a presigned GET URL is created via `@aws-sdk/s3-request-presigner` and fetched directly with no SDK signing
- **THEN** the response is `200` with the object body
#### Scenario: Multipart upload works
- **WHEN** an object larger than the part-size threshold (≥ 5 MB) is uploaded via `CreateMultipartUpload` + `UploadPart` + `CompleteMultipartUpload`
- **THEN** the assembled object's body equals the concatenated source AND `HeadObject` reports the full content length
#### Scenario: Bucket policy attach
- **WHEN** a `PutBucketPolicy` request attaches a public-read policy
- **THEN** `GetBucketPolicy` returns the same JSON document

View File

@@ -0,0 +1,106 @@
## ADDED Requirements
### Requirement: Layer 0 dep-graph isolation
The `@suaveplan/testing-integration-mongodb` 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-mongodb...` is run
- **THEN** the only `@suaveplan/*` package in the runtime closure is `@suaveplan/testing-integration`
### Requirement: Image-pinning policy
The default image MUST be `mongo:7.0`. The `constants/index.ts` file MUST carry a `# TODO: replace with sha256:<digest>` comment until the digest is recorded.
#### Scenario: Pinned tag
- **WHEN** the constants file is read
- **THEN** the image string equals `mongo:7.0` AND a sha256 TODO line is present
### Requirement: Env-bridge passthrough
The fixture MUST consult `MONGODB_URL` and `MONGODB_DATABASE`. When both are present and non-empty, `start()` MUST skip container creation and return a handle whose config points at the live MongoDB.
#### Scenario: Live MongoDB present
- **WHEN** both env vars are set
- **THEN** `start()` returns without invoking `containerFactory` AND the returned config's `url` equals `MONGODB_URL`
#### Scenario: Partial env triggers container
- **WHEN** `MONGODB_URL` is set but `MONGODB_DATABASE` is empty
- **THEN** `start()` falls back to container mode
### Requirement: Healthcheck
The fixture MUST run `mongosh --eval "db.runCommand('ping')"` in-container via the injectable `execFn` and treat exit code `0` with stdout containing `ok` as ready. The default `healthTimeoutMs` MUST be `60000`. Callers MAY override.
#### Scenario: Healthcheck succeeds
- **WHEN** the in-container `mongosh` ping returns exit code 0 with `{ ok: 1 }` stdout
- **THEN** `start()` resolves
#### Scenario: Healthcheck times out
- **WHEN** the ping never succeeds within `healthTimeoutMs`
- **THEN** `start()` rejects with code `INTEGRATION.MONGODB.HEALTHCHECK_TIMEOUT`
### Requirement: Replica-set initialisation
When `replicaSet: true` is supplied, the fixture MUST start `mongod` with `--replSet rs0` and run `rs.initiate()` via `execFn` after the ping healthcheck passes. The fixture MUST NOT resolve `start()` until `rs.status().ok === 1`.
#### Scenario: Replica-set ready
- **WHEN** `start({ database: 'app', replicaSet: true })` resolves
- **THEN** a subsequent `rs.status()` call reports `ok: 1` AND a multi-document transaction succeeds against the returned `url`
### Requirement: Authentication mode
When `authentication: { username, password }` is supplied, the fixture MUST start `mongod` with `--auth`, create the supplied user via `execFn` against the `admin` database, and embed the credentials in the returned `url` (`mongodb://user:pass@host:port/database`).
#### Scenario: Auth rejects bad credentials
- **WHEN** the fixture starts in auth mode and a client connects with an incorrect password
- **THEN** the connection fails with a MongoDB authentication error
### Requirement: Config shape
The exported `MongoDbConfig` interface MUST include `database`, optional `replicaSet?: boolean`, optional `authentication?: { username: string; password: string }`, and the resolved `url`. A sibling `mongodb-fixture.schemas.ts` MUST export a Zod `MongoDbConfigSchema` that validates the same shape.
#### Scenario: Schema parity
- **WHEN** `MongoDbConfigSchema.safeParse` is run against a `MongoDbConfig` value
- **THEN** valid configs parse OK AND missing required fields fail with `ZodError`
### Requirement: Error-code prefix
All errors thrown from the fixture MUST carry a `code` matching `^INTEGRATION\.MONGODB\.[A-Z_]+$`.
#### Scenario: Coded error
- **WHEN** any fixture method throws
- **THEN** `code` starts with `INTEGRATION.MONGODB.`
### Requirement: Injectable fakes
The fixture constructor MUST accept `containerFactory?: () => GenericContainer` and `execFn?: (cmd: string[]) => Promise<{ stdout: string; stderr: string; exitCode: number }>`. Unit tests MUST be able to drive 100% Istanbul branch coverage without Docker.
#### Scenario: Fake injection
- **WHEN** the fixture is constructed with both fakes
- **THEN** real Docker is never invoked during the test
### Requirement: Plugin scenarios
The fixture MUST support, end-to-end against a real container, the following flows in its smoke test:
#### Scenario: Plain mode write/read round-trip
- **WHEN** the fixture is started in plain mode and an `insertOne` followed by a `findOne` is performed
- **THEN** the inserted document is read back unchanged
#### Scenario: Replica-set transaction round-trip
- **WHEN** the fixture is started with `replicaSet: true` and a multi-document transaction is committed
- **THEN** all inserts are visible after `commitTransaction()` AND none are visible if `abortTransaction()` is called instead

View File

@@ -0,0 +1,102 @@
## ADDED Requirements
### Requirement: Layer 0 dep-graph isolation
The `@suaveplan/testing-integration-mysql` 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-mysql...` is run
- **THEN** the only `@suaveplan/*` package in the runtime closure is `@suaveplan/testing-integration`
### Requirement: Image-pinning policy
The default image MUST be `mysql:8.4`. The `constants/index.ts` file MUST carry a `# TODO: replace with sha256:<digest>` comment until the digest is recorded.
#### Scenario: Pinned tag
- **WHEN** the constants file is read
- **THEN** the image string equals `mysql:8.4` AND a sha256 TODO line is present
### Requirement: Env-bridge passthrough
The fixture MUST consult `MYSQL_URL`, `MYSQL_DATABASE`, `MYSQL_USER`, `MYSQL_PASSWORD`, and `MYSQL_ROOT_PASSWORD`. When all five are present and non-empty, `start()` MUST skip container creation and return a handle whose config points at the live MySQL.
#### Scenario: Live MySQL present
- **WHEN** all five env vars are set
- **THEN** `start()` returns without invoking `containerFactory` AND the returned config's `url` equals `MYSQL_URL`
#### Scenario: Partial env triggers container
- **WHEN** `MYSQL_URL` is set but `MYSQL_ROOT_PASSWORD` is empty
- **THEN** `start()` falls back to container mode
### Requirement: Healthcheck
The fixture MUST run `mysqladmin ping -h localhost -p$MYSQL_ROOT_PASSWORD` in-container via the injectable `execFn` and treat exit code `0` with stdout containing `mysqld is alive` as ready. The default `healthTimeoutMs` MUST be `90000` (initial DB init can be slow on cold image pulls). Callers MAY override.
#### Scenario: Healthcheck succeeds
- **WHEN** in-container `mysqladmin ping` returns exit code 0 with `mysqld is alive`
- **THEN** `start()` resolves
#### Scenario: Healthcheck times out
- **WHEN** the ping never succeeds within `healthTimeoutMs`
- **THEN** `start()` rejects with code `INTEGRATION.MYSQL.HEALTHCHECK_TIMEOUT`
### Requirement: Charset and collation overrides
The fixture MUST default to `charset: 'utf8mb4'` and `collation: 'utf8mb4_0900_ai_ci'` and MUST pass `--character-set-server=<charset>` and `--collation-server=<collation>` to `mysqld` when overrides are supplied via config.
#### Scenario: UTF-8mb4 default
- **WHEN** the fixture is started with no charset override
- **THEN** `SHOW VARIABLES LIKE 'character_set_server'` returns `utf8mb4`
#### Scenario: Charset override applied
- **WHEN** the fixture is started with `charset: 'utf8'`
- **THEN** `SHOW VARIABLES LIKE 'character_set_server'` returns `utf8`
### Requirement: Config shape
The exported `MySqlConfig` interface MUST include `database`, `user`, `password`, `rootPassword`, optional `charset?: string`, optional `collation?: string`, and the resolved `url`. A sibling `mysql-fixture.schemas.ts` MUST export a Zod `MySqlConfigSchema` that validates the same shape.
#### Scenario: Schema parity
- **WHEN** `MySqlConfigSchema.safeParse` is run against a `MySqlConfig` value
- **THEN** valid configs parse OK AND missing required fields fail with `ZodError`
### Requirement: Error-code prefix
All errors thrown from the fixture MUST carry a `code` matching `^INTEGRATION\.MYSQL\.[A-Z_]+$`.
#### Scenario: Coded error
- **WHEN** any fixture method throws
- **THEN** `code` starts with `INTEGRATION.MYSQL.`
### Requirement: Injectable fakes
The fixture constructor MUST accept `containerFactory?: () => GenericContainer` and `execFn?: (cmd: string[]) => Promise<{ stdout: string; stderr: string; exitCode: number }>`. Unit tests MUST drive 100% Istanbul branch coverage without Docker.
#### Scenario: Fake injection
- **WHEN** the fixture is constructed with both fakes
- **THEN** real Docker is never invoked during the test
### Requirement: Plugin scenarios
The fixture MUST support, end-to-end against a real container, the following flows in its smoke test:
#### Scenario: Transaction round-trip
- **WHEN** a transaction inserts rows and commits
- **THEN** the rows are visible to a subsequent connection AND a rolled-back transaction's rows are not visible
#### Scenario: DDL post-start
- **WHEN** `CREATE TABLE`, `ALTER TABLE ADD COLUMN`, and `DROP TABLE` statements run after `start()` resolves
- **THEN** each statement succeeds AND `INFORMATION_SCHEMA.TABLES` reflects the change

View File

@@ -0,0 +1,120 @@
## ADDED Requirements
### Requirement: Layer 0 dep-graph isolation
The `@suaveplan/testing-integration-nats` 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-nats...` is run
- **THEN** the only `@suaveplan/*` package in the runtime closure is `@suaveplan/testing-integration`
### Requirement: Image-pinning policy
The default image MUST be `nats:2.10-alpine`. The `constants/index.ts` file MUST carry a `# TODO: replace with sha256:<digest>` comment until the digest is recorded.
#### Scenario: Pinned tag
- **WHEN** the constants file is read
- **THEN** the image string equals `nats:2.10-alpine` AND a sha256 TODO line is present
### Requirement: Env-bridge passthrough
The fixture MUST consult `NATS_URL`, optional `NATS_CLUSTER_NAME`, and optional `NATS_JETSTREAM_DOMAIN`. When `NATS_URL` is present and non-empty, `start()` MUST skip container creation and return a handle whose config points at the live NATS server. Optional vars influence the returned config but are not required.
#### Scenario: Live NATS present
- **WHEN** `NATS_URL` is set
- **THEN** `start()` returns without invoking `containerFactory` AND the returned config's `url` equals `NATS_URL`
#### Scenario: No env triggers container
- **WHEN** `NATS_URL` is unset or empty
- **THEN** `start()` falls back to container mode
### Requirement: Healthcheck
The fixture MUST perform a TCP PING on the mapped client port (`4222` inside the container) via the injectable `tcpPingFn`. When the container exposes the monitoring endpoint (`-m 8222`), the fixture MUST also poll `HTTP GET /healthz` and treat a `200` response as ready. The default `healthTimeoutMs` MUST be `60000`. Callers MAY override.
#### Scenario: TCP ping succeeds
- **WHEN** `tcpPingFn(host, mappedClientPort)` returns true
- **THEN** `start()` resolves once any pre-seeding has completed
#### Scenario: Healthcheck times out
- **WHEN** the TCP ping never succeeds within `healthTimeoutMs`
- **THEN** `start()` rejects with code `INTEGRATION.NATS.HEALTHCHECK_TIMEOUT`
### Requirement: JetStream initialisation
When `jetstream: true` is supplied, the fixture MUST start `nats-server` with `-js` and pre-seed any supplied streams and durable consumers via `nats stream add` / `nats consumer add` invoked through the injectable `execFn` after the TCP-ping healthcheck passes. The fixture MUST NOT resolve `start()` until each declared stream reports `state.messages >= 0` via `nats stream info`.
#### Scenario: JetStream stream seeded
- **WHEN** `start({ jetstream: true, streams: [{ name: 'EVENTS', subjects: ['events.>'] }] })` resolves
- **THEN** `nats stream info EVENTS` reports the stream exists with the declared subjects
### Requirement: Cluster mode
When `cluster: { name, routes }` is supplied, the fixture MUST start `nats-server` with `--cluster_name <name>` and the supplied `--routes` list. The fixture MUST verify cluster formation by calling `nats server list` via `execFn` and checking the cluster name matches.
#### Scenario: Cluster formed
- **WHEN** the fixture is started with a cluster config
- **THEN** `nats server list` reports `cluster: <name>`
### Requirement: Authentication mode
The fixture MUST accept `auth: { user: string; password: string }` OR `auth: { token: string }` (mutually exclusive). When supplied, the fixture MUST start `nats-server` with the corresponding `--user`/`--pass` or `--auth` flags AND embed credentials in the returned `url` (`nats://user:pass@host:port` or `nats://token@host:port`).
#### Scenario: Token auth rejects bad token
- **WHEN** the fixture starts with `auth: { token: 'good' }` and a client connects with token `bad`
- **THEN** the client connection is refused with a NATS authorization error
### Requirement: Config shape
The exported `NatsConfig` interface MUST include optional `jetstream?: boolean`, optional `cluster?: { name: string; routes: string[] }`, optional `auth?: { user: string; password: string } | { token: string }`, optional `streams?: Array<{ name: string; subjects: string[] }>`, optional `consumers?: Array<{ stream: string; durable: string }>`, and the resolved `url`. A sibling `nats-fixture.schemas.ts` MUST export a Zod `NatsConfigSchema` that validates the same shape including the `auth` discriminated union.
#### Scenario: Schema parity
- **WHEN** `NatsConfigSchema.safeParse` runs against a `NatsConfig` value
- **THEN** valid configs parse OK AND mixing `user`/`password` with `token` in the same `auth` object fails with `ZodError`
### Requirement: Error-code prefix
All errors thrown from the fixture MUST carry a `code` matching `^INTEGRATION\.NATS\.[A-Z_]+$`.
#### Scenario: Coded error
- **WHEN** any fixture method throws
- **THEN** `code` starts with `INTEGRATION.NATS.`
### Requirement: Injectable fakes
The fixture constructor MUST accept `containerFactory?: () => GenericContainer`, `tcpPingFn?: (host: string, port: number) => Promise<boolean>`, and `execFn?: (cmd: string[]) => Promise<{ stdout: string; stderr: string; exitCode: number }>`. Unit tests MUST drive 100% Istanbul branch coverage without Docker.
#### Scenario: Fake injection
- **WHEN** the fixture is constructed with all three fakes
- **THEN** real Docker / real TCP / real exec are never invoked during the test
### Requirement: Plugin scenarios
The fixture MUST support, end-to-end against a real container, the following flows in its smoke test:
#### Scenario: Core pub/sub round-trip
- **WHEN** a subscriber subscribes to `test.>` and a publisher publishes to `test.hello`
- **THEN** the subscriber receives the message within 1s
#### Scenario: JetStream durable consumer
- **WHEN** the fixture seeds `EVENTS` stream + `worker` durable consumer, a publisher publishes 10 messages, and the consumer pulls
- **THEN** the consumer acks all 10 messages AND `nats consumer info EVENTS worker` reports `num_pending: 0`
#### Scenario: Request/reply
- **WHEN** a responder subscribes to `svc.echo` and replies with the request payload, and a client issues a request
- **THEN** the client receives the original payload within the request timeout

View File

@@ -0,0 +1,107 @@
## 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<OidcConfig>` 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:<digest>` 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 <issuerUrl>/.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`