diff --git a/.gitattributes b/.gitattributes index 1b85546..2c7d664 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ migrations/*.sql text eol=crlf +*.sh text eol=lf diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 55b8287..8edd3e1 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -8,6 +8,32 @@ on: jobs: verify: runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_PASSWORD: codex_test + POSTGRES_DB: imageforge_test + options: >- + --health-cmd "pg_isready -U postgres -d imageforge_test" + --health-interval 5s + --health-timeout 3s + --health-retries 20 + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 5s + --health-timeout 3s + --health-retries 20 + env: + DATABASE_URL: postgres://postgres:codex_test@postgres:5432/imageforge_test + REDIS_URL: redis://redis:6379/ + IMAGEFORGE_TEST_DATABASE_URL: postgres://postgres:codex_test@postgres:5432/imageforge_test + IMAGEFORGE_TEST_REDIS_URL: redis://redis:6379/ + JWT_SECRET: imageforge-ci-jwt-secret + API_KEY_PEPPER: imageforge-ci-api-key-pepper + EXPECTED_EXTERNAL_TESTS: '9' steps: - name: Checkout uses: actions/checkout@v4 @@ -40,10 +66,13 @@ jobs: run: cargo fmt --all -- --check - name: Run Clippy - run: cargo clippy --all-targets -- -D warnings + run: cargo clippy --all-targets --all-features --locked -- -D warnings - name: Run Rust tests - run: cargo test --all-targets + run: cargo test --all-targets --locked + + - name: Run external-state tests + run: bash scripts/run_external_state_tests.sh - name: Install cargo-audit run: cargo install cargo-audit --locked --version 0.22.2 diff --git a/scripts/run_external_state_tests.sh b/scripts/run_external_state_tests.sh new file mode 100644 index 0000000..dd96322 --- /dev/null +++ b/scripts/run_external_state_tests.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +EXPECTED_EXTERNAL_TESTS="${EXPECTED_EXTERNAL_TESTS:-9}" +WORK_DIR="$(mktemp -d)" +POSTGRES_CONTAINER="" +REDIS_CONTAINER="" +MINIO_PID="" + +cleanup() { + local status=$? + trap - EXIT INT TERM + set +e + if [[ -n "$MINIO_PID" ]]; then + kill "$MINIO_PID" 2>/dev/null + wait "$MINIO_PID" 2>/dev/null + fi + if [[ -n "$POSTGRES_CONTAINER" ]]; then + docker rm -f "$POSTGRES_CONTAINER" >/dev/null 2>&1 + fi + if [[ -n "$REDIS_CONTAINER" ]]; then + docker rm -f "$REDIS_CONTAINER" >/dev/null 2>&1 + fi + rm -rf "$WORK_DIR" + exit "$status" +} +trap cleanup EXIT INT TERM + +wait_for_command() { + local description=$1 + shift + for _ in $(seq 1 60); do + if "$@" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + echo "Timed out waiting for ${description}" >&2 + return 1 +} + +if [[ -z "${IMAGEFORGE_TEST_DATABASE_URL:-}" || -z "${IMAGEFORGE_TEST_REDIS_URL:-}" ]]; then + if ! command -v docker >/dev/null 2>&1; then + echo "Set IMAGEFORGE_TEST_DATABASE_URL and IMAGEFORGE_TEST_REDIS_URL, or install Docker." >&2 + exit 1 + fi + + suffix="$$-${RANDOM}" + postgres_port="${IMAGEFORGE_TEST_POSTGRES_PORT:-25432}" + redis_port="${IMAGEFORGE_TEST_REDIS_PORT:-26379}" + POSTGRES_CONTAINER="imageforge-test-postgres-${suffix}" + REDIS_CONTAINER="imageforge-test-redis-${suffix}" + + docker run -d \ + --label imageforge.external-tests=true \ + --name "$POSTGRES_CONTAINER" \ + -e POSTGRES_PASSWORD=codex_test \ + -e POSTGRES_DB=imageforge_test \ + -p "127.0.0.1:${postgres_port}:5432" \ + postgres:16-alpine >/dev/null + docker run -d \ + --label imageforge.external-tests=true \ + --name "$REDIS_CONTAINER" \ + -p "127.0.0.1:${redis_port}:6379" \ + redis:7-alpine >/dev/null + + wait_for_command PostgreSQL docker exec "$POSTGRES_CONTAINER" \ + pg_isready -U postgres -d imageforge_test + wait_for_command Redis docker exec "$REDIS_CONTAINER" redis-cli ping + + export IMAGEFORGE_TEST_DATABASE_URL="postgres://postgres:codex_test@127.0.0.1:${postgres_port}/imageforge_test" + export IMAGEFORGE_TEST_REDIS_URL="redis://127.0.0.1:${redis_port}/" +fi + +arch="$(uname -m)" +case "$arch" in + x86_64) minio_arch=amd64 ;; + aarch64|arm64) minio_arch=arm64 ;; + *) echo "Unsupported MinIO architecture: ${arch}" >&2; exit 1 ;; +esac + +MINIO_BIN="${MINIO_BIN:-${WORK_DIR}/minio}" +MC_BIN="${MC_BIN:-${WORK_DIR}/mc}" +if [[ ! -x "$MINIO_BIN" ]]; then + curl --fail --silent --show-error --location --retry 3 \ + "https://dl.min.io/server/minio/release/linux-${minio_arch}/minio" \ + --output "$MINIO_BIN" + chmod +x "$MINIO_BIN" +fi +if [[ ! -x "$MC_BIN" ]]; then + curl --fail --silent --show-error --location --retry 3 \ + "https://dl.min.io/client/mc/release/linux-${minio_arch}/mc" \ + --output "$MC_BIN" + chmod +x "$MC_BIN" +fi + +export IMAGEFORGE_TEST_S3_ENDPOINT="${IMAGEFORGE_TEST_S3_ENDPOINT:-http://127.0.0.1:19000}" +export IMAGEFORGE_TEST_S3_BUCKET="${IMAGEFORGE_TEST_S3_BUCKET:-imageforge-test}" +export IMAGEFORGE_TEST_S3_ACCESS_KEY="${IMAGEFORGE_TEST_S3_ACCESS_KEY:-codexminio}" +export IMAGEFORGE_TEST_S3_SECRET_KEY="${IMAGEFORGE_TEST_S3_SECRET_KEY:-codexminio123}" + +if [[ "$IMAGEFORGE_TEST_S3_ENDPOINT" == "http://127.0.0.1:19000" ]]; then + MINIO_ROOT_USER="$IMAGEFORGE_TEST_S3_ACCESS_KEY" \ + MINIO_ROOT_PASSWORD="$IMAGEFORGE_TEST_S3_SECRET_KEY" \ + "$MINIO_BIN" server "${WORK_DIR}/minio-data" \ + --address 127.0.0.1:19000 >"${WORK_DIR}/minio.log" 2>&1 & + MINIO_PID=$! + wait_for_command MinIO curl --fail --silent \ + "${IMAGEFORGE_TEST_S3_ENDPOINT}/minio/health/ready" +fi + +"$MC_BIN" alias set imageforge-test \ + "$IMAGEFORGE_TEST_S3_ENDPOINT" \ + "$IMAGEFORGE_TEST_S3_ACCESS_KEY" \ + "$IMAGEFORGE_TEST_S3_SECRET_KEY" >/dev/null +"$MC_BIN" mb --ignore-existing \ + "imageforge-test/${IMAGEFORGE_TEST_S3_BUCKET}" >/dev/null + +export DATABASE_URL="$IMAGEFORGE_TEST_DATABASE_URL" +export REDIS_URL="$IMAGEFORGE_TEST_REDIS_URL" +export JWT_SECRET="${JWT_SECRET:-imageforge-external-test-jwt-secret}" +export API_KEY_PEPPER="${API_KEY_PEPPER:-imageforge-external-test-api-key-pepper}" + +test_log="${WORK_DIR}/external-tests.log" +cargo test --all-targets --locked -- \ + --ignored --test-threads=1 --nocapture 2>&1 | tee "$test_log" + +expected_summary="test result: ok. ${EXPECTED_EXTERNAL_TESTS} passed; 0 failed; 0 ignored;" +if ! grep --fixed-strings --quiet "$expected_summary" "$test_log"; then + echo "External-state test count mismatch; expected: ${expected_summary}" >&2 + exit 1 +fi + +echo "External-state gate passed: ${EXPECTED_EXTERNAL_TESTS} passed, 0 failed, 0 ignored."