#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" ENV_FILE="${ROOT_DIR}/credentials/.env" if [[ -f "${ENV_FILE}" ]]; then # shellcheck disable=SC1090 source "${ENV_FILE}" fi BASE_URL="${BASE_URL:-${WP_URL:-}}" AUTH_USER="${CAL_TEST_USER:-adrians@chezstephens.org.uk}" AUTH_PASS="${CAL_TEST_PASSWORD:-brillig1}" ENABLE_PREFIX_CHECK="${CAL_ENABLE_PREFIX_CHECK:-1}" CAL_URL_SLUG="${CAL_URL_SLUG:-}" CAL_CALDAV_PATH="${CAL_CALDAV_PATH:-}" REMOTE_WP_PATH="${REMOTE_WP_PATH:-}" if [[ -z "${REMOTE_WP_PATH}" ]] && [[ -n "${REMOTE_APP_DIR:-}" ]]; then REMOTE_WP_PATH="$(dirname "$(dirname "${REMOTE_APP_DIR}")")" fi if [[ -z "${REMOTE_WP_PATH}" ]]; then REMOTE_WP_PATH="/var/www/wordpress" fi FAILURES=0 CREATED_EVENT_ID="" CREATED_REC_EVENT_ID="" CREATED_PRIVATE_EVENT_ID="" CREATED_SYNC_DELETE_EVENT_ID="" DETECTED_URL_SLUG="" SSH_OK=0 usage() { cat <<'TXT' Remote API+E2E smoke runner for calendar plugin. Usage: tests/run_remote_tests.sh [--base-url URL] [--user EMAIL] [--password PASS] Env overrides: BASE_URL CAL_TEST_USER CAL_TEST_PASSWORD CAL_ENABLE_PREFIX_CHECK CAL_URL_SLUG CAL_CALDAV_PATH Defaults: BASE_URL -> credentials/.env:WP_URL user/password -> seeded smoke account TXT } while [[ $# -gt 0 ]]; do case "$1" in --base-url) BASE_URL="${2:-}" shift 2 ;; --user) AUTH_USER="${2:-}" shift 2 ;; --password) AUTH_PASS="${2:-}" shift 2 ;; --help|-h) usage exit 0 ;; *) echo "[remote-tests] unknown argument: $1" >&2 usage exit 2 ;; esac done if [[ -z "${BASE_URL}" ]]; then echo "[remote-tests] BASE_URL is required (set WP_URL in credentials/.env or pass --base-url)" >&2 exit 2 fi BASE_URL="${BASE_URL%/}" step() { printf '\n[remote-tests] %s\n' "$1" } record_fail() { FAILURES=$((FAILURES + 1)) echo "[remote-tests] FAIL: $1" >&2 } json_assert() { local file="$1" local expr="$2" python3 - "$file" "$expr" <<'PY' import json, sys path, expr = sys.argv[1], sys.argv[2] data = json.load(open(path)) safe = {"bool": bool, "isinstance": isinstance, "list": list, "dict": dict, "str": str, "int": int} ok = eval(expr, {"__builtins__": safe}, {"data": data}) if not ok: raise SystemExit(1) PY } cleanup() { if [[ -n "${CREATED_EVENT_ID}" ]]; then curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X DELETE \ "${BASE_URL}/wp-json/calendar/v1/events/${CREATED_EVENT_ID}" >/dev/null || true fi if [[ -n "${CREATED_REC_EVENT_ID}" ]]; then curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X DELETE \ "${BASE_URL}/wp-json/calendar/v1/events/${CREATED_REC_EVENT_ID}" >/dev/null || true fi if [[ -n "${CREATED_PRIVATE_EVENT_ID}" ]]; then curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X DELETE \ "${BASE_URL}/wp-json/calendar/v1/events/${CREATED_PRIVATE_EVENT_ID}" >/dev/null || true fi if [[ -n "${CREATED_SYNC_DELETE_EVENT_ID}" ]]; then curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X DELETE \ "${BASE_URL}/wp-json/calendar/v1/events/${CREATED_SYNC_DELETE_EVENT_ID}" >/dev/null || true fi } trap cleanup EXIT step "health" if curl -fsS "${BASE_URL}/wp-json/calendar/v1/health" >/tmp/remote_test_health.json; then if ! json_assert /tmp/remote_test_health.json "data.get('status') == 'ok'"; then record_fail "health payload invalid" fi else record_fail "health endpoint unreachable" fi if [[ -n "${REMOTE_HOST:-}" ]] && [[ -n "${REMOTE_USER:-}" ]] && [[ -n "${REMOTE_SSH_KEY_PATH:-}" ]] && [[ -n "${REMOTE_PORT:-}" ]] && [[ -n "${REMOTE_WP_CLI:-}" ]]; then SSH_OK=1 fi if [[ "${ENABLE_PREFIX_CHECK}" == "1" ]] && [[ "${SSH_OK}" == "1" ]]; then step "table prefix configuration" SSH_KEY_PATH="${REMOTE_SSH_KEY_PATH}" if [[ "${SSH_KEY_PATH}" != /* ]]; then SSH_KEY_PATH="${ROOT_DIR}/${SSH_KEY_PATH}" fi SSH_PREFIX=(ssh -F /dev/null -i "${SSH_KEY_PATH}" -p "${REMOTE_PORT}" -o StrictHostKeyChecking=accept-new "${REMOTE_USER}@${REMOTE_HOST}") if ! "${SSH_PREFIX[@]}" "echo ok" >/dev/null 2>&1; then echo "[remote-tests] WARN: SSH unavailable, skipping table prefix check" SSH_OK=0 else DETECTED_URL_SLUG="$("${SSH_PREFIX[@]}" "cd ${REMOTE_WP_PATH} && ${REMOTE_WP_CLI} option get calendar_plugin_url_slug --allow-root 2>/dev/null || true" | tr -d '\r' | tail -n1)" STEM="$("${SSH_PREFIX[@]}" "cd ${REMOTE_WP_PATH} && ${REMOTE_WP_CLI} option get calendar_plugin_table_stem --allow-root 2>/dev/null || true" | tr -d '\r' | tail -n1)" WP_DB_PREFIX="$("${SSH_PREFIX[@]}" "cd ${REMOTE_WP_PATH} && ${REMOTE_WP_CLI} eval 'global \$wpdb; echo \$wpdb->prefix;' --allow-root 2>/dev/null || true" | tr -d '\r' | tail -n1)" TABLE_LIST="$("${SSH_PREFIX[@]}" "cd ${REMOTE_WP_PATH} && ${REMOTE_WP_CLI} db query \"SHOW TABLES;\" --allow-root --silent --skip-column-names 2>/dev/null || true" | tr -d '\r')" if [[ -n "${STEM}" ]] && [[ -n "${WP_DB_PREFIX}" ]]; then EXPECTED_EVENTS_TABLE="${WP_DB_PREFIX}${STEM}_events" if ! printf '%s\n' "${TABLE_LIST}" | grep -Fxq "${EXPECTED_EVENTS_TABLE}"; then record_fail "table stem '${STEM}' does not match an existing events table (${EXPECTED_EVENTS_TABLE})" fi else if ! printf '%s\n' "${TABLE_LIST}" | grep -Eq '_calendar_.*_events$|_calendar_events$'; then record_fail "no calendar events table detected from SHOW TABLES output" fi fi fi fi step "public read month" if curl -fsS "${BASE_URL}/wp-json/calendar/v1/public/events?view=month&date=2026-04-01" >/tmp/remote_test_public.json; then if ! json_assert /tmp/remote_test_public.json "isinstance(data.get('data'), list)"; then record_fail "public events payload invalid" fi else record_fail "public events endpoint failed" fi step "authenticated /users/me" if curl -fsS -u "${AUTH_USER}:${AUTH_PASS}" "${BASE_URL}/wp-json/calendar/v1/users/me" >/tmp/remote_test_me.json; then if ! json_assert /tmp/remote_test_me.json "bool(data.get('data', {}).get('email'))"; then record_fail "auth user context invalid" fi else record_fail "auth /users/me failed" fi step "event CRUD" EVENT_UID="remote-api-e2e-$(date +%s)@calendar-plugin" CREATE_JSON=$(cat <${PRE_DELETE_TOKEN}1" SYNC_REPORT_HTTP="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X REPORT -H 'Depth: 1' -H 'Content-Type: application/xml; charset=utf-8' \ --data "${SYNC_REPORT_BODY}" -o /tmp/remote_test_sync_report.xml -w '%{http_code}' \ "${CALDAV_COLLECTION_URL}" || true)" if [[ "${SYNC_REPORT_HTTP}" != "207" ]]; then record_fail "sync-collection report failed (http ${SYNC_REPORT_HTTP})" else SYNC_REPORT_HTTP_NOSLASH="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X REPORT -H 'Depth: 1' -H 'Content-Type: application/xml; charset=utf-8' \ --data "${SYNC_REPORT_BODY}" -o /tmp/remote_test_sync_report_noslash.xml -w '%{http_code}' \ "${CALDAV_COLLECTION_URL%/}" || true)" if [[ "${SYNC_REPORT_HTTP_NOSLASH}" != "207" ]]; then record_fail "sync-collection report without trailing slash failed (http ${SYNC_REPORT_HTTP_NOSLASH})" fi if grep -Fq 'HTTP/1.1 404 Not Found' /tmp/remote_test_sync_report.xml; then record_fail "sync-collection returned 404 entries for incremental sync" fi SYNC_TOKEN_1="$(python3 - <<'PY' import re xml=open('/tmp/remote_test_sync_report.xml', encoding='utf-8', errors='ignore').read() m=re.search(r'(.*?)', xml, re.S) print((m.group(1).strip() if m else '')) PY )" if [[ -z "${SYNC_TOKEN_1}" ]]; then record_fail "incremental sync-collection missing sync-token" else SYNC_REPORT_BODY_NOCHANGE="${SYNC_TOKEN_1}1" SYNC_REPORT_NOCHANGE_HTTP="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X REPORT -H 'Depth: 1' -H 'Content-Type: application/xml; charset=utf-8' \ --data "${SYNC_REPORT_BODY_NOCHANGE}" -o /tmp/remote_test_sync_report_nochange.xml -w '%{http_code}' \ "${CALDAV_COLLECTION_URL}" || true)" if [[ "${SYNC_REPORT_NOCHANGE_HTTP}" != "207" ]]; then record_fail "no-change sync-collection report failed (http ${SYNC_REPORT_NOCHANGE_HTTP})" else RESPONSE_COUNT_NOCHANGE="$(python3 - <<'PY' import re xml=open('/tmp/remote_test_sync_report_nochange.xml', encoding='utf-8', errors='ignore').read() print(len(re.findall(r'', xml))) PY )" if [[ "${RESPONSE_COUNT_NOCHANGE}" != "0" ]]; then record_fail "no-change sync-collection should return 0 responses, got ${RESPONSE_COUNT_NOCHANGE}" fi fi fi fi fi fi fi printf '\n[remote-tests] completed with %d failure(s)\n' "${FAILURES}" if [[ "${FAILURES}" -gt 0 ]]; then exit 1 fi echo "[remote-tests] PASS"