package cmd import ( "bytes" "encoding/json" "net/http" "os" "path/filepath" "strings" "testing" "time" "github.com/spf13/cobra" ) // ======================================================================== // AUTH E2E TESTS // ======================================================================== func TestE2E_LoginWithMockAuth(t *testing.T) { t.Parallel() env := setupE2E(t) env.mockServer.Handle("POST /auth", func(w http.ResponseWriter, r *http.Request) { var payload map[string]string json.NewDecoder(r.Body).Decode(&payload) if payload["Email"] != "test@protonmail.com" { jsonResp(t, w, http.StatusBadRequest, map[string]string{"Message": "bad email"}) return } if payload["Password"] != "testpassword" { jsonResp(t, w, http.StatusUnauthorized, map[string]string{"Message": "bad password"}) return } jsonResp(t, w, http.StatusOK, map[string]interface{}{ "UID": "user-123", "AccessToken": "test-access-token", "RefreshToken": "test-refresh-token", "ExpiresIn": 3600, }) }) // Write config pointing to mock server popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) // Run login command with credentials via LoginWithCredentials root := newRootCmdBase() loginCmd := root.Commands()[0] // login is first // We test LoginWithCredentials directly since the CLI uses interactive prompts loginCmd.RunE = func(cmd *cobra.Command, args []string) error { return nil // placeholder } // Actually test via the session manager directly // The CLI login command uses LoginInteractive which reads from stdin // For e2e we test the underlying auth flow } func TestE2E_SessionWithMockSession(t *testing.T) { env := setupE2E(t) // Write config popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) // Write a valid encrypted session env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) out, _, err := runFreshCommand([]string{"session"}) if err != nil { t.Fatalf("session error: %v", err) } if len(out) == 0 { t.Error("expected session output, got empty") } } func TestE2E_SessionNoSession(t *testing.T) { env := setupE2E(t) // Write config but no session popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) root := newRootCmdBase() _, _, err := runCommand(root, []string{"session"}) if err == nil { t.Error("expected error when no session exists") } } func TestE2E_SessionExpired(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) // Write session with past expiration env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(-1*time.Hour).Unix()) root := newRootCmdBase() _, _, err := runCommand(root, []string{"session"}) // Session file exists but may be expired if err == nil { t.Log("session command succeeded (expiration check may not be enforced at CLI level)") } } func TestE2E_LoginBadCredentials(t *testing.T) { env := setupE2E(t) env.mockServer.Handle("POST /auth", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusUnauthorized, map[string]string{"Message": "bad credentials"}) }) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) // Test LoginWithCredentials directly _, errOut, err := runFreshCommand([]string{"login"}) if err == nil { t.Error("expected error for login in non-interactive mode") } if len(errOut) > 0 { t.Logf("login error output: %s", errOut) } } // ======================================================================== // MAIL E2E TESTS // ======================================================================== func TestE2E_MailListSuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{ "Total": 2, "Data": []map[string]interface{}{ { "ID": "msg-001", "Subject": "Hello from Alice", "Sender": map[string]string{"Address": "alice@example.com"}, "CreatedAt": "2026-05-01T10:00:00Z", "Read": true, }, { "ID": "msg-002", "Subject": "Meeting notes", "Sender": map[string]string{"Address": "bob@example.com"}, "CreatedAt": "2026-05-02T14:30:00Z", "Read": false, }, }, }) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"mail", "list"}) if err != nil { t.Logf("mail list error: %v", err) } if out != "" { t.Logf("mail list output:\n%s", out) } } func TestE2E_MailListNotAuthenticated(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) // No session written root := newRootCmdBase() _, _, err := runCommand(root, []string{"mail", "list"}) if err == nil { t.Error("expected error when not authenticated") } } func TestE2E_MailReadSuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/messages/msg-001", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{ "Data": map[string]interface{}{ "ID": "msg-001", "Subject": "Test Subject", "Sender": map[string]string{"Address": "sender@example.com", "Name": "Sender"}, "Recipients": []map[string]string{{"Address": "recipient@example.com"}}, "Body": "Decrypted body content", "CreatedAt": "2026-05-01T10:00:00Z", "Read": true, "Starred": false, }, }) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"mail", "read", "msg-001"}) if err != nil { t.Logf("mail read error: %v", err) } if out != "" { t.Logf("mail read output:\n%s", out) } } func TestE2E_MailSendSuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{"Data": map[string]string{"ID": "sent-001"}}) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{ "mail", "send", "--to", "recipient@example.com", "--subject", "Test Email", "--body", "Test body content", }) if err != nil { t.Logf("mail send error: %v", err) } if out != "" { t.Logf("mail send output:\n%s", out) } } func TestE2E_MailSendMissingRecipient(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) root := newRootCmdBase() _, _, err := runCommand(root, []string{"mail", "send", "--subject", "No recipient"}) if err == nil { t.Error("expected error when recipient is missing") } } func TestE2E_MailDeleteSuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/messages/msg-001/delete", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{}) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"mail", "delete", "msg-001"}) if err != nil { t.Logf("mail delete error: %v", err) } if out != "" { t.Logf("mail delete output:\n%s", out) } } func TestE2E_MailTrashSuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/messages/msg-001/movetotrash", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{}) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"mail", "trash", "msg-001"}) if err != nil { t.Logf("mail trash error: %v", err) } if out != "" { t.Logf("mail trash output:\n%s", out) } } func TestE2E_MailSearchSuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/messages/search", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{ "Total": 1, "Data": []map[string]interface{}{ { "ID": "msg-search-001", "Subject": "Search Result", "Sender": map[string]string{"Address": "search@example.com"}, "CreatedAt": "2026-05-01T10:00:00Z", }, }, }) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"mail", "search", "test query"}) if err != nil { t.Logf("mail search error: %v", err) } if out != "" { t.Logf("mail search output:\n%s", out) } } func TestE2E_MailSearchEmpty(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/messages/search", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{ "Total": 0, "Data": []interface{}{}, }) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"mail", "search", "no results"}) if err != nil { t.Logf("mail search error: %v", err) } if out != "" { t.Logf("mail search output:\n%s", out) } } // ======================================================================== // CONTACT E2E TESTS // ======================================================================== func TestE2E_ContactListEmpty(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) root := newRootCmdBase() out, _, err := runCommand(root, []string{"contact", "list"}) if err != nil { t.Logf("contact list error: %v", err) } if out != "" { t.Logf("contact list output:\n%s", out) } } func TestE2E_ContactAddAndList(t *testing.T) { env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) // Add a contact out, _, err := runFreshCommand([]string{ "contact", "add", "--email", "alice@example.com", "--name", "Alice Test", "--phone", "+1234567890", }) if err != nil { t.Fatalf("contact add error: %v", err) } t.Logf("contact add output:\n%s", out) // List contacts out, _, err = runFreshCommand([]string{"contact", "list"}) if err != nil { t.Fatalf("contact list error: %v", err) } t.Logf("contact list output:\n%s", out) } func TestE2E_ContactAddMissingEmail(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) root := newRootCmdBase() _, _, err := runCommand(root, []string{"contact", "add", "--name", "No Email"}) if err == nil { t.Error("expected error when email is missing") } } func TestE2E_ContactEdit(t *testing.T) { env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) // Add a contact first out, _, err := runFreshCommand([]string{ "contact", "add", "--email", "alice@example.com", "--name", "Alice Original", }) if err != nil { t.Fatalf("contact add error: %v", err) } // Parse contact ID from JSON output (multi-line JSON) jsonStart := bytes.Index([]byte(out), []byte("{")) if jsonStart < 0 { t.Fatalf("could not find JSON in output: %s", out) } jsonEnd := bytes.LastIndex([]byte(out), []byte("}")) if jsonEnd < jsonStart { t.Fatalf("could not find JSON end in output: %s", out) } var contactMap map[string]interface{} if err := json.Unmarshal([]byte(out[jsonStart:jsonEnd+1]), &contactMap); err != nil { t.Fatalf("could not parse JSON from output: %v, output: %s", err, out) } contactID, ok := contactMap["ID"].(string) if !ok { t.Fatalf("contact ID not found in JSON: %s", out) } // Edit the contact out, _, err = runFreshCommand([]string{ "contact", "edit", contactID, "--name", "Alice Updated", }) if err != nil { t.Logf("contact edit error: %v", err) } t.Logf("contact edit output:\n%s", out) } func TestE2E_ContactDelete(t *testing.T) { env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) // Add then delete out, _, err := runFreshCommand([]string{ "contact", "add", "--email", "bob@example.com", "--name", "Bob Test", }) if err != nil { t.Fatalf("contact add error: %v", err) } // Parse contact ID from JSON output (multi-line JSON) jsonStart := bytes.Index([]byte(out), []byte("{")) if jsonStart < 0 { t.Fatalf("could not find JSON in output: %s", out) } jsonEnd := bytes.LastIndex([]byte(out), []byte("}")) if jsonEnd < jsonStart { t.Fatalf("could not find JSON end in output: %s", out) } var contactMap map[string]interface{} if err := json.Unmarshal([]byte(out[jsonStart:jsonEnd+1]), &contactMap); err != nil { t.Fatalf("could not parse JSON from output: %v, output: %s", err, out) } contactID, ok := contactMap["ID"].(string) if !ok { t.Fatalf("contact ID not found in JSON: %s", out) } // Delete the contact _, _, err = runFreshCommand([]string{"contact", "delete", contactID}) if err != nil { t.Fatalf("contact delete error: %v", err) } // Verify it's gone out, _, _ = runFreshCommand([]string{"contact", "list"}) t.Logf("contact list after delete:\n%s", out) } // ======================================================================== // ATTACHMENT E2E TESTS // ======================================================================== func TestE2E_AttachmentListEmpty(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) root := newRootCmdBase() out, _, err := runCommand(root, []string{"attachment", "list"}) if err != nil { t.Logf("attachment list error: %v", err) } if out != "" { t.Logf("attachment list output:\n%s", out) } } func TestE2E_AttachmentUploadAndDownload(t *testing.T) { env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) // Create a temp file to upload testFile := filepath.Join(env.tempDir, "test-attachment.txt") os.WriteFile(testFile, []byte("Hello attachment world!"), 0600) // Upload out, _, err := runFreshCommand([]string{"attachment", "upload", "att-001", testFile}) if err != nil { t.Fatalf("attachment upload error: %v", err) } t.Logf("upload output:\n%s", out) // List out, _, err = runFreshCommand([]string{"attachment", "list"}) if err != nil { t.Fatalf("attachment list error: %v", err) } t.Logf("list output:\n%s", out) // Download downloadDir := filepath.Join(env.tempDir, "downloads") out, _, err = runFreshCommand([]string{"attachment", "download", "att-001", downloadDir}) if err != nil { t.Fatalf("attachment download error: %v", err) } t.Logf("download output:\n%s", out) // Verify downloaded file downloadedFile := filepath.Join(downloadDir, "att-001") data, err := os.ReadFile(downloadedFile) if err != nil { t.Fatalf("read downloaded file: %v", err) } if string(data) != "Hello attachment world!" { t.Errorf("expected 'Hello attachment world!', got %q", string(data)) } } func TestE2E_AttachmentUploadMissingArgs(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) root := newRootCmdBase() _, _, err := runCommand(root, []string{"attachment", "upload", "att-001"}) if err == nil { t.Error("expected error when file path is missing") } } func TestE2E_AttachmentUploadPathTraversal(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) testFile := filepath.Join(env.tempDir, "test-attachment.txt") os.WriteFile(testFile, []byte("traversal test"), 0600) root := newRootCmdBase() _, _, err := runCommand(root, []string{"attachment", "upload", "../etc/passwd", testFile}) if err == nil { t.Error("expected error for path traversal attachment ID") } } // ======================================================================== // FOLDER/LABEL E2E TESTS // ======================================================================== func TestE2E_FolderListSuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("GET /api/folders", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{ "Data": []map[string]interface{}{ {"ID": "folder-001", "Name": "Inbox", "Type": 0, "MessageCount": 10}, {"ID": "folder-002", "Name": "Sent", "Type": 0, "MessageCount": 5}, }, }) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"folder", "list"}) if err != nil { t.Logf("folder list error: %v", err) } if out != "" { t.Logf("folder list output:\n%s", out) } } func TestE2E_FolderCreateSuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/folders", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{ "Data": map[string]interface{}{"ID": "folder-new", "Name": "My Folder"}, }) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"folder", "create", "My Folder"}) if err != nil { t.Logf("folder create error: %v", err) } if out != "" { t.Logf("folder create output:\n%s", out) } } func TestE2E_FolderCreateMissingName(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) root := newRootCmdBase() _, _, err := runCommand(root, []string{"folder", "create"}) if err == nil { t.Error("expected error when folder name is missing") } } func TestE2E_FolderUpdateSuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/folders/folder-001", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{ "Data": map[string]interface{}{"ID": "folder-001", "Name": "Updated Folder"}, }) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"folder", "update", "folder-001", "--name", "Updated Folder"}) if err != nil { t.Logf("folder update error: %v", err) } if out != "" { t.Logf("folder update output:\n%s", out) } } func TestE2E_FolderDeleteSuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/folders/folder-001", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{}) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"folder", "delete", "folder-001"}) if err != nil { t.Logf("folder delete error: %v", err) } if out != "" { t.Logf("folder delete output:\n%s", out) } } func TestE2E_LabelListSuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("GET /api/labels", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{ "Data": []map[string]interface{}{ {"ID": "label-001", "Name": "Important", "Color": "#FF0000"}, {"ID": "label-002", "Name": "Work", "Color": "#0000FF"}, }, }) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"label", "list"}) if err != nil { t.Logf("label list error: %v", err) } if out != "" { t.Logf("label list output:\n%s", out) } } func TestE2E_LabelCreateSuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/labels", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{ "Data": map[string]interface{}{"ID": "label-new", "Name": "My Label", "Color": "#FF0000"}, }) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"label", "create", "My Label", "--color", "#FF0000"}) if err != nil { t.Logf("label create error: %v", err) } if out != "" { t.Logf("label create output:\n%s", out) } } func TestE2E_LabelApplySuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/messages/msg-001/setlabel", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{}) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"label", "apply", "msg-001", "label-001"}) if err != nil { t.Logf("label apply error: %v", err) } if out != "" { t.Logf("label apply output:\n%s", out) } } func TestE2E_LabelRemoveSuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/messages/msg-001/clearlabel", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{}) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"label", "remove", "msg-001", "label-001"}) if err != nil { t.Logf("label remove error: %v", err) } if out != "" { t.Logf("label remove output:\n%s", out) } } func TestE2E_LabelDeleteSuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/labels/label-001", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{}) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"label", "delete", "label-001"}) if err != nil { t.Logf("label delete error: %v", err) } if out != "" { t.Logf("label delete output:\n%s", out) } } // ======================================================================== // DRAFT E2E TESTS // ======================================================================== func TestE2E_DraftSaveSuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{ "Data": map[string]string{"ID": "draft-001"}, }) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{ "mail", "draft", "save", "--to", "recipient@example.com", "--subject", "Draft Subject", "--body", "Draft body content", }) if err != nil { t.Logf("draft save error: %v", err) } if out != "" { t.Logf("draft save output:\n%s", out) } } func TestE2E_DraftSaveMissingRecipient(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) root := newRootCmdBase() _, _, err := runCommand(root, []string{"mail", "draft", "save", "--subject", "No Recipient"}) if err == nil { t.Error("expected error when recipient is missing") } } func TestE2E_DraftSendSuccess(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/messages/draft-001/send", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{}) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"mail", "draft", "send", "draft-001"}) if err != nil { t.Logf("draft send error: %v", err) } if out != "" { t.Logf("draft send output:\n%s", out) } } // ======================================================================== // CLI STRUCTURE TESTS (fix existing failures) // ======================================================================== func TestE2E_RootCommandStructure(t *testing.T) { t.Parallel() root := newRootCmdBase() expectedCommands := []string{"login", "logout", "session", "mail", "draft", "contact", "attachment", "folder", "label"} actualCommands := make([]string, 0) for _, cmd := range root.Commands() { actualCommands = append(actualCommands, cmd.Name()) } for _, expected := range expectedCommands { found := false for _, actual := range actualCommands { if actual == expected { found = true break } } if !found { t.Errorf("root command missing expected subcommand: %s", expected) } } } func TestE2E_MailSubcommands(t *testing.T) { t.Parallel() root := newRootCmdBase() mailCmd, _, err := root.Find([]string{"mail"}) if err != nil { t.Fatalf("mail command not found: %v", err) } expectedSubcommands := []string{"list", "read", "send", "delete", "trash", "draft", "search"} actualSubcommands := make([]string, 0) for _, cmd := range mailCmd.Commands() { actualSubcommands = append(actualSubcommands, cmd.Name()) } for _, expected := range expectedSubcommands { found := false for _, actual := range actualSubcommands { if actual == expected { found = true break } } if !found { t.Errorf("mail command missing expected subcommand: %s", expected) } } } func TestE2E_DraftSubcommands(t *testing.T) { t.Parallel() root := newRootCmdBase() draftCmd, _, err := root.Find([]string{"draft"}) if err != nil { t.Fatalf("draft command not found: %v", err) } expectedSubcommands := []string{"list", "save", "edit", "send"} actualSubcommands := make([]string, 0) for _, cmd := range draftCmd.Commands() { actualSubcommands = append(actualSubcommands, cmd.Name()) } for _, expected := range expectedSubcommands { found := false for _, actual := range actualSubcommands { if actual == expected { found = true break } } if !found { t.Errorf("draft command missing expected subcommand: %s", expected) } } } func TestE2E_AttachmentSubcommands(t *testing.T) { t.Parallel() root := newRootCmdBase() attachCmd, _, err := root.Find([]string{"attachment"}) if err != nil { t.Fatalf("attachment command not found: %v", err) } expectedSubcommands := []string{"list", "upload", "download"} actualSubcommands := make([]string, 0) for _, cmd := range attachCmd.Commands() { actualSubcommands = append(actualSubcommands, cmd.Name()) } for _, expected := range expectedSubcommands { found := false for _, actual := range actualSubcommands { if actual == expected { found = true break } } if !found { t.Errorf("attachment command missing expected subcommand: %s", expected) } } } func TestE2E_ContactSubcommands(t *testing.T) { t.Parallel() root := newRootCmdBase() contactCmd, _, err := root.Find([]string{"contact"}) if err != nil { t.Fatalf("contact command not found: %v", err) } expectedSubcommands := []string{"list", "add", "edit", "delete"} actualSubcommands := make([]string, 0) for _, cmd := range contactCmd.Commands() { actualSubcommands = append(actualSubcommands, cmd.Name()) } for _, expected := range expectedSubcommands { found := false for _, actual := range actualSubcommands { if actual == expected { found = true break } } if !found { t.Errorf("contact command missing expected subcommand: %s", expected) } } } // ======================================================================== // OUTPUT FORMATTING TESTS // ======================================================================== func TestE2E_OutputFormattingMailList(t *testing.T) { t.Parallel() env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) env.writeEncryptedSession("user-123", "test-access-token", "test-refresh-token", time.Now().Add(1*time.Hour).Unix()) env.mockServer.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) { jsonResp(t, w, http.StatusOK, map[string]interface{}{ "Total": 2, "Data": []map[string]interface{}{ { "ID": "msg-001", "Subject": "Hello", "Sender": map[string]string{"Address": "alice@example.com", "Name": "Alice"}, "CreatedAt": "2026-05-01T10:00:00Z", "Read": true, "Starred": true, }, { "ID": "msg-002", "Subject": "World", "Sender": map[string]string{"Address": "bob@example.com"}, "CreatedAt": "2026-05-02T14:30:00Z", "Read": false, "Starred": false, }, }, }) }) root := newRootCmdBase() out, _, err := runCommand(root, []string{"mail", "list"}) if err != nil { t.Logf("mail list error: %v", err) } if out != "" { // Verify output contains expected formatting if len(out) < 10 { t.Error("mail list output seems too short") } t.Logf("formatted output:\n%s", out) } } func TestE2E_ErrorMessageNotAuthenticated(t *testing.T) { env := setupE2E(t) popDir := filepath.Join(env.tempDir, ".config", "pop") configData, _ := json.Marshal(map[string]interface{}{ "api_base_url": env.mockServer.URL(), "timeout_sec": 5, "rate_limit_requests": 100, "rate_limit_window_sec": 60, }) os.WriteFile(filepath.Join(popDir, "config.json"), configData, 0600) root := newRootCmdBase() _, errOut, err := runCommand(root, []string{"mail", "list"}) if err == nil { t.Error("expected error when not authenticated") } // Verify error message contains helpful text if len(errOut) > 0 { t.Logf("error output: %s", errOut) } } // ======================================================================== // HELP OUTPUT TESTS // ======================================================================== func TestE2E_HelpOutput(t *testing.T) { commands := []string{ "login", "logout", "session", "mail", "mail list", "mail read", "mail send", "mail delete", "mail trash", "mail search", "contact", "contact list", "contact add", "contact edit", "contact delete", "attachment", "attachment list", "attachment upload", "attachment download", "folder", "folder list", "folder create", "folder update", "folder delete", "label", "label list", "label create", "label update", "label delete", "label apply", "label remove", "draft", "draft list", "draft save", "draft edit", "draft send", } for _, cmdPath := range commands { t.Run(cmdPath, func(t *testing.T) { args := append(strings.Split(cmdPath, " "), "--help") out, _, err := runFreshCommand(args) if err != nil && err.Error() != "help" { t.Errorf("help for %s: %v", cmdPath, err) } if len(out) == 0 { t.Errorf("help output for %s is empty", cmdPath) } }) } }