#!/usr/bin/env bash set -euo pipefail BASE_URL="${BASE_URL:-http://127.0.0.1:8080}" echo "[smoke] checking api list" curl -fsS -H 'X-WP-User: admin' "$BASE_URL/wp-json/calendar/v1/events" >/dev/null echo "[smoke] checking ics" curl -fsS "$BASE_URL/calendar.ics" | grep -q 'BEGIN:VCALENDAR' echo "[smoke] checking caldav read" curl -fsS -u rw_user@example.test:rwpass123456 "$BASE_URL/caldav/calendars/public/1.ics" >/dev/null echo "[smoke] checking caldav multiget href filtering" TMP_XML="$(mktemp)" cleanup_tmp_xml() { rm -f "$TMP_XML"; } trap cleanup_tmp_xml EXIT cat > "$TMP_XML" <<'XML' /caldav/calendars/public/4.ics XML REPORT_XML="$(curl -fsS -u rw_user@example.test:rwpass123456 -X REPORT \ "$BASE_URL/caldav/calendars/public/" \ -H 'Content-Type: text/xml; charset=utf-8' \ -H 'Depth: 1' \ --data-binary @"$TMP_XML")" echo "$REPORT_XML" | grep -q '/caldav/calendars/public/4.ics' if echo "$REPORT_XML" | grep -q '/caldav/calendars/public/1.ics'; then echo "[smoke] multiget returned unrelated resources" exit 1 fi trap - EXIT cleanup_tmp_xml echo "[smoke] checking recurrence exception delete" curl -fsS -X DELETE -H 'X-WP-User: admin' \ "$BASE_URL/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00" \ -o /dev/null -w '%{http_code}' | grep -q '^204$' echo "[smoke] checking event occurrences endpoint + idempotent delete" CREATE_JSON='{"title":"Smoke Occurrences API","description":"occ-endpoint","location":"","category":"","all_day_event":false,"start_datetime":"2026-04-01T10:00:00+01:00","end_datetime":"2026-04-01T11:00:00+01:00","repeat_type":"daily","repeat_interval":1,"repeat_range_mode":"count","repeat_count":3}' CREATE_RESP="$(curl -fsS -H 'Content-Type: application/json' -H 'X-WP-User: admin' \ -d "$CREATE_JSON" "$BASE_URL/wp-json/calendar/v1/events")" EVENT_ID="$(python3 - <<'PY' "$CREATE_RESP" import json,sys print(json.loads(sys.argv[1])["data"]["id"]) PY )" OCC_RESP="$(curl -fsS -H 'X-WP-User: admin' \ "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences?from=2026-04-01&months=1")" python3 - <<'PY' "$OCC_RESP" import json,sys data=json.loads(sys.argv[1]) days=[x["occurrence_start"][:10] for x in data["data"]] assert len(data["data"]) >= 3 assert "2026-04-02" in days PY curl -fsS -X DELETE -H 'X-WP-User: admin' \ "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences/2026-04-02T10:00:00+01:00" \ -o /dev/null -w '%{http_code}' | grep -q '^204$' curl -fsS -X DELETE -H 'X-WP-User: admin' \ "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences/2026-04-02T10:00:00+01:00" \ -o /dev/null -w '%{http_code}' | grep -q '^204$' OCC_RESP="$(curl -fsS -H 'X-WP-User: admin' \ "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences?from=2026-04-01&months=1")" python3 - <<'PY' "$OCC_RESP" import json,sys days=[x["occurrence_start"][:10] for x in json.loads(sys.argv[1])["data"]] assert "2026-04-02" not in days PY echo "[smoke] checking caldav vtimezone parsing regression" TMP_ICS="$(mktemp)" cleanup_tmp_ics() { rm -f "$TMP_ICS"; } trap cleanup_tmp_ics EXIT cat > "$TMP_ICS" <<'ICS' BEGIN:VCALENDAR PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:Europe/London BEGIN:STANDARD DTSTART:18471201T000000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9 TZOFFSETFROM:+0115 TZOFFSETTO:+0000 TZNAME:GMT END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:smoke-vtimezone-parser-001 SUMMARY:Smoke VTIMEZONE Parse DTSTART;TZID=Europe/London:20260423T150000 DTEND;TZID=Europe/London:20260423T160000 END:VEVENT END:VCALENDAR ICS curl -fsS -u rw_user@example.test:rwpass123456 -X PUT \ -H 'Content-Type: text/calendar; charset=utf-8' \ --data-binary @"$TMP_ICS" \ "$BASE_URL/caldav/calendars/public/smoke-vtimezone.ics" >/dev/null CALDAV_ICS="$(curl -fsS -u rw_user@example.test:rwpass123456 \ "$BASE_URL/caldav/calendars/public/smoke-vtimezone.ics")" echo "$CALDAV_ICS" | grep -q 'DTSTART[^[:space:]]*:20260423T150000' if echo "$CALDAV_ICS" | grep -q 'RRULE'; then echo "[smoke] unexpected RRULE found in VEVENT-only upload" exit 1 fi trap - EXIT cleanup_tmp_ics echo "[smoke] checking caldav monthly nth-weekday roundtrip" TMP_ICS="$(mktemp)" cleanup_tmp_ics() { rm -f "$TMP_ICS"; } trap cleanup_tmp_ics EXIT cat > "$TMP_ICS" <<'ICS' BEGIN:VCALENDAR PRODID:-//Smoke//EN VERSION:2.0 BEGIN:VEVENT UID:smoke-monthly-nth-001 SUMMARY:Smoke Monthly Nth DTSTART;TZID=Europe/London:20260402T150000 DTEND;TZID=Europe/London:20260402T160000 RRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4 END:VEVENT END:VCALENDAR ICS curl -fsS -u rw_user@example.test:rwpass123456 -X PUT \ -H 'Content-Type: text/calendar; charset=utf-8' \ --data-binary @"$TMP_ICS" \ "$BASE_URL/caldav/calendars/public/smoke-monthly-nth.ics" >/dev/null CALDAV_ICS="$(curl -fsS -u rw_user@example.test:rwpass123456 \ "$BASE_URL/caldav/calendars/public/smoke-monthly-nth.ics")" echo "$CALDAV_ICS" | grep -q 'RRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4' trap - EXIT cleanup_tmp_ics echo "[smoke] checking caldav monthly BYDAY ordinal import" TMP_ICS="$(mktemp)" cleanup_tmp_ics() { rm -f "$TMP_ICS"; } trap cleanup_tmp_ics EXIT cat > "$TMP_ICS" <<'ICS' BEGIN:VCALENDAR PRODID:-//Smoke//EN VERSION:2.0 BEGIN:VEVENT UID:smoke-monthly-ordinal-001 SUMMARY:Smoke Monthly Ordinal DTSTART;TZID=Europe/London:20260411T150000 DTEND;TZID=Europe/London:20260411T160000 RRULE:FREQ=MONTHLY;BYDAY=2SA END:VEVENT END:VCALENDAR ICS curl -fsS -u rw_user@example.test:rwpass123456 -X PUT \ -H 'Content-Type: text/calendar; charset=utf-8' \ --data-binary @"$TMP_ICS" \ "$BASE_URL/caldav/calendars/public/smoke-monthly-ordinal.ics" >/dev/null CALDAV_ICS="$(curl -fsS -u rw_user@example.test:rwpass123456 \ "$BASE_URL/caldav/calendars/public/smoke-monthly-ordinal.ics")" echo "$CALDAV_ICS" | grep -q 'RRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2' trap - EXIT cleanup_tmp_ics echo "[smoke] checking caldav monthly last-weekday roundtrip" TMP_ICS="$(mktemp)" cleanup_tmp_ics() { rm -f "$TMP_ICS"; } trap cleanup_tmp_ics EXIT cat > "$TMP_ICS" <<'ICS' BEGIN:VCALENDAR PRODID:-//Smoke//EN VERSION:2.0 BEGIN:VEVENT UID:smoke-monthly-last-001 SUMMARY:Smoke Monthly Last DTSTART;TZID=Europe/London:20260425T150000 DTEND;TZID=Europe/London:20260425T160000 RRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1 END:VEVENT END:VCALENDAR ICS curl -fsS -u rw_user@example.test:rwpass123456 -X PUT \ -H 'Content-Type: text/calendar; charset=utf-8' \ --data-binary @"$TMP_ICS" \ "$BASE_URL/caldav/calendars/public/smoke-monthly-last.ics" >/dev/null CALDAV_ICS="$(curl -fsS -u rw_user@example.test:rwpass123456 \ "$BASE_URL/caldav/calendars/public/smoke-monthly-last.ics")" echo "$CALDAV_ICS" | grep -q 'RRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1' trap - EXIT cleanup_tmp_ics echo "[smoke] checking monthly nth-weekday delete-exception preserves rule" CREATE_JSON='{"title":"Smoke 4th Sunday Anchor","description":"anchor-normalization","location":"","category":"","all_day_event":false,"start_datetime":"2026-05-19T15:00:00+01:00","end_datetime":"2026-05-19T16:00:00+01:00","repeat_type":"monthly","repeat_interval":1,"repeat_nth_mode":"weekday_of_month","repeat_nth_pos":4,"repeat_nth_weekday":0,"repeat_range_mode":"no_end"}' CREATE_RESP="$(curl -fsS -H 'Content-Type: application/json' -H 'X-WP-User: admin' \ -d "$CREATE_JSON" "$BASE_URL/wp-json/calendar/v1/events")" python3 - <<'PY' "$CREATE_RESP" import json,sys data=json.loads(sys.argv[1])["data"] assert data["start_datetime"] == "2026-05-24T15:00:00+01:00" PY EVENT_ID="$(python3 - <<'PY' "$CREATE_RESP" import json,sys print(json.loads(sys.argv[1])["data"]["id"]) PY )" curl -fsS -X DELETE -H 'X-WP-User: admin' \ "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences/2026-05-24T15:00:00+01:00" \ -o /dev/null -w '%{http_code}' | grep -q '^204$' CALDAV_ICS="$(curl -fsS -u rw_user@example.test:rwpass123456 \ "$BASE_URL/caldav/calendars/public/${EVENT_ID}.ics")" echo "$CALDAV_ICS" | grep -q 'RRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4' echo "$CALDAV_ICS" | grep -q 'EXDATE;TZID=Europe/London:20260524T150000' echo "[smoke] checking delete-exception canonical key matching" CREATE_JSON='{"title":"Smoke Daily Exception TZ","description":"tz-key","location":"","category":"","all_day_event":false,"start_datetime":"2026-03-02T10:00:00+00:00","end_datetime":"2026-03-02T11:00:00+00:00","repeat_type":"daily","repeat_interval":1,"repeat_range_mode":"until","repeat_until":"2026-03-19"}' CREATE_RESP="$(curl -fsS -H 'Content-Type: application/json' -H 'X-WP-User: admin' \ -d "$CREATE_JSON" "$BASE_URL/wp-json/calendar/v1/events")" EVENT_ID="$(python3 - <<'PY' "$CREATE_RESP" import json,sys print(json.loads(sys.argv[1])["data"]["id"]) PY )" curl -fsS -X DELETE -H 'X-WP-User: admin' \ "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences/2026-03-11T11:00:00+01:00" \ -o /dev/null -w '%{http_code}' | grep -q '^204$' OCC_RESP="$(curl -fsS -H 'X-WP-User: admin' \ "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences?from=2026-03-01&months=1")" python3 - <<'PY' "$OCC_RESP" import json,sys days=[x["occurrence_start"][:10] for x in json.loads(sys.argv[1])["data"]] assert "2026-03-11" not in days PY curl -fsS -X DELETE -H 'X-WP-User: admin' \ "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences/2026-03-10" \ -o /dev/null -w '%{http_code}' | grep -q '^204$' OCC_RESP="$(curl -fsS -H 'X-WP-User: admin' \ "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences?from=2026-03-01&months=1")" python3 - <<'PY' "$OCC_RESP" import json,sys days=[x["occurrence_start"][:10] for x in json.loads(sys.argv[1])["data"]] assert "2026-03-10" not in days PY echo "[smoke] checking caldav EXDATE import to recurrence exceptions" TMP_ICS="$(mktemp)" cleanup_tmp_ics() { rm -f "$TMP_ICS"; } trap cleanup_tmp_ics EXIT cat > "$TMP_ICS" <<'ICS' BEGIN:VCALENDAR PRODID:-//Smoke//EN VERSION:2.0 BEGIN:VEVENT UID:smoke-exdate-import-001 SUMMARY:Smoke EXDATE Import DTSTART;TZID=Europe/London:20260302T100000 DTEND;TZID=Europe/London:20260302T110000 RRULE:FREQ=DAILY;UNTIL=20260319T235959 EXDATE;TZID=Europe/London:20260310T100000,20260311T100000 END:VEVENT END:VCALENDAR ICS curl -fsS -u rw_user@example.test:rwpass123456 -X PUT \ -H 'Content-Type: text/calendar; charset=utf-8' \ --data-binary @"$TMP_ICS" \ "$BASE_URL/caldav/calendars/public/smoke-exdate-import.ics" >/dev/null EVENT_ID="$(curl -fsS -H 'X-WP-User: admin' "$BASE_URL/wp-json/calendar/v1/events" | python3 -c "import json,sys; data=json.load(sys.stdin)['data']; print([e for e in data if e.get('uid')=='smoke-exdate-import-001'][0]['id'])")" OCC_RESP="$(curl -fsS -H 'X-WP-User: admin' \ "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences?from=2026-03-01&months=1")" python3 - <<'PY' "$OCC_RESP" import json,sys days=[x["occurrence_start"][:10] for x in json.loads(sys.argv[1])["data"]] assert "2026-03-10" not in days assert "2026-03-11" not in days PY trap - EXIT cleanup_tmp_ics echo "[smoke] checking caldav cancelled-occurrence component handling" TMP_ICS="$(mktemp)" cleanup_tmp_ics() { rm -f "$TMP_ICS"; } trap cleanup_tmp_ics EXIT cat > "$TMP_ICS" <<'ICS' BEGIN:VCALENDAR PRODID:-//Smoke//EN VERSION:2.0 BEGIN:VEVENT UID:smoke-cancelled-occurrence-001 SUMMARY:Smoke Cancelled Occurrence DTSTART;TZID=Europe/London:20260408T123000 DTEND;TZID=Europe/London:20260408T133000 RRULE:FREQ=WEEKLY END:VEVENT BEGIN:VEVENT UID:smoke-cancelled-occurrence-001 RECURRENCE-ID;TZID=Europe/London:20260506T123000 DTSTART;TZID=Europe/London:20260506T123000 DTEND;TZID=Europe/London:20260506T133000 STATUS:CANCELLED END:VEVENT END:VCALENDAR ICS curl -fsS -u rw_user@example.test:rwpass123456 -X PUT \ -H 'Content-Type: text/calendar; charset=utf-8' \ --data-binary @"$TMP_ICS" \ "$BASE_URL/caldav/calendars/public/smoke-cancelled-occurrence.ics" >/dev/null CALDAV_ICS="$(curl -fsS -u rw_user@example.test:rwpass123456 \ "$BASE_URL/caldav/calendars/public/smoke-cancelled-occurrence.ics")" echo "$CALDAV_ICS" | grep -q 'RRULE:FREQ=WEEKLY' echo "$CALDAV_ICS" | grep -q 'EXDATE;TZID=Europe/London:20260506T123000' EVENT_ID="$(curl -fsS -H 'X-WP-User: admin' "$BASE_URL/wp-json/calendar/v1/events" | python3 -c "import json,sys; data=json.load(sys.stdin)['data']; print([e for e in data if e.get('uid')=='smoke-cancelled-occurrence-001'][0]['id'])")" OCC_RESP="$(curl -fsS -H 'X-WP-User: admin' \ "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences?from=2026-05-01&months=1")" python3 - <<'PY' "$OCC_RESP" import json,sys days=[x["occurrence_start"][:10] for x in json.loads(sys.argv[1])["data"]] assert "2026-05-06" not in days assert "2026-05-13" in days PY trap - EXIT cleanup_tmp_ics echo "[smoke] checking future-only default" curl -fsS "$BASE_URL/calendar" | grep -q 'id="futureOnly" type="checkbox" checked' echo "[smoke] all checks passed"