Skip to content

feat(session-management): implement session expiration handling and U… #352

feat(session-management): implement session expiration handling and U…

feat(session-management): implement session expiration handling and U… #352

name: Docker Setup Integration Test
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: docker-setup-integration-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
integration:
name: Simulate new-user setup (setup.sh → docker compose up)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
# ── 1. Clone ─────────────────────────────────────────────────────────────
- name: Checkout
uses: actions/checkout@v4
# ── 2. setup.sh ──────────────────────────────────────────────────────────
- name: Run setup.sh (new-user step 1)
id: setup_sh
run: |
chmod +x ./setup.sh
./setup.sh
- name: Verify .env was generated with all required keys
id: env_keys
run: |
set -euo pipefail
required=(
BETTER_AUTH_SECRET
BETTER_AUTH_URL
DATABASE_URL
DB_USER
DB_PASSWORD
DB_NAME
S3_ENDPOINT
S3_ACCESS_KEY
S3_SECRET_KEY
S3_BUCKET
STORAGE_USER
STORAGE_PASSWORD
)
missing=()
for key in "${required[@]}"; do
if ! grep -q "^${key}=" .env; then
missing+=("$key")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
echo "❌ Missing keys in .env: ${missing[*]}"
exit 1
fi
echo "✅ All required keys present in .env"
- name: Verify STORAGE_PASSWORD and S3_SECRET_KEY match (prevents credential mismatch bug)
id: storage_creds
run: |
set -euo pipefail
storage_pass="$(grep '^STORAGE_PASSWORD=' .env | cut -d= -f2-)"
s3_key="$(grep '^S3_SECRET_KEY=' .env | cut -d= -f2-)"
if [ "$storage_pass" != "$s3_key" ]; then
echo "❌ STORAGE_PASSWORD and S3_SECRET_KEY do not match — MinIO uploads will fail"
exit 1
fi
echo "✅ MinIO credentials are consistent"
- name: Verify BETTER_AUTH_SECRET is at least 32 characters
id: auth_secret
run: |
set -euo pipefail
secret="$(grep '^BETTER_AUTH_SECRET=' .env | cut -d= -f2-)"
len="${#secret}"
if [ "$len" -lt 32 ]; then
echo "❌ BETTER_AUTH_SECRET is only ${len} chars (minimum 32)"
exit 1
fi
echo "✅ BETTER_AUTH_SECRET is ${len} chars"
- name: Verify setup.sh refuses to overwrite existing .env
id: env_overwrite
run: |
set -euo pipefail
if ./setup.sh 2>&1; then
echo "❌ setup.sh should have refused to overwrite .env but exited 0"
exit 1
fi
echo "✅ setup.sh correctly refused to overwrite existing .env"
# ── 3. docker compose up --build ─────────────────────────────────────────
- name: Build image and start all services (new-user step 2)
id: docker_build
run: docker compose up --build -d
- name: Wait for db to be healthy
id: db_health
run: |
set -euo pipefail
echo "Waiting for db..."
for i in $(seq 60); do
state="$(docker inspect --format='{{.State.Health.Status}}' reqcore_db 2>/dev/null || echo 'not-started')"
echo " db: $state"
[ "$state" = "healthy" ] && break
if [ "$i" -eq 60 ]; then
echo "❌ db did not become healthy in time"
docker compose logs db --tail=50
exit 1
fi
sleep 3
done
echo "✅ db is healthy"
- name: Wait for minio to be healthy
id: minio_health
run: |
set -euo pipefail
echo "Waiting for minio..."
for i in $(seq 60); do
state="$(docker inspect --format='{{.State.Health.Status}}' reqcore_minio 2>/dev/null || echo 'not-started')"
echo " minio: $state"
[ "$state" = "healthy" ] && break
if [ "$i" -eq 60 ]; then
echo "❌ minio did not become healthy in time"
docker compose logs minio --tail=50
exit 1
fi
sleep 3
done
echo "✅ minio is healthy"
- name: Wait for app to be reachable on :3000
id: app_health
run: |
set -euo pipefail
echo "Waiting for app..."
for i in $(seq 60); do
if curl -fs http://localhost:3000 > /dev/null 2>&1; then
echo "✅ App is reachable on http://localhost:3000"
exit 0
fi
state="$(docker inspect --format='{{.State.Status}}' reqcore_app 2>/dev/null || echo 'missing')"
if [ "$state" = "exited" ] || [ "$state" = "dead" ]; then
echo "❌ App container exited unexpectedly"
docker compose logs app --tail=100
exit 1
fi
echo " attempt $i/60 — waiting..."
sleep 3
done
echo "❌ App did not become reachable in time"
docker compose logs app --tail=100
exit 1
# ── 4. Startup log assertions ─────────────────────────────────────────────
- name: Assert DB migrations ran successfully
id: db_migrations
run: |
set -euo pipefail
if ! docker compose logs app | grep -q "Database migrations applied successfully"; then
echo "❌ Migration success message not found in app logs"
docker compose logs app
exit 1
fi
echo "✅ Migrations applied successfully"
- name: Assert S3 bucket is ready
id: s3_bucket
run: |
set -euo pipefail
if ! docker compose logs app | grep -q 'S3 bucket "reqcore" is ready'; then
echo "❌ S3 bucket ready message not found in app logs"
docker compose logs app
exit 1
fi
echo "✅ S3 bucket is ready"
# ── 5. HTTP smoke tests ───────────────────────────────────────────────────
- name: HTTP smoke tests
id: http_smoke
run: |
set -euo pipefail
fail=0
check() {
local label="$1" url="$2" expected="$3"
local actual
actual="$(curl -s -o /dev/null -w "%{http_code}" "$url")"
if [ "$actual" = "$expected" ]; then
echo "✅ $label → $actual"
else
echo "❌ $label → expected $expected, got $actual"
fail=1
fi
}
check "Home page" "http://localhost:3000" "200"
check "Sign-in page" "http://localhost:3000/auth/sign-in" "200"
check "Sign-up page" "http://localhost:3000/auth/sign-up" "200"
check "Public job board" "http://localhost:3000/jobs" "200"
check "API/jobs (no auth→401)" "http://localhost:3000/api/jobs" "401"
check "API/candidates (no auth)" "http://localhost:3000/api/candidates" "401"
exit $fail
# ── 6. Seed command (optional step from README) ───────────────────────────
- name: Seed demo data (docker compose exec app npm run db:seed)
id: seed_data
run: |
set -euo pipefail
output="$(docker compose exec app npm run db:seed 2>&1)"
echo "$output"
if ! echo "$output" | grep -q "Seed complete"; then
echo "❌ Seed did not complete successfully"
exit 1
fi
echo "✅ Seed completed"
- name: Sign in with seeded demo account (auth smoke test)
id: seed_signin
run: |
set -euo pipefail
response="$(curl -s -X POST http://localhost:3000/api/auth/sign-in/email \
-H "Content-Type: application/json" \
-d '{"email":"demo@reqcore.com","password":"demo1234"}' \
-w "\n%{http_code}")"
body="$(echo "$response" | head -n -1)"
status="$(echo "$response" | tail -n 1)"
echo "Status: $status"
echo "Body: $body"
if [ "$status" != "200" ]; then
echo "❌ Sign-in failed — expected 200, got $status"
exit 1
fi
if ! echo "$body" | grep -q "demo@reqcore.com"; then
echo "❌ Response body does not contain expected email"
exit 1
fi
echo "✅ Demo account sign-in succeeded"
- name: Seed idempotency — re-running seed must not crash
id: seed_idempotent
run: |
set -euo pipefail
output="$(docker compose exec app npm run db:seed 2>&1)"
echo "$output"
if echo "$output" | grep -qi "^npm error\|unhandledRejection\|UnhandledPromiseRejection"; then
echo "❌ Seed second run produced an error"
exit 1
fi
echo "✅ Seed is idempotent"
# ── 7. Adminer --profile tools ────────────────────────────────────────────
- name: Start Adminer via --profile tools
id: adminer_start
run: docker compose --profile tools up -d adminer
- name: Wait for Adminer to respond on :8080
id: adminer_health
run: |
set -euo pipefail
for i in $(seq 20); do
if curl -fs http://localhost:8080 > /dev/null 2>&1; then
echo "✅ Adminer is reachable on http://localhost:8080"
exit 0
fi
sleep 2
done
echo "❌ Adminer did not become reachable"
docker compose logs adminer --tail=30
exit 1
# ── 8. Restart resilience (migrations must be idempotent) ────────────────
- name: Restart app and verify it comes back clean
id: restart_resilience
run: |
set -euo pipefail
docker compose restart app
for i in $(seq 30); do
if curl -fs http://localhost:3000 > /dev/null 2>&1; then
echo "✅ App reachable again after restart"
break
fi
if [ "$i" -eq 30 ]; then
echo "❌ App did not come back after restart"
docker compose logs app --tail=50
exit 1
fi
sleep 3
done
if docker compose logs app | grep -q "Migration failed"; then
echo "❌ Migration error found in logs after restart"
docker compose logs app
exit 1
fi
echo "✅ Restart handled cleanly — no migration errors"
# ── Always: dump logs on failure ──────────────────────────────────────────
- name: Docker Integration Summary
if: ${{ !cancelled() }}
run: |
echo "## Docker Setup Integration Results" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Simulates the full new-user onboarding: \`setup.sh → docker compose up → seed → smoke tests\`" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| # | Check | Status |" >> "$GITHUB_STEP_SUMMARY"
echo "|---|-------|--------|" >> "$GITHUB_STEP_SUMMARY"
row() {
local n="$1" name="$2" outcome="$3"
local icon="❌"
[ "$outcome" = "success" ] && icon="✅"
[ "$outcome" = "skipped" ] && icon="⏭️"
echo "| $n | $name | $icon |" >> "$GITHUB_STEP_SUMMARY"
}
row 1 "Run setup.sh" "${{ steps.setup_sh.outcome }}"
row 2 ".env has all required keys" "${{ steps.env_keys.outcome }}"
row 3 "Storage credentials match" "${{ steps.storage_creds.outcome }}"
row 4 "Auth secret ≥ 32 chars" "${{ steps.auth_secret.outcome }}"
row 5 "setup.sh refuses overwrite" "${{ steps.env_overwrite.outcome }}"
row 6 "Docker build & start" "${{ steps.docker_build.outcome }}"
row 7 "Database healthy" "${{ steps.db_health.outcome }}"
row 8 "MinIO healthy" "${{ steps.minio_health.outcome }}"
row 9 "App reachable on :3000" "${{ steps.app_health.outcome }}"
row 10 "DB migrations applied" "${{ steps.db_migrations.outcome }}"
row 11 "S3 bucket ready" "${{ steps.s3_bucket.outcome }}"
row 12 "HTTP smoke tests" "${{ steps.http_smoke.outcome }}"
row 13 "Seed demo data" "${{ steps.seed_data.outcome }}"
row 14 "Demo account sign-in" "${{ steps.seed_signin.outcome }}"
row 15 "Seed idempotency" "${{ steps.seed_idempotent.outcome }}"
row 16 "Adminer starts" "${{ steps.adminer_start.outcome }}"
row 17 "Adminer reachable on :8080" "${{ steps.adminer_health.outcome }}"
row 18 "Restart resilience" "${{ steps.restart_resilience.outcome }}"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "_Run: \`${{ github.run_id }}\` · Commit: \`${{ github.sha }}\`_" >> "$GITHUB_STEP_SUMMARY"
- name: Generate JUnit XML report
if: ${{ !cancelled() }}
run: |
# Generate JUnit XML so test results can be consumed by dashboards/reporters
cat > docker-integration-results.xml << 'XMLEOF'
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="Docker Setup Integration" tests="18">
<testsuite name="Setup" tests="5">
XMLEOF
junit_case() {
local name="$1" outcome="$2"
if [ "$outcome" = "success" ]; then
echo " <testcase name=\"$name\" classname=\"docker-integration\" />" >> docker-integration-results.xml
elif [ "$outcome" = "skipped" ]; then
echo " <testcase name=\"$name\" classname=\"docker-integration\"><skipped /></testcase>" >> docker-integration-results.xml
else
echo " <testcase name=\"$name\" classname=\"docker-integration\"><failure message=\"Step failed\" /></testcase>" >> docker-integration-results.xml
fi
}
junit_case "Run setup.sh" "${{ steps.setup_sh.outcome }}"
junit_case ".env has all required keys" "${{ steps.env_keys.outcome }}"
junit_case "Storage credentials match" "${{ steps.storage_creds.outcome }}"
junit_case "Auth secret >= 32 chars" "${{ steps.auth_secret.outcome }}"
junit_case "setup.sh refuses overwrite" "${{ steps.env_overwrite.outcome }}"
echo " </testsuite>" >> docker-integration-results.xml
echo ' <testsuite name="Infrastructure" tests="4">' >> docker-integration-results.xml
junit_case "Docker build and start" "${{ steps.docker_build.outcome }}"
junit_case "Database healthy" "${{ steps.db_health.outcome }}"
junit_case "MinIO healthy" "${{ steps.minio_health.outcome }}"
junit_case "App reachable on :3000" "${{ steps.app_health.outcome }}"
echo " </testsuite>" >> docker-integration-results.xml
echo ' <testsuite name="Startup Assertions" tests="2">' >> docker-integration-results.xml
junit_case "DB migrations applied" "${{ steps.db_migrations.outcome }}"
junit_case "S3 bucket ready" "${{ steps.s3_bucket.outcome }}"
echo " </testsuite>" >> docker-integration-results.xml
echo ' <testsuite name="Smoke Tests" tests="4">' >> docker-integration-results.xml
junit_case "HTTP smoke tests" "${{ steps.http_smoke.outcome }}"
junit_case "Seed demo data" "${{ steps.seed_data.outcome }}"
junit_case "Demo account sign-in" "${{ steps.seed_signin.outcome }}"
junit_case "Seed idempotency" "${{ steps.seed_idempotent.outcome }}"
echo " </testsuite>" >> docker-integration-results.xml
echo ' <testsuite name="Optional Services" tests="3">' >> docker-integration-results.xml
junit_case "Adminer starts" "${{ steps.adminer_start.outcome }}"
junit_case "Adminer reachable on :8080" "${{ steps.adminer_health.outcome }}"
junit_case "Restart resilience" "${{ steps.restart_resilience.outcome }}"
echo " </testsuite>" >> docker-integration-results.xml
echo " </testsuites>" >> docker-integration-results.xml
- name: Upload JUnit XML results
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: docker-integration-junit
path: docker-integration-results.xml
retention-days: 30
- name: Dump all service logs
if: always()
run: |
echo "=== docker compose ps ==="
docker compose --profile tools ps || true
echo ""
echo "=== app ==="
docker compose logs app --no-color --tail=200 || true
echo ""
echo "=== db ==="
docker compose logs db --no-color --tail=50 || true
echo ""
echo "=== minio ==="
docker compose logs minio --no-color --tail=50 || true
- name: Tear down all services and volumes
if: always()
run: docker compose --profile tools down -v || true