mirror of
https://github.com/supabase/supabase.git
synced 2026-06-13 01:39:53 +08:00
470 lines
17 KiB
Bash
470 lines
17 KiB
Bash
#!/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
|
|
# - jq (for JSON parsing)
|
|
# - sha256sum or shasum (for file integrity checks)
|
|
#
|
|
|
|
set -e
|
|
|
|
cleanup_files=""
|
|
trap 'rm -f $cleanup_files' EXIT
|
|
|
|
BASE_URL="${1:-http://localhost:8000}"
|
|
|
|
if [ ! -f .env ]; then
|
|
echo "Error: .env file not found. Run from the project directory."
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
echo "Error: jq not found. Install it: https://jqlang.github.io/jq/download/"
|
|
exit 1
|
|
fi
|
|
|
|
# Portable file hash: prefers sha256sum (Linux), falls back to shasum (macOS)
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
file_hash() { sha256sum "$1" | awk '{print $1}'; }
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
file_hash() { shasum -a 256 "$1" | awk '{print $1}'; }
|
|
else
|
|
echo "Error: sha256sum or shasum not found."
|
|
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
|
|
container_status=$(docker compose ps --format json 2>/dev/null | jq -rs '
|
|
[.[] | select(.State != "running" or (.Health != "" and .Health != "healthy"))]
|
|
| (length | tostring) + "|" + ([.[] | .Service + ": State=" + .State + " Health=" + (.Health // "none")] | join(", "))
|
|
' 2>/dev/null || echo "?|")
|
|
unhealthy="${container_status%%|*}"
|
|
container_issues="${container_status#*|}"
|
|
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 ($container_issues)" "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: create user, sign in, get user, public signup, delete
|
|
# ---------------------------------------------
|
|
|
|
echo ""
|
|
echo "--- Auth: user lifecycle ---"
|
|
|
|
test_email="smoke-test-$$@example.com"
|
|
test_password="smoke-test-password-123456"
|
|
|
|
# Create user via admin API (works regardless of email autoconfirm setting)
|
|
create_resp=$(http_body "$BASE_URL/auth/v1/admin/users" \
|
|
-X POST \
|
|
-H "apikey: $SERVICE_ROLE_KEY" \
|
|
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"email\":\"$test_email\",\"password\":\"$test_password\",\"email_confirm\":true}")
|
|
|
|
user_id=$(echo "$create_resp" | jq -r '.id // empty' 2>/dev/null)
|
|
|
|
if [ -n "$user_id" ]; then
|
|
check "Create user (admin)" "true" "true"
|
|
|
|
# Sign in via public endpoint
|
|
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" | jq -r '.access_token // empty' 2>/dev/null)
|
|
|
|
if [ -n "$access_token" ]; then
|
|
check "Sign in user" "true" "true"
|
|
|
|
# Get user profile with session JWT
|
|
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
|
|
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
|
|
check "Create user (admin)" "true" "false"
|
|
fi
|
|
|
|
# Public signup (optional — depends on email autoconfirm setting)
|
|
signup_email="smoke-signup-$$@example.com"
|
|
signup_resp=$(http_body "$BASE_URL/auth/v1/signup" \
|
|
-H "apikey: $ANON_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"email\":\"$signup_email\",\"password\":\"$test_password\"}")
|
|
|
|
signup_token=$(echo "$signup_resp" | jq -r '.access_token // empty' 2>/dev/null)
|
|
signup_user_id=$(echo "$signup_resp" | jq -r '.id // .user.id // empty' 2>/dev/null)
|
|
|
|
if [ -n "$signup_token" ]; then
|
|
check "Public signup (autoconfirm on)" "true" "true"
|
|
else
|
|
echo " SKIP: Public signup (autoconfirm is off)"
|
|
fi
|
|
|
|
# Clean up signup user if created
|
|
if [ -n "$signup_user_id" ]; then
|
|
http_status "$BASE_URL/auth/v1/admin/users/$signup_user_id" \
|
|
-X DELETE \
|
|
-H "apikey: $SERVICE_ROLE_KEY" \
|
|
-H "Authorization: Bearer $SERVICE_ROLE_KEY" >/dev/null 2>&1
|
|
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" | jq -r 'if .data then "true" else "false" end' 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); cleanup_files="$cleanup_files $tmpfile"
|
|
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 integrity
|
|
download_tmp=$(mktemp); cleanup_files="$cleanup_files $download_tmp"
|
|
curl -s "$BASE_URL/storage/v1/object/public/$bucket_name/test-large-file.bin" -o "$download_tmp"
|
|
original_size=$(wc -c < "$tmpfile" | tr -d ' ')
|
|
download_size=$(wc -c < "$download_tmp" | tr -d ' ')
|
|
check "Download file (size matches)" "$original_size" "$download_size"
|
|
original_hash=$(file_hash "$tmpfile")
|
|
download_hash=$(file_hash "$download_tmp")
|
|
check "Download file (hash matches)" "$original_hash" "$download_hash"
|
|
rm -f "$download_tmp"
|
|
|
|
rm -f "$tmpfile"
|
|
|
|
# Signed URL: upload a small file, create signed URL, fetch without auth
|
|
sign_upload_status=$(http_status "$BASE_URL/storage/v1/object/$bucket_name/sign-test.txt" \
|
|
-X POST \
|
|
-H "apikey: $SERVICE_ROLE_KEY" \
|
|
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
|
-H "Content-Type: text/plain" \
|
|
--data-binary "signed url test content")
|
|
check "Upload file for signing" "200" "$sign_upload_status"
|
|
|
|
if [ "$sign_upload_status" = "200" ]; then
|
|
sign_resp=$(http_body "$BASE_URL/storage/v1/object/sign/$bucket_name/sign-test.txt" \
|
|
-X POST \
|
|
-H "apikey: $SERVICE_ROLE_KEY" \
|
|
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"expiresIn": 600}')
|
|
signed_path=$(echo "$sign_resp" | jq -r '.signedURL // empty' 2>/dev/null)
|
|
|
|
if [ -n "$signed_path" ]; then
|
|
check "Create signed URL" "true" "true"
|
|
# Fetch signed URL without any auth headers (goes through Kong)
|
|
signed_content=$(curl -s "$BASE_URL/storage/v1$signed_path")
|
|
check "Fetch signed URL (no auth)" "signed url test content" "$signed_content"
|
|
else
|
|
check "Create signed URL" "true" "false"
|
|
fi
|
|
fi
|
|
|
|
# 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 signed test file
|
|
http_status "$BASE_URL/storage/v1/object/$bucket_name/sign-test.txt" \
|
|
-X DELETE \
|
|
-H "apikey: $SERVICE_ROLE_KEY" \
|
|
-H "Authorization: Bearer $SERVICE_ROLE_KEY" >/dev/null 2>&1
|
|
|
|
# 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
|
|
|
|
# ---------------------------------------------
|
|
# 6b. Storage: TUS resumable upload
|
|
# ---------------------------------------------
|
|
|
|
echo ""
|
|
echo "--- Storage: TUS resumable upload ---"
|
|
|
|
tus_bucket="smoke-tus-$$"
|
|
|
|
tus_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\":\"$tus_bucket\",\"name\":\"$tus_bucket\",\"public\":true}")
|
|
check "TUS: create bucket" "200" "$tus_bucket_status"
|
|
|
|
if [ "$tus_bucket_status" = "200" ]; then
|
|
# Generate a ~7MB file (above Studio's 6MB TUS threshold)
|
|
tusfile=$(mktemp); cleanup_files="$cleanup_files $tusfile"
|
|
dd if=/dev/urandom of="$tusfile" bs=1048576 count=7 2>/dev/null
|
|
tus_file_size=$(wc -c < "$tusfile" | tr -d ' ')
|
|
tus_chunk_size=$((4 * 1048576)) # 4MB first chunk
|
|
|
|
# Encode TUS metadata values as base64
|
|
tus_bucket_b64=$(printf '%s' "$tus_bucket" | base64)
|
|
tus_object_b64=$(printf '%s' "tus-test-file.bin" | base64)
|
|
tus_mime_b64=$(printf '%s' "application/octet-stream" | base64)
|
|
|
|
# 1. Create resumable upload
|
|
tus_create_resp=$(curl -s -i -X POST \
|
|
"$BASE_URL/storage/v1/upload/resumable" \
|
|
-H "apikey: $SERVICE_ROLE_KEY" \
|
|
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
|
-H "Tus-Resumable: 1.0.0" \
|
|
-H "Upload-Length: $tus_file_size" \
|
|
-H "Upload-Metadata: bucketName $tus_bucket_b64,objectName $tus_object_b64,contentType $tus_mime_b64" \
|
|
-H "x-upsert: true")
|
|
tus_create_status=$(echo "$tus_create_resp" | grep -m1 '^HTTP/' | grep -o '[0-9][0-9][0-9]')
|
|
# Supabase Storage always returns an absolute Location URL (see generateUrl in storage/src/http/routes/tus/lifecycle.ts)
|
|
tus_location=$(echo "$tus_create_resp" | grep -i '^location:' | tr -d '\r' | sed 's/^[Ll]ocation: *//')
|
|
check "TUS: create resumable upload" "201" "$tus_create_status"
|
|
|
|
if [ -n "$tus_location" ]; then
|
|
# 2. Upload first chunk (0 to 4MB)
|
|
tus_chunk1_status=$(dd if="$tusfile" bs=1048576 count=4 2>/dev/null | \
|
|
curl -s -o /dev/null -w "%{http_code}" -X PATCH \
|
|
"$tus_location" \
|
|
-H "apikey: $SERVICE_ROLE_KEY" \
|
|
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
|
-H "Tus-Resumable: 1.0.0" \
|
|
-H "Upload-Offset: 0" \
|
|
-H "Content-Type: application/offset+octet-stream" \
|
|
--data-binary @-)
|
|
check "TUS: upload chunk 1 (4MB)" "204" "$tus_chunk1_status"
|
|
|
|
# 3. Upload second chunk (4MB to end)
|
|
tus_remaining=$((tus_file_size - tus_chunk_size))
|
|
tus_chunk2_status=$(dd if="$tusfile" bs=1048576 skip=4 count=3 2>/dev/null | \
|
|
curl -s -o /dev/null -w "%{http_code}" -X PATCH \
|
|
"$tus_location" \
|
|
-H "apikey: $SERVICE_ROLE_KEY" \
|
|
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
|
-H "Tus-Resumable: 1.0.0" \
|
|
-H "Upload-Offset: $tus_chunk_size" \
|
|
-H "Content-Type: application/offset+octet-stream" \
|
|
--data-binary @-)
|
|
check "TUS: upload chunk 2 (remaining)" "204" "$tus_chunk2_status"
|
|
|
|
# 4. Verify download matches original (hash check proves correct chunk reassembly)
|
|
tus_download_tmp=$(mktemp); cleanup_files="$cleanup_files $tus_download_tmp"
|
|
curl -s "$BASE_URL/storage/v1/object/public/$tus_bucket/tus-test-file.bin" -o "$tus_download_tmp"
|
|
tus_download_size=$(wc -c < "$tus_download_tmp" | tr -d ' ')
|
|
check "TUS: download size matches" "$tus_file_size" "$tus_download_size"
|
|
tus_original_hash=$(file_hash "$tusfile")
|
|
tus_download_hash=$(file_hash "$tus_download_tmp")
|
|
check "TUS: download hash matches" "$tus_original_hash" "$tus_download_hash"
|
|
rm -f "$tus_download_tmp"
|
|
fi
|
|
|
|
rm -f "$tusfile"
|
|
|
|
# Cleanup: delete file and bucket
|
|
http_status "$BASE_URL/storage/v1/object/$tus_bucket/tus-test-file.bin" \
|
|
-X DELETE \
|
|
-H "apikey: $SERVICE_ROLE_KEY" \
|
|
-H "Authorization: Bearer $SERVICE_ROLE_KEY" >/dev/null 2>&1
|
|
|
|
tus_delete_bucket=$(http_status "$BASE_URL/storage/v1/bucket/$tus_bucket" \
|
|
-X DELETE \
|
|
-H "apikey: $SERVICE_ROLE_KEY" \
|
|
-H "Authorization: Bearer $SERVICE_ROLE_KEY")
|
|
check "TUS: delete bucket" "200" "$tus_delete_bucket"
|
|
fi
|
|
|
|
# ---------------------------------------------
|
|
# 7. Edge Functions
|
|
# ---------------------------------------------
|
|
|
|
echo ""
|
|
echo "--- Edge Functions ---"
|
|
fn_resp=$(http_body "$BASE_URL/functions/v1/hello" \
|
|
-X POST \
|
|
-H "Authorization: Bearer $ANON_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{}')
|
|
check "Call hello function" '"Hello from Edge Functions!"' "$fn_resp"
|
|
|
|
# ---------------------------------------------
|
|
# 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 (ping)" "200" \
|
|
"$(http_status "$BASE_URL/realtime/v1/api/ping" \
|
|
-H "apikey: $ANON_KEY")"
|
|
|
|
# Management endpoints must be blocked at the gateway (even with a valid key)
|
|
check "Realtime /api/tenants blocked" "403" \
|
|
"$(http_status "$BASE_URL/realtime/v1/api/tenants" \
|
|
-H "apikey: $ANON_KEY")"
|
|
check "Realtime /api/openapi blocked" "403" \
|
|
"$(http_status "$BASE_URL/realtime/v1/api/openapi" \
|
|
-H "apikey: $ANON_KEY")"
|
|
|
|
# ---------------------------------------------
|
|
# Summary
|
|
# ---------------------------------------------
|
|
|
|
echo ""
|
|
echo "=== Results: $pass passed, $fail failed ==="
|
|
echo ""
|
|
|
|
if [ "$fail" -gt 0 ]; then
|
|
exit 1
|
|
fi
|