Disambiguate shared calendars with a name colliding with the owner's

If a calendar shared with a user has the same name as one of the user's
own calendars (or as another share from a different owner), DAVx5 would
show two calendars with an identical, indistinguishable title. The
shared calendar's CalDAV display name (Calendar.Name, exposed via
displayname/DAVx5's calendar list) now gets the owning user's username
appended in parentheses in that case, e.g. "personal (alice)" — the
underlying calendar path/URL is unaffected, and a user's own calendars
are never renamed (only shared ones can collide with something else).

internal/caldav/backend.go: calendarMeta split into calendarMeta (own
calendars, unchanged name) and calendarMetaNamed (explicit display name);
new sharedDisplayName(requester, owner, realName) checks requester's own
calendars and other shares for a collision. Wired into both
ListCalendars and GetCalendar (the latter used by direct
PROPFIND/REPORT against a shared calendar's own URL).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-08-20 23:20:17 +02:00
co-authored by Copilot
parent 213b22f821
commit b58a6ba4ea
2 changed files with 103 additions and 3 deletions
+54
View File
@@ -277,3 +277,57 @@ func TestPropFindExplicitCalendarColorRequest(t *testing.T) {
t.Fatalf("expected a single 200 OK propstat with no 404, got: %s", body)
}
}
// TestSharedCalendarNameCollidingWithOwnGetsDisambiguated covers the case
// where a calendar shared with a user has the same name as one of that
// user's own calendars: DAVx5 (and any other client) would otherwise show
// two calendars with an identical, indistinguishable title, so the shared
// one's display name gets the owner's username appended in parentheses.
func TestSharedCalendarNameCollidingWithOwnGetsDisambiguated(t *testing.T) {
b, dbase := newTestBackend(t)
// alice creates her own "personal" calendar, colliding with bob's
// pre-existing "personal" calendar (see newTestBackend), then shares
// it with bob.
if err := dbase.CreateCalendar("alice", "personal"); err != nil {
t.Fatalf("CreateCalendar alice/personal: %v", err)
}
if err := dbase.ShareCalendar("alice", "personal", "bob", db.PermRead); err != nil {
t.Fatalf("ShareCalendar: %v", err)
}
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(bob): %v", err)
}
wantPath := calHomePath() + sharedCalendarName("alice", "personal") + "/"
var sharedName, ownName string
for _, c := range cals {
if c.Path == wantPath {
sharedName = c.Name
}
if c.Path == calHomePath()+"personal/" {
ownName = c.Name
}
}
if ownName != "personal" {
t.Fatalf("expected bob's own calendar to keep its plain name, got %q", ownName)
}
if want := "personal (alice)"; sharedName != want {
t.Fatalf("expected shared calendar display name %q, got %q", want, sharedName)
}
// GetCalendar (single-collection lookup, as used by PROPFIND on the
// calendar's own URL) must disambiguate the same way.
cal, err := b.GetCalendar(ctxFor("bob"), wantPath)
if err != nil {
t.Fatalf("GetCalendar: %v", err)
}
if want := "personal (alice)"; cal.Name != want {
t.Fatalf("GetCalendar: expected display name %q, got %q", want, cal.Name)
}
}