Files
vikunja/pkg/caldavtests/client_compat_test.go
kolaente ef85a22f99 fix(caldav): resolve lint issues in caldavtests package
- Remove unused helper functions (findResponse, assertMultistatusHasResponses, caldavRequestAsUser)
- Fix gofmt formatting
- Convert WriteString(fmt.Sprintf(...)) to fmt.Fprintf
- Fix unused parameter warnings
- Fix testifylint suggestions (assert.NotEmpty, assert.Positive)
- Add nolint:unparam for assertResponseStatus
2026-04-02 11:34:55 +00:00

216 lines
8.3 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 (
"net/http"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestClientDAVx5Flow(t *testing.T) {
t.Run("Full DAVx5 sync flow", func(t *testing.T) {
e := setupTestEnv(t)
// Step 1: Discover principal
// DAVx5 sends PROPFIND to the server root or well-known URL
rec := caldavPROPFIND(t, e, "/dav/", "0", PropfindCurrentUserPrincipal)
assert.True(t, rec.Code == 207 || rec.Code == 301,
"Step 1: PROPFIND /dav/ should return 207 or redirect. Got %d", rec.Code)
// Step 2: Get calendar-home-set from principal
rec = caldavPROPFIND(t, e, "/dav/principals/user15/", "0", PropfindCalendarHomeSet)
assertResponseStatus(t, rec, 207)
assert.Contains(t, rec.Body.String(), "calendar-home-set",
"Step 2: Principal should advertise calendar-home-set")
// Step 3: List calendars
rec = caldavPROPFIND(t, e, "/dav/projects", "1", PropfindCalendarCollectionProperties)
assertResponseStatus(t, rec, 207)
ms := parseMultistatus(t, rec)
assert.GreaterOrEqual(t, len(ms.Responses), 2,
"Step 3: Should list calendars")
// Step 4: Check CTag for a specific calendar
rec = caldavPROPFIND(t, e, "/dav/projects/36", "0", PropfindCalendarCollectionProperties)
assertResponseStatus(t, rec, 207)
// Step 5: Full sync — calendar-query to get all task ETags
rec = caldavREPORT(t, e, "/dav/projects/36", ReportCalendarQuery)
assertResponseStatus(t, rec, 207)
ms = parseMultistatus(t, rec)
assert.NotEmpty(t, ms.Responses,
"Step 5: calendar-query should return tasks")
// Collect hrefs for multiget
var hrefs []string
for _, r := range ms.Responses {
if strings.HasSuffix(r.Href, ".ics") {
hrefs = append(hrefs, r.Href)
}
}
// Step 6: Multiget to fetch specific tasks
if len(hrefs) > 0 {
body := ReportCalendarMultiget(hrefs[:1]...) // Just fetch first task
rec = caldavREPORT(t, e, "/dav/projects/36", body)
assertResponseStatus(t, rec, 207)
ms = parseMultistatus(t, rec)
assert.Len(t, ms.Responses, 1,
"Step 6: multiget should return requested task")
}
// Step 7: Push a local change via PUT
vtodo := NewVTodo("davx5-sync-test", "DAVx5 Synced Task").
Due(time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)).
Build()
rec = caldavPUT(t, e, "/dav/projects/36/davx5-sync-test.ics", vtodo)
assert.Equal(t, http.StatusCreated, rec.Code,
"Step 7: PUT should create the task")
})
}
func TestClientThunderbirdFlow(t *testing.T) {
t.Run("Thunderbird discovery and initial sync", func(t *testing.T) {
e := setupTestEnv(t)
// Step 1: Thunderbird starts with OPTIONS to check DAV support
rec := caldavOPTIONS(t, e, "/dav/")
assert.Equal(t, http.StatusOK, rec.Code,
"Step 1: OPTIONS should succeed")
davHeader := rec.Header().Get("DAV")
assert.NotEmpty(t, davHeader,
"Step 1: Should have DAV header")
// Step 2: PROPFIND on well-known for principal
rec = caldavRequest(t, e, "PROPFIND", "/.well-known/caldav", PropfindCurrentUserPrincipal, map[string]string{
"Depth": "0",
})
assert.True(t, rec.Code == 207 || rec.Code == 301 || rec.Code == 302,
"Step 2: well-known should respond. Got %d", rec.Code)
// Step 3: PROPFIND principal for calendar-home-set
rec = caldavPROPFIND(t, e, "/dav/principals/user15/", "0", PropfindCalendarHomeSet)
assertResponseStatus(t, rec, 207)
// Step 4: Thunderbird checks current-user-privilege-set to know if it can write
// RFC 3744 §5.4 (rfc3744.txt line 1158)
rec = caldavPROPFIND(t, e, "/dav/projects/36", "0", PropfindCurrentUserPrivilegeSet)
// This may return 207 with or without the property — document the behavior
assert.True(t, rec.Code == 207 || rec.Code == 200,
"Step 4: PROPFIND for privileges should not error. Got %d", rec.Code)
// Step 5: List calendars
rec = caldavPROPFIND(t, e, "/dav/projects", "1", PropfindCalendarCollectionProperties)
assertResponseStatus(t, rec, 207)
// Step 6: Sync via calendar-query
rec = caldavREPORT(t, e, "/dav/projects/36", ReportCalendarQuery)
assertResponseStatus(t, rec, 207)
})
}
func TestClientTasksOrgSubtasks(t *testing.T) {
t.Run("Tasks.org subtask sync: child-only RELATED-TO", func(t *testing.T) {
// Tasks.org behavior:
// - Child tasks include RELATED-TO;RELTYPE=PARENT:<parent-uid>
// - Parent tasks have NO RELATED-TO at all
// - Tasks may arrive in any order
// - On re-sync, parent is sent again without RELATED-TO
e := setupTestEnv(t)
// Round 1: Initial sync — parent first, then children
parent := NewVTodo("tasks-org-parent", "Buy groceries").Build()
rec := caldavPUT(t, e, "/dav/projects/36/tasks-org-parent.ics", parent)
require.Equal(t, 201, rec.Code)
child1 := NewVTodo("tasks-org-child-1", "Buy milk").
RelatedToParent("tasks-org-parent").Build()
rec = caldavPUT(t, e, "/dav/projects/36/tasks-org-child-1.ics", child1)
require.Equal(t, 201, rec.Code)
child2 := NewVTodo("tasks-org-child-2", "Buy eggs").
RelatedToParent("tasks-org-parent").Build()
rec = caldavPUT(t, e, "/dav/projects/36/tasks-org-child-2.ics", child2)
require.Equal(t, 201, rec.Code)
// Verify parent shows children
rec = caldavGET(t, e, "/dav/projects/36/tasks-org-parent.ics")
body := rec.Body.String()
assert.Contains(t, body, "tasks-org-child-1")
assert.Contains(t, body, "tasks-org-child-2")
// Round 2: Re-sync — parent updated (title change), still no RELATED-TO
parentUpdated := NewVTodo("tasks-org-parent", "Buy groceries (updated list)").Build()
rec = caldavPUT(t, e, "/dav/projects/36/tasks-org-parent.ics", parentUpdated)
require.True(t, rec.Code >= 200 && rec.Code < 300)
// Verify children are still linked after parent re-sync
rec = caldavGET(t, e, "/dav/projects/36/tasks-org-parent.ics")
body = rec.Body.String()
assert.Contains(t, body, "Buy groceries (updated list)",
"Parent title should be updated")
assert.Contains(t, body, "tasks-org-child-1",
"Child 1 relation should survive parent re-sync")
assert.Contains(t, body, "tasks-org-child-2",
"Child 2 relation should survive parent re-sync")
// Round 3: Complete child via PUT with STATUS:COMPLETED
child1Done := NewVTodo("tasks-org-child-1", "Buy milk").
RelatedToParent("tasks-org-parent").
Status("COMPLETED").
Completed(time.Now().UTC()).
Build()
rec = caldavPUT(t, e, "/dav/projects/36/tasks-org-child-1.ics", child1Done)
require.True(t, rec.Code >= 200 && rec.Code < 300)
// Verify child is completed
rec = caldavGET(t, e, "/dav/projects/36/tasks-org-child-1.ics")
assert.Contains(t, rec.Body.String(), "STATUS:COMPLETED")
})
t.Run("Tasks.org subtask sync: children arrive before parent", func(t *testing.T) {
e := setupTestEnv(t)
// Children arrive first (reverse order)
child := NewVTodo("tasks-rev-child", "Subtask").
RelatedToParent("tasks-rev-parent").Build()
rec := caldavPUT(t, e, "/dav/projects/36/tasks-rev-child.ics", child)
require.Equal(t, 201, rec.Code)
// Parent arrives later — no RELATED-TO
parent := NewVTodo("tasks-rev-parent", "Main Task").Build()
rec = caldavPUT(t, e, "/dav/projects/36/tasks-rev-parent.ics", parent)
require.Equal(t, 201, rec.Code)
// Verify bidirectional relations
rec = caldavGET(t, e, "/dav/projects/36/tasks-rev-parent.ics")
assert.Contains(t, rec.Body.String(), "SUMMARY:Main Task",
"Parent should have real title, not DUMMY")
assert.Contains(t, rec.Body.String(), "tasks-rev-child",
"Parent should show child relation")
rec = caldavGET(t, e, "/dav/projects/36/tasks-rev-child.ics")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:tasks-rev-parent")
})
}