From 07d99884d8c117566f49e215f66866c7f6bc65b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Mon, 27 Jul 2026 04:14:43 -0500 Subject: [PATCH] caldav: add support for more props on MKCOL and PROPFIND Read calendar-description, calendar-timezone, calendar-color and supported-calendar-component-set from a MKCOL request body, and report the color and the timezone in PROPFIND responses. Each property in the request body is matched together with its namespace. A flat "set>prop>name" struct tag cannot do that, because encoding/xml checks the namespace of such a tag against every element along the path rather than against the last one: naming one there matches nothing, and leaving it out matches the element in any namespace. The body is decoded through nested types instead. Calendar.Timezone is a parsed *ical.Calendar. RFC 4791 section 5.2.2 requires the property value to be an iCalendar object with exactly one VTIMEZONE component, and a value that is not one is rejected with the CALDAV:valid-calendar-data precondition of section 5.3.1.1. calendar-color belongs to Apple rather than to CalDAV, and is read and reported in the http://apple.com/ns/ical/ namespace. --- caldav/caldav.go | 2 + caldav/elements.go | 38 +++- caldav/server.go | 50 +++- caldav/server_test.go | 514 ++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 575 insertions(+), 29 deletions(-) diff --git a/caldav/caldav.go b/caldav/caldav.go index 1df6c66..37985b2 100644 --- a/caldav/caldav.go +++ b/caldav/caldav.go @@ -67,8 +67,10 @@ type Calendar struct { Path string Name string Description string + Color string MaxResourceSize int64 SupportedComponentSet []string + Timezone *ical.Calendar // ReadOnly reports that the current user may only read this calendar. It // controls the DAV:current-user-privilege-set reported by the server. ReadOnly bool diff --git a/caldav/elements.go b/caldav/elements.go index aed6050..a107e3e 100644 --- a/caldav/elements.go +++ b/caldav/elements.go @@ -8,16 +8,22 @@ import ( "github.com/emersion/go-webdav/internal" ) -const namespace = "urn:ietf:params:xml:ns:caldav" +const ( + namespace = "urn:ietf:params:xml:ns:caldav" + appleNamespace = "http://apple.com/ns/ical/" +) var ( calendarHomeSetName = xml.Name{namespace, "calendar-home-set"} calendarDescriptionName = xml.Name{namespace, "calendar-description"} + calendarTimezoneName = xml.Name{namespace, "calendar-timezone"} supportedCalendarDataName = xml.Name{namespace, "supported-calendar-data"} supportedCalendarComponentSetName = xml.Name{namespace, "supported-calendar-component-set"} maxResourceSizeName = xml.Name{namespace, "max-resource-size"} + calendarColorName = xml.Name{appleNamespace, "calendar-color"} + calendarQueryName = xml.Name{namespace, "calendar-query"} calendarMultigetName = xml.Name{namespace, "calendar-multiget"} @@ -41,6 +47,17 @@ type calendarDescription struct { Description string `xml:",chardata"` } +// https://tools.ietf.org/html/rfc4791#section-5.2.2 +type calendarTimezone struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav calendar-timezone"` + Timezone string `xml:",chardata"` +} + +type calendarColor struct { + XMLName xml.Name `xml:"http://apple.com/ns/ical/ calendar-color"` + Color string `xml:",chardata"` +} + // https://tools.ietf.org/html/rfc4791#section-5.2.4 type supportedCalendarData struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav supported-calendar-data"` @@ -237,8 +254,19 @@ func (r *reportReq) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { } type mkcolReq struct { - XMLName xml.Name `xml:"DAV: mkcol"` - ResourceType internal.ResourceType `xml:"set>prop>resourcetype"` - DisplayName string `xml:"set>prop>displayname"` - // TODO this could theoretically contain all addressbook properties? + XMLName xml.Name `xml:"DAV: mkcol"` + Set mkcolSet `xml:"DAV: set"` +} + +type mkcolSet struct { + Prop mkcolProp `xml:"DAV: prop"` +} + +type mkcolProp struct { + ResourceType internal.ResourceType `xml:"DAV: resourcetype"` + DisplayName string `xml:"DAV: displayname"` + CalendarDescription string `xml:"urn:ietf:params:xml:ns:caldav calendar-description"` + CalendarTimezone string `xml:"urn:ietf:params:xml:ns:caldav calendar-timezone"` + CalendarColor string `xml:"http://apple.com/ns/ical/ calendar-color"` + SupportedCalendarComponentSet supportedCalendarComponentSet `xml:"urn:ietf:params:xml:ns:caldav supported-calendar-component-set"` } diff --git a/caldav/server.go b/caldav/server.go index 0606c7b..20716ce 100644 --- a/caldav/server.go +++ b/caldav/server.go @@ -559,13 +559,29 @@ func (b *backend) propFindCalendar(ctx context.Context, propfind *internal.PropF Description: cal.Description, }) } + if cal.Color != "" { + props[calendarColorName] = internal.PropFindValue(&calendarColor{ + Color: cal.Color, + }) + } + if cal.Timezone != nil { + props[calendarTimezoneName] = func(*internal.RawXMLValue) (interface{}, error) { + var buf bytes.Buffer + if err := ical.NewEncoder(&buf).Encode(cal.Timezone); err != nil { + return nil, err + } + return &calendarTimezone{ + Timezone: buf.String(), + }, nil + } + } if cal.MaxResourceSize > 0 { props[maxResourceSizeName] = internal.PropFindValue(&maxResourceSize{ Size: cal.MaxResourceSize, }) } - // TODO: CALDAV:calendar-timezone, CALDAV:supported-calendar-component-set, CALDAV:min-date-time, CALDAV:max-date-time, CALDAV:max-instances, CALDAV:max-attendees-per-instance + // TODO: CALDAV:min-date-time, CALDAV:max-date-time, CALDAV:max-instances, CALDAV:max-attendees-per-instance return internal.NewPropFindResponse(cal.Path, propfind, props) } @@ -724,16 +740,42 @@ func (b *backend) Mkcol(r *http.Request) error { return internal.HTTPErrorf(http.StatusBadRequest, "carddav: error parsing mkcol request: %s", err.Error()) } - if !m.ResourceType.Is(internal.CollectionName) || !m.ResourceType.Is(calendarName) { + prop := m.Set.Prop + if !prop.ResourceType.Is(internal.CollectionName) || !prop.ResourceType.Is(calendarName) { return internal.HTTPErrorf(http.StatusBadRequest, "carddav: unexpected resource type") } - cal.Name = m.DisplayName - // TODO ... + cal.Name = prop.DisplayName + cal.Description = prop.CalendarDescription + cal.Color = strings.TrimSpace(prop.CalendarColor) + + if s := strings.TrimSpace(prop.CalendarTimezone); s != "" { + tz, err := decodeCalendarTimezone(s) + if err != nil { + return err + } + cal.Timezone = tz + } + + cal.SupportedComponentSet = make([]string, len(prop.SupportedCalendarComponentSet.Comp)) + for i, v := range prop.SupportedCalendarComponentSet.Comp { + cal.SupportedComponentSet[i] = v.Name + } } return b.Backend.CreateCalendar(r.Context(), &cal) } +func decodeCalendarTimezone(s string) (*ical.Calendar, error) { + cal, err := ical.NewDecoder(strings.NewReader(s)).Decode() + if err != nil { + return nil, NewPreconditionError(PreconditionValidCalendarData) + } + if len(cal.Children) != 1 || cal.Children[0].Name != ical.CompTimezone { + return nil, NewPreconditionError(PreconditionValidCalendarData) + } + return cal, nil +} + func (b *backend) Copy(r *http.Request, dest *internal.Href, recursive, overwrite bool) (created bool, err error) { return false, internal.HTTPErrorf(http.StatusNotImplemented, "caldav: Copy not implemented") } diff --git a/caldav/server_test.go b/caldav/server_test.go index 594b87b..8fa5811 100644 --- a/caldav/server_test.go +++ b/caldav/server_test.go @@ -2,6 +2,7 @@ package caldav import ( "context" + "encoding/xml" "fmt" "io" "io/ioutil" @@ -11,6 +12,7 @@ import ( "time" "github.com/emersion/go-ical" + "github.com/emersion/go-webdav/internal" ) var propFindSupportedCalendarComponentRequest = ` @@ -22,9 +24,9 @@ var propFindSupportedCalendarComponentRequest = ` ` var testPropFindSupportedCalendarComponentCases = map[*Calendar][]string{ - &Calendar{Path: "/user/calendars/cal"}: []string{"VEVENT"}, - &Calendar{Path: "/user/calendars/cal", SupportedComponentSet: []string{"VTODO"}}: []string{"VTODO"}, - &Calendar{Path: "/user/calendars/cal", SupportedComponentSet: []string{"VEVENT", "VTODO"}}: []string{"VEVENT", "VTODO"}, + {Path: "/user/calendars/cal"}: {"VEVENT"}, + {Path: "/user/calendars/cal", SupportedComponentSet: []string{"VTODO"}}: {"VTODO"}, + {Path: "/user/calendars/cal", SupportedComponentSet: []string{"VEVENT", "VTODO"}}: {"VEVENT", "VTODO"}, } func TestPropFindSupportedCalendarComponent(t *testing.T) { @@ -33,7 +35,7 @@ func TestPropFindSupportedCalendarComponent(t *testing.T) { req.Body = io.NopCloser(strings.NewReader(propFindSupportedCalendarComponentRequest)) req.Header.Set("Content-Type", "application/xml") w := httptest.NewRecorder() - handler := Handler{Backend: testBackend{calendars: []Calendar{*calendar}}} + handler := Handler{Backend: &testBackend{calendars: []Calendar{*calendar}}} handler.ServeHTTP(w, req) res := w.Result() @@ -52,6 +54,141 @@ func TestPropFindSupportedCalendarComponent(t *testing.T) { } } +var propFindCalendarRequest = ` + + + + + + + + +` + +var calendarTimezoneData = `BEGIN:VCALENDAR +PRODID:-//Example Corp.//CalDAV Client//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:US-Eastern +LAST-MODIFIED:19870101T000000Z +BEGIN:STANDARD +DTSTART:19671029T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +TZNAME:Eastern Standard Time (US & Canada) +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19870405T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +TZNAME:Eastern Daylight Time (US & Canada) +END:DAYLIGHT +END:VTIMEZONE +END:VCALENDAR +` + +func TestPropFindCalendar(t *testing.T) { + timezone, err := decodeCalendarTimezone(calendarTimezoneData) + if err != nil { + t.Fatalf("Unexpected error in decodeCalendarTimezone: %s", err) + } + + calendar := Calendar{ + Path: "/user/calendars/cal", + Name: "Test Calendar", + Description: "This is a test calendar", + Timezone: timezone, + Color: "#DEADBEEF", + } + + req := httptest.NewRequest("PROPFIND", calendar.Path, nil) + req.Body = io.NopCloser(strings.NewReader(propFindCalendarRequest)) + req.Header.Set("Content-Type", "application/xml") + w := httptest.NewRecorder() + handler := Handler{Backend: &testBackend{calendars: []Calendar{calendar}}} + handler.ServeHTTP(w, req) + + resp := w.Result() + + var ms internal.MultiStatus + err = xml.NewDecoder(resp.Body).Decode(&ms) + if err != nil { + t.Fatalf("Unexpcted error in xml.NewDecoder: %s", err) + } + if len(ms.Responses) != 1 { + t.Fatalf("Found %d multi status responses, expected 1", len(ms.Responses)) + } + if len(ms.Responses[0].PropStats) != 1 { + t.Fatalf("Found %d prop stats, expected 1", len(ms.Responses[0].PropStats)) + } + if ms.Responses[0].PropStats[0].Status.Code != 200 { + t.Fatalf("Received %d prop stat status, expected 200", ms.Responses[0].PropStats[0].Status.Code) + } + if len(ms.Responses[0].PropStats[0].Prop.Raw) != 4 { + t.Fatalf("Found %d props, expected 4", len(ms.Responses[0].PropStats[0].Prop.Raw)) + } + + rawDisplayName := ms.Responses[0].PropStats[0].Prop.Get(internal.DisplayNameName) + rawCalendarDescription := ms.Responses[0].PropStats[0].Prop.Get(calendarDescriptionName) + rawTimezone := ms.Responses[0].PropStats[0].Prop.Get(calendarTimezoneName) + rawColor := ms.Responses[0].PropStats[0].Prop.Get(calendarColorName) + if rawDisplayName == nil { + t.Fatal("Got unexpected nil rawDisplayName") + } + if rawCalendarDescription == nil { + t.Fatal("Got unexpected nil rawCalendarDescription") + } + if rawTimezone == nil { + t.Fatal("Got unexpected nil rawTimezone") + } + if rawColor == nil { + t.Fatal("Got unexpected nil rawColor") + } + + v0 := internal.DisplayName{} + err = rawDisplayName.Decode(&v0) + if err != nil { + t.Fatalf("Unexpcted error in rawDisplayName.Decode: %s", err) + } + if calendar.Name != v0.Name { + t.Fatalf("Calendar name is '%s', expected '%s'", calendar.Name, v0.Name) + } + + v1 := calendarDescription{} + err = rawCalendarDescription.Decode(&v1) + if err != nil { + t.Fatalf("Unexpcted error in rawCalendarDescription.Decode: %s", err) + } + if calendar.Description != v1.Description { + t.Fatalf("Calendar description is '%s', expected '%s'", calendar.Description, v1.Description) + } + + v2 := calendarTimezone{} + err = rawTimezone.Decode(&v2) + if err != nil { + t.Fatalf("Unexpected error in rawTimezone.Decode: %s", err) + } + gotTimezone, err := decodeCalendarTimezone(v2.Timezone) + if err != nil { + t.Fatalf("Calendar timezone is not a valid iCalendar object: %s", err) + } + if tzid := gotTimezone.Children[0].Props.Get(ical.PropTimezoneID); tzid == nil || tzid.Value != "US-Eastern" { + t.Fatalf("Calendar timezone is '%v', expected 'US-Eastern'", tzid) + } + + v3 := calendarColor{} + err = rawColor.Decode(&v3) + if err != nil { + t.Fatalf("Unexpcted error in rawColor.Decode: %s", err) + } + if calendar.Color != v3.Color { + t.Fatalf("Calendar color is '%s', expected '%s'", calendar.Color, v3.Color) + } +} + var propFindUserPrincipal = ` @@ -68,7 +205,7 @@ func TestPropFindRoot(t *testing.T) { req.Header.Set("Content-Type", "application/xml") w := httptest.NewRecorder() calendar := &Calendar{} - handler := Handler{Backend: testBackend{calendars: []Calendar{*calendar}}} + handler := Handler{Backend: &testBackend{calendars: []Calendar{*calendar}}} handler.ServeHTTP(w, req) res := w.Result() @@ -96,7 +233,7 @@ var reportCalendarData = ` func TestMultiCalendarBackend(t *testing.T) { calendarB := Calendar{Path: "/user/calendars/b", SupportedComponentSet: []string{"VTODO"}} calendars := []Calendar{ - Calendar{Path: "/user/calendars/a"}, + {Path: "/user/calendars/a"}, calendarB, } eventSummary := "This is a todo" @@ -118,10 +255,10 @@ func TestMultiCalendarBackend(t *testing.T) { req := httptest.NewRequest("PROPFIND", "/user/calendars/", strings.NewReader(propFindUserPrincipal)) req.Header.Set("Content-Type", "application/xml") w := httptest.NewRecorder() - handler := Handler{Backend: testBackend{ + handler := Handler{Backend: &testBackend{ calendars: calendars, objectMap: map[string][]CalendarObject{ - calendarB.Path: []CalendarObject{object}, + calendarB.Path: {object}, }, }} handler.ServeHTTP(w, req) @@ -177,41 +314,378 @@ func TestMultiCalendarBackend(t *testing.T) { } } +var mkcolRequestData = ` + + + + + + + + + Test calendar + A calendar for testing + #009688FF + + + + + + + + + + + +` + +func TestCreateCalendar(t *testing.T) { + tb := testBackend{ + calendars: nil, + objectMap: nil, + } + b := backend{ + Backend: &tb, + Prefix: "/dav", + } + req := httptest.NewRequest("MKCOL", "/dav/calendars/user0/test-calendar", strings.NewReader(mkcolRequestData)) + req.Header.Set("Content-Type", "application/xml") + + err := b.Mkcol(req) + if err != nil { + t.Fatalf("Unexpcted error in Mkcol: %s", err) + } + if len(tb.calendars) != 1 { + t.Fatalf("Found %d calendars, expected 1", len(tb.calendars)) + } + c := tb.calendars[0] + if c.Name != "Test calendar" { + t.Fatalf("Calendar name is '%s', expected 'Test calendar'", c.Name) + } + expectedPath := "/dav/calendars/user0/test-calendar" + if c.Path != expectedPath { + t.Fatalf("Calendar path is '%s', expected '%s'", c.Path, expectedPath) + } + expectedDescription := "A calendar for testing" + if c.Description != expectedDescription { + t.Fatalf("Calendar description is '%s', expected '%s'", c.Description, expectedDescription) + } + expectedColor := "#009688FF" + if c.Color != expectedColor { + t.Fatalf("Calendar color is '%s', expected '%s'", c.Color, expectedColor) + } + if c.Timezone == nil { + t.Fatal("Got unexpected nil calendar timezone") + } + if n := len(c.Timezone.Children); n != 1 { + t.Fatalf("Found %d calendar timezone components, expected 1", n) + } + if name := c.Timezone.Children[0].Name; name != ical.CompTimezone { + t.Fatalf("Calendar timezone component is '%s', expected '%s'", name, ical.CompTimezone) + } + expectedTimezone := "Europe/Berlin" + if tzid := c.Timezone.Children[0].Props.Get(ical.PropTimezoneID); tzid == nil || tzid.Value != expectedTimezone { + t.Fatalf("Calendar timezone is '%v', expected '%s'", tzid, expectedTimezone) + } + if len(c.SupportedComponentSet) != 3 { + t.Fatalf("Found %d SupportedComponentSet, expected 3", len(c.SupportedComponentSet)) + } + if c.SupportedComponentSet[0] != "VEVENT" { + t.Fatalf("Calendar 0.SupportedComponentSet is '%s', expected '%s'", c.SupportedComponentSet[0], "VEVENT") + } + if c.SupportedComponentSet[1] != "VTODO" { + t.Fatalf("Calendar 1.SupportedComponentSet is '%s', expected '%s'", c.SupportedComponentSet[1], "VTODO") + } + if c.SupportedComponentSet[2] != "VJOURNAL" { + t.Fatalf("Calendar 2.SupportedComponentSet is '%s', expected '%s'", c.SupportedComponentSet[2], "VJOURNAL") + } +} + +var mkcolRequestDataMinimalBody = ` + + + + + + + + + Test calendar + + +` + +func TestCreateCalendarMinimalBody(t *testing.T) { + tb := testBackend{ + calendars: nil, + objectMap: nil, + } + b := backend{ + Backend: &tb, + Prefix: "/dav", + } + req := httptest.NewRequest("MKCOL", "/dav/calendars/user0/test-calendar", strings.NewReader(mkcolRequestDataMinimalBody)) + req.Header.Set("Content-Type", "application/xml") + + err := b.Mkcol(req) + if err != nil { + t.Fatalf("Unexpcted error in Mkcol: %s", err) + } + if len(tb.calendars) != 1 { + t.Fatalf("Found %d calendars, expected 1", len(tb.calendars)) + } + c := tb.calendars[0] + if c.Name != "Test calendar" { + t.Fatalf("Calendar name is '%s', expected 'Test calendar'", c.Name) + } + expectedPath := "/dav/calendars/user0/test-calendar" + if c.Path != expectedPath { + t.Fatalf("Calendar path is '%s', expected '%s'", c.Path, expectedPath) + } + expectedDescription := "" + if c.Description != expectedDescription { + t.Fatalf("Calendar description is '%s', expected '%s'", c.Description, expectedDescription) + } + expectedColor := "" + if c.Color != expectedColor { + t.Fatalf("Calendar color is '%s', expected '%s'", c.Color, expectedColor) + } + if c.Timezone != nil { + t.Fatalf("Calendar timezone is '%v', expected none", c.Timezone) + } + if len(c.SupportedComponentSet) != 0 { + t.Fatalf("Found %d SupportedComponentSet, expected 0", len(c.SupportedComponentSet)) + } +} + +var mkcolRequestDataForeignColor = ` + + + + + + + + + Test calendar + #009688FF + + +` + +func TestCreateCalendarForeignColor(t *testing.T) { + tb := testBackend{} + b := backend{ + Backend: &tb, + Prefix: "/dav", + } + req := httptest.NewRequest("MKCOL", "/dav/calendars/user0/test-calendar", strings.NewReader(mkcolRequestDataForeignColor)) + req.Header.Set("Content-Type", "application/xml") + + if err := b.Mkcol(req); err != nil { + t.Fatalf("Unexpected error in Mkcol: %s", err) + } + if len(tb.calendars) != 1 { + t.Fatalf("Found %d calendars, expected 1", len(tb.calendars)) + } + if color := tb.calendars[0].Color; color != "" { + t.Fatalf("Calendar color is '%s', expected none", color) + } +} + +var invalidCalendarTimezoneCases = []struct { + name string + data string +}{ + {"not an iCalendar object", "Europe/Berlin"}, + {"no component", "BEGIN:VCALENDAR\nEND:VCALENDAR\n"}, + {"another component", `BEGIN:VCALENDAR +BEGIN:VEVENT +UID:test +END:VEVENT +END:VCALENDAR +`}, + {"two VTIMEZONE components", `BEGIN:VCALENDAR +BEGIN:VTIMEZONE +TZID:US-Eastern +END:VTIMEZONE +BEGIN:VTIMEZONE +TZID:Europe/Berlin +END:VTIMEZONE +END:VCALENDAR +`}, +} + +func TestDecodeCalendarTimezone(t *testing.T) { + if _, err := decodeCalendarTimezone(calendarTimezoneData); err != nil { + t.Errorf("Unexpected error for a valid calendar timezone: %s", err) + } + + for _, tc := range invalidCalendarTimezoneCases { + t.Run(tc.name, func(t *testing.T) { + if _, err := decodeCalendarTimezone(tc.data); err == nil { + t.Error("Invalid calendar timezone accepted") + } + }) + } +} + type testBackend struct { calendars []Calendar objectMap map[string][]CalendarObject } -func (t testBackend) CreateCalendar(ctx context.Context, calendar *Calendar) error { +func (t *testBackend) CreateCalendar(ctx context.Context, calendar *Calendar) error { + t.calendars = append(t.calendars, *calendar) return nil } -func (t testBackend) ListCalendars(ctx context.Context) ([]Calendar, error) { +func (t *testBackend) ListCalendars(ctx context.Context) ([]Calendar, error) { return t.calendars, nil } -func (t testBackend) GetCalendar(ctx context.Context, path string) (*Calendar, error) { +func (t *testBackend) GetCalendar(ctx context.Context, path string) (*Calendar, error) { for _, cal := range t.calendars { if cal.Path == path { return &cal, nil } } - return nil, fmt.Errorf("Calendar for path: %s not found", path) + return nil, fmt.Errorf("calendar for path: %s not found", path) } -func (t testBackend) CalendarHomeSetPath(ctx context.Context) (string, error) { +func (t *testBackend) CalendarHomeSetPath(ctx context.Context) (string, error) { return "/user/calendars/", nil } -func (t testBackend) CurrentUserPrincipal(ctx context.Context) (string, error) { +func (t *testBackend) CurrentUserPrincipal(ctx context.Context) (string, error) { return "/user/", nil } -func (t testBackend) DeleteCalendarObject(ctx context.Context, path string) error { +func (t *testBackend) DeleteCalendarObject(ctx context.Context, path string) error { return nil } -func (t testBackend) GetCalendarObject(ctx context.Context, path string, req *CalendarCompRequest) (*CalendarObject, error) { +func (t *testBackend) GetCalendarObject(ctx context.Context, path string, req *CalendarCompRequest) (*CalendarObject, error) { for _, objs := range t.objectMap { for _, obj := range objs { if obj.Path == path { @@ -219,17 +693,17 @@ func (t testBackend) GetCalendarObject(ctx context.Context, path string, req *Ca } } } - return nil, fmt.Errorf("Couldn't find calendar object at: %s", path) + return nil, fmt.Errorf("couldn't find calendar object at: %s", path) } -func (t testBackend) PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *PutCalendarObjectOptions) (*CalendarObject, error) { +func (t *testBackend) PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *PutCalendarObjectOptions) (*CalendarObject, error) { return nil, nil } -func (t testBackend) ListCalendarObjects(ctx context.Context, path string, req *CalendarCompRequest) ([]CalendarObject, error) { +func (t *testBackend) ListCalendarObjects(ctx context.Context, path string, req *CalendarCompRequest) ([]CalendarObject, error) { return t.objectMap[path], nil } -func (t testBackend) QueryCalendarObjects(ctx context.Context, path string, query *CalendarQuery) ([]CalendarObject, error) { +func (t *testBackend) QueryCalendarObjects(ctx context.Context, path string, query *CalendarQuery) ([]CalendarObject, error) { return nil, nil }