test(db): resolve the integration DSN at run time instead of storing an IP (#161)
ci / lint-test (push) Successful in 1m5s

setup-test-db.sh wrote .env.test once with the Postgres container's bridge IP
baked in, and Docker reassigns those on recreation. The file rotted silently:
its stored 172.22.0.18 belongs to **gitea** as of today, and an earlier drift
had the suite pointing its TRUNCATE at **postgres-pantry** — which it would
have wiped had the credentials happened to match.

Root-cause fix: stop persisting a host at all.

- scripts/resolve-test-db.sh re-derives the DSN from the live container and
  rewrites .env.test. Resolution order is TEST_DATABASE_URL, then the
  container's published host port, then the bridge IP. `docker port
  postgres-personal` reports 127.0.0.1:5432 since the 2026-08-17 PG17
  migration, so loopback is what gets used here — and a loopback address
  cannot be reassigned to another container, which is the whole point.
  (Note this supersedes the smarthome CLAUDE.md's "postgres-personal publishes
  no host port"; that is now stale.) The bridge-IP fallback is kept for a host
  where the port is not published, and the script never prints the password.
- Wired as `pretest:integration`, so npm runs it before every integration run.
  .env.test becomes a generated artifact rather than stored configuration.
- setup-test-db.sh now only creates the database and copies the schema, then
  delegates to the resolver.
- helpers.ts::assertTestDatabase stays and its message now points at the
  resolver rather than at `docker inspect`. It still earns its place: running
  vitest directly bypasses the pretest hook, and a stale .env.test would then
  still be read.

Docs: CLAUDE.md gains a Testing section carrying this, the prisma-generate
trap for a fresh checkout or worktree, and the frozen-clock rule for tests
whose subject is defined relative to "now". AGENTS.md updated for the same, and
for lint now being blocking.
This commit is contained in:
2026-08-21 00:52:54 +10:00
parent eaf5d5ac66
commit b958d05287
6 changed files with 143 additions and 30 deletions
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
# Resolve the integration-test DSN at run time and (re)write .env.test.
#
# Why this exists (#161): .env.test used to be written ONCE by
# setup-test-db.sh with the Postgres container's bridge IP baked in. Container
# IPs are reassigned on recreation, so the file rotted silently — by
# 2026-08-21 its 172.22.0.18 belonged to *gitea*, and an earlier drift had the
# suite pointing its TRUNCATE at postgres-pantry. It only failed safe because
# the credentials happened not to match.
#
# The fix is to stop persisting a host at all: this runs as `pretest:integration`,
# so the DSN is re-derived from the live container immediately before every run.
#
# Resolution order:
# 1. An explicit TEST_DATABASE_URL in the environment wins (CI, remote DB).
# 2. The container's published host port (`docker port`) — postgres-personal
# publishes 127.0.0.1:5432 since the 2026-08-17 PG17 migration. Preferred
# because a loopback address cannot be reassigned to another container.
# 3. The container's bridge IP, for a host where the port is not published.
#
# helpers.ts::assertTestDatabase is the backstop: it refuses to TRUNCATE
# anything whose current_database() is not `personal_test`.
set -euo pipefail
PG_CONTAINER=${PG_CONTAINER:-postgres-personal}
PG_USER=${PG_USER:-personal}
PG_PASS=${PG_PASS:-personalpassword123}
TEST_DB=${TEST_DB:-personal_test}
ENV_FILE="$(cd "$(dirname "$0")/.." && pwd)/.env.test"
if [ -n "${TEST_DATABASE_URL:-}" ]; then
DSN="$TEST_DATABASE_URL"
SOURCE="TEST_DATABASE_URL"
else
if ! docker inspect "$PG_CONTAINER" >/dev/null 2>&1; then
echo "ERROR: container '$PG_CONTAINER' not found. Start it, or set TEST_DATABASE_URL." >&2
exit 1
fi
# `docker port` prints e.g. "127.0.0.1:5432" (possibly several lines).
PUBLISHED=$(docker port "$PG_CONTAINER" 5432/tcp 2>/dev/null | head -1 || true)
if [ -n "$PUBLISHED" ]; then
HOSTPORT="$PUBLISHED"
SOURCE="published port"
else
IP=$(docker inspect "$PG_CONTAINER" \
--format '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' 2>/dev/null \
| tr ' ' '\n' | grep -v '^$' | head -1)
if [ -z "$IP" ]; then
echo "ERROR: '$PG_CONTAINER' has neither a published 5432 nor a bridge IP." >&2
exit 1
fi
HOSTPORT="$IP:5432"
SOURCE="bridge IP"
fi
DSN="postgresql://$PG_USER:$PG_PASS@$HOSTPORT/$TEST_DB"
fi
printf 'DATABASE_URL=%s\n' "$DSN" > "$ENV_FILE"
# Never print the password.
echo "resolve-test-db: ${DSN%%://*}://***@${DSN#*@} (via $SOURCE)"
+14 -18
View File
@@ -1,25 +1,24 @@
#!/bin/bash
# Creates the personal_test database and writes .env.test
# Run once before integration tests: npm run test:setup
# Creates the personal_test database and copies the production schema into it.
# Run once (and after any migration) before integration tests: npm run test:setup
#
# It no longer writes a host into .env.test. That was the #161 defect: the file
# was written once with the container's bridge IP and then rotted as the IP was
# reassigned. scripts/resolve-test-db.sh now re-derives the DSN before every
# integration run (wired as `pretest:integration`), and this script just calls it
# at the end so a fresh setup leaves a usable .env.test behind.
set -e
PG_CONTAINER=postgres-personal
PG_USER=personal
PG_PASS=personalpassword123
TEST_DB=personal_test
PG_CONTAINER=${PG_CONTAINER:-postgres-personal}
PG_USER=${PG_USER:-personal}
TEST_DB=${TEST_DB:-personal_test}
# Discover the container's bridge IP (accessible from the host on Linux)
PG_HOST=$(docker inspect "$PG_CONTAINER" \
--format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 2>/dev/null | head -1)
if [ -z "$PG_HOST" ]; then
if ! docker inspect "$PG_CONTAINER" >/dev/null 2>&1; then
echo "ERROR: Could not find container '$PG_CONTAINER'"
exit 1
fi
echo "Postgres container at $PG_HOST:5432"
# Create test database (ignore error if it already exists)
docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d postgres \
-c "CREATE DATABASE $TEST_DB;" 2>/dev/null || true
@@ -31,9 +30,6 @@ docker exec "$PG_CONTAINER" pg_dump -U "$PG_USER" --schema-only "$PG_USER" \
echo "Schema ready."
# Write .env.test
cat > "$(dirname "$0")/../.env.test" << EOF
DATABASE_URL=postgresql://$PG_USER:$PG_PASS@$PG_HOST:5432/$TEST_DB
EOF
bash "$(dirname "$0")/resolve-test-db.sh"
echo ".env.test written — integration tests can now run with: npm run test:integration"
echo "Integration tests can now run with: npm run test:integration"