# testing-integration-postgres ## Purpose PostgreSQL 16 `ServiceFixture` for integration tests. Provides env-bridge passthrough (`POSTGRES_URL`), `pg_isready` in-container health checking, injectable exec function, and configurable database, user, and password. Enables real PostgreSQL integration tests without mocks. Depends on `@suaveplan/testing-integration` and `testcontainers`. ## Requirements ### Requirement: PostgresFixture implements ServiceFixture The package SHALL export `PostgresFixture` implementing `ServiceFixture` 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"`