Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e4a528d88 | |||
| b61c90def6 | |||
| e8ed81a32c |
10 changed files with 1411 additions and 1 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
[](https://github.com/jakoubek/sendamatic)
|
[](https://github.com/jakoubek/sendamatic)
|
||||||
[](https://pkg.go.dev/code.beautifulmachines.dev/jakoubek/sendamatic)
|
[](https://pkg.go.dev/code.beautifulmachines.dev/jakoubek/sendamatic)
|
||||||
|
[](https://goreportcard.com/report/code.beautifulmachines.dev/jakoubek/sendamatic)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
A Go client library for the [Sendamatic](https://www.sendamatic.net) email delivery API.
|
A Go client library for the [Sendamatic](https://www.sendamatic.net) email delivery API.
|
||||||
|
|
|
||||||
406
client_test.go
Normal file
406
client_test.go
Normal file
|
|
@ -0,0 +1,406 @@
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
229
errors_test.go
Normal file
229
errors_test.go
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
2
go.mod
2
go.mod
|
|
@ -1,3 +1,3 @@
|
||||||
module code.beautifulmachines.dev/jakoubek/sendamatic
|
module code.beautifulmachines.dev/jakoubek/sendamatic
|
||||||
|
|
||||||
go 1.25.4
|
go 1.22
|
||||||
|
|
|
||||||
285
message_test.go
Normal file
285
message_test.go
Normal file
|
|
@ -0,0 +1,285 @@
|
||||||
|
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("<p>Test Body</p>").
|
||||||
|
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 != "<p>Test Body</p>" {
|
||||||
|
t.Errorf("HTMLBody = %q, want %q", msg.HTMLBody, "<p>Test Body</p>")
|
||||||
|
}
|
||||||
|
|
||||||
|
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("<p>Body</p>"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid with both bodies",
|
||||||
|
msg: NewMessage().
|
||||||
|
SetSender("sender@example.com").
|
||||||
|
AddTo("to@example.com").
|
||||||
|
SetSubject("Subject").
|
||||||
|
SetTextBody("Body").
|
||||||
|
SetHTMLBody("<p>Body</p>"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
154
options_test.go
Normal file
154
options_test.go
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
276
response_test.go
Normal file
276
response_test.go
Normal file
|
|
@ -0,0 +1,276 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
58
testdata/test.pdf
vendored
Normal file
58
testdata/test.pdf
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
%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
|
||||||
BIN
testdata/test.png
vendored
Normal file
BIN
testdata/test.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 B |
1
testdata/test.txt
vendored
Normal file
1
testdata/test.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
This is a test text file for attachment testing.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue