package carddav import ( "context" "io" "log/slog" "path/filepath" "strings" "testing" vcard "github.com/emersion/go-vcard" "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{} if err := dbase.CreateUser("alice", "pw", "", ""); err != nil { t.Fatalf("CreateUser alice: %v", err) } if err := dbase.CreateUser("bob", "pw", "", ""); err != nil { t.Fatalf("CreateUser bob: %v", err) } if err := dbase.CreateAddressBook("alice", "contacts"); err != nil { t.Fatalf("CreateAddressBook alice/contacts: %v", err) } if err := dbase.CreateAddressBook("bob", "personal"); err != nil { t.Fatalf("CreateAddressBook bob/personal: %v", err) } 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}) } // minimalCard returns a minimal, valid vCard for use in tests. func minimalCard() vcard.Card { const raw = "BEGIN:VCARD\r\n" + "VERSION:3.0\r\n" + "UID:card1@nidus.test\r\n" + "FN:Test Person\r\n" + "END:VCARD\r\n" card, err := vcard.NewDecoder(strings.NewReader(raw)).Decode() if err != nil { panic("minimalCard: " + err.Error()) } return card } func TestListAddressBooksIncludesSharedBook(t *testing.T) { b, dbase := newTestBackend(t) if err := dbase.ShareAddressBook("alice", "contacts", "bob", db.PermRead); err != nil { t.Fatalf("ShareAddressBook: %v", err) } // The shared book must exist on disk for it to be listed; normally this // happens when alice's own ListAddressBooks runs and ensures it. if _, err := b.ListAddressBooks(ctxFor("alice")); err != nil { t.Fatalf("ListAddressBooks(alice): %v", err) } books, err := b.ListAddressBooks(ctxFor("bob")) if err != nil { t.Fatalf("ListAddressBooks: %v", err) } var found bool wantName := sharedBookName("alice", "contacts") for _, book := range books { if book.Name == wantName { found = true } } if !found { t.Errorf("shared address book %q not found in ListAddressBooks result: %+v", wantName, books) } } func TestSharedAddressBookReadOnlyRejectsWrite(t *testing.T) { b, dbase := newTestBackend(t) if err := dbase.ShareAddressBook("alice", "contacts", "bob", db.PermRead); err != nil { t.Fatalf("ShareAddressBook: %v", err) } localName := sharedBookName("alice", "contacts") objPath := cardObjectPath(localName, "card1.vcf") _, err := b.PutAddressObject(ctxFor("bob"), objPath, minimalCard(), nil) if err == nil { t.Fatal("expected error writing to read-only shared address book, got nil") } } func TestSharedAddressBookWriteAllowed(t *testing.T) { b, dbase := newTestBackend(t) if err := dbase.ShareAddressBook("alice", "contacts", "bob", db.PermWrite); err != nil { t.Fatalf("ShareAddressBook: %v", err) } localName := sharedBookName("alice", "contacts") objPath := cardObjectPath(localName, "card1.vcf") if _, err := b.PutAddressObject(ctxFor("bob"), objPath, minimalCard(), nil); err != nil { t.Fatalf("PutAddressObject with write share: %v", err) } // The object should now be visible under alice's own address book too, // since it's stored in her namespace. obj, err := b.GetAddressObject(ctxFor("alice"), cardObjectPath("contacts", "card1.vcf"), nil) if err != nil { t.Fatalf("GetAddressObject as owner: %v", err) } if obj == nil { t.Fatal("expected non-nil object") } } func TestUnauthorizedUserCannotAccessUnsharedBook(t *testing.T) { b, _ := newTestBackend(t) localName := sharedBookName("alice", "contacts") _, err := b.GetAddressBook(ctxFor("bob"), cardHomePath()+localName+"/") if err == nil { t.Fatal("expected error accessing unshared address book, got nil") } }