137 lines
6.4 KiB
Markdown
137 lines
6.4 KiB
Markdown
# 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<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"`
|