Files
mailgoserver/internal/webui/caldav_test.go
T

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