package calendar import ( "testing" "time" ) const sampleICS = `BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//EN BEGIN:VTIMEZONE TZID:Europe/Berlin BEGIN:STANDARD DTSTART:19701025T030000 TZOFFSETFROM:+0200 TZOFFSETTO:+0100 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:1 DTSTART;TZID=Europe/Berlin:20260825T090000 DTEND;TZID=Europe/Berlin:20260825T100000 SUMMARY:Zahnarzt END:VEVENT BEGIN:VEVENT UID:2 DTSTART:20260826T120000Z DURATION:PT45M SUMMARY:Call END:VEVENT BEGIN:VEVENT UID:3 DTSTART;VALUE=DATE:20260827 SUMMARY:Ganztägig END:VEVENT BEGIN:VEVENT UID:4 DTSTART:20260828T080000Z DTEND:20260828T090000Z STATUS:CANCELLED SUMMARY:Abgesagt END:VEVENT BEGIN:VEVENT UID:5 DTSTART:20260828T100000Z DTEND:20260828T110000Z TRANSP:TRANSPARENT SUMMARY:Verfügbar END:VEVENT BEGIN:VEVENT UID:6 DTSTART:20260829T080000Z DTEND:202609 01T080000Z SUMMARY:Gefaltet END:VEVENT END:VCALENDAR ` func TestParseICS(t *testing.T) { berlin, err := time.LoadLocation("Europe/Berlin") if err != nil { t.Fatal(err) } events := parseICS(sampleICS, berlin) if len(events) != 6 { t.Fatalf("6 Events erwartet, got %d", len(events)) } // TZID-Event if !events[0].start.Equal(time.Date(2026, 8, 25, 9, 0, 0, 0, berlin)) { t.Errorf("TZID-Start falsch: %v", events[0].start) } if !events[0].end.Equal(time.Date(2026, 8, 25, 10, 0, 0, 0, berlin)) { t.Errorf("TZID-Ende falsch: %v", events[0].end) } // UTC + DURATION if !events[1].end.Equal(events[1].start.Add(45*time.Minute)) { t.Errorf("DURATION nicht angewandt: %v", events[1].end) } // Ganztägig if !events[2].allDay { t.Error("VALUE=DATE nicht erkannt") } // Cancelled / Transparent markiert if !events[3].cancelled { t.Error("CANCELLED nicht erkannt") } if !events[4].transparent { t.Error("TRANSPARENT nicht erkannt") } // Gefaltete Zeile (DTEND über zwei Zeilen) if !events[5].end.Equal(time.Date(2026, 9, 1, 8, 0, 0, 0, time.UTC)) { t.Errorf("Folding nicht aufgelöst, Ende: %v", events[5].end) } } func TestParseISODuration(t *testing.T) { cases := map[string]time.Duration{ "PT15M": 15 * time.Minute, "PT1H30M": 90 * time.Minute, "P1D": 24 * time.Hour, "P2W": 14 * 24 * time.Hour, "PT45S": 45 * time.Second, "": 0, "X": 0, "P-1D": 0, } for in, want := range cases { got, ok := parseISODuration(in) if in == "" || in == "X" || in == "P-1D" { if ok { t.Errorf("%q sollte ungültig sein", in) } continue } if !ok || got != want { t.Errorf("parseISODuration(%q) = %v (ok=%v), want %v", in, got, ok, want) } } } func TestClampIntervals(t *testing.T) { from := time.Date(2026, 8, 25, 0, 0, 0, 0, time.UTC) to := from.Add(48 * time.Hour) in := []Interval{ {Start: from.Add(-2 * time.Hour), End: from.Add(time.Hour)}, {Start: from.Add(3 * time.Hour), End: from.Add(2 * time.Hour)}, // ungültig {Start: from.Add(10 * time.Hour), End: to.Add(time.Hour)}, } out := clampIntervals(in, from, to) if len(out) != 2 { t.Fatalf("2 Intervalle erwartet, got %d", len(out)) } if !out[0].Start.Equal(from) || !out[0].End.Equal(from.Add(time.Hour)) { t.Errorf("Intervall 1 nicht geklemmt: %v–%v", out[0].Start, out[0].End) } if !out[1].End.Equal(to) { t.Errorf("Intervall 2 nicht geklemmt: %v", out[1].End) } }