fix(auth): use SET LOCAL search_path to prevent connection pool contamination; fix api-test routing

This commit is contained in:
hailin 2026-03-07 03:35:49 -08:00
parent 09d9200235
commit 7a6752bf74
2 changed files with 53 additions and 30 deletions

View File

@ -203,12 +203,17 @@ export class AuthService {
const qr = this.dataSource.createQueryRunner(); const qr = this.dataSource.createQueryRunner();
await qr.connect(); await qr.connect();
try { try {
await qr.query(`SET search_path TO "${schemaName}", public`); await qr.startTransaction();
await qr.query(`SET LOCAL search_path TO "${schemaName}", public`);
await qr.query( await qr.query(
`INSERT INTO users (id, tenant_id, email, phone, password_hash, name, roles, is_active, created_at, updated_at) `INSERT INTO users (id, tenant_id, email, phone, password_hash, name, roles, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
[userId, slug, email ?? null, phone ?? null, passwordHash, name, [RoleType.ADMIN], true, now, now], [userId, slug, email ?? null, phone ?? null, passwordHash, name, [RoleType.ADMIN], true, now, now],
); );
await qr.commitTransaction();
} catch (err) {
await qr.rollbackTransaction();
throw err;
} finally { } finally {
await qr.release(); await qr.release();
} }
@ -369,7 +374,8 @@ export class AuthService {
const qr = this.dataSource.createQueryRunner(); const qr = this.dataSource.createQueryRunner();
await qr.connect(); await qr.connect();
try { try {
await qr.query(`SET search_path TO "${schemaName}", public`); await qr.startTransaction();
await qr.query(`SET LOCAL search_path TO "${schemaName}", public`);
// Check if email already exists in this tenant // Check if email already exists in this tenant
const existingRows = await qr.query( const existingRows = await qr.query(
@ -385,6 +391,10 @@ export class AuthService {
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[userId, tenant.slug, invite.email, passwordHash, name, [invite.role], true, now, now], [userId, tenant.slug, invite.email, passwordHash, name, [invite.role], true, now, now],
); );
await qr.commitTransaction();
} catch (err) {
await qr.rollbackTransaction();
throw err;
} finally { } finally {
await qr.release(); await qr.release();
} }

View File

@ -46,8 +46,10 @@ TEST_PASS="Test@12345"
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "1. Health Checks" section "1. Health Checks"
STATUS=$(get_status "${BASE}/health" 2>/dev/null || echo "000") # Kong gateway is healthy if auth routes respond (no /health route on Kong)
[[ "$STATUS" == "200" ]] && ok "GET /health → 200" || fail "GET /health → ${STATUS}" STATUS=$(get_status "${BASE}/api/v1/auth/profile")
[[ "$STATUS" == "401" ]] && ok "Kong gateway reachable (auth/profile returns 401 without token)" \
|| fail "Kong gateway unreachable → ${STATUS}"
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "2. Auth — Register (Email)" section "2. Auth — Register (Email)"
@ -85,10 +87,11 @@ fi
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "4. Auth — Duplicate registration should fail" section "4. Auth — Duplicate registration should fail"
# Same company name → 409 on slug conflict
RESP=$(post_json "${BASE}/api/v1/auth/register" \ RESP=$(post_json "${BASE}/api/v1/auth/register" \
-d "{\"email\":\"${TEST_EMAIL}\",\"password\":\"${TEST_PASS}\",\"name\":\"Dup\",\"companyName\":\"Dup${TS}\"}") -d "{\"email\":\"dup_${TS}@example.com\",\"password\":\"${TEST_PASS}\",\"name\":\"Dup\",\"companyName\":\"${TEST_COMPANY}\"}")
STATUS=$(echo "$RESP" | grep -o '__STATUS__[0-9]*' | sed 's/__STATUS__//') STATUS=$(echo "$RESP" | grep -o '__STATUS__[0-9]*' | sed 's/__STATUS__//')
[[ "$STATUS" == "409" ]] && ok "Duplicate email → 409 Conflict" || fail "Duplicate email → expected 409, got ${STATUS}" [[ "$STATUS" == "409" ]] && ok "Duplicate company name → 409 Conflict" || fail "Duplicate company → expected 409, got ${STATUS}"
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "5. Auth — Login (email)" section "5. Auth — Login (email)"
@ -181,35 +184,39 @@ else
fi fi
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "13. Tenants — List (platform admin)" section "13. Tenants — List (platform admin via /api/v1/admin/tenants)"
if [[ -n "${ADMIN_TOKEN:-}" ]]; then if [[ -n "${ADMIN_TOKEN:-}" ]]; then
STATUS=$(get_status "${BASE}/api/v1/tenants" \ STATUS=$(get_status "${BASE}/api/v1/admin/tenants" \
-H "Authorization: Bearer ${ADMIN_TOKEN}") -H "Authorization: Bearer ${ADMIN_TOKEN}")
[[ "$STATUS" == "200" ]] && ok "GET /api/v1/tenants → 200" || fail "GET /api/v1/tenants → ${STATUS}" [[ "$STATUS" == "200" ]] && ok "GET /api/v1/admin/tenants → 200" || fail "GET /api/v1/admin/tenants → ${STATUS}"
else else
info "Skipped — no admin token" info "Skipped — no admin token"
fi fi
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "14. Users — List (tenant admin)" section "14. Users — List (tenant admin via /api/v1/auth/users)"
if [[ -n "$TOKEN" ]]; then if [[ -n "$TOKEN" ]]; then
STATUS=$(get_status "${BASE}/api/v1/users" \ STATUS=$(get_status "${BASE}/api/v1/auth/users" \
-H "Authorization: Bearer ${TOKEN}") -H "Authorization: Bearer ${TOKEN}")
[[ "$STATUS" == "200" ]] && ok "GET /api/v1/users → 200" || fail "GET /api/v1/users → ${STATUS}" [[ "$STATUS" == "200" ]] && ok "GET /api/v1/auth/users → 200" || fail "GET /api/v1/auth/users → ${STATUS}"
else else
info "Skipped — no token" info "Skipped — no token"
fi fi
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "15. Billing — Plans (public)" section "15. Billing — Plans (JWT required per Kong config)"
STATUS=$(get_status "${BASE}/api/v1/billing/plans") if [[ -n "$TOKEN" ]]; then
[[ "$STATUS" == "200" ]] && ok "GET /api/v1/billing/plans → 200" || fail "GET /api/v1/billing/plans → ${STATUS}" STATUS=$(get_status "${BASE}/api/v1/billing/plans" \
-H "Authorization: Bearer ${TOKEN}")
[[ "$STATUS" == "200" ]] && ok "GET /api/v1/billing/plans (JWT) → 200" \
|| fail "GET /api/v1/billing/plans (JWT) → ${STATUS}"
fi
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "16. Billing — Current subscription (JWT required)" section "16. Billing — Subscription (JWT required)"
if [[ -n "$TOKEN" ]]; then if [[ -n "$TOKEN" ]]; then
STATUS=$(get_status "${BASE}/api/v1/billing/subscription" \ STATUS=$(get_status "${BASE}/api/v1/billing/subscription" \
@ -243,23 +250,27 @@ else
fi fi
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "19. App Versions — Admin CRUD (JWT required)" section "19. App Versions — Admin list (JWT required)"
if [[ -n "${ADMIN_TOKEN:-}" ]]; then if [[ -n "${ADMIN_TOKEN:-}" ]]; then
STATUS=$(get_status "${BASE}/api/v1/versions" \ STATUS=$(get_status "${BASE}/api/v1/versions" \
-H "Authorization: Bearer ${ADMIN_TOKEN}") -H "Authorization: Bearer ${ADMIN_TOKEN}")
[[ "$STATUS" == "200" ]] && ok "GET /api/v1/versions → 200" || fail "GET /api/v1/versions → ${STATUS}" [[ "$STATUS" == "200" ]] && ok "GET /api/v1/versions → 200" || fail "GET /api/v1/versions → ${STATUS}"
elif [[ -n "$TOKEN" ]]; then
STATUS=$(get_status "${BASE}/api/v1/versions" \
-H "Authorization: Bearer ${TOKEN}")
[[ "$STATUS" == "200" ]] && ok "GET /api/v1/versions → 200" || fail "GET /api/v1/versions → ${STATUS}"
else else
info "Skipped — no admin token" info "Skipped — no token"
fi fi
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "20. Servers — List (JWT required)" section "20. Inventory — Servers (JWT required)"
if [[ -n "$TOKEN" ]]; then if [[ -n "$TOKEN" ]]; then
STATUS=$(get_status "${BASE}/api/v1/servers" \ STATUS=$(get_status "${BASE}/api/v1/inventory/servers" \
-H "Authorization: Bearer ${TOKEN}") -H "Authorization: Bearer ${TOKEN}")
[[ "$STATUS" == "200" ]] && ok "GET /api/v1/servers → 200" || fail "GET /api/v1/servers → ${STATUS}" [[ "$STATUS" == "200" ]] && ok "GET /api/v1/inventory/servers → 200" || fail "GET /api/v1/inventory/servers → ${STATUS}"
fi fi
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
@ -273,12 +284,12 @@ if [[ -n "$TOKEN" ]]; then
fi fi
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "22. Roles — List (JWT required)" section "22. Roles — List (JWT required via /api/v1/auth/roles)"
if [[ -n "$TOKEN" ]]; then if [[ -n "$TOKEN" ]]; then
STATUS=$(get_status "${BASE}/api/v1/roles" \ STATUS=$(get_status "${BASE}/api/v1/auth/roles" \
-H "Authorization: Bearer ${TOKEN}") -H "Authorization: Bearer ${TOKEN}")
[[ "$STATUS" == "200" ]] && ok "GET /api/v1/roles → 200" || fail "GET /api/v1/roles → ${STATUS}" [[ "$STATUS" == "200" ]] && ok "GET /api/v1/auth/roles → 200" || fail "GET /api/v1/auth/roles → ${STATUS}"
fi fi
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
@ -294,17 +305,17 @@ if [[ -n "$TOKEN" ]]; then
fi fi
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "24. Invite flow — Create invite" section "24. Invite flow — Create + validate invite"
if [[ -n "$TOKEN" ]]; then if [[ -n "${ADMIN_TOKEN:-}" && -n "${TENANT_ID:-}" ]]; then
INVITE_EMAIL="invite_${TS}@example.com" INVITE_EMAIL="invite_${TS}@example.com"
RESP=$(post_json "${BASE}/api/v1/tenants/invites" \ RESP=$(post_json "${BASE}/api/v1/admin/tenants/${TENANT_ID}/invites" \
-H "Authorization: Bearer ${TOKEN}" \ -H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d "{\"email\":\"${INVITE_EMAIL}\",\"role\":\"viewer\"}") -d "{\"email\":\"${INVITE_EMAIL}\",\"role\":\"viewer\"}")
STATUS=$(echo "$RESP" | grep -o '__STATUS__[0-9]*' | sed 's/__STATUS__//') STATUS=$(echo "$RESP" | grep -o '__STATUS__[0-9]*' | sed 's/__STATUS__//')
BODY=$(echo "$RESP" | sed 's/__STATUS__[0-9]*//') BODY=$(echo "$RESP" | sed 's/__STATUS__[0-9]*//')
if [[ "$STATUS" == "200" || "$STATUS" == "201" ]]; then if [[ "$STATUS" == "200" || "$STATUS" == "201" ]]; then
ok "POST /api/v1/tenants/invites → ${STATUS}" ok "POST /api/v1/admin/tenants/:id/invites → ${STATUS}"
INVITE_TOKEN=$(echo "$BODY" | grep -o '"token":"[^"]*"' | cut -d'"' -f4) INVITE_TOKEN=$(echo "$BODY" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
if [[ -n "$INVITE_TOKEN" ]]; then if [[ -n "$INVITE_TOKEN" ]]; then
STATUS2=$(get_status "${BASE}/api/v1/auth/invite/${INVITE_TOKEN}") STATUS2=$(get_status "${BASE}/api/v1/auth/invite/${INVITE_TOKEN}")
@ -312,8 +323,10 @@ if [[ -n "$TOKEN" ]]; then
|| fail "GET /api/v1/auth/invite/:token → ${STATUS2}" || fail "GET /api/v1/auth/invite/:token → ${STATUS2}"
fi fi
else else
fail "POST /api/v1/tenants/invites → ${STATUS}: ${BODY}" fail "POST /api/v1/admin/tenants/:id/invites → ${STATUS}: ${BODY}"
fi fi
else
info "Skipped invite flow — need admin token + tenantId"
fi fi
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════