package main import ( "encoding/xml" "strings" "testing" "time" ) func TestParseICal(t *testing.T) { t.Run("Complex", func(t *testing.T) { i, err := ParseICal(strings.NewReader(icsWithTZ)) if err != nil { t.Fatalf("Parse failed: %s", err) } if i.Summary() != "WithTZ" { t.Error("Summary is wrong.") } if i.UID() != "e726899a5ca4c9e478842669bac9688144bb95d9" { t.Error("UID is wrong") } if i.Start().Format(ICAL_TIME) != "20180411T170000" { t.Error("Start time wrong") } if i.End().Format(ICAL_TIME) != "20180411T210000" { t.Error("End time wrong") } // No "Z" at the end --> local time zone. if i.Start().Location() != time.Local { t.Error("Timezone is wrong") } }) t.Run("Simple", func(t *testing.T) { i, err := ParseICal(strings.NewReader(icsSimple)) if err != nil { t.Fatalf("Parse failed: %s", err) } if i.Summary() != "Anon" { t.Error("Summary is wrong.") } if i.Start().Format(ICAL_TIME) != "20180308T113000" { t.Error("Start time wrong") } if i.End().Format(ICAL_TIME) != "20180308T130000" { t.Error("End time wrong") } // "Z" at the end --> UTC. if i.Start().Location() != time.UTC { t.Error("Start time should be UTC") } }) t.Run("First event only", func(t *testing.T) { i, err := ParseICal(strings.NewReader(icsMultiEvents)) if err != nil { t.Fatalf("Parse failed: %s", err) } if i.Summary() != "First" { t.Error("Summary is wrong.") } if i.UID() != "123" { t.Error("UID is wrong.") } // Check that our sub slice is correct: if i.eventData[0] != "UID:123" { t.Error("First line is wrong") } if i.eventData[len(i.eventData)-1] != "LAST-MODIFIED:20180401T182238Z" { t.Error("Last line is wrong") } }) t.Run("Whole day", func(t *testing.T) { i, err := ParseICal(strings.NewReader(icsWithDateOnly)) if err != nil { t.Fatalf("Parse failed: %s", err) } start := i.Start() end := i.End() expectedStart := time.Date(2018, 4, 7, 0, 0, 0, 0, time.Local) expectedEnd := time.Date(2018, 4, 9, 0, 0, 0, 0, time.Local) if !start.Equal(expectedStart) { t.Errorf("Unexpected start time (%s != %s)", start, expectedStart) } if !end.Equal(expectedEnd) { t.Errorf("Unexpected end time (%s != %s)", end, expectedEnd) } }) } func TestICal_Update(t *testing.T) { newStart := time.Date(2019, 10, 05, 10, 25, 0, 0, time.UTC) newEnd := time.Date(2019, 10, 05, 10, 55, 30, 0, time.UTC) t.Run("Values updated", func(t *testing.T) { ical, _ := ParseICal(strings.NewReader(icsSimple)) ical.Update(newStart, newEnd, false) if ical.Start() != newStart { t.Error("Start not updated correctly") } if ical.End() != newEnd { t.Error("End not updated correctly") } if ical.getTimeField("DTSTAMP").Sub(time.Now()) > 5*time.Second { t.Error("Timestamp doesn't seem to be updated") } }) t.Run("Other values untouched", func(t *testing.T) { // Use a ICal structure with dummy/unknown fields but also without // DTSTAMP or LAST-MODIFIED, so we can do a string comparison against // known values. ical, _ := ParseICal(strings.NewReader(`BEGIN:VCALENDAR PRODID:-//some//thing//EN VERSION:2.0 UNKNOWN:FIELD SOME;MORE:STUFF BEGIN:VEVENT UID:WHATEVERID SUMMARY:Summary CLASS:PUBLIC DTSTART:20180308T113000Z DTEND:20180308T130000Z CREATED:20180401T182238Z END:VEVENT EVEN;MORE:STUFF END:VCALENDAR`)) ical.Update(newStart, newEnd, false) if ical.String() != `BEGIN:VCALENDAR PRODID:-//some//thing//EN VERSION:2.0 UNKNOWN:FIELD SOME;MORE:STUFF BEGIN:VEVENT UID:WHATEVERID SUMMARY:Summary CLASS:PUBLIC DTSTART:20191005T102500Z DTEND:20191005T105530Z CREATED:20180401T182238Z END:VEVENT EVEN;MORE:STUFF END:VCALENDAR` { t.Error("The resulting ICS seems wrong.") } }) t.Run("Values wholeDay", func(t *testing.T) { newStart := time.Date(2019, 1, 2, 0, 0, 0, 0, time.Local) newEnd := time.Date(2019, 1, 3, 0, 0, 0, 0, time.Local) ical, _ := ParseICal(strings.NewReader(icsSimple)) ical.Update(newStart, newEnd, true) if ical.Start() != newStart { t.Error("Start not updated correctly") } if ical.End() != newEnd { t.Error("End not updated correctly") } if ical.getTimeField("DTSTAMP").Sub(time.Now()) > 5*time.Second { t.Error("Timestamp doesn't seem to be updated") } }) } func TestXMLUnmarshal(t *testing.T) { response := `/calendar/HTTP/1.1 404 Not Found/calendar/f10545b91c12a44ef7ed2ee3aa250df4-z2QSIWY.icsBEGIN:VCALENDAR VERSION:2.0 PRODID:-//some//NONSGML Calendar//EN X-WR-Timezone: Europe/London BEGIN:VEVENT DTSTART:20180404T080000Z DTEND:20180404T081500Z DTSTAMP:20180404T070805Z SEQUENCE:1522825685 SUMMARY:Test UID:f10545b91c12a44ef7ed2ee3aa250df4-z2QSIWY END:VEVENT END:VCALENDARHTTP/1.1 200 OK` ms := &MultiStatus{} err := xml.Unmarshal([]byte(response), ms) if err != nil { t.Fatal("Decoding should have worked") } if len(ms.Response) != 2 { t.Fatal("Invalid number of results.") } if ms.Response[0].PropStat.Prop.CalendarData.Valid() { t.Error("CalendarData 0 should not be valid") } if !ms.Response[1].PropStat.Prop.CalendarData.Valid() { t.Error("CalendarData 1 should be valid") } } const icsWithTZ = `BEGIN:VCALENDAR PRODID:-//Ximian//NONSGML Evolution Calendar//EN VERSION:2.0 CALSCALE:GREGORIAN BEGIN:VTIMEZONE TZID:/freeassociation.sourceforge.net/Europe/Berlin X-LIC-LOCATION:Europe/Berlin BEGIN:DAYLIGHT TZNAME:CEST DTSTART:19160429T230000 TZOFFSETFROM:+0100 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;UNTIL=19160430T220000Z;BYMONTH=4;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZNAME:CET DTSTART:19161007T010000 TZOFFSETFROM:+0200 TZOFFSETTO:+0100 RRULE:FREQ=YEARLY;UNTIL=19160930T230000Z;BYMONTH=10;BYDAY=1SU END:STANDARD BEGIN:DAYLIGHT TZNAME:CEST DTSTART:19170416T020000 TZOFFSETFROM:+0100 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYMONTH=4;BYDAY=3MO END:DAYLIGHT BEGIN:STANDARD TZNAME:CET DTSTART:19170917T030000 TZOFFSETFROM:+0200 TZOFFSETTO:+0100 RRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYMONTH=9;BYDAY=3MO END:STANDARD BEGIN:DAYLIGHT TZNAME:CEST DTSTART:19400402T020000 TZOFFSETFROM:+0100 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;UNTIL=19400401T010000Z;BYMONTH=4;BYDAY=1MO END:DAYLIGHT BEGIN:STANDARD TZNAME:CET DTSTART:19421105T030000 TZOFFSETFROM:+0200 TZOFFSETTO:+0100 RRULE:FREQ=YEARLY;UNTIL=19421102T010000Z;BYMONTH=11;BYDAY=1MO END:STANDARD BEGIN:DAYLIGHT TZNAME:CEST DTSTART:19430326T020000 TZOFFSETFROM:+0100 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;UNTIL=19430329T010000Z;BYMONTH=3;BYDAY=-1MO END:DAYLIGHT BEGIN:STANDARD TZNAME:CET DTSTART:19431001T030000 TZOFFSETFROM:+0200 TZOFFSETTO:+0100 RRULE:FREQ=YEARLY;UNTIL=19441002T010000Z;BYMONTH=10;BYDAY=1MO END:STANDARD BEGIN:DAYLIGHT TZNAME:CEST DTSTART:19440402T020000 TZOFFSETFROM:+0100 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;UNTIL=19450402T000000Z;BYMONTH=4;BYDAY=1MO END:DAYLIGHT BEGIN:DAYLIGHT TZNAME:CEMT DTSTART:19450524T020000 TZOFFSETFROM:+0200 TZOFFSETTO:+0300 RRULE:FREQ=YEARLY;UNTIL=19450523T230000Z;BYMONTH=5;BYDAY=-2TH END:DAYLIGHT BEGIN:DAYLIGHT TZNAME:CEST DTSTART:19450924T030000 TZOFFSETFROM:+0300 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;UNTIL=19450924T020000Z;BYMONTH=9;BYDAY=-1MO END:DAYLIGHT BEGIN:STANDARD TZNAME:CET DTSTART:19451118T030000 TZOFFSETFROM:+0200 TZOFFSETTO:+0100 RRULE:FREQ=YEARLY;UNTIL=19451118T020000Z;BYMONTH=11;BYDAY=3SU END:STANDARD BEGIN:STANDARD TZNAME:CET DTSTART:19460102T000000 TZOFFSETFROM:+0100 TZOFFSETTO:+0100 RRULE:FREQ=YEARLY;UNTIL=19451231T220000Z;BYMONTH=1;BYDAY=1TU END:STANDARD BEGIN:DAYLIGHT TZNAME:CEST DTSTART:19460408T020000 TZOFFSETFROM:+0100 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;UNTIL=19460414T010000Z;BYMONTH=4;BYDAY=2SU END:DAYLIGHT BEGIN:STANDARD TZNAME:CET DTSTART:19461001T030000 TZOFFSETFROM:+0200 TZOFFSETTO:+0100 RRULE:FREQ=YEARLY;UNTIL=19461007T010000Z;BYMONTH=10;BYDAY=1MO END:STANDARD BEGIN:DAYLIGHT TZNAME:CEST DTSTART:19470401T030000 TZOFFSETFROM:+0100 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;UNTIL=19470406T010000Z;BYMONTH=4;BYDAY=1SU END:DAYLIGHT BEGIN:DAYLIGHT TZNAME:CEMT DTSTART:19470513T030000 TZOFFSETFROM:+0200 TZOFFSETTO:+0300 RRULE:FREQ=YEARLY;UNTIL=19470511T000000Z;BYMONTH=5;BYDAY=2SU END:DAYLIGHT BEGIN:DAYLIGHT TZNAME:CEST DTSTART:19470624T030000 TZOFFSETFROM:+0300 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;UNTIL=19470629T020000Z;BYMONTH=6;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZNAME:CET DTSTART:19471007T030000 TZOFFSETFROM:+0200 TZOFFSETTO:+0100 RRULE:FREQ=YEARLY;UNTIL=19491002T020000Z;BYMONTH=10;BYDAY=1SU END:STANDARD BEGIN:DAYLIGHT TZNAME:CEST DTSTART:19480415T020000 TZOFFSETFROM:+0100 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;UNTIL=19480418T010000Z;BYMONTH=4;BYDAY=3SU END:DAYLIGHT BEGIN:DAYLIGHT TZNAME:CEST DTSTART:19490408T020000 TZOFFSETFROM:+0100 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;UNTIL=19490410T010000Z;BYMONTH=4;BYDAY=2SU END:DAYLIGHT BEGIN:STANDARD TZNAME:CET DTSTART:19800102T000000 TZOFFSETFROM:+0100 TZOFFSETTO:+0100 RRULE:FREQ=YEARLY;UNTIL=19791231T220000Z;BYMONTH=1;BYDAY=1TU END:STANDARD BEGIN:DAYLIGHT TZNAME:CEST DTSTART:19800401T020000 TZOFFSETFROM:+0100 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;UNTIL=19800406T010000Z;BYMONTH=4;BYDAY=1SU END:DAYLIGHT BEGIN:STANDARD TZNAME:CET DTSTART:19800930T030000 TZOFFSETFROM:+0200 TZOFFSETTO:+0100 RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU END:STANDARD BEGIN:DAYLIGHT TZNAME:CEST DTSTART:19810325T020000 TZOFFSETFROM:+0100 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZNAME:CET DTSTART:19961028T030000 TZOFFSETFROM:+0200 TZOFFSETTO:+0100 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:e726899a5ca4c9e478842669bac9688144bb95d9 DTSTAMP:20180312T092638Z DTSTART;TZID=/freeassociation.sourceforge.net/Europe/Berlin:20180411T170000 DTEND;TZID=/freeassociation.sourceforge.net/Europe/Berlin:20180411T210000 SEQUENCE:2 SUMMARY:WithTZ TRANSP:OPAQUE CLASS:PUBLIC CREATED:20180313T074119Z LAST-MODIFIED:20180313T074119Z END:VEVENT END:VCALENDAR` const icsWithDateOnly = `BEGIN:VCALENDAR PRODID:+//IDN bitfire.at//DAVdroid/1.10.1.1-gplay ical4j/2.x VERSION:2.0 BEGIN:VEVENT DTSTAMP:20180308T192428Z UID:b3cf0d84-b746-420c-8642-3b107ddfd834 SUMMARY:WholeDay DTSTART;VALUE=DATE:20180407 DTEND;VALUE=DATE:20180409 STATUS:CONFIRMED TRANSP:TRANSPARENT CLASS:PUBLIC END:VEVENT END:VCALENDAR` const icsSimple = `BEGIN:VCALENDAR PRODID:-//aksdb//calanonsync//EN VERSION:2.0 BEGIN:VEVENT UID:0A6768CB8B086C58F2942757C6E515DF SUMMARY:Anon CLASS:PUBLIC DTSTART:20180308T113000Z DTEND:20180308T130000Z CREATED:20180401T182238Z DTSTAMP:20180401T182238Z LAST-MODIFIED:20180401T182238Z END:VEVENT END:VCALENDAR` const icsMultiEvents = `BEGIN:VCALENDAR PRODID:-//some//thing//EN VERSION:2.0 BEGIN:VEVENT UID:123 SUMMARY:First CLASS:PUBLIC DTSTART:20180308T113000Z DTEND:20180308T130000Z CREATED:20180401T182238Z DTSTAMP:20180401T182238Z LAST-MODIFIED:20180401T182238Z END:VEVENT BEGIN:VEVENT UID:456 SUMMARY:Second CLASS:PUBLIC DTSTART:20180309T113000Z DTEND:20180309T130000Z CREATED:20180401T182238Z DTSTAMP:20180401T182238Z LAST-MODIFIED:20180401T182239Z END:VEVENT END:VCALENDAR`