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://pkg.go.dev/code.beautifulmachines.dev/jakoubek/sendamatic)
|
||||
[](https://goreportcard.com/report/code.beautifulmachines.dev/jakoubek/sendamatic)
|
||||
[](LICENSE)
|
||||
|
||||
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
|
||||
|
||||
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