Compare commits
No commits in common. "main" and "v1.0.0" have entirely different histories.
10 changed files with 1 additions and 1411 deletions
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
[](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
406
client_test.go
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
229
errors_test.go
229
errors_test.go
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -1,3 +1,3 @@
|
||||||
module code.beautifulmachines.dev/jakoubek/sendamatic
|
module code.beautifulmachines.dev/jakoubek/sendamatic
|
||||||
|
|
||||||
go 1.22
|
go 1.25.4
|
||||||
|
|
|
||||||
285
message_test.go
285
message_test.go
|
|
@ -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("<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
154
options_test.go
|
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
276
response_test.go
276
response_test.go
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
58
testdata/test.pdf
vendored
58
testdata/test.pdf
vendored
|
|
@ -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
|
|
||||||
BIN
testdata/test.png
vendored
BIN
testdata/test.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 67 B |
1
testdata/test.txt
vendored
1
testdata/test.txt
vendored
|
|
@ -1 +0,0 @@
|
||||||
This is a test text file for attachment testing.
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue