fix(caldav): add tags and sync token to collections (#2482)
Fixes #2401
This commit is contained in:
parent
9d8c6a0a72
commit
8e8ffac016
2
go.mod
2
go.mod
|
|
@ -208,4 +208,4 @@ tool (
|
|||
src.techknowlogick.com/xgo
|
||||
)
|
||||
|
||||
replace github.com/samedi/caldav-go => github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible // Branch: feature/dynamic-supported-components, PR: https://github.com/samedi/caldav-go/pull/6 and https://github.com/samedi/caldav-go/pull/7
|
||||
replace github.com/samedi/caldav-go => github.com/kolaente/caldav-go v3.0.1-0.20260326091743-a55d55891017+incompatible
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -328,8 +328,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C
|
|||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible h1:q7DbyV+sFjEoTuuUdRDNl2nlyfztkZgxVVCV7JhzIkY=
|
||||
github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible/go.mod h1:y1UhTNI4g0hVymJrI6yJ5/ohy09hNBeU8iJEZjgdDOw=
|
||||
github.com/kolaente/caldav-go v3.0.1-0.20260326091743-a55d55891017+incompatible h1:81Hr6g9bunxXhRv4AZv0anKcS1WwHLMgo6wbBjamJlY=
|
||||
github.com/kolaente/caldav-go v3.0.1-0.20260326091743-a55d55891017+incompatible/go.mod h1:y1UhTNI4g0hVymJrI6yJ5/ohy09hNBeU8iJEZjgdDOw=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
|
|
|
|||
|
|
@ -662,15 +662,6 @@ func (vlra *VikunjaProjectResourceAdapter) IsCollection() bool {
|
|||
// CalculateEtag returns the etag of a resource
|
||||
func (vlra *VikunjaProjectResourceAdapter) CalculateEtag() string {
|
||||
|
||||
// If we're updating a task, the client sends the etag of the project instead of the one from the task.
|
||||
// And therefore, updating the task fails since these etags don't match.
|
||||
// To fix that, we use this extra field to determine if we're currently updating a task and return the
|
||||
// etag of the project instead.
|
||||
// if vlra.project != nil {
|
||||
// return `"` + strconv.FormatInt(vlra.project.ID, 10) + `-` + strconv.FormatInt(vlra.project.Updated, 10) + `"`
|
||||
// }
|
||||
|
||||
// Return the etag of a task if we have one
|
||||
if vlra.task != nil {
|
||||
return `"` + strconv.FormatInt(vlra.task.ID, 10) + `-` + strconv.FormatInt(vlra.task.Updated.Unix(), 10) + `"`
|
||||
}
|
||||
|
|
@ -679,10 +670,17 @@ func (vlra *VikunjaProjectResourceAdapter) CalculateEtag() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// This also returns the etag of the project, and not of the task,
|
||||
// which becomes problematic because the client uses this etag (= the one from the project) to make
|
||||
// Requests to update a task. These do not match and thus updating a task fails.
|
||||
return `"` + strconv.FormatInt(vlra.project.ID, 10) + `-` + strconv.FormatInt(vlra.project.Updated.Unix(), 10) + `"`
|
||||
// For collections, use the latest modification time across all tasks
|
||||
// so that the etag (and derived ctag/sync-token) changes whenever
|
||||
// any task in the project is added, modified, or deleted.
|
||||
latest := vlra.project.Updated
|
||||
for _, t := range vlra.projectTasks {
|
||||
if t.Updated.After(latest) {
|
||||
latest = t.Updated
|
||||
}
|
||||
}
|
||||
|
||||
return `"` + strconv.FormatInt(vlra.project.ID, 10) + `-` + strconv.FormatInt(latest.Unix(), 10) + `"`
|
||||
}
|
||||
|
||||
// GetContent returns the content string of a resource (a task in our case)
|
||||
|
|
|
|||
|
|
@ -662,6 +662,136 @@ END:VCALENDAR`
|
|||
})
|
||||
}
|
||||
|
||||
func TestCaldavCollectionProperties(t *testing.T) {
|
||||
t.Run("PROPFIND returns getetag, getctag, and sync-token for collections", func(t *testing.T) {
|
||||
e, _ := setupTestEnv()
|
||||
|
||||
propfindBody := `<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:propfind xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
|
||||
<D:prop>
|
||||
<D:getetag/>
|
||||
<CS:getctag/>
|
||||
<D:sync-token/>
|
||||
<D:displayname/>
|
||||
</D:prop>
|
||||
</D:propfind>`
|
||||
|
||||
rec, err := newCaldavTestRequestWithUser(t, e, "PROPFIND", caldav.ProjectHandler, &testuser15, propfindBody,
|
||||
nil, map[string]string{"project": "36"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 207, rec.Result().StatusCode)
|
||||
|
||||
responseBody := rec.Body.String()
|
||||
|
||||
type Propstat struct {
|
||||
Prop struct {
|
||||
Getetag string `xml:"getetag"`
|
||||
Getctag string `xml:"http://calendarserver.org/ns/ getctag"`
|
||||
SyncToken string `xml:"sync-token"`
|
||||
Displayname string `xml:"displayname"`
|
||||
} `xml:"prop"`
|
||||
Status string `xml:"status"`
|
||||
}
|
||||
type Response struct {
|
||||
Href string `xml:"href"`
|
||||
Propstats []Propstat `xml:"propstat"`
|
||||
}
|
||||
type Multistatus struct {
|
||||
Responses []Response `xml:"response"`
|
||||
}
|
||||
|
||||
var multistatus Multistatus
|
||||
err = xml.Unmarshal([]byte(responseBody), &multistatus)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEmpty(t, multistatus.Responses, "Should have at least one response")
|
||||
|
||||
collectionResp := multistatus.Responses[0]
|
||||
|
||||
var foundEtag, foundCtag, foundSyncToken bool
|
||||
for _, ps := range collectionResp.Propstats {
|
||||
if strings.Contains(ps.Status, "200") {
|
||||
if ps.Prop.Getetag != "" {
|
||||
foundEtag = true
|
||||
assert.Contains(t, ps.Prop.Getetag, "36-", "Collection etag should contain the project ID")
|
||||
}
|
||||
if ps.Prop.Getctag != "" {
|
||||
foundCtag = true
|
||||
assert.Contains(t, ps.Prop.Getctag, "36-", "Collection ctag should contain the project ID")
|
||||
}
|
||||
if ps.Prop.SyncToken != "" {
|
||||
foundSyncToken = true
|
||||
assert.Contains(t, ps.Prop.SyncToken, "data:,", "Sync token should be a data: URI")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, foundEtag, "Collection should have getetag")
|
||||
assert.True(t, foundCtag, "Collection should have getctag")
|
||||
assert.True(t, foundSyncToken, "Collection should have sync-token")
|
||||
})
|
||||
|
||||
t.Run("Each collection has a unique etag", func(t *testing.T) {
|
||||
e, _ := setupTestEnv()
|
||||
|
||||
propfindBody := `<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:prop>
|
||||
<D:getetag/>
|
||||
</D:prop>
|
||||
</D:propfind>`
|
||||
|
||||
rec1, err := newCaldavTestRequestWithUser(t, e, "PROPFIND", caldav.ProjectHandler, &testuser15, propfindBody,
|
||||
nil, map[string]string{"project": "36"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 207, rec1.Result().StatusCode)
|
||||
|
||||
rec2, err := newCaldavTestRequestWithUser(t, e, "PROPFIND", caldav.ProjectHandler, &testuser15, propfindBody,
|
||||
nil, map[string]string{"project": "38"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 207, rec2.Result().StatusCode)
|
||||
|
||||
type Propstat struct {
|
||||
Prop struct {
|
||||
Getetag string `xml:"getetag"`
|
||||
} `xml:"prop"`
|
||||
Status string `xml:"status"`
|
||||
}
|
||||
type Response struct {
|
||||
Href string `xml:"href"`
|
||||
Propstats []Propstat `xml:"propstat"`
|
||||
}
|
||||
type Multistatus struct {
|
||||
Responses []Response `xml:"response"`
|
||||
}
|
||||
|
||||
var ms1, ms2 Multistatus
|
||||
err = xml.Unmarshal(rec1.Body.Bytes(), &ms1)
|
||||
require.NoError(t, err)
|
||||
err = xml.Unmarshal(rec2.Body.Bytes(), &ms2)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEmpty(t, ms1.Responses)
|
||||
require.NotEmpty(t, ms2.Responses)
|
||||
|
||||
var etag1, etag2 string
|
||||
for _, ps := range ms1.Responses[0].Propstats {
|
||||
if strings.Contains(ps.Status, "200") && ps.Prop.Getetag != "" {
|
||||
etag1 = ps.Prop.Getetag
|
||||
}
|
||||
}
|
||||
for _, ps := range ms2.Responses[0].Propstats {
|
||||
if strings.Contains(ps.Status, "200") && ps.Prop.Getetag != "" {
|
||||
etag2 = ps.Prop.Getetag
|
||||
}
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, etag1, "Project 36 should have an etag")
|
||||
assert.NotEmpty(t, etag2, "Project 38 should have an etag")
|
||||
assert.NotEqual(t, etag1, etag2, "Different projects should have different etags")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCaldavProjectReport(t *testing.T) {
|
||||
t.Run("REPORT calendar-query returns all tasks", func(t *testing.T) {
|
||||
e, _ := setupTestEnv()
|
||||
|
|
|
|||
Loading…
Reference in New Issue