278 lines
10 KiB
Go
278 lines
10 KiB
Go
package webui
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCalDAVPutGetDeleteRoundTrip(t *testing.T) {
|
|
app := newTestApp(t)
|
|
domains, _ := app.DB.ListDomains()
|
|
createTestMailboxWithAppPassword(t, app, "caluser@example.com", domains[0].ID)
|
|
srv := httptest.NewServer(app.Mux())
|
|
defer srv.Close()
|
|
|
|
objURL := srv.URL + "/caldav/caluser@example.com/calendars/default/test-uid-1.ics"
|
|
icsBody := "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//test//EN\r\n" +
|
|
"BEGIN:VEVENT\r\nUID:test-uid-1\r\nDTSTAMP:20260310T120000Z\r\n" +
|
|
"SUMMARY:Team Standup\r\nLOCATION:Room 1\r\n" +
|
|
"DTSTART:20260310T140000Z\r\nDTEND:20260310T150000Z\r\n" +
|
|
"BEGIN:VALARM\r\nACTION:DISPLAY\r\nDESCRIPTION:Reminder\r\nTRIGGER:-PT15M\r\nEND:VALARM\r\n" +
|
|
"END:VEVENT\r\nEND:VCALENDAR\r\n"
|
|
|
|
put := func() *http.Response {
|
|
req, _ := http.NewRequest(http.MethodPut, objURL, strings.NewReader(icsBody))
|
|
req.Header.Set("Content-Type", "text/calendar")
|
|
req.SetBasicAuth("caluser@example.com", carddavTestAppPassword)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return resp
|
|
}
|
|
resp := put()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("PUT: status=%d body=%s, want 201", resp.StatusCode, body)
|
|
}
|
|
|
|
// The webmail Calendar page reads the same underlying row — a PUT over CalDAV
|
|
// must show up there too, reminder included.
|
|
mailboxID := mustMailboxID(t, app, "caluser@example.com")
|
|
events, err := app.DB.ListAllEvents(mailboxID)
|
|
if err != nil || len(events) != 1 || events[0].Summary != "Team Standup" || events[0].Location != "Room 1" {
|
|
t.Fatalf("expected 1 event from CalDAV PUT visible via ListAllEvents, got %+v (err=%v)", events, err)
|
|
}
|
|
if events[0].ReminderMinutes == nil || *events[0].ReminderMinutes != 15 {
|
|
t.Fatalf("expected reminder parsed from VALARM TRIGGER:-PT15M as 15 minutes, got %+v", events[0].ReminderMinutes)
|
|
}
|
|
|
|
get := func() *http.Response {
|
|
req, _ := http.NewRequest(http.MethodGet, objURL, nil)
|
|
req.SetBasicAuth("caluser@example.com", carddavTestAppPassword)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return resp
|
|
}
|
|
resp = get()
|
|
body, _ = io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("GET: status=%d body=%s", resp.StatusCode, body)
|
|
}
|
|
for _, want := range []string{"SUMMARY:Team Standup", "BEGIN:VALARM", "TRIGGER"} {
|
|
if !strings.Contains(string(body), want) {
|
|
t.Errorf("GET body missing %q: %s", want, body)
|
|
}
|
|
}
|
|
|
|
del := func() *http.Response {
|
|
req, _ := http.NewRequest(http.MethodDelete, objURL, nil)
|
|
req.SetBasicAuth("caluser@example.com", carddavTestAppPassword)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return resp
|
|
}
|
|
resp = del()
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("DELETE: status=%d, want 204/200", resp.StatusCode)
|
|
}
|
|
resp = get()
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("GET after DELETE: status=%d, want 404", resp.StatusCode)
|
|
}
|
|
remaining, err := app.DB.ListAllEvents(mailboxID)
|
|
if err != nil || len(remaining) != 0 {
|
|
t.Fatalf("expected no events left, got %+v (err=%v)", remaining, err)
|
|
}
|
|
}
|
|
|
|
func TestCalDAVRecurringEventRoundTrip(t *testing.T) {
|
|
app := newTestApp(t)
|
|
domains, _ := app.DB.ListDomains()
|
|
createTestMailboxWithAppPassword(t, app, "recur@example.com", domains[0].ID)
|
|
srv := httptest.NewServer(app.Mux())
|
|
defer srv.Close()
|
|
|
|
objURL := srv.URL + "/caldav/recur@example.com/calendars/default/weekly-1.ics"
|
|
icsBody := "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//test//EN\r\n" +
|
|
"BEGIN:VEVENT\r\nUID:weekly-1\r\nDTSTAMP:20260301T120000Z\r\n" +
|
|
"SUMMARY:Weekly Sync\r\nDTSTART:20260303T090000Z\r\nDTEND:20260303T100000Z\r\n" +
|
|
"RRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
|
|
|
|
req, _ := http.NewRequest(http.MethodPut, objURL, strings.NewReader(icsBody))
|
|
req.Header.Set("Content-Type", "text/calendar")
|
|
req.SetBasicAuth("recur@example.com", carddavTestAppPassword)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("PUT: status=%d body=%s", resp.StatusCode, body)
|
|
}
|
|
|
|
mailboxID := mustMailboxID(t, app, "recur@example.com")
|
|
events, err := app.DB.ListAllEvents(mailboxID)
|
|
if err != nil || len(events) != 1 || events[0].RRule != "FREQ=WEEKLY" {
|
|
t.Fatalf("expected 1 event with RRule FREQ=WEEKLY, got %+v (err=%v)", events, err)
|
|
}
|
|
|
|
// GET round-trips the RRULE back out too.
|
|
getReq, _ := http.NewRequest(http.MethodGet, objURL, nil)
|
|
getReq.SetBasicAuth("recur@example.com", carddavTestAppPassword)
|
|
getResp, err := http.DefaultClient.Do(getReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
getBody, _ := io.ReadAll(getResp.Body)
|
|
getResp.Body.Close()
|
|
if !strings.Contains(string(getBody), "RRULE:FREQ=WEEKLY") {
|
|
t.Fatalf("expected RRULE round-tripped in GET response: %s", getBody)
|
|
}
|
|
}
|
|
|
|
func TestCalDAVListsWebmailCreatedEvent(t *testing.T) {
|
|
app := newTestApp(t)
|
|
domains, _ := app.DB.ListDomains()
|
|
mailboxID := createTestMailboxWithAppPassword(t, app, "calseen@example.com", domains[0].ID)
|
|
start := time.Date(2026, 3, 10, 14, 0, 0, 0, time.UTC)
|
|
if _, err := app.DB.CreateEvent(mailboxID, "Webmail Event", "made in webmail", "", start, start.Add(time.Hour), false, "", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
events, err := app.DB.ListAllEvents(mailboxID)
|
|
if err != nil || len(events) != 1 {
|
|
t.Fatal(err)
|
|
}
|
|
uid := events[0].UID
|
|
|
|
srv := httptest.NewServer(app.Mux())
|
|
defer srv.Close()
|
|
req, _ := http.NewRequest(http.MethodGet, srv.URL+"/caldav/calseen@example.com/calendars/default/"+uid+".ics", nil)
|
|
req.SetBasicAuth("calseen@example.com", carddavTestAppPassword)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK || !strings.Contains(string(body), "Webmail Event") {
|
|
t.Fatalf("expected the webmail-created event visible over CalDAV: status=%d body=%s", resp.StatusCode, body)
|
|
}
|
|
}
|
|
|
|
// TestCalDAVScopedToOwnMailbox mirrors TestCardDAVScopedToOwnMailbox: one mailbox's
|
|
// app-password credentials can never read another mailbox's events, even by guessing
|
|
// a path with the right uid.
|
|
func TestCalDAVScopedToOwnMailbox(t *testing.T) {
|
|
app := newTestApp(t)
|
|
domains, _ := app.DB.ListDomains()
|
|
victimID := createTestMailboxWithAppPassword(t, app, "calvictim@example.com", domains[0].ID)
|
|
createTestMailboxWithAppPassword(t, app, "calattacker@example.com", domains[0].ID)
|
|
start := time.Date(2026, 3, 10, 14, 0, 0, 0, time.UTC)
|
|
if _, err := app.DB.CreateEvent(victimID, "Secret Meeting", "", "", start, start.Add(time.Hour), false, "", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
events, _ := app.DB.ListAllEvents(victimID)
|
|
uid := events[0].UID
|
|
|
|
srv := httptest.NewServer(app.Mux())
|
|
defer srv.Close()
|
|
req, _ := http.NewRequest(http.MethodGet, srv.URL+"/caldav/calvictim@example.com/calendars/default/"+uid+".ics", nil)
|
|
req.SetBasicAuth("calattacker@example.com", carddavTestAppPassword)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("expected 404 (attacker's own ctx mailbox has no such event), got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestCalDAVPropfindDiscovery(t *testing.T) {
|
|
app := newTestApp(t)
|
|
domains, _ := app.DB.ListDomains()
|
|
createTestMailboxWithAppPassword(t, app, "discover@example.com", domains[0].ID)
|
|
srv := httptest.NewServer(app.Mux())
|
|
defer srv.Close()
|
|
|
|
req, _ := http.NewRequest("PROPFIND", srv.URL+"/caldav/discover@example.com/calendars/default/", nil)
|
|
req.Header.Set("Depth", "0")
|
|
req.SetBasicAuth("discover@example.com", carddavTestAppPassword)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusMultiStatus {
|
|
t.Fatalf("PROPFIND: status=%d body=%s, want 207", resp.StatusCode, body)
|
|
}
|
|
if !strings.Contains(string(body), "calendar") {
|
|
t.Errorf("expected calendar resourcetype in PROPFIND response: %s", body)
|
|
}
|
|
}
|
|
|
|
// TestCalDAVColorRoundTrip confirms a COLOR property round-trips through PUT/GET —
|
|
// both directions matter: a color set via the webmail UI must show up for a CalDAV
|
|
// client, and a color set by a CalDAV client (RFC 7986) must show up in the webmail
|
|
// Calendar page.
|
|
func TestCalDAVColorRoundTrip(t *testing.T) {
|
|
app := newTestApp(t)
|
|
domains, _ := app.DB.ListDomains()
|
|
createTestMailboxWithAppPassword(t, app, "calcolor@example.com", domains[0].ID)
|
|
srv := httptest.NewServer(app.Mux())
|
|
defer srv.Close()
|
|
|
|
objURL := srv.URL + "/caldav/calcolor@example.com/calendars/default/color-1.ics"
|
|
icsBody := "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//test//EN\r\n" +
|
|
"BEGIN:VEVENT\r\nUID:color-1\r\nDTSTAMP:20260301T120000Z\r\n" +
|
|
"SUMMARY:Colorful Event\r\nCOLOR:#e83e8c\r\n" +
|
|
"DTSTART:20260310T140000Z\r\nDTEND:20260310T150000Z\r\n" +
|
|
"END:VEVENT\r\nEND:VCALENDAR\r\n"
|
|
|
|
req, _ := http.NewRequest(http.MethodPut, objURL, strings.NewReader(icsBody))
|
|
req.Header.Set("Content-Type", "text/calendar")
|
|
req.SetBasicAuth("calcolor@example.com", carddavTestAppPassword)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("PUT: status=%d body=%s", resp.StatusCode, body)
|
|
}
|
|
|
|
mailboxID := mustMailboxID(t, app, "calcolor@example.com")
|
|
events, err := app.DB.ListAllEvents(mailboxID)
|
|
if err != nil || len(events) != 1 || events[0].Color != "#e83e8c" {
|
|
t.Fatalf("expected color stored from CalDAV PUT, got %+v (err=%v)", events, err)
|
|
}
|
|
|
|
getReq, _ := http.NewRequest(http.MethodGet, objURL, nil)
|
|
getReq.SetBasicAuth("calcolor@example.com", carddavTestAppPassword)
|
|
getResp, err := http.DefaultClient.Do(getReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
getBody, _ := io.ReadAll(getResp.Body)
|
|
getResp.Body.Close()
|
|
if !strings.Contains(string(getBody), "COLOR:#e83e8c") {
|
|
t.Fatalf("expected COLOR round-tripped in GET response: %s", getBody)
|
|
}
|
|
}
|