Initial Commit
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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`
|
||||
Reference in New Issue
Block a user