gogcli/internal/cmd/gmail_sendas_test.go
salmonumbrella 3371e3f3ad
feat(cli): agent ergonomics + gmail watch exclude labels (#201)
* feat(cli): improve agent ergonomics

* fix(cli): address code review findings

- Fix nil pointer dereference in confirmDestructive when flags is nil
- Deduplicate dry-run logic by delegating to dryRunExit
- Remove deprecated net.Error.Temporary() call (dead since Go 1.18)
- Add unit tests for resolveTasklistID and resolveCalendarID

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

* fix: resolve PR #201 conflicts and follow-ups (#201) (thanks @salmonumbrella)

* fix: resolve rebase fallout for PR #201 landing (#201) (thanks @salmonumbrella)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 03:09:49 +01:00

556 lines
17 KiB
Go

package cmd
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"google.golang.org/api/gmail/v1"
"google.golang.org/api/option"
"github.com/steipete/gogcli/internal/outfmt"
"github.com/steipete/gogcli/internal/ui"
)
func TestGmailSendAsListCmd_JSON(t *testing.T) {
origNew := newGmailService
t.Cleanup(func() { newGmailService = origNew })
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/settings/sendAs") && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"sendAs": []map[string]any{
{
"sendAsEmail": "primary@example.com",
"displayName": "Primary User",
"isDefault": true,
"isPrimary": true,
"treatAsAlias": false,
"verificationStatus": "accepted",
},
{
"sendAsEmail": "work@company.com",
"displayName": "Work Alias",
"isDefault": false,
"isPrimary": false,
"treatAsAlias": true,
"verificationStatus": "accepted",
},
},
})
return
}
http.NotFound(w, r)
}))
defer srv.Close()
svc, err := gmail.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newGmailService = func(context.Context, string) (*gmail.Service, error) { return svc, nil }
flags := &RootFlags{Account: "a@b.com", Force: true}
out := captureStdout(t, func() {
u, uiErr := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
if uiErr != nil {
t.Fatalf("ui.New: %v", uiErr)
}
ctx := ui.WithUI(context.Background(), u)
ctx = outfmt.WithMode(ctx, outfmt.Mode{JSON: true})
if err := runKong(t, &GmailSendAsListCmd{}, []string{}, ctx, flags); err != nil {
t.Fatalf("execute: %v", err)
}
})
var parsed struct {
SendAs []struct {
SendAsEmail string `json:"sendAsEmail"`
DisplayName string `json:"displayName"`
IsDefault bool `json:"isDefault"`
VerificationStatus string `json:"verificationStatus"`
} `json:"sendAs"`
}
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
t.Fatalf("json parse: %v\nout=%q", err, out)
}
if len(parsed.SendAs) != 2 {
t.Fatalf("unexpected sendAs count: %d", len(parsed.SendAs))
}
if parsed.SendAs[0].SendAsEmail != "primary@example.com" {
t.Fatalf("unexpected first sendAs: %#v", parsed.SendAs[0])
}
if parsed.SendAs[1].SendAsEmail != "work@company.com" {
t.Fatalf("unexpected second sendAs: %#v", parsed.SendAs[1])
}
}
func TestGmailSendAsGetCmd_JSON(t *testing.T) {
origNew := newGmailService
t.Cleanup(func() { newGmailService = origNew })
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/settings/sendAs/work@company.com") && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"sendAsEmail": "work@company.com",
"displayName": "Work Alias",
"replyToAddress": "replies@company.com",
"signature": "<b>Signature</b>",
"isDefault": false,
"isPrimary": false,
"treatAsAlias": true,
"verificationStatus": "accepted",
})
return
}
http.NotFound(w, r)
}))
defer srv.Close()
svc, err := gmail.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newGmailService = func(context.Context, string) (*gmail.Service, error) { return svc, nil }
flags := &RootFlags{Account: "a@b.com", Force: true}
out := captureStdout(t, func() {
u, uiErr := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
if uiErr != nil {
t.Fatalf("ui.New: %v", uiErr)
}
ctx := ui.WithUI(context.Background(), u)
ctx = outfmt.WithMode(ctx, outfmt.Mode{JSON: true})
if err := runKong(t, &GmailSendAsGetCmd{}, []string{"work@company.com"}, ctx, flags); err != nil {
t.Fatalf("execute: %v", err)
}
})
var parsed struct {
SendAs struct {
SendAsEmail string `json:"sendAsEmail"`
DisplayName string `json:"displayName"`
ReplyToAddress string `json:"replyToAddress"`
VerificationStatus string `json:"verificationStatus"`
} `json:"sendAs"`
}
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
t.Fatalf("json parse: %v\nout=%q", err, out)
}
if parsed.SendAs.SendAsEmail != "work@company.com" {
t.Fatalf("unexpected sendAs: %#v", parsed.SendAs)
}
if parsed.SendAs.DisplayName != "Work Alias" {
t.Fatalf("unexpected displayName: %q", parsed.SendAs.DisplayName)
}
}
func TestGmailBatchDeleteCmd_JSON(t *testing.T) {
origNew := newGmailService
t.Cleanup(func() { newGmailService = origNew })
var receivedIDs []string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/messages/batchDelete") && r.Method == http.MethodPost {
var body struct {
IDs []string `json:"ids"`
}
_ = json.NewDecoder(r.Body).Decode(&body)
receivedIDs = body.IDs
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNoContent)
return
}
http.NotFound(w, r)
}))
defer srv.Close()
svc, err := gmail.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newGmailService = func(context.Context, string) (*gmail.Service, error) { return svc, nil }
flags := &RootFlags{Account: "a@b.com", Force: true}
out := captureStdout(t, func() {
u, uiErr := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
if uiErr != nil {
t.Fatalf("ui.New: %v", uiErr)
}
ctx := ui.WithUI(context.Background(), u)
ctx = outfmt.WithMode(ctx, outfmt.Mode{JSON: true})
if err := runKong(t, &GmailBatchDeleteCmd{}, []string{"msg1", "msg2", "msg3"}, ctx, flags); err != nil {
t.Fatalf("execute: %v", err)
}
})
if len(receivedIDs) != 3 || receivedIDs[0] != "msg1" {
t.Fatalf("unexpected IDs sent: %v", receivedIDs)
}
var parsed struct {
Deleted []string `json:"deleted"`
Count int `json:"count"`
}
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
t.Fatalf("json parse: %v\nout=%q", err, out)
}
if parsed.Count != 3 {
t.Fatalf("unexpected count: %d", parsed.Count)
}
}
func TestGmailBatchModifyCmd_JSON(t *testing.T) {
origNew := newGmailService
t.Cleanup(func() { newGmailService = origNew })
var receivedRequest struct {
IDs []string `json:"ids"`
AddLabelIds []string `json:"addLabelIds"`
RemoveLabelIds []string `json:"removeLabelIds"`
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/users/me/labels"):
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"labels": []map[string]any{
{"id": "INBOX", "name": "INBOX", "type": "system"},
{"id": "SPAM", "name": "SPAM", "type": "system"},
},
})
return
case strings.Contains(r.URL.Path, "/messages/batchModify") && r.Method == http.MethodPost:
_ = json.NewDecoder(r.Body).Decode(&receivedRequest)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNoContent)
return
}
http.NotFound(w, r)
}))
defer srv.Close()
svc, err := gmail.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newGmailService = func(context.Context, string) (*gmail.Service, error) { return svc, nil }
flags := &RootFlags{Account: "a@b.com", Force: true}
out := captureStdout(t, func() {
u, uiErr := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
if uiErr != nil {
t.Fatalf("ui.New: %v", uiErr)
}
ctx := ui.WithUI(context.Background(), u)
ctx = outfmt.WithMode(ctx, outfmt.Mode{JSON: true})
if err := runKong(t, &GmailBatchModifyCmd{}, []string{
"msg1", "msg2",
"--add", "INBOX",
"--remove", "SPAM",
}, ctx, flags); err != nil {
t.Fatalf("execute: %v", err)
}
})
if len(receivedRequest.IDs) != 2 {
t.Fatalf("unexpected IDs: %v", receivedRequest.IDs)
}
if len(receivedRequest.AddLabelIds) != 1 || receivedRequest.AddLabelIds[0] != "INBOX" {
t.Fatalf("unexpected addLabelIds: %v", receivedRequest.AddLabelIds)
}
if len(receivedRequest.RemoveLabelIds) != 1 || receivedRequest.RemoveLabelIds[0] != "SPAM" {
t.Fatalf("unexpected removeLabelIds: %v", receivedRequest.RemoveLabelIds)
}
var parsed struct {
Modified []string `json:"modified"`
Count int `json:"count"`
}
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
t.Fatalf("json parse: %v\nout=%q", err, out)
}
if parsed.Count != 2 {
t.Fatalf("unexpected count: %d", parsed.Count)
}
}
func TestGmailSendAsCreateCmd_JSON(t *testing.T) {
origNew := newGmailService
t.Cleanup(func() { newGmailService = origNew })
var receivedBody map[string]any
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/settings/sendAs") && r.Method == http.MethodPost {
_ = json.NewDecoder(r.Body).Decode(&receivedBody)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"sendAsEmail": "alias@example.com",
"displayName": "Test Alias",
"verificationStatus": "pending",
})
return
}
http.NotFound(w, r)
}))
defer srv.Close()
svc, err := gmail.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newGmailService = func(context.Context, string) (*gmail.Service, error) { return svc, nil }
flags := &RootFlags{Account: "a@b.com", Force: true}
out := captureStdout(t, func() {
u, uiErr := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
if uiErr != nil {
t.Fatalf("ui.New: %v", uiErr)
}
ctx := ui.WithUI(context.Background(), u)
ctx = outfmt.WithMode(ctx, outfmt.Mode{JSON: true})
if err := runKong(t, &GmailSendAsCreateCmd{}, []string{
"alias@example.com",
"--display-name", "Test Alias",
}, ctx, flags); err != nil {
t.Fatalf("execute: %v", err)
}
})
var parsed struct {
SendAs struct {
SendAsEmail string `json:"sendAsEmail"`
VerificationStatus string `json:"verificationStatus"`
} `json:"sendAs"`
}
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
t.Fatalf("json parse: %v\nout=%q", err, out)
}
if parsed.SendAs.SendAsEmail != "alias@example.com" {
t.Fatalf("unexpected sendAs: %#v", parsed.SendAs)
}
if parsed.SendAs.VerificationStatus != "pending" {
t.Fatalf("unexpected status: %q", parsed.SendAs.VerificationStatus)
}
}
func TestGmailSendAsDeleteCmd_JSON(t *testing.T) {
origNew := newGmailService
t.Cleanup(func() { newGmailService = origNew })
var deletedEmail string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/settings/sendAs/") && r.Method == http.MethodDelete {
parts := strings.Split(r.URL.Path, "/")
deletedEmail = parts[len(parts)-1]
w.WriteHeader(http.StatusNoContent)
return
}
http.NotFound(w, r)
}))
defer srv.Close()
svc, err := gmail.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newGmailService = func(context.Context, string) (*gmail.Service, error) { return svc, nil }
flags := &RootFlags{Account: "a@b.com", Force: true}
out := captureStdout(t, func() {
u, uiErr := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
if uiErr != nil {
t.Fatalf("ui.New: %v", uiErr)
}
ctx := ui.WithUI(context.Background(), u)
ctx = outfmt.WithMode(ctx, outfmt.Mode{JSON: true})
if err := runKong(t, &GmailSendAsDeleteCmd{}, []string{"delete-me@example.com"}, ctx, flags); err != nil {
t.Fatalf("execute: %v", err)
}
})
if deletedEmail != "delete-me@example.com" {
t.Fatalf("unexpected deleted email: %q", deletedEmail)
}
var parsed struct {
Email string `json:"email"`
Deleted bool `json:"deleted"`
}
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
t.Fatalf("json parse: %v\nout=%q", err, out)
}
if !parsed.Deleted {
t.Fatalf("expected deleted=true")
}
}
func TestGmailSendAsVerifyCmd_JSON(t *testing.T) {
origNew := newGmailService
t.Cleanup(func() { newGmailService = origNew })
var verifiedEmail string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/settings/sendAs/") && strings.HasSuffix(r.URL.Path, "/verify") && r.Method == http.MethodPost {
parts := strings.Split(r.URL.Path, "/")
verifiedEmail = parts[len(parts)-2]
w.WriteHeader(http.StatusNoContent)
return
}
http.NotFound(w, r)
}))
defer srv.Close()
svc, err := gmail.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newGmailService = func(context.Context, string) (*gmail.Service, error) { return svc, nil }
flags := &RootFlags{Account: "a@b.com"}
out := captureStdout(t, func() {
u, uiErr := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
if uiErr != nil {
t.Fatalf("ui.New: %v", uiErr)
}
ctx := ui.WithUI(context.Background(), u)
ctx = outfmt.WithMode(ctx, outfmt.Mode{JSON: true})
if err := runKong(t, &GmailSendAsVerifyCmd{}, []string{"verify-me@example.com"}, ctx, flags); err != nil {
t.Fatalf("execute: %v", err)
}
})
if verifiedEmail != "verify-me@example.com" {
t.Fatalf("unexpected verified email: %q", verifiedEmail)
}
var parsed struct {
Email string `json:"email"`
Message string `json:"message"`
}
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
t.Fatalf("json parse: %v\nout=%q", err, out)
}
if parsed.Email != "verify-me@example.com" {
t.Fatalf("unexpected email: %q", parsed.Email)
}
}
func TestGmailSendAsUpdateCmd_JSON(t *testing.T) {
origNew := newGmailService
t.Cleanup(func() { newGmailService = origNew })
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.Contains(r.URL.Path, "/settings/sendAs/update@example.com") && r.Method == http.MethodGet:
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"sendAsEmail": "update@example.com",
"displayName": "Old Name",
"verificationStatus": "accepted",
})
return
case strings.Contains(r.URL.Path, "/settings/sendAs/update@example.com") && r.Method == http.MethodPut:
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"sendAsEmail": "update@example.com",
"displayName": "New Name",
"verificationStatus": "accepted",
})
return
}
http.NotFound(w, r)
}))
defer srv.Close()
svc, err := gmail.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newGmailService = func(context.Context, string) (*gmail.Service, error) { return svc, nil }
flags := &RootFlags{Account: "a@b.com"}
out := captureStdout(t, func() {
u, uiErr := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
if uiErr != nil {
t.Fatalf("ui.New: %v", uiErr)
}
ctx := ui.WithUI(context.Background(), u)
ctx = outfmt.WithMode(ctx, outfmt.Mode{JSON: true})
if err := runKong(t, &GmailSendAsUpdateCmd{}, []string{
"update@example.com",
"--display-name", "New Name",
}, ctx, flags); err != nil {
t.Fatalf("execute: %v", err)
}
})
var parsed struct {
SendAs struct {
SendAsEmail string `json:"sendAsEmail"`
DisplayName string `json:"displayName"`
} `json:"sendAs"`
}
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
t.Fatalf("json parse: %v\nout=%q", err, out)
}
if parsed.SendAs.DisplayName != "New Name" {
t.Fatalf("unexpected displayName: %q", parsed.SendAs.DisplayName)
}
}