gogcli/internal/cmd/execute_gmail_text_test.go
salmonumbrella 524f198d3d
feat(gmail): add --full to thread get
- Add --full to show complete thread message bodies\n- Default output truncates previews (500 runes) with a hint\n\nThanks @salmonumbrella.
2026-01-07 18:49:53 +00:00

378 lines
11 KiB
Go

package cmd
import (
"context"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"google.golang.org/api/gmail/v1"
"google.golang.org/api/option"
)
func TestExecute_GmailThread_Text_Download(t *testing.T) {
origNew := newGmailService
t.Cleanup(func() { newGmailService = origNew })
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
wd := t.TempDir()
origWD, _ := os.Getwd()
t.Cleanup(func() { _ = os.Chdir(origWD) })
if err := os.Chdir(wd); err != nil {
t.Fatalf("chdir: %v", err)
}
attData := []byte("hello")
attEncoded := base64.RawURLEncoding.EncodeToString(attData)
bodyEncoded := base64.RawURLEncoding.EncodeToString([]byte("body"))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.Contains(r.URL.Path, "/gmail/v1/users/me/threads/t-thread-1"):
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "t-thread-1",
"messages": []map[string]any{
{
"id": "m-thread-1",
"payload": map[string]any{
"headers": []map[string]any{
{"name": "From", "value": "Me <me@example.com>"},
{"name": "To", "value": "You <you@example.com>"},
{"name": "Subject", "value": "Hello"},
{"name": "Date", "value": "Wed, 17 Dec 2025 14:00:00 -0800"},
},
"parts": []map[string]any{
{ // body
"mimeType": "text/plain",
"body": map[string]any{"data": bodyEncoded},
},
{ // attachment
"filename": "a.txt",
"mimeType": "text/plain",
"body": map[string]any{"attachmentId": "a-thread-1", "size": len(attData)},
},
},
},
},
},
})
return
case strings.Contains(r.URL.Path, "/gmail/v1/users/me/messages/m-thread-1/attachments/a-thread-1"):
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"data": attEncoded})
return
default:
http.NotFound(w, r)
return
}
}))
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 }
out := captureStdout(t, func() {
_ = captureStderr(t, func() {
if execErr := Execute([]string{"--account", "a@b.com", "gmail", "thread", "get", "t-thread-1", "--download"}); execErr != nil {
t.Fatalf("Execute: %v", execErr)
}
})
})
if !strings.Contains(out, "=== Message 1/1: m-thread-1 ===") || !strings.Contains(out, "Attachments:") || !(strings.Contains(out, "Saved:") || strings.Contains(out, "Cached:")) {
t.Fatalf("unexpected out=%q", out)
}
expectedPath := filepath.Join(wd, "m-thread-1_a-thread_a.txt")
b, err := os.ReadFile(expectedPath)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if string(b) != string(attData) {
t.Fatalf("content=%q", string(b))
}
}
func TestExecute_GmailThread_Text_FullFlag(t *testing.T) {
origNew := newGmailService
t.Cleanup(func() { newGmailService = origNew })
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
longBody := strings.Repeat("a", 600)
bodyEncoded := base64.RawURLEncoding.EncodeToString([]byte(longBody))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.Contains(r.URL.Path, "/gmail/v1/users/me/threads/t-thread-1"):
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "t-thread-1",
"messages": []map[string]any{
{
"id": "m-thread-1",
"payload": map[string]any{
"headers": []map[string]any{
{"name": "From", "value": "Me <me@example.com>"},
{"name": "To", "value": "You <you@example.com>"},
{"name": "Subject", "value": "Hello"},
{"name": "Date", "value": "Wed, 17 Dec 2025 14:00:00 -0800"},
},
"parts": []map[string]any{
{
"mimeType": "text/plain",
"body": map[string]any{"data": bodyEncoded},
},
},
},
},
},
})
return
default:
http.NotFound(w, r)
return
}
}))
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 }
t.Run("default truncates", func(t *testing.T) {
out := captureStdout(t, func() {
_ = captureStderr(t, func() {
if execErr := Execute([]string{"--account", "a@b.com", "gmail", "thread", "get", "t-thread-1"}); execErr != nil {
t.Fatalf("Execute: %v", execErr)
}
})
})
if !strings.Contains(out, "[truncated") {
t.Fatalf("expected truncated output, got=%q", out)
}
if strings.Contains(out, longBody) {
t.Fatalf("expected body to be truncated, got=%q", out)
}
})
t.Run("--full shows complete body", func(t *testing.T) {
out := captureStdout(t, func() {
_ = captureStderr(t, func() {
if execErr := Execute([]string{"--account", "a@b.com", "gmail", "thread", "get", "t-thread-1", "--full"}); execErr != nil {
t.Fatalf("Execute: %v", execErr)
}
})
})
if strings.Contains(out, "[truncated") {
t.Fatalf("expected full output, got=%q", out)
}
if !strings.Contains(out, longBody) {
t.Fatalf("expected full body, got=%q", out)
}
})
}
func TestExecute_GmailDraftsGet_Text_Download(t *testing.T) {
origNew := newGmailService
t.Cleanup(func() { newGmailService = origNew })
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
attData := []byte("hello")
attEncoded := base64.RawURLEncoding.EncodeToString(attData)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.Contains(r.URL.Path, "/gmail/v1/users/me/drafts/d1"):
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "d1",
"message": map[string]any{
"id": "m-draft-1",
"payload": map[string]any{
"headers": []map[string]any{
{"name": "To", "value": "x@y.com"},
{"name": "Subject", "value": "S"},
},
"parts": []map[string]any{
{
"filename": "a.txt",
"mimeType": "text/plain",
"body": map[string]any{"attachmentId": "a-draft-1", "size": len(attData)},
},
},
},
},
})
return
case strings.Contains(r.URL.Path, "/gmail/v1/users/me/messages/m-draft-1/attachments/a-draft-1"):
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"data": attEncoded})
return
default:
http.NotFound(w, r)
return
}
}))
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 }
out := captureStdout(t, func() {
_ = captureStderr(t, func() {
if execErr := Execute([]string{"--account", "a@b.com", "gmail", "drafts", "get", "d1", "--download"}); execErr != nil {
t.Fatalf("Execute: %v", execErr)
}
})
})
if !strings.Contains(out, "Draft-ID: d1") || !strings.Contains(out, "Attachments:") || (!strings.Contains(out, "Saved:") && !strings.Contains(out, "Cached:")) {
t.Fatalf("unexpected out=%q", out)
}
}
func TestExecute_GmailThread_OutDir_CreatesParents_JSON(t *testing.T) {
origNew := newGmailService
t.Cleanup(func() { newGmailService = origNew })
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
wd := t.TempDir()
origWD, _ := os.Getwd()
t.Cleanup(func() { _ = os.Chdir(origWD) })
if err := os.Chdir(wd); err != nil {
t.Fatalf("chdir: %v", err)
}
attData := []byte("hello")
attEncoded := base64.RawURLEncoding.EncodeToString(attData)
attachmentCalls := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.Contains(r.URL.Path, "/gmail/v1/users/me/threads/t-thread-1"):
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "t-thread-1",
"messages": []map[string]any{
{
"id": "m-thread-1",
"payload": map[string]any{
"parts": []map[string]any{
{
"filename": "a.txt",
"mimeType": "text/plain",
"body": map[string]any{"attachmentId": "a-thread-1", "size": len(attData)},
},
},
},
},
},
})
return
case strings.Contains(r.URL.Path, "/gmail/v1/users/me/messages/m-thread-1/attachments/a-thread-1"):
attachmentCalls++
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"data": attEncoded})
return
default:
http.NotFound(w, r)
return
}
}))
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 }
outDir := filepath.Join("nested", "attachments")
run := func() map[string]any {
out := captureStdout(t, func() {
_ = captureStderr(t, func() {
if execErr := Execute([]string{
"--json",
"--account", "a@b.com",
"gmail", "thread", "get", "t-thread-1",
"--download",
"--out-dir", outDir,
}); execErr != nil {
t.Fatalf("Execute: %v", execErr)
}
})
})
var parsed map[string]any
if unmarshalErr := json.Unmarshal([]byte(out), &parsed); unmarshalErr != nil {
t.Fatalf("json parse: %v\nout=%q", unmarshalErr, out)
}
return parsed
}
parsed1 := run()
if attachmentCalls != 1 {
t.Fatalf("attachmentCalls=%d", attachmentCalls)
}
downloaded, _ := parsed1["downloaded"].([]any)
if len(downloaded) != 1 {
t.Fatalf("downloaded=%v", parsed1["downloaded"])
}
item, _ := downloaded[0].(map[string]any)
path, _ := item["path"].(string)
if path != filepath.Join(outDir, "m-thread-1_a-thread_a.txt") {
t.Fatalf("path=%q", path)
}
b, err := os.ReadFile(filepath.Join(wd, path))
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if string(b) != string(attData) {
t.Fatalf("content=%q", string(b))
}
parsed2 := run()
if attachmentCalls != 1 {
t.Fatalf("attachmentCalls=%d", attachmentCalls)
}
downloaded2, _ := parsed2["downloaded"].([]any)
if len(downloaded2) != 1 {
t.Fatalf("downloaded=%v", parsed2["downloaded"])
}
item2, _ := downloaded2[0].(map[string]any)
if item2["cached"] != true {
t.Fatalf("cached=%v", item2["cached"])
}
}