diff --git a/README.md b/README.md index 6e00379..6dd371c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![Mirror on GitHub](https://img.shields.io/badge/mirror-GitHub-blue)](https://github.com/jakoubek/sendamatic) [![Go Reference](https://pkg.go.dev/badge/code.beautifulmachines.dev/jakoubek/sendamatic.svg)](https://pkg.go.dev/code.beautifulmachines.dev/jakoubek/sendamatic) -[![Go Report Card](https://goreportcard.com/badge/code.beautifulmachines.dev/jakoubek/sendamatic)](https://goreportcard.com/report/code.beautifulmachines.dev/jakoubek/sendamatic) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) A Go client library for the [Sendamatic](https://www.sendamatic.net) email delivery API. diff --git a/client_test.go b/client_test.go deleted file mode 100644 index d5bd971..0000000 --- a/client_test.go +++ /dev/null @@ -1,406 +0,0 @@ -package sendamatic - -import ( - "context" - "encoding/json" - "errors" - "io" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" -) - -func TestNewClient(t *testing.T) { - client := NewClient("test-user", "test-pass") - - if client == nil { - t.Fatal("NewClient returned nil") - } - - expectedAPIKey := "test-user-test-pass" - if client.apiKey != expectedAPIKey { - t.Errorf("apiKey = %q, want %q", client.apiKey, expectedAPIKey) - } - - if client.baseURL != defaultBaseURL { - t.Errorf("baseURL = %q, want %q", client.baseURL, defaultBaseURL) - } - - if client.httpClient == nil { - t.Fatal("httpClient is nil") - } - - if client.httpClient.Timeout != defaultTimeout { - t.Errorf("httpClient.Timeout = %v, want %v", client.httpClient.Timeout, defaultTimeout) - } -} - -func TestClient_Send_Success(t *testing.T) { - // Create a test server that returns a successful response - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Verify request method - if r.Method != http.MethodPost { - t.Errorf("Method = %s, want POST", r.Method) - } - - // Verify request path - if r.URL.Path != "/send" { - t.Errorf("Path = %s, want /send", r.URL.Path) - } - - // Verify headers - if ct := r.Header.Get("Content-Type"); ct != "application/json" { - t.Errorf("Content-Type = %s, want application/json", ct) - } - - if apiKey := r.Header.Get("x-api-key"); apiKey != "user-pass" { - t.Errorf("x-api-key = %s, want user-pass", apiKey) - } - - // Verify request body - body, _ := io.ReadAll(r.Body) - var msg Message - if err := json.Unmarshal(body, &msg); err != nil { - t.Errorf("Failed to unmarshal request body: %v", err) - } - - // Send successful response - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "application/json") - response := map[string][2]interface{}{ - "recipient@example.com": {float64(200), "msg-12345"}, - } - json.NewEncoder(w).Encode(response) - })) - defer server.Close() - - client := NewClient("user", "pass", WithBaseURL(server.URL)) - - msg := NewMessage(). - SetSender("sender@example.com"). - AddTo("recipient@example.com"). - SetSubject("Test"). - SetTextBody("Body") - - resp, err := client.Send(context.Background(), msg) - if err != nil { - t.Fatalf("Send() error = %v, want nil", err) - } - - if !resp.IsSuccess() { - t.Error("Expected successful response") - } - - if resp.StatusCode != 200 { - t.Errorf("StatusCode = %d, want 200", resp.StatusCode) - } - - msgID, ok := resp.GetMessageID("recipient@example.com") - if !ok { - t.Error("Expected to find message ID") - } - if msgID != "msg-12345" { - t.Errorf("MessageID = %q, want %q", msgID, "msg-12345") - } -} - -func TestClient_Send_ValidationError(t *testing.T) { - client := NewClient("user", "pass") - - // Create an invalid message (no recipients) - msg := NewMessage(). - SetSender("sender@example.com"). - SetSubject("Test"). - SetTextBody("Body") - - _, err := client.Send(context.Background(), msg) - if err == nil { - t.Fatal("Expected validation error, got nil") - } - - if !strings.Contains(err.Error(), "validation failed") { - t.Errorf("Error message = %q, want to contain 'validation failed'", err.Error()) - } -} - -func TestClient_Send_APIError(t *testing.T) { - tests := []struct { - name string - statusCode int - responseBody string - wantErrMessage string - }{ - { - name: "400 bad request", - statusCode: 400, - responseBody: `{"error": "Invalid request"}`, - wantErrMessage: "sendamatic api error (status 400): Invalid request", - }, - { - name: "401 unauthorized", - statusCode: 401, - responseBody: `{"error": "Invalid API key"}`, - wantErrMessage: "sendamatic api error (status 401): Invalid API key", - }, - { - name: "422 validation error", - statusCode: 422, - responseBody: `{"error": "Validation failed", "validation_errors": "sender is required", "json_path": "$.sender"}`, - wantErrMessage: "sendamatic api error (status 422): sender is required (path: $.sender)", - }, - { - name: "500 server error", - statusCode: 500, - responseBody: `{"error": "Internal server error"}`, - wantErrMessage: "sendamatic api error (status 500): Internal server error", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tt.statusCode) - w.Write([]byte(tt.responseBody)) - })) - defer server.Close() - - client := NewClient("user", "pass", WithBaseURL(server.URL)) - - msg := NewMessage(). - SetSender("sender@example.com"). - AddTo("recipient@example.com"). - SetSubject("Test"). - SetTextBody("Body") - - _, err := client.Send(context.Background(), msg) - if err == nil { - t.Fatal("Expected error, got nil") - } - - var apiErr *APIError - if !errors.As(err, &apiErr) { - t.Fatalf("Error type = %T, want *APIError", err) - } - - if apiErr.StatusCode != tt.statusCode { - t.Errorf("StatusCode = %d, want %d", apiErr.StatusCode, tt.statusCode) - } - - if err.Error() != tt.wantErrMessage { - t.Errorf("Error message = %q, want %q", err.Error(), tt.wantErrMessage) - } - }) - } -} - -func TestClient_Send_ContextTimeout(t *testing.T) { - // Create a server that delays the response - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(100 * time.Millisecond) - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"recipient@example.com": [200, "msg-12345"]}`)) - })) - defer server.Close() - - client := NewClient("user", "pass", WithBaseURL(server.URL)) - - msg := NewMessage(). - SetSender("sender@example.com"). - AddTo("recipient@example.com"). - SetSubject("Test"). - SetTextBody("Body") - - // Create a context that times out quickly - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) - defer cancel() - - _, err := client.Send(ctx, msg) - if err == nil { - t.Fatal("Expected timeout error, got nil") - } - - if !errors.Is(err, context.DeadlineExceeded) && !strings.Contains(err.Error(), "context deadline exceeded") { - t.Errorf("Expected context deadline exceeded error, got: %v", err) - } -} - -func TestClient_Send_ContextCancellation(t *testing.T) { - // Create a server that delays the response - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(100 * time.Millisecond) - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"recipient@example.com": [200, "msg-12345"]}`)) - })) - defer server.Close() - - client := NewClient("user", "pass", WithBaseURL(server.URL)) - - msg := NewMessage(). - SetSender("sender@example.com"). - AddTo("recipient@example.com"). - SetSubject("Test"). - SetTextBody("Body") - - ctx, cancel := context.WithCancel(context.Background()) - - // Cancel the context immediately - cancel() - - _, err := client.Send(ctx, msg) - if err == nil { - t.Fatal("Expected cancellation error, got nil") - } - - if !errors.Is(err, context.Canceled) && !strings.Contains(err.Error(), "context canceled") { - t.Errorf("Expected context canceled error, got: %v", err) - } -} - -func TestClient_Send_MultipleRecipients(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "application/json") - response := map[string][2]interface{}{ - "recipient1@example.com": {float64(200), "msg-11111"}, - "recipient2@example.com": {float64(200), "msg-22222"}, - "recipient3@example.com": {float64(550), "msg-33333"}, // Failed delivery - } - json.NewEncoder(w).Encode(response) - })) - defer server.Close() - - client := NewClient("user", "pass", WithBaseURL(server.URL)) - - msg := NewMessage(). - SetSender("sender@example.com"). - AddTo("recipient1@example.com"). - AddTo("recipient2@example.com"). - AddTo("recipient3@example.com"). - SetSubject("Test"). - SetTextBody("Body") - - resp, err := client.Send(context.Background(), msg) - if err != nil { - t.Fatalf("Send() error = %v, want nil", err) - } - - // Check each recipient - for email, expected := range map[string]struct { - status int - msgID string - }{ - "recipient1@example.com": {200, "msg-11111"}, - "recipient2@example.com": {200, "msg-22222"}, - "recipient3@example.com": {550, "msg-33333"}, - } { - status, ok := resp.GetStatus(email) - if !ok { - t.Errorf("Expected to find status for %s", email) - continue - } - if status != expected.status { - t.Errorf("Status for %s = %d, want %d", email, status, expected.status) - } - - msgID, ok := resp.GetMessageID(email) - if !ok { - t.Errorf("Expected to find message ID for %s", email) - continue - } - if msgID != expected.msgID { - t.Errorf("MessageID for %s = %q, want %q", email, msgID, expected.msgID) - } - } -} - -func TestClient_Send_InvalidJSON(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(`not valid json`)) - })) - defer server.Close() - - client := NewClient("user", "pass", WithBaseURL(server.URL)) - - msg := NewMessage(). - SetSender("sender@example.com"). - AddTo("recipient@example.com"). - SetSubject("Test"). - SetTextBody("Body") - - _, err := client.Send(context.Background(), msg) - if err == nil { - t.Fatal("Expected error for invalid JSON, got nil") - } - - if !strings.Contains(err.Error(), "unmarshal") { - t.Errorf("Error should mention unmarshal, got: %v", err) - } -} - -func TestClient_Send_EmptyResponse(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{}`)) - })) - defer server.Close() - - client := NewClient("user", "pass", WithBaseURL(server.URL)) - - msg := NewMessage(). - SetSender("sender@example.com"). - AddTo("recipient@example.com"). - SetSubject("Test"). - SetTextBody("Body") - - resp, err := client.Send(context.Background(), msg) - if err != nil { - t.Fatalf("Send() error = %v, want nil", err) - } - - if resp.StatusCode != 200 { - t.Errorf("StatusCode = %d, want 200", resp.StatusCode) - } - - if len(resp.Recipients) != 0 { - t.Errorf("Recipients length = %d, want 0", len(resp.Recipients)) - } -} - -func TestClient_Send_WithAttachments(t *testing.T) { - var receivedMsg Message - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, _ := io.ReadAll(r.Body) - json.Unmarshal(body, &receivedMsg) - - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"recipient@example.com": [200, "msg-12345"]}`)) - })) - defer server.Close() - - client := NewClient("user", "pass", WithBaseURL(server.URL)) - - msg := NewMessage(). - SetSender("sender@example.com"). - AddTo("recipient@example.com"). - SetSubject("Test"). - SetTextBody("Body"). - AttachFile("test.txt", "text/plain", []byte("test content")) - - _, err := client.Send(context.Background(), msg) - if err != nil { - t.Fatalf("Send() error = %v, want nil", err) - } - - // Verify attachment was sent - if len(receivedMsg.Attachments) != 1 { - t.Fatalf("Attachments length = %d, want 1", len(receivedMsg.Attachments)) - } - - if receivedMsg.Attachments[0].Filename != "test.txt" { - t.Errorf("Filename = %q, want %q", receivedMsg.Attachments[0].Filename, "test.txt") - } -} diff --git a/errors_test.go b/errors_test.go deleted file mode 100644 index 6e9b0ac..0000000 --- a/errors_test.go +++ /dev/null @@ -1,229 +0,0 @@ -package sendamatic - -import ( - "encoding/json" - "strings" - "testing" -) - -func TestAPIError_Error(t *testing.T) { - tests := []struct { - name string - apiErr *APIError - wantText string - }{ - { - name: "simple error", - apiErr: &APIError{ - StatusCode: 400, - Message: "Invalid request", - }, - wantText: "sendamatic api error (status 400): Invalid request", - }, - { - name: "error with validation details", - apiErr: &APIError{ - StatusCode: 422, - Message: "Validation failed", - ValidationErrors: "sender is required", - JSONPath: "$.sender", - }, - wantText: "sendamatic api error (status 422): sender is required (path: $.sender)", - }, - { - name: "error with SMTP code", - apiErr: &APIError{ - StatusCode: 500, - Message: "SMTP error", - SMTPCode: 550, - Sender: "test@example.com", - }, - wantText: "sendamatic api error (status 500): SMTP error", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := tt.apiErr.Error() - if got != tt.wantText { - t.Errorf("Error() = %q, want %q", got, tt.wantText) - } - }) - } -} - -func TestParseErrorResponse_ValidJSON(t *testing.T) { - tests := []struct { - name string - statusCode int - body string - want *APIError - }{ - { - name: "simple error", - statusCode: 400, - body: `{"error": "Invalid API key"}`, - want: &APIError{ - StatusCode: 400, - Message: "Invalid API key", - }, - }, - { - name: "validation error", - statusCode: 422, - body: `{"error": "Validation failed", "validation_errors": "sender is required", "json_path": "$.sender"}`, - want: &APIError{ - StatusCode: 422, - Message: "Validation failed", - ValidationErrors: "sender is required", - JSONPath: "$.sender", - }, - }, - { - name: "smtp error", - statusCode: 500, - body: `{"error": "SMTP error", "smtp_code": 550, "sender": "test@example.com"}`, - want: &APIError{ - StatusCode: 500, - Message: "SMTP error", - SMTPCode: 550, - Sender: "test@example.com", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := parseErrorResponse(tt.statusCode, []byte(tt.body)) - - apiErr, ok := err.(*APIError) - if !ok { - t.Fatalf("parseErrorResponse returned %T, want *APIError", err) - } - - if apiErr.StatusCode != tt.want.StatusCode { - t.Errorf("StatusCode = %d, want %d", apiErr.StatusCode, tt.want.StatusCode) - } - - if apiErr.Message != tt.want.Message { - t.Errorf("Message = %q, want %q", apiErr.Message, tt.want.Message) - } - - if apiErr.ValidationErrors != tt.want.ValidationErrors { - t.Errorf("ValidationErrors = %q, want %q", apiErr.ValidationErrors, tt.want.ValidationErrors) - } - - if apiErr.JSONPath != tt.want.JSONPath { - t.Errorf("JSONPath = %q, want %q", apiErr.JSONPath, tt.want.JSONPath) - } - - if apiErr.SMTPCode != tt.want.SMTPCode { - t.Errorf("SMTPCode = %d, want %d", apiErr.SMTPCode, tt.want.SMTPCode) - } - - if apiErr.Sender != tt.want.Sender { - t.Errorf("Sender = %q, want %q", apiErr.Sender, tt.want.Sender) - } - }) - } -} - -func TestParseErrorResponse_InvalidJSON(t *testing.T) { - statusCode := 500 - body := []byte("Internal Server Error - not JSON") - - err := parseErrorResponse(statusCode, body) - - apiErr, ok := err.(*APIError) - if !ok { - t.Fatalf("parseErrorResponse returned %T, want *APIError", err) - } - - if apiErr.StatusCode != statusCode { - t.Errorf("StatusCode = %d, want %d", apiErr.StatusCode, statusCode) - } - - // When JSON parsing fails, the raw body should be used as the message - if apiErr.Message != string(body) { - t.Errorf("Message = %q, want %q", apiErr.Message, string(body)) - } -} - -func TestParseErrorResponse_EmptyBody(t *testing.T) { - statusCode := 404 - body := []byte("") - - err := parseErrorResponse(statusCode, body) - - apiErr, ok := err.(*APIError) - if !ok { - t.Fatalf("parseErrorResponse returned %T, want *APIError", err) - } - - if apiErr.StatusCode != statusCode { - t.Errorf("StatusCode = %d, want %d", apiErr.StatusCode, statusCode) - } -} - -func TestParseErrorResponse_MalformedJSON(t *testing.T) { - statusCode := 400 - body := []byte(`{"error": "Missing closing brace"`) - - err := parseErrorResponse(statusCode, body) - - apiErr, ok := err.(*APIError) - if !ok { - t.Fatalf("parseErrorResponse returned %T, want *APIError", err) - } - - if apiErr.StatusCode != statusCode { - t.Errorf("StatusCode = %d, want %d", apiErr.StatusCode, statusCode) - } - - // Should fall back to raw body as message - if apiErr.Message != string(body) { - t.Errorf("Message = %q, want %q", apiErr.Message, string(body)) - } -} - -func TestAPIError_JSONRoundtrip(t *testing.T) { - original := &APIError{ - StatusCode: 422, - Message: "Validation error", - ValidationErrors: "sender format invalid", - JSONPath: "$.sender", - Sender: "invalid@", - SMTPCode: 0, - } - - // Marshal to JSON - data, err := json.Marshal(original) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - - // Unmarshal back - var decoded APIError - err = json.Unmarshal(data, &decoded) - if err != nil { - t.Fatalf("Unmarshal failed: %v", err) - } - - // StatusCode should not be in JSON (json:"-" tag) - if strings.Contains(string(data), "StatusCode") { - t.Error("StatusCode should not be marshaled to JSON") - } - - // Compare fields (except StatusCode which has json:"-") - if decoded.Message != original.Message { - t.Errorf("Message = %q, want %q", decoded.Message, original.Message) - } - - if decoded.ValidationErrors != original.ValidationErrors { - t.Errorf("ValidationErrors = %q, want %q", decoded.ValidationErrors, original.ValidationErrors) - } - - if decoded.JSONPath != original.JSONPath { - t.Errorf("JSONPath = %q, want %q", decoded.JSONPath, original.JSONPath) - } -} diff --git a/go.mod b/go.mod index b61f016..bf8c1e1 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module code.beautifulmachines.dev/jakoubek/sendamatic -go 1.22 +go 1.25.4 diff --git a/message_test.go b/message_test.go deleted file mode 100644 index 5a432b8..0000000 --- a/message_test.go +++ /dev/null @@ -1,285 +0,0 @@ -package sendamatic - -import ( - "encoding/base64" - "os" - "path/filepath" - "testing" -) - -func TestNewMessage(t *testing.T) { - msg := NewMessage() - - if msg == nil { - t.Fatal("NewMessage returned nil") - } - - if msg.To == nil || msg.CC == nil || msg.BCC == nil { - t.Error("Slices not initialized") - } - - if msg.Headers == nil || msg.Attachments == nil { - t.Error("Headers or Attachments not initialized") - } -} - -func TestMessageBuilderMethods(t *testing.T) { - msg := NewMessage(). - SetSender("sender@example.com"). - AddTo("to@example.com"). - AddCC("cc@example.com"). - AddBCC("bcc@example.com"). - SetSubject("Test Subject"). - SetTextBody("Test Body"). - SetHTMLBody("

Test Body

"). - AddHeader("X-Custom", "value") - - if msg.Sender != "sender@example.com" { - t.Errorf("Sender = %q, want %q", msg.Sender, "sender@example.com") - } - - if len(msg.To) != 1 || msg.To[0] != "to@example.com" { - t.Errorf("To = %v, want [to@example.com]", msg.To) - } - - if len(msg.CC) != 1 || msg.CC[0] != "cc@example.com" { - t.Errorf("CC = %v, want [cc@example.com]", msg.CC) - } - - if len(msg.BCC) != 1 || msg.BCC[0] != "bcc@example.com" { - t.Errorf("BCC = %v, want [bcc@example.com]", msg.BCC) - } - - if msg.Subject != "Test Subject" { - t.Errorf("Subject = %q, want %q", msg.Subject, "Test Subject") - } - - if msg.TextBody != "Test Body" { - t.Errorf("TextBody = %q, want %q", msg.TextBody, "Test Body") - } - - if msg.HTMLBody != "

Test Body

" { - t.Errorf("HTMLBody = %q, want %q", msg.HTMLBody, "

Test Body

") - } - - if len(msg.Headers) != 1 { - t.Fatalf("Headers length = %d, want 1", len(msg.Headers)) - } - - if msg.Headers[0].Header != "X-Custom" || msg.Headers[0].Value != "value" { - t.Errorf("Header = %+v, want {X-Custom value}", msg.Headers[0]) - } -} - -func TestAddMultipleRecipients(t *testing.T) { - msg := NewMessage(). - AddTo("to1@example.com"). - AddTo("to2@example.com"). - AddTo("to3@example.com") - - if len(msg.To) != 3 { - t.Errorf("To length = %d, want 3", len(msg.To)) - } - - expected := []string{"to1@example.com", "to2@example.com", "to3@example.com"} - for i, email := range expected { - if msg.To[i] != email { - t.Errorf("To[%d] = %q, want %q", i, msg.To[i], email) - } - } -} - -func TestAttachFile(t *testing.T) { - msg := NewMessage() - data := []byte("test file content") - - msg.AttachFile("test.txt", "text/plain", data) - - if len(msg.Attachments) != 1 { - t.Fatalf("Attachments length = %d, want 1", len(msg.Attachments)) - } - - att := msg.Attachments[0] - if att.Filename != "test.txt" { - t.Errorf("Filename = %q, want %q", att.Filename, "test.txt") - } - - if att.MimeType != "text/plain" { - t.Errorf("MimeType = %q, want %q", att.MimeType, "text/plain") - } - - // Verify base64 encoding - decoded, err := base64.StdEncoding.DecodeString(att.Data) - if err != nil { - t.Fatalf("Failed to decode base64: %v", err) - } - - if string(decoded) != string(data) { - t.Errorf("Decoded data = %q, want %q", decoded, data) - } -} - -func TestAttachFileFromPath(t *testing.T) { - msg := NewMessage() - - testFile := filepath.Join("testdata", "test.txt") - err := msg.AttachFileFromPath(testFile, "text/plain") - if err != nil { - t.Fatalf("AttachFileFromPath failed: %v", err) - } - - if len(msg.Attachments) != 1 { - t.Fatalf("Attachments length = %d, want 1", len(msg.Attachments)) - } - - att := msg.Attachments[0] - if att.Filename != "test.txt" { - t.Errorf("Filename = %q, want %q", att.Filename, "test.txt") - } - - // Verify content - decoded, err := base64.StdEncoding.DecodeString(att.Data) - if err != nil { - t.Fatalf("Failed to decode base64: %v", err) - } - - expected, _ := os.ReadFile(testFile) - if string(decoded) != string(expected) { - t.Errorf("File content mismatch") - } -} - -func TestAttachFileFromPath_NonExistent(t *testing.T) { - msg := NewMessage() - - err := msg.AttachFileFromPath("nonexistent.txt", "text/plain") - if err == nil { - t.Error("Expected error for non-existent file, got nil") - } -} - -func TestAttachMultipleFiles(t *testing.T) { - msg := NewMessage(). - AttachFile("file1.txt", "text/plain", []byte("content1")). - AttachFile("file2.pdf", "application/pdf", []byte("content2")) - - if len(msg.Attachments) != 2 { - t.Errorf("Attachments length = %d, want 2", len(msg.Attachments)) - } -} - -func TestValidate_Success(t *testing.T) { - tests := []struct { - name string - msg *Message - }{ - { - name: "valid with text body", - msg: NewMessage(). - SetSender("sender@example.com"). - AddTo("to@example.com"). - SetSubject("Subject"). - SetTextBody("Body"), - }, - { - name: "valid with html body", - msg: NewMessage(). - SetSender("sender@example.com"). - AddTo("to@example.com"). - SetSubject("Subject"). - SetHTMLBody("

Body

"), - }, - { - name: "valid with both bodies", - msg: NewMessage(). - SetSender("sender@example.com"). - AddTo("to@example.com"). - SetSubject("Subject"). - SetTextBody("Body"). - SetHTMLBody("

Body

"), - }, - { - name: "valid with multiple recipients", - msg: NewMessage(). - SetSender("sender@example.com"). - AddTo("to1@example.com"). - AddTo("to2@example.com"). - AddCC("cc@example.com"). - AddBCC("bcc@example.com"). - SetSubject("Subject"). - SetTextBody("Body"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.msg.Validate() - if err != nil { - t.Errorf("Validate() error = %v, want nil", err) - } - }) - } -} - -func TestValidate_Errors(t *testing.T) { - tests := []struct { - name string - msg *Message - wantErrText string - }{ - { - name: "no recipients", - msg: NewMessage().SetSender("sender@example.com").SetSubject("Subject").SetTextBody("Body"), - wantErrText: "at least one recipient required", - }, - { - name: "no sender", - msg: NewMessage().AddTo("to@example.com").SetSubject("Subject").SetTextBody("Body"), - wantErrText: "sender is required", - }, - { - name: "no subject", - msg: NewMessage().SetSender("sender@example.com").AddTo("to@example.com").SetTextBody("Body"), - wantErrText: "subject is required", - }, - { - name: "no body", - msg: NewMessage().SetSender("sender@example.com").AddTo("to@example.com").SetSubject("Subject"), - wantErrText: "either text_body or html_body is required", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.msg.Validate() - if err == nil { - t.Fatal("Validate() error = nil, want error") - } - if err.Error() != tt.wantErrText { - t.Errorf("Validate() error = %q, want %q", err.Error(), tt.wantErrText) - } - }) - } -} - -func TestValidate_TooManyRecipients(t *testing.T) { - msg := NewMessage(). - SetSender("sender@example.com"). - SetSubject("Subject"). - SetTextBody("Body") - - // Add 256 recipients (more than the limit of 255) - for i := 0; i < 256; i++ { - msg.AddTo("recipient@example.com") - } - - err := msg.Validate() - if err == nil { - t.Fatal("Validate() error = nil, want error for too many recipients") - } - - expected := "maximum 255 recipients allowed" - if err.Error() != expected { - t.Errorf("Validate() error = %q, want %q", err.Error(), expected) - } -} diff --git a/options_test.go b/options_test.go deleted file mode 100644 index 1f235c3..0000000 --- a/options_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package sendamatic - -import ( - "net/http" - "testing" - "time" -) - -func TestWithBaseURL(t *testing.T) { - customURL := "https://custom.api.url" - client := NewClient("user", "pass", WithBaseURL(customURL)) - - if client.baseURL != customURL { - t.Errorf("baseURL = %q, want %q", client.baseURL, customURL) - } -} - -func TestWithTimeout(t *testing.T) { - customTimeout := 60 * time.Second - client := NewClient("user", "pass", WithTimeout(customTimeout)) - - if client.httpClient.Timeout != customTimeout { - t.Errorf("httpClient.Timeout = %v, want %v", client.httpClient.Timeout, customTimeout) - } -} - -func TestWithHTTPClient(t *testing.T) { - customClient := &http.Client{ - Timeout: 90 * time.Second, - } - - client := NewClient("user", "pass", WithHTTPClient(customClient)) - - if client.httpClient != customClient { - t.Error("httpClient not set to custom client") - } - - if client.httpClient.Timeout != 90*time.Second { - t.Errorf("httpClient.Timeout = %v, want 90s", client.httpClient.Timeout) - } -} - -func TestMultipleOptions(t *testing.T) { - customURL := "https://test.api.url" - customTimeout := 45 * time.Second - - client := NewClient("user", "pass", - WithBaseURL(customURL), - WithTimeout(customTimeout), - ) - - if client.baseURL != customURL { - t.Errorf("baseURL = %q, want %q", client.baseURL, customURL) - } - - if client.httpClient.Timeout != customTimeout { - t.Errorf("httpClient.Timeout = %v, want %v", client.httpClient.Timeout, customTimeout) - } -} - -func TestDefaultValues(t *testing.T) { - client := NewClient("user", "pass") - - if client.baseURL != defaultBaseURL { - t.Errorf("baseURL = %q, want %q", client.baseURL, defaultBaseURL) - } - - if client.httpClient.Timeout != defaultTimeout { - t.Errorf("httpClient.Timeout = %v, want %v", client.httpClient.Timeout, defaultTimeout) - } - - expectedAPIKey := "user-pass" - if client.apiKey != expectedAPIKey { - t.Errorf("apiKey = %q, want %q", client.apiKey, expectedAPIKey) - } -} - -func TestWithHTTPClient_PreservesCustomTransport(t *testing.T) { - customTransport := &http.Transport{ - MaxIdleConns: 100, - } - - customClient := &http.Client{ - Timeout: 45 * time.Second, - Transport: customTransport, - } - - client := NewClient("user", "pass", WithHTTPClient(customClient)) - - if client.httpClient.Transport != customTransport { - t.Error("Custom transport was not preserved") - } -} - -func TestOptionsOrder(t *testing.T) { - // Test that options are applied in order - // First set timeout to 30s, then provide a custom client with 60s - customClient := &http.Client{ - Timeout: 60 * time.Second, - } - - client := NewClient("user", "pass", - WithTimeout(30*time.Second), - WithHTTPClient(customClient), - ) - - // The custom client should override the previous timeout setting - if client.httpClient.Timeout != 60*time.Second { - t.Errorf("httpClient.Timeout = %v, want 60s (custom client should override)", client.httpClient.Timeout) - } -} - -func TestWithTimeout_OverridesDefault(t *testing.T) { - tests := []struct { - name string - timeout time.Duration - }{ - {"short timeout", 5 * time.Second}, - {"long timeout", 120 * time.Second}, - {"zero timeout", 0}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := NewClient("user", "pass", WithTimeout(tt.timeout)) - - if client.httpClient.Timeout != tt.timeout { - t.Errorf("httpClient.Timeout = %v, want %v", client.httpClient.Timeout, tt.timeout) - } - }) - } -} - -func TestWithBaseURL_VariousURLs(t *testing.T) { - tests := []struct { - name string - url string - }{ - {"production", "https://send.api.sendamatic.net"}, - {"staging", "https://staging.api.sendamatic.net"}, - {"local", "http://localhost:8080"}, - {"custom port", "https://api.example.com:8443"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := NewClient("user", "pass", WithBaseURL(tt.url)) - - if client.baseURL != tt.url { - t.Errorf("baseURL = %q, want %q", client.baseURL, tt.url) - } - }) - } -} diff --git a/response_test.go b/response_test.go deleted file mode 100644 index a9efac3..0000000 --- a/response_test.go +++ /dev/null @@ -1,276 +0,0 @@ -package sendamatic - -import ( - "encoding/json" - "testing" -) - -func TestSendResponse_IsSuccess(t *testing.T) { - tests := []struct { - name string - statusCode int - want bool - }{ - {"success 200", 200, true}, - {"bad request 400", 400, false}, - {"unauthorized 401", 401, false}, - {"server error 500", 500, false}, - {"created 201", 201, false}, // Only 200 is considered success - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - resp := &SendResponse{StatusCode: tt.statusCode} - got := resp.IsSuccess() - if got != tt.want { - t.Errorf("IsSuccess() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestSendResponse_GetMessageID(t *testing.T) { - tests := []struct { - name string - recipients map[string][2]interface{} - email string - wantID string - wantOK bool - }{ - { - name: "existing recipient", - recipients: map[string][2]interface{}{ - "test@example.com": {float64(200), "msg-12345"}, - }, - email: "test@example.com", - wantID: "msg-12345", - wantOK: true, - }, - { - name: "non-existent recipient", - recipients: map[string][2]interface{}{ - "test@example.com": {float64(200), "msg-12345"}, - }, - email: "other@example.com", - wantID: "", - wantOK: false, - }, - { - name: "multiple recipients", - recipients: map[string][2]interface{}{ - "test1@example.com": {float64(200), "msg-11111"}, - "test2@example.com": {float64(200), "msg-22222"}, - "test3@example.com": {float64(400), "msg-33333"}, - }, - email: "test2@example.com", - wantID: "msg-22222", - wantOK: true, - }, - { - name: "empty recipients", - recipients: map[string][2]interface{}{}, - email: "test@example.com", - wantID: "", - wantOK: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - resp := &SendResponse{ - StatusCode: 200, - Recipients: tt.recipients, - } - - gotID, gotOK := resp.GetMessageID(tt.email) - if gotID != tt.wantID { - t.Errorf("GetMessageID() id = %q, want %q", gotID, tt.wantID) - } - if gotOK != tt.wantOK { - t.Errorf("GetMessageID() ok = %v, want %v", gotOK, tt.wantOK) - } - }) - } -} - -func TestSendResponse_GetStatus(t *testing.T) { - tests := []struct { - name string - recipients map[string][2]interface{} - email string - wantStatus int - wantOK bool - }{ - { - name: "existing recipient with success", - recipients: map[string][2]interface{}{ - "test@example.com": {float64(200), "msg-12345"}, - }, - email: "test@example.com", - wantStatus: 200, - wantOK: true, - }, - { - name: "existing recipient with error", - recipients: map[string][2]interface{}{ - "test@example.com": {float64(400), "msg-12345"}, - }, - email: "test@example.com", - wantStatus: 400, - wantOK: true, - }, - { - name: "non-existent recipient", - recipients: map[string][2]interface{}{ - "test@example.com": {float64(200), "msg-12345"}, - }, - email: "other@example.com", - wantStatus: 0, - wantOK: false, - }, - { - name: "multiple recipients", - recipients: map[string][2]interface{}{ - "test1@example.com": {float64(200), "msg-11111"}, - "test2@example.com": {float64(550), "msg-22222"}, - "test3@example.com": {float64(200), "msg-33333"}, - }, - email: "test2@example.com", - wantStatus: 550, - wantOK: true, - }, - { - name: "empty recipients", - recipients: map[string][2]interface{}{}, - email: "test@example.com", - wantStatus: 0, - wantOK: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - resp := &SendResponse{ - StatusCode: 200, - Recipients: tt.recipients, - } - - gotStatus, gotOK := resp.GetStatus(tt.email) - if gotStatus != tt.wantStatus { - t.Errorf("GetStatus() status = %d, want %d", gotStatus, tt.wantStatus) - } - if gotOK != tt.wantOK { - t.Errorf("GetStatus() ok = %v, want %v", gotOK, tt.wantOK) - } - }) - } -} - -func TestSendResponse_JSONUnmarshal(t *testing.T) { - // Test that we can properly unmarshal the API response format - jsonResp := `{ - "test1@example.com": [200, "msg-11111"], - "test2@example.com": [400, "msg-22222"] - }` - - var recipients map[string][2]interface{} - err := json.Unmarshal([]byte(jsonResp), &recipients) - if err != nil { - t.Fatalf("Unmarshal failed: %v", err) - } - - resp := &SendResponse{ - StatusCode: 200, - Recipients: recipients, - } - - // Test first recipient - status1, ok1 := resp.GetStatus("test1@example.com") - if !ok1 { - t.Error("Expected to find test1@example.com") - } - if status1 != 200 { - t.Errorf("Status for test1 = %d, want 200", status1) - } - - msgID1, ok1 := resp.GetMessageID("test1@example.com") - if !ok1 { - t.Error("Expected to find message ID for test1@example.com") - } - if msgID1 != "msg-11111" { - t.Errorf("MessageID for test1 = %q, want %q", msgID1, "msg-11111") - } - - // Test second recipient - status2, ok2 := resp.GetStatus("test2@example.com") - if !ok2 { - t.Error("Expected to find test2@example.com") - } - if status2 != 400 { - t.Errorf("Status for test2 = %d, want 400", status2) - } - - msgID2, ok2 := resp.GetMessageID("test2@example.com") - if !ok2 { - t.Error("Expected to find message ID for test2@example.com") - } - if msgID2 != "msg-22222" { - t.Errorf("MessageID for test2 = %q, want %q", msgID2, "msg-22222") - } -} - -func TestSendResponse_GetStatus_Float64Conversion(t *testing.T) { - // Explicitly test the float64 to int conversion - // This mimics how JSON unmarshaling works with numbers - resp := &SendResponse{ - StatusCode: 200, - Recipients: map[string][2]interface{}{ - "test@example.com": {float64(200.0), "msg-12345"}, - }, - } - - status, ok := resp.GetStatus("test@example.com") - if !ok { - t.Fatal("Expected to find recipient") - } - - if status != 200 { - t.Errorf("GetStatus() = %d, want 200", status) - } -} - -func TestSendResponse_GetMessageID_InvalidType(t *testing.T) { - // Test behavior when message ID is not a string - resp := &SendResponse{ - StatusCode: 200, - Recipients: map[string][2]interface{}{ - "test@example.com": {float64(200), 12345}, // number instead of string - }, - } - - msgID, ok := resp.GetMessageID("test@example.com") - if ok { - t.Error("Expected ok = false when message ID is not a string") - } - if msgID != "" { - t.Errorf("Expected empty string, got %q", msgID) - } -} - -func TestSendResponse_GetStatus_InvalidType(t *testing.T) { - // Test behavior when status is not a number - resp := &SendResponse{ - StatusCode: 200, - Recipients: map[string][2]interface{}{ - "test@example.com": {"OK", "msg-12345"}, // string instead of number - }, - } - - status, ok := resp.GetStatus("test@example.com") - if ok { - t.Error("Expected ok = false when status is not a number") - } - if status != 0 { - t.Errorf("Expected 0, got %d", status) - } -} diff --git a/testdata/test.pdf b/testdata/test.pdf deleted file mode 100644 index 49efd7f..0000000 --- a/testdata/test.pdf +++ /dev/null @@ -1,58 +0,0 @@ -%PDF-1.4 -1 0 obj -<< -/Type /Catalog -/Pages 2 0 R ->> -endobj -2 0 obj -<< -/Type /Pages -/Kids [3 0 R] -/Count 1 ->> -endobj -3 0 obj -<< -/Type /Page -/Parent 2 0 R -/Resources << -/Font << -/F1 << -/Type /Font -/Subtype /Type1 -/BaseFont /Helvetica ->> ->> ->> -/MediaBox [0 0 612 792] -/Contents 4 0 R ->> -endobj -4 0 obj -<< -/Length 44 ->> -stream -BT -/F1 12 Tf -100 700 Td -(Test PDF) Tj -ET -endstream -endobj -xref -0 5 -0000000000 65535 f -0000000009 00000 n -0000000058 00000 n -0000000115 00000 n -0000000317 00000 n -trailer -<< -/Size 5 -/Root 1 0 R ->> -startxref -408 -%%EOF \ No newline at end of file diff --git a/testdata/test.png b/testdata/test.png deleted file mode 100644 index 91a99b9..0000000 Binary files a/testdata/test.png and /dev/null differ diff --git a/testdata/test.txt b/testdata/test.txt deleted file mode 100644 index 100ff51..0000000 --- a/testdata/test.txt +++ /dev/null @@ -1 +0,0 @@ -This is a test text file for attachment testing.