diff --git a/internal/cmd/auth_accounts.go b/internal/cmd/auth_accounts.go index 2f7966b..420f6dd 100644 --- a/internal/cmd/auth_accounts.go +++ b/internal/cmd/auth_accounts.go @@ -463,10 +463,10 @@ func (c *AuthKeepCmd) Run(ctx context.Context, _ *RootFlags) error { return err } - if err := os.WriteFile(destPath, data, 0o600); err != nil { + if err := writePrivateFile(destPath, data, 0o600); err != nil { return fmt.Errorf("write service account: %w", err) } - if err := os.WriteFile(genericPath, data, 0o600); err != nil { + if err := writePrivateFile(genericPath, data, 0o600); err != nil { return fmt.Errorf("write service account: %w", err) } diff --git a/internal/cmd/calendar_create_update_test.go b/internal/cmd/calendar_create_update_test.go index 20b4737..cc43adc 100644 --- a/internal/cmd/calendar_create_update_test.go +++ b/internal/cmd/calendar_create_update_test.go @@ -490,6 +490,12 @@ func TestCalendarCreateCmd_EventTypeWorkingLocation(t *testing.T) { if gotEvent.WorkingLocationProperties == nil || gotEvent.WorkingLocationProperties.Type != "officeLocation" { t.Fatalf("unexpected working location props: %#v", gotEvent.WorkingLocationProperties) } + if gotEvent.Transparency != transparencyTransparent { + t.Fatalf("expected transparent working location, got %q", gotEvent.Transparency) + } + if gotEvent.Visibility != "public" { + t.Fatalf("expected public working location visibility, got %q", gotEvent.Visibility) + } } func TestCalendarUpdateCmd_EventTypeOOO(t *testing.T) { @@ -549,6 +555,59 @@ func TestCalendarUpdateCmd_EventTypeOOO(t *testing.T) { } } +func TestCalendarUpdateCmd_EventTypeWorkingLocationDefaults(t *testing.T) { + origNew := newCalendarService + t.Cleanup(func() { newCalendarService = origNew }) + + var gotEvent calendar.Event + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := strings.TrimPrefix(r.URL.Path, "/calendar/v3") + if r.Method == http.MethodPatch && path == "/calendars/cal@example.com/events/ev" { + _ = json.NewDecoder(r.Body).Decode(&gotEvent) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "id": "ev", + }) + return + } + http.NotFound(w, r) + })) + defer 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) + } + newCalendarService = func(context.Context, string) (*calendar.Service, error) { return svc, nil } + + ctx := newCalendarJSONOutputContext(t, os.Stdout, os.Stderr) + + cmd := &CalendarUpdateCmd{} + if err := runKong(t, cmd, []string{ + "cal@example.com", + "ev", + "--event-type", "working-location", + "--working-location-type", "office", + "--working-office-label", "HQ", + }, ctx, &RootFlags{Account: "a@b.com"}); err != nil { + t.Fatalf("runKong: %v", err) + } + + if gotEvent.EventType != eventTypeWorkingLocation { + t.Fatalf("expected workingLocation event type, got %q", gotEvent.EventType) + } + if gotEvent.Transparency != transparencyTransparent { + t.Fatalf("expected transparent working location, got %q", gotEvent.Transparency) + } + if gotEvent.Visibility != "public" { + t.Fatalf("expected public working location visibility, got %q", gotEvent.Visibility) + } +} + func TestCalendarUpdateCmd_SendUpdates(t *testing.T) { origNew := newCalendarService t.Cleanup(func() { newCalendarService = origNew }) diff --git a/internal/cmd/calendar_edit.go b/internal/cmd/calendar_edit.go index 47813e2..302fa84 100644 --- a/internal/cmd/calendar_edit.go +++ b/internal/cmd/calendar_edit.go @@ -133,9 +133,19 @@ func applyEventTypeTransparencyDefault(transparency, eventType string) string { if transparency == "" && (eventType == eventTypeFocusTime || eventType == eventTypeOutOfOffice) { return transparencyOpaque } + if transparency == "" && eventType == eventTypeWorkingLocation { + return transparencyTransparent + } return transparency } +func applyEventTypeVisibilityDefault(visibility, eventType string) string { + if visibility == "" && eventType == eventTypeWorkingLocation { + return visibilityPublic + } + return visibility +} + func (c *CalendarCreateCmd) applyCreateEventType(event *calendar.Event, eventType string) error { switch eventType { case eventTypeDefault: @@ -646,6 +656,14 @@ func (c *CalendarUpdateCmd) applyEventTypeProperties(kctx *kong.Context, patch * patch.Transparency = transparencyOpaque changed = true } + if eventTypeRequested && !flagProvided(kctx, "transparency") && eventType == eventTypeWorkingLocation { + patch.Transparency = transparencyTransparent + changed = true + } + if eventTypeRequested && !flagProvided(kctx, "visibility") && eventType == eventTypeWorkingLocation { + patch.Visibility = visibilityPublic + changed = true + } switch eventType { case eventTypeFocusTime: diff --git a/internal/cmd/calendar_event_plan.go b/internal/cmd/calendar_event_plan.go index 73fcf9f..7168e69 100644 --- a/internal/cmd/calendar_event_plan.go +++ b/internal/cmd/calendar_event_plan.go @@ -74,7 +74,7 @@ func buildCalendarCreatePlan(c *CalendarCreateCmd) (*calendarCreatePlan, error) Recurrence: buildRecurrence(c.Recurrence), Reminders: reminders, ColorId: colorID, - Visibility: visibility, + Visibility: applyEventTypeVisibilityDefault(visibility, eventType), Transparency: applyEventTypeTransparencyDefault(transparency, eventType), ConferenceData: buildConferenceData(c.WithMeet), Attachments: buildAttachments(c.Attachments), diff --git a/internal/cmd/calendar_validate.go b/internal/cmd/calendar_validate.go index b7cd456..e755bcc 100644 --- a/internal/cmd/calendar_validate.go +++ b/internal/cmd/calendar_validate.go @@ -9,6 +9,7 @@ import ( const ( transparencyOpaque = "opaque" transparencyTransparent = "transparent" + visibilityPublic = "public" sendUpdatesNone = "none" ) diff --git a/internal/cmd/calendar_working_location.go b/internal/cmd/calendar_working_location.go index 60dff72..e3740c1 100644 --- a/internal/cmd/calendar_working_location.go +++ b/internal/cmd/calendar_working_location.go @@ -37,6 +37,8 @@ func (c *CalendarWorkingLocationCmd) Run(ctx context.Context, flags *RootFlags) Start: &calendar.EventDateTime{Date: strings.TrimSpace(c.From)}, End: &calendar.EventDateTime{Date: strings.TrimSpace(c.To)}, EventType: eventTypeWorkingLocation, + Visibility: visibilityPublic, + Transparency: transparencyTransparent, WorkingLocationProperties: props, } diff --git a/internal/cmd/calendar_working_location_test.go b/internal/cmd/calendar_working_location_test.go index 55591e6..488c5ed 100644 --- a/internal/cmd/calendar_working_location_test.go +++ b/internal/cmd/calendar_working_location_test.go @@ -125,6 +125,12 @@ func TestCalendarWorkingLocation_RunJSON(t *testing.T) { if gotEvent.EventType != "workingLocation" { t.Fatalf("unexpected event type: %q", gotEvent.EventType) } + if gotEvent.Transparency != transparencyTransparent { + t.Fatalf("unexpected transparency: %q", gotEvent.Transparency) + } + if gotEvent.Visibility != "public" { + t.Fatalf("unexpected visibility: %q", gotEvent.Visibility) + } if gotEvent.Summary != "Working from HQ" { t.Fatalf("unexpected summary: %q", gotEvent.Summary) } diff --git a/internal/cmd/output_file_helpers.go b/internal/cmd/output_file_helpers.go index 2e94a88..f9aabc1 100644 --- a/internal/cmd/output_file_helpers.go +++ b/internal/cmd/output_file_helpers.go @@ -60,3 +60,19 @@ func createUserOutputFile(path string) (*os.File, string, error) { DirMode: 0o700, }) } + +func writePrivateFile(path string, data []byte, mode os.FileMode) error { + if mode == 0 { + mode = 0o600 + } + // Path is resolved by the caller. This helper is for app-owned/private outputs. + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) //nolint:gosec // caller controls target path semantics + if err != nil { + return err + } + if _, err := f.Write(data); err != nil { + _ = f.Close() + return err + } + return f.Close() +} diff --git a/scripts/live-tests/calendar.sh b/scripts/live-tests/calendar.sh index 380e9c4..afbd0f9 100644 --- a/scripts/live-tests/calendar.sh +++ b/scripts/live-tests/calendar.sh @@ -43,7 +43,9 @@ PY run_required "calendar" "calendar delete event" gog calendar delete primary "$ev_id" --force >/dev/null - if ! skip "calendar-enterprise"; then + if is_consumer_account "$ACCOUNT"; then + echo "==> calendar enterprise event types (skipped; Workspace/enterprise only)" + elif ! skip "calendar-enterprise"; then local focus_json focus_id ooo_json ooo_id wl_json wl_id focus_json=$(gog calendar create primary --event-type focus-time --from "$START" --to "$END" --json 2>/dev/null || true) if [ -n "$focus_json" ]; then