calendar-plugin/tests/run_remote_tests.sh

613 lines
24 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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=""
CREATED_SIDEBAR_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
if [[ -n "${CREATED_SIDEBAR_EVENT_ID}" ]]; then
curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X DELETE \
"${BASE_URL}/wp-json/calendar/v1/events/${CREATED_SIDEBAR_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 <<JSON
{"uid":"${EVENT_UID}","title":"Remote API E2E","description":"created by remote runner","location":"Remote","category":"QA","start_datetime":"2026-04-20T10:00:00+01:00","end_datetime":"2026-04-20T11:00:00+01:00","repeat_type":"none","repeat_interval":1,"repeat_range_mode":"none"}
JSON
)
CREATE_HTTP="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -H 'Content-Type: application/json' \
-d "${CREATE_JSON}" -o /tmp/remote_test_create.json -w '%{http_code}' \
"${BASE_URL}/wp-json/calendar/v1/events" || true)"
if [[ "${CREATE_HTTP}" != "200" ]]; then
record_fail "event create failed (http ${CREATE_HTTP})"
else
CREATED_EVENT_ID="$(python3 - <<'PY'
import json
p=json.load(open('/tmp/remote_test_create.json'))
print(p.get('data', {}).get('id', ''))
PY
)"
if [[ -z "${CREATED_EVENT_ID}" ]]; then
record_fail "event create returned no id"
else
PATCH_HTTP="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X PATCH -H 'Content-Type: application/json' \
-d '{"title":"Remote API E2E Updated"}' -o /tmp/remote_test_patch.json -w '%{http_code}' \
"${BASE_URL}/wp-json/calendar/v1/events/${CREATED_EVENT_ID}" || true)"
if [[ "${PATCH_HTTP}" != "200" ]]; then
record_fail "event update failed (http ${PATCH_HTTP})"
elif ! json_assert /tmp/remote_test_patch.json "data.get('data', {}).get('title') == 'Remote API E2E Updated'"; then
record_fail "event update payload invalid"
fi
fi
fi
step "recurrence exception behavior"
REC_UID="remote-rec-exc-$(date +%s)@calendar-plugin"
REC_JSON=$(cat <<JSON
{"uid":"${REC_UID}","title":"Remote Recurrence","description":"exception test","location":"Remote","category":"QA","start_datetime":"2026-04-02T10:00:00+01:00","end_datetime":"2026-04-02T11:00:00+01:00","repeat_type":"daily","repeat_interval":1,"repeat_range_mode":"until","repeat_until":"2026-04-10"}
JSON
)
REC_HTTP="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -H 'Content-Type: application/json' \
-d "${REC_JSON}" -o /tmp/remote_test_rec_create.json -w '%{http_code}' \
"${BASE_URL}/wp-json/calendar/v1/events" || true)"
if [[ "${REC_HTTP}" != "200" ]]; then
record_fail "recurring create failed (http ${REC_HTTP})"
else
CREATED_REC_EVENT_ID="$(python3 - <<'PY'
import json
p=json.load(open('/tmp/remote_test_rec_create.json'))
print(p.get('data', {}).get('id', ''))
PY
)"
if [[ -z "${CREATED_REC_EVENT_ID}" ]]; then
record_fail "recurring create returned no id"
else
PRE_LIST_HTTP="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" \
-o /tmp/remote_test_occ_pre_list.json -w '%{http_code}' \
"${BASE_URL}/wp-json/calendar/v1/events/${CREATED_REC_EVENT_ID}/occurrences?from=2026-04-01&months=1" || true)"
if [[ "${PRE_LIST_HTTP}" != "200" ]]; then
record_fail "list occurrences before delete failed (http ${PRE_LIST_HTTP})"
OCC_KEY=""
else
OCC_KEY="$(python3 - <<'PY'
import json
p=json.load(open('/tmp/remote_test_occ_pre_list.json'))
items=p.get('data', [])
print(items[2].get('occurrence_start', '') if len(items) >= 3 else (items[0].get('occurrence_start', '') if items else ''))
PY
)"
fi
if [[ -z "${OCC_KEY}" ]]; then
record_fail "no occurrence key available for delete test"
fi
OCC_KEY_ENC="$(python3 - "${OCC_KEY}" <<'PY'
import sys, urllib.parse
print(urllib.parse.quote(sys.argv[1], safe=''))
PY
)"
OCC_HTTP="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X DELETE \
-o /tmp/remote_test_occ_del.json -w '%{http_code}' \
"${BASE_URL}/wp-json/calendar/v1/events/${CREATED_REC_EVENT_ID}/occurrences/${OCC_KEY_ENC}" || true)"
if [[ "${OCC_HTTP}" != "200" ]]; then
record_fail "delete single occurrence failed (http ${OCC_HTTP})"
else
LIST_HTTP="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" \
-o /tmp/remote_test_occ_list.json -w '%{http_code}' \
"${BASE_URL}/wp-json/calendar/v1/events/${CREATED_REC_EVENT_ID}/occurrences?from=2026-04-01&months=1" || true)"
if [[ "${LIST_HTTP}" != "200" ]]; then
record_fail "list occurrences after exception failed (http ${LIST_HTTP})"
else
if ! python3 - "${OCC_KEY}" <<'PY'
import json
import sys
deleted_key=sys.argv[1][:10]
p=json.load(open('/tmp/remote_test_occ_list.json'))
starts=[x.get('occurrence_start','')[:10] for x in p.get('data',[])]
assert deleted_key not in starts
PY
then
record_fail "occurrence exception not preserved"
fi
fi
fi
fi
fi
step "public privacy redaction"
PRIV_UID="remote-private-redact-$(date +%s)@calendar-plugin"
PRIV_JSON=$(cat <<JSON
{"uid":"${PRIV_UID}","title":"Remote Private Leak Check","visibility":"private","description":"SHOULD_NOT_LEAK","location":"SECRET_ROOM","category":"SECRET_CAT","start_datetime":"2026-04-22T10:00:00+01:00","end_datetime":"2026-04-22T11:00:00+01:00","repeat_type":"none","repeat_interval":1,"repeat_range_mode":"none"}
JSON
)
PRIV_HTTP="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -H 'Content-Type: application/json' \
-d "${PRIV_JSON}" -o /tmp/remote_test_private_create.json -w '%{http_code}' \
"${BASE_URL}/wp-json/calendar/v1/events" || true)"
if [[ "${PRIV_HTTP}" != "200" ]]; then
record_fail "private event create failed (http ${PRIV_HTTP})"
else
CREATED_PRIVATE_UID="$(python3 - <<'PY'
import json
p=json.load(open('/tmp/remote_test_private_create.json'))
print(p.get('data', {}).get('uid', ''))
PY
)"
CREATED_PRIVATE_EVENT_ID="$(python3 - <<'PY'
import json
p=json.load(open('/tmp/remote_test_private_create.json'))
print(p.get('data', {}).get('id', ''))
PY
)"
PUB_HTTP="$(curl -sS \
-o /tmp/remote_test_private_public.json -w '%{http_code}' \
"${BASE_URL}/wp-json/calendar/v1/public/events?view=month&date=2026-04-01" || true)"
if [[ "${PUB_HTTP}" != "200" ]]; then
record_fail "public events read for privacy check failed (http ${PUB_HTTP})"
else
if ! python3 - "${CREATED_PRIVATE_UID}" <<'PY'
import json
import sys
uid=sys.argv[1]
payload=json.load(open('/tmp/remote_test_private_public.json'))
items=payload.get('data', [])
target=None
for item in items:
if item.get('uid') == uid:
target=item
break
assert target is not None
assert target.get('title') == 'Private Event'
assert (target.get('description') or '') == ''
assert (target.get('location') or '') == ''
assert (target.get('category') or '') == ''
PY
then
record_fail "private event leaked in public payload"
fi
fi
fi
step "sidebar timezone rendering (Europe/London)"
SIDEBAR_UID="remote-sidebar-tz-$(date +%s)@calendar-plugin"
SIDEBAR_TITLE="Remote Sidebar TZ Check"
SIDEBAR_JSON=$(cat <<JSON
{"uid":"${SIDEBAR_UID}","title":"${SIDEBAR_TITLE}","description":"timezone regression","location":"Remote","category":"QA","start_datetime":"2026-04-10T09:00:00+00:00","end_datetime":"2026-04-10T10:00:00+00:00","repeat_type":"none","repeat_interval":1,"repeat_range_mode":"none"}
JSON
)
SIDEBAR_CREATE_HTTP="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -H 'Content-Type: application/json' \
-d "${SIDEBAR_JSON}" -o /tmp/remote_test_sidebar_create.json -w '%{http_code}' \
"${BASE_URL}/wp-json/calendar/v1/events" || true)"
if [[ "${SIDEBAR_CREATE_HTTP}" != "200" ]]; then
record_fail "sidebar timezone setup create failed (http ${SIDEBAR_CREATE_HTTP})"
else
CREATED_SIDEBAR_EVENT_ID="$(python3 - <<'PY'
import json
p=json.load(open('/tmp/remote_test_sidebar_create.json'))
print(p.get('data', {}).get('id', ''))
PY
)"
if [[ -z "${CREATED_SIDEBAR_EVENT_ID}" ]]; then
record_fail "sidebar timezone setup returned no id"
elif [[ "${SSH_OK}" != "1" ]]; then
echo "[remote-tests] WARN: SSH unavailable, skipping sidebar shortcode rendering check"
else
if ! "${SSH_PREFIX[@]}" "cd ${REMOTE_WP_PATH} && ${REMOTE_WP_CLI} eval 'echo do_shortcode(\"[calendar_sidebar_upcoming]\");' --allow-root" >/tmp/remote_test_sidebar_render.html 2>/tmp/remote_test_sidebar_render.err; then
record_fail "sidebar shortcode render check failed"
else
if ! python3 - <<'PY'
import re
html=open('/tmp/remote_test_sidebar_render.html', encoding='utf-8', errors='ignore').read()
assert 'Remote Sidebar TZ Check' in html
assert re.search(r'10(?:\.00)?(?:|-|&ndash;)11am', html), html
PY
then
record_fail "sidebar shortcode did not render expected Europe/London time range (expected 1011am for 09:00Z input)"
fi
fi
fi
fi
step "caldav discovery compatibility"
CALDAV_ROOT_URL=""
if [[ -n "${CAL_CALDAV_PATH}" ]]; then
if [[ "${CAL_CALDAV_PATH}" == http://* || "${CAL_CALDAV_PATH}" == https://* ]]; then
CALDAV_ROOT_URL="${CAL_CALDAV_PATH%/}/"
else
CALDAV_ROOT_URL="${BASE_URL}/${CAL_CALDAV_PATH#/}"
CALDAV_ROOT_URL="${CALDAV_ROOT_URL%/}/"
fi
else
URL_SLUG="${CAL_URL_SLUG}"
if [[ -z "${URL_SLUG}" ]]; then
URL_SLUG="${DETECTED_URL_SLUG}"
fi
if [[ -n "${URL_SLUG}" ]]; then
CALDAV_ROOT_URL="${BASE_URL}/${URL_SLUG#/}/caldav/"
else
CALDAV_ROOT_URL="${BASE_URL}/caldav/"
fi
fi
CALDAV_UNAUTH="$(curl -s -o /tmp/remote_test_caldav_unauth.txt -w '%{http_code}' "${CALDAV_ROOT_URL}" || true)"
if [[ "${CALDAV_UNAUTH}" == "404" ]]; then
ALT_CANDIDATES=("${BASE_URL}/caldav/")
if [[ -n "${DETECTED_URL_SLUG}" ]]; then
ALT_CANDIDATES+=("${BASE_URL}/${DETECTED_URL_SLUG#/}/caldav/")
fi
if [[ -n "${CAL_URL_SLUG}" ]]; then
ALT_CANDIDATES+=("${BASE_URL}/${CAL_URL_SLUG#/}/caldav/")
fi
for candidate in "${ALT_CANDIDATES[@]}"; do
code="$(curl -s -o /tmp/remote_test_caldav_unauth.txt -w '%{http_code}' "${candidate}" || true)"
if [[ "${code}" != "404" ]]; then
CALDAV_ROOT_URL="${candidate}"
CALDAV_UNAUTH="${code}"
break
fi
done
fi
if [[ "${CALDAV_UNAUTH}" != "401" ]]; then
record_fail "caldav unauth challenge expected 401 got ${CALDAV_UNAUTH} (${CALDAV_ROOT_URL})"
fi
CALDAV_PROP="$(curl -s -u "${AUTH_USER}:${AUTH_PASS}" -X PROPFIND -H 'Depth: 0' \
-o /tmp/remote_test_caldav_propfind.xml -w '%{http_code}' \
"${CALDAV_ROOT_URL}" || true)"
if [[ "${CALDAV_PROP}" != "207" ]]; then
record_fail "caldav root PROPFIND expected 207 got ${CALDAV_PROP} (${CALDAV_ROOT_URL})"
elif ! grep -Eqi 'calendar-home-set|current-user-principal' /tmp/remote_test_caldav_propfind.xml; then
record_fail "caldav PROPFIND missing expected discovery properties"
fi
CALDAV_COLLECTION_URL="${CALDAV_ROOT_URL%/}/calendars/public/"
CALDAV_COLLECTION_GET="$(curl -s -u "${AUTH_USER}:${AUTH_PASS}" -X GET \
-o /tmp/remote_test_caldav_collection_get.txt -w '%{http_code}' \
"${CALDAV_COLLECTION_URL}" || true)"
if [[ "${CALDAV_COLLECTION_GET}" != "200" ]]; then
record_fail "caldav collection GET expected 200 got ${CALDAV_COLLECTION_GET} (${CALDAV_COLLECTION_URL})"
fi
step "caldav DESCRIPTION ALTREP parsing regression"
ALTREP_UID="remote-caldav-altrep-$(date +%s)@calendar-plugin"
ALTREP_RESOURCE="remote-altrep-$(date +%s).ics"
TMP_ALTREP_ICS="$(mktemp)"
cat > "${TMP_ALTREP_ICS}" <<ICS
BEGIN:VCALENDAR
PRODID:-//Remote Regression//EN
VERSION:2.0
BEGIN:VEVENT
UID:${ALTREP_UID}
SUMMARY:Remote CalDAV ALTREP Regression
DTSTART;TZID=Europe/London:20260428T120000
DTEND;TZID=Europe/London:20260428T130000
DESCRIPTION;ALTREP="data:text/html,test%C2%A0":test
END:VEVENT
END:VCALENDAR
ICS
ALTREP_PUT_HTTP="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X PUT \
-H 'Content-Type: text/calendar; charset=utf-8' \
--data-binary @"${TMP_ALTREP_ICS}" \
-o /tmp/remote_test_altrep_put.out -w '%{http_code}' \
"${CALDAV_COLLECTION_URL}${ALTREP_RESOURCE}" || true)"
rm -f "${TMP_ALTREP_ICS}"
if [[ "${ALTREP_PUT_HTTP}" != "201" && "${ALTREP_PUT_HTTP}" != "204" ]]; then
record_fail "caldav ALTREP PUT failed (http ${ALTREP_PUT_HTTP})"
else
ALTREP_EVENT_ID=""
ALTREP_LIST_HTTP="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -o /tmp/remote_test_altrep_list.json -w '%{http_code}' \
"${BASE_URL}/wp-json/calendar/v1/events" || true)"
if [[ "${ALTREP_LIST_HTTP}" != "200" ]]; then
record_fail "caldav ALTREP event list fetch failed (http ${ALTREP_LIST_HTTP})"
else
ALTREP_EVENT_ID="$(python3 - "${ALTREP_UID}" <<'PY'
import json,sys
uid=sys.argv[1]
payload=json.load(open('/tmp/remote_test_altrep_list.json'))
for event in payload.get("data", []):
if event.get("uid") == uid:
print(event.get("id", ""))
break
else:
print("")
PY
)"
fi
if [[ -z "${ALTREP_EVENT_ID}" ]]; then
record_fail "caldav ALTREP event not found via API list"
else
if ! curl -fsS -u "${AUTH_USER}:${AUTH_PASS}" "${BASE_URL}/wp-json/calendar/v1/events/${ALTREP_EVENT_ID}" >/tmp/remote_test_altrep_event.json; then
record_fail "caldav ALTREP event fetch failed"
elif ! json_assert /tmp/remote_test_altrep_event.json "data.get('data', {}).get('description') == 'test'"; then
record_fail "caldav ALTREP description parse regression (expected 'test')"
fi
curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X DELETE \
"${BASE_URL}/wp-json/calendar/v1/events/${ALTREP_EVENT_ID}" >/dev/null || true
fi
fi
step "caldav sync-collection stability"
SYNC_UID="remote-caldav-sync-del-$(date +%s)@calendar-plugin"
SYNC_CREATE_JSON=$(cat <<JSON
{"uid":"${SYNC_UID}","title":"Remote CalDAV Sync Delete","description":"sync tombstone regression","location":"Remote","category":"QA","start_datetime":"2026-04-25T10:00:00+01:00","end_datetime":"2026-04-25T11:00:00+01:00","repeat_type":"none","repeat_interval":1,"repeat_range_mode":"none"}
JSON
)
SYNC_CREATE_HTTP="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -H 'Content-Type: application/json' \
-d "${SYNC_CREATE_JSON}" -o /tmp/remote_test_sync_create.json -w '%{http_code}' \
"${BASE_URL}/wp-json/calendar/v1/events" || true)"
if [[ "${SYNC_CREATE_HTTP}" != "200" ]]; then
record_fail "sync tombstone setup create failed (http ${SYNC_CREATE_HTTP})"
else
CREATED_SYNC_DELETE_EVENT_ID="$(python3 - <<'PY'
import json
p=json.load(open('/tmp/remote_test_sync_create.json'))
print(p.get('data', {}).get('id', ''))
PY
)"
SYNC_RESOURCE="$(python3 - <<'PY'
import json
p=json.load(open('/tmp/remote_test_sync_create.json'))
print(p.get('data', {}).get('caldav_resource', ''))
PY
)"
if [[ -z "${CREATED_SYNC_DELETE_EVENT_ID}" || -z "${SYNC_RESOURCE}" ]]; then
record_fail "sync tombstone setup missing id/resource"
else
SYNC_DELETE_HTTP="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X DELETE \
-o /tmp/remote_test_sync_delete.json -w '%{http_code}' \
"${BASE_URL}/wp-json/calendar/v1/events/${CREATED_SYNC_DELETE_EVENT_ID}" || true)"
if [[ "${SYNC_DELETE_HTTP}" != "200" ]]; then
record_fail "sync stability delete setup failed (http ${SYNC_DELETE_HTTP})"
else
CREATED_SYNC_DELETE_EVENT_ID=""
SYNC_REPORT_BODY_INITIAL='<?xml version="1.0" encoding="utf-8"?><D:sync-collection xmlns:D="DAV:"><D:sync-token/><D:sync-level>1</D:sync-level><D:prop><D:getetag/></D:prop></D:sync-collection>'
SYNC_REPORT_INITIAL_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_INITIAL}" -o /tmp/remote_test_sync_report_initial.xml -w '%{http_code}' \
"${CALDAV_COLLECTION_URL}" || true)"
if [[ "${SYNC_REPORT_INITIAL_HTTP}" != "207" ]]; then
record_fail "initial sync-collection report failed (http ${SYNC_REPORT_INITIAL_HTTP})"
fi
PRE_DELETE_TOKEN="$(python3 - <<'PY'
import re
xml=open('/tmp/remote_test_sync_report_initial.xml', encoding='utf-8', errors='ignore').read()
m=re.search(r'<D:sync-token>(.*?)</D:sync-token>', xml, re.S)
print((m.group(1).strip() if m else ''))
PY
)"
if [[ -z "${PRE_DELETE_TOKEN}" ]]; then
record_fail "initial sync-collection missing sync-token"
fi
SYNC_REPORT_BODY="<?xml version=\"1.0\" encoding=\"utf-8\"?><D:sync-collection xmlns:D=\"DAV:\"><D:sync-token>${PRE_DELETE_TOKEN}</D:sync-token><D:sync-level>1</D:sync-level><D:prop><D:getetag/></D:prop></D:sync-collection>"
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'<D:sync-token>(.*?)</D:sync-token>', 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="<?xml version=\"1.0\" encoding=\"utf-8\"?><D:sync-collection xmlns:D=\"DAV:\"><D:sync-token>${SYNC_TOKEN_1}</D:sync-token><D:sync-level>1</D:sync-level><D:prop><D:getetag/></D:prop></D:sync-collection>"
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'<D:response>', 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"