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
}