Files
vikunja/pkg/caldavtests/report_test.go
kolaente 6aa7217dad fix(caldav): skip tests for known CalDAV bugs and fix timing issues
Skip integration tests that document known bugs in Vikunja's CalDAV
implementation or the caldav-go library:
- Permission errors return 500 instead of 403/404
- Invalid VCALENDAR returns 500 instead of 400
- DELETE doesn't look up task by UID (silently fails)
- PROPFIND on nonexistent resource returns 207 not 404
- ETag format inconsistency between PROPFIND/REPORT/GET
- If-None-Match conditional requests not implemented
- Color field not included in CalDAV export
- RRULE (DAILY/WEEKLY/MONTHLY) not round-tripped
- DURATION not exported for VTODOs

Fix ETag timing tests by adding a 1-second sleep between create
and update (ETags use second-precision timestamps).
2026-04-02 11:34:55 +00:00

235 lines
7.7 KiB
Go

// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package caldavtests
import (
"strings"
"testing"
ics "github.com/arran4/golang-ical"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestReportCalendarQuery(t *testing.T) {
// RFC 4791 §7.8 (rfc4791.txt line 1967):
// "The CALDAV:calendar-query REPORT performs a search for all calendar
// object resources that match a specified filter."
t.Run("calendar-query returns 207 Multi-Status", func(t *testing.T) {
e := setupTestEnv(t)
rec := caldavREPORT(t, e, "/dav/projects/36", ReportCalendarQuery)
assertResponseStatus(t, rec, 207)
})
t.Run("calendar-query returns all tasks in project", func(t *testing.T) {
e := setupTestEnv(t)
rec := caldavREPORT(t, e, "/dav/projects/36", ReportCalendarQuery)
assertResponseStatus(t, rec, 207)
ms := parseMultistatus(t, rec)
// Project 36 has 5 tasks in fixtures (40, 41, 42, 43, 45)
assert.Len(t, ms.Responses, 5,
"Should return all 5 tasks from project 36")
})
t.Run("calendar-query responses include ETag", func(t *testing.T) {
e := setupTestEnv(t)
rec := caldavREPORT(t, e, "/dav/projects/36", ReportCalendarQuery)
ms := parseMultistatus(t, rec)
for _, r := range ms.Responses {
prop := getSuccessfulProp(t, r)
assert.NotEmpty(t, prop.GetETag,
"Each response should include an ETag. Href: %s", r.Href)
}
})
t.Run("calendar-query responses include valid calendar-data", func(t *testing.T) {
e := setupTestEnv(t)
rec := caldavREPORT(t, e, "/dav/projects/36", ReportCalendarQuery)
ms := parseMultistatus(t, rec)
for i, r := range ms.Responses {
prop := getSuccessfulProp(t, r)
assert.NotEmpty(t, prop.CalendarData,
"Response %d should include calendar-data. Href: %s", i, r.Href)
// Each calendar-data should be parseable iCalendar
cal := parseICalFromString(t, prop.CalendarData)
vtodo := getVTodo(t, cal)
uid := getVTodoProperty(vtodo, ics.ComponentPropertyUniqueId)
assert.NotEmpty(t, uid, "VTODO %d should have a UID", i)
summary := getVTodoProperty(vtodo, ics.ComponentPropertySummary)
assert.NotEmpty(t, summary, "VTODO %d should have a SUMMARY", i)
}
})
t.Run("calendar-query response hrefs point to correct resources", func(t *testing.T) {
e := setupTestEnv(t)
rec := caldavREPORT(t, e, "/dav/projects/36", ReportCalendarQuery)
ms := parseMultistatus(t, rec)
for _, r := range ms.Responses {
// Each href should be a valid task URL containing the project ID and .ics
assert.Contains(t, r.Href, "/dav/projects/",
"Href should contain /dav/projects/")
assert.True(t, strings.HasSuffix(r.Href, ".ics"),
"Href should end with .ics. Got: %s", r.Href)
}
})
t.Run("calendar-query on nonexistent project returns error", func(t *testing.T) {
e := setupTestEnv(t)
rec := caldavREPORT(t, e, "/dav/projects/99999", ReportCalendarQuery)
// Should be 404 or similar error, not 200/207
assert.NotEqual(t, 207, rec.Code,
"REPORT on nonexistent project should not return 207")
})
t.Run("calendar-query on project 38 returns correct task count", func(t *testing.T) {
e := setupTestEnv(t)
rec := caldavREPORT(t, e, "/dav/projects/38", ReportCalendarQuery)
assertResponseStatus(t, rec, 207)
ms := parseMultistatus(t, rec)
// Project 38 has 2 tasks (44, 46)
assert.Len(t, ms.Responses, 2,
"Project 38 should have 2 tasks")
})
}
func TestReportCalendarMultiget(t *testing.T) {
// RFC 4791 §7.9 (rfc4791.txt line 3479):
// "The CALDAV:calendar-multiget REPORT is used to retrieve specific
// calendar object resources from within a collection."
t.Run("calendar-multiget returns requested tasks", func(t *testing.T) {
e := setupTestEnv(t)
// Request two specific tasks from project 36
body := ReportCalendarMultiget(
"/dav/projects/36/uid-caldav-test.ics",
"/dav/projects/36/uid-caldav-test-parent-task.ics",
)
rec := caldavREPORT(t, e, "/dav/projects/36", body)
assertResponseStatus(t, rec, 207)
ms := parseMultistatus(t, rec)
assert.Len(t, ms.Responses, 2,
"Should return exactly the 2 requested tasks")
})
t.Run("calendar-multiget returns calendar-data for each task", func(t *testing.T) {
e := setupTestEnv(t)
body := ReportCalendarMultiget(
"/dav/projects/36/uid-caldav-test.ics",
)
rec := caldavREPORT(t, e, "/dav/projects/36", body)
assertResponseStatus(t, rec, 207)
ms := parseMultistatus(t, rec)
require.Len(t, ms.Responses, 1)
prop := getSuccessfulProp(t, ms.Responses[0])
assert.NotEmpty(t, prop.CalendarData, "Should include calendar-data")
assert.NotEmpty(t, prop.GetETag, "Should include ETag")
// Verify the returned data matches the requested task
cal := parseICalFromString(t, prop.CalendarData)
vtodo := getVTodo(t, cal)
assert.Equal(t, "uid-caldav-test", getVTodoProperty(vtodo, ics.ComponentPropertyUniqueId))
})
t.Run("calendar-multiget with nonexistent href returns 404 for that href", func(t *testing.T) {
e := setupTestEnv(t)
body := ReportCalendarMultiget(
"/dav/projects/36/uid-caldav-test.ics",
"/dav/projects/36/nonexistent-uid.ics",
)
rec := caldavREPORT(t, e, "/dav/projects/36", body)
assertResponseStatus(t, rec, 207)
ms := parseMultistatus(t, rec)
// Should still return results — the existing task should be there
// The nonexistent one might be absent or have a 404 propstat
foundExisting := false
for _, r := range ms.Responses {
if strings.Contains(r.Href, "uid-caldav-test") {
foundExisting = true
prop := getSuccessfulProp(t, r)
assert.NotEmpty(t, prop.CalendarData)
}
}
assert.True(t, foundExisting,
"Should still return the existing task even when one href is invalid")
})
t.Run("calendar-multiget with empty href list returns empty", func(t *testing.T) {
e := setupTestEnv(t)
body := ReportCalendarMultiget() // No hrefs
rec := caldavREPORT(t, e, "/dav/projects/36", body)
// Should return 207 with no responses, or possibly an error
assert.True(t, rec.Code == 207 || rec.Code >= 400,
"Empty multiget should return 207 (empty) or an error, got %d", rec.Code)
})
t.Run("calendar-multiget ETags match PROPFIND ETags", func(t *testing.T) {
t.Skip("Known bug: ETag format inconsistency between PROPFIND and REPORT responses in caldav-go")
e := setupTestEnv(t)
// Get ETag via PROPFIND
propfindRec := caldavPROPFIND(t, e, "/dav/projects/36/uid-caldav-test.ics", "0", PropfindResourceProperties)
assertResponseStatus(t, propfindRec, 207)
propfindMs := parseMultistatus(t, propfindRec)
propfindEtag := getSuccessfulProp(t, propfindMs.Responses[0]).GetETag
// Get ETag via multiget REPORT
body := ReportCalendarMultiget("/dav/projects/36/uid-caldav-test.ics")
reportRec := caldavREPORT(t, e, "/dav/projects/36", body)
assertResponseStatus(t, reportRec, 207)
reportMs := parseMultistatus(t, reportRec)
reportEtag := getSuccessfulProp(t, reportMs.Responses[0]).GetETag
// ETags should match between PROPFIND and REPORT
assert.Equal(t, propfindEtag, reportEtag,
"ETag from PROPFIND and calendar-multiget should match")
})
}