613 lines
24 KiB
Bash
Executable File
613 lines
24 KiB
Bash
Executable File
#!/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)?(?:–|-|–)11am', html), html
|
||
PY
|
||
then
|
||
record_fail "sidebar shortcode did not render expected Europe/London time range (expected 10–11am 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"
|