From d4347f00f673bfc13ff66977090cb35f0403c979 Mon Sep 17 00:00:00 2001 From: Vlad Yarotsky Date: Mon, 14 Jul 2025 14:57:55 -0700 Subject: [PATCH] fix(caldav): make CalDAV REPORT request properly respond with VTODO objects (#1116) --- pkg/routes/caldav/listStorageProvider.go | 2 +- pkg/webtests/caldav_test.go | 76 ++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/pkg/routes/caldav/listStorageProvider.go b/pkg/routes/caldav/listStorageProvider.go index 82843c783..5c23d3012 100644 --- a/pkg/routes/caldav/listStorageProvider.go +++ b/pkg/routes/caldav/listStorageProvider.go @@ -600,7 +600,7 @@ func (vlra *VikunjaProjectResourceAdapter) CalculateEtag() string { // GetContent returns the content string of a resource (a task in our case) func (vlra *VikunjaProjectResourceAdapter) GetContent() string { - if vlra.project != nil && vlra.project.Tasks != nil { + if vlra.project != nil && vlra.projectTasks != nil { return caldav.GetCaldavTodosForTasks(vlra.project, vlra.projectTasks) } diff --git a/pkg/webtests/caldav_test.go b/pkg/webtests/caldav_test.go index c255bb4da..7bdefcfa6 100644 --- a/pkg/webtests/caldav_test.go +++ b/pkg/webtests/caldav_test.go @@ -17,11 +17,14 @@ package webtests import ( + "encoding/xml" "net/http" + "strings" "testing" "code.vikunja.io/api/pkg/routes/caldav" + ics "github.com/arran4/golang-ical" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -318,3 +321,76 @@ END:VCALENDAR` assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task-another-list") }) } + +func TestCaldavProjectReport(t *testing.T) { + t.Run("REPORT calendar-query returns all tasks", func(t *testing.T) { + e, _ := setupTestEnv() + + // CalDAV REPORT request for calendar-query + reportBody := ` + + + + + + + + + + +` + + rec, err := newCaldavTestRequestWithUser(t, e, "REPORT", caldav.ProjectHandler, &testuser15, reportBody, nil, map[string]string{"project": "36"}) + require.NoError(t, err) + assert.Equal(t, 207, rec.Result().StatusCode) // Multi-Status response + + responseBody := rec.Body.String() + + assert.Contains(t, responseBody, "multistatus") + assert.Contains(t, responseBody, "response") + assert.Contains(t, responseBody, "href") + assert.Contains(t, responseBody, "propstat") + + // Parse XML to verify structure + type Multistatus struct { + Response []struct { + Href string `xml:"href"` + Propstat struct { + Prop struct { + Getetag string `xml:"getetag"` + CalendarData string `xml:"calendar-data"` + } `xml:"prop"` + } `xml:"propstat"` + } `xml:"response"` + } + + var multistatus Multistatus + err = xml.Unmarshal([]byte(responseBody), &multistatus) + require.NoError(t, err) + + assert.Len(t, multistatus.Response, 5, "Should have all tasks from the project") + + for i, response := range multistatus.Response { + assert.NotEmpty(t, response.Href, "Response %d should have an href", i) + assert.NotEmpty(t, response.Propstat.Prop.CalendarData, "Response %d should have calendar-data", i) + assert.NotEmpty(t, response.Propstat.Prop.Getetag, "Response %d should have an ETag", i) + + calendarData := response.Propstat.Prop.CalendarData + + cal, err := ics.ParseCalendar(strings.NewReader(calendarData)) + require.NoError(t, err, "Response %d should contain valid iCalendar data", i) + + require.Len(t, cal.Components, 1, "Response %d should contain exactly one VTODO component") + + component := cal.Components[0] + require.IsType(t, &ics.VTodo{}, component) + + vtodo, _ := component.(*ics.VTodo) + uid := vtodo.GetProperty(ics.ComponentPropertyUniqueId) + assert.NotEmpty(t, uid.Value, "Response %d VTODO UID should not be empty", i) + + summary := vtodo.GetProperty(ics.ComponentPropertySummary) + assert.NotEmpty(t, summary.Value, "Response %d VTODO SUMMARY should not be empty", i) + } + }) +}