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();
await qr.connect();
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(
`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)`,
[userId, slug, email ?? null, phone ?? null, passwordHash, name, [RoleType.ADMIN], true, now, now],
);
await qr.commitTransaction();
} catch (err) {
await qr.rollbackTransaction();
throw err;
} finally {
await qr.release();
}
@ -369,7 +374,8 @@ export class AuthService {
const qr = this.dataSource.createQueryRunner();
await qr.connect();
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
const existingRows = await qr.query(
@ -385,6 +391,10 @@ export class AuthService {
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[userId, tenant.slug, invite.email, passwordHash, name, [invite.role], true, now, now],
);
await qr.commitTransaction();
} catch (err) {
await qr.rollbackTransaction();
throw err;
} finally {
await qr.release();
}

View File

@ -46,8 +46,10 @@ TEST_PASS="Test@12345"
# ══════════════════════════════════════════════════════════════════════════════
section "1. Health Checks"
STATUS=$(get_status "${BASE}/health" 2>/dev/null || echo "000")
[[ "$STATUS" == "200" ]] && ok "GET /health → 200" || fail "GET /health → ${STATUS}"
# Kong gateway is healthy if auth routes respond (no /health route on Kong)
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)"
@ -85,10 +87,11 @@ fi
# ══════════════════════════════════════════════════════════════════════════════
section "4. Auth — Duplicate registration should fail"
# Same company name → 409 on slug conflict
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" == "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)"
@ -181,35 +184,39 @@ else
fi
# ══════════════════════════════════════════════════════════════════════════════
section "13. Tenants — List (platform admin)"
section "13. Tenants — List (platform admin via /api/v1/admin/tenants)"
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}")
[[ "$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
info "Skipped — no admin token"
fi
# ══════════════════════════════════════════════════════════════════════════════
section "14. Users — List (tenant admin)"
section "14. Users — List (tenant admin via /api/v1/auth/users)"
if [[ -n "$TOKEN" ]]; then
STATUS=$(get_status "${BASE}/api/v1/users" \
STATUS=$(get_status "${BASE}/api/v1/auth/users" \
-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
info "Skipped — no token"
fi
# ══════════════════════════════════════════════════════════════════════════════
section "15. Billing — Plans (public)"
section "15. Billing — Plans (JWT required per Kong config)"
STATUS=$(get_status "${BASE}/api/v1/billing/plans")
[[ "$STATUS" == "200" ]] && ok "GET /api/v1/billing/plans → 200" || fail "GET /api/v1/billing/plans → ${STATUS}"
if [[ -n "$TOKEN" ]]; then
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
STATUS=$(get_status "${BASE}/api/v1/billing/subscription" \
@ -243,23 +250,27 @@ else
fi
# ══════════════════════════════════════════════════════════════════════════════
section "19. App Versions — Admin CRUD (JWT required)"
section "19. App Versions — Admin list (JWT required)"
if [[ -n "${ADMIN_TOKEN:-}" ]]; then
STATUS=$(get_status "${BASE}/api/v1/versions" \
-H "Authorization: Bearer ${ADMIN_TOKEN}")
[[ "$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
info "Skipped — no admin token"
info "Skipped — no token"
fi
# ══════════════════════════════════════════════════════════════════════════════
section "20. Servers — List (JWT required)"
section "20. Inventory — Servers (JWT required)"
if [[ -n "$TOKEN" ]]; then
STATUS=$(get_status "${BASE}/api/v1/servers" \
STATUS=$(get_status "${BASE}/api/v1/inventory/servers" \
-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
# ══════════════════════════════════════════════════════════════════════════════
@ -273,12 +284,12 @@ if [[ -n "$TOKEN" ]]; then
fi
# ══════════════════════════════════════════════════════════════════════════════
section "22. Roles — List (JWT required)"
section "22. Roles — List (JWT required via /api/v1/auth/roles)"
if [[ -n "$TOKEN" ]]; then
STATUS=$(get_status "${BASE}/api/v1/roles" \
STATUS=$(get_status "${BASE}/api/v1/auth/roles" \
-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
# ══════════════════════════════════════════════════════════════════════════════
@ -294,17 +305,17 @@ if [[ -n "$TOKEN" ]]; then
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"
RESP=$(post_json "${BASE}/api/v1/tenants/invites" \
-H "Authorization: Bearer ${TOKEN}" \
RESP=$(post_json "${BASE}/api/v1/admin/tenants/${TENANT_ID}/invites" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d "{\"email\":\"${INVITE_EMAIL}\",\"role\":\"viewer\"}")
STATUS=$(echo "$RESP" | grep -o '__STATUS__[0-9]*' | sed 's/__STATUS__//')
BODY=$(echo "$RESP" | sed 's/__STATUS__[0-9]*//')
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)
if [[ -n "$INVITE_TOKEN" ]]; then
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}"
fi
else
fail "POST /api/v1/tenants/invites → ${STATUS}: ${BODY}"
fail "POST /api/v1/admin/tenants/:id/invites → ${STATUS}: ${BODY}"
fi
else
info "Skipped invite flow — need admin token + tenantId"
fi
# ══════════════════════════════════════════════════════════════════════════════