From b48f91d6ab9328b059de144894476eb806ff18bc Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 7 Oct 2026 21:40:47 +0000 Subject: [PATCH 1/6] test(images): add app runtime acceptance test Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- Makefile | 15 +++- docs/images.md | 20 +++++ tests/test-app-image.sh | 188 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 tests/test-app-image.sh diff --git a/Makefile b/Makefile index fa9655d..f5c5968 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ COMPOSE ?= docker compose GARAGES3_COMPOSE_FILE ?= docker-compose-garages3.yml +APP_TEST_IMAGE ?= nextcloud-app:acceptance -.PHONY: up-garages3 down-garages3 bootstrap-garages3 garage-status-garages3 start-garages3 wait-nextcloud-garages3 setup-garages3 test-hooks test-scan-images test-ncdd scan-images +.PHONY: up-garages3 down-garages3 bootstrap-garages3 garage-status-garages3 start-garages3 wait-nextcloud-garages3 setup-garages3 test-hooks test-scan-images test-ncdd test-app-image test-current-app-image scan-images up-garages3: $(COMPOSE) -f $(GARAGES3_COMPOSE_FILE) up -d garage @@ -35,6 +36,18 @@ test-scan-images: test-ncdd: bats tests/ncdd.bats +test-app-image: + @test -n "$(APP_IMAGE)" || { echo 'Usage: make test-app-image APP_IMAGE=' >&2; exit 2; } + bash tests/test-app-image.sh "$(APP_IMAGE)" + +test-current-app-image: + @set -e; \ + version="$$(sed -n 's/^NEXTCLOUD_VERSION=//p' .env.example | head -n 1)"; \ + test -n "$$version" || { echo 'NEXTCLOUD_VERSION is missing from .env.example' >&2; exit 1; }; \ + docker buildx build --platform linux/amd64 --load --tag "$(APP_TEST_IMAGE)" \ + --build-arg "NEXTCLOUD_VERSION=$$version" --file .docker/app/Dockerfile .docker/app; \ + $(MAKE) test-app-image APP_IMAGE="$(APP_TEST_IMAGE)" + scan-images: @set -e; \ version="$$(sed -n 's/^NEXTCLOUD_VERSION=//p' .env.example | head -n 1)"; \ diff --git a/docs/images.md b/docs/images.md index af436fd..5d3ed88 100644 --- a/docs/images.md +++ b/docs/images.md @@ -149,6 +149,26 @@ Immutable web image: ghcr.io/librecodecoop/nextcloud-docker-web@sha256: ``` +## Runtime acceptance + +The app image has a runtime acceptance test based on the same behavioral checks used by the official Nextcloud container projects. + +The test operates on an already-built local image. It does not rebuild the image and does not use the repository deployment Compose files: + +```bash +make test-app-image APP_IMAGE=scan/app:amd64 +``` + +For a local build of the current app image followed by the same acceptance test: + +```bash +make test-current-app-image +``` + +The acceptance test creates an isolated Docker network and PostgreSQL container, starts the image with Nextcloud autoinstall variables, waits for the installation, runs `occ status` and `occ check`, and sends a FastCGI request through the FPM runtime. Test-created resources are removed on success and failure. + +CI must run this test against the exact locally loaded app images produced by the build step. Runtime acceptance is a publication gate alongside vulnerability scanning; a separate deployment-stack test is not required for this contract. + ## Implementation boundary This document defines the target contract. It does not by itself migrate the current Dockerfiles, workflows, or historical tags. diff --git a/tests/test-app-image.sh b/tests/test-app-image.sh new file mode 100644 index 0000000..916d39e --- /dev/null +++ b/tests/test-app-image.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +if [ "$#" -ne 1 ]; then + printf 'Usage: %s IMAGE\n' "$0" >&2 + exit 2 +fi + +image=$1 +postgres_image=${NEXTCLOUD_IMAGE_TEST_POSTGRES_IMAGE:-postgres:18-alpine} +timeout_seconds=${NEXTCLOUD_IMAGE_TEST_TIMEOUT:-300} +runtime_user=${NEXTCLOUD_IMAGE_TEST_RUNTIME_USER:-www-data} + +if ! command -v docker >/dev/null 2>&1; then + printf 'Docker is required to run the app image acceptance test.\n' >&2 + exit 127 +fi + +if ! docker image inspect "$image" >/dev/null 2>&1; then + printf 'Image not found locally: %s\n' "$image" >&2 + exit 2 +fi + +image_arch=$(docker image inspect --format '{{.Architecture}}' "$image") +case "$image_arch" in + amd64|arm64) + image_platform="linux/$image_arch" + ;; + *) + printf 'Unsupported image architecture: %s\n' "$image_arch" >&2 + exit 2 + ;; +esac + +token="nextcloud-app-test-$$-${RANDOM}" +network_name="$token" +db_container="${token}-db" +app_container="${token}-app" +fcgi_client_image="${token}-fcgi-client" + +network_created=false +db_created=false +app_created=false +fcgi_client_created=false + +db_user=nextcloud +db_database=nextcloud +db_password="test-${RANDOM}-${RANDOM}-password" +admin_user=test_admin +admin_password="test-${RANDOM}-${RANDOM}-admin-password" + +diagnostics() { + printf '\n=== app container logs ===\n' >&2 + if $app_created; then + docker logs "$app_container" >&2 || true + else + printf 'app container was not created\n' >&2 + fi + + printf '\n=== postgres container logs ===\n' >&2 + if $db_created; then + docker logs "$db_container" >&2 || true + else + printf 'postgres container was not created\n' >&2 + fi +} + +cleanup() { + local status=$? + set +e + + if [ "$status" -ne 0 ]; then + diagnostics + fi + + if $app_created; then + docker rm -f "$app_container" >/dev/null 2>&1 || true + fi + if $db_created; then + docker rm -f "$db_container" >/dev/null 2>&1 || true + fi + if $network_created; then + docker network rm "$network_name" >/dev/null 2>&1 || true + fi + if $fcgi_client_created; then + docker image rm -f "$fcgi_client_image" >/dev/null 2>&1 || true + fi + + exit "$status" +} +trap cleanup EXIT + +wait_until() { + local description=$1 + shift + + local started now + started=$(date +%s) + + while true; do + if "$@"; then + return 0 + fi + + now=$(date +%s) + if [ $((now - started)) -ge "$timeout_seconds" ]; then + printf 'Timed out after %ss waiting for %s.\n' "$timeout_seconds" "$description" >&2 + return 1 + fi + + sleep 2 + done +} + +postgres_ready() { + docker exec "$db_container" pg_isready -U "$db_user" -d "$db_database" >/dev/null 2>&1 +} + +nextcloud_installed() { + local status + status=$(docker exec -u "$runtime_user" "$app_container" php occ status --output=json 2>/dev/null) || return 1 + grep -Eq '"installed"[[:space:]]*:[[:space:]]*true' <<<"$status" +} + +printf 'Testing %s (%s)\n' "$image" "$image_platform" + +docker network create "$network_name" >/dev/null +network_created=true + +docker run -d \ + --name "$db_container" \ + --network "$network_name" \ + --network-alias db \ + -e "POSTGRES_USER=$db_user" \ + -e "POSTGRES_PASSWORD=$db_password" \ + -e "POSTGRES_DB=$db_database" \ + "$postgres_image" >/dev/null +db_created=true + +wait_until 'PostgreSQL readiness' postgres_ready + +docker run -d \ + --name "$app_container" \ + --network "$network_name" \ + --network-alias app \ + --platform "$image_platform" \ + -e POSTGRES_HOST=db \ + -e "POSTGRES_USER=$db_user" \ + -e "POSTGRES_PASSWORD=$db_password" \ + -e "POSTGRES_DB=$db_database" \ + -e "NEXTCLOUD_ADMIN_USER=$admin_user" \ + -e "NEXTCLOUD_ADMIN_PASSWORD=$admin_password" \ + -e NEXTCLOUD_TRUSTED_DOMAINS=localhost \ + "$image" >/dev/null +app_created=true + +wait_until 'Nextcloud installation' nextcloud_installed + +printf 'Nextcloud installed successfully.\n' +docker exec -u "$runtime_user" "$app_container" php occ status --output=json +docker exec -u "$runtime_user" "$app_container" php occ check + +docker build -q -t "$fcgi_client_image" - <<'EOF' >/dev/null +FROM debian:trixie-slim +RUN apt-get update \ + && apt-get install -y --no-install-recommends libfcgi-bin \ + && rm -rf /var/lib/apt/lists/* +ENTRYPOINT ["cgi-fcgi"] +EOF +fcgi_client_created=true + +fcgi_response=$( + docker run --rm -i \ + --network "$network_name" \ + -e REQUEST_METHOD=GET \ + -e SCRIPT_NAME=/status.php \ + -e SCRIPT_FILENAME=/var/www/html/status.php \ + "$fcgi_client_image" \ + -bind -connect app:9000 +) + +if ! grep -Eq '"installed"[[:space:]]*:[[:space:]]*true' <<<"$fcgi_response"; then + printf 'FPM responded, but status.php did not report an installed instance.\n' >&2 + printf '%s\n' "$fcgi_response" >&2 + exit 1 +fi + +printf 'FPM accepted a FastCGI request and returned installed Nextcloud status.\n' From 2bfcbba8abe1ec6f8c75cc1687f395c9ef5915ee Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 7 Oct 2026 21:41:36 +0000 Subject: [PATCH 2/6] ci(images): gate publication on app runtime acceptance Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/actions/build-and-scan/action.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/actions/build-and-scan/action.yml b/.github/actions/build-and-scan/action.yml index 8814794..5aee67b 100644 --- a/.github/actions/build-and-scan/action.yml +++ b/.github/actions/build-and-scan/action.yml @@ -7,6 +7,10 @@ inputs: runs: using: composite steps: + - name: ShellCheck app image acceptance test + shell: bash + run: shellcheck tests/test-app-image.sh + - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: @@ -73,3 +77,11 @@ runs: 'app@linux/arm64=scan/app:arm64' \ 'web@linux/amd64=scan/web:amd64' \ 'web@linux/arm64=scan/web:arm64' + + - name: Runtime acceptance app image (linux/amd64) + shell: bash + run: bash tests/test-app-image.sh scan/app:amd64 + + - name: Runtime acceptance app image (linux/arm64) + shell: bash + run: bash tests/test-app-image.sh scan/app:arm64 From 998f2e76ed7624e3ff46808e33f1b71adacdb2a8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 7 Oct 2026 21:42:39 +0000 Subject: [PATCH 3/6] test(images): harden acceptance test cleanup Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/actions/build-and-scan/action.yml | 4 ++-- tests/test-app-image.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/build-and-scan/action.yml b/.github/actions/build-and-scan/action.yml index 5aee67b..fd30117 100644 --- a/.github/actions/build-and-scan/action.yml +++ b/.github/actions/build-and-scan/action.yml @@ -1,5 +1,5 @@ -name: Build and scan runtime images -description: Build both runtime images for amd64 and arm64, then scan each image +name: Build, scan, and validate runtime images +description: Build and scan both runtime images, then run app image acceptance tests inputs: nextcloud_version: description: Nextcloud version passed to the app image build diff --git a/tests/test-app-image.sh b/tests/test-app-image.sh index 916d39e..c36929e 100644 --- a/tests/test-app-image.sh +++ b/tests/test-app-image.sh @@ -66,7 +66,7 @@ diagnostics() { } cleanup() { - local status=$? + local status=$1 set +e if [ "$status" -ne 0 ]; then @@ -88,7 +88,7 @@ cleanup() { exit "$status" } -trap cleanup EXIT +trap 'cleanup $?' EXIT wait_until() { local description=$1 From 3c6523f1c785e49b4f77d8f119ec8ce12484eef2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 7 Oct 2026 21:54:09 +0000 Subject: [PATCH 4/6] test(images): structure runtime acceptance with Bats Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/actions/build-and-scan/action.yml | 18 ++- Makefile | 2 +- docs/images.md | 2 +- tests/app-image.bats | 185 +++++++++++++++++++++ tests/test-app-image.sh | 188 ---------------------- 5 files changed, 200 insertions(+), 195 deletions(-) create mode 100644 tests/app-image.bats delete mode 100644 tests/test-app-image.sh diff --git a/.github/actions/build-and-scan/action.yml b/.github/actions/build-and-scan/action.yml index fd30117..f633326 100644 --- a/.github/actions/build-and-scan/action.yml +++ b/.github/actions/build-and-scan/action.yml @@ -7,9 +7,13 @@ inputs: runs: using: composite steps: - - name: ShellCheck app image acceptance test - shell: bash - run: shellcheck tests/test-app-image.sh + - name: Setup Bats + uses: bats-core/bats-action@77d6fb60505b4d0d1d73e48bd035b55074bbfb43 # 4.0.0 + with: + support-install: false + assert-install: false + detik-install: false + file-install: false - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -80,8 +84,12 @@ runs: - name: Runtime acceptance app image (linux/amd64) shell: bash - run: bash tests/test-app-image.sh scan/app:amd64 + env: + APP_IMAGE: scan/app:amd64 + run: bats tests/app-image.bats - name: Runtime acceptance app image (linux/arm64) shell: bash - run: bash tests/test-app-image.sh scan/app:arm64 + env: + APP_IMAGE: scan/app:arm64 + run: bats tests/app-image.bats diff --git a/Makefile b/Makefile index f5c5968..16e5fcc 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ test-ncdd: test-app-image: @test -n "$(APP_IMAGE)" || { echo 'Usage: make test-app-image APP_IMAGE=' >&2; exit 2; } - bash tests/test-app-image.sh "$(APP_IMAGE)" + APP_IMAGE="$(APP_IMAGE)" bats tests/app-image.bats test-current-app-image: @set -e; \ diff --git a/docs/images.md b/docs/images.md index 5d3ed88..a04aa50 100644 --- a/docs/images.md +++ b/docs/images.md @@ -165,7 +165,7 @@ For a local build of the current app image followed by the same acceptance test: make test-current-app-image ``` -The acceptance test creates an isolated Docker network and PostgreSQL container, starts the image with Nextcloud autoinstall variables, waits for the installation, runs `occ status` and `occ check`, and sends a FastCGI request through the FPM runtime. Test-created resources are removed on success and failure. +The Bats acceptance test creates an isolated Docker network and PostgreSQL container, starts the image with Nextcloud autoinstall variables, waits for the installation, runs `occ status` and `occ check`, and sends a FastCGI request through the FPM runtime. Test-created resources are removed on success and failure. CI must run this test against the exact locally loaded app images produced by the build step. Runtime acceptance is a publication gate alongside vulnerability scanning; a separate deployment-stack test is not required for this contract. diff --git a/tests/app-image.bats b/tests/app-image.bats new file mode 100644 index 0000000..a764f15 --- /dev/null +++ b/tests/app-image.bats @@ -0,0 +1,185 @@ +#!/usr/bin/env bats + +setup() { + : "${APP_IMAGE:?APP_IMAGE must reference an already-built local image}" + + command -v docker >/dev/null 2>&1 || { + echo "Docker is required to run the app image acceptance test." >&2 + return 127 + } + + docker image inspect "$APP_IMAGE" >/dev/null 2>&1 || { + echo "Image not found locally: $APP_IMAGE" >&2 + return 2 + } + + POSTGRES_IMAGE=${NEXTCLOUD_IMAGE_TEST_POSTGRES_IMAGE:-postgres:18-alpine} + TIMEOUT_SECONDS=${NEXTCLOUD_IMAGE_TEST_TIMEOUT:-300} + RUNTIME_USER=${NEXTCLOUD_IMAGE_TEST_RUNTIME_USER:-www-data} + + IMAGE_ARCH=$(docker image inspect --format '{{.Architecture}}' "$APP_IMAGE") + case "$IMAGE_ARCH" in + amd64|arm64) IMAGE_PLATFORM="linux/$IMAGE_ARCH" ;; + *) + echo "Unsupported image architecture: $IMAGE_ARCH" >&2 + return 2 + ;; + esac + + TEST_ID="nextcloud-app-test-$$-${BATS_TEST_NUMBER}-${RANDOM}" + NETWORK_NAME="$TEST_ID" + DB_CONTAINER="${TEST_ID}-db" + APP_CONTAINER="${TEST_ID}-app" + FCGI_CLIENT_IMAGE="nextcloud-app-test-fcgi-client" + + DB_USER=nextcloud + DB_DATABASE=nextcloud + DB_PASSWORD="test-${RANDOM}-${RANDOM}-password" + ADMIN_USER=test_admin + ADMIN_PASSWORD="test-${RANDOM}-${RANDOM}-admin-password" + + NETWORK_CREATED=false + DB_CREATED=false + APP_CREATED=false +} + +teardown() { + if $APP_CREATED; then + docker rm -f "$APP_CONTAINER" >/dev/null 2>&1 || true + fi + if $DB_CREATED; then + docker rm -f "$DB_CONTAINER" >/dev/null 2>&1 || true + fi + if $NETWORK_CREATED; then + docker network rm "$NETWORK_NAME" >/dev/null 2>&1 || true + fi +} + +diagnostics() { + echo + echo "=== app container logs ===" >&2 + if $APP_CREATED; then + docker logs "$APP_CONTAINER" >&2 || true + else + echo "app container was not created" >&2 + fi + + echo + echo "=== postgres container logs ===" >&2 + if $DB_CREATED; then + docker logs "$DB_CONTAINER" >&2 || true + else + echo "postgres container was not created" >&2 + fi +} + +fail_with_diagnostics() { + local message=$1 + diagnostics + echo "$message" >&2 + return 1 +} + +wait_until() { + local description=$1 + shift + + local started now + started=$(date +%s) + + while ! "$@"; do + now=$(date +%s) + if [ $((now - started)) -ge "$TIMEOUT_SECONDS" ]; then + fail_with_diagnostics "Timed out after ${TIMEOUT_SECONDS}s waiting for $description." + return 1 + fi + sleep 2 + done +} + +postgres_ready() { + docker exec "$DB_CONTAINER" pg_isready -U "$DB_USER" -d "$DB_DATABASE" >/dev/null 2>&1 +} + +nextcloud_installed() { + local status + status=$(docker exec -u "$RUNTIME_USER" "$APP_CONTAINER" php occ status --output=json 2>/dev/null) || return 1 + grep -Eq '"installed"[[:space:]]*:[[:space:]]*true' <<<"$status" +} + +ensure_fcgi_client() { + if docker image inspect "$FCGI_CLIENT_IMAGE" >/dev/null 2>&1; then + return + fi + + docker build -q -t "$FCGI_CLIENT_IMAGE" - <<'EOF' >/dev/null +FROM debian:trixie-slim +RUN apt-get update \ + && apt-get install -y --no-install-recommends libfcgi-bin \ + && rm -rf /var/lib/apt/lists/* +ENTRYPOINT ["cgi-fcgi"] +EOF +} + +@test "app image installs Nextcloud and serves it through FPM" { + echo "Testing $APP_IMAGE ($IMAGE_PLATFORM)" + + run docker network create "$NETWORK_NAME" + [ "$status" -eq 0 ] + NETWORK_CREATED=true + + run docker run -d \ + --name "$DB_CONTAINER" \ + --network "$NETWORK_NAME" \ + --network-alias db \ + -e "POSTGRES_USER=$DB_USER" \ + -e "POSTGRES_PASSWORD=$DB_PASSWORD" \ + -e "POSTGRES_DB=$DB_DATABASE" \ + "$POSTGRES_IMAGE" + [ "$status" -eq 0 ] + DB_CREATED=true + + wait_until "PostgreSQL readiness" postgres_ready + + run docker run -d \ + --name "$APP_CONTAINER" \ + --network "$NETWORK_NAME" \ + --network-alias app \ + --platform "$IMAGE_PLATFORM" \ + -e POSTGRES_HOST=db \ + -e "POSTGRES_USER=$DB_USER" \ + -e "POSTGRES_PASSWORD=$DB_PASSWORD" \ + -e "POSTGRES_DB=$DB_DATABASE" \ + -e "NEXTCLOUD_ADMIN_USER=$ADMIN_USER" \ + -e "NEXTCLOUD_ADMIN_PASSWORD=$ADMIN_PASSWORD" \ + -e NEXTCLOUD_TRUSTED_DOMAINS=localhost \ + "$APP_IMAGE" + [ "$status" -eq 0 ] + APP_CREATED=true + + wait_until "Nextcloud installation" nextcloud_installed + + run docker exec -u "$RUNTIME_USER" "$APP_CONTAINER" php occ status --output=json + [ "$status" -eq 0 ] + [[ "$output" =~ "installed"[[:space:]]*:[[:space:]]*true ]] + + run docker exec -u "$RUNTIME_USER" "$APP_CONTAINER" php occ check + if [ "$status" -ne 0 ]; then + fail_with_diagnostics "occ check failed." + fi + + ensure_fcgi_client + + run docker run --rm -i \ + --network "$NETWORK_NAME" \ + -e REQUEST_METHOD=GET \ + -e SCRIPT_NAME=/status.php \ + -e SCRIPT_FILENAME=/var/www/html/status.php \ + "$FCGI_CLIENT_IMAGE" \ + -bind -connect app:9000 + + if [ "$status" -ne 0 ]; then + fail_with_diagnostics "FPM did not accept the FastCGI request." + fi + [[ "$output" =~ "installed"[[:space:]]*:[[:space:]]*true ]] +} diff --git a/tests/test-app-image.sh b/tests/test-app-image.sh deleted file mode 100644 index c36929e..0000000 --- a/tests/test-app-image.sh +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env bash -set -Eeuo pipefail - -if [ "$#" -ne 1 ]; then - printf 'Usage: %s IMAGE\n' "$0" >&2 - exit 2 -fi - -image=$1 -postgres_image=${NEXTCLOUD_IMAGE_TEST_POSTGRES_IMAGE:-postgres:18-alpine} -timeout_seconds=${NEXTCLOUD_IMAGE_TEST_TIMEOUT:-300} -runtime_user=${NEXTCLOUD_IMAGE_TEST_RUNTIME_USER:-www-data} - -if ! command -v docker >/dev/null 2>&1; then - printf 'Docker is required to run the app image acceptance test.\n' >&2 - exit 127 -fi - -if ! docker image inspect "$image" >/dev/null 2>&1; then - printf 'Image not found locally: %s\n' "$image" >&2 - exit 2 -fi - -image_arch=$(docker image inspect --format '{{.Architecture}}' "$image") -case "$image_arch" in - amd64|arm64) - image_platform="linux/$image_arch" - ;; - *) - printf 'Unsupported image architecture: %s\n' "$image_arch" >&2 - exit 2 - ;; -esac - -token="nextcloud-app-test-$$-${RANDOM}" -network_name="$token" -db_container="${token}-db" -app_container="${token}-app" -fcgi_client_image="${token}-fcgi-client" - -network_created=false -db_created=false -app_created=false -fcgi_client_created=false - -db_user=nextcloud -db_database=nextcloud -db_password="test-${RANDOM}-${RANDOM}-password" -admin_user=test_admin -admin_password="test-${RANDOM}-${RANDOM}-admin-password" - -diagnostics() { - printf '\n=== app container logs ===\n' >&2 - if $app_created; then - docker logs "$app_container" >&2 || true - else - printf 'app container was not created\n' >&2 - fi - - printf '\n=== postgres container logs ===\n' >&2 - if $db_created; then - docker logs "$db_container" >&2 || true - else - printf 'postgres container was not created\n' >&2 - fi -} - -cleanup() { - local status=$1 - set +e - - if [ "$status" -ne 0 ]; then - diagnostics - fi - - if $app_created; then - docker rm -f "$app_container" >/dev/null 2>&1 || true - fi - if $db_created; then - docker rm -f "$db_container" >/dev/null 2>&1 || true - fi - if $network_created; then - docker network rm "$network_name" >/dev/null 2>&1 || true - fi - if $fcgi_client_created; then - docker image rm -f "$fcgi_client_image" >/dev/null 2>&1 || true - fi - - exit "$status" -} -trap 'cleanup $?' EXIT - -wait_until() { - local description=$1 - shift - - local started now - started=$(date +%s) - - while true; do - if "$@"; then - return 0 - fi - - now=$(date +%s) - if [ $((now - started)) -ge "$timeout_seconds" ]; then - printf 'Timed out after %ss waiting for %s.\n' "$timeout_seconds" "$description" >&2 - return 1 - fi - - sleep 2 - done -} - -postgres_ready() { - docker exec "$db_container" pg_isready -U "$db_user" -d "$db_database" >/dev/null 2>&1 -} - -nextcloud_installed() { - local status - status=$(docker exec -u "$runtime_user" "$app_container" php occ status --output=json 2>/dev/null) || return 1 - grep -Eq '"installed"[[:space:]]*:[[:space:]]*true' <<<"$status" -} - -printf 'Testing %s (%s)\n' "$image" "$image_platform" - -docker network create "$network_name" >/dev/null -network_created=true - -docker run -d \ - --name "$db_container" \ - --network "$network_name" \ - --network-alias db \ - -e "POSTGRES_USER=$db_user" \ - -e "POSTGRES_PASSWORD=$db_password" \ - -e "POSTGRES_DB=$db_database" \ - "$postgres_image" >/dev/null -db_created=true - -wait_until 'PostgreSQL readiness' postgres_ready - -docker run -d \ - --name "$app_container" \ - --network "$network_name" \ - --network-alias app \ - --platform "$image_platform" \ - -e POSTGRES_HOST=db \ - -e "POSTGRES_USER=$db_user" \ - -e "POSTGRES_PASSWORD=$db_password" \ - -e "POSTGRES_DB=$db_database" \ - -e "NEXTCLOUD_ADMIN_USER=$admin_user" \ - -e "NEXTCLOUD_ADMIN_PASSWORD=$admin_password" \ - -e NEXTCLOUD_TRUSTED_DOMAINS=localhost \ - "$image" >/dev/null -app_created=true - -wait_until 'Nextcloud installation' nextcloud_installed - -printf 'Nextcloud installed successfully.\n' -docker exec -u "$runtime_user" "$app_container" php occ status --output=json -docker exec -u "$runtime_user" "$app_container" php occ check - -docker build -q -t "$fcgi_client_image" - <<'EOF' >/dev/null -FROM debian:trixie-slim -RUN apt-get update \ - && apt-get install -y --no-install-recommends libfcgi-bin \ - && rm -rf /var/lib/apt/lists/* -ENTRYPOINT ["cgi-fcgi"] -EOF -fcgi_client_created=true - -fcgi_response=$( - docker run --rm -i \ - --network "$network_name" \ - -e REQUEST_METHOD=GET \ - -e SCRIPT_NAME=/status.php \ - -e SCRIPT_FILENAME=/var/www/html/status.php \ - "$fcgi_client_image" \ - -bind -connect app:9000 -) - -if ! grep -Eq '"installed"[[:space:]]*:[[:space:]]*true' <<<"$fcgi_response"; then - printf 'FPM responded, but status.php did not report an installed instance.\n' >&2 - printf '%s\n' "$fcgi_response" >&2 - exit 1 -fi - -printf 'FPM accepted a FastCGI request and returned installed Nextcloud status.\n' From 02992da9a3be6bb0e7d056d995c28169293ca9d4 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 7 Oct 2026 22:04:46 +0000 Subject: [PATCH 5/6] fix(tests): match installed status consistently Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/app-image.bats | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/app-image.bats b/tests/app-image.bats index a764f15..dab606b 100644 --- a/tests/app-image.bats +++ b/tests/app-image.bats @@ -160,8 +160,13 @@ EOF wait_until "Nextcloud installation" nextcloud_installed run docker exec -u "$RUNTIME_USER" "$APP_CONTAINER" php occ status --output=json - [ "$status" -eq 0 ] - [[ "$output" =~ "installed"[[:space:]]*:[[:space:]]*true ]] + if [ "$status" -ne 0 ]; then + fail_with_diagnostics "occ status failed." + fi + if ! grep -Eq '"installed"[[:space:]]*:[[:space:]]*true' <<<"$output"; then + printf '%s\n' "$output" >&2 + fail_with_diagnostics "occ status did not report installed: true." + fi run docker exec -u "$RUNTIME_USER" "$APP_CONTAINER" php occ check if [ "$status" -ne 0 ]; then @@ -181,5 +186,8 @@ EOF if [ "$status" -ne 0 ]; then fail_with_diagnostics "FPM did not accept the FastCGI request." fi - [[ "$output" =~ "installed"[[:space:]]*:[[:space:]]*true ]] + if ! grep -Eq '"installed"[[:space:]]*:[[:space:]]*true' <<<"$output"; then + printf '%s\n' "$output" >&2 + fail_with_diagnostics "FPM response did not report installed: true." + fi } From 6a6cbe4bfc0a71d677f784d55a24a1cd7a9d3866 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 7 Oct 2026 22:15:35 +0000 Subject: [PATCH 6/6] fix(tests): wait for FPM readiness on emulated images Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/app-image.bats | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/tests/app-image.bats b/tests/app-image.bats index dab606b..6ba75d2 100644 --- a/tests/app-image.bats +++ b/tests/app-image.bats @@ -121,6 +121,22 @@ ENTRYPOINT ["cgi-fcgi"] EOF } +fcgi_status() { + docker run --rm -i \ + --network "$NETWORK_NAME" \ + -e REQUEST_METHOD=GET \ + -e SCRIPT_NAME=/status.php \ + -e SCRIPT_FILENAME=/var/www/html/status.php \ + "$FCGI_CLIENT_IMAGE" \ + -bind -connect app:9000 2>/dev/null +} + +fpm_ready() { + local response + response=$(fcgi_status) || return 1 + grep -Eq '"installed"[[:space:]]*:[[:space:]]*true' <<<"$response" +} + @test "app image installs Nextcloud and serves it through FPM" { echo "Testing $APP_IMAGE ($IMAGE_PLATFORM)" @@ -175,16 +191,11 @@ EOF ensure_fcgi_client - run docker run --rm -i \ - --network "$NETWORK_NAME" \ - -e REQUEST_METHOD=GET \ - -e SCRIPT_NAME=/status.php \ - -e SCRIPT_FILENAME=/var/www/html/status.php \ - "$FCGI_CLIENT_IMAGE" \ - -bind -connect app:9000 + wait_until "FPM readiness" fpm_ready + run fcgi_status if [ "$status" -ne 0 ]; then - fail_with_diagnostics "FPM did not accept the FastCGI request." + fail_with_diagnostics "FPM did not accept the FastCGI request after becoming ready." fi if ! grep -Eq '"installed"[[:space:]]*:[[:space:]]*true' <<<"$output"; then printf '%s\n' "$output" >&2