Files
Aether/backend/internal/calendar/propfind_test.go
T
marcandClaude Opus 5 f71ab710e1 feat(calendar): Kalender anlegen, umbenennen, loeschen -- vom Client aus
Bisher konnte ein CalDAV-Client Termine verwalten, aber keine Kalender:

  MKCALENDAR -> 405    anlegen
  PROPPATCH  -> 501    umbenennen, umfaerben
  DELETE     -> 404    loeschen

go-webdav deckt davon nur MKCOL ab, und selbst da verwarf unser Backend
die vom Client gewaehlte Adresse. Alle drei Verben faengt der Handler
jetzt vorher ab (collections.go).

Der Kern steckt aber nicht in den Verben, sondern in der Adresse: Bei
MKCALENDAR sucht *der Client* aus, wo der Kalender liegen soll
("…/calendars/max/arbeit/"). Unsere Kalender lagen bisher zwingend unter
ihrer UUID -- der Client haette angelegt und auf seine eigene Adresse
sofort ein 404 bekommen.

Deshalb Migration 23 mit einer Spalte path_segment: gesetzt = die
gewaehlte Adresse, NULL = es gilt weiterhin die UUID. Bestehende
Kalender bleiben damit unter genau der Adresse erreichbar, unter der die
eingerichteten Geraete sie kennen -- ein Wechsel haette jedes Telefon
dazu gebracht, seine Termine fuer geloescht zu halten. Aufgeloest wird
beides in einer Abfrage (GetCalendarByRef), die gewaehlte Adresse hat
Vorrang.

PROPPATCH antwortet mit einem Status je Eigenschaft, auch wenn alles
geklappt hat: Es kann teilweise gelingen, und ein nacktes 200
verschwiege, welcher Teil angekommen ist. Was wir nicht aendern koennen,
bekommt 403 statt stillem Schlucken -- ein Client, der seine Aenderung
fuer angekommen haelt, ist schlimmer dran als einer mit klarer Absage.
Ebenso eine unlesbare Farbe: absagen statt raten.

DELETE auf eine Sammlung loescht den Kalender mitsamt allen Terminen --
das ist in WebDAV seine Bedeutung.

CalDAV-Selbsttest von 45 auf 62 Pruefungen: anlegen, unter der
gewaehlten Adresse erreichbar, Farbe aus dem MKCALENDAR uebernommen,
Termin darin ablegen und abrufen, zweites Anlegen auf dieselbe Adresse
405, umbenennen und umfaerben mit Status je Eigenschaft, loeschen, danach
404 -- und dasselbe noch einmal ueber MKCOL, das Verb, das DAVx5 nimmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 03:37:41 +02:00

122 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package calendar
import (
"strings"
"testing"
"github.com/google/uuid"
)
const propfindHead = `<?xml version="1.0" encoding="utf-8"?>
<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/" xmlns:i="http://apple.com/ns/ical/">`
func TestParsePropfindTrenntUnsereEigenschaftenAb(t *testing.T) {
body := []byte(propfindHead + `<d:prop><d:displayname/><cs:getctag/><i:calendar-color/></d:prop></d:propfind>`)
want, rewritten := parsePropfind(body)
if !want.ctag || !want.color {
t.Fatalf("beide Eigenschaften sollten erkannt werden, bekam %+v", want)
}
out := string(rewritten)
if !strings.Contains(out, "<displayname/>") {
t.Errorf("die übrigen Eigenschaften müssen erhalten bleiben: %s", out)
}
// Der Kern: go-webdav darf sie nicht zu sehen bekommen, sonst meldet es
// sie als 404 und dieselbe Eigenschaft stünde zweimal in der Antwort.
if strings.Contains(out, "getctag") || strings.Contains(out, "calendar-color") {
t.Errorf("unsere Eigenschaften müssen entfernt sein: %s", out)
}
}
func TestParsePropfindLaesstFremdeAnfragenInRuhe(t *testing.T) {
body := []byte(propfindHead + `<d:prop><d:resourcetype/><d:getetag/></d:prop></d:propfind>`)
want, rewritten := parsePropfind(body)
if want.any() {
t.Errorf("nichts von uns gefragt, trotzdem %+v", want)
}
if string(rewritten) != string(body) {
t.Errorf("Rumpf muss unverändert durchgereicht werden")
}
}
func TestParsePropfindNurUnsereEigenschaftHinterlaesstKeinLeeresProp(t *testing.T) {
// Ein leeres <prop> weist go-webdav ab – dann käme auf eine gültige
// Anfrage ein Fehler statt einer Antwort.
body := []byte(propfindHead + `<d:prop><cs:getctag/></d:prop></d:propfind>`)
_, rewritten := parsePropfind(body)
if !strings.Contains(string(rewritten), "<resourcetype/>") {
t.Errorf("Platzhalter fehlt: %s", rewritten)
}
}
func TestParsePropfindAllpropUndLeererRumpf(t *testing.T) {
for _, tc := range []struct {
name string
body []byte
}{
{"allprop", []byte(propfindHead + `<d:allprop/></d:propfind>`)},
{"leerer Rumpf", nil}, // RFC 4918, 9.1: leer bedeutet allprop
} {
want, rewritten := parsePropfind(tc.body)
if !want.ctag || !want.color {
t.Errorf("%s: sollte alles liefern, bekam %+v", tc.name, want)
}
if string(rewritten) != string(tc.body) {
t.Errorf("%s: Rumpf darf nicht umgebaut werden", tc.name)
}
}
}
func TestCalendarOfResponseTrifftNurKalender(t *testing.T) {
id := uuid.New()
cal := &Calendar{ID: id, Color: "#a142f4", CTag: 7}
// Ein Kalender unter seiner UUID, einer unter einer vom Client
// gewählten Adresse – beide müssen gefunden werden.
eigen := &Calendar{ID: uuid.New(), PathSegment: "arbeit", Color: "#34a853"}
byRef := map[string]*Calendar{id.String(): cal, "arbeit": eigen}
tests := []struct {
name string
href string
will *Calendar
}{
{"Kalender", "/caldav/calendars/max/" + id.String() + "/", cal},
{"Home-Set", "/caldav/calendars/max/", nil},
{"Principal", "/caldav/principals-max/", nil},
{"einzelner Termin", "/caldav/calendars/max/" + id.String() + "/abc.ics", nil},
{"fremder Kalender", "/caldav/calendars/max/" + uuid.New().String() + "/", nil},
{"selbst gewählte Adresse", "/caldav/calendars/max/arbeit/", eigen},
{"Termin darin", "/caldav/calendars/max/arbeit/abc.ics", nil},
}
for _, tc := range tests {
part := `<response xmlns="DAV:"><href>` + tc.href + `</href><propstat/>`
if got := calendarOfResponse(part, byRef); got != tc.will {
t.Errorf("%s: bekam %v, erwartet %v", tc.name, got, tc.will)
}
}
}
func TestExtrasPropstat(t *testing.T) {
cal := &Calendar{Color: "#a142f4", CTag: 12}
got := extrasPropstat(cal, wantedExtras{ctag: true, color: true})
if !strings.Contains(got, ">12</getctag>") {
t.Errorf("ctag fehlt: %s", got)
}
// Apple-Clients und DAVx5 erwarten den Alpha-Anteil.
if !strings.Contains(got, ">#A142F4FF</calendar-color>") {
t.Errorf("Farbe nicht in Apple-Schreibweise: %s", got)
}
if !strings.Contains(got, "HTTP/1.1 200 OK") {
t.Errorf("Status fehlt: %s", got)
}
// Ohne Farbe bleibt die Eigenschaft weg – lieber gar nicht melden als
// leer, sonst färbt der Client den Kalender schwarz.
if got := extrasPropstat(&Calendar{CTag: 1}, wantedExtras{color: true}); got != "" {
t.Errorf("ohne Farbe sollte nichts entstehen, bekam %s", got)
}
}