Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion internal/cmd/time_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
"log/slog"
"strings"
"time"

Expand Down Expand Up @@ -52,14 +53,19 @@ func getCalendarLocation(ctx context.Context, svc *calendar.Service, calendarID
}

// getUserTimezone fetches the timezone from the user's primary calendar.
// When no primary calendar exists (e.g. pure service-account mode), it
// falls back to UTC so that calendar commands remain functional.
func getUserTimezone(ctx context.Context, svc *calendar.Service) (*time.Location, error) {
cal, err := svc.CalendarList.Get("primary").Context(ctx).Do()
if err != nil {
if isNotFoundAPIError(err) {
slog.Warn("primary calendar not found, falling back to UTC (expected in pure SA mode)")
return time.UTC, nil
}
return nil, fmt.Errorf("failed to get primary calendar: %w", err)
}

if cal.TimeZone == "" {
// Fall back to UTC if no timezone set
return time.UTC, nil
}

Expand Down
64 changes: 64 additions & 0 deletions internal/cmd/time_helpers_sa_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package cmd

import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"

"google.golang.org/api/calendar/v3"
"google.golang.org/api/option"
)

// newCalendarServicePrimaryNotFound creates a calendar service whose
// CalendarList.Get("primary") endpoint returns 404, simulating pure
// service-account mode where no primary calendar exists.
func newCalendarServicePrimaryNotFound(t *testing.T) *calendar.Service {
t.Helper()

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// All calendarList requests return 404.
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"error":{"code":404,"message":"Not Found","errors":[{"message":"Not Found","domain":"global","reason":"notFound"}]}}`))
}))
t.Cleanup(srv.Close)

svc, err := calendar.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
return svc
}

func TestGetUserTimezone_PrimaryNotFound_FallsBackToUTC(t *testing.T) {
svc := newCalendarServicePrimaryNotFound(t)

loc, err := getUserTimezone(context.Background(), svc)
if err != nil {
t.Fatalf("getUserTimezone returned error: %v", err)
}
if loc != time.UTC {
t.Fatalf("expected UTC fallback, got %v", loc)
}
}

func TestResolveTimeRange_PrimaryNotFound_Succeeds(t *testing.T) {
svc := newCalendarServicePrimaryNotFound(t)

tr, err := ResolveTimeRange(context.Background(), svc, TimeRangeFlags{})
if err != nil {
t.Fatalf("ResolveTimeRange returned error: %v", err)
}
if tr.Location != time.UTC {
t.Fatalf("expected UTC location, got %v", tr.Location)
}
if tr.From.IsZero() || tr.To.IsZero() {
t.Fatal("expected non-zero time range")
}
}
Loading