diff --git a/caldav/server.go b/caldav/server.go index bb86925..c67b3cb 100644 --- a/caldav/server.go +++ b/caldav/server.go @@ -17,8 +17,6 @@ import ( "github.com/emersion/go-webdav/internal" ) -// TODO: add support for multiple calendars - // TODO if nothing more Caldav-specific needs to be added this should be merged with carddav.PutAddressObjectOptions type PutCalendarObjectOptions struct { // IfNoneMatch indicates that the client does not want to overwrite @@ -32,9 +30,10 @@ type PutCalendarObjectOptions struct { // Backend is a CalDAV server backend. type Backend interface { CalendarHomeSetPath(ctx context.Context) (string, error) - Calendar(ctx context.Context) (*Calendar, error) + ListCalendars(ctx context.Context) ([]Calendar, error) + GetCalendar(ctx context.Context, path string) (*Calendar, error) GetCalendarObject(ctx context.Context, path string, req *CalendarCompRequest) (*CalendarObject, error) - ListCalendarObjects(ctx context.Context, req *CalendarCompRequest) ([]CalendarObject, error) + ListCalendarObjects(ctx context.Context, path string, req *CalendarCompRequest) ([]CalendarObject, error) QueryCalendarObjects(ctx context.Context, query *CalendarQuery) ([]CalendarObject, error) PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *PutCalendarObjectOptions) (loc string, err error) DeleteCalendarObject(ctx context.Context, path string) error @@ -424,24 +423,21 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i } } case resourceTypeCalendar: - // TODO for multiple calendars, look through all of them - ab, err := b.Backend.Calendar(r.Context()) + ab, err := b.Backend.GetCalendar(r.Context(), r.URL.Path) if err != nil { return nil, err } - if r.URL.Path == ab.Path { - resp, err := b.propFindCalendar(r.Context(), propfind, ab) + resp, err := b.propFindCalendar(r.Context(), propfind, ab) + if err != nil { + return nil, err + } + resps = append(resps, *resp) + if depth != internal.DepthZero { + resps_, err := b.propFindAllCalendarObjects(r.Context(), propfind, ab) if err != nil { return nil, err } - resps = append(resps, *resp) - if depth != internal.DepthZero { - resps_, err := b.propFindAllCalendarObjects(r.Context(), propfind, ab) - if err != nil { - return nil, err - } - resps = append(resps, resps_...) - } + resps = append(resps, resps_...) } case resourceTypeCalendarObject: ao, err := b.Backend.GetCalendarObject(r.Context(), r.URL.Path, &dataReq) @@ -580,22 +576,20 @@ func (b *backend) propFindCalendar(ctx context.Context, propfind *internal.PropF } func (b *backend) propFindAllCalendars(ctx context.Context, propfind *internal.PropFind, recurse bool) ([]internal.Response, error) { - // TODO iterate over all calendars once having multiple is supported - ab, err := b.Backend.Calendar(ctx) + abs, err := b.Backend.ListCalendars(ctx) if err != nil { return nil, err } - abs := []*Calendar{ab} var resps []internal.Response for _, ab := range abs { - resp, err := b.propFindCalendar(ctx, propfind, ab) + resp, err := b.propFindCalendar(ctx, propfind, &ab) if err != nil { return nil, err } resps = append(resps, *resp) if recurse { - resps_, err := b.propFindAllCalendarObjects(ctx, propfind, ab) + resps_, err := b.propFindAllCalendarObjects(ctx, propfind, &ab) if err != nil { return nil, err } @@ -650,7 +644,7 @@ func (b *backend) propFindCalendarObject(ctx context.Context, propfind *internal func (b *backend) propFindAllCalendarObjects(ctx context.Context, propfind *internal.PropFind, cal *Calendar) ([]internal.Response, error) { var dataReq CalendarCompRequest - aos, err := b.Backend.ListCalendarObjects(ctx, &dataReq) + aos, err := b.Backend.ListCalendarObjects(ctx, cal.Path, &dataReq) if err != nil { return nil, err } diff --git a/caldav/server_test.go b/caldav/server_test.go index c063871..f6e5f40 100644 --- a/caldav/server_test.go +++ b/caldav/server_test.go @@ -2,11 +2,13 @@ package caldav import ( "context" + "fmt" "io" "io/ioutil" "net/http/httptest" "strings" "testing" + "time" "github.com/emersion/go-ical" ) @@ -31,7 +33,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{calendar: calendar}} + handler := Handler{Backend: testBackend{calendars: []Calendar{*calendar}}} handler.ServeHTTP(w, req) res := w.Result() @@ -66,7 +68,7 @@ func TestPropFindRoot(t *testing.T) { req.Header.Set("Content-Type", "application/xml") w := httptest.NewRecorder() calendar := &Calendar{} - handler := Handler{Backend: testBackend{calendar: calendar}} + handler := Handler{Backend: testBackend{calendars: []Calendar{*calendar}}} handler.ServeHTTP(w, req) res := w.Result() @@ -81,12 +83,116 @@ func TestPropFindRoot(t *testing.T) { } } -type testBackend struct { - calendar *Calendar +var reportCalendarData = ` + + + + + + %s + +` + +func TestMultiCalendarBackend(t *testing.T) { + calendarB := Calendar{Path: "/user/calendars/b", SupportedComponentSet: []string{"VTODO"}} + calendars := []Calendar{ + Calendar{Path: "/user/calendars/a"}, + calendarB, + } + eventSummary := "This is a todo" + event := ical.NewEvent() + event.Name = ical.CompToDo + event.Props.SetText(ical.PropUID, "46bbf47a-1861-41a3-ae06-8d8268c6d41e") + event.Props.SetDateTime(ical.PropDateTimeStamp, time.Now()) + event.Props.SetText(ical.PropSummary, eventSummary) + cal := ical.NewCalendar() + cal.Props.SetText(ical.PropVersion, "2.0") + cal.Props.SetText(ical.PropProductID, "-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN") + cal.Children = []*ical.Component{ + event.Component, + } + object := CalendarObject{ + Path: "/user/calendars/b/test.ics", + Data: cal, + } + req := httptest.NewRequest("PROPFIND", "/user/calendars/", strings.NewReader(propFindUserPrincipal)) + req.Header.Set("Content-Type", "application/xml") + w := httptest.NewRecorder() + handler := Handler{Backend: testBackend{ + calendars: calendars, + objectMap: map[string][]CalendarObject{ + calendarB.Path: []CalendarObject{object}, + }, + }} + handler.ServeHTTP(w, req) + + res := w.Result() + defer res.Body.Close() + data, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Error(err) + } + resp := string(data) + for _, calendar := range calendars { + if !strings.Contains(resp, fmt.Sprintf(`%s`, calendar.Path)) { + t.Errorf("Calendar: %v not returned in PROPFIND, response:\n%s", calendar, resp) + } + } + + // Now do a PROPFIND for the last calendar + req = httptest.NewRequest("PROPFIND", calendarB.Path, strings.NewReader(propFindSupportedCalendarComponentRequest)) + req.Header.Set("Content-Type", "application/xml") + w = httptest.NewRecorder() + handler.ServeHTTP(w, req) + + res = w.Result() + defer res.Body.Close() + data, err = ioutil.ReadAll(res.Body) + if err != nil { + t.Error(err) + } + resp = string(data) + if !strings.Contains(resp, "VTODO") { + t.Errorf("Expected component: VTODO not found in response:\n%v", resp) + } + if !strings.Contains(resp, object.Path) { + t.Errorf("Expected calendar object: %v not found in response:\n%v", object, resp) + } + + // Now do a REPORT to get the actual data for the event + req = httptest.NewRequest("REPORT", calendarB.Path, strings.NewReader(fmt.Sprintf(reportCalendarData, object.Path))) + req.Header.Set("Content-Type", "application/xml") + w = httptest.NewRecorder() + handler.ServeHTTP(w, req) + + res = w.Result() + defer res.Body.Close() + data, err = ioutil.ReadAll(res.Body) + if err != nil { + t.Error(err) + } + resp = string(data) + if !strings.Contains(resp, fmt.Sprintf("SUMMARY:%s", eventSummary)) { + t.Errorf("ICAL content not properly returned in response:\n%v", resp) + } } -func (t testBackend) Calendar(ctx context.Context) (*Calendar, error) { - return t.calendar, nil +type testBackend struct { + calendars []Calendar + objectMap map[string][]CalendarObject +} + +func (t testBackend) ListCalendars(ctx context.Context) ([]Calendar, error) { + return t.calendars, nil +} + +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) } func (t testBackend) CalendarHomeSetPath(ctx context.Context) (string, error) { @@ -102,15 +208,22 @@ func (t testBackend) DeleteCalendarObject(ctx context.Context, path string) erro } func (t testBackend) GetCalendarObject(ctx context.Context, path string, req *CalendarCompRequest) (*CalendarObject, error) { - return nil, nil + for _, objs := range t.objectMap { + for _, obj := range objs { + if obj.Path == path { + return &obj, nil + } + } + } + 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) (string, error) { return "", nil } -func (t testBackend) ListCalendarObjects(ctx context.Context, req *CalendarCompRequest) ([]CalendarObject, error) { - return nil, nil +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, query *CalendarQuery) ([]CalendarObject, error) {