gogcli/internal/cmd/gmail_thread_run_test.go
Peter Steinberger c1ebd60210 test: expand coverage and tracking regressions (#35)
Thanks @salmonumbrella.

Co-authored-by: salmonumbrella <salmonumbrella@users.noreply.github.com>
2026-01-09 04:21:16 +01:00

251 lines
7.8 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 TestGmailThreadGetAndAttachments_JSON(t *testing.T) {
origNew := newGmailService
t.Cleanup(func() { newGmailService = origNew })
attachmentData := base64.RawURLEncoding.EncodeToString([]byte("payload"))
threadResp := map[string]any{
"id": "t1",
"messages": []map[string]any{
{
"id": "m1",
"payload": map[string]any{
"headers": []map[string]any{
{"name": "From", "value": "a@example.com"},
{"name": "To", "value": "b@example.com"},
{"name": "Subject", "value": "Hi"},
{"name": "Date", "value": "Mon, 1 Jan 2025 00:00:00 +0000"},
},
"mimeType": "multipart/mixed",
"parts": []map[string]any{
{
"mimeType": "text/plain",
"body": map[string]any{
"data": base64.RawURLEncoding.EncodeToString([]byte("hello")),
},
},
{
"filename": "note.txt",
"mimeType": "text/plain",
"body": map[string]any{
"attachmentId": "att1",
"size": 7,
},
},
},
},
},
},
}
emptyThreadResp := map[string]any{
"id": "empty",
"messages": []map[string]any{},
}
noAttsThreadResp := map[string]any{
"id": "noatts",
"messages": []map[string]any{
{
"id": "m2",
"payload": map[string]any{
"mimeType": "text/plain",
"body": map[string]any{
"data": base64.RawURLEncoding.EncodeToString([]byte("hello")),
},
},
},
},
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/gmail/v1")
switch {
case r.Method == http.MethodGet && path == "/users/me/threads/t1":
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(threadResp)
return
case r.Method == http.MethodGet && path == "/users/me/threads/empty":
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(emptyThreadResp)
return
case r.Method == http.MethodGet && path == "/users/me/threads/noatts":
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(noAttsThreadResp)
return
case r.Method == http.MethodGet && path == "/users/me/messages/m1/attachments/att1":
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"data": attachmentData,
})
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 := t.TempDir()
getOut := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--json", "--account", "a@b.com", "gmail", "thread", "get", "t1", "--download", "--out-dir", outDir}); err != nil {
t.Fatalf("Execute thread get: %v", err)
}
})
})
var payload struct {
Thread map[string]any `json:"thread"`
Downloaded []map[string]any `json:"downloaded"`
}
if err := json.Unmarshal([]byte(getOut), &payload); err != nil {
t.Fatalf("decode thread json: %v", err)
}
if payload.Thread == nil || len(payload.Downloaded) != 1 {
t.Fatalf("unexpected thread payload: %#v", payload)
}
path, ok := payload.Downloaded[0]["path"].(string)
if !ok || path == "" {
t.Fatalf("expected download path, got: %#v", payload.Downloaded)
}
if _, statErr := os.Stat(path); statErr != nil {
t.Fatalf("expected downloaded file: %v", statErr)
}
attachmentsOut := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--json", "--account", "a@b.com", "gmail", "thread", "attachments", "t1"}); err != nil {
t.Fatalf("Execute attachments: %v", err)
}
})
})
var attachments struct {
ThreadID string `json:"threadId"`
Attachments []map[string]any `json:"attachments"`
}
if err := json.Unmarshal([]byte(attachmentsOut), &attachments); err != nil {
t.Fatalf("decode attachments json: %v", err)
}
if attachments.ThreadID != "t1" || len(attachments.Attachments) != 1 {
t.Fatalf("unexpected attachments payload: %#v", attachments)
}
if attachments.Attachments[0]["filename"] != "note.txt" {
t.Fatalf("unexpected attachment filename: %#v", attachments.Attachments[0])
}
attachmentsDownloadOut := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--json", "--account", "a@b.com", "gmail", "thread", "attachments", "t1", "--download", "--out-dir", outDir}); err != nil {
t.Fatalf("Execute attachments download: %v", err)
}
})
})
var attachmentsDownloaded struct {
Attachments []map[string]any `json:"attachments"`
}
if err := json.Unmarshal([]byte(attachmentsDownloadOut), &attachmentsDownloaded); err != nil {
t.Fatalf("decode attachments download: %v", err)
}
if len(attachmentsDownloaded.Attachments) != 1 {
t.Fatalf("unexpected download attachments: %#v", attachmentsDownloaded.Attachments)
}
if _, ok := attachmentsDownloaded.Attachments[0]["path"]; !ok {
t.Fatalf("expected download path in attachments: %#v", attachmentsDownloaded.Attachments[0])
}
plainOutDir := t.TempDir()
plainDownloadOut := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--account", "a@b.com", "gmail", "thread", "attachments", "t1", "--download", "--out-dir", plainOutDir}); err != nil {
t.Fatalf("Execute attachments download plain: %v", err)
}
})
})
if !strings.Contains(plainDownloadOut, "Saved") {
t.Fatalf("unexpected download output: %q", plainDownloadOut)
}
cachedOut := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--account", "a@b.com", "gmail", "thread", "attachments", "t1", "--download", "--out-dir", plainOutDir}); err != nil {
t.Fatalf("Execute attachments cached: %v", err)
}
})
})
if !strings.Contains(cachedOut, "Cached") {
t.Fatalf("unexpected cached output: %q", cachedOut)
}
// Ensure path is within the requested output dir when downloading attachments.
if !strings.HasPrefix(path, filepath.Clean(outDir)+string(os.PathSeparator)) {
t.Fatalf("unexpected download path: %s", path)
}
plainOut := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--account", "a@b.com", "gmail", "thread", "get", "t1"}); err != nil {
t.Fatalf("Execute thread get plain: %v", err)
}
})
})
if !strings.Contains(plainOut, "Thread contains") {
t.Fatalf("unexpected plain output: %q", plainOut)
}
emptyErr := captureStderr(t, func() {
if err := Execute([]string{"--account", "a@b.com", "gmail", "thread", "get", "empty"}); err != nil {
t.Fatalf("Execute empty thread: %v", err)
}
})
if !strings.Contains(emptyErr, "Empty thread") {
t.Fatalf("unexpected empty thread stderr: %q", emptyErr)
}
noAttsOut := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--account", "a@b.com", "gmail", "thread", "attachments", "noatts"}); err != nil {
t.Fatalf("Execute no attachments: %v", err)
}
})
})
if !strings.Contains(noAttsOut, "No attachments found") {
t.Fatalf("unexpected no attachments output: %q", noAttsOut)
}
emptyAttachOut := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--json", "--account", "a@b.com", "gmail", "thread", "attachments", "empty"}); err != nil {
t.Fatalf("Execute empty attachments json: %v", err)
}
})
})
if !strings.Contains(emptyAttachOut, "\"attachments\"") {
t.Fatalf("unexpected empty attachments output: %q", emptyAttachOut)
}
}