Files
suaveplan/openspec/specs/testing-integration-postgres/spec.md
2026-05-27 23:29:53 +01:00

6.4 KiB

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<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"