test(caldav): add PROPFIND tests (RFC 4918 §9.1)
This commit is contained in:
parent
e2478e2fd6
commit
ebedd312c1
|
|
@ -0,0 +1,239 @@
|
|||
// 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"
|
||||
|
||||
ics "github.com/arran4/golang-ical"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPropfindCollection(t *testing.T) {
|
||||
// RFC 4918 §9.1 (rfc4918.txt line 1939):
|
||||
// "The PROPFIND method retrieves properties defined on the resource
|
||||
// identified by the Request-URI."
|
||||
|
||||
t.Run("Depth 0 on project returns collection properties", func(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
|
||||
rec := caldavPROPFIND(t, e, "/dav/projects/36", "0", PropfindCalendarCollectionProperties)
|
||||
|
||||
assertResponseStatus(t, rec, 207)
|
||||
ms := parseMultistatus(t, rec)
|
||||
|
||||
// Depth 0 should return exactly 1 response (the collection itself)
|
||||
assert.Len(t, ms.Responses, 1,
|
||||
"Depth 0 should return exactly the collection")
|
||||
|
||||
r := ms.Responses[0]
|
||||
prop := getSuccessfulProp(t, r)
|
||||
|
||||
// displayname should be the project title
|
||||
assert.Equal(t, "Project 36 for Caldav tests", prop.DisplayName,
|
||||
"displayname should match project title")
|
||||
|
||||
// resourcetype should include both DAV:collection and CALDAV:calendar
|
||||
assert.Contains(t, prop.ResourceType.InnerXML, "collection",
|
||||
"resourcetype should include DAV:collection")
|
||||
})
|
||||
|
||||
t.Run("Depth 1 on project returns collection plus tasks", func(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
|
||||
rec := caldavPROPFIND(t, e, "/dav/projects/36", "1", PropfindCalendarCollectionProperties)
|
||||
|
||||
assertResponseStatus(t, rec, 207)
|
||||
ms := parseMultistatus(t, rec)
|
||||
|
||||
// Project 36 has 5 tasks in fixtures (tasks 40-43, 45)
|
||||
// Depth 1 should return the collection + all tasks = 6 responses
|
||||
assert.GreaterOrEqual(t, len(ms.Responses), 6,
|
||||
"Depth 1 should return collection + all tasks")
|
||||
|
||||
// First response should be the collection itself
|
||||
// Subsequent responses should be individual tasks
|
||||
body := rec.Body.String()
|
||||
assert.Contains(t, body, ".ics",
|
||||
"Task responses should have .ics hrefs")
|
||||
})
|
||||
|
||||
t.Run("Depth 1 on project returns ETags for each resource", func(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
|
||||
rec := caldavPROPFIND(t, e, "/dav/projects/36", "1", PropfindResourceProperties)
|
||||
|
||||
assertResponseStatus(t, rec, 207)
|
||||
ms := parseMultistatus(t, rec)
|
||||
|
||||
for _, r := range ms.Responses {
|
||||
prop := getSuccessfulProp(t, r)
|
||||
// Every resource should have an ETag
|
||||
// RFC 4918 §15.6: "strong ETags MUST be used"
|
||||
assert.NotEmpty(t, prop.GetETag,
|
||||
"Every resource in PROPFIND should have an ETag. Href: %s", r.Href)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PROPFIND on nonexistent project returns 404", func(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
|
||||
rec := caldavPROPFIND(t, e, "/dav/projects/99999", "0", PropfindCalendarCollectionProperties)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, rec.Code,
|
||||
"PROPFIND on nonexistent project should return 404")
|
||||
})
|
||||
|
||||
t.Run("Depth 1 includes calendar-data for each task", func(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
|
||||
rec := caldavPROPFIND(t, e, "/dav/projects/36", "1", PropfindResourceProperties)
|
||||
|
||||
assertResponseStatus(t, rec, 207)
|
||||
ms := parseMultistatus(t, rec)
|
||||
|
||||
taskCount := 0
|
||||
for _, r := range ms.Responses {
|
||||
prop := getSuccessfulProp(t, r)
|
||||
if prop.CalendarData != "" {
|
||||
taskCount++
|
||||
// Each calendar-data should be valid iCalendar
|
||||
cal := parseICalFromString(t, prop.CalendarData)
|
||||
vtodo := getVTodo(t, cal)
|
||||
uid := getVTodoProperty(vtodo, ics.ComponentPropertyUniqueId)
|
||||
assert.NotEmpty(t, uid, "Each VTODO should have a UID")
|
||||
}
|
||||
}
|
||||
assert.Greater(t, taskCount, 0, "Should have at least one task with calendar-data")
|
||||
})
|
||||
}
|
||||
|
||||
func TestPropfindResource(t *testing.T) {
|
||||
t.Run("Depth 0 on task returns task properties", func(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
|
||||
// Task 40 has UID "uid-caldav-test" in project 36
|
||||
rec := caldavPROPFIND(t, e, "/dav/projects/36/uid-caldav-test.ics", "0", PropfindResourceProperties)
|
||||
|
||||
assertResponseStatus(t, rec, 207)
|
||||
ms := parseMultistatus(t, rec)
|
||||
|
||||
assert.Len(t, ms.Responses, 1,
|
||||
"Depth 0 on a task should return exactly 1 response")
|
||||
|
||||
r := ms.Responses[0]
|
||||
prop := getSuccessfulProp(t, r)
|
||||
|
||||
assert.NotEmpty(t, prop.GetETag, "Task should have an ETag")
|
||||
assert.NotEmpty(t, prop.CalendarData, "Task should have calendar-data")
|
||||
|
||||
// Parse and validate the calendar data
|
||||
cal := parseICalFromString(t, prop.CalendarData)
|
||||
vtodo := getVTodo(t, cal)
|
||||
assert.Equal(t, "uid-caldav-test", getVTodoProperty(vtodo, ics.ComponentPropertyUniqueId))
|
||||
assert.Equal(t, "Title Caldav Test", getVTodoProperty(vtodo, ics.ComponentPropertySummary))
|
||||
})
|
||||
|
||||
t.Run("PROPFIND on nonexistent task returns 404", func(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
|
||||
rec := caldavPROPFIND(t, e, "/dav/projects/36/nonexistent-uid.ics", "0", PropfindResourceProperties)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, rec.Code,
|
||||
"PROPFIND on nonexistent task should return 404")
|
||||
})
|
||||
|
||||
t.Run("ETag format is quoted string", func(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
|
||||
rec := caldavPROPFIND(t, e, "/dav/projects/36/uid-caldav-test.ics", "0", PropfindResourceProperties)
|
||||
|
||||
assertResponseStatus(t, rec, 207)
|
||||
ms := parseMultistatus(t, rec)
|
||||
r := ms.Responses[0]
|
||||
prop := getSuccessfulProp(t, r)
|
||||
|
||||
// RFC 4918 requires ETags to be quoted strings
|
||||
assert.True(t, len(prop.GetETag) > 2 &&
|
||||
prop.GetETag[0] == '"' && prop.GetETag[len(prop.GetETag)-1] == '"',
|
||||
"ETag should be a quoted string, got: %s", prop.GetETag)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPropfindCalendarHome(t *testing.T) {
|
||||
t.Run("Depth 1 on /dav/projects lists all accessible calendars", func(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
|
||||
rec := caldavPROPFIND(t, e, "/dav/projects", "1", PropfindCalendarCollectionProperties)
|
||||
|
||||
assertResponseStatus(t, rec, 207)
|
||||
ms := parseMultistatus(t, rec)
|
||||
|
||||
// testuser15 should see at least projects 36 and 38
|
||||
projectFound36 := false
|
||||
projectFound38 := false
|
||||
for _, r := range ms.Responses {
|
||||
if strings.Contains(r.Href, "36") {
|
||||
projectFound36 = true
|
||||
}
|
||||
if strings.Contains(r.Href, "38") {
|
||||
projectFound38 = true
|
||||
}
|
||||
}
|
||||
assert.True(t, projectFound36, "Should list project 36 in calendar home")
|
||||
assert.True(t, projectFound38, "Should list project 38 in calendar home")
|
||||
})
|
||||
|
||||
t.Run("Each calendar has displayname matching project title", func(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
|
||||
rec := caldavPROPFIND(t, e, "/dav/projects", "1", PropfindCalendarCollectionProperties)
|
||||
|
||||
assertResponseStatus(t, rec, 207)
|
||||
ms := parseMultistatus(t, rec)
|
||||
|
||||
for _, r := range ms.Responses {
|
||||
prop := getSuccessfulProp(t, r)
|
||||
if prop.DisplayName != "" {
|
||||
// Every calendar with a displayname should have a reasonable title
|
||||
assert.NotEmpty(t, prop.DisplayName,
|
||||
"Calendar at %s should have a displayname", r.Href)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("User only sees projects they have access to", func(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
|
||||
// testuser1 should NOT see testuser15's projects (36, 38)
|
||||
rec := caldavRequest(t, e, "PROPFIND", "/dav/projects", PropfindCalendarCollectionProperties, map[string]string{
|
||||
"Depth": "1",
|
||||
"Authorization": basicAuthHeader(testuser1.Username, fixturePassword),
|
||||
})
|
||||
|
||||
assertResponseStatus(t, rec, 207)
|
||||
|
||||
body := rec.Body.String()
|
||||
// user1 should not see project 36 or 38 (owned by user15)
|
||||
assert.NotContains(t, body, "Project 36 for Caldav tests",
|
||||
"user1 should not see user15's project 36")
|
||||
assert.NotContains(t, body, "Project 38 for Caldav tests",
|
||||
"user1 should not see user15's project 38")
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue