diff --git a/messenger/providers/twilio/twilio.go b/messenger/providers/twilio/twilio.go index 886f30fb..4e95768d 100644 --- a/messenger/providers/twilio/twilio.go +++ b/messenger/providers/twilio/twilio.go @@ -162,7 +162,7 @@ func (p *Provider) sendSMS(ctx context.Context, message *types.Message) error { } for _, to := range message.To { - err := p.sendSMSToRecipient(to, message) + err := p.sendSMSToRecipient(ctx, to, message) if err != nil { return fmt.Errorf("failed to send SMS to %s: %w", to, err) } @@ -171,7 +171,7 @@ func (p *Provider) sendSMS(ctx context.Context, message *types.Message) error { } // sendSMSToRecipient sends SMS to a single recipient -func (p *Provider) sendSMSToRecipient(to string, message *types.Message) error { +func (p *Provider) sendSMSToRecipient(ctx context.Context, to string, message *types.Message) error { apiURL := fmt.Sprintf("%s/Accounts/%s/Messages.json", p.baseURL, p.accountSID) // Prepare form data @@ -185,7 +185,7 @@ func (p *Provider) sendSMSToRecipient(to string, message *types.Message) error { data.Set("From", p.fromPhone) } - return p.sendTwilioRequest(apiURL, data) + return p.sendTwilioRequest(ctx, apiURL, data) } // sendWhatsApp sends a WhatsApp message via Twilio @@ -195,7 +195,7 @@ func (p *Provider) sendWhatsApp(ctx context.Context, message *types.Message) err } for _, to := range message.To { - err := p.sendWhatsAppToRecipient(to, message) + err := p.sendWhatsAppToRecipient(ctx, to, message) if err != nil { return fmt.Errorf("failed to send WhatsApp message to %s: %w", to, err) } @@ -204,7 +204,7 @@ func (p *Provider) sendWhatsApp(ctx context.Context, message *types.Message) err } // sendWhatsAppToRecipient sends WhatsApp message to a single recipient -func (p *Provider) sendWhatsAppToRecipient(to string, message *types.Message) error { +func (p *Provider) sendWhatsAppToRecipient(ctx context.Context, to string, message *types.Message) error { apiURL := fmt.Sprintf("%s/Accounts/%s/Messages.json", p.baseURL, p.accountSID) // Ensure phone numbers have WhatsApp prefix @@ -224,7 +224,7 @@ func (p *Provider) sendWhatsAppToRecipient(to string, message *types.Message) er data.Set("To", toWhatsApp) data.Set("Body", message.Body) - return p.sendTwilioRequest(apiURL, data) + return p.sendTwilioRequest(ctx, apiURL, data) } // sendEmail sends an email via Twilio SendGrid API @@ -288,7 +288,7 @@ func (p *Provider) sendEmail(ctx context.Context, message *types.Message) error // Send via SendGrid API apiURL := "https://api.sendgrid.com/v3/mail/send" - req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData)) + req, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -311,9 +311,9 @@ func (p *Provider) sendEmail(ctx context.Context, message *types.Message) error } // sendTwilioRequest sends a request to Twilio API -func (p *Provider) sendTwilioRequest(apiURL string, data url.Values) error { +func (p *Provider) sendTwilioRequest(ctx context.Context, apiURL string, data url.Values) error { // Add custom metadata as status callback parameters - req, err := http.NewRequest("POST", apiURL, strings.NewReader(data.Encode())) + req, err := http.NewRequestWithContext(ctx, "POST", apiURL, strings.NewReader(data.Encode())) if err != nil { return fmt.Errorf("failed to create request: %w", err) } diff --git a/messenger/providers/twilio/twilio_sms_test.go b/messenger/providers/twilio/twilio_sms_test.go new file mode 100644 index 00000000..a6c80269 --- /dev/null +++ b/messenger/providers/twilio/twilio_sms_test.go @@ -0,0 +1,316 @@ +package twilio + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/yaoapp/yao/messenger/types" +) + +// Test phone numbers for SMS (placeholder for future implementation) +const ( + TestSMSPhoneAgent = "+1234567890" // Placeholder - replace with authorized test numbers + TestSMSPhoneX = "+1234567891" // Placeholder - replace with authorized test numbers + TestSMSPhoneXiang = "+1234567892" // Placeholder - replace with authorized test numbers +) + +// createTestSMSMessage creates a test SMS message +func createTestSMSMessage() *types.Message { + return &types.Message{ + Type: types.MessageTypeSMS, + To: []string{TestSMSPhoneAgent}, + Body: "Test SMS from Twilio Provider - This is a test message.", + } +} + +// loadSMSTestConfig loads configuration optimized for SMS testing +func loadSMSTestConfig(t *testing.T) types.ProviderConfig { + config := loadTestConfig(t) // Reuse base config loading + + // Ensure SMS-specific options are available + // In real implementation, verify TWILIO_FROM_PHONE is configured + + return config +} + +// ============================================================================= +// SMS Provider Configuration Tests +// ============================================================================= + +func TestSMS_ProviderConfig_WithFromPhone(t *testing.T) { + config := types.ProviderConfig{ + Name: "sms_test", + Connector: "twilio", + Options: map[string]interface{}{ + "account_sid": "test_account_sid", + "auth_token": "test_auth_token", + "from_phone": "+15551234567", // SMS requires from_phone + }, + } + + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + assert.NotNil(t, provider) + assert.Equal(t, "+15551234567", provider.fromPhone) +} + +func TestSMS_ProviderConfig_WithMessagingService(t *testing.T) { + config := types.ProviderConfig{ + Name: "sms_test", + Connector: "twilio", + Options: map[string]interface{}{ + "account_sid": "test_account_sid", + "auth_token": "test_auth_token", + "messaging_service_sid": "MGXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", // Alternative to from_phone + }, + } + + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + assert.NotNil(t, provider) + assert.Equal(t, "MGXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", provider.messagingServiceSID) +} + +func TestSMS_ProviderConfig_MissingPhoneAndService(t *testing.T) { + config := types.ProviderConfig{ + Name: "sms_test", + Connector: "twilio", + Options: map[string]interface{}{ + "account_sid": "test_account_sid", + "auth_token": "test_auth_token", + // Missing both from_phone and messaging_service_sid + }, + } + + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + ctx := context.Background() + smsMessage := createTestSMSMessage() + + err = provider.Send(ctx, smsMessage) + assert.Error(t, err) + assert.Contains(t, err.Error(), "either from_phone or messaging_service_sid is required for SMS") +} + +// ============================================================================= +// SMS Sending Tests (Future Implementation) +// ============================================================================= + +// TODO: Implement real SMS sending tests +func TestSend_SMSMessage_RealAPI(t *testing.T) { + t.Skip("SMS real API tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // config := loadSMSTestConfig(t) + // provider, err := NewTwilioProvider(config) + // require.NoError(t, err) + // + // // Skip if from_phone is not configured + // if provider.fromPhone == "" { + // t.Skip("TWILIO_FROM_PHONE not configured, skipping real SMS API test") + // } + // + // ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + // defer cancel() + // + // smsMessage := createTestSMSMessage() + // err = provider.Send(ctx, smsMessage) + // if err == nil { + // t.Log("Real Twilio SMS API call succeeded") + // } else { + // t.Logf("Real Twilio SMS API call failed: %v", err) + // // Handle expected failures in test environments + // } +} + +func TestSend_SMSMessage_WithMessagingService_RealAPI(t *testing.T) { + t.Skip("SMS Messaging Service real API tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - SMS sending using Messaging Service SID instead of from_phone + // - Service-based features like automatic failover, delivery optimization + // - Compliance and opt-out handling + // - Alpha sender ID support + // - Short code support +} + +func TestSend_SMSMessage_ContextTimeout_RealAPI(t *testing.T) { + t.Skip("SMS context timeout tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // config := loadSMSTestConfig(t) + // provider, err := NewTwilioProvider(config) + // require.NoError(t, err) + // + // // Create a very short timeout context + // ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) + // defer cancel() + // + // smsMessage := createTestSMSMessage() + // err = provider.Send(ctx, smsMessage) + // if err != nil { + // t.Log("Context timeout working correctly with real SMS API") + // } +} + +func TestSendBatch_SMS_RealAPI(t *testing.T) { + t.Skip("SMS batch real API tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // config := loadSMSTestConfig(t) + // provider, err := NewTwilioProvider(config) + // require.NoError(t, err) + // + // ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + // defer cancel() + // + // messages := []*types.Message{ + // { + // Type: types.MessageTypeSMS, + // To: []string{TestSMSPhoneAgent}, + // Body: "Batch SMS Test 1", + // }, + // { + // Type: types.MessageTypeSMS, + // To: []string{TestSMSPhoneX}, + // Body: "Batch SMS Test 2", + // }, + // } + // + // err = provider.SendBatch(ctx, messages) + // if err == nil { + // t.Log("Real Twilio SMS batch API call succeeded") + // } +} + +func TestSend_SMS_MultipleRecipients_RealAPI(t *testing.T) { + t.Skip("SMS multiple recipients tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // config := loadSMSTestConfig(t) + // provider, err := NewTwilioProvider(config) + // require.NoError(t, err) + // + // ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) + // defer cancel() + // + // smsMessage := &types.Message{ + // Type: types.MessageTypeSMS, + // To: []string{TestSMSPhoneAgent, TestSMSPhoneX, TestSMSPhoneXiang}, + // Body: "Multi-recipient SMS test from Twilio Provider", + // } + // + // err = provider.Send(ctx, smsMessage) + // if err == nil { + // t.Log("Twilio SMS multiple recipients API call succeeded") + // } +} + +// ============================================================================= +// SMS Advanced Features Tests (Future Implementation) +// ============================================================================= + +func TestSend_SMS_WithCustomMetadata(t *testing.T) { + t.Skip("SMS metadata tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Custom metadata in status callbacks + // - Tracking and analytics integration + // - Custom parameters for delivery reporting +} + +func TestSend_SMS_WithDeliveryStatus(t *testing.T) { + t.Skip("SMS delivery status tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Status callback configuration + // - Delivery receipt handling + // - Failed message retry logic +} + +func TestSend_SMS_PhoneNumberValidation(t *testing.T) { + t.Skip("SMS phone validation tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - E.164 format validation + // - International number support + // - Invalid number error handling + // - Carrier lookup integration +} + +func TestSend_SMS_RateLimiting(t *testing.T) { + t.Skip("SMS rate limiting tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Rate limit handling + // - Queue management for high-volume sending + // - Backoff strategies + // - Error recovery from rate limit exceeded +} + +func TestSend_SMS_LongMessages(t *testing.T) { + t.Skip("SMS long message tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Automatic message segmentation + // - Multi-part SMS handling + // - Character encoding (GSM 7-bit vs UCS-2) + // - Cost calculation for long messages +} + +// ============================================================================= +// SMS Error Handling Tests (Future Implementation) +// ============================================================================= + +func TestSend_SMS_InvalidPhoneNumber(t *testing.T) { + t.Skip("SMS invalid phone tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Invalid phone number format errors + // - Undeliverable number handling + // - Landline vs mobile detection +} + +func TestSend_SMS_InsufficientBalance(t *testing.T) { + t.Skip("SMS balance tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Account balance insufficient errors + // - Graceful degradation when funds are low + // - Balance monitoring and alerts +} + +func TestSend_SMS_APIError_Scenarios(t *testing.T) { + t.Skip("SMS API error tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Various Twilio API error codes + // - Network timeout handling + // - Authentication failures + // - Service unavailable scenarios +} + +// ============================================================================= +// SMS Benchmark Tests (Future Implementation) +// ============================================================================= + +func BenchmarkSend_SMS(b *testing.B) { + b.Skip("SMS benchmarks not implemented yet - placeholder for future implementation") + + // Future implementation will benchmark: + // - Single SMS sending performance + // - Memory allocation patterns + // - Connection reuse efficiency +} + +func BenchmarkSendBatch_SMS(b *testing.B) { + b.Skip("SMS batch benchmarks not implemented yet - placeholder for future implementation") + + // Future implementation will benchmark: + // - Batch SMS sending throughput + // - Optimal batch sizes + // - Resource utilization under load +} diff --git a/messenger/providers/twilio/twilio_test.go b/messenger/providers/twilio/twilio_test.go new file mode 100644 index 00000000..bcb62815 --- /dev/null +++ b/messenger/providers/twilio/twilio_test.go @@ -0,0 +1,547 @@ +package twilio + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/messenger/types" + "github.com/yaoapp/yao/test" +) + +// Test recipient email addresses - use authorized addresses for real API tests +const ( + TestEmailAgent = "agent@iqka.com" + TestEmailX = "x@iqka.com" + TestEmailXiang = "xiang@iqka.com" +) + +// Email-focused tests - SMS and WhatsApp tests are in separate files + +// loadTestConfig loads the unified.twilio.yao configuration for testing +func loadTestConfig(t *testing.T) types.ProviderConfig { + test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION") + defer test.Clean() + + config := types.ProviderConfig{ + Name: "unified", + Connector: "twilio", + Options: map[string]interface{}{ + "account_sid": os.Getenv("TWILIO_ACCOUNT_SID"), + "auth_token": os.Getenv("TWILIO_AUTH_TOKEN"), + "from_phone": os.Getenv("TWILIO_FROM_PHONE"), + "from_email": os.Getenv("TWILIO_FROM_EMAIL"), + "api_sid": os.Getenv("TWILIO_API_SID"), + "api_key": os.Getenv("TWILIO_API_KEY"), + "sendgrid_api_key": os.Getenv("TWILIO_SENDGRID_API_KEY"), + }, + } + + return config +} + +// createTestMessage creates a test message of the specified type +func createTestMessage(messageType types.MessageType) *types.Message { + switch messageType { + case types.MessageTypeEmail: + return &types.Message{ + Type: types.MessageTypeEmail, + To: []string{TestEmailAgent}, + Subject: "Test Email from Twilio Provider", + Body: "This is a test email sent via Twilio SendGrid API.", + HTML: "
This is a test email sent via Twilio SendGrid API.
", + } + // SMS and WhatsApp message creation moved to separate test files + default: + return &types.Message{ + Type: types.MessageTypeEmail, + To: []string{TestEmailAgent}, + Body: "Default test message", + } + } +} + +// ============================================================================= +// Basic Provider Tests +// ============================================================================= + +func TestNewTwilioProvider(t *testing.T) { + config := loadTestConfig(t) + + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + assert.NotNil(t, provider) + + // Verify configuration using actual environment variables + assert.Equal(t, os.Getenv("TWILIO_ACCOUNT_SID"), provider.accountSID) + assert.Equal(t, os.Getenv("TWILIO_AUTH_TOKEN"), provider.authToken) + assert.Equal(t, os.Getenv("TWILIO_FROM_PHONE"), provider.fromPhone) + assert.Equal(t, os.Getenv("TWILIO_FROM_EMAIL"), provider.fromEmail) + assert.Equal(t, os.Getenv("TWILIO_API_SID"), provider.apiSID) + assert.Equal(t, os.Getenv("TWILIO_API_KEY"), provider.apiKey) + assert.Equal(t, os.Getenv("TWILIO_SENDGRID_API_KEY"), provider.sendGridAPIKey) + assert.Equal(t, "unified", provider.config.Name) +} + +func TestNewTwilioProvider_MissingOptions(t *testing.T) { + config := types.ProviderConfig{ + Name: "test", + Connector: "twilio", + Options: nil, + } + + provider, err := NewTwilioProvider(config) + assert.Error(t, err) + assert.Nil(t, provider) + assert.Contains(t, err.Error(), "Twilio provider requires options") +} + +func TestNewTwilioProvider_MissingAccountSID(t *testing.T) { + config := types.ProviderConfig{ + Name: "test", + Connector: "twilio", + Options: map[string]interface{}{ + "auth_token": "test_token", + }, + } + + provider, err := NewTwilioProvider(config) + assert.Error(t, err) + assert.Nil(t, provider) + assert.Contains(t, err.Error(), "account_sid") +} + +func TestGetType(t *testing.T) { + config := loadTestConfig(t) + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + assert.Equal(t, "twilio", provider.GetType()) +} + +func TestGetName(t *testing.T) { + config := loadTestConfig(t) + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + assert.Equal(t, "unified", provider.GetName()) +} + +func TestValidate(t *testing.T) { + config := loadTestConfig(t) + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + err = provider.Validate() + assert.NoError(t, err) +} + +func TestValidate_MissingAccountSID(t *testing.T) { + config := types.ProviderConfig{ + Name: "test", + Connector: "twilio", + Options: map[string]interface{}{ + "auth_token": "test_token", + }, + } + + provider, err := NewTwilioProvider(config) + require.Error(t, err) + assert.Nil(t, provider) +} + +func TestValidate_MissingAuth(t *testing.T) { + config := types.ProviderConfig{ + Name: "test", + Connector: "twilio", + Options: map[string]interface{}{ + "account_sid": "test_sid", + }, + } + + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + err = provider.Validate() + assert.Error(t, err) + assert.Contains(t, err.Error(), "auth_token") +} + +func TestValidate_PartialAPIKeys(t *testing.T) { + config := types.ProviderConfig{ + Name: "test", + Connector: "twilio", + Options: map[string]interface{}{ + "account_sid": "test_sid", + "auth_token": "valid_token", // Provide auth_token to pass first check + "api_sid": "test_api_sid", + // Missing api_key - this should trigger the partial API keys error + }, + } + + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + err = provider.Validate() + assert.Error(t, err) + // Should trigger the "both api_sid and api_key must be provided together" error + assert.Contains(t, err.Error(), "both 'api_sid' and 'api_key' must be provided together") +} + +func TestClose(t *testing.T) { + config := loadTestConfig(t) + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + err = provider.Close() + assert.NoError(t, err) +} + +// ============================================================================= +// Email Sending Tests (via SendGrid API) +// ============================================================================= + +func TestSend_EmailMessage_RealAPI(t *testing.T) { + config := loadTestConfig(t) + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + // Skip if SendGrid API key is not configured + if provider.sendGridAPIKey == "" { + t.Skip("TWILIO_SENDGRID_API_KEY not configured, skipping real API test") + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + emailMessage := createTestMessage(types.MessageTypeEmail) + + err = provider.Send(ctx, emailMessage) + if err == nil { + t.Log("Real Twilio SendGrid API call succeeded") + } else { + t.Logf("Real Twilio SendGrid API call failed (expected in some test environments): %v", err) + // Don't fail the test if it's just an API configuration issue + if !strings.Contains(err.Error(), "SendGrid API error") { + assert.NoError(t, err) + } + } +} + +func TestSend_EmailMessage_APIError(t *testing.T) { + // Create a mock HTTP server that returns an error + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]interface{}{ + "errors": []map[string]interface{}{ + {"message": "Bad Request", "field": "from.email", "help": "Invalid from email"}, + }, + }) + })) + defer server.Close() + + config := types.ProviderConfig{ + Name: "test", + Connector: "twilio", + Options: map[string]interface{}{ + "account_sid": "test_sid", + "auth_token": "test_token", + "from_email": "test@example.com", + "sendgrid_api_key": "test_api_key", + }, + } + + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + // Mock the SendGrid API endpoint by temporarily replacing the sendEmail method behavior + // For this test, we'll create a custom provider with a modified http client + provider.httpClient = &http.Client{ + Transport: &mockTransport{server: server}, + } + + ctx := context.Background() + emailMessage := createTestMessage(types.MessageTypeEmail) + + err = provider.Send(ctx, emailMessage) + assert.Error(t, err) + assert.Contains(t, err.Error(), "SendGrid API error") +} + +func TestSend_ContextTimeout_Email_RealAPI(t *testing.T) { + config := loadTestConfig(t) + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + // Skip if SendGrid API key is not configured + if provider.sendGridAPIKey == "" { + t.Skip("TWILIO_SENDGRID_API_KEY not configured, skipping real API test") + } + + // Create a very short timeout context + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) + defer cancel() + + emailMessage := createTestMessage(types.MessageTypeEmail) + + err = provider.Send(ctx, emailMessage) + if err != nil { + t.Log("Context timeout working correctly with real API") + // Could be timeout or other error, both are acceptable for this test + } else { + t.Log("Request completed faster than timeout") + } +} + +func TestSendBatch_Email_RealAPI(t *testing.T) { + config := loadTestConfig(t) + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + // Skip if SendGrid API key is not configured + if provider.sendGridAPIKey == "" { + t.Skip("TWILIO_SENDGRID_API_KEY not configured, skipping real API test") + } + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + messages := []*types.Message{ + { + Type: types.MessageTypeEmail, + To: []string{TestEmailAgent}, + Subject: "Batch Test Email 1", + Body: "This is batch test email 1 via Twilio SendGrid API.", + }, + { + Type: types.MessageTypeEmail, + To: []string{TestEmailX}, + Subject: "Batch Test Email 2", + Body: "This is batch test email 2 via Twilio SendGrid API.", + }, + } + + err = provider.SendBatch(ctx, messages) + if err == nil { + t.Log("Real Twilio SendGrid batch API call succeeded") + } else { + t.Logf("Real Twilio SendGrid batch API call failed (expected in some test environments): %v", err) + // Don't fail the test if it's just an API configuration issue + if !strings.Contains(err.Error(), "SendGrid API error") { + assert.NoError(t, err) + } + } +} + +func TestSend_EmailMessage_WithCustomFrom(t *testing.T) { + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var payload map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&payload) + assert.NoError(t, err) + + // Verify custom from address is used + from := payload["from"].(map[string]interface{}) + assert.Equal(t, "custom@example.com", from["email"]) + + w.WriteHeader(http.StatusAccepted) + json.NewEncoder(w).Encode(map[string]interface{}{"message": "success"}) + })) + defer server.Close() + + config := types.ProviderConfig{ + Name: "test", + Connector: "twilio", + Options: map[string]interface{}{ + "account_sid": "test_sid", + "auth_token": "test_token", + "from_email": "default@example.com", + "sendgrid_api_key": "test_api_key", + }, + } + + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + provider.httpClient = &http.Client{ + Transport: &mockTransport{server: server}, + } + + ctx := context.Background() + emailMessage := createTestMessage(types.MessageTypeEmail) + emailMessage.From = "custom@example.com" + + err = provider.Send(ctx, emailMessage) + assert.NoError(t, err) +} + +func TestSend_EmailMessage_WithScheduledTime(t *testing.T) { + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var payload map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&payload) + assert.NoError(t, err) + + // Verify scheduled time is set + sendAt, exists := payload["send_at"] + assert.True(t, exists) + assert.NotNil(t, sendAt) + + w.WriteHeader(http.StatusAccepted) + json.NewEncoder(w).Encode(map[string]interface{}{"message": "success"}) + })) + defer server.Close() + + config := types.ProviderConfig{ + Name: "test", + Connector: "twilio", + Options: map[string]interface{}{ + "account_sid": "test_sid", + "auth_token": "test_token", + "from_email": "test@example.com", + "sendgrid_api_key": "test_api_key", + }, + } + + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + provider.httpClient = &http.Client{ + Transport: &mockTransport{server: server}, + } + + ctx := context.Background() + emailMessage := createTestMessage(types.MessageTypeEmail) + scheduledTime := time.Now().Add(1 * time.Hour) + emailMessage.ScheduledAt = &scheduledTime + + err = provider.Send(ctx, emailMessage) + assert.NoError(t, err) +} + +func TestSend_EmailMessage_MultipleRecipients_RealAPI(t *testing.T) { + config := loadTestConfig(t) + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + // Skip if SendGrid API key is not configured + if provider.sendGridAPIKey == "" { + t.Skip("TWILIO_SENDGRID_API_KEY not configured, skipping real API test") + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + emailMessage := createTestMessage(types.MessageTypeEmail) + emailMessage.To = []string{TestEmailAgent, TestEmailX, TestEmailXiang} + emailMessage.Subject = "Multiple Recipients Test" + + err = provider.Send(ctx, emailMessage) + if err == nil { + t.Log("Twilio SendGrid multiple recipients API call succeeded") + } else { + t.Logf("Twilio SendGrid multiple recipients API call failed (expected in some test environments): %v", err) + // Don't fail the test if it's just an API configuration issue + if !strings.Contains(err.Error(), "SendGrid API error") { + assert.NoError(t, err) + } + } +} + +func TestSend_UnsupportedMessageType(t *testing.T) { + config := loadTestConfig(t) + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + ctx := context.Background() + + // Test unsupported message type + unsupportedMessage := &types.Message{ + Type: "unsupported_type", + To: []string{TestEmailAgent}, + Body: "This should fail", + } + + err = provider.Send(ctx, unsupportedMessage) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unsupported message type") +} + +// ============================================================================= +// Note: SMS and WhatsApp tests have been moved to separate files: +// - twilio_sms_test.go: SMS-specific tests and functionality +// - twilio_whatsapp_test.go: WhatsApp-specific tests and functionality +// ============================================================================= + +// ============================================================================= +// Benchmark Tests +// ============================================================================= + +func BenchmarkSend_Email(b *testing.B) { + config := loadTestConfig(&testing.T{}) + provider, err := NewTwilioProvider(config) + if err != nil { + b.Fatalf("Failed to create provider: %v", err) + } + + // Skip if SendGrid API key is not configured + if provider.sendGridAPIKey == "" { + b.Skip("TWILIO_SENDGRID_API_KEY not configured, skipping benchmark") + } + + ctx := context.Background() + emailMessage := createTestMessage(types.MessageTypeEmail) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = provider.Send(ctx, emailMessage) + } +} + +func BenchmarkSendBatch_Email(b *testing.B) { + config := loadTestConfig(&testing.T{}) + provider, err := NewTwilioProvider(config) + if err != nil { + b.Fatalf("Failed to create provider: %v", err) + } + + // Skip if SendGrid API key is not configured + if provider.sendGridAPIKey == "" { + b.Skip("TWILIO_SENDGRID_API_KEY not configured, skipping benchmark") + } + + ctx := context.Background() + messages := []*types.Message{ + createTestMessage(types.MessageTypeEmail), + createTestMessage(types.MessageTypeEmail), + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = provider.SendBatch(ctx, messages) + } +} + +// ============================================================================= +// Helper Types for Mocking +// ============================================================================= + +// mockTransport is a custom RoundTripper for mocking HTTP requests +type mockTransport struct { + server *httptest.Server +} + +func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // Redirect all requests to our mock server + req.URL.Scheme = "http" + req.URL.Host = strings.TrimPrefix(t.server.URL, "http://") + return http.DefaultTransport.RoundTrip(req) +} diff --git a/messenger/providers/twilio/twilio_whatsapp_test.go b/messenger/providers/twilio/twilio_whatsapp_test.go new file mode 100644 index 00000000..a8058fcb --- /dev/null +++ b/messenger/providers/twilio/twilio_whatsapp_test.go @@ -0,0 +1,380 @@ +package twilio + +import ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/yaoapp/yao/messenger/types" +) + +// Test phone numbers for WhatsApp (placeholder for future implementation) +const ( + TestWhatsAppPhoneAgent = "+1234567890" // Placeholder - replace with authorized WhatsApp Business numbers + TestWhatsAppPhoneX = "+1234567891" // Placeholder - replace with authorized WhatsApp Business numbers + TestWhatsAppPhoneXiang = "+1234567892" // Placeholder - replace with authorized WhatsApp Business numbers +) + +// createTestWhatsAppMessage creates a test WhatsApp message +func createTestWhatsAppMessage() *types.Message { + return &types.Message{ + Type: types.MessageTypeWhatsApp, + To: []string{TestWhatsAppPhoneAgent}, + Body: "Test WhatsApp message from Twilio Provider - Hello from Yao! 👋", + } +} + +// loadWhatsAppTestConfig loads configuration optimized for WhatsApp testing +func loadWhatsAppTestConfig(t *testing.T) types.ProviderConfig { + config := loadTestConfig(t) // Reuse base config loading + + // Ensure WhatsApp-specific options are available + // In real implementation, verify TWILIO_FROM_PHONE is a WhatsApp Business number + + return config +} + +// ============================================================================= +// WhatsApp Provider Configuration Tests +// ============================================================================= + +func TestWhatsApp_ProviderConfig_WithFromPhone(t *testing.T) { + config := types.ProviderConfig{ + Name: "whatsapp_test", + Connector: "twilio", + Options: map[string]interface{}{ + "account_sid": "test_account_sid", + "auth_token": "test_auth_token", + "from_phone": "+15551234567", // WhatsApp requires from_phone (WhatsApp Business number) + }, + } + + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + assert.NotNil(t, provider) + assert.Equal(t, "+15551234567", provider.fromPhone) +} + +func TestWhatsApp_ProviderConfig_MissingFromPhone(t *testing.T) { + config := types.ProviderConfig{ + Name: "whatsapp_test", + Connector: "twilio", + Options: map[string]interface{}{ + "account_sid": "test_account_sid", + "auth_token": "test_auth_token", + // Missing from_phone - required for WhatsApp + }, + } + + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + ctx := context.Background() + whatsappMessage := createTestWhatsAppMessage() + + err = provider.Send(ctx, whatsappMessage) + assert.Error(t, err) + assert.Contains(t, err.Error(), "from_phone is required for WhatsApp messages") +} + +func TestWhatsApp_PhoneNumberFormatting(t *testing.T) { + // Test that phone numbers are properly formatted with whatsapp: prefix + // This tests the internal logic without making API calls + + config := types.ProviderConfig{ + Name: "whatsapp_test", + Connector: "twilio", + Options: map[string]interface{}{ + "account_sid": "test_account_sid", + "auth_token": "test_auth_token", + "from_phone": "+15551234567", // Will be formatted to whatsapp:+15551234567 + }, + } + + provider, err := NewTwilioProvider(config) + require.NoError(t, err) + + // Test internal phone number formatting logic + // In real implementation, we'd test the sendWhatsAppToRecipient method + // For now, just verify the provider stores the number correctly + assert.Equal(t, "+15551234567", provider.fromPhone) + assert.False(t, strings.HasPrefix(provider.fromPhone, "whatsapp:")) + + // The whatsapp: prefix should be added during sending, not during configuration +} + +// ============================================================================= +// WhatsApp Sending Tests (Future Implementation) +// ============================================================================= + +// TODO: Implement real WhatsApp sending tests +func TestSend_WhatsAppMessage_RealAPI(t *testing.T) { + t.Skip("WhatsApp real API tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // config := loadWhatsAppTestConfig(t) + // provider, err := NewTwilioProvider(config) + // require.NoError(t, err) + // + // // Skip if from_phone is not configured or not a WhatsApp Business number + // if provider.fromPhone == "" { + // t.Skip("TWILIO_FROM_PHONE not configured, skipping real WhatsApp API test") + // } + // + // ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + // defer cancel() + // + // whatsappMessage := createTestWhatsAppMessage() + // err = provider.Send(ctx, whatsappMessage) + // if err == nil { + // t.Log("Real Twilio WhatsApp API call succeeded") + // } else { + // t.Logf("Real Twilio WhatsApp API call failed: %v", err) + // // Handle expected failures in test environments + // } +} + +func TestSend_WhatsAppMessage_PhoneNumberFormatting_RealAPI(t *testing.T) { + t.Skip("WhatsApp phone formatting real API tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Automatic "whatsapp:" prefix addition for both from and to numbers + // - Handling of numbers that already have the prefix + // - International phone number format validation + // - E.164 format compliance + // - Error handling for invalid WhatsApp numbers +} + +func TestSend_WhatsAppMessage_ContextTimeout_RealAPI(t *testing.T) { + t.Skip("WhatsApp context timeout tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // config := loadWhatsAppTestConfig(t) + // provider, err := NewTwilioProvider(config) + // require.NoError(t, err) + // + // // Create a very short timeout context + // ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) + // defer cancel() + // + // whatsappMessage := createTestWhatsAppMessage() + // err = provider.Send(ctx, whatsappMessage) + // if err != nil { + // t.Log("Context timeout working correctly with real WhatsApp API") + // } +} + +func TestSendBatch_WhatsApp_RealAPI(t *testing.T) { + t.Skip("WhatsApp batch real API tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // config := loadWhatsAppTestConfig(t) + // provider, err := NewTwilioProvider(config) + // require.NoError(t, err) + // + // ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + // defer cancel() + // + // messages := []*types.Message{ + // { + // Type: types.MessageTypeWhatsApp, + // To: []string{TestWhatsAppPhoneAgent}, + // Body: "Batch WhatsApp Test 1 - Hello! 👋", + // }, + // { + // Type: types.MessageTypeWhatsApp, + // To: []string{TestWhatsAppPhoneX}, + // Body: "Batch WhatsApp Test 2 - How are you? 😊", + // }, + // } + // + // err = provider.SendBatch(ctx, messages) + // if err == nil { + // t.Log("Real Twilio WhatsApp batch API call succeeded") + // } +} + +func TestSend_WhatsApp_MultipleRecipients_RealAPI(t *testing.T) { + t.Skip("WhatsApp multiple recipients tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // config := loadWhatsAppTestConfig(t) + // provider, err := NewTwilioProvider(config) + // require.NoError(t, err) + // + // ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) + // defer cancel() + // + // whatsappMessage := &types.Message{ + // Type: types.MessageTypeWhatsApp, + // To: []string{TestWhatsAppPhoneAgent, TestWhatsAppPhoneX, TestWhatsAppPhoneXiang}, + // Body: "Multi-recipient WhatsApp test from Twilio Provider 🚀", + // } + // + // err = provider.Send(ctx, whatsappMessage) + // if err == nil { + // t.Log("Twilio WhatsApp multiple recipients API call succeeded") + // } +} + +// ============================================================================= +// WhatsApp Advanced Features Tests (Future Implementation) +// ============================================================================= + +func TestSend_WhatsApp_WithMediaMessage(t *testing.T) { + t.Skip("WhatsApp media message tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Image messages with media URLs + // - Document attachments + // - Audio messages + // - Video messages + // - Media size and format validation +} + +func TestSend_WhatsApp_WithTemplateMessage(t *testing.T) { + t.Skip("WhatsApp template message tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - WhatsApp Business template messages + // - Template parameter substitution + // - Template approval status handling + // - Language-specific templates + // - Template versioning +} + +func TestSend_WhatsApp_WithInteractiveMessage(t *testing.T) { + t.Skip("WhatsApp interactive message tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Button messages + // - List messages + // - Quick reply buttons + // - Interactive message validation + // - Response handling +} + +func TestSend_WhatsApp_WithLocationMessage(t *testing.T) { + t.Skip("WhatsApp location message tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Location sharing messages + // - Address and coordinates + // - Location name and description + // - Venue information +} + +func TestSend_WhatsApp_WithContactMessage(t *testing.T) { + t.Skip("WhatsApp contact message tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Contact card messages + // - vCard format support + // - Multiple contact sharing + // - Contact information validation +} + +// ============================================================================= +// WhatsApp Business Features Tests (Future Implementation) +// ============================================================================= + +func TestSend_WhatsApp_BusinessProfile(t *testing.T) { + t.Skip("WhatsApp Business profile tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Business profile information + // - Verified business badge + // - Business hours and description + // - Website and contact info +} + +func TestSend_WhatsApp_OptInOptOut(t *testing.T) { + t.Skip("WhatsApp opt-in/out tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - User opt-in confirmation + // - Opt-out request handling + // - Compliance with WhatsApp policies + // - Subscription management +} + +func TestSend_WhatsApp_MessageStatus(t *testing.T) { + t.Skip("WhatsApp message status tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Message delivery status + // - Read receipts + // - Failed message handling + // - Status webhook configuration +} + +// ============================================================================= +// WhatsApp Error Handling Tests (Future Implementation) +// ============================================================================= + +func TestSend_WhatsApp_InvalidPhoneNumber(t *testing.T) { + t.Skip("WhatsApp invalid phone tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Invalid WhatsApp number format errors + // - Non-WhatsApp numbers + // - Blocked or suspended numbers + // - Number verification failures +} + +func TestSend_WhatsApp_RateLimiting(t *testing.T) { + t.Skip("WhatsApp rate limiting tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - WhatsApp Business API rate limits + // - 24-hour messaging window + // - Template message limits + // - Conversation-based pricing +} + +func TestSend_WhatsApp_PolicyViolation(t *testing.T) { + t.Skip("WhatsApp policy tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Content policy violations + // - Spam detection and prevention + // - Business policy compliance + // - Account suspension scenarios +} + +func TestSend_WhatsApp_APIError_Scenarios(t *testing.T) { + t.Skip("WhatsApp API error tests not implemented yet - placeholder for future implementation") + + // Future implementation will test: + // - Various WhatsApp API error codes + // - Network timeout handling + // - Authentication failures + // - Service unavailable scenarios + // - Webhook delivery failures +} + +// ============================================================================= +// WhatsApp Benchmark Tests (Future Implementation) +// ============================================================================= + +func BenchmarkSend_WhatsApp(b *testing.B) { + b.Skip("WhatsApp benchmarks not implemented yet - placeholder for future implementation") + + // Future implementation will benchmark: + // - Single WhatsApp message sending performance + // - Memory allocation patterns + // - Connection reuse efficiency + // - Media message processing time +} + +func BenchmarkSendBatch_WhatsApp(b *testing.B) { + b.Skip("WhatsApp batch benchmarks not implemented yet - placeholder for future implementation") + + // Future implementation will benchmark: + // - Batch WhatsApp sending throughput + // - Optimal batch sizes for WhatsApp + // - Resource utilization under load + // - Template message processing efficiency +}