testharness

package
v0.5.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: AGPL-3.0 Imports: 21 Imported by: 0

README

testharness

Single source of Postgres + Redis wiring for the API service's integration tests.

What it provides

testharness.New(t) returns a *Harness with:

Method Returns Purpose
Pool() *pgxpool.Pool pgx connection pool (for pgx-based stores)
SQLDB() *sql.DB database/sql handle (for stores like PgEmailTokenStore)
Redis() *redis.Client client backed by an isolated miniredis
Miniredis() *miniredis.Miniredis direct access for TTL / key-scan assertions
MigrateUp() / MigrateDown() error apply/revert the embedded migration set (idempotent)
Reset() error TRUNCATE every user table (RESTART IDENTITY CASCADE); never touches schema_migrations
NewContext() context.Context context with a 30s test deadline (cancel wired to t.Cleanup)
Logger() / Logs() *zap.Logger / *ObservedLogs in-memory log capture for "no ERROR was logged" assertions
Seed(table, row) string thin typed convenience for "insert a user, get its id"
DSN() / ID() string resolved connection string / per-instance unique marker

Every handle is closed via t.Cleanup, so tests never manage teardown.

When to use the harness vs. mocks

The test needs… Use
One function, no I/O A plain unit test — no harness
Real Postgres semantics (constraints, arrays, ON CONFLICT, FK cascade) The harness
Real Redis semantics (TTL, pipelining, Lua) The harness (Redis() / Miniredis())
Multiple services wired against a real DB The harness
A full HTTP request through router → service → DB The harness + httptest
K8s client interactions controller-runtime/client/fakenot this harness

Rule of thumb (epic design principle P5): a mock that reimplements a database badly is the most common source of "tests pass, prod breaks" in this codebase. If the behaviour under test depends on Postgres/Redis being correct, use the harness; if it depends only on call sequencing, a small testify mock is fine.

Isolation model

The harness connects to a single, shared, externally-provisioned test Postgres given by TEST_DATABASE_URL (default postgres://postgres:testpass@localhost:5433/llmsafespaces_test?sslmode=disable). If Postgres is unreachable, New(t) calls t.Skip — so go test ./... stays green on a machine without Docker/Postgres, and CI (where TEST_DATABASE_URL is provisioned) runs the suite for real.

  • Migrations are applied on construction and are idempotent: in CI the DB is already migrated by the migrate step, so MigrateUp is a no-op (ErrNoChange); on a fresh local DB the embedded migration set brings the schema current.
  • Per-test isolation follows the project convention: use a unique marker per test (see ID()) so parallel tests do not collide. Reset() is provided for non-parallel tests that need a clean slate; it truncates user tables and never touches schema_migrations.
  • Redis is a per-harness miniredis instance, so Redis state is always isolated automatically.

This matches how the codebase's integration tests already work (unique IDs on a shared DB). It deliberately does not provide per-instance database isolation (the testcontainers path); that was evaluated and rejected to avoid introducing a Docker dependency — see the worklog for US-52.6.

Minimal example

//go:build integration

package myfeature_test

import (
	"context"
	"testing"

	"github.com/lenaxia/llmsafespaces/api/internal/testharness"
)

func TestMyStore_RoundTrip(t *testing.T) {
	h := testharness.New(t)
	store := NewMyStore(h.Pool())
	ctx := h.NewContext()

	id := "my-" + h.ID()
	if _, err := h.Pool().Exec(ctx,
		`INSERT INTO users (id, username, email, password_hash, active, role)
		 VALUES ($1, $2, $3, 'h', true, 'user')`,
		id, "u_"+id, id+"@test"); err != nil {
		t.Fatalf("seed: %v", err)
	}
	t.Cleanup(func() {
		_, _ = h.Pool().Exec(context.Background(), "DELETE FROM users WHERE id = $1", id)
	})

	// … exercise the store …
}

Scope and limitations

  • Only api/-rooted tests can import this package. It lives under api/internal/, so Go's internal-visibility rule forbids imports from outside api/. In particular, pkg/secrets integration tests cannot use it; they retain their own getTestPool helper. Cross-layer consolidation of those tests is deferred (would require relocating the harness core to pkg/testharness with an injectable migration source).
  • No K8s. This harness is for Postgres + Redis. K8s integration uses envtest (US-52.1) and the kind e2e runner (US-52.7).
  • MigrateDown() is destructive. On the shared test DB it destroys the schema for every other test; use it only against a throwaway database.

Verification

# Pure-helper unit tests (run everywhere, no DB required):
go test -timeout 60s -race ./api/internal/testharness/...

# Contract tests (require TEST_DATABASE_URL; skip otherwise):
go test -tags integration -timeout 120s -race ./api/internal/testharness/...

Documentation

Overview

Package testharness is the single source of Postgres + Redis wiring for the API service's integration tests.

It consolidates the two near-identical pool constructors duplicated across api/internal/services/database (getIntegrationPool, newIntegrationDB) and adds the migration runner the codebase was missing — so an integration test never has to reinvent Postgres/Redis setup or assume the schema is pre-migrated. The third constructor, getTestPool in pkg/secrets, cannot be consolidated here because Go's internal-package visibility forbids pkg/ imports of api/internal/; that duplication is documented and deferred (see README.md).

Isolation model

The harness connects to a single, shared, externally-provisioned test Postgres (TEST_DATABASE_URL; skipped if unreachable). This matches the project's existing integration-test contract. Per-test isolation follows the project convention: use unique IDs/markers per test so parallel tests do not collide. Reset() is provided for non-parallel tests that need a clean slate; it never touches schema_migrations.

See README.md in this package for when to use the harness vs. unit-test mocks.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func MigrationFiles

func MigrationFiles() ([]string, error)

MigrationFiles returns the embedded migration file names (the .up.sql set), sorted ascending by name so callers see versions in apply order.

Types

type Harness

type Harness struct {
	// contains filtered or unexported fields
}

Harness holds the Postgres and Redis handles for one integration test.

Construct with New; never zero-value it. New registers a t.Cleanup that closes every handle, so tests do not manage teardown themselves.

func New

func New(t *testing.T) *Harness

New connects to the test Postgres and a fresh miniredis. If Postgres is unreachable it skips the calling test (matching the project's existing integration-test contract, so a dev without Docker still gets a green `go test ./...`). Migrations are applied if the schema is not current.

All handles are torn down via t.Cleanup.

func (*Harness) Close

func (h *Harness) Close()

Close releases every handle. Idempotent. New registers this as the test's t.Cleanup; tests may also call it directly to verify teardown behavior.

func (*Harness) DSN

func (h *Harness) DSN() string

DSN returns the resolved connection string, for tests that construct their own pool (e.g. to exercise pool-level behavior).

func (*Harness) ID

func (h *Harness) ID() string

ID returns a short, process-unique identifier for this harness instance, for generating unique test markers (the project's parallel-isolation convention).

func (*Harness) Logger

func (h *Harness) Logger() *zap.Logger

Logger returns a zap.Logger whose output is captured in memory. Assert on emitted entries via Logs().

func (*Harness) Logs

func (h *Harness) Logs() *observer.ObservedLogs

Logs returns the captured log entries for log-based assertions (e.g. "no ERROR was emitted").

func (*Harness) MigrateDown

func (h *Harness) MigrateDown() error

MigrateDown reverts all migrations. Use only against a throwaway database; on the shared test DB it destroys the schema for every other test.

func (*Harness) MigrateUp

func (h *Harness) MigrateUp() error

MigrateUp applies all pending migrations. It is idempotent: a no-op (returns nil) when the schema is already current.

func (*Harness) Miniredis

func (h *Harness) Miniredis() *miniredis.Miniredis

Miniredis returns the underlying miniredis for advanced assertions (TTL, key scans) that the redis client cannot express.

func (*Harness) NewContext

func (h *Harness) NewContext() context.Context

NewContext returns a context carrying a test-scoped deadline, derived from the harness root context so Close cancels it. Each call's cancel is wired to t.Cleanup so timers never leak.

func (*Harness) Pool

func (h *Harness) Pool() *pgxpool.Pool

Pool returns a pgx connection pool to the test Postgres.

func (*Harness) Redis

func (h *Harness) Redis() *redis.Client

Redis returns a redis client backed by the harness's isolated miniredis.

func (*Harness) Reset

func (h *Harness) Reset() error

Reset truncates every user table in the public schema, restarting identity sequences, and never touches schema_migrations. Use it in non-parallel tests that need a clean slate; for parallel tests, prefer unique per-test IDs (see ID()).

func (*Harness) SQLDB

func (h *Harness) SQLDB() *sql.DB

SQLDB returns a database/sql handle to the same Postgres, for stores built on database/sql (e.g. the email-token store). It is a distinct handle from the one used internally for migrations.

func (*Harness) Seed

func (h *Harness) Seed(table string, row map[string]any) string

Seed inserts one row into the named table from a column→value map and returns the value of its "id" column. It is a thin convenience over Pool() for the common "insert a user/workspace/org, get its id" shape; it does not model relationships or arbitrary primary-key names.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL