gogcli/internal/cmd/gmail_track_cmd_test.go
2026-04-28 10:22:55 +01:00

369 lines
10 KiB
Go

package cmd
import (
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"github.com/steipete/gogcli/internal/tracking"
)
func setupTrackingEnv(t *testing.T) {
t.Helper()
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, "xdg"))
t.Setenv("GOG_KEYRING_BACKEND", "file")
t.Setenv("GOG_KEYRING_PASSWORD", "testpass")
}
func TestGmailTrackSetupAndStatus(t *testing.T) {
setupTrackingEnv(t)
out := captureStdout(t, func() {
errOut := captureStderr(t, func() {
if err := Execute([]string{"--account", "a@b.com", "--no-input", "gmail", "track", "setup", "--worker-url", "https://example.com"}); err != nil {
t.Fatalf("Execute: %v", err)
}
})
if !strings.Contains(errOut, "Next steps") {
t.Fatalf("expected next steps in stderr: %q", errOut)
}
})
if !strings.Contains(out, "configured\ttrue") {
t.Fatalf("unexpected setup output: %q", out)
}
if !strings.Contains(out, "tracking_key_version\t1") {
t.Fatalf("missing setup key version: %q", out)
}
statusOut := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--account", "a@b.com", "gmail", "track", "status"}); err != nil {
t.Fatalf("Execute: %v", err)
}
})
})
if !strings.Contains(statusOut, "configured\ttrue") {
t.Fatalf("unexpected status output: %q", statusOut)
}
if !strings.Contains(statusOut, "tracking_key_version\t1") {
t.Fatalf("missing status key version: %q", statusOut)
}
}
func TestGmailTrackKeyRotateNoDeploy(t *testing.T) {
setupTrackingEnv(t)
_ = captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--account", "a@b.com", "--no-input", "gmail", "track", "setup", "--worker-url", "https://example.com"}); err != nil {
t.Fatalf("setup: %v", err)
}
})
})
rotateOut := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--account", "a@b.com", "--no-input", "gmail", "track", "key", "rotate", "--no-deploy"}); err != nil {
t.Fatalf("rotate: %v", err)
}
})
})
if !strings.Contains(rotateOut, "tracking_key_version\t2") {
t.Fatalf("unexpected rotate output: %q", rotateOut)
}
if !strings.Contains(rotateOut, "tracking_key_versions\t1,2") {
t.Fatalf("unexpected rotate versions: %q", rotateOut)
}
if !strings.Contains(rotateOut, "deployed\tfalse") {
t.Fatalf("unexpected rotate deployed output: %q", rotateOut)
}
statusOut := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--account", "a@b.com", "gmail", "track", "status"}); err != nil {
t.Fatalf("status: %v", err)
}
})
})
if !strings.Contains(statusOut, "tracking_key_version\t2") {
t.Fatalf("missing rotated status key version: %q", statusOut)
}
_ = captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--account", "a@b.com", "--no-input", "gmail", "track", "setup", "--worker-url", "https://example.com"}); err != nil {
t.Fatalf("rerun setup: %v", err)
}
})
})
statusOut = captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--account", "a@b.com", "gmail", "track", "status"}); err != nil {
t.Fatalf("status after setup rerun: %v", err)
}
})
})
if !strings.Contains(statusOut, "tracking_key_version\t2") {
t.Fatalf("setup rerun lost rotated key version: %q", statusOut)
}
}
func TestGmailTrackStatus_NotConfigured(t *testing.T) {
setupTrackingEnv(t)
out := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--account", "a@b.com", "gmail", "track", "status"}); err != nil {
t.Fatalf("Execute: %v", err)
}
})
})
if !strings.Contains(out, "configured\tfalse") {
t.Fatalf("unexpected status output: %q", out)
}
}
func TestGmailTrackOpens(t *testing.T) {
setupTrackingEnv(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.Contains(r.URL.Path, "/q/"):
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"tracking_id": "tid",
"recipient": "user@example.com",
"sent_at": "2025-01-01T00:00:00Z",
"total_opens": 2,
"human_opens": 1,
"first_human_open": map[string]any{
"at": "2025-01-01T02:00:00Z",
"location": map[string]any{
"city": "SF",
"region": "CA",
"country": "US",
},
},
})
return
case strings.Contains(r.URL.Path, "/opens"):
if r.Header.Get("Authorization") != "Bearer adminkey" {
t.Fatalf("unexpected auth: %q", r.Header.Get("Authorization"))
}
if r.URL.Query().Get("recipient") != "user@example.com" {
t.Fatalf("unexpected recipient: %q", r.URL.RawQuery)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"opens": []map[string]any{
{
"tracking_id": "tid",
"recipient": "user@example.com",
"subject_hash": "hash",
"sent_at": "2025-01-01T00:00:00Z",
"opened_at": "2025-01-01T01:00:00Z",
"is_bot": false,
"location": map[string]any{"city": "SF", "region": "CA", "country": "US"},
},
},
})
return
default:
http.NotFound(w, r)
return
}
}))
defer srv.Close()
cfg := &tracking.Config{
Enabled: true,
WorkerURL: srv.URL,
TrackingKey: "trackkey",
AdminKey: "adminkey",
}
if err := tracking.SaveConfig("a@b.com", cfg); err != nil {
t.Fatalf("SaveConfig: %v", err)
}
out := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--account", "a@b.com", "gmail", "track", "opens", "tid"}); err != nil {
t.Fatalf("Execute: %v", err)
}
})
})
if !strings.Contains(out, "tracking_id\ttid") {
t.Fatalf("unexpected tracking id output: %q", out)
}
if !strings.Contains(out, "first_human_open\t2025-01-01T02:00:00Z") {
t.Fatalf("unexpected first open output: %q", out)
}
if !strings.Contains(out, "first_human_open_location\tSF, CA") {
t.Fatalf("unexpected first open location output: %q", out)
}
adminOut := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--account", "a@b.com", "gmail", "track", "opens", "--to", "user@example.com", "--since", "2025-01-01"}); err != nil {
t.Fatalf("Execute: %v", err)
}
})
})
if !strings.Contains(adminOut, "tid\tuser@example.com") {
t.Fatalf("unexpected admin output: %q", adminOut)
}
if _, err := parseTrackingSince("not-a-date"); err == nil {
t.Fatalf("expected parseTrackingSince error")
}
}
func TestGmailTrackOpens_JSON(t *testing.T) {
setupTrackingEnv(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.Contains(r.URL.Path, "/q/"):
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"tracking_id": "tid",
"recipient": "user@example.com",
"sent_at": "2025-01-01T00:00:00Z",
"total_opens": 2,
"human_opens": 1,
})
return
case strings.Contains(r.URL.Path, "/opens"):
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"opens": []map[string]any{
{
"tracking_id": "tid",
"recipient": "user@example.com",
"subject_hash": "hash",
"sent_at": "2025-01-01T00:00:00Z",
"opened_at": "2025-01-01T01:00:00Z",
"is_bot": false,
},
},
})
return
default:
http.NotFound(w, r)
return
}
}))
defer srv.Close()
cfg := &tracking.Config{
Enabled: true,
WorkerURL: srv.URL,
TrackingKey: "trackkey",
AdminKey: "adminkey",
}
if err := tracking.SaveConfig("a@b.com", cfg); err != nil {
t.Fatalf("SaveConfig: %v", err)
}
trackOut := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--json", "--account", "a@b.com", "gmail", "track", "opens", "tid"}); err != nil {
t.Fatalf("Execute: %v", err)
}
})
})
if !strings.Contains(trackOut, "\"tracking_id\"") {
t.Fatalf("unexpected track json output: %q", trackOut)
}
adminOut := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--json", "--account", "a@b.com", "gmail", "track", "opens", "--to", "user@example.com"}); err != nil {
t.Fatalf("Execute: %v", err)
}
})
})
if !strings.Contains(adminOut, "\"opens\"") {
t.Fatalf("unexpected admin json output: %q", adminOut)
}
if parsed, err := parseTrackingSince("24h"); err != nil || parsed == "" {
t.Fatalf("unexpected parseTrackingSince duration result: %q err=%v", parsed, err)
}
if parsed, err := parseTrackingSince("2025-01-01"); err != nil || parsed == "" {
t.Fatalf("unexpected parseTrackingSince date result: %q err=%v", parsed, err)
}
}
func TestGmailTrackOpens_AdminEmpty(t *testing.T) {
setupTrackingEnv(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/opens") {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"opens": []map[string]any{},
})
return
}
http.NotFound(w, r)
}))
defer srv.Close()
cfg := &tracking.Config{
Enabled: true,
WorkerURL: srv.URL,
TrackingKey: "trackkey",
AdminKey: "adminkey",
}
if err := tracking.SaveConfig("a@b.com", cfg); err != nil {
t.Fatalf("SaveConfig: %v", err)
}
out := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--account", "a@b.com", "gmail", "track", "opens", "--to", "user@example.com"}); err != nil {
t.Fatalf("Execute: %v", err)
}
})
})
if !strings.Contains(out, "opens\t0") {
t.Fatalf("unexpected empty admin output: %q", out)
}
}
func TestGmailTrackOpens_NotConfigured(t *testing.T) {
setupTrackingEnv(t)
cfg := &tracking.Config{Enabled: false}
if err := tracking.SaveConfig("a@b.com", cfg); err != nil {
t.Fatalf("SaveConfig: %v", err)
}
if err := Execute([]string{"--account", "a@b.com", "gmail", "track", "opens"}); err == nil {
t.Fatalf("expected error for unconfigured tracking")
}
}
func TestParseTrackingSince_FlexibleFormats(t *testing.T) {
t.Parallel()
if parsed, err := parseTrackingSince("2026-02-13T10:20"); err != nil || parsed == "" {
t.Fatalf("unexpected local datetime parse: %q err=%v", parsed, err)
}
parsedNano, err := parseTrackingSince("2026-02-13T10:20:30.123456789Z")
if err != nil {
t.Fatalf("unexpected RFC3339Nano parse error: %v", err)
}
if !strings.Contains(parsedNano, ".123456789Z") {
t.Fatalf("expected nano precision output, got %q", parsedNano)
}
}