- Pass nil refresher to NewProtonMailClient at all 5 call sites - Change TestListMessages_APIError from 401 to 403 (avoids refresh interception) - Add error content assertion to TestGetMessage_NotFound
1390 lines
35 KiB
Go
1390 lines
35 KiB
Go
package mail
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/frenocorp/pop/internal/api"
|
|
"github.com/frenocorp/pop/internal/config"
|
|
)
|
|
|
|
// mockServer wraps an httptest.Server with configurable handlers.
|
|
type mockServer struct {
|
|
mux *http.ServeMux
|
|
server *httptest.Server
|
|
handlers sync.Map
|
|
}
|
|
|
|
func newMockServer(t *testing.T) *mockServer {
|
|
t.Helper()
|
|
mux := http.NewServeMux()
|
|
srv := httptest.NewServer(mux)
|
|
|
|
ms := &mockServer{
|
|
mux: mux,
|
|
server: srv,
|
|
}
|
|
|
|
mux.HandleFunc("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
resolveHandler(ms, w, r)
|
|
})
|
|
mux.HandleFunc("POST /api/messages/search", func(w http.ResponseWriter, r *http.Request) {
|
|
resolveHandler(ms, w, r)
|
|
})
|
|
mux.HandleFunc("POST /api/messages/{id}", func(w http.ResponseWriter, r *http.Request) {
|
|
resolveHandler(ms, w, r)
|
|
})
|
|
mux.HandleFunc("POST /api/messages/{id}/movetotrash", func(w http.ResponseWriter, r *http.Request) {
|
|
resolveHandler(ms, w, r)
|
|
})
|
|
mux.HandleFunc("POST /api/messages/{id}/delete", func(w http.ResponseWriter, r *http.Request) {
|
|
resolveHandler(ms, w, r)
|
|
})
|
|
mux.HandleFunc("POST /api/messages/{id}/send", func(w http.ResponseWriter, r *http.Request) {
|
|
resolveHandler(ms, w, r)
|
|
})
|
|
|
|
return ms
|
|
}
|
|
|
|
func (ms *mockServer) URL() string {
|
|
return ms.server.URL
|
|
}
|
|
|
|
func (ms *mockServer) Close() {
|
|
ms.server.Close()
|
|
}
|
|
|
|
func (ms *mockServer) Handle(key string, handler http.HandlerFunc) {
|
|
ms.handlers.Store(key, handler)
|
|
}
|
|
|
|
func resolveHandler(ms *mockServer, w http.ResponseWriter, r *http.Request) {
|
|
key := r.Method + " " + r.URL.Path
|
|
if handler, loaded := ms.handlers.Load(key); loaded {
|
|
handler.(http.HandlerFunc)(w, r)
|
|
return
|
|
}
|
|
// When POST /api/messages is called, it matches both list and send/draft.
|
|
// The generic handler for POST /api/messages catches all unmatched POST /api/messages calls.
|
|
w.WriteHeader(http.StatusNotFound)
|
|
fmt.Fprintf(w, `{"Code":404,"Message":"not found"}`)
|
|
}
|
|
|
|
// newTestClient creates a mail.Client backed by a mock server.
|
|
func newTestClient(t *testing.T, srv *mockServer) *Client {
|
|
t.Helper()
|
|
cfg := &config.Config{
|
|
APIBaseURL: srv.URL(),
|
|
TimeoutSec: 5,
|
|
RateLimitReq: 100,
|
|
RateLimitWin: 60,
|
|
}
|
|
apiClient := api.NewProtonMailClient(cfg, nil)
|
|
apiClient.SetAuthHeader("test-token")
|
|
return NewClient(apiClient)
|
|
}
|
|
|
|
// readJSON reads request body and returns parsed map.
|
|
func readJSON(t *testing.T, r *http.Request) map[string]interface{} {
|
|
t.Helper()
|
|
data, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Fatalf("read body: %v", err)
|
|
}
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
t.Fatalf("unmarshal body: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// writeJSON writes a JSON response.
|
|
func writeJSON(t *testing.T, w http.ResponseWriter, code int, v interface{}) {
|
|
t.Helper()
|
|
w.WriteHeader(code)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
// ---------- ListMessages ----------
|
|
|
|
func TestListMessages_Success(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
expected := ListMessagesResponse{
|
|
Total: 2,
|
|
Messages: []Message{
|
|
{MessageID: "msg-1", Subject: "Hello", Sender: Recipient{Address: "alice@example.com"}},
|
|
{MessageID: "msg-2", Subject: "World", Sender: Recipient{Address: "bob@example.com"}},
|
|
},
|
|
}
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
if body["Page"] != float64(1) {
|
|
t.Errorf("expected page 1, got %v", body["Page"])
|
|
}
|
|
if body["PageSize"] != float64(25) {
|
|
t.Errorf("expected page size 25, got %v", body["PageSize"])
|
|
}
|
|
if body["Passphrase"] != "test-pass" {
|
|
t.Errorf("expected passphrase test-pass, got %v", body["Passphrase"])
|
|
}
|
|
writeJSON(t, w, http.StatusOK, expected)
|
|
})
|
|
|
|
resp, err := client.ListMessages(ListMessagesRequest{
|
|
Page: 1,
|
|
PageSize: 25,
|
|
Passphrase: "test-pass",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ListMessages: %v", err)
|
|
}
|
|
if resp.Total != 2 {
|
|
t.Errorf("expected 2 messages, got %d", resp.Total)
|
|
}
|
|
if len(resp.Messages) != 2 {
|
|
t.Errorf("expected 2 messages in slice, got %d", len(resp.Messages))
|
|
}
|
|
if resp.Messages[0].Subject != "Hello" {
|
|
t.Errorf("expected subject Hello, got %s", resp.Messages[0].Subject)
|
|
}
|
|
}
|
|
|
|
func TestListMessages_WithFolderFilter(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
if body["Type"] != float64(FolderSent) {
|
|
t.Errorf("expected Type=3 (sent), got %v", body["Type"])
|
|
}
|
|
writeJSON(t, w, http.StatusOK, ListMessagesResponse{Total: 0, Messages: []Message{}})
|
|
})
|
|
|
|
_, err := client.ListMessages(ListMessagesRequest{
|
|
Folder: FolderSent,
|
|
Page: 1,
|
|
PageSize: 10,
|
|
Passphrase: "pass",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ListMessages: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestListMessages_InboxOmitsType(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
if _, ok := body["Type"]; ok {
|
|
t.Error("Inbox should omit Type field")
|
|
}
|
|
writeJSON(t, w, http.StatusOK, ListMessagesResponse{Total: 0, Messages: []Message{}})
|
|
})
|
|
|
|
_, err := client.ListMessages(ListMessagesRequest{
|
|
Folder: FolderInbox,
|
|
Page: 1,
|
|
PageSize: 10,
|
|
Passphrase: "pass",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ListMessages: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestListMessages_WithStarredFilter(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
starred := true
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
if body["Starred"] != true {
|
|
t.Errorf("expected Starred=true, got %v", body["Starred"])
|
|
}
|
|
writeJSON(t, w, http.StatusOK, ListMessagesResponse{Total: 0, Messages: []Message{}})
|
|
})
|
|
|
|
_, err := client.ListMessages(ListMessagesRequest{
|
|
Folder: FolderInbox,
|
|
Page: 1,
|
|
PageSize: 10,
|
|
Passphrase: "pass",
|
|
Starred: &starred,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ListMessages: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestListMessages_WithReadFilter(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
unread := false
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
if body["Read"] != false {
|
|
t.Errorf("expected Read=false, got %v", body["Read"])
|
|
}
|
|
writeJSON(t, w, http.StatusOK, ListMessagesResponse{Total: 0, Messages: []Message{}})
|
|
})
|
|
|
|
_, err := client.ListMessages(ListMessagesRequest{
|
|
Folder: FolderInbox,
|
|
Page: 1,
|
|
PageSize: 10,
|
|
Passphrase: "pass",
|
|
Read: &unread,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ListMessages: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestListMessages_WithSinceFilter(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
since := int64(1700000000)
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
if body["Since"] != float64(since) {
|
|
t.Errorf("expected Since=%d, got %v", since, body["Since"])
|
|
}
|
|
writeJSON(t, w, http.StatusOK, ListMessagesResponse{Total: 0, Messages: []Message{}})
|
|
})
|
|
|
|
_, err := client.ListMessages(ListMessagesRequest{
|
|
Folder: FolderInbox,
|
|
Page: 1,
|
|
PageSize: 10,
|
|
Passphrase: "pass",
|
|
Since: since,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ListMessages: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestListMessages_APIError(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
fmt.Fprintf(w, `{"Code":403,"Message":"invalid token"}`)
|
|
})
|
|
|
|
_, err := client.ListMessages(ListMessagesRequest{
|
|
Page: 1,
|
|
PageSize: 10,
|
|
Passphrase: "pass",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for 403 response")
|
|
}
|
|
if !strings.Contains(err.Error(), "invalid token") {
|
|
t.Errorf("expected 'invalid token' in error, got: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestListMessages_BadJSON(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprint(w, `{"bad json`)
|
|
})
|
|
|
|
_, err := client.ListMessages(ListMessagesRequest{
|
|
Page: 1,
|
|
PageSize: 10,
|
|
Passphrase: "pass",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for bad JSON")
|
|
}
|
|
if !strings.Contains(err.Error(), "failed to parse response") {
|
|
t.Errorf("unexpected error: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
// ---------- GetMessage ----------
|
|
|
|
func TestGetMessage_Success(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
expectedMsg := Message{
|
|
MessageID: "msg-42",
|
|
Subject: "Test Message",
|
|
Sender: Recipient{Address: "sender@example.com"},
|
|
Body: "Decrypted body content",
|
|
}
|
|
|
|
srv.Handle("POST /api/messages/msg-42", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
if body["Passphrase"] != "pass" {
|
|
t.Errorf("expected passphrase pass, got %v", body["Passphrase"])
|
|
}
|
|
writeJSON(t, w, http.StatusOK, map[string]interface{}{"Data": expectedMsg})
|
|
})
|
|
|
|
msg, err := client.GetMessage("msg-42", "pass")
|
|
if err != nil {
|
|
t.Fatalf("GetMessage: %v", err)
|
|
}
|
|
if msg.MessageID != "msg-42" {
|
|
t.Errorf("expected msg-42, got %s", msg.MessageID)
|
|
}
|
|
if msg.Subject != "Test Message" {
|
|
t.Errorf("expected subject Test Message, got %s", msg.Subject)
|
|
}
|
|
if msg.Body != "Decrypted body content" {
|
|
t.Errorf("expected body content, got %s", msg.Body)
|
|
}
|
|
}
|
|
|
|
func TestGetMessage_URLEscape(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
msgID := "msg/with/slashes"
|
|
srv.Handle("POST /api/messages/msg/with/slashes", func(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(t, w, http.StatusOK, map[string]interface{}{"Data": Message{MessageID: msgID}})
|
|
})
|
|
|
|
msg, err := client.GetMessage(msgID, "pass")
|
|
if err != nil {
|
|
t.Fatalf("GetMessage: %v", err)
|
|
}
|
|
if msg.MessageID != msgID {
|
|
t.Errorf("expected %s, got %s", msgID, msg.MessageID)
|
|
}
|
|
}
|
|
|
|
func TestGetMessage_NotFound(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages/msg-999", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
fmt.Fprintf(w, `{"Code":404,"Message":"message not found"}`)
|
|
})
|
|
|
|
_, err := client.GetMessage("msg-999", "pass")
|
|
if err == nil {
|
|
t.Fatal("expected error for 404")
|
|
}
|
|
if !strings.Contains(err.Error(), "message not found") {
|
|
t.Errorf("expected 'message not found' in error, got: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestGetMessage_DecryptBody(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
|
|
svc, _, pass := newTestService(t)
|
|
cfg := &config.Config{
|
|
APIBaseURL: srv.URL(),
|
|
TimeoutSec: 5,
|
|
RateLimitReq: 100,
|
|
RateLimitWin: 60,
|
|
}
|
|
apiClient := api.NewProtonMailClient(cfg, nil)
|
|
apiClient.SetAuthHeader("test-token")
|
|
client := NewClient(apiClient)
|
|
client.SetPGPService(svc)
|
|
|
|
// Simulate an encrypted body that the API returns
|
|
encryptedBody, err := svc.EncryptBody("Secret message content", pass)
|
|
if err != nil {
|
|
t.Fatalf("EncryptBody: %v", err)
|
|
}
|
|
|
|
msgWithEncryptedBody := Message{
|
|
MessageID: "msg-enc",
|
|
Subject: "Encrypted",
|
|
BodyEnc: encryptedBody,
|
|
}
|
|
|
|
srv.Handle("POST /api/messages/msg-enc", func(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(t, w, http.StatusOK, map[string]interface{}{"Data": msgWithEncryptedBody})
|
|
})
|
|
|
|
msg, err := client.GetMessage("msg-enc", pass)
|
|
if err != nil {
|
|
t.Fatalf("GetMessage: %v", err)
|
|
}
|
|
if msg.BodyEnc != encryptedBody {
|
|
t.Error("encrypted body should be preserved")
|
|
}
|
|
}
|
|
|
|
// ---------- Send ----------
|
|
|
|
func TestSend_Success(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
if body["Subject"] != "Test Subject" {
|
|
t.Errorf("expected subject Test Subject, got %v", body["Subject"])
|
|
}
|
|
if body["HTML"] != true {
|
|
t.Error("expected HTML=true")
|
|
}
|
|
writeJSON(t, w, http.StatusOK, map[string]interface{}{"Data": map[string]string{"MessageID": "sent-1"}})
|
|
})
|
|
|
|
err := client.Send(SendRequest{
|
|
To: []Recipient{{Address: "to@example.com"}},
|
|
Subject: "Test Subject",
|
|
Body: "Plain body",
|
|
HTML: true,
|
|
Passphrase: "pass",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Send: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSend_WithPGP(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
|
|
svc, _, pass := newTestService(t)
|
|
cfg := &config.Config{
|
|
APIBaseURL: srv.URL(),
|
|
TimeoutSec: 5,
|
|
RateLimitReq: 100,
|
|
RateLimitWin: 60,
|
|
}
|
|
apiClient := api.NewProtonMailClient(cfg, nil)
|
|
apiClient.SetAuthHeader("test-token")
|
|
client := NewClient(apiClient)
|
|
client.SetPGPService(svc)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
// When PGP service is set, BodyEnc should be present instead of Body
|
|
if _, hasBody := body["Body"]; hasBody {
|
|
t.Error("expected BodyEnc instead of Body when PGP service is set")
|
|
}
|
|
if body["BodyEnc"] == "" {
|
|
t.Error("expected BodyEnc to be set")
|
|
}
|
|
writeJSON(t, w, http.StatusCreated, map[string]interface{}{})
|
|
})
|
|
|
|
err := client.Send(SendRequest{
|
|
To: []Recipient{{Address: "to@example.com"}},
|
|
Subject: "PGP Encrypted",
|
|
Body: "Secret content",
|
|
Passphrase: pass,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Send with PGP: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSend_WithCC(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
cc, ok := body["CC"].([]interface{})
|
|
if !ok || len(cc) != 1 {
|
|
t.Error("expected CC with 1 recipient")
|
|
}
|
|
writeJSON(t, w, http.StatusOK, map[string]interface{}{})
|
|
})
|
|
|
|
err := client.Send(SendRequest{
|
|
To: []Recipient{{Address: "to@example.com"}},
|
|
CC: []Recipient{{Address: "cc@example.com"}},
|
|
Subject: "With CC",
|
|
Passphrase: "pass",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Send: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSend_WithBCC(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
bcc, ok := body["BCC"].([]interface{})
|
|
if !ok || len(bcc) != 1 {
|
|
t.Error("expected BCC with 1 recipient")
|
|
}
|
|
writeJSON(t, w, http.StatusOK, map[string]interface{}{})
|
|
})
|
|
|
|
err := client.Send(SendRequest{
|
|
To: []Recipient{{Address: "to@example.com"}},
|
|
BCC: []Recipient{{Address: "bcc@example.com"}},
|
|
Subject: "With BCC",
|
|
Passphrase: "pass",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Send: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSend_HTTPError(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprintf(w, `{"Code":400,"Message":"invalid recipient"}`)
|
|
})
|
|
|
|
err := client.Send(SendRequest{
|
|
To: []Recipient{{Address: ""}},
|
|
Subject: "Bad",
|
|
Passphrase: "pass",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for 400")
|
|
}
|
|
// API error is returned directly by Do(), then wrapped by Send()
|
|
if !strings.Contains(err.Error(), "invalid recipient") {
|
|
t.Errorf("expected 'invalid recipient' in error, got: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestSend_CreatedStatus(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusCreated)
|
|
fmt.Fprintf(w, `{"Data":{"MessageID":"created-1"}}`)
|
|
})
|
|
|
|
err := client.Send(SendRequest{
|
|
To: []Recipient{{Address: "to@example.com"}},
|
|
Subject: "Created",
|
|
Passphrase: "pass",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Send with 201: %v", err)
|
|
}
|
|
}
|
|
|
|
// ---------- MoveToTrash ----------
|
|
|
|
func TestMoveToTrash_Success(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages/msg-1/movetotrash", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
|
|
t.Error("expected form-urlencoded content type")
|
|
}
|
|
body, _ := io.ReadAll(r.Body)
|
|
if !strings.Contains(string(body), "Passphrase=pass") {
|
|
t.Errorf("expected Passphrase in form body, got %s", body)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
err := client.MoveToTrash("msg-1", "pass")
|
|
if err != nil {
|
|
t.Fatalf("MoveToTrash: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMoveToTrash_Error(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages/msg-1/movetotrash", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
fmt.Fprint(w, "server error")
|
|
})
|
|
|
|
err := client.MoveToTrash("msg-1", "pass")
|
|
if err == nil {
|
|
t.Fatal("expected error for 500")
|
|
}
|
|
if !strings.Contains(err.Error(), "trash failed") {
|
|
t.Errorf("unexpected error: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
// ---------- PermanentlyDelete ----------
|
|
|
|
func TestPermanentlyDelete_Success(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages/msg-1/delete", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
err := client.PermanentlyDelete("msg-1")
|
|
if err != nil {
|
|
t.Fatalf("PermanentlyDelete: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPermanentlyDelete_Error(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages/msg-1/delete", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
fmt.Fprintf(w, `{"Code":404,"Message":"not found"}`)
|
|
})
|
|
|
|
err := client.PermanentlyDelete("msg-1")
|
|
if err == nil {
|
|
t.Fatal("expected error for 404")
|
|
}
|
|
// When API returns parseable error JSON, Do() returns *APIError which is wrapped
|
|
if !strings.Contains(err.Error(), "not found") {
|
|
t.Errorf("expected 'not found' in error, got: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestPermanentlyDelete_URLEscape(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
msgID := "msg/with/slashes"
|
|
srv.Handle("POST /api/messages/msg/with/slashes/delete", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
err := client.PermanentlyDelete(msgID)
|
|
if err != nil {
|
|
t.Fatalf("PermanentlyDelete: %v", err)
|
|
}
|
|
}
|
|
|
|
// ---------- SaveDraft ----------
|
|
|
|
func TestSaveDraft_Success(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
if body["Type"] != MessageTypeDraft {
|
|
t.Errorf("expected Type=%s, got %v", MessageTypeDraft, body["Type"])
|
|
}
|
|
if body["Subject"] != "Draft Subject" {
|
|
t.Errorf("expected subject Draft Subject, got %v", body["Subject"])
|
|
}
|
|
writeJSON(t, w, http.StatusOK, map[string]interface{}{
|
|
"Data": map[string]string{"MessageID": "draft-1"},
|
|
})
|
|
})
|
|
|
|
id, err := client.SaveDraft(Draft{
|
|
To: []Recipient{{Address: "to@example.com"}},
|
|
Subject: "Draft Subject",
|
|
Body: "Draft body",
|
|
}, "pass")
|
|
if err != nil {
|
|
t.Fatalf("SaveDraft: %v", err)
|
|
}
|
|
if id != "draft-1" {
|
|
t.Errorf("expected draft-1, got %s", id)
|
|
}
|
|
}
|
|
|
|
func TestSaveDraft_WithCC(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
if _, ok := body["CC"]; !ok {
|
|
t.Error("expected CC field")
|
|
}
|
|
if _, ok := body["BCC"]; !ok {
|
|
t.Error("expected BCC field")
|
|
}
|
|
writeJSON(t, w, http.StatusOK, map[string]interface{}{
|
|
"Data": map[string]string{"MessageID": "draft-2"},
|
|
})
|
|
})
|
|
|
|
_, err := client.SaveDraft(Draft{
|
|
To: []Recipient{{Address: "to@example.com"}},
|
|
CC: []Recipient{{Address: "cc@example.com"}},
|
|
BCC: []Recipient{{Address: "bcc@example.com"}},
|
|
Subject: "Draft with CC/BCC",
|
|
Body: "Body",
|
|
}, "pass")
|
|
if err != nil {
|
|
t.Fatalf("SaveDraft: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSaveDraft_Error(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprintf(w, `{"Code":400,"Message":"invalid recipient"}`)
|
|
})
|
|
|
|
_, err := client.SaveDraft(Draft{
|
|
To: []Recipient{{Address: ""}},
|
|
Subject: "Bad Draft",
|
|
Body: "Body",
|
|
}, "pass")
|
|
if err == nil {
|
|
t.Fatal("expected error for 400")
|
|
}
|
|
}
|
|
|
|
func TestSaveDraft_BadJSON(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprint(w, `{"bad json`)
|
|
})
|
|
|
|
_, err := client.SaveDraft(Draft{
|
|
To: []Recipient{{Address: "to@example.com"}},
|
|
Subject: "Bad JSON",
|
|
Body: "Body",
|
|
}, "pass")
|
|
if err == nil {
|
|
t.Fatal("expected error for bad JSON")
|
|
}
|
|
if !strings.Contains(err.Error(), "failed to parse response") {
|
|
t.Errorf("unexpected error: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
// ---------- UpdateDraft ----------
|
|
|
|
func TestUpdateDraft_Success(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages/draft-1", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
if body["Subject"] != "Updated Subject" {
|
|
t.Errorf("expected Updated Subject, got %v", body["Subject"])
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
err := client.UpdateDraft("draft-1", Draft{
|
|
To: []Recipient{{Address: "updated@example.com"}},
|
|
Subject: "Updated Subject",
|
|
Body: "Updated body",
|
|
}, "pass")
|
|
if err != nil {
|
|
t.Fatalf("UpdateDraft: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUpdateDraft_WithCC(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages/draft-1", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
if _, ok := body["CC"]; !ok {
|
|
t.Error("expected CC field in update")
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
err := client.UpdateDraft("draft-1", Draft{
|
|
To: []Recipient{{Address: "to@example.com"}},
|
|
CC: []Recipient{{Address: "cc@example.com"}},
|
|
Subject: "Updated",
|
|
Body: "Body",
|
|
}, "pass")
|
|
if err != nil {
|
|
t.Fatalf("UpdateDraft: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUpdateDraft_Error(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages/draft-1", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusConflict)
|
|
fmt.Fprintf(w, `{"Code":409,"Message":"draft locked"}`)
|
|
})
|
|
|
|
err := client.UpdateDraft("draft-1", Draft{
|
|
To: []Recipient{{Address: "to@example.com"}},
|
|
Subject: "Conflict",
|
|
Body: "Body",
|
|
}, "pass")
|
|
if err == nil {
|
|
t.Fatal("expected error for 409")
|
|
}
|
|
// API error JSON is parsed by Do(), returns *APIError wrapped by UpdateDraft
|
|
if !strings.Contains(err.Error(), "draft locked") {
|
|
t.Errorf("expected 'draft locked' in error, got: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
// ---------- SendDraft ----------
|
|
|
|
func TestSendDraft_Success(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages/draft-1/send", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
|
|
t.Error("expected form-urlencoded content type")
|
|
}
|
|
body, _ := io.ReadAll(r.Body)
|
|
if !strings.Contains(string(body), "Passphrase=pass") {
|
|
t.Errorf("expected Passphrase in form, got %s", body)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
err := client.SendDraft("draft-1", "pass")
|
|
if err != nil {
|
|
t.Fatalf("SendDraft: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSendDraft_Error(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages/draft-1/send", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
fmt.Fprint(w, "not found")
|
|
})
|
|
|
|
err := client.SendDraft("draft-1", "pass")
|
|
if err == nil {
|
|
t.Fatal("expected error for 404")
|
|
}
|
|
if !strings.Contains(err.Error(), "send draft failed") {
|
|
t.Errorf("unexpected error: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
// ---------- ListDrafts ----------
|
|
|
|
func TestListDrafts_Success(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
expected := ListMessagesResponse{
|
|
Total: 1,
|
|
Messages: []Message{
|
|
{MessageID: "draft-1", Subject: "My Draft", Type: int(FolderDraft)},
|
|
},
|
|
}
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
if body["Type"] != float64(FolderDraft) {
|
|
t.Errorf("expected Type=2 (draft), got %v", body["Type"])
|
|
}
|
|
writeJSON(t, w, http.StatusOK, expected)
|
|
})
|
|
|
|
resp, err := client.ListDrafts(1, 10, "pass")
|
|
if err != nil {
|
|
t.Fatalf("ListDrafts: %v", err)
|
|
}
|
|
if resp.Total != 1 {
|
|
t.Errorf("expected 1 draft, got %d", resp.Total)
|
|
}
|
|
}
|
|
|
|
// ---------- SearchMessages ----------
|
|
|
|
func TestSearchMessages_Success(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
expected := SearchResponse{
|
|
Total: 3,
|
|
Messages: []Message{
|
|
{MessageID: "s1", Subject: "Result 1"},
|
|
{MessageID: "s2", Subject: "Result 2"},
|
|
{MessageID: "s3", Subject: "Result 3"},
|
|
},
|
|
}
|
|
|
|
srv.Handle("POST /api/messages/search", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
if body["Query"] != "invoice" {
|
|
t.Errorf("expected query invoice, got %v", body["Query"])
|
|
}
|
|
if body["Page"] != float64(1) {
|
|
t.Errorf("expected page 1, got %v", body["Page"])
|
|
}
|
|
if body["PageSize"] != float64(20) {
|
|
t.Errorf("expected page size 20, got %v", body["PageSize"])
|
|
}
|
|
writeJSON(t, w, http.StatusOK, expected)
|
|
})
|
|
|
|
resp, err := client.SearchMessages(SearchRequest{
|
|
Query: "invoice",
|
|
Page: 1,
|
|
PageSize: 20,
|
|
Passphrase: "pass",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SearchMessages: %v", err)
|
|
}
|
|
if resp.Total != 3 {
|
|
t.Errorf("expected 3 results, got %d", resp.Total)
|
|
}
|
|
if len(resp.Messages) != 3 {
|
|
t.Errorf("expected 3 messages, got %d", len(resp.Messages))
|
|
}
|
|
}
|
|
|
|
func TestSearchMessages_EmptyResults(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages/search", func(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(t, w, http.StatusOK, SearchResponse{Total: 0, Messages: []Message{}})
|
|
})
|
|
|
|
resp, err := client.SearchMessages(SearchRequest{
|
|
Query: "nonexistent",
|
|
Page: 1,
|
|
PageSize: 10,
|
|
Passphrase: "pass",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SearchMessages: %v", err)
|
|
}
|
|
if resp.Total != 0 {
|
|
t.Errorf("expected 0 results, got %d", resp.Total)
|
|
}
|
|
}
|
|
|
|
func TestSearchMessages_APIError(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages/search", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusTooManyRequests)
|
|
fmt.Fprintf(w, `{"Code":429,"Message":"rate limited"}`)
|
|
})
|
|
|
|
_, err := client.SearchMessages(SearchRequest{
|
|
Query: "test",
|
|
Page: 1,
|
|
PageSize: 10,
|
|
Passphrase: "pass",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for 429")
|
|
}
|
|
if !strings.Contains(err.Error(), "rate limited") {
|
|
t.Errorf("expected 'rate limited' in error, got: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestSearchMessages_BadJSON(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages/search", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprint(w, `not json at all`)
|
|
})
|
|
|
|
_, err := client.SearchMessages(SearchRequest{
|
|
Query: "test",
|
|
Page: 1,
|
|
PageSize: 10,
|
|
Passphrase: "pass",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for bad JSON")
|
|
}
|
|
if !strings.Contains(err.Error(), "failed to parse response") {
|
|
t.Errorf("unexpected error: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
// ---------- Auth Header Propagation ----------
|
|
|
|
func TestAuthHeader_Propagated(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
|
|
cfg := &config.Config{
|
|
APIBaseURL: srv.URL(),
|
|
TimeoutSec: 5,
|
|
RateLimitReq: 100,
|
|
RateLimitWin: 60,
|
|
}
|
|
apiClient := api.NewProtonMailClient(cfg, nil)
|
|
apiClient.SetAuthHeader("my-test-token")
|
|
client := NewClient(apiClient)
|
|
|
|
var capturedAuth string
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
capturedAuth = r.Header.Get("Authorization")
|
|
writeJSON(t, w, http.StatusOK, ListMessagesResponse{})
|
|
})
|
|
|
|
_, _ = client.ListMessages(ListMessagesRequest{
|
|
Page: 1,
|
|
PageSize: 10,
|
|
Passphrase: "pass",
|
|
})
|
|
|
|
if capturedAuth != "Bearer my-test-token" {
|
|
t.Errorf("expected Bearer my-test-token, got %s", capturedAuth)
|
|
}
|
|
}
|
|
|
|
// ---------- Content-Type Headers ----------
|
|
|
|
func TestContentTypes_JSON(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
ct := r.Header.Get("Content-Type")
|
|
if ct != "application/json" {
|
|
t.Errorf("expected application/json, got %s", ct)
|
|
}
|
|
writeJSON(t, w, http.StatusOK, ListMessagesResponse{})
|
|
})
|
|
|
|
_, _ = client.ListMessages(ListMessagesRequest{
|
|
Page: 1,
|
|
PageSize: 10,
|
|
Passphrase: "pass",
|
|
})
|
|
}
|
|
|
|
func TestContentTypes_FormUrlEncoded(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages/msg-1/movetotrash", func(w http.ResponseWriter, r *http.Request) {
|
|
ct := r.Header.Get("Content-Type")
|
|
if ct != "application/x-www-form-urlencoded" {
|
|
t.Errorf("expected application/x-www-form-urlencoded, got %s", ct)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
_ = client.MoveToTrash("msg-1", "pass")
|
|
}
|
|
|
|
// ---------- Concurrent Access ----------
|
|
|
|
func TestListMessages_Concurrent(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
var mu sync.Mutex
|
|
callCount := 0
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
mu.Lock()
|
|
callCount++
|
|
mu.Unlock()
|
|
writeJSON(t, w, http.StatusOK, ListMessagesResponse{Total: 0})
|
|
})
|
|
|
|
const goroutines = 10
|
|
var wg sync.WaitGroup
|
|
var results [10]error
|
|
for i := 0; i < goroutines; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
_, results[idx] = client.ListMessages(ListMessagesRequest{
|
|
Page: 1,
|
|
PageSize: 10,
|
|
Passphrase: "pass",
|
|
})
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
for i, err := range results {
|
|
if err != nil {
|
|
t.Errorf("goroutine %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
mu.Lock()
|
|
if callCount != goroutines {
|
|
t.Errorf("expected %d calls, got %d", goroutines, callCount)
|
|
}
|
|
mu.Unlock()
|
|
}
|
|
|
|
// ---------- Folder Name ----------
|
|
|
|
func TestFolder_Name(t *testing.T) {
|
|
tests := []struct {
|
|
folder Folder
|
|
name string
|
|
}{
|
|
{FolderInbox, "Inbox"},
|
|
{FolderSent, "Sent"},
|
|
{FolderDraft, "Drafts"},
|
|
{FolderTrash, "Trash"},
|
|
{FolderSpam, "Spam"},
|
|
{Folder(99), "Unknown"},
|
|
}
|
|
for _, tt := range tests {
|
|
if tt.folder.Name() != tt.name {
|
|
t.Errorf("Folder(%d).Name() = %q, want %q", tt.folder, tt.folder.Name(), tt.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------- Recipient DisplayName ----------
|
|
|
|
func TestRecipient_DisplayName(t *testing.T) {
|
|
tests := []struct {
|
|
r Recipient
|
|
expect string
|
|
}{
|
|
{Recipient{Name: "Alice", Address: "alice@example.com"}, "Alice <alice@example.com>"},
|
|
{Recipient{Address: "bob@example.com"}, "bob@example.com"},
|
|
}
|
|
for i, tt := range tests {
|
|
got := tt.r.DisplayName()
|
|
if got != tt.expect {
|
|
t.Errorf("case %d: got %q, want %q", i, got, tt.expect)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------- Message Folder ----------
|
|
|
|
func TestMessage_Folder(t *testing.T) {
|
|
tests := []struct {
|
|
msg Message
|
|
folder Folder
|
|
}{
|
|
{Message{Type: int(FolderDraft)}, FolderDraft},
|
|
{Message{Type: int(FolderSent)}, FolderSent},
|
|
{Message{Type: int(FolderInbox)}, FolderInbox},
|
|
{Message{Type: 0}, FolderInbox},
|
|
{Message{Type: 99}, FolderInbox},
|
|
}
|
|
for i, tt := range tests {
|
|
got := tt.msg.Folder()
|
|
if got != tt.folder {
|
|
t.Errorf("case %d: got %v, want %v", i, got, tt.folder)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------- SetPGPService ----------
|
|
|
|
func TestSetPGPService(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
svc, _, _ := newTestService(t)
|
|
client.SetPGPService(svc)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
// PGP service should cause BodyEnc instead of Body
|
|
if _, hasBody := body["Body"]; hasBody {
|
|
t.Error("expected BodyEnc when PGP service is set")
|
|
}
|
|
writeJSON(t, w, http.StatusCreated, map[string]interface{}{})
|
|
})
|
|
|
|
err := client.Send(SendRequest{
|
|
To: []Recipient{{Address: "to@example.com"}},
|
|
Subject: "PGP Test",
|
|
Body: "Secret",
|
|
Passphrase: "test-passphrase",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Send: %v", err)
|
|
}
|
|
}
|
|
|
|
// ---------- Send without Body ----------
|
|
|
|
func TestSend_WithoutBody(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
if _, hasBody := body["Body"]; hasBody {
|
|
t.Error("Body should be omitted when empty")
|
|
}
|
|
if _, hasBodyEnc := body["BodyEnc"]; hasBodyEnc {
|
|
t.Error("BodyEnc should be omitted when Body is empty")
|
|
}
|
|
writeJSON(t, w, http.StatusOK, map[string]interface{}{})
|
|
})
|
|
|
|
err := client.Send(SendRequest{
|
|
To: []Recipient{{Address: "to@example.com"}},
|
|
Subject: "No Body",
|
|
Passphrase: "pass",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Send: %v", err)
|
|
}
|
|
}
|
|
|
|
// ---------- Timeout ----------
|
|
|
|
func TestListMessages_Timeout(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
|
|
cfg := &config.Config{
|
|
APIBaseURL: srv.URL(),
|
|
TimeoutSec: 1,
|
|
RateLimitReq: 100,
|
|
RateLimitWin: 60,
|
|
}
|
|
apiClient := api.NewProtonMailClient(cfg, nil)
|
|
apiClient.SetAuthHeader("test-token")
|
|
client := NewClient(apiClient)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
time.Sleep(2 * time.Second)
|
|
writeJSON(t, w, http.StatusOK, ListMessagesResponse{})
|
|
})
|
|
|
|
_, err := client.ListMessages(ListMessagesRequest{
|
|
Page: 1,
|
|
PageSize: 10,
|
|
Passphrase: "pass",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected timeout error")
|
|
}
|
|
}
|
|
|
|
// ---------- Multiple Combined Filters ----------
|
|
|
|
func TestListMessages_CombinedFilters(t *testing.T) {
|
|
srv := newMockServer(t)
|
|
defer srv.Close()
|
|
client := newTestClient(t, srv)
|
|
|
|
starred := true
|
|
unread := false
|
|
since := int64(1700000000)
|
|
|
|
srv.Handle("POST /api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
body := readJSON(t, r)
|
|
if body["Type"] != float64(FolderSent) {
|
|
t.Errorf("expected Type=3, got %v", body["Type"])
|
|
}
|
|
if body["Starred"] != true {
|
|
t.Errorf("expected Starred=true, got %v", body["Starred"])
|
|
}
|
|
if body["Read"] != false {
|
|
t.Errorf("expected Read=false, got %v", body["Read"])
|
|
}
|
|
if body["Since"] != float64(since) {
|
|
t.Errorf("expected Since=%d, got %v", since, body["Since"])
|
|
}
|
|
writeJSON(t, w, http.StatusOK, ListMessagesResponse{Total: 0, Messages: []Message{}})
|
|
})
|
|
|
|
_, err := client.ListMessages(ListMessagesRequest{
|
|
Folder: FolderSent,
|
|
Page: 1,
|
|
PageSize: 10,
|
|
Passphrase: "pass",
|
|
Starred: &starred,
|
|
Read: &unread,
|
|
Since: since,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ListMessages: %v", err)
|
|
}
|
|
}
|