From d23d30e69514ed6c99e69a4eef97377bfeed6d22 Mon Sep 17 00:00:00 2001 From: dev_agent Date: Wed, 7 Oct 2026 21:36:37 -0400 Subject: [PATCH] List each Google calendar once when calendarList pages overlap Deduplicate by calendar id as the pages are collected, keeping the first occurrence in Google's order (MQ-126-01), with a test serving two overlapping pages. Co-Authored-By: Claude Opus 5.5 --- internal/gcal/calendars.go | 10 +++++++--- internal/gcal/calendars_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/internal/gcal/calendars.go b/internal/gcal/calendars.go index ffbc48a..c7c3961 100644 --- a/internal/gcal/calendars.go +++ b/internal/gcal/calendars.go @@ -102,14 +102,17 @@ func (s *Service) SelectCalendar(ctx context.Context, id string) (Selection, err return Selection{}, ErrUnknownCalendar } -// calendars lists the writable calendars with the stored refresh token. The -// caller holds s.account. +// calendars lists the writable calendars, each once, with the stored refresh +// token. The caller holds s.account. func (s *Service) calendars(ctx context.Context) ([]Calendar, error) { access, err := s.accessToken(ctx) if err != nil { return nil, err } cals := []Calendar{} + // listed holds the ids in cals: pages can overlap, and a calendar is + // listed once, as Google first lists it. + listed := map[string]bool{} pageToken := "" for range maxCalendarPages { page, err := s.calendarPage(ctx, access, pageToken) @@ -120,7 +123,8 @@ func (s *Service) calendars(ctx context.Context) ([]Calendar, error) { if item.ID == "" { return nil, &CalendarError{msg: "gcal: Google listed a calendar without an id"} } - if item.AccessRole == "writer" || item.AccessRole == "owner" { + if (item.AccessRole == "writer" || item.AccessRole == "owner") && !listed[item.ID] { + listed[item.ID] = true cals = append(cals, Calendar{ID: item.ID, Summary: item.Summary, Primary: item.Primary, AccessRole: item.AccessRole}) } } diff --git a/internal/gcal/calendars_test.go b/internal/gcal/calendars_test.go index f85d1d8..e2cece6 100644 --- a/internal/gcal/calendars_test.go +++ b/internal/gcal/calendars_test.go @@ -66,6 +66,35 @@ func TestCalendarsListsWritableCalendars(t *testing.T) { } } +func TestCalendarsOverlappingPages(t *testing.T) { + t.Parallel() + h := connected(t) + // Two to a page, family ends the first page and starts the second + // again, renamed in between. + renamed := family + renamed.Summary = "Family (renamed)" + h.google.SetCalendars(2, ownPrimary, family, renamed, dinners) + cals, err := h.svc.Calendars(context.Background()) + if err != nil { + t.Fatalf("Calendars: %v", err) + } + got, err := json.Marshal(cals) + if err != nil { + t.Fatal(err) + } + // family once, as first listed, in Google's order. + want := `[` + + `{"id":"household@gmail.com","summary":"household@gmail.com","primary":true,"accessRole":"owner"},` + + `{"id":"family@group.calendar.google.com","summary":"Family","primary":false,"accessRole":"writer"},` + + `{"id":"dinners@group.calendar.google.com","summary":"Dinners","primary":false,"accessRole":"owner"}]` + if string(got) != want { + t.Errorf("Calendars =\n%s\nwant\n%s", got, want) + } + if n := h.google.CalendarCalls(); n != 2 { + t.Errorf("calendarList called %d times, want 2 (every page)", n) + } +} + func TestCalendarsEmpty(t *testing.T) { t.Parallel() h := connected(t)