#!/usr/bin/env bash set -Eeuo pipefail repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" cd "$repository_root" command -v docker >/dev/null || { echo "Docker is required for the infrastructure smoke test." >&2 exit 1 } command -v curl >/dev/null || { echo "curl is required for the infrastructure smoke test." >&2 exit 1 } command -v cmp >/dev/null || { echo "cmp is required for the infrastructure smoke test." >&2 exit 1 } docker compose version >/dev/null curl_help="$(curl --help all)" if [[ "$curl_help" != *"--aws-sigv4"* ]]; then echo "curl must support --aws-sigv4 for authenticated S3 checks." >&2 exit 1 fi raw_project="ekklesia-smoke-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-0}-$$" project="${raw_project//[^a-zA-Z0-9_-]/-}" project="${project,,}" temporary_directory="$(mktemp -d)" payload_file="$temporary_directory/payload.txt" download_file="$temporary_directory/download.txt" bucket_list_file="$temporary_directory/buckets.xml" : >"$bucket_list_file" export POSTGRES_DB="ekklesia_smoke" export POSTGRES_USER="ekklesia_smoke" export POSTGRES_PASSWORD="ekklesia-smoke-postgres-password" export POSTGRES_PORT="${EKKLESIA_SMOKE_POSTGRES_PORT:-0}" export S3_ACCESS_KEY="ekklesia-smoke-access" export S3_SECRET_KEY="ekklesia-smoke-secret-key" export S3_BUCKET="ekklesia-smoke" export S3_REGION="us-east-1" export S3_PORT="${EKKLESIA_SMOKE_S3_PORT:-0}" compose=( docker compose --project-name "$project" --env-file .env.example --file infrastructure/compose.yaml ) cleanup() { local exit_status=$? local cleanup_output trap - EXIT INT TERM if ((exit_status != 0)); then "${compose[@]}" logs --no-color >&2 || true fi if ! cleanup_output="$( "${compose[@]}" down --volumes --remove-orphans --timeout 10 2>&1 )"; then echo "Infrastructure smoke cleanup failed:" >&2 echo "$cleanup_output" >&2 if ((exit_status == 0)); then exit_status=1 fi fi rm -rf -- "$temporary_directory" exit "$exit_status" } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM "${compose[@]}" config --quiet "${compose[@]}" up --detach --wait --wait-timeout 120 "${compose[@]}" up --detach --wait --wait-timeout 120 database_result="$( "${compose[@]}" exec -T \ -e "PGPASSWORD=$POSTGRES_PASSWORD" \ postgres \ psql \ --no-psqlrc \ --host 127.0.0.1 \ --username "$POSTGRES_USER" \ --dbname "$POSTGRES_DB" \ --tuples-only \ --no-align \ --set ON_ERROR_STOP=1 \ --command "SELECT current_database(), current_user" )" if [[ "$database_result" != "$POSTGRES_DB|$POSTGRES_USER" ]]; then echo "PostgreSQL returned an unexpected identity: $database_result" >&2 exit 1 fi if "${compose[@]}" exec -T \ -e "PGPASSWORD=incorrect-password" \ postgres \ psql \ --no-psqlrc \ --host 127.0.0.1 \ --username "$POSTGRES_USER" \ --dbname "$POSTGRES_DB" \ --command "SELECT 1" >/dev/null 2>&1; then echo "PostgreSQL unexpectedly accepted an invalid password." >&2 exit 1 fi s3_binding="$("${compose[@]}" port object-storage 8333)" s3_port="${s3_binding##*:}" if [[ ! "$s3_port" =~ ^[0-9]+$ ]]; then echo "Compose returned an invalid S3 port binding: $s3_binding" >&2 exit 1 fi s3_endpoint="http://127.0.0.1:$s3_port" signed_curl=( curl --silent --show-error --aws-sigv4 "aws:amz:$S3_REGION:s3" --user "$S3_ACCESS_KEY:$S3_SECRET_KEY" ) s3_ready=false for _ in {1..30}; do status="$( "${signed_curl[@]}" \ --output "$bucket_list_file" \ --write-out "%{http_code}" \ "$s3_endpoint/" || true )" bucket_list="$(<"$bucket_list_file")" if [[ "$status" == "200" && "$bucket_list" == *"$S3_BUCKET"* ]]; then s3_ready=true break fi sleep 2 done if [[ "$s3_ready" != "true" ]]; then echo "The authenticated S3 endpoint and bucket did not become ready." >&2 exit 1 fi printf '%s' "ekklesia-storage-smoke" >"$payload_file" object_key="smoke/$project/object.txt" object_url="$s3_endpoint/$S3_BUCKET/$object_key" put_status="$( "${signed_curl[@]}" \ --upload-file "$payload_file" \ --header "Content-Type: text/plain" \ --output /dev/null \ --write-out "%{http_code}" \ "$object_url" )" if [[ "$put_status" != "200" ]]; then echo "S3 PUT returned $put_status instead of 200." >&2 exit 1 fi get_status="$( "${signed_curl[@]}" \ --output "$download_file" \ --write-out "%{http_code}" \ "$object_url" )" if [[ "$get_status" != "200" ]] || ! cmp --silent "$payload_file" "$download_file"; then echo "S3 GET did not return the uploaded object unchanged." >&2 exit 1 fi head_status="$( "${signed_curl[@]}" --head --output /dev/null --write-out "%{http_code}" "$object_url" )" if [[ "$head_status" != "200" ]]; then echo "S3 HEAD returned $head_status instead of 200." >&2 exit 1 fi anonymous_status="$( curl --silent --show-error --output /dev/null --write-out "%{http_code}" "$object_url" )" if [[ "$anonymous_status" != "403" ]]; then echo "S3 anonymous GET returned $anonymous_status instead of 403." >&2 exit 1 fi bad_signature_status="$( curl \ --silent \ --show-error \ --aws-sigv4 "aws:amz:$S3_REGION:s3" \ --user "$S3_ACCESS_KEY:incorrect-secret" \ --output /dev/null \ --write-out "%{http_code}" \ "$object_url" )" if [[ "$bad_signature_status" != "403" ]]; then echo "S3 invalid signature returned $bad_signature_status instead of 403." >&2 exit 1 fi delete_status="$( "${signed_curl[@]}" \ --request DELETE \ --output /dev/null \ --write-out "%{http_code}" \ "$object_url" )" if [[ "$delete_status" != "204" ]]; then echo "S3 DELETE returned $delete_status instead of 204." >&2 exit 1 fi missing_status="$( "${signed_curl[@]}" --head --output /dev/null --write-out "%{http_code}" "$object_url" )" if [[ "$missing_status" != "404" ]]; then echo "Deleted S3 object returned $missing_status instead of 404." >&2 exit 1 fi echo "Infrastructure smoke test passed for PostgreSQL and authenticated S3 operations."