105 lines
3.8 KiB
Go
105 lines
3.8 KiB
Go
package icalfix
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
|
|
ical "github.com/emersion/go-ical"
|
|
)
|
|
|
|
// TestNormalizeTimeZonesFixesRecurringEvent reproduces the bug reported by
|
|
// a DAVx5 client: a recurring VEVENT with a Windows-style TZID
|
|
// ("W. Europe Standard Time") decodes fine, but expanding its recurrence
|
|
// via RecurrenceSet (as go-webdav's caldav.Filter does for a time-range
|
|
// REPORT) fails with "unknown time zone" because go-ical resolves TZID
|
|
// via a plain time.LoadLocation call. Normalizing the TZID to its IANA
|
|
// equivalent before decoding must make that expansion succeed.
|
|
func TestNormalizeTimeZonesFixesRecurringEvent(t *testing.T) {
|
|
const raw = "BEGIN:VCALENDAR\r\n" +
|
|
"VERSION:2.0\r\n" +
|
|
"PRODID:-//Test//Test//EN\r\n" +
|
|
"BEGIN:VTIMEZONE\r\n" +
|
|
"TZID:W. Europe Standard Time\r\n" +
|
|
"END:VTIMEZONE\r\n" +
|
|
"BEGIN:VEVENT\r\n" +
|
|
"UID:recurring@example.com\r\n" +
|
|
"DTSTAMP:20240101T000000Z\r\n" +
|
|
"DTSTART;TZID=W. Europe Standard Time:20260522T220000\r\n" +
|
|
"DTEND;TZID=W. Europe Standard Time:20260522T230000\r\n" +
|
|
"RRULE:FREQ=WEEKLY;COUNT=5\r\n" +
|
|
"SUMMARY:Weekly sync\r\n" +
|
|
"END:VEVENT\r\n" +
|
|
"END:VCALENDAR\r\n"
|
|
|
|
normalized := NormalizeTimeZones([]byte(raw))
|
|
|
|
cal, err := ical.NewDecoder(bytes.NewReader(normalized)).Decode()
|
|
if err != nil {
|
|
t.Fatalf("decoding normalized calendar: %v", err)
|
|
}
|
|
events := cal.Events()
|
|
if len(events) != 1 {
|
|
t.Fatalf("expected 1 event, got %d", len(events))
|
|
}
|
|
|
|
if _, err := events[0].RecurrenceSet(nil); err != nil {
|
|
t.Fatalf("expanding recurrence after normalization: %v", err)
|
|
}
|
|
|
|
// The un-normalized original must reproduce the reported failure, so
|
|
// this test actually demonstrates the fix rather than trivially
|
|
// passing regardless.
|
|
origCal, err := ical.NewDecoder(bytes.NewReader([]byte(raw))).Decode()
|
|
if err != nil {
|
|
t.Fatalf("decoding original calendar: %v", err)
|
|
}
|
|
if _, err := origCal.Events()[0].RecurrenceSet(nil); err == nil {
|
|
t.Fatalf("expected RecurrenceSet to fail on un-normalized Windows TZID, it didn't")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeTimeZonesLeavesUnknownAndIANAZonesAlone(t *testing.T) {
|
|
const raw = "BEGIN:VCALENDAR\r\n" +
|
|
"DTSTART;TZID=Europe/Berlin:20260522T220000\r\n" +
|
|
"DTEND;TZID=Some/Unknown-Zone:20260522T230000\r\n" +
|
|
"END:VCALENDAR\r\n"
|
|
|
|
got := string(NormalizeTimeZones([]byte(raw)))
|
|
if got != raw {
|
|
t.Fatalf("expected data to be unchanged, got:\n%s", got)
|
|
}
|
|
}
|
|
|
|
// TestNormalizeTimeZonesFixesVTIMEZONECRLFLine reproduces a real bug: the
|
|
// standalone "TZID:<name>" property line inside a VTIMEZONE component
|
|
// (as opposed to a "TZID=<name>" parameter on DTSTART/DTEND/etc.) was
|
|
// never being normalized on real-world feeds, because every
|
|
// Outlook/Exchange-published ICS uses CRLF line endings and Go's RE2 "$"
|
|
// anchor in multi-line mode only matches immediately before a literal
|
|
// "\n" — not before "\r\n". So on a line like
|
|
// "TZID:W. Europe Standard Time\r\n", the old
|
|
// `^TZID:([^\r\n]+)$` pattern never matched at all, leaving the
|
|
// VTIMEZONE's own declared TZID un-normalized while every "TZID=..."
|
|
// parameter elsewhere in the same object WAS normalized (that regex has
|
|
// no such issue) — so nothing referencing the VTIMEZONE by name could
|
|
// ever find it again. See internal/icssub.Series.referencedTimezones,
|
|
// which depends on exactly this match to embed the right VTIMEZONE in a
|
|
// synthetic per-series VCALENDAR.
|
|
func TestNormalizeTimeZonesFixesVTIMEZONECRLFLine(t *testing.T) {
|
|
const raw = "BEGIN:VCALENDAR\r\n" +
|
|
"BEGIN:VTIMEZONE\r\n" +
|
|
"TZID:W. Europe Standard Time\r\n" +
|
|
"END:VTIMEZONE\r\n" +
|
|
"END:VCALENDAR\r\n"
|
|
const want = "BEGIN:VCALENDAR\r\n" +
|
|
"BEGIN:VTIMEZONE\r\n" +
|
|
"TZID:Europe/Berlin\r\n" +
|
|
"END:VTIMEZONE\r\n" +
|
|
"END:VCALENDAR\r\n"
|
|
|
|
got := string(NormalizeTimeZones([]byte(raw)))
|
|
if got != want {
|
|
t.Fatalf("CRLF-terminated VTIMEZONE TZID line was not normalized:\ngot: %q\nwant: %q", got, want)
|
|
}
|
|
}
|