// 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" "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") }) }