managarten/scripts/run-integration-tests.sh
Till JS d941ff2231 fix(mana-auth): account lockout was structurally dead + add failure-path tests
While adding negative-path integration tests for the auth flow I
discovered that *neither* of the lockout primitives in
services/mana-auth/src/services/security.ts has actually been
working in production. Two independent silent failures that combined
into a "the lockout never triggers, ever" outcome:

1. recordAttempt() inserted into auth.login_attempts with explicit
   `id = gen_random_uuid()`, but auth.login_attempts.id is a
   `serial integer` column with `nextval('auth.login_attempts_id_seq')`
   as default. The UUID-into-integer cast threw a type error every
   single time, the bare `catch {}` swallowed it as "non-critical",
   and not a single login attempt was ever persisted. Lockout's "5
   failures in 15 min" check was running against an empty table.

2. checkLockout() built `attempted_at > ${new Date(...)}` via the
   drizzle sql template, but postgres-js cannot bind a JS Date object
   directly — it tries to byteLength() the parameter and crashes with
   `Received an instance of Date`. Same anti-pattern: bare `catch`,
   returns `{locked: false}` (fail-open), no log, completely invisible.

Both are "silent broken since the encryption-vault series of changes"
class — caught only because the integration test for the lockout flow
expected the 6th login attempt to return 429 and got 200 instead.

Fixes:
- recordAttempt(): drop the bogus `id` column from the INSERT (let the
  sequence default assign it), default ipAddress to null instead of
  letting `${undefined}` collapse the parameter slot, and surface
  errors in the catch instead of swallowing them silently.
- checkLockout(): pass `windowStart.toISOString()` instead of the Date
  object so postgres-js can serialize it. Same catch upgrade — log the
  cause when failing open.

Failure-path test additions (tests/integration/auth-failures.test.ts):
- wrong password: assert 401, no JWT, +1 LOGIN_FAILURE in security_events,
  +1 row in auth.login_attempts
- account lockout: 5 failed attempts then 6th returns 429 with
  remainingSeconds, even with the correct password
- unverified email login: 403 with code = EMAIL_NOT_VERIFIED
- validate with garbage token: valid !== true
- resend verification: second mail arrives in mailpit

Plus the run-integration-tests.sh helper now runs both .test.ts files
and tests/integration/package.json's `test` script does the same.

Negative-control: reverted the recordAttempt fix (re-added the bogus
gen_random_uuid id), the wrong-password test failed at the
login_attempts assertion. Reverted the checkLockout fix, the lockout
test failed at the 429 assertion. Both fixes verified to be load-bearing.

6 tests, 45 expects, ~1.3s on a warm cache.
2026-04-08 18:29:00 +02:00

76 lines
2.7 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# Run the auth-flow integration test against a fresh docker-compose.test.yml stack.
#
# Usage:
# ./scripts/run-integration-tests.sh # build, run, tear down
# KEEP_STACK=1 ./scripts/run-integration-tests.sh # leave stack up after run
#
# In CI, just call this. Locally, useful for both quick reruns and the
# "wait what does Mailpit look like" debugging step (set KEEP_STACK=1 then
# open http://127.0.0.1:8026).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
COMPOSE_FILE="$REPO_ROOT/docker-compose.test.yml"
TEST_DIR="$REPO_ROOT/tests/integration"
# Pick a docker binary that's actually on PATH. macOS layouts vary.
if command -v docker >/dev/null 2>&1; then
DOCKER=docker
elif [ -x /opt/homebrew/bin/docker ]; then
DOCKER=/opt/homebrew/bin/docker
elif [ -x /usr/local/bin/docker ]; then
DOCKER=/usr/local/bin/docker
else
echo "error: docker not found in PATH" >&2
exit 1
fi
cd "$REPO_ROOT"
cleanup() {
if [ "${KEEP_STACK:-0}" = "1" ]; then
echo
echo "==> KEEP_STACK=1, leaving the test stack up."
echo " Mailpit UI: http://127.0.0.1:8026"
echo " mana-auth: http://127.0.0.1:3091"
echo " Postgres: psql postgresql://mana:testpassword@localhost:5443/mana_platform"
echo " Tear down: $DOCKER compose -f $COMPOSE_FILE down -v"
return
fi
echo
echo "==> Tearing down test stack"
$DOCKER compose -f "$COMPOSE_FILE" down -v --remove-orphans >/dev/null 2>&1 || true
}
trap cleanup EXIT
echo "==> Building & starting test stack"
$DOCKER compose -f "$COMPOSE_FILE" up -d --build --wait
echo "==> Pushing mana-auth Drizzle schema into test postgres"
DATABASE_URL="postgresql://mana:testpassword@localhost:5443/mana_platform" \
pnpm --filter @mana/auth db:push --force >/dev/null
echo "==> Applying encryption-vault SQL migrations (002, 003)"
$DOCKER cp "$REPO_ROOT/services/mana-auth/sql/002_encryption_vaults.sql" \
mana-test-postgres:/tmp/002.sql
$DOCKER cp "$REPO_ROOT/services/mana-auth/sql/003_recovery_wrap.sql" \
mana-test-postgres:/tmp/003.sql
$DOCKER exec mana-test-postgres psql -U mana -d mana_platform -f /tmp/002.sql >/dev/null
$DOCKER exec mana-test-postgres psql -U mana -d mana_platform -f /tmp/003.sql >/dev/null
echo "==> Restarting mana-auth so it picks up the freshly-created tables"
# mana-auth's connection pool might have been opened against an empty DB. A
# quick restart guarantees a clean cache + schema view.
$DOCKER compose -f "$COMPOSE_FILE" restart mana-auth >/dev/null
$DOCKER compose -f "$COMPOSE_FILE" up -d --wait mana-auth
echo "==> Running integration tests"
cd "$TEST_DIR"
bun test auth-flow.test.ts auth-failures.test.ts
echo
echo "✅ integration tests passed"