chore: initial import of tests for self-hosted

This commit is contained in:
Andrey A. committed 2026-03-09 18:19:50 +01:00
1 parent 517171b246
commit cdbbde9ef8
3 files changed
+789

No files matched your search

+382
View File
@@ -0,0 +1,382 @@
#!/bin/sh
#
# Test API key types and asymmetric auth against a running self-hosted instance.
#
# Usage:
# sh test-auth-keys.sh # Uses http://localhost:8000
# sh test-auth-keys.sh <base_url> # Custom URL
#
# Prerequisites:
# - Running self-hosted Supabase instance
# - .env file with all keys configured
# - node >= 16 (for HS256 token minting test)
#
set -e
BASE_URL="${1:-http://localhost:8000}"
if [ ! -f .env ]; then
echo "Error: .env file not found. Run from the project directory."
exit 1
fi
# Read keys from .env
JWT_SECRET=$(grep '^JWT_SECRET=' .env | cut -d= -f2-)
ANON_KEY=$(grep '^ANON_KEY=' .env | cut -d= -f2-)
SERVICE_ROLE_KEY=$(grep '^SERVICE_ROLE_KEY=' .env | cut -d= -f2-)
SUPABASE_PUBLISHABLE_KEY=$(grep '^SUPABASE_PUBLISHABLE_KEY=' .env | cut -d= -f2-)
SUPABASE_SECRET_KEY=$(grep '^SUPABASE_SECRET_KEY=' .env | cut -d= -f2-)
pass=0
fail=0
check() {
test_name="$1"
expected="$2"
actual="$3"
if [ "$actual" = "$expected" ]; then
echo " PASS: $test_name (HTTP $actual)"
pass=$((pass + 1))
else
echo " FAIL: $test_name (expected $expected, got $actual)"
fail=$((fail + 1))
fi
}
http_status() {
url="$1"
shift
curl -s -o /dev/null -w "%{http_code}" "$@" "$url"
}
echo ""
echo "=== Testing against $BASE_URL ==="
echo ""
# ---------------------------------------------
# 1. Route tests with API key types
# ---------------------------------------------
echo "--- REST API (/rest/v1/) ---"
check "Legacy ANON_KEY" "200" \
"$(http_status "$BASE_URL/rest/v1/" -H "apikey: $ANON_KEY")"
check "Legacy SERVICE_ROLE_KEY" "200" \
"$(http_status "$BASE_URL/rest/v1/" -H "apikey: $SERVICE_ROLE_KEY")"
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
check "New PUBLISHABLE_KEY" "200" \
"$(http_status "$BASE_URL/rest/v1/" -H "apikey: $SUPABASE_PUBLISHABLE_KEY")"
check "New SECRET_KEY" "200" \
"$(http_status "$BASE_URL/rest/v1/" -H "apikey: $SUPABASE_SECRET_KEY")"
else
echo " SKIP: Opaque keys not configured"
fi
check "No key -> 401" "401" \
"$(http_status "$BASE_URL/rest/v1/")"
check "Invalid key -> 401" "401" \
"$(http_status "$BASE_URL/rest/v1/" -H "apikey: invalid-key")"
echo ""
echo "--- Auth (/auth/v1/settings) ---"
check "Legacy ANON_KEY" "200" \
"$(http_status "$BASE_URL/auth/v1/settings" -H "apikey: $ANON_KEY")"
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
check "New PUBLISHABLE_KEY" "200" \
"$(http_status "$BASE_URL/auth/v1/settings" -H "apikey: $SUPABASE_PUBLISHABLE_KEY")"
fi
check "No key -> 401" "401" \
"$(http_status "$BASE_URL/auth/v1/settings")"
echo ""
echo "--- Storage (/storage/v1/bucket) ---"
# Storage has no key-auth - passes through, Storage returns its own errors
check "No key -> not 401 (Storage handles auth)" "true" \
"$([ "$(http_status "$BASE_URL/storage/v1/bucket")" != "401" ] && echo true || echo false)"
check "Legacy ANON_KEY" "200" \
"$(http_status "$BASE_URL/storage/v1/bucket" -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY")"
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
# With opaque key, Kong translates to asymmetric JWT in Authorization
check "New PUBLISHABLE_KEY" "200" \
"$(http_status "$BASE_URL/storage/v1/bucket" -H "apikey: $SUPABASE_PUBLISHABLE_KEY")"
fi
echo ""
echo "--- Storage S3 (/storage/v1/s3/) ---"
# S3 route has no request-transformer - Authorization passes through unchanged
check "S3 route accessible (no transform)" "true" \
"$([ "$(http_status "$BASE_URL/storage/v1/s3/")" != "502" ] && echo true || echo false)"
echo ""
echo "--- GraphQL (/graphql/v1) ---"
check "Legacy ANON_KEY" "200" \
"$(http_status "$BASE_URL/graphql/v1" \
-H "apikey: $ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"{ __typename }"}')"
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
check "New PUBLISHABLE_KEY" "200" \
"$(http_status "$BASE_URL/graphql/v1" \
-H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"{ __typename }"}')"
fi
check "No key -> 401" "401" \
"$(http_status "$BASE_URL/graphql/v1" \
-H "Content-Type: application/json" \
-d '{"query":"{ __typename }"}')"
echo ""
echo "--- Realtime REST (/realtime/v1/api/) ---"
# Realtime REST API - expect 200 or other non-401 response with valid key
check "Legacy ANON_KEY -> not 401" "true" \
"$([ "$(http_status "$BASE_URL/realtime/v1/api/tenants" -H "apikey: $ANON_KEY")" != "401" ] && echo true || echo false)"
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
check "New PUBLISHABLE_KEY -> not 401" "true" \
"$([ "$(http_status "$BASE_URL/realtime/v1/api/tenants" -H "apikey: $SUPABASE_PUBLISHABLE_KEY")" != "401" ] && echo true || echo false)"
fi
check "No key -> 401" "401" \
"$(http_status "$BASE_URL/realtime/v1/api/tenants")"
echo ""
echo "--- supabase-js style requests (apikey + Authorization) ---"
# supabase-js sends both apikey header AND Authorization: Bearer <apikey>
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
check "apikey + Authorization: Bearer sb_ (replace path)" "200" \
"$(http_status "$BASE_URL/rest/v1/" \
-H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
-H "Authorization: Bearer $SUPABASE_PUBLISHABLE_KEY")"
fi
check "Legacy apikey + Authorization: Bearer <legacy jwt>" "200" \
"$(http_status "$BASE_URL/rest/v1/" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ANON_KEY")"
echo ""
echo "--- Edge cases ---"
# Opaque key in Authorization only (no apikey header) - should be rejected by key-auth
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
check "sb_ in Authorization only (no apikey) -> 401" "401" \
"$(http_status "$BASE_URL/rest/v1/" \
-H "Authorization: Bearer $SUPABASE_PUBLISHABLE_KEY")"
fi
echo ""
echo "--- JWKS endpoint ---"
check "JWKS public endpoint (no auth)" "200" \
"$(http_status "$BASE_URL/auth/v1/.well-known/jwks.json")"
# Verify JWKS content: should have EC key, should NOT have symmetric key
jwks_content=$(curl -s "$BASE_URL/auth/v1/.well-known/jwks.json")
jwks_has_ec=$(echo "$jwks_content" | node -e "
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
try{const j=JSON.parse(d);console.log(j.keys&&j.keys.some(k=>k.kty==='EC')?'true':'false')}
catch{console.log('false')}
})" 2>/dev/null)
jwks_has_oct=$(echo "$jwks_content" | node -e "
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
try{const j=JSON.parse(d);console.log(j.keys&&j.keys.some(k=>k.kty==='oct')?'true':'false')}
catch{console.log('false')}
})" 2>/dev/null)
check "JWKS contains EC public key" "true" "$jwks_has_ec"
check "JWKS does NOT contain symmetric key" "false" "$jwks_has_oct"
echo ""
echo "--- OAuth metadata endpoint ---"
check "well-known oauth (no auth)" "200" \
"$(http_status "$BASE_URL/.well-known/oauth-authorization-server")"
echo ""
echo "--- Realtime WebSocket upgrade ---"
# Test that WebSocket upgrade request gets through (expect 101 or non-401)
# curl --max-time to prevent hanging on successful upgrade (101 keeps connection open)
ws_status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 \
"$BASE_URL/realtime/v1/websocket?apikey=$ANON_KEY&vsn=1.0.0" \
-H "Upgrade: websocket" \
-H "Connection: Upgrade" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Sec-WebSocket-Version: 13" 2>/dev/null || echo "000")
# 101 = upgrade success, 000 = timeout (connection stayed open = success)
check "WebSocket upgrade with legacy key -> not 401" "true" \
"$([ "$ws_status" != "401" ] && echo true || echo false)"
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
ws_status_new=$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 \
"$BASE_URL/realtime/v1/websocket?apikey=$SUPABASE_PUBLISHABLE_KEY&vsn=1.0.0" \
-H "Upgrade: websocket" \
-H "Connection: Upgrade" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Sec-WebSocket-Version: 13" 2>/dev/null || echo "000")
check "WebSocket upgrade with opaque key -> not 401" "true" \
"$([ "$ws_status_new" != "401" ] && echo true || echo false)"
fi
# ---------------------------------------------
# 2. User session JWT tests
# ---------------------------------------------
echo ""
echo "--- User session JWT ---"
# Try signin first (user may exist from previous test run), fall back to signup
test_email="test-keys@example.com"
test_password="test-password-123456"
auth_response=$(curl -s "$BASE_URL/auth/v1/token?grant_type=password" \
-H "apikey: $ANON_KEY" \
-H "Content-Type: application/json" \
-d "{\"email\":\"$test_email\",\"password\":\"$test_password\"}")
access_token=$(echo "$auth_response" | node -e "
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
try{console.log(JSON.parse(d).access_token||'')}catch{console.log('')}
})" 2>/dev/null)
if [ -z "$access_token" ]; then
# User doesn't exist - sign up
auth_response=$(curl -s "$BASE_URL/auth/v1/signup" \
-H "apikey: $ANON_KEY" \
-H "Content-Type: application/json" \
-d "{\"email\":\"$test_email\",\"password\":\"$test_password\"}")
access_token=$(echo "$auth_response" | node -e "
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
try{console.log(JSON.parse(d).access_token||'')}catch{console.log('')}
})" 2>/dev/null)
fi
if [ -n "$access_token" ]; then
# Check the algorithm in the JWT header
jwt_header=$(echo "$access_token" | cut -d. -f1 | node -e "
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
console.log(Buffer.from(d.trim(),'base64url').toString())
})" 2>/dev/null)
jwt_alg=$(echo "$jwt_header" | node -e "
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
try{console.log(JSON.parse(d.trim()).alg||'')}catch{console.log('')}
})" 2>/dev/null)
if [ -n "$jwt_alg" ]; then
echo " INFO: User session JWT signed with: $jwt_alg"
if [ "$jwt_alg" = "ES256" ]; then
check "JWT uses ES256 (asymmetric)" "ES256" "$jwt_alg"
else
check "JWT uses HS256 (legacy)" "HS256" "$jwt_alg"
fi
fi
# Use the session JWT with PostgREST
check "Session JWT with PostgREST" "200" \
"$(http_status "$BASE_URL/rest/v1/" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $access_token")"
# Use the session JWT with Storage
check "Session JWT with Storage" "200" \
"$(http_status "$BASE_URL/storage/v1/bucket" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $access_token")"
# CRITICAL: Authenticated user + opaque key (most common supabase-js flow)
# supabase-js sends apikey: sb_publishable_xxx AND Authorization: Bearer <user_session_jwt>
# The expression MUST keep the user JWT and NOT replace it with the anon asymmetric JWT
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
echo ""
echo "--- Authenticated user + opaque key (critical path) ---"
check "Opaque apikey + user JWT -> PostgREST uses user JWT" "200" \
"$(http_status "$BASE_URL/rest/v1/" \
-H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
-H "Authorization: Bearer $access_token")"
check "Opaque apikey + user JWT -> Storage uses user JWT" "200" \
"$(http_status "$BASE_URL/storage/v1/bucket" \
-H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
-H "Authorization: Bearer $access_token")"
check "Opaque apikey + user JWT -> Auth uses user JWT" "200" \
"$(http_status "$BASE_URL/auth/v1/user" \
-H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
-H "Authorization: Bearer $access_token")"
fi
else
echo " SKIP: Could not sign in or sign up test user (Auth may require email confirmation)"
echo " Response: $auth_response"
fi
# ---------------------------------------------
# 3. HS256 backward compatibility
# ---------------------------------------------
echo ""
echo "--- HS256 backward compatibility ---"
# Mint a legacy HS256 JWT with role=anon (simulating a pre-migration token)
hs256_token=$(node -e "
const crypto = require('crypto');
const header = Buffer.from(JSON.stringify({alg:'HS256',typ:'JWT'})).toString('base64url');
const payload = Buffer.from(JSON.stringify({
role:'anon',iss:'supabase',
iat:Math.floor(Date.now()/1000),
exp:Math.floor(Date.now()/1000)+3600
})).toString('base64url');
const sig = crypto.createHmac('sha256','$JWT_SECRET')
.update(header+'.'+payload).digest('base64url');
console.log(header+'.'+payload+'.'+sig);
" 2>/dev/null)
if [ -n "$hs256_token" ]; then
check "HS256 token with PostgREST (backward compat)" "200" \
"$(http_status "$BASE_URL/rest/v1/" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $hs256_token")"
else
echo " SKIP: Could not mint HS256 token (node required)"
fi
# ---------------------------------------------
# 4. JWT_KEYS format validation
# ---------------------------------------------
echo ""
echo "--- JWT_KEYS format ---"
JWT_KEYS_VAL=$(grep '^JWT_KEYS=' .env | cut -d= -f2-)
if [ -n "$JWT_KEYS_VAL" ]; then
# Auth expects a JSON array, not a JWKS object
jwt_keys_is_array=$(echo "$JWT_KEYS_VAL" | node -e "
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
try{console.log(Array.isArray(JSON.parse(d.trim()))?'true':'false')}
catch{console.log('false')}
})" 2>/dev/null)
check "JWT_KEYS is JSON array (not JWKS object)" "true" "$jwt_keys_is_array"
jwt_keys_has_sign=$(echo "$JWT_KEYS_VAL" | node -e "
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
try{const a=JSON.parse(d.trim());
console.log(a.some(k=>k.key_ops&&k.key_ops.includes('sign'))?'true':'false')}
catch{console.log('false')}
})" 2>/dev/null)
check "JWT_KEYS has a signing key (key_ops: sign)" "true" "$jwt_keys_has_sign"
else
echo " SKIP: JWT_KEYS not configured"
fi
# ---------------------------------------------
# Summary
# ---------------------------------------------
echo ""
echo "=== Results: $pass passed, $fail failed ==="
echo ""
if [ "$fail" -gt 0 ]; then
exit 1
fi
+112
View File
@@ -0,0 +1,112 @@
#!/bin/sh
#
# Verify all self-hosted Supabase services started correctly by checking log output.
#
# Usage:
# sh test-container-logs.sh
#
# Prerequisites:
# - Running self-hosted Supabase instance (docker compose up)
#
set -e
checks_failed=0
logs_failed_for=""
fail_msg() {
checks_failed=$((checks_failed + 1))
if [ -z "$logs_failed_for" ]; then
logs_failed_for="$1"
else
logs_failed_for="${logs_failed_for}, $1"
fi
echo " FAIL: $1"
}
pass_msg() {
echo " PASS: $1"
}
# Check that a service's logs contain all expected patterns
check_logs() {
service="$1"
shift
logs=$(docker compose logs "$service" 2>/dev/null)
if [ -z "$logs" ]; then
fail_msg "$service (no logs found)"
return
fi
for pattern in "$@"; do
if ! echo "$logs" | grep -q -i -E "$pattern"; then
fail_msg "$service (missing: $pattern)"
return
fi
done
pass_msg "$service"
}
echo ""
echo "=== Checking service startup logs ==="
echo ""
check_logs db \
'PostgreSQL init process complete; ready for start up.|Skipping initialization'
check_logs auth \
'db worker started'
check_logs kong \
'init.lua.*declarative config loaded'
check_logs rest \
'Schema cache loaded in.*milliseconds'
check_logs realtime \
'Starting Realtime' \
'Connected to Postgres database' \
'Janitor started' \
'Starting MetricsCleaner'
check_logs storage \
'Started Successfully'
check_logs studio \
'ready in.*s$'
check_logs meta \
'Server listening at http'
check_logs functions \
'main function started'
check_logs analytics \
'Access LogflareWeb.Endpoint at http://localhost:4000' \
'Initializing alerts scheduler'
check_logs supavisor \
'Connected to Postgres database' \
'HEAD /api/health$'
check_logs vector \
'Vector has started'
check_logs imgproxy \
'Starting server at :5001'
echo ""
if [ $checks_failed -gt 0 ]; then
echo "=== $checks_failed failed: $logs_failed_for ==="
echo ""
echo "Inspect logs: docker compose logs <service>"
echo ""
exit 1
else
echo "=== All checks passed ==="
fi
echo ""
+295
View File
@@ -0,0 +1,295 @@
#!/bin/sh
#
# Smoke test for self-hosted Supabase - verifies core functionality end-to-end.
#
# Usage:
# sh test-self-hosted.sh # Uses http://localhost:8000
# sh test-self-hosted.sh <base_url> # Custom URL
#
# Prerequisites:
# - Running self-hosted Supabase instance
# - .env file with keys configured
# - node >= 16 (for file generation)
#
set -e
BASE_URL="${1:-http://localhost:8000}"
if [ ! -f .env ]; then
echo "Error: .env file not found. Run from the project directory."
exit 1
fi
# Read keys from .env
ANON_KEY=$(grep '^ANON_KEY=' .env | cut -d= -f2-)
SERVICE_ROLE_KEY=$(grep '^SERVICE_ROLE_KEY=' .env | cut -d= -f2-)
DASHBOARD_USERNAME=$(grep '^DASHBOARD_USERNAME=' .env | cut -d= -f2-)
DASHBOARD_PASSWORD=$(grep '^DASHBOARD_PASSWORD=' .env | cut -d= -f2-)
pass=0
fail=0
check() {
test_name="$1"
expected="$2"
actual="$3"
if [ "$actual" = "$expected" ]; then
echo " PASS: $test_name"
pass=$((pass + 1))
else
echo " FAIL: $test_name (expected $expected, got $actual)"
fail=$((fail + 1))
fi
}
http_status() {
url="$1"
shift
curl -s -o /dev/null -w "%{http_code}" "$@" "$url"
}
http_body() {
url="$1"
shift
curl -s "$@" "$url"
}
echo ""
echo "=== Self-hosted smoke test against $BASE_URL ==="
echo ""
# ---------------------------------------------
# 1. Container health (via docker compose)
# ---------------------------------------------
echo "--- Container health ---"
if command -v docker >/dev/null 2>&1; then
unhealthy=$(docker compose ps --format json 2>/dev/null | node -e "
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
const lines=d.trim().split('\n').filter(Boolean);
const bad=lines.filter(l=>{try{const o=JSON.parse(l);return o.Health&&o.Health!=='healthy'}catch{return false}});
console.log(bad.length)
})" 2>/dev/null || echo "?")
if [ "$unhealthy" = "0" ]; then
check "All containers healthy" "0" "$unhealthy"
elif [ "$unhealthy" = "?" ]; then
echo " SKIP: Could not check container health"
else
check "All containers healthy" "0" "$unhealthy"
fi
else
echo " SKIP: docker not available"
fi
# ---------------------------------------------
# 2. Studio dashboard
# ---------------------------------------------
echo ""
echo "--- Studio dashboard ---"
# Studio may redirect (307/302) after auth - follow redirects
check "Studio accessible with basic auth" "200" \
"$(http_status "$BASE_URL/" -L -u "$DASHBOARD_USERNAME:$DASHBOARD_PASSWORD")"
check "Studio rejects without auth" "401" \
"$(http_status "$BASE_URL/")"
# ---------------------------------------------
# 3. Auth: sign up, sign in, get user, delete user
# ---------------------------------------------
echo ""
echo "--- Auth: user lifecycle ---"
test_email="smoke-test-$$@example.com"
test_password="smoke-test-password-123456"
# Sign up
signup_resp=$(http_body "$BASE_URL/auth/v1/signup" \
-H "apikey: $ANON_KEY" \
-H "Content-Type: application/json" \
-d "{\"email\":\"$test_email\",\"password\":\"$test_password\"}")
user_id=$(echo "$signup_resp" | node -e "
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
try{const j=JSON.parse(d);console.log(j.id||j.user?.id||'')}catch{console.log('')}
})" 2>/dev/null)
if [ -n "$user_id" ]; then
check "Sign up user" "true" "true"
# Sign in
signin_resp=$(http_body "$BASE_URL/auth/v1/token?grant_type=password" \
-H "apikey: $ANON_KEY" \
-H "Content-Type: application/json" \
-d "{\"email\":\"$test_email\",\"password\":\"$test_password\"}")
access_token=$(echo "$signin_resp" | node -e "
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
try{console.log(JSON.parse(d).access_token||'')}catch{console.log('')}
})" 2>/dev/null)
if [ -n "$access_token" ]; then
check "Sign in user" "true" "true"
# Get user
check "Get user profile" "200" \
"$(http_status "$BASE_URL/auth/v1/user" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $access_token")"
else
check "Sign in user" "true" "false"
fi
# Delete user (admin API with service_role key)
delete_status=$(http_status "$BASE_URL/auth/v1/admin/users/$user_id" \
-X DELETE \
-H "apikey: $SERVICE_ROLE_KEY" \
-H "Authorization: Bearer $SERVICE_ROLE_KEY")
check "Delete user (admin)" "200" "$delete_status"
else
echo " SKIP: Could not sign up user (email confirmation may be required)"
echo " Response: $signup_resp"
fi
# ---------------------------------------------
# 4. PostgREST: query
# ---------------------------------------------
echo ""
echo "--- PostgREST ---"
check "REST API query" "200" \
"$(http_status "$BASE_URL/rest/v1/" \
-H "apikey: $ANON_KEY")"
# ---------------------------------------------
# 5. GraphQL
# ---------------------------------------------
echo ""
echo "--- GraphQL ---"
gql_resp=$(http_body "$BASE_URL/graphql/v1" \
-H "apikey: $ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"{ __typename }"}')
gql_has_data=$(echo "$gql_resp" | node -e "
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
try{console.log(JSON.parse(d).data?'true':'false')}catch{console.log('false')}
})" 2>/dev/null)
check "GraphQL introspection" "true" "$gql_has_data"
# ---------------------------------------------
# 6. Storage: create bucket, upload >6MB file, download, cleanup
# ---------------------------------------------
echo ""
echo "--- Storage: bucket + file lifecycle ---"
bucket_name="smoke-test-$$"
# Create bucket
create_bucket_status=$(http_status "$BASE_URL/storage/v1/bucket" \
-X POST \
-H "apikey: $SERVICE_ROLE_KEY" \
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
-H "Content-Type: application/json" \
-d "{\"id\":\"$bucket_name\",\"name\":\"$bucket_name\",\"public\":true}")
check "Create bucket" "200" "$create_bucket_status"
if [ "$create_bucket_status" = "200" ]; then
# Generate a ~7MB file
tmpfile=$(mktemp)
dd if=/dev/urandom of="$tmpfile" bs=1048576 count=7 2>/dev/null
# Upload file
upload_status=$(http_status "$BASE_URL/storage/v1/object/$bucket_name/test-large-file.bin" \
-X POST \
-H "apikey: $SERVICE_ROLE_KEY" \
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$tmpfile")
check "Upload 7MB file" "200" "$upload_status"
# Download file and verify size
download_size=$(curl -s \
"$BASE_URL/storage/v1/object/public/$bucket_name/test-large-file.bin" | wc -c | tr -d ' ')
original_size=$(wc -c < "$tmpfile" | tr -d ' ')
check "Download file (size matches)" "true" \
"$([ "$download_size" = "$original_size" ] && echo true || echo false)"
rm -f "$tmpfile"
# Delete file
delete_file_status=$(http_status "$BASE_URL/storage/v1/object/$bucket_name/test-large-file.bin" \
-X DELETE \
-H "apikey: $SERVICE_ROLE_KEY" \
-H "Authorization: Bearer $SERVICE_ROLE_KEY")
check "Delete file" "200" "$delete_file_status"
# Delete bucket
delete_bucket_status=$(http_status "$BASE_URL/storage/v1/bucket/$bucket_name" \
-X DELETE \
-H "apikey: $SERVICE_ROLE_KEY" \
-H "Authorization: Bearer $SERVICE_ROLE_KEY")
check "Delete bucket" "200" "$delete_bucket_status"
fi
# ---------------------------------------------
# 7. Edge Functions
# ---------------------------------------------
echo ""
echo "--- Edge Functions ---"
fn_status=$(http_status "$BASE_URL/functions/v1/hello" \
-X POST \
-H "Authorization: Bearer $ANON_KEY" \
-H "Content-Type: application/json" \
-d '{}')
# hello function may return 200 or 204 depending on implementation
check "Call hello function -> not error" "true" \
"$([ "$fn_status" -lt 400 ] 2>/dev/null && echo true || echo false)"
# ---------------------------------------------
# 8. pg-meta (Studio backend)
# ---------------------------------------------
echo ""
echo "--- pg-meta ---"
check "pg-meta with service_role key" "200" \
"$(http_status "$BASE_URL/pg/schemas" \
-H "apikey: $SERVICE_ROLE_KEY")"
check "pg-meta rejects anon key" "403" \
"$(http_status "$BASE_URL/pg/schemas" \
-H "apikey: $ANON_KEY")"
check "pg-meta rejects no key" "401" \
"$(http_status "$BASE_URL/pg/schemas")"
echo ""
echo "--- MCP (blocked by default) ---"
check "/api/mcp blocked" "403" \
"$(http_status "$BASE_URL/api/mcp")"
check "/mcp blocked" "403" \
"$(http_status "$BASE_URL/mcp")"
# ---------------------------------------------
# 9. Realtime
# ---------------------------------------------
echo ""
echo "--- Realtime ---"
check "Realtime health" "true" \
"$([ "$(http_status "$BASE_URL/realtime/v1/api/tenants" \
-H "apikey: $ANON_KEY")" != "401" ] && echo true || echo false)"
# ---------------------------------------------
# Summary
# ---------------------------------------------
echo ""
echo "=== Results: $pass passed, $fail failed ==="
echo ""
if [ "$fail" -gt 0 ]; then
exit 1
fi