Skip to content

Commit 77fc811

Browse files
committed
test: substantially raise unit coverage
1 parent b15eda9 commit 77fc811

File tree

6 files changed

+468
-0
lines changed

6 files changed

+468
-0
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package cmd
2+
3+
import (
4+
"testing"
5+
6+
"google.golang.org/api/people/v1"
7+
)
8+
9+
func TestPrimaryName_EdgeCases(t *testing.T) {
10+
if got := primaryName(nil); got != "" {
11+
t.Fatalf("expected empty, got %q", got)
12+
}
13+
if got := primaryName(&people.Person{}); got != "" {
14+
t.Fatalf("expected empty, got %q", got)
15+
}
16+
if got := primaryName(&people.Person{Names: []*people.Name{nil}}); got != "" {
17+
t.Fatalf("expected empty, got %q", got)
18+
}
19+
20+
p1 := &people.Person{Names: []*people.Name{{DisplayName: "Ada Lovelace"}}}
21+
if got := primaryName(p1); got != "Ada Lovelace" {
22+
t.Fatalf("unexpected: %q", got)
23+
}
24+
25+
p2 := &people.Person{Names: []*people.Name{{GivenName: "Ada", FamilyName: "Lovelace"}}}
26+
if got := primaryName(p2); got != "Ada Lovelace" {
27+
t.Fatalf("unexpected: %q", got)
28+
}
29+
}
30+
31+
func TestPrimaryEmailAndPhone_EdgeCases(t *testing.T) {
32+
if got := primaryEmail(nil); got != "" {
33+
t.Fatalf("expected empty, got %q", got)
34+
}
35+
if got := primaryEmail(&people.Person{}); got != "" {
36+
t.Fatalf("expected empty, got %q", got)
37+
}
38+
if got := primaryEmail(&people.Person{EmailAddresses: []*people.EmailAddress{nil}}); got != "" {
39+
t.Fatalf("expected empty, got %q", got)
40+
}
41+
if got := primaryEmail(&people.Person{EmailAddresses: []*people.EmailAddress{{Value: "a@b.com"}}}); got != "a@b.com" {
42+
t.Fatalf("unexpected: %q", got)
43+
}
44+
45+
if got := primaryPhone(nil); got != "" {
46+
t.Fatalf("expected empty, got %q", got)
47+
}
48+
if got := primaryPhone(&people.Person{}); got != "" {
49+
t.Fatalf("expected empty, got %q", got)
50+
}
51+
if got := primaryPhone(&people.Person{PhoneNumbers: []*people.PhoneNumber{nil}}); got != "" {
52+
t.Fatalf("expected empty, got %q", got)
53+
}
54+
if got := primaryPhone(&people.Person{PhoneNumbers: []*people.PhoneNumber{{Value: "+1"}}}); got != "+1" {
55+
t.Fatalf("unexpected: %q", got)
56+
}
57+
}

internal/cmd/drive_ls_cmd_test.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"io"
8+
"net/http"
9+
"net/http/httptest"
10+
"strings"
11+
"testing"
12+
13+
"github.com/steipete/gogcli/internal/outfmt"
14+
"github.com/steipete/gogcli/internal/ui"
15+
"google.golang.org/api/drive/v3"
16+
"google.golang.org/api/option"
17+
)
18+
19+
func TestDriveLsCmd_TextAndJSON(t *testing.T) {
20+
origNew := newDriveService
21+
t.Cleanup(func() { newDriveService = origNew })
22+
23+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24+
switch {
25+
case r.Method == http.MethodGet && (r.URL.Path == "/drive/v3/files" || r.URL.Path == "/files"):
26+
w.Header().Set("Content-Type", "application/json")
27+
_ = json.NewEncoder(w).Encode(map[string]any{
28+
"files": []map[string]any{
29+
{
30+
"id": "f1",
31+
"name": "Doc",
32+
"mimeType": "application/pdf",
33+
"size": "1024",
34+
"modifiedTime": "2025-12-12T14:37:47Z",
35+
},
36+
{
37+
"id": "d1",
38+
"name": "Folder",
39+
"mimeType": "application/vnd.google-apps.folder",
40+
"size": "0",
41+
"modifiedTime": "2025-12-11T00:00:00Z",
42+
},
43+
},
44+
"nextPageToken": "npt",
45+
})
46+
return
47+
default:
48+
http.NotFound(w, r)
49+
return
50+
}
51+
}))
52+
defer srv.Close()
53+
54+
svc, err := drive.NewService(context.Background(),
55+
option.WithoutAuthentication(),
56+
option.WithHTTPClient(srv.Client()),
57+
option.WithEndpoint(srv.URL+"/"),
58+
)
59+
if err != nil {
60+
t.Fatalf("NewService: %v", err)
61+
}
62+
newDriveService = func(context.Context, string) (*drive.Service, error) { return svc, nil }
63+
64+
flags := &rootFlags{Account: "a@b.com"}
65+
66+
// Text mode: table to stdout + next page hint to stderr.
67+
var errBuf bytes.Buffer
68+
u, err := ui.New(ui.Options{Stdout: io.Discard, Stderr: &errBuf, Color: "never"})
69+
if err != nil {
70+
t.Fatalf("ui.New: %v", err)
71+
}
72+
ctx := ui.WithUI(context.Background(), u)
73+
ctx = outfmt.WithMode(ctx, outfmt.ModeText)
74+
75+
textOut := captureStdout(t, func() {
76+
cmd := newDriveLsCmd(flags)
77+
cmd.SetContext(ctx)
78+
cmd.SetArgs([]string{})
79+
if execErr := cmd.Execute(); execErr != nil {
80+
t.Fatalf("execute: %v", execErr)
81+
}
82+
})
83+
84+
if !strings.Contains(textOut, "ID") || !strings.Contains(textOut, "NAME") {
85+
t.Fatalf("unexpected table header: %q", textOut)
86+
}
87+
if !strings.Contains(textOut, "f1") || !strings.Contains(textOut, "Doc") || !strings.Contains(textOut, "1.0 KB") {
88+
t.Fatalf("missing file row: %q", textOut)
89+
}
90+
if !strings.Contains(textOut, "d1") || !strings.Contains(textOut, "Folder") || !strings.Contains(textOut, "folder") {
91+
t.Fatalf("missing folder row: %q", textOut)
92+
}
93+
if !strings.Contains(errBuf.String(), "--page npt") {
94+
t.Fatalf("missing next page hint: %q", errBuf.String())
95+
}
96+
97+
// JSON mode: JSON to stdout and no next-page hint to stderr.
98+
var errBuf2 bytes.Buffer
99+
u2, err := ui.New(ui.Options{Stdout: io.Discard, Stderr: &errBuf2, Color: "never"})
100+
if err != nil {
101+
t.Fatalf("ui.New: %v", err)
102+
}
103+
ctx2 := ui.WithUI(context.Background(), u2)
104+
ctx2 = outfmt.WithMode(ctx2, outfmt.ModeJSON)
105+
106+
jsonOut := captureStdout(t, func() {
107+
cmd := newDriveLsCmd(flags)
108+
cmd.SetContext(ctx2)
109+
cmd.SetArgs([]string{})
110+
if execErr := cmd.Execute(); execErr != nil {
111+
t.Fatalf("execute: %v", execErr)
112+
}
113+
})
114+
if errBuf2.String() != "" {
115+
t.Fatalf("expected no stderr in json mode, got: %q", errBuf2.String())
116+
}
117+
118+
var parsed struct {
119+
Files []*drive.File `json:"files"`
120+
NextPageToken string `json:"nextPageToken"`
121+
}
122+
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
123+
t.Fatalf("json parse: %v\nout=%q", err, jsonOut)
124+
}
125+
if parsed.NextPageToken != "npt" || len(parsed.Files) != 2 {
126+
t.Fatalf("unexpected json: %#v", parsed)
127+
}
128+
}

internal/cmd/gmail_labels_cmd_test.go

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,190 @@ func TestGmailLabelsGetCmd_JSON(t *testing.T) {
106106
t.Fatalf("unexpected counts: %#v", parsed.Label)
107107
}
108108
}
109+
110+
func TestGmailLabelsListCmd_TextAndJSON(t *testing.T) {
111+
origNew := newGmailService
112+
t.Cleanup(func() { newGmailService = origNew })
113+
114+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
115+
switch {
116+
case strings.HasSuffix(r.URL.Path, "/users/me/labels") || strings.HasSuffix(r.URL.Path, "/gmail/v1/users/me/labels"):
117+
w.Header().Set("Content-Type", "application/json")
118+
_ = json.NewEncoder(w).Encode(map[string]any{
119+
"labels": []map[string]any{
120+
{"id": "INBOX", "name": "INBOX", "type": "system"},
121+
{"id": "Label_1", "name": "Custom", "type": "user"},
122+
},
123+
})
124+
return
125+
default:
126+
http.NotFound(w, r)
127+
return
128+
}
129+
}))
130+
defer srv.Close()
131+
132+
svc, err := gmail.NewService(context.Background(),
133+
option.WithoutAuthentication(),
134+
option.WithHTTPClient(srv.Client()),
135+
option.WithEndpoint(srv.URL+"/"),
136+
)
137+
if err != nil {
138+
t.Fatalf("NewService: %v", err)
139+
}
140+
newGmailService = func(context.Context, string) (*gmail.Service, error) { return svc, nil }
141+
142+
flags := &rootFlags{Account: "a@b.com"}
143+
144+
// Text output uses tabwriter to os.Stdout.
145+
textOut := captureStdout(t, func() {
146+
u, uiErr := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
147+
if uiErr != nil {
148+
t.Fatalf("ui.New: %v", uiErr)
149+
}
150+
ctx := ui.WithUI(context.Background(), u)
151+
ctx = outfmt.WithMode(ctx, outfmt.ModeText)
152+
153+
cmd := newGmailLabelsListCmd(flags)
154+
cmd.SetContext(ctx)
155+
cmd.SetArgs([]string{})
156+
if err := cmd.Execute(); err != nil {
157+
t.Fatalf("execute: %v", err)
158+
}
159+
})
160+
if !strings.Contains(textOut, "ID") || !strings.Contains(textOut, "NAME") {
161+
t.Fatalf("unexpected output: %q", textOut)
162+
}
163+
if !strings.Contains(textOut, "INBOX") || !strings.Contains(textOut, "Custom") {
164+
t.Fatalf("missing labels: %q", textOut)
165+
}
166+
167+
jsonOut := captureStdout(t, func() {
168+
u, uiErr := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
169+
if uiErr != nil {
170+
t.Fatalf("ui.New: %v", uiErr)
171+
}
172+
ctx := ui.WithUI(context.Background(), u)
173+
ctx = outfmt.WithMode(ctx, outfmt.ModeJSON)
174+
175+
cmd := newGmailLabelsListCmd(flags)
176+
cmd.SetContext(ctx)
177+
cmd.SetArgs([]string{})
178+
if err := cmd.Execute(); err != nil {
179+
t.Fatalf("execute: %v", err)
180+
}
181+
})
182+
183+
var parsed struct {
184+
Labels []*gmail.Label `json:"labels"`
185+
}
186+
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
187+
t.Fatalf("json parse: %v\nout=%q", err, jsonOut)
188+
}
189+
if len(parsed.Labels) != 2 {
190+
t.Fatalf("unexpected labels: %#v", parsed.Labels)
191+
}
192+
}
193+
194+
func TestGmailLabelsModifyCmd_JSON(t *testing.T) {
195+
origNew := newGmailService
196+
t.Cleanup(func() { newGmailService = origNew })
197+
198+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
199+
switch {
200+
case r.Method == http.MethodGet && (strings.HasSuffix(r.URL.Path, "/users/me/labels") || strings.HasSuffix(r.URL.Path, "/gmail/v1/users/me/labels")):
201+
w.Header().Set("Content-Type", "application/json")
202+
_ = json.NewEncoder(w).Encode(map[string]any{
203+
"labels": []map[string]any{
204+
{"id": "INBOX", "name": "INBOX", "type": "system"},
205+
{"id": "Label_1", "name": "Custom", "type": "user"},
206+
},
207+
})
208+
return
209+
case r.Method == http.MethodPost && (strings.Contains(r.URL.Path, "/users/me/threads/") || strings.Contains(r.URL.Path, "/gmail/v1/users/me/threads/")) && strings.HasSuffix(r.URL.Path, "/modify"):
210+
parts := strings.Split(r.URL.Path, "/")
211+
threadID := parts[len(parts)-2]
212+
213+
var body struct {
214+
AddLabelIds []string `json:"addLabelIds"`
215+
RemoveLabelIds []string `json:"removeLabelIds"`
216+
}
217+
_ = json.NewDecoder(r.Body).Decode(&body)
218+
if len(body.AddLabelIds) != 1 || body.AddLabelIds[0] != "INBOX" {
219+
http.Error(w, "bad addLabelIds", http.StatusBadRequest)
220+
return
221+
}
222+
if len(body.RemoveLabelIds) != 1 || body.RemoveLabelIds[0] != "Label_1" {
223+
http.Error(w, "bad removeLabelIds", http.StatusBadRequest)
224+
return
225+
}
226+
227+
if threadID == "t2" {
228+
w.Header().Set("Content-Type", "application/json")
229+
w.WriteHeader(http.StatusInternalServerError)
230+
_ = json.NewEncoder(w).Encode(map[string]any{
231+
"error": map[string]any{"code": 500, "message": "boom"},
232+
})
233+
return
234+
}
235+
236+
w.Header().Set("Content-Type", "application/json")
237+
_ = json.NewEncoder(w).Encode(map[string]any{})
238+
return
239+
default:
240+
http.NotFound(w, r)
241+
return
242+
}
243+
}))
244+
defer srv.Close()
245+
246+
svc, err := gmail.NewService(context.Background(),
247+
option.WithoutAuthentication(),
248+
option.WithHTTPClient(srv.Client()),
249+
option.WithEndpoint(srv.URL+"/"),
250+
)
251+
if err != nil {
252+
t.Fatalf("NewService: %v", err)
253+
}
254+
newGmailService = func(context.Context, string) (*gmail.Service, error) { return svc, nil }
255+
256+
flags := &rootFlags{Account: "a@b.com"}
257+
258+
out := captureStdout(t, func() {
259+
u, uiErr := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
260+
if uiErr != nil {
261+
t.Fatalf("ui.New: %v", uiErr)
262+
}
263+
ctx := ui.WithUI(context.Background(), u)
264+
ctx = outfmt.WithMode(ctx, outfmt.ModeJSON)
265+
266+
cmd := newGmailLabelsModifyCmd(flags)
267+
cmd.SetContext(ctx)
268+
cmd.SetArgs([]string{"t1", "t2"})
269+
cmd.Flags().Set("add", "INBOX")
270+
cmd.Flags().Set("remove", "Custom")
271+
if err := cmd.Execute(); err != nil {
272+
t.Fatalf("execute: %v", err)
273+
}
274+
})
275+
276+
var parsed struct {
277+
Results []struct {
278+
ThreadID string `json:"threadId"`
279+
Success bool `json:"success"`
280+
Error string `json:"error"`
281+
} `json:"results"`
282+
}
283+
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
284+
t.Fatalf("json parse: %v\nout=%q", err, out)
285+
}
286+
if len(parsed.Results) != 2 {
287+
t.Fatalf("unexpected results: %#v", parsed.Results)
288+
}
289+
if parsed.Results[0].ThreadID != "t1" || !parsed.Results[0].Success {
290+
t.Fatalf("unexpected result 0: %#v", parsed.Results[0])
291+
}
292+
if parsed.Results[1].ThreadID != "t2" || parsed.Results[1].Success || parsed.Results[1].Error == "" {
293+
t.Fatalf("unexpected result 1: %#v", parsed.Results[1])
294+
}
295+
}

0 commit comments

Comments
 (0)