diff --git a/pkg/caldavtests/bugs_test.go b/pkg/caldavtests/bugs_test.go
new file mode 100644
index 000000000..a75b28128
--- /dev/null
+++ b/pkg/caldavtests/bugs_test.go
@@ -0,0 +1,51 @@
+// 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 .
+
+package caldavtests
+
+import (
+ "testing"
+)
+
+// TestBugs contains tests that reproduce specific bugs reported by users.
+// Each test references the GitHub issue it reproduces.
+// These tests are expected to FAIL until the bug is fixed.
+//
+// To add a new bug reproduction test:
+// 1. Create a new t.Run with the issue number in the name
+// 2. Reproduce the exact CalDAV request sequence from the bug report
+// 3. Assert what the correct behavior SHOULD be (not what it currently does)
+// 4. The test will fail until the bug is fixed — this is expected and good
+
+func TestBugs(t *testing.T) {
+ // Template for adding bug reproductions:
+ //
+ // t.Run("GitHub_Issue_NNNN_short_description", func(t *testing.T) {
+ // e := setupTestEnv(t)
+ //
+ // // Reproduce the steps from the issue...
+ // vtodo := NewVTodo("issue-NNNN", "...").Build()
+ // rec := caldavPUT(t, e, "/dav/projects/36/issue-NNNN.ics", vtodo)
+ //
+ // // Assert the expected (correct) behavior
+ // assert.Equal(t, 201, rec.Code)
+ // })
+
+ t.Run("placeholder_no_bugs_yet", func(t *testing.T) {
+ // Remove this placeholder once real bug tests are added
+ t.Skip("No bug reproductions added yet")
+ })
+}
diff --git a/pkg/caldavtests/client_compat_test.go b/pkg/caldavtests/client_compat_test.go
new file mode 100644
index 000000000..f7a103e87
--- /dev/null
+++ b/pkg/caldavtests/client_compat_test.go
@@ -0,0 +1,215 @@
+// 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 .
+
+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.Greater(t, len(ms.Responses), 0,
+ "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 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")
+ })
+}