Skip to content

Commit b27757d

Browse files
test(calendar): add integration tests for propose-time command
1 parent 3071ee1 commit b27757d

2 files changed

Lines changed: 173 additions & 1 deletion

File tree

internal/cmd/calendar_propose_time.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,4 @@ var openProposeTimeBrowser = func(url string) error {
179179
cmd = exec.Command("xdg-open", url)
180180
}
181181
return cmd.Start()
182-
}
182+
}

internal/cmd/calendar_propose_time_test.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
package cmd
22

33
import (
4+
"context"
45
"encoding/base64"
6+
"encoding/json"
7+
"io"
8+
"net/http"
9+
"net/http/httptest"
10+
"os"
11+
"strings"
512
"testing"
13+
14+
"google.golang.org/api/calendar/v3"
15+
"google.golang.org/api/option"
16+
17+
"github.com/steipete/gogcli/internal/ui"
618
)
719

820
func TestProposeTimeURLGeneration(t *testing.T) {
@@ -38,3 +50,163 @@ func TestProposeTimeURLGeneration(t *testing.T) {
3850
})
3951
}
4052
}
53+
54+
func TestCalendarProposeTimeCmd_Text(t *testing.T) {
55+
origNew := newCalendarService
56+
origOpen := openProposeTimeBrowser
57+
t.Cleanup(func() {
58+
newCalendarService = origNew
59+
openProposeTimeBrowser = origOpen
60+
})
61+
62+
// Mock browser open to track if called
63+
var browserOpened string
64+
openProposeTimeBrowser = func(url string) error {
65+
browserOpened = url
66+
return nil
67+
}
68+
69+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
70+
path := strings.TrimPrefix(r.URL.Path, "/calendar/v3")
71+
if strings.Contains(path, "/calendars/cal1/events/evt1") && r.Method == http.MethodGet {
72+
w.Header().Set("Content-Type", "application/json")
73+
_ = json.NewEncoder(w).Encode(map[string]any{
74+
"id": "evt1",
75+
"summary": "Team Meeting",
76+
"start": map[string]string{"dateTime": "2026-01-16T19:30:00-08:00"},
77+
"end": map[string]string{"dateTime": "2026-01-16T20:30:00-08:00"},
78+
"attendees": []map[string]any{
79+
{"email": "a@b.com", "self": true},
80+
{"email": "organizer@b.com", "organizer": true},
81+
},
82+
})
83+
return
84+
}
85+
http.NotFound(w, r)
86+
}))
87+
defer srv.Close()
88+
89+
svc, err := calendar.NewService(context.Background(),
90+
option.WithoutAuthentication(),
91+
option.WithHTTPClient(srv.Client()),
92+
option.WithEndpoint(srv.URL+"/"),
93+
)
94+
if err != nil {
95+
t.Fatalf("NewService: %v", err)
96+
}
97+
newCalendarService = func(context.Context, string) (*calendar.Service, error) { return svc, nil }
98+
99+
flags := &RootFlags{Account: "a@b.com"}
100+
out := captureStdout(t, func() {
101+
u, uiErr := ui.New(ui.Options{Stdout: os.Stdout, Stderr: io.Discard, Color: "never"})
102+
if uiErr != nil {
103+
t.Fatalf("ui.New: %v", uiErr)
104+
}
105+
ctx := ui.WithUI(context.Background(), u)
106+
107+
cmd := &CalendarProposeTimeCmd{}
108+
if err := runKong(t, cmd, []string{"cal1", "evt1", "--open"}, ctx, flags); err != nil {
109+
t.Fatalf("propose-time: %v", err)
110+
}
111+
})
112+
113+
// Verify output contains expected fields
114+
if !strings.Contains(out, "propose_url") {
115+
t.Errorf("output missing propose_url: %q", out)
116+
}
117+
if !strings.Contains(out, "Team Meeting") {
118+
t.Errorf("output missing event summary: %q", out)
119+
}
120+
if !strings.Contains(out, "proposetime/") {
121+
t.Errorf("output missing proposetime URL path: %q", out)
122+
}
123+
124+
// Verify browser was opened
125+
if browserOpened == "" {
126+
t.Error("browser was not opened despite --open flag")
127+
}
128+
if !strings.Contains(browserOpened, "proposetime/") {
129+
t.Errorf("browser URL incorrect: %q", browserOpened)
130+
}
131+
}
132+
133+
func TestCalendarProposeTimeCmd_WithDecline(t *testing.T) {
134+
origNew := newCalendarService
135+
origOpen := openProposeTimeBrowser
136+
t.Cleanup(func() {
137+
newCalendarService = origNew
138+
openProposeTimeBrowser = origOpen
139+
})
140+
openProposeTimeBrowser = func(url string) error { return nil }
141+
142+
var patchCalled bool
143+
var patchedComment string
144+
145+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
146+
path := strings.TrimPrefix(r.URL.Path, "/calendar/v3")
147+
switch {
148+
case strings.Contains(path, "/calendars/cal1/events/evt1") && r.Method == http.MethodGet:
149+
w.Header().Set("Content-Type", "application/json")
150+
_ = json.NewEncoder(w).Encode(map[string]any{
151+
"id": "evt1",
152+
"summary": "Team Meeting",
153+
"start": map[string]string{"dateTime": "2026-01-16T19:30:00-08:00"},
154+
"end": map[string]string{"dateTime": "2026-01-16T20:30:00-08:00"},
155+
"attendees": []map[string]any{
156+
{"email": "a@b.com", "self": true},
157+
{"email": "organizer@b.com", "organizer": true},
158+
},
159+
})
160+
case strings.Contains(path, "/calendars/cal1/events/evt1") && r.Method == http.MethodPatch:
161+
patchCalled = true
162+
var body map[string]any
163+
_ = json.NewDecoder(r.Body).Decode(&body)
164+
if attendees, ok := body["attendees"].([]any); ok && len(attendees) > 0 {
165+
if att, ok := attendees[0].(map[string]any); ok {
166+
if c, ok := att["comment"].(string); ok {
167+
patchedComment = c
168+
}
169+
}
170+
}
171+
w.Header().Set("Content-Type", "application/json")
172+
_ = json.NewEncoder(w).Encode(map[string]any{"id": "evt1", "summary": "Team Meeting"})
173+
default:
174+
http.NotFound(w, r)
175+
}
176+
}))
177+
defer srv.Close()
178+
179+
svc, err := calendar.NewService(context.Background(),
180+
option.WithoutAuthentication(),
181+
option.WithHTTPClient(srv.Client()),
182+
option.WithEndpoint(srv.URL+"/"),
183+
)
184+
if err != nil {
185+
t.Fatalf("NewService: %v", err)
186+
}
187+
newCalendarService = func(context.Context, string) (*calendar.Service, error) { return svc, nil }
188+
189+
flags := &RootFlags{Account: "a@b.com"}
190+
out := captureStdout(t, func() {
191+
u, uiErr := ui.New(ui.Options{Stdout: os.Stdout, Stderr: io.Discard, Color: "never"})
192+
if uiErr != nil {
193+
t.Fatalf("ui.New: %v", uiErr)
194+
}
195+
ctx := ui.WithUI(context.Background(), u)
196+
197+
cmd := &CalendarProposeTimeCmd{}
198+
if err := runKong(t, cmd, []string{"cal1", "evt1", "--comment", "Can we do 5pm instead?"}, ctx, flags); err != nil {
199+
t.Fatalf("propose-time with decline: %v", err)
200+
}
201+
})
202+
203+
if !patchCalled {
204+
t.Error("PATCH was not called despite --comment flag")
205+
}
206+
if patchedComment != "Can we do 5pm instead?" {
207+
t.Errorf("comment not passed correctly, got: %q", patchedComment)
208+
}
209+
if !strings.Contains(out, "declined\tyes") {
210+
t.Errorf("output should show declined status: %q", out)
211+
}
212+
}

0 commit comments

Comments
 (0)