package nextcloud import ( "strings" "testing" ) func TestParseICSBasics(t *testing.T) { ics := strings.Join([]string{ "BEGIN:VCALENDAR", "VERSION:2.0", "BEGIN:VTIMEZONE", "TZID:Europe/Paris", "BEGIN:STANDARD", "DTSTART:19961027T030000", "END:STANDARD", "END:VTIMEZONE", "BEGIN:VEVENT", "UID:abc-123", "SUMMARY:Réunion\\, équipe", "DESCRIPTION:Ligne 1\\nLigne 2", "DTSTART;TZID=Europe/Paris:20260611T100000", "DTEND;TZID=Europe/Paris:20260611T110000", "RRULE:FREQ=WEEKLY;BYDAY=TH", "EXDATE;TZID=Europe/Paris:20260618T100000", "END:VEVENT", "END:VCALENDAR", }, "\r\n") e := parseICS(ics) if e.UID != "abc-123" { t.Fatalf("UID = %q", e.UID) } if e.Summary != "Réunion, équipe" { t.Fatalf("Summary = %q", e.Summary) } if e.Description != "Ligne 1\nLigne 2" { t.Fatalf("Description = %q", e.Description) } // VTIMEZONE DTSTART must not leak into the event. if e.Start != "20260611T080000Z" { t.Fatalf("Start = %q, want UTC-converted 20260611T080000Z", e.Start) } if e.End != "20260611T090000Z" { t.Fatalf("End = %q", e.End) } if e.AllDay { t.Fatal("AllDay should be false") } if e.RRule != "FREQ=WEEKLY;BYDAY=TH" { t.Fatalf("RRule = %q", e.RRule) } if len(e.ExDates) != 1 || e.ExDates[0] != "20260618T080000Z" { t.Fatalf("ExDates = %v", e.ExDates) } } func TestParseICSAllDayAndFolding(t *testing.T) { ics := strings.Join([]string{ "BEGIN:VCALENDAR", "BEGIN:VEVENT", "UID:x", "SUMMARY:Long title that", " continues folded", "DTSTART;VALUE=DATE:20260611", "DTEND;VALUE=DATE:20260612", "END:VEVENT", "END:VCALENDAR", }, "\r\n") e := parseICS(ics) if !e.AllDay { t.Fatal("AllDay should be true") } if e.Start != "20260611" || e.End != "20260612" { t.Fatalf("Start/End = %q/%q", e.Start, e.End) } if e.Summary != "Long title thatcontinues folded" { t.Fatalf("Summary = %q", e.Summary) } } func TestParseICSPrefersMasterVEvent(t *testing.T) { ics := strings.Join([]string{ "BEGIN:VCALENDAR", "BEGIN:VEVENT", "UID:rec", "RECURRENCE-ID:20260618T100000Z", "SUMMARY:Exception", "DTSTART:20260618T120000Z", "DTEND:20260618T130000Z", "END:VEVENT", "BEGIN:VEVENT", "UID:rec", "SUMMARY:Master", "DTSTART:20260611T100000Z", "DTEND:20260611T110000Z", "RRULE:FREQ=DAILY", "END:VEVENT", "END:VCALENDAR", }, "\r\n") e := parseICS(ics) if e.Summary != "Master" { t.Fatalf("Summary = %q, want master VEVENT", e.Summary) } } func TestBuildICSRoundTrip(t *testing.T) { event := &Event{ UID: "round-trip", Summary: "Point; hebdo, équipe", Description: "Ordre du jour:\npoint 1", Location: "Salle A", Start: "20260611T100000Z", End: "20260611T110000Z", RRule: "FREQ=WEEKLY;BYDAY=TH", ExDates: []string{"20260618T100000Z"}, Color: "#1A73E8", Attendees: []EventAttendee{ {Email: "invite@example.com", Name: "Invité", Status: "ACCEPTED"}, }, } parsed := parseICS(buildICS(event)) if parsed.Summary != event.Summary { t.Fatalf("Summary = %q", parsed.Summary) } if parsed.Description != event.Description { t.Fatalf("Description = %q", parsed.Description) } if parsed.RRule != event.RRule { t.Fatalf("RRule = %q", parsed.RRule) } if parsed.Color != "" { t.Fatalf("Color should not be serialized in ICS, got %q", parsed.Color) } if len(parsed.ExDates) != 1 || parsed.ExDates[0] != "20260618T100000Z" { t.Fatalf("ExDates = %v", parsed.ExDates) } if len(parsed.Attendees) != 1 || parsed.Attendees[0].Email != "invite@example.com" || parsed.Attendees[0].Status != "ACCEPTED" { t.Fatalf("Attendees = %+v", parsed.Attendees) } if parsed.Start != event.Start || parsed.End != event.End { t.Fatalf("Start/End = %q/%q", parsed.Start, parsed.End) } } func TestMergeEventPreservesUID(t *testing.T) { existing := &Event{ UID: "abc@ulti", Summary: "Original", Start: "20260611T100000Z", End: "20260611T110000Z", Path: "/remote.php/dav/calendars/user/personal/abc@ulti.ics", MeetURL: "https://meet.example/room", ExDates: []string{"20260618T100000Z"}, } patch := &Event{ Summary: "Updated title", Start: "20260612T100000Z", End: "20260612T110000Z", } merged := MergeEvent(existing, patch) if merged.UID != "abc@ulti" { t.Fatalf("UID = %q", merged.UID) } if merged.Summary != "Updated title" { t.Fatalf("Summary = %q", merged.Summary) } if merged.MeetURL != "https://meet.example/room" { t.Fatalf("MeetURL should be preserved, got %q", merged.MeetURL) } if len(merged.ExDates) != 1 { t.Fatalf("ExDates = %v", merged.ExDates) } } func TestMergeEventUIDFromPath(t *testing.T) { existing := &Event{ Summary: "Keep", Path: "/remote.php/dav/calendars/user/personal/fallback@ulti.ics", } patch := &Event{Summary: "New"} merged := MergeEvent(existing, patch) if merged.UID != "fallback@ulti" { t.Fatalf("UID = %q", merged.UID) } } func TestBuildICSAllDay(t *testing.T) { ics := buildICS(&Event{UID: "ad", Summary: "Férié", Start: "20260714", End: "20260715", AllDay: true}) if !strings.Contains(ics, "DTSTART;VALUE=DATE:20260714") { t.Fatalf("missing all-day DTSTART:\n%s", ics) } parsed := parseICS(ics) if !parsed.AllDay { t.Fatal("AllDay should round-trip") } } func TestParseCalendarListNormalizesCloudPrefix(t *testing.T) { basePath := "/remote.php/dav/calendars/user@example.com/" raw := ` /cloud/remote.php/dav/calendars/user@example.com/ Root /cloud/remote.php/dav/calendars/user@example.com/personal/ Personal #1a73e8 ` cals, err := parseCalendarList(strings.NewReader(raw), basePath) if err != nil { t.Fatal(err) } if len(cals) != 1 { t.Fatalf("len = %d, want 1", len(cals)) } if cals[0].ID != "personal" { t.Fatalf("ID = %q, want personal", cals[0].ID) } if cals[0].Path != "/remote.php/dav/calendars/user@example.com/personal/" { t.Fatalf("Path = %q", cals[0].Path) } }