diff --git a/.drone.yml b/.drone.yml index 97969b8..a45a782 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,7 +1,19 @@ --- kind: pipeline -name: deploy to swarm-test.autonomic.zone +name: test and deploy to swarm-test.autonomic.zone + steps: + - name: test + image: alpine:3.21 + environment: + SHELLCHECK_OPTS: -s bash + commands: + - apk add --no-cache bash shellcheck py3-pip py3-yaml curl + - pip3 install --break-system-packages yamllint 2>/dev/null || true + - curl -sSLo /usr/local/bin/gomplate https://github.com/hairyhenderson/gomplate/releases/latest/download/gomplate_linux-amd64 + - chmod +x /usr/local/bin/gomplate + - tests/run.sh + - name: deployment image: git.coopcloud.tech/coop-cloud/stack-ssh-deploy:latest settings: diff --git a/tests/fixtures/composer.env b/tests/fixtures/composer.env new file mode 100644 index 0000000..c112d58 --- /dev/null +++ b/tests/fixtures/composer.env @@ -0,0 +1,2 @@ +DOMAIN=wordpress.example.com +ENABLE_COMPOSER=1 diff --git a/tests/fixtures/cors.env b/tests/fixtures/cors.env new file mode 100644 index 0000000..32c7b05 --- /dev/null +++ b/tests/fixtures/cors.env @@ -0,0 +1,2 @@ +DOMAIN=wordpress.example.com +CORS_ALLOW_ALL=1 diff --git a/tests/fixtures/default.env b/tests/fixtures/default.env new file mode 100644 index 0000000..85785cd --- /dev/null +++ b/tests/fixtures/default.env @@ -0,0 +1 @@ +DOMAIN=wordpress.example.com diff --git a/tests/fixtures/multisite-enable.env b/tests/fixtures/multisite-enable.env new file mode 100644 index 0000000..475cd82 --- /dev/null +++ b/tests/fixtures/multisite-enable.env @@ -0,0 +1,2 @@ +DOMAIN=wordpress.example.com +MULTISITE=enable diff --git a/tests/fixtures/multisite-subdomain.env b/tests/fixtures/multisite-subdomain.env new file mode 100644 index 0000000..7d43e37 --- /dev/null +++ b/tests/fixtures/multisite-subdomain.env @@ -0,0 +1,2 @@ +DOMAIN=wordpress.example.com +MULTISITE=subdomain diff --git a/tests/fixtures/multisite-subfolder.env b/tests/fixtures/multisite-subfolder.env new file mode 100644 index 0000000..bdecd9d --- /dev/null +++ b/tests/fixtures/multisite-subfolder.env @@ -0,0 +1,2 @@ +DOMAIN=wordpress.example.com +MULTISITE=subfolder diff --git a/tests/fixtures/php-extensions.env b/tests/fixtures/php-extensions.env new file mode 100644 index 0000000..0b7fdfb --- /dev/null +++ b/tests/fixtures/php-extensions.env @@ -0,0 +1,2 @@ +DOMAIN=wordpress.example.com +PHP_EXTENSIONS=calendar diff --git a/tests/fixtures/smtp-full.env b/tests/fixtures/smtp-full.env new file mode 100644 index 0000000..e77c13a --- /dev/null +++ b/tests/fixtures/smtp-full.env @@ -0,0 +1,8 @@ +DOMAIN=wordpress.example.com +SMTP_HOST=mail.example.com +SMTP_PORT=587 +MAIL_FROM=wordpress@example.com +SMTP_USER=relay@example.com +SMTP_AUTH=on +SMTP_TLS=on +SMTP_OVERRIDE_FROM=on diff --git a/tests/fixtures/upload-sizes.env b/tests/fixtures/upload-sizes.env new file mode 100644 index 0000000..046435b --- /dev/null +++ b/tests/fixtures/upload-sizes.env @@ -0,0 +1,3 @@ +DOMAIN=wordpress.example.com +UPLOAD_MAX_SIZE=512M +UPLOAD_MAX_TIME=60 diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..5a7a5ea --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -euo pipefail + +ROOT="$(dirname "$(realpath "$0")")" + +echo "===================================" +echo " WordPress Recipe Test Suite" +echo "===================================" +echo "" + +# Collect all compose files +COMPOSE_FILES=() +while IFS= read -r f; do + COMPOSE_FILES+=("$f") +done < <(find "$ROOT/.." -maxdepth 1 -name 'compose*.yml' | sort) + +# Collect all tmpl files with bash content +SHELL_TMPL_FILES=() +while IFS= read -r f; do + SHELL_TMPL_FILES+=("$f") +done < <(find "$ROOT/.." -maxdepth 1 -name '*.tmpl' | sort) + +TMPL_FILES=() +while IFS= read -r f; do + TMPL_FILES+=("$f") +done < <(find "$ROOT/.." -maxdepth 1 -name '*.tmpl' | sort) + +failures=0 + +echo "========================================================================" +"$ROOT/test_yaml.sh" "${COMPOSE_FILES[@]}" || failures=$((failures + 1)) +echo "" + +echo "========================================================================" +"$ROOT/test_shell.sh" "${SHELL_TMPL_FILES[@]}" || failures=$((failures + 1)) +echo "" + +echo "========================================================================" +"$ROOT/test_templates.sh" || failures=$((failures + 1)) +echo "" + +echo "===================================" +if [ "$failures" -eq 0 ]; then + echo " All tests passed!" +else + echo " $failures test suite(s) failed" +fi +echo "===================================" +exit "$failures" diff --git a/tests/test_shell.sh b/tests/test_shell.sh new file mode 100755 index 0000000..65a7844 --- /dev/null +++ b/tests/test_shell.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -euo pipefail + +pass=0 +fail=0 + +EXTRA_SHELLCHECK_OPTS="${SHELLCHECK_OPTS:-}" + +shellcheck_tmpl() { + local tmpl=$1 + # Strip Go template tags ({{ ... }} and {{- ... -}}) into whitespace + # so shellcheck can parse the remaining bash. + local cleaned + cleaned=$(sed 's/{{[-]*[^}]*[-]*}}/true/g' "$tmpl") + + local tmpfile + tmpfile=$(mktemp) + printf '%s\n' "$cleaned" > "$tmpfile" + + if shellcheck $EXTRA_SHELLCHECK_OPTS "$tmpfile"; then + echo " PASS $tmpl" + pass=$((pass + 1)) + else + echo " FAIL $tmpl" + fail=$((fail + 1)) + fi + rm -f "$tmpfile" +} + +echo "=== ShellCheck ===" +for f in "$@"; do + [ -f "$f" ] && shellcheck_tmpl "$f" +done + +echo "---" +echo "Passed: $pass Failed: $fail" +[ "$fail" -eq 0 ] diff --git a/tests/test_templates.sh b/tests/test_templates.sh new file mode 100755 index 0000000..ca9ed61 --- /dev/null +++ b/tests/test_templates.sh @@ -0,0 +1,278 @@ +#!/bin/bash +set -euo pipefail + +ROOT="$(dirname "$(realpath "$0")")/.." +pass=0 +fail=0 + +gomplate="${GOMPLATE_BIN:-gomplate}" + +require_gomplate() { + if ! command -v "$gomplate" &>/dev/null; then + echo "gomplate not found. Install it from https://github.com/hairyhenderson/gomplate" + echo "or set GOMPLATE_BIN env var." + exit 1 + fi +} + +render() { + local tmpl=$1 envfile=$2 + "$gomplate" \ + --template t="$tmpl" \ + --context "_=fmt:%s" \ + --datasource "env=env://?$envfile" \ + -f "$tmpl" 2>/dev/null +} + +# Render by exporting env vars directly (avoids gomplate datasource quirks) +render_via_env() { + local tmpl=$1 envfile=$2 + # shellcheck disable=2046 + env $(xargs < "$envfile") "$gomplate" -f "$tmpl" 2>/dev/null +} + +# --- entrypoint.sh.tmpl tests --- +test_entrypoint_default() { + local envfile=$1 + local output + output=$(render_via_env "entrypoint.sh.tmpl" "$envfile") + + # Should NOT have multisite config + if echo "$output" | grep -q "WP_ALLOW_MULTISITE"; then + echo " FAIL entrypoint default: unexpected WP_ALLOW_MULTISITE" + return 1 + fi + # Should have uploads .htaccess guard + if ! echo "$output" | grep -q "Prevent PHP execution in uploads"; then + echo " FAIL entrypoint default: missing uploads htaccess" + return 1 + fi + # Should use --from=root:root on chown + if ! echo "$output" | grep -q "chown -R --from=root:root"; then + echo " FAIL entrypoint default: missing --from=root:root on chown" + return 1 + fi + # Should have wp-cli download + if ! echo "$output" | grep -q "wp-cli.phar"; then + echo " FAIL entrypoint default: missing wp-cli download" + return 1 + fi + echo " PASS entrypoint default" +} + +test_entrypoint_multisite_enable() { + local envfile=$1 + local output + output=$(render_via_env "entrypoint.sh.tmpl" "$envfile") + + if ! echo "$output" | grep -q "WP_ALLOW_MULTISITE"; then + echo " FAIL entrypoint multisite enable: missing WP_ALLOW_MULTISITE" + return 1 + fi + echo " PASS entrypoint multisite enable" +} + +test_entrypoint_multisite_subdomain() { + local envfile=$1 + local output + output=$(render_via_env "entrypoint.sh.tmpl" "$envfile") + + if ! echo "$output" | grep -q "MULTISITE"; then + echo " FAIL entrypoint multisite subdomain: missing MULTISITE" + return 1 + fi + if ! echo "$output" | grep -q "SUBDOMAIN_INSTALL"; then + echo " FAIL entrypoint multisite subdomain: missing SUBDOMAIN_INSTALL" + return 1 + fi + if ! echo "$output" | grep -q "DOMAIN_CURRENT_SITE"; then + echo " FAIL entrypoint multisite subdomain: missing DOMAIN_CURRENT_SITE" + return 1 + fi + echo " PASS entrypoint multisite subdomain" +} + +test_entrypoint_multisite_subfolder() { + local envfile=$1 + local output + output=$(render_via_env "entrypoint.sh.tmpl" "$envfile") + + if ! echo "$output" | grep -q "MULTISITE"; then + echo " FAIL entrypoint multisite subfolder: missing MULTISITE" + return 1 + fi + if ! echo "$output" | grep -q "SUBDOMAIN_INSTALL"; then + echo " FAIL entrypoint multisite subfolder: missing SUBDOMAIN_INSTALL" + return 1 + fi + echo " PASS entrypoint multisite subfolder" +} + +test_entrypoint_cors() { + local envfile=$1 + local output + output=$(render_via_env "entrypoint.sh.tmpl" "$envfile") + + if ! echo "$output" | grep -q "a2enmod headers"; then + echo " FAIL entrypoint CORS: missing a2enmod headers" + return 1 + fi + if ! echo "$output" | grep -q "Access-Control-Allow-Origin"; then + echo " FAIL entrypoint CORS: missing Access-Control-Allow-Origin" + return 1 + fi + echo " PASS entrypoint CORS" +} + +test_entrypoint_php_extensions() { + local envfile=$1 + local output + output=$(render_via_env "entrypoint.sh.tmpl" "$envfile") + + if ! echo "$output" | grep -q "docker-php-ext-install calendar"; then + echo " FAIL entrypoint PHP extensions: missing docker-php-ext-install calendar" + return 1 + fi + echo " PASS entrypoint PHP extensions" +} + +test_entrypoint_composer() { + local envfile=$1 + local output + output=$(render_via_env "entrypoint.sh.tmpl" "$envfile") + + if ! echo "$output" | grep -q "getcomposer.org"; then + echo " FAIL entrypoint composer: missing composer download" + return 1 + fi + echo " PASS entrypoint composer" +} + +# --- htaccess.tmpl tests --- +test_htaccess_default() { + local envfile=$1 + local output + output=$(render_via_env "htaccess.tmpl" "$envfile") + + if ! echo "$output" | grep -q "RewriteRule . /index.php"; then + echo " FAIL htaccess default: missing standard rewrite rule" + return 1 + fi + if echo "$output" | grep -q "WordPress Multisite"; then + echo " FAIL htaccess default: unexpected multisite section" + return 1 + fi + echo " PASS htaccess default" +} + +test_htaccess_multisite() { + local envfile=$1 mode=$2 + local output + output=$(render_via_env "htaccess.tmpl" "$envfile") + + if ! echo "$output" | grep -q "WordPress Multisite"; then + echo " FAIL htaccess multisite $mode: missing multisite section" + return 1 + fi + if echo "$output" | grep -q "^RewriteRule . /index.php"; then + echo " FAIL htaccess multisite $mode: has non-multisite rewrite rule" + return 1 + fi + echo " PASS htaccess multisite $mode" +} + +# --- uploads.ini.tmpl tests --- +test_uploads_default() { + local output + output=$(render_via_env "uploads.ini.tmpl" "tests/fixtures/default.env") + + if ! echo "$output" | grep -q "upload_max_filesize = 256M"; then + echo " FAIL uploads default: expected 256M upload_max_filesize" + return 1 + fi + if ! echo "$output" | grep -q "max_execution_time = 30"; then + echo " FAIL uploads default: expected 30 max_execution_time" + return 1 + fi + echo " PASS uploads default" +} + +test_uploads_custom() { + local envfile=$1 + local output + output=$(render_via_env "uploads.ini.tmpl" "$envfile") + + if ! echo "$output" | grep -q "upload_max_filesize = 512M"; then + echo " FAIL uploads custom: expected 512M" + return 1 + fi + if ! echo "$output" | grep -q "max_execution_time = 60"; then + echo " FAIL uploads custom: expected 60" + return 1 + fi + echo " PASS uploads custom" +} + +# --- msmtp.conf.tmpl tests --- +test_msmtp_default() { + local output + output=$(render_via_env "msmtp.conf.tmpl" "tests/fixtures/smtp-full.env") + + if ! echo "$output" | grep -q "host mail.example.com"; then + echo " FAIL msmtp default: missing host" + return 1 + fi + if ! echo "$output" | grep -q "from wordpress@example.com"; then + echo " FAIL msmtp default: missing from" + return 1 + fi + if ! echo "$output" | grep -q "auth on"; then + echo " FAIL msmtp default: missing auth" + return 1 + fi + if ! echo "$output" | grep -q "passwordeval"; then + echo " FAIL msmtp default: missing passwordeval" + return 1 + fi + if ! echo "$output" | grep -q "tls on"; then + echo " FAIL msmtp default: missing tls" + return 1 + fi + if ! echo "$output" | grep -q "set_from_header on"; then + echo " FAIL msmtp default: missing set_from_header" + return 1 + fi + echo " PASS msmtp full config" +} + +# --- Run all template tests --- +echo "=== Template Rendering Tests ===" + +cd "$ROOT" + +echo "--- entrypoint.sh.tmpl ---" +require_gomplate + +test_entrypoint_default "tests/fixtures/default.env" && pass=$((pass+1)) || fail=$((fail+1)) +test_entrypoint_multisite_enable "tests/fixtures/multisite-enable.env" && pass=$((pass+1)) || fail=$((fail+1)) +test_entrypoint_multisite_subdomain "tests/fixtures/multisite-subdomain.env" && pass=$((pass+1)) || fail=$((fail+1)) +test_entrypoint_multisite_subfolder "tests/fixtures/multisite-subfolder.env" && pass=$((pass+1)) || fail=$((fail+1)) +test_entrypoint_cors "tests/fixtures/cors.env" && pass=$((pass+1)) || fail=$((fail+1)) +test_entrypoint_php_extensions "tests/fixtures/php-extensions.env" && pass=$((pass+1)) || fail=$((fail+1)) +test_entrypoint_composer "tests/fixtures/composer.env" && pass=$((pass+1)) || fail=$((fail+1)) + +echo "--- htaccess.tmpl ---" +test_htaccess_default "tests/fixtures/default.env" && pass=$((pass+1)) || fail=$((fail+1)) +test_htaccess_multisite "tests/fixtures/multisite-subfolder.env" "subfolder" && pass=$((pass+1)) || fail=$((fail+1)) +test_htaccess_multisite "tests/fixtures/multisite-subdomain.env" "subdomain" && pass=$((pass+1)) || fail=$((fail+1)) + +echo "--- uploads.ini.tmpl ---" +test_uploads_default && pass=$((pass+1)) || fail=$((fail+1)) +test_uploads_custom "tests/fixtures/upload-sizes.env" && pass=$((pass+1)) || fail=$((fail+1)) + +echo "--- msmtp.conf.tmpl ---" +test_msmtp_default && pass=$((pass+1)) || fail=$((fail+1)) + +echo "---" +echo "Passed: $pass Failed: $fail" +[ "$fail" -eq 0 ] diff --git a/tests/test_yaml.sh b/tests/test_yaml.sh new file mode 100755 index 0000000..a722b84 --- /dev/null +++ b/tests/test_yaml.sh @@ -0,0 +1,54 @@ +#!/bin/bash +set -euo pipefail + +pass=0 +fail=0 + +checker="" +if command -v yamllint &>/dev/null; then + checker=yamllint +elif python3 -c "import yaml" 2>/dev/null; then + checker=python +fi + +test_yaml() { + local file=$1 + + case "$checker" in + yamllint) + if yamllint -d "{extends: relaxed, rules: {line-length: disable}}" "$file"; then + echo " PASS $file" + pass=$((pass+1)) + else + echo " FAIL $file" + fail=$((fail+1)) + fi + ;; + python) + if python3 -c " +import yaml, sys +with open('$file') as f: + yaml.safe_load(f) +" 2>/dev/null; then + echo " PASS $file" + pass=$((pass+1)) + else + echo " FAIL $file" + fail=$((fail+1)) + fi + ;; + *) + echo " SKIP $file (no yamllint or PyYAML)" + pass=$((pass+1)) + ;; + esac +} + +echo "=== YAML Validation ===" +for f in "$@"; do + [ -f "$f" ] && test_yaml "$f" +done + +echo "---" +echo "Passed: $pass Failed: $fail" +[ "$fail" -eq 0 ]