diff --git a/tools/test_davx5_sync.sh b/tools/test_davx5_sync.sh
new file mode 100755
index 0000000..f0dcf0e
--- /dev/null
+++ b/tools/test_davx5_sync.sh
@@ -0,0 +1,165 @@
+#!/usr/bin/env bash
+#
+# test_davx5_sync.sh — Protocol-level test of the Xandikos CardDAV server that
+# DAVx5 (Android) syncs against. Exercises exactly what DAVx5 does under the
+# hood: PROPFIND discovery, addressbook-query REPORT, plus auth and TLS hardening.
+#
+# This is the reproducible baseline that does NOT need the Android emulator: it
+# validates the server contract DAVx5 depends on. The emulator-side verification
+# (1065 raw_contacts in the device contacts provider) is documented in the task
+# report but is inherently interactive (UI-driven account setup).
+#
+# Credentials are read from `pass` at runtime — NEVER hardcoded.
+# pass entry: dav/xandikos-enmanuel (first line = password)
+#
+# Usage:
+# ./test_davx5_sync.sh # run all checks
+# ./test_davx5_sync.sh -v # verbose (show curl details)
+#
+# Exit code 0 = all checks passed; non-zero = at least one failure.
+
+set -u
+
+# ---- Config -----------------------------------------------------------------
+BASE="https://dav-eedeb681c4ab89ab8e444ac9.organic-machine.com"
+PRINCIPAL_PATH="/enmanuel/"
+ADDRESSBOOK_PATH="/enmanuel/contacts/addressbook/"
+USER="enmanuel"
+PASS_ENTRY="dav/xandikos-enmanuel"
+
+# Expected number of contacts (vCards) currently served. Allow a tolerance band
+# so the test survives small day-to-day changes without going green on a
+# catastrophic loss (e.g. empty collection).
+EXPECTED_VCARDS=1065
+TOLERANCE=50 # accept EXPECTED +/- TOLERANCE
+
+# A known contact that must round-trip with its TEL and EMAIL intact.
+KNOWN_NAME="Nieves"
+KNOWN_TEL="676 95 90 40"
+KNOWN_EMAIL="nieves@gomezdeseguraabogados.com"
+
+VERBOSE=0
+[ "${1:-}" = "-v" ] && VERBOSE=1
+
+# ---- Helpers ----------------------------------------------------------------
+PASS_COUNT=0
+FAIL_COUNT=0
+RED=$'\e[31m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'; RESET=$'\e[0m'
+
+ok() { echo "${GREEN}PASS${RESET} $1"; PASS_COUNT=$((PASS_COUNT+1)); }
+fail() { echo "${RED}FAIL${RESET} $1"; FAIL_COUNT=$((FAIL_COUNT+1)); }
+info() { [ "$VERBOSE" = 1 ] && echo " $1"; return 0; }
+
+# Read the DAV password from pass into a variable. Never echo it.
+DAV_PASS="$(pass "$PASS_ENTRY" 2>/dev/null | head -1)"
+if [ -z "$DAV_PASS" ]; then
+ echo "${RED}ABORT${RESET}: could not read password from 'pass $PASS_ENTRY'"
+ exit 2
+fi
+
+AB_URL="${BASE}${ADDRESSBOOK_PATH}"
+HTTP_URL="http://${BASE#https://}${ADDRESSBOOK_PATH}"
+
+PROPFIND_BODY=''
+REPORT_BODY=''
+
+echo "=== DAVx5 / Xandikos CardDAV protocol test ==="
+echo "Server: $BASE"
+echo "Collection: $ADDRESSBOOK_PATH"
+echo
+
+# ---- (a) Auth required -------------------------------------------------------
+echo "--- (a) Authentication ---"
+
+code=$(curl -s -o /dev/null -w '%{http_code}' -X PROPFIND -H "Depth: 0" "$AB_URL")
+[ "$code" = "401" ] && ok "no auth -> 401 (got $code)" || fail "no auth -> expected 401, got $code"
+
+code=$(curl -s -o /dev/null -w '%{http_code}' -u "${USER}:definitely-wrong-password" -X PROPFIND -H "Depth: 0" "$AB_URL")
+[ "$code" = "401" ] && ok "bad password -> 401 (got $code)" || fail "bad password -> expected 401, got $code"
+
+code=$(curl -s -o /dev/null -w '%{http_code}' -u "ghost:${DAV_PASS}" -X PROPFIND -H "Depth: 0" "$AB_URL")
+# Xandikos returns 401 for an unknown user too.
+{ [ "$code" = "401" ] || [ "$code" = "403" ] || [ "$code" = "404" ]; } \
+ && ok "unknown user -> $code (rejected)" || fail "unknown user -> expected 401/403/404, got $code"
+
+code=$(curl -s -o /dev/null -w '%{http_code}' -u "${USER}:${DAV_PASS}" -X PROPFIND -H "Depth: 0" "$AB_URL")
+[ "$code" = "207" ] && ok "valid auth -> 207 Multi-Status (got $code)" || fail "valid auth -> expected 207, got $code"
+echo
+
+# ---- (b) TLS -----------------------------------------------------------------
+echo "--- (b) TLS / transport hardening ---"
+
+# Valid cert: curl WITHOUT -k must succeed.
+if curl -sf -o /dev/null -u "${USER}:${DAV_PASS}" -X PROPFIND -H "Depth: 0" "$AB_URL"; then
+ ok "valid TLS chain (no -k needed)"
+else
+ fail "TLS verification failed without -k"
+fi
+
+# Cert issuer should be a real CA (Let's Encrypt here), not self-signed.
+issuer=$(echo | openssl s_client -connect "${BASE#https://}:443" -servername "${BASE#https://}" 2>/dev/null \
+ | openssl x509 -noout -issuer 2>/dev/null)
+info "issuer: $issuer"
+echo "$issuer" | grep -qi "Let's Encrypt" \
+ && ok "cert issued by Let's Encrypt" || fail "unexpected cert issuer: $issuer"
+
+# Plain HTTP must NOT serve data in cleartext: expect a redirect to HTTPS (3xx)
+# or refusal — never a 200 with contact data.
+code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 -X PROPFIND -H "Depth: 0" "$HTTP_URL" 2>/dev/null)
+case "$code" in
+ 301|302|307|308) ok "plain HTTP -> $code redirect to HTTPS (no cleartext)";;
+ 000) ok "plain HTTP refused/unreachable (no cleartext)";;
+ 200) fail "plain HTTP served 200 in CLEARTEXT — server leaks data over http!";;
+ *) ok "plain HTTP -> $code (not 200, no cleartext data)";;
+esac
+echo
+
+# ---- (c) Reception: N contacts via REPORT -----------------------------------
+echo "--- (c) Contact reception (DAVx5's sync mechanism) ---"
+
+# PROPFIND Depth:1 -> count .vcf hrefs
+pf=$(curl -s -u "${USER}:${DAV_PASS}" -X PROPFIND -H "Depth: 1" \
+ -H "Content-Type: application/xml" --data "$PROPFIND_BODY" "$AB_URL")
+hrefs=$(printf '%s' "$pf" | grep -oE '<[a-zA-Z0-9]+:href>[^<]+\.vcf[a-zA-Z0-9]+:href>' | wc -l | tr -d ' ')
+info "PROPFIND .vcf hrefs: $hrefs"
+if [ "$hrefs" -ge $((EXPECTED_VCARDS - TOLERANCE)) ] && [ "$hrefs" -le $((EXPECTED_VCARDS + TOLERANCE)) ]; then
+ ok "PROPFIND Depth:1 lists $hrefs vCards (expected ~$EXPECTED_VCARDS)"
+else
+ fail "PROPFIND Depth:1 listed $hrefs vCards (expected ~$EXPECTED_VCARDS +/-$TOLERANCE)"
+fi
+
+# REPORT addressbook-query -> count BEGIN:VCARD (actual data download)
+rep=$(curl -s -u "${USER}:${DAV_PASS}" -X REPORT -H "Depth: 1" \
+ -H "Content-Type: application/xml" --data "$REPORT_BODY" "$AB_URL")
+vcards=$(printf '%s' "$rep" | grep -c 'BEGIN:VCARD')
+info "REPORT BEGIN:VCARD count: $vcards"
+if [ "$vcards" -ge $((EXPECTED_VCARDS - TOLERANCE)) ] && [ "$vcards" -le $((EXPECTED_VCARDS + TOLERANCE)) ]; then
+ ok "REPORT addressbook-query returns $vcards vCards (expected ~$EXPECTED_VCARDS)"
+else
+ fail "REPORT returned $vcards vCards (expected ~$EXPECTED_VCARDS +/-$TOLERANCE)"
+fi
+echo
+
+# ---- (d) Known contact integrity --------------------------------------------
+echo "--- (d) Known-contact integrity (TEL + EMAIL) ---"
+
+# Extract the single vCard block that matches KNOWN_NAME and check fields.
+block=$(printf '%s' "$rep" | awk -v name="$KNOWN_NAME" '
+ /BEGIN:VCARD/ {buf=""}
+ {buf=buf"\n"$0}
+ /END:VCARD/ { if (buf ~ ("FN:.*"name)) {print buf; exit} }')
+
+if [ -z "$block" ]; then
+ fail "known contact matching '$KNOWN_NAME' not found in REPORT"
+else
+ ok "known contact '$KNOWN_NAME' present in addressbook"
+ printf '%s' "$block" | grep -qF "$KNOWN_TEL" \
+ && ok " -> TEL '$KNOWN_TEL' intact" || fail " -> TEL '$KNOWN_TEL' MISSING"
+ printf '%s' "$block" | grep -qiF "$KNOWN_EMAIL" \
+ && ok " -> EMAIL '$KNOWN_EMAIL' intact" || fail " -> EMAIL '$KNOWN_EMAIL' MISSING"
+fi
+echo
+
+# ---- Summary ----------------------------------------------------------------
+echo "=== Summary: ${GREEN}${PASS_COUNT} passed${RESET}, ${RED}${FAIL_COUNT} failed${RESET} ==="
+[ "$FAIL_COUNT" -eq 0 ] && exit 0 || exit 1