Files
2026-05-27 23:29:53 +01:00

136 lines
6.2 KiB
Markdown

# Capability Spec Delta: testing-integration-postgres (testing-integration-spec-and-generics-hardening)
**Package**: `@suaveplan/testing-integration-postgres`
**Status**: PROPOSED
## ADDED Requirements
### Requirement: PostgresFixture implements ServiceFixture<PostgresConfig>
The package SHALL export `PostgresFixture` implementing `ServiceFixture<PostgresConfig>` with `name === "postgres"`. `PostgresConfig` SHALL be `{ url: string; host: string; port: number; database: string; username: string; password: string }`.
#### Scenario: name property is "postgres"
- **WHEN** an instance's `.name` is read
- **THEN** the value is the literal `"postgres"`
**Verified by:** `src/fixture/postgres-fixture.test.ts::"name property returns postgres"`
---
### Requirement: Env-bridge passthrough mode
When `POSTGRES_URL` is set and non-empty, `start()` MUST parse it via `new URL(...)` and return the config without starting a container. The fallback port (when omitted) MUST be `POSTGRES_CONTAINER_PORT`. In passthrough mode `stop()` MUST be a no-op.
#### Scenario: parses all config from POSTGRES_URL
- **WHEN** `POSTGRES_URL=postgresql://u:p@h:5433/db` is set
- **THEN** `start()` returns `{ url, host, port: 5433, database: "db", username: "u", password: "p" }`
#### Scenario: stop is a no-op in passthrough mode
- **WHEN** the fixture started in passthrough mode and `stop()` is called
- **THEN** no container is touched
#### Scenario: uses default port when URL has no port
- **WHEN** the URL omits the port
- **THEN** the fixture falls back to `POSTGRES_CONTAINER_PORT`
**Verified by:** `src/fixture/postgres-fixture.test.ts::"passthrough mode: parses all config from POSTGRES_URL"` AND `src/fixture/postgres-fixture.test.ts::"passthrough mode: stop is a no-op"` AND `src/fixture/postgres-fixture.test.ts::"passthrough mode: uses default port when URL has no port"`
---
### Requirement: Container mode
When no `POSTGRES_URL` is set, `start()` MUST start a Postgres container via the injected `containerFactory` (or `GenericContainer`), set `POSTGRES_USER`/`POSTGRES_PASSWORD`/`POSTGRES_DB` env vars, expose `POSTGRES_CONTAINER_PORT`, and return a config with a populated `url`.
#### Scenario: container mode happy path returns correct URL
- **WHEN** `start()` completes container boot
- **THEN** `result.url` reflects the username, password, mapped port, and database
#### Scenario: stop calls container.stop()
- **WHEN** `stop()` follows `start()` in container mode
- **THEN** the container's `stop()` is invoked exactly once
#### Scenario: custom database/username/password reflected in config and URL
- **WHEN** the fixture is constructed with overrides
- **THEN** the URL and config fields use those overrides
#### Scenario: factory falls back to dynamic GenericContainer import
- **WHEN** no `containerFactory` is injected
- **THEN** the fixture dynamically imports testcontainers and uses `GenericContainer`
**Verified by:** `src/fixture/postgres-fixture.test.ts::"container mode happy path: returns correct URL"` AND `src/fixture/postgres-fixture.test.ts::"container mode: stop calls container.stop()"` AND `src/fixture/postgres-fixture.test.ts::"custom database/username/password reflected in config and URL"` AND `src/fixture/postgres-fixture.test.ts::"resolveContainerFactory: falls back to GenericContainer import"`
---
### Requirement: pg_isready in-container health check
In container mode, `healthCheck()` MUST execute `pg_isready -U postgres` inside the container and return `true` only when `exitCode === 0`. The exec function MUST be injectable as `options.execFn`. Without injection it MUST default to `container.exec(cmd)`. Without a started container, `healthCheck()` MUST return `false`.
#### Scenario: returns true when execFn exit code 0
- **WHEN** the injected `execFn` resolves to `{ exitCode: 0 }`
- **THEN** `healthCheck()` resolves to `true`
#### Scenario: returns false on non-zero exit code
- **WHEN** the injected `execFn` resolves to `{ exitCode: 1 }`
- **THEN** `healthCheck()` resolves to `false`
#### Scenario: defaults to container.exec when no execFn injected
- **WHEN** no `execFn` is supplied
- **THEN** `healthCheck()` calls into `container.exec(["pg_isready", "-U", "postgres"])`
#### Scenario: returns false when no container
- **WHEN** `healthCheck()` is called before `start()`
- **THEN** the result is `false`
**Verified by:** `src/fixture/postgres-fixture.test.ts::"healthCheck returns true via injected execFn (exitCode 0)"` AND `src/fixture/postgres-fixture.test.ts::"healthCheck returns false via injected execFn (exitCode 1)"` AND `src/fixture/postgres-fixture.test.ts::"healthCheck uses container.exec when no execFn provided"` AND `src/fixture/postgres-fixture.test.ts::"healthCheck returns false when no container"`
---
### Requirement: stop() is idempotent
`stop()` MUST be safe to call when no container has been started.
#### Scenario: stop without start is a no-op
- **WHEN** `stop()` is called on a freshly-constructed fixture
- **THEN** no error is thrown
**Verified by:** `src/fixture/postgres-fixture.test.ts::"stop when no container is a no-op"`
---
### Requirement: passthrough healthCheck semantics — documented
In passthrough mode the fixture has no container; `healthCheck()` returns `false`. Consumers using passthrough mode MUST NOT poll passthrough fixtures via `HealthPoller` — readiness is the operator's responsibility (the env-bridge URL is assumed to point at an already-healthy service).
#### Scenario: passthrough mode does not call container.stop()
- **WHEN** the fixture started in passthrough mode and `stop()` is called
- **THEN** `container.stop()` is never invoked
**Verified by:** `src/fixture/postgres-fixture.test.ts::"passthrough mode: stop is a no-op"`
---
### Requirement: testing-integration is a peer dependency
The package's `package.json` MUST list `@suaveplan/testing-integration` and `testcontainers` in `peerDependencies`, NOT in `dependencies`. They MUST also appear in `devDependencies` so the workspace symlink resolves for the package's own tests.
#### Scenario: variant does not bundle the core or testcontainers
- **WHEN** the published `package.json` is inspected
- **THEN** `dependencies` does not contain either symbol
**Verified by:** `src/fixture/postgres-fixture.test.ts::"name property returns postgres"`