gogcli/internal/integration/integration_test.go
Toni Melisma 2c7426711c
feat(calendar): add --send-updates flag to update and delete commands (#163)
* feat(calendar): add --send-updates flag to update and delete commands

Add --send-updates flag to CalendarUpdateCmd and CalendarDeleteCmd to
control notification behavior when modifying or canceling events with
attendees.

Without this flag, attendees are not notified of updates or cancellations,
causing:
- Stale events remaining in attendees' calendars
- Duplicate/overlapping events when updates are made
- Data synchronization issues for systems managing calendars at arm's length

Changes:
- Add SendUpdates field to CalendarUpdateCmd and CalendarDeleteCmd
- Validate SendUpdates value (all, externalOnly, none)
- Pass SendUpdates to Patch and Delete API calls
- Update truncateParentRecurrence helper to accept sendUpdates parameter
- Add tests for validation and API integration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(calendar): add integration tests for --send-updates flag

Add TestCalendarSendUpdates Go integration test and shell live test
that verify attendee notifications are sent on create/update/delete.

Tests are opt-in via GOG_IT_ATTENDEE / GOG_LIVE_CALENDAR_ATTENDEE env vars.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(calendar): polish --send-updates landing

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 23:02:27 +01:00

204 lines
5.1 KiB
Go

//go:build integration
package integration
import (
"context"
"os"
"strings"
"testing"
"time"
"google.golang.org/api/calendar/v3"
"github.com/steipete/gogcli/internal/authclient"
"github.com/steipete/gogcli/internal/config"
"github.com/steipete/gogcli/internal/googleapi"
"github.com/steipete/gogcli/internal/googleauth"
"github.com/steipete/gogcli/internal/secrets"
)
func integrationAccount(t *testing.T) string {
t.Helper()
if v := strings.TrimSpace(os.Getenv("GOG_IT_ACCOUNT")); v != "" {
return v
}
store, err := secrets.OpenDefault()
if err != nil {
t.Skipf("open secrets store (set GOG_IT_ACCOUNT to avoid keyring prompts): %v", err)
}
if v, err := store.GetDefaultAccount(config.DefaultClientName); err == nil && strings.TrimSpace(v) != "" {
return v
}
tokens, err := store.ListTokens()
if err != nil {
t.Skipf("list tokens: %v", err)
}
if len(tokens) == 1 && strings.TrimSpace(tokens[0].Email) != "" {
return tokens[0].Email
}
t.Skip("set GOG_IT_ACCOUNT (or set a default account via `gog auth manage`, or store exactly one token)")
return ""
}
func TestDriveSmoke(t *testing.T) {
account := integrationAccount(t)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
svc, err := googleapi.NewDrive(ctx, account)
if err != nil {
t.Fatalf("NewDrive: %v", err)
}
_, err = svc.Files.List().
Q("trashed = false").
PageSize(1).
SupportsAllDrives(true).
IncludeItemsFromAllDrives(true).
Fields("files(id)").
Do()
if err != nil {
t.Fatalf("Drive list: %v", err)
}
}
func TestCalendarSmoke(t *testing.T) {
account := integrationAccount(t)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
svc, err := googleapi.NewCalendar(ctx, account)
if err != nil {
t.Fatalf("NewCalendar: %v", err)
}
_, err = svc.CalendarList.List().MaxResults(1).Do()
if err != nil {
t.Fatalf("Calendar list: %v", err)
}
}
func TestGmailSmoke(t *testing.T) {
account := integrationAccount(t)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
svc, err := googleapi.NewGmail(ctx, account)
if err != nil {
t.Fatalf("NewGmail: %v", err)
}
_, err = svc.Users.Labels.List("me").Do()
if err != nil {
t.Fatalf("Gmail labels: %v", err)
}
}
func TestAuthRefreshTokenSmoke(t *testing.T) {
account := integrationAccount(t)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
store, err := secrets.OpenDefault()
if err != nil {
t.Fatalf("OpenDefault: %v", err)
}
client, err := authclient.ResolveClientWithOverride(account, "")
if err != nil {
t.Fatalf("ResolveClient: %v", err)
}
tok, err := store.GetToken(client, account)
if err != nil {
t.Fatalf("GetToken: %v", err)
}
scopes := tok.Scopes
if len(scopes) == 0 {
scopes = nil
}
if err := googleauth.CheckRefreshToken(ctx, client, tok.RefreshToken, scopes, 15*time.Second); err != nil {
t.Fatalf("CheckRefreshToken: %v", err)
}
}
func TestContactsSmoke(t *testing.T) {
account := integrationAccount(t)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
svc, err := googleapi.NewPeopleContacts(ctx, account)
if err != nil {
t.Fatalf("NewPeople: %v", err)
}
_, err = svc.People.Connections.List("people/me").PersonFields("names").PageSize(1).Do()
if err != nil {
t.Fatalf("People connections: %v", err)
}
}
func TestClassroomSmoke(t *testing.T) {
account := integrationAccount(t)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
svc, err := googleapi.NewClassroom(ctx, account)
if err != nil {
t.Fatalf("NewClassroom: %v", err)
}
_, err = svc.Courses.List().PageSize(1).Do()
if err != nil {
t.Fatalf("Courses list: %v", err)
}
}
func TestCalendarSendUpdates(t *testing.T) {
account := integrationAccount(t)
attendee := strings.TrimSpace(os.Getenv("GOG_IT_ATTENDEE"))
if attendee == "" {
t.Skip("set GOG_IT_ATTENDEE to test --send-updates with attendees")
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
svc, err := googleapi.NewCalendar(ctx, account)
if err != nil {
t.Fatalf("NewCalendar: %v", err)
}
// Create event with attendee
start := time.Now().Add(time.Hour).Truncate(time.Minute)
event := &calendar.Event{
Summary: "gogcli-send-updates-test",
Start: &calendar.EventDateTime{DateTime: start.Format(time.RFC3339)},
End: &calendar.EventDateTime{DateTime: start.Add(time.Hour).Format(time.RFC3339)},
Attendees: []*calendar.EventAttendee{{Email: attendee}},
}
created, err := svc.Events.Insert("primary", event).SendUpdates("all").Do()
if err != nil {
t.Fatalf("Insert: %v", err)
}
defer svc.Events.Delete("primary", created.Id).SendUpdates("all").Do()
// Update with SendUpdates
_, err = svc.Events.Patch("primary", created.Id, &calendar.Event{
Summary: "gogcli-send-updates-test-UPDATED",
}).SendUpdates("all").Do()
if err != nil {
t.Fatalf("Patch with SendUpdates: %v", err)
}
// Delete happens in defer with SendUpdates
t.Logf("Created and updated event %s with attendee %s - check attendee email for notifications", created.Id, attendee)
}