Beide Eigenschaften fragt jeder verbreitete Client ab, und beide kennt
go-webdav v0.7.0 nicht:
getctag die billige Frage "hat sich in diesem Kalender etwas
geaendert?". Ohne sie laedt ein Client bei jedem
Abgleich die Liste aller Termine herunter, um sie zu
vergleichen -- richtig, aber teuer an Daten und Akku.
calendar-color die Farbe des Kalenders. Ohne sie zeigt das Telefon
ihn in seiner eigenen Vorgabefarbe, und die Farbgebung
dort hat mit der in der Cloud nichts mehr zu tun.
Die Bibliothek beantwortet einen festen Satz Eigenschaften und bietet
keinen Haken, eigene dazuzulegen (server.go, propFindCalendar). Deshalb
faengt der Handler PROPFIND jetzt ab: Er nimmt die beiden aus der
Anfrage heraus, bevor go-webdav sie zu sehen bekommt, und haengt sie der
Antwort als eigenen propstat-Block wieder an.
Das Herausnehmen ist der Punkt, an dem es sonst schiefgeht: Ohne das
meldet go-webdav sie als "nicht gefunden", und dieselbe Eigenschaft
stuende zweimal in derselben Antwort -- einmal mit 404, einmal mit 200.
Was ein Client daraus macht, ist Gluecksache.
Zwei Fallen dabei, beide abgedeckt: Fragt ein Client *nur* nach diesen
Eigenschaften, bliebe ein leeres <prop> uebrig, das go-webdav abweist --
dann schiebt Aether resourcetype als Platzhalter unter. Und die Farbe
geht als #RRGGBBAA hinaus, mit Alpha-Anteil, weil manche Clients sie
sonst gar nicht anzeigen.
Der Neuaufbau des Anfragerumpfs ist verlustfrei, weil in einem PROPFIND
ausschliesslich leere Elemente stehen: Name und Namensraum sind alles,
was eine Eigenschaft dort ausmacht.
Unit-Tests fuer die XML-Arbeit (propfind_test.go), CalDAV-Selbsttest von
39 auf 45 Pruefungen: beide Eigenschaften mit 200, Apple-Schreibweise,
ctag aendert sich bei Aenderungen (ein festgefrorenes ctag ist schlimmer
als gar keins), Anfrage nur nach ihnen, Farben in der Kalenderliste, und
ohne Nachfrage kommt auch nichts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
368 lines
17 KiB
Python
368 lines
17 KiB
Python
#!/usr/bin/env python3
|
||
"""Selbsttest des CalDAV-Wegs gegen eine laufende Instanz.
|
||
|
||
python3 scripts/caldav-selbsttest.py [BASIS-URL] [BENUTZER] [PASSWORT]
|
||
|
||
Der Schwestertest kalender-selbsttest.py prüft die JSON-Schnittstelle, also
|
||
das, was die Web-Oberfläche benutzt. Dieser hier geht den Weg, den ein echter
|
||
Client geht: DAVx5, Thunderbird, Apple Kalender. Beide Wege führen auf
|
||
dieselben Daten, aber über völlig verschiedenen Code – ein Fehler im einen
|
||
sagt nichts über den anderen.
|
||
|
||
Legt sich ein eigenes App-Passwort und einen eigenen Kalender an und räumt
|
||
beides am Ende wieder weg. Die vorhandenen Kalender werden nur gelesen.
|
||
"""
|
||
import http.cookiejar
|
||
import json
|
||
import re
|
||
import sys
|
||
import urllib.error
|
||
import urllib.request
|
||
|
||
BASE = (sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8090").rstrip("/")
|
||
USER = sys.argv[2] if len(sys.argv) > 2 else "admin"
|
||
PASSWORT = sys.argv[3] if len(sys.argv) > 3 else "admin123"
|
||
|
||
ok_count = 0
|
||
fail = []
|
||
hinweise = []
|
||
|
||
|
||
def check(name, cond, detail=""):
|
||
global ok_count
|
||
if cond:
|
||
ok_count += 1
|
||
print(f" \033[32mok\033[0m {name}")
|
||
else:
|
||
fail.append(name)
|
||
print(f" \033[31mFEHL\033[0m {name} {detail}")
|
||
|
||
|
||
def hinweis(text):
|
||
hinweise.append(text)
|
||
print(f" \033[33m!\033[0m {text}")
|
||
|
||
|
||
# ── JSON-Schnittstelle: Anmelden, App-Passwort besorgen ─────────────────
|
||
jar = http.cookiejar.CookieJar()
|
||
OPENER = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
|
||
|
||
|
||
def api(method, path, body=None, headers=None):
|
||
data = json.dumps(body).encode() if body is not None else None
|
||
hdrs = dict(headers or {})
|
||
hdrs["Content-Type"] = "application/json"
|
||
req = urllib.request.Request(BASE + path, data=data, headers=hdrs, method=method)
|
||
try:
|
||
with OPENER.open(req) as r:
|
||
payload = r.read().decode()
|
||
return r.status, (json.loads(payload).get("data") if payload else None)
|
||
except urllib.error.HTTPError as e:
|
||
return e.code, None
|
||
|
||
|
||
st, login = api("POST", "/api/v1/auth/login", {"username": USER, "password": PASSWORT})
|
||
assert st == 200, f"Anmeldung fehlgeschlagen: {st}"
|
||
CSRF = {"X-CSRF-Token": login["csrf_token"]}
|
||
|
||
st, pw = api("POST", "/api/v1/app-passwords", {"name": "caldav-selbsttest"}, CSRF)
|
||
assert st == 201, f"App-Passwort anlegen fehlgeschlagen: {st}"
|
||
APP_PW, PW_ID = pw["password"], pw["id"]
|
||
|
||
st, testcal = api("POST", "/api/v1/calendars",
|
||
{"name": "CalDAV-Selbsttest", "color": "#a142f4"}, CSRF)
|
||
assert st == 201, f"Testkalender anlegen fehlgeschlagen: {st}"
|
||
CAL = testcal["id"]
|
||
st, zielcal = api("POST", "/api/v1/calendars",
|
||
{"name": "CalDAV-Selbsttest Ziel", "color": "#34a853"}, CSRF)
|
||
ZIEL = zielcal["id"]
|
||
|
||
|
||
def aufraeumen():
|
||
api("DELETE", f"/api/v1/calendars/{CAL}", None, CSRF)
|
||
api("DELETE", f"/api/v1/calendars/{ZIEL}", None, CSRF)
|
||
api("DELETE", f"/api/v1/app-passwords/{PW_ID}", None, CSRF)
|
||
|
||
|
||
# ── CalDAV: eigener Opener mit Basic Auth ───────────────────────────────
|
||
import base64
|
||
|
||
BASIC = "Basic " + base64.b64encode(f"{USER}:{APP_PW}".encode()).decode()
|
||
KONTO = "Basic " + base64.b64encode(f"{USER}:{PASSWORT}".encode()).decode()
|
||
|
||
HOME = f"/caldav/calendars/{USER}/"
|
||
CAL_PFAD = f"{HOME}{CAL}/"
|
||
ZIEL_PFAD = f"{HOME}{ZIEL}/"
|
||
|
||
|
||
def dav(method, path, body=None, headers=None, auth=BASIC, folge_um=True):
|
||
"""Gibt (Status, Kopfzeilen, Text) zurück. 3xx wird nicht verfolgt, wenn
|
||
folge_um falsch ist – die Umleitung selbst ist ja Teil der Prüfung."""
|
||
hdrs = {"Authorization": auth}
|
||
hdrs.update(headers or {})
|
||
data = body.encode() if isinstance(body, str) else body
|
||
req = urllib.request.Request(BASE + path, data=data, headers=hdrs, method=method)
|
||
|
||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||
def redirect_request(self, *a, **k):
|
||
return None
|
||
|
||
opener = urllib.request.build_opener() if folge_um else urllib.request.build_opener(NoRedirect)
|
||
try:
|
||
with opener.open(req) as r:
|
||
return r.status, r.headers, r.read().decode(errors="replace")
|
||
except urllib.error.HTTPError as e:
|
||
return e.code, e.headers, e.read().decode(errors="replace")
|
||
|
||
|
||
def propfind(path, props_xml, depth="0", auth=BASIC):
|
||
body = ('<?xml version="1.0" encoding="utf-8"?>'
|
||
'<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" '
|
||
'xmlns:cs="http://calendarserver.org/ns/" xmlns:i="http://apple.com/ns/ical/">'
|
||
f'<d:prop>{props_xml}</d:prop></d:propfind>')
|
||
return dav("PROPFIND", path, body,
|
||
{"Depth": depth, "Content-Type": "application/xml"}, auth=auth)
|
||
|
||
|
||
def hrefs(xml):
|
||
return re.findall(r"<href[^>]*>([^<]+)</href>", xml)
|
||
|
||
|
||
ICS = ("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Selbsttest//DE\r\n"
|
||
"BEGIN:VEVENT\r\nUID:{uid}\r\nDTSTAMP:20260101T000000Z\r\n"
|
||
"DTSTART:{start}\r\nDTEND:{end}\r\nSUMMARY:{summary}\r\n"
|
||
"END:VEVENT\r\nEND:VCALENDAR\r\n")
|
||
|
||
print(f"CalDAV-Selbsttest gegen {BASE} als {USER}\n")
|
||
|
||
try:
|
||
# ── Anmeldung und Auffinden ─────────────────────────────────────────
|
||
print("── Anmeldung ──")
|
||
st, _, _ = dav("PROPFIND", "/caldav/", auth="Basic Zm9vOmJhcg==")
|
||
check("falsches Passwort: 401", st == 401, st)
|
||
st, _, _ = dav("PROPFIND", "/caldav/", auth=KONTO)
|
||
check("Kontopasswort wird abgelehnt (nur App-Passwörter)", st == 401, st)
|
||
|
||
print("\n── Auffinden (RFC 6764) ──")
|
||
st, h, _ = dav("GET", "/.well-known/caldav", folge_um=False)
|
||
ziel = h.get("Location", "")
|
||
check(".well-known/caldav leitet um", st in (301, 302, 307, 308), st)
|
||
check("Umleitung zeigt auf den Principal", ziel.endswith(f"/caldav/principals-{USER}/"), ziel)
|
||
|
||
# Clients folgen der Umleitung mit GET. Ein 404 dort brach die
|
||
# Einrichtung ab – deshalb bleibt diese Prüfung stehen.
|
||
st, _, _ = dav("GET", f"/caldav/principals-{USER}/")
|
||
check("GET auf den Principal antwortet (kein 404)", st == 200, st)
|
||
|
||
st, _, x = propfind("/caldav/", "<d:current-user-principal/>")
|
||
check("current-user-principal", st == 207 and f"/caldav/principals-{USER}/" in x, st)
|
||
|
||
st, _, x = propfind(f"/caldav/principals-{USER}/", "<c:calendar-home-set/>")
|
||
check("calendar-home-set", st == 207 and HOME in x, x[:200])
|
||
|
||
print("\n── Kalenderliste ──")
|
||
st, _, x = propfind(HOME, "<d:resourcetype/><d:displayname/>"
|
||
"<c:supported-calendar-component-set/>", depth="1")
|
||
check("Sammlung listet auf (207)", st == 207, st)
|
||
check("Testkalender ist dabei", CAL_PFAD in hrefs(x), hrefs(x))
|
||
check("als calendar gekennzeichnet", "<calendar xmlns=\"urn:ietf:params:xml:ns:caldav\">" in x
|
||
or "<calendar/>" in x or "<calendar " in x)
|
||
check("Anzeigename kommt mit", "CalDAV-Selbsttest" in x)
|
||
check("VEVENT als unterstützte Komponente", 'name="VEVENT"' in x)
|
||
|
||
st, _, x = propfind(CAL_PFAD, "<cs:getctag/><i:calendar-color/>")
|
||
|
||
def prop_status(xml_text, prop):
|
||
"""Status des propstat-Blocks, in dem die Eigenschaft steht.
|
||
|
||
Ein PROPFIND antwortet nicht mit einem Status, sondern mit einem je
|
||
Gruppe: bekannte Eigenschaften unter 200, unbekannte unter 404. Nur
|
||
wer den zugehörigen Block liest, weiß, ob eine Eigenschaft wirklich
|
||
geliefert wurde."""
|
||
for block in xml_text.split("<propstat")[1:]:
|
||
if "<" + prop in block or ":" + prop in block:
|
||
m = re.search(r"HTTP/1\.1 (\d+)", block)
|
||
return int(m.group(1)) if m else 0
|
||
return 0
|
||
|
||
def wert(xml_text, prop):
|
||
m = re.search(rf"<{prop}[^>]*>([^<]*)</{prop}>", xml_text)
|
||
return m.group(1) if m else None
|
||
|
||
# Diese beiden kennt go-webdav nicht; Aether ergänzt sie selbst
|
||
# (backend/internal/calendar/propfind.go). Genau deshalb gehören sie
|
||
# geprüft: Sie hängen an eigenem Code, nicht an der Bibliothek.
|
||
check("getctag wird geliefert (200)", prop_status(x, "getctag") == 200,
|
||
prop_status(x, "getctag"))
|
||
check("calendar-color wird geliefert (200)", prop_status(x, "calendar-color") == 200,
|
||
prop_status(x, "calendar-color"))
|
||
check("Farbe in Apple-Schreibweise #RRGGBBAA",
|
||
wert(x, "calendar-color") == "#A142F4FF", wert(x, "calendar-color"))
|
||
|
||
# Ein ctag, das sich nicht ändert, ist schlimmer als gar keins: Der
|
||
# Client fragt einmal und hält den Kalender danach für unverändert.
|
||
vor = wert(x, "getctag")
|
||
st_, tmp = api("POST", f"/api/v1/calendars/{CAL}/events",
|
||
{"summary": "ctag-Probe", "start": "2026-12-01T09:00:00Z",
|
||
"end": "2026-12-01T10:00:00Z", "all_day": False}, CSRF)
|
||
_, _, x2 = propfind(CAL_PFAD, "<cs:getctag/>")
|
||
check("ctag ändert sich, wenn sich etwas ändert", wert(x2, "getctag") != vor,
|
||
f"{vor} -> {wert(x2, 'getctag')}")
|
||
api("DELETE", f"/api/v1/calendars/{CAL}/events/{tmp['uid']}", None, CSRF)
|
||
|
||
# Fragt ein Client *nur* danach, bliebe für go-webdav ein leeres <prop>
|
||
# übrig – das weist es ab. Aether schiebt dann einen Platzhalter unter.
|
||
_, _, x3 = propfind(CAL_PFAD, "<cs:getctag/>")
|
||
check("Anfrage nur nach getctag funktioniert", prop_status(x3, "getctag") == 200,
|
||
x3[:200])
|
||
|
||
# In der Übersicht (Depth 1 auf das Home-Set) muss jeder Kalender seine
|
||
# eigene Farbe tragen – dort holt sich ein Client sie beim Einrichten.
|
||
_, _, x4 = propfind(HOME, "<d:displayname/><i:calendar-color/>", depth="1")
|
||
farben = re.findall(r"<calendar-color[^>]*>([^<]*)</calendar-color>", x4)
|
||
check("Kalenderliste trägt die Farben", "#A142F4FF" in farben and "#34A853FF" in farben,
|
||
farben)
|
||
|
||
# Und eine Anfrage, in der nichts davon vorkommt, darf auch nichts davon
|
||
# zurückbekommen – sonst antwortete der Server auf ungestellte Fragen.
|
||
_, _, x5 = propfind(CAL_PFAD, "<d:displayname/>")
|
||
check("ohne Nachfrage keine Zusatz-Eigenschaften",
|
||
"getctag" not in x5 and "calendar-color" not in x5, x5[:200])
|
||
|
||
# ── Termine schreiben und lesen wie ein Client ──────────────────────
|
||
print("\n── Termin anlegen, ändern, löschen (PUT/GET/DELETE) ──")
|
||
uid = "selbsttest-1"
|
||
pfad = f"{CAL_PFAD}{uid}.ics"
|
||
st, h, _ = dav("PUT", pfad, ICS.format(uid=uid, start="20261110T090000Z",
|
||
end="20261110T100000Z", summary="Per CalDAV"),
|
||
{"Content-Type": "text/calendar; charset=utf-8"})
|
||
etag = h.get("ETag", "")
|
||
check("PUT legt an (201)", st == 201, st)
|
||
check("ETag in der Antwort", bool(etag), h)
|
||
|
||
st, h, body = dav("GET", pfad)
|
||
check("GET liefert den Termin", st == 200 and "SUMMARY:Per CalDAV" in body, st)
|
||
check("Content-Type ist text/calendar", "text/calendar" in h.get("Content-Type", ""),
|
||
h.get("Content-Type"))
|
||
|
||
st, _, x = propfind(CAL_PFAD, "<d:getetag/><d:getcontenttype/>", depth="1")
|
||
check("PROPFIND listet die Datei", pfad in hrefs(x), hrefs(x))
|
||
check("mit getetag", "getetag" in x)
|
||
|
||
st, _, _ = dav("PUT", pfad, ICS.format(uid=uid, start="20261110T090000Z",
|
||
end="20261110T100000Z", summary="Geaendert"),
|
||
{"Content-Type": "text/calendar", "If-Match": '"falsch"'})
|
||
check("PUT mit falschem If-Match: 412", st == 412, st)
|
||
|
||
st, _, _ = dav("PUT", pfad, ICS.format(uid=uid, start="20261110T090000Z",
|
||
end="20261110T100000Z", summary="Geaendert"),
|
||
{"Content-Type": "text/calendar", "If-Match": etag})
|
||
# 201 statt 204 beim Überschreiben kommt aus go-webdav und ist eine
|
||
# Ungenauigkeit gegenüber RFC 4918, die kein Client bestraft: Alle prüfen
|
||
# auf 2xx. Deshalb hier 2xx und nicht der genaue Wert.
|
||
check("PUT mit richtigem If-Match geht durch", 200 <= st < 300, st)
|
||
st, _, body = dav("GET", pfad)
|
||
check("Änderung ist drin", "SUMMARY:Geaendert" in body)
|
||
|
||
# Gegenrichtung: Was ein Client schreibt, muss die Oberfläche sehen.
|
||
st, evs = api("GET", f"/api/v1/calendars/{CAL}/events"
|
||
"?from=2026-01-01T00:00:00Z&to=2027-01-01T00:00:00Z")
|
||
check("per CalDAV angelegter Termin steht in der Web-Liste",
|
||
any(e["uid"] == uid and e["summary"] == "Geaendert" for e in evs or []), evs)
|
||
|
||
print("\n── REPORT: das, womit Clients wirklich abgleichen ──")
|
||
q = ('<?xml version="1.0" encoding="utf-8"?>'
|
||
'<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">'
|
||
'<d:prop><d:getetag/><c:calendar-data/></d:prop>'
|
||
'<c:filter><c:comp-filter name="VCALENDAR"><c:comp-filter name="VEVENT">'
|
||
'<c:time-range start="{a}" end="{b}"/>'
|
||
'</c:comp-filter></c:comp-filter></c:filter></c:calendar-query>')
|
||
st, _, x = dav("REPORT", CAL_PFAD, q.format(a="20261101T000000Z", b="20261201T000000Z"),
|
||
{"Depth": "1", "Content-Type": "application/xml"})
|
||
check("calendar-query im Zeitfenster findet ihn", st == 207 and pfad in hrefs(x), st)
|
||
check("calendar-data ist dabei", "BEGIN:VEVENT" in x)
|
||
|
||
st, _, x = dav("REPORT", CAL_PFAD, q.format(a="20270101T000000Z", b="20270201T000000Z"),
|
||
{"Depth": "1", "Content-Type": "application/xml"})
|
||
check("außerhalb des Fensters nicht", pfad not in hrefs(x), hrefs(x))
|
||
|
||
mg = ('<?xml version="1.0" encoding="utf-8"?>'
|
||
'<c:calendar-multiget xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">'
|
||
'<d:prop><d:getetag/><c:calendar-data/></d:prop>'
|
||
f'<d:href>{pfad}</d:href></c:calendar-multiget>')
|
||
st, _, x = dav("REPORT", CAL_PFAD, mg, {"Depth": "1", "Content-Type": "application/xml"})
|
||
check("calendar-multiget liefert die angefragte Datei",
|
||
st == 207 and "SUMMARY:Geaendert" in x, st)
|
||
|
||
# ── Was die Oberfläche schreibt, muss beim Client richtig ankommen ──
|
||
print("\n── Von der Weboberfläche geschrieben, per CalDAV gelesen ──")
|
||
st, ganztag = api("POST", f"/api/v1/calendars/{CAL}/events",
|
||
{"summary": "Ganztag", "start": "2026-09-03T00:00:00Z",
|
||
"end": "2026-09-03T00:00:00Z", "all_day": True}, CSRF)
|
||
_, _, body = dav("GET", f"{CAL_PFAD}{ganztag['uid']}.ics")
|
||
check("ganztägig als reines Datum", "DTSTART;VALUE=DATE:20260903" in body,
|
||
[l for l in body.splitlines() if "DTSTART" in l])
|
||
check("DTEND ist der Folgetag (exklusiv, RFC 5545)",
|
||
"DTEND;VALUE=DATE:20260904" in body,
|
||
[l for l in body.splitlines() if "DTEND" in l])
|
||
|
||
st, farbig = api("POST", f"/api/v1/calendars/{CAL}/events",
|
||
{"summary": "Farbe", "start": "2026-09-16T10:00:00Z",
|
||
"end": "2026-09-16T11:00:00Z", "all_day": False,
|
||
"color": "#ea4335"}, CSRF)
|
||
_, _, body = dav("GET", f"{CAL_PFAD}{farbig['uid']}.ics")
|
||
check("Terminfarbe als CSS3-Name (das liest DAVx5)", "COLOR:tomato" in body,
|
||
[l for l in body.splitlines() if "COLOR" in l])
|
||
|
||
st, reihe = api("POST", f"/api/v1/calendars/{CAL}/events",
|
||
{"summary": "Reihe", "start": "2026-10-05T08:00:00Z",
|
||
"end": "2026-10-05T09:00:00Z", "all_day": False,
|
||
"recurrence": {"freq": "WEEKLY", "interval": 1, "count": 4}}, CSRF)
|
||
_, _, body = dav("GET", f"{CAL_PFAD}{reihe['uid']}.ics")
|
||
check("Wiederholung als RRULE", "RRULE:" in body and "FREQ=WEEKLY" in body,
|
||
[l for l in body.splitlines() if "RRULE" in l])
|
||
|
||
api("DELETE", f"/api/v1/calendars/{CAL}/events/{reihe['uid']}"
|
||
"?occurrence=2026-10-12T08:00:00Z", None, CSRF)
|
||
_, _, body = dav("GET", f"{CAL_PFAD}{reihe['uid']}.ics")
|
||
check("gestrichener Einzeltermin als EXDATE",
|
||
any("EXDATE" in l and "20261012" in l for l in body.splitlines()),
|
||
[l for l in body.splitlines() if "EXDATE" in l])
|
||
|
||
print("\n── Umhängen: was der Client davon sieht ──")
|
||
_, _, vorher_quelle = propfind(CAL_PFAD, "<d:getetag/>", depth="1")
|
||
st, _ = api("POST", f"/api/v1/calendars/{CAL}/events/{reihe['uid']}/move",
|
||
{"calendar_id": ZIEL}, CSRF)
|
||
_, _, nachher_quelle = propfind(CAL_PFAD, "<d:getetag/>", depth="1")
|
||
_, _, nachher_ziel = propfind(ZIEL_PFAD, "<d:getetag/>", depth="1")
|
||
alt = f"{CAL_PFAD}{reihe['uid']}.ics"
|
||
neu = f"{ZIEL_PFAD}{reihe['uid']}.ics"
|
||
check("verschwindet aus der Quell-Sammlung",
|
||
alt in hrefs(vorher_quelle) and alt not in hrefs(nachher_quelle))
|
||
check("taucht in der Ziel-Sammlung auf", neu in hrefs(nachher_ziel), hrefs(nachher_ziel))
|
||
st, _, body = dav("GET", neu)
|
||
check("dort abrufbar, UID unverändert", st == 200 and f"UID:{reihe['uid']}" in body, st)
|
||
st, _, _ = dav("GET", alt)
|
||
check("unter der alten Adresse weg (404)", st == 404, st)
|
||
|
||
print("\n── Fremde Kalender ──")
|
||
st, _, _ = dav("PROPFIND", "/caldav/calendars/gibtsnicht/",
|
||
headers={"Depth": "0", "Content-Type": "application/xml"})
|
||
check("fremder Benutzerpfad: 404", st == 404, st)
|
||
|
||
print("\n── Löschen ──")
|
||
st, _, _ = dav("DELETE", pfad)
|
||
check("DELETE räumt weg (204)", st in (200, 204), st)
|
||
st, _, _ = dav("GET", pfad)
|
||
check("danach 404", st == 404, st)
|
||
|
||
finally:
|
||
aufraeumen()
|
||
|
||
print(f"\n{ok_count} bestanden, {len(fail)} fehlgeschlagen")
|
||
for h in hinweise:
|
||
print(f"Hinweis: {h}")
|
||
if fail:
|
||
print("Fehlgeschlagen: " + ", ".join(fail))
|
||
sys.exit(1 if fail else 0)
|