Files
vikunja/pkg/caldavtests/auth_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

183 lines
6.4 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"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAuth(t *testing.T) {
t.Run("Valid credentials return 200/207", func(t *testing.T) {
e := setupTestEnv(t)
rec := caldavGET(t, e, "/dav/projects/36")
assert.True(t, rec.Code >= 200 && rec.Code < 300,
"Valid credentials should succeed. Got %d", rec.Code)
})
t.Run("No auth returns 401", func(t *testing.T) {
e := setupTestEnv(t)
req := httptest.NewRequest(http.MethodGet, "/dav/projects/36", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
assert.Equal(t, http.StatusUnauthorized, rec.Code,
"Request without auth should return 401")
})
t.Run("Wrong password returns 401", func(t *testing.T) {
e := setupTestEnv(t)
rec := caldavRequest(t, e, http.MethodGet, "/dav/projects/36", "", map[string]string{
"Authorization": basicAuthHeader(testuser15.Username, "wrongpassword"),
})
assert.Equal(t, http.StatusUnauthorized, rec.Code,
"Wrong password should return 401")
})
t.Run("Nonexistent user returns 401", func(t *testing.T) {
e := setupTestEnv(t)
rec := caldavRequest(t, e, http.MethodGet, "/dav/projects/36", "", map[string]string{
"Authorization": basicAuthHeader("nonexistent_user", fixturePassword),
})
assert.Equal(t, http.StatusUnauthorized, rec.Code,
"Nonexistent user should return 401")
})
t.Run("Empty Authorization header returns 401", func(t *testing.T) {
e := setupTestEnv(t)
req := httptest.NewRequest(http.MethodGet, "/dav/projects/36", nil)
req.Header.Set("Authorization", "")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
assert.Equal(t, http.StatusUnauthorized, rec.Code,
"Empty auth header should return 401")
})
t.Run("Auth on /dav/ entry point", func(t *testing.T) {
e := setupTestEnv(t)
rec := caldavRequest(t, e, "PROPFIND", "/dav/", PropfindCurrentUserPrincipal, map[string]string{
"Depth": "0",
})
// Should succeed with valid auth
assert.True(t, rec.Code >= 200 && rec.Code < 300 || rec.Code == 207,
"Authenticated PROPFIND on /dav/ should succeed. Got %d", rec.Code)
})
t.Run("Auth on /.well-known/caldav", func(t *testing.T) {
e := setupTestEnv(t)
// Without auth
req := httptest.NewRequest("PROPFIND", "/.well-known/caldav", strings.NewReader(PropfindCurrentUserPrincipal))
req.Header.Set("Depth", "0")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
assert.Equal(t, http.StatusUnauthorized, rec.Code,
"/.well-known/caldav without auth should return 401")
})
}
func TestPermissions(t *testing.T) {
t.Run("User cannot GET project they do not have access to", func(t *testing.T) {
t.Skip("Known bug: CalDAV returns 500 instead of 403/404 — ErrUserDoesNotHaveAccessToProject is not recognized by caldav-go")
e := setupTestEnv(t)
// testuser1 should not be able to access project 36 (owned by user15)
rec := caldavRequest(t, e, http.MethodGet, "/dav/projects/36", "", map[string]string{
"Authorization": basicAuthHeader(testuser1.Username, fixturePassword),
})
// Should be 403 Forbidden or 404 Not Found (both are acceptable for access denial)
assert.True(t, rec.Code == http.StatusForbidden || rec.Code == http.StatusNotFound,
"Unauthorized user should get 403 or 404, got %d. Body:\n%s", rec.Code, rec.Body.String())
})
t.Run("User cannot PUT task to project they do not have access to", func(t *testing.T) {
e := setupTestEnv(t)
vtodo := NewVTodo("unauthorized-task", "Should Fail").Build()
rec := caldavRequest(t, e, http.MethodPut, "/dav/projects/36/unauthorized-task.ics", vtodo, map[string]string{
"Authorization": basicAuthHeader(testuser1.Username, fixturePassword),
"Content-Type": "text/calendar; charset=utf-8",
})
assert.True(t, rec.Code == http.StatusForbidden || rec.Code == http.StatusNotFound,
"PUT to unauthorized project should fail with 403 or 404, got %d", rec.Code)
})
t.Run("User cannot DELETE task from project they do not have access to", func(t *testing.T) {
e := setupTestEnv(t)
// Try to delete task 40 (uid-caldav-test) in project 36 as user1
rec := caldavRequest(t, e, http.MethodDelete, "/dav/projects/36/uid-caldav-test.ics", "", map[string]string{
"Authorization": basicAuthHeader(testuser1.Username, fixturePassword),
})
assert.True(t, rec.Code == http.StatusForbidden || rec.Code == http.StatusNotFound,
"DELETE on unauthorized project should fail with 403 or 404, got %d", rec.Code)
})
t.Run("User cannot REPORT on project they do not have access to", func(t *testing.T) {
e := setupTestEnv(t)
rec := caldavRequest(t, e, "REPORT", "/dav/projects/36", ReportCalendarQuery, map[string]string{
"Authorization": basicAuthHeader(testuser1.Username, fixturePassword),
})
assert.True(t, rec.Code == http.StatusForbidden || rec.Code == http.StatusNotFound || rec.Code == 207,
"REPORT on unauthorized project should fail or return empty, got %d", rec.Code)
// If it returns 207, it should have no results
if rec.Code == 207 {
ms := parseMultistatus(t, rec)
assert.Empty(t, ms.Responses,
"REPORT on unauthorized project should return empty multistatus if 207")
}
})
t.Run("Project listing only shows accessible projects", func(t *testing.T) {
e := setupTestEnv(t)
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 see their own projects but NOT user15's projects
assert.NotContains(t, body, "Project 36 for Caldav tests",
"user1 should not see user15's Project 36")
})
}