Files
Aether/backend/internal/calendar/propfind_test.go
T
marcandClaude Opus 5 128d54035a feat(calendar): getctag und calendar-color fuer CalDAV-Clients
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>
2026-09-05 18:11:25 +02:00

117 lines
3.9 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}
byID := map[uuid.UUID]*Calendar{id: cal}
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},
}
for _, tc := range tests {
part := `<response xmlns="DAV:"><href>` + tc.href + `</href><propstat/>`
if got := calendarOfResponse(part, byID); 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)
}
}