#!/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