package caldav import ( "context" "fmt" "io" "log/slog" "path/filepath" "strings" "testing" ical "github.com/emersion/go-ical" "github.com/yourusername/caldav-server/internal/auth" "github.com/yourusername/caldav-server/internal/config" "github.com/yourusername/caldav-server/internal/db" "github.com/yourusername/caldav-server/internal/store" ) func newTestBackend(t *testing.T) (*Backend, *db.DB) { t.Helper() dir := t.TempDir() st, err := store.NewStore(filepath.Join(dir, "data")) if err != nil { t.Fatalf("NewStore: %v", err) } dbase, err := db.Open(filepath.Join(dir, "test.db")) if err != nil { t.Fatalf("db.Open: %v", err) } t.Cleanup(func() { dbase.Close() }) cfg := &config.Config{ Users: map[string]config.UserConfig{ "alice": {Calendars: []string{"work"}}, "bob": {Calendars: []string{"personal"}}, }, } logger := slog.New(slog.NewTextHandler(io.Discard, nil)) return NewBackend(cfg, st, dbase, logger), dbase } func ctxFor(username string) context.Context { return auth.NewContext(context.Background(), &auth.Principal{Username: username}) } // minimalEvent returns a minimal, valid VCALENDAR/VEVENT for use in tests. func minimalEvent() *ical.Calendar { const raw = "BEGIN:VCALENDAR\r\n" + "VERSION:2.0\r\n" + "PRODID:-//nidus//test//EN\r\n" + "BEGIN:VEVENT\r\n" + "UID:event1@nidus.test\r\n" + "DTSTAMP:20240101T000000Z\r\n" + "DTSTART:20240101T100000Z\r\n" + "SUMMARY:Test Event\r\n" + "END:VEVENT\r\n" + "END:VCALENDAR\r\n" cal, err := ical.NewDecoder(strings.NewReader(raw)).Decode() if err != nil { panic(fmt.Sprintf("minimalEvent: %v", err)) } return cal } func TestListCalendarsIncludesSharedCalendar(t *testing.T) { b, dbase := newTestBackend(t) if err := dbase.ShareCalendar("alice", "work", "bob", db.PermRead); err != nil { t.Fatalf("ShareCalendar: %v", err) } // The shared calendar must exist on disk for it to be listed; normally // this happens when alice's own ListCalendars runs and ensures it. if _, err := b.ListCalendars(ctxFor("alice")); err != nil { t.Fatalf("ListCalendars(alice): %v", err) } cals, err := b.ListCalendars(ctxFor("bob")) if err != nil { t.Fatalf("ListCalendars: %v", err) } var found bool wantName := sharedCalendarName("alice", "work") for _, c := range cals { if c.Name == wantName { found = true } } if !found { t.Errorf("shared calendar %q not found in ListCalendars result: %+v", wantName, cals) } } func TestSharedCalendarReadOnlyRejectsWrite(t *testing.T) { b, dbase := newTestBackend(t) if err := dbase.ShareCalendar("alice", "work", "bob", db.PermRead); err != nil { t.Fatalf("ShareCalendar: %v", err) } localName := sharedCalendarName("alice", "work") objPath := calObjectPath(localName, "event1.ics") _, err := b.PutCalendarObject(ctxFor("bob"), objPath, minimalEvent(), nil) if err == nil { t.Fatal("expected error writing to read-only shared calendar, got nil") } } func TestSharedCalendarWriteAllowed(t *testing.T) { b, dbase := newTestBackend(t) if err := dbase.ShareCalendar("alice", "work", "bob", db.PermWrite); err != nil { t.Fatalf("ShareCalendar: %v", err) } localName := sharedCalendarName("alice", "work") objPath := calObjectPath(localName, "event1.ics") if _, err := b.PutCalendarObject(ctxFor("bob"), objPath, minimalEvent(), nil); err != nil { t.Fatalf("PutCalendarObject with write share: %v", err) } // The object should now be visible under alice's own calendar too, // since it's stored in her namespace. obj, err := b.GetCalendarObject(ctxFor("alice"), calObjectPath("work", "event1.ics"), nil) if err != nil { t.Fatalf("GetCalendarObject as owner: %v", err) } if obj == nil { t.Fatal("expected non-nil object") } } func TestUnauthorizedUserCannotAccessUnsharedCalendar(t *testing.T) { b, _ := newTestBackend(t) localName := sharedCalendarName("alice", "work") _, err := b.GetCalendar(ctxFor("bob"), calHomePath()+localName+"/") if err == nil { t.Fatal("expected error accessing unshared calendar, got nil") } }