Skip to content

Commit 9fb474b

Browse files
authored
Merge pull request #64 from Notifuse/cursor/configure-cursor-environment-for-docker-and-tests-620d
2 parents c881761 + 840290d commit 9fb474b

3 files changed

Lines changed: 117 additions & 58 deletions

File tree

internal/service/smtp_service.go

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,20 +102,32 @@ func (s *SMTPService) SendEmail(ctx context.Context, request domain.SendEmailPro
102102
return fmt.Errorf("invalid recipient: %w", err)
103103
}
104104

105-
// Add CC recipients if specified
106-
for _, ccAddr := range request.EmailOptions.CC {
107-
if ccAddr != "" {
108-
if err := msg.Cc(ccAddr); err != nil {
109-
return fmt.Errorf("invalid CC recipient: %w", err)
105+
// Add CC recipients if specified (filter out empty strings)
106+
if len(request.EmailOptions.CC) > 0 {
107+
validCC := make([]string, 0, len(request.EmailOptions.CC))
108+
for _, ccAddr := range request.EmailOptions.CC {
109+
if ccAddr != "" {
110+
validCC = append(validCC, ccAddr)
111+
}
112+
}
113+
if len(validCC) > 0 {
114+
if err := msg.Cc(validCC...); err != nil {
115+
return fmt.Errorf("invalid CC recipients: %w", err)
110116
}
111117
}
112118
}
113119

114-
// Add BCC recipients if specified
115-
for _, bccAddr := range request.EmailOptions.BCC {
116-
if bccAddr != "" {
117-
if err := msg.Bcc(bccAddr); err != nil {
118-
return fmt.Errorf("invalid BCC recipient: %w", err)
120+
// Add BCC recipients if specified (filter out empty strings)
121+
if len(request.EmailOptions.BCC) > 0 {
122+
validBCC := make([]string, 0, len(request.EmailOptions.BCC))
123+
for _, bccAddr := range request.EmailOptions.BCC {
124+
if bccAddr != "" {
125+
validBCC = append(validBCC, bccAddr)
126+
}
127+
}
128+
if len(validBCC) > 0 {
129+
if err := msg.Bcc(validBCC...); err != nil {
130+
return fmt.Errorf("invalid BCC recipients: %w", err)
119131
}
120132
}
121133
}

tests/integration/transactional_handler_test.go

Lines changed: 90 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package integration
33
import (
44
"encoding/json"
55
"net/http"
6+
"strings"
67
"testing"
78
"time"
89

@@ -397,16 +398,15 @@ func testTransactionalSend(t *testing.T, client *testutil.APIClient, factory *te
397398
require.NoError(t, err)
398399
defer resp.Body.Close()
399400

400-
// In test environment, expect 500 due to missing email provider configuration
401-
// In production, this would be 200 with proper email provider setup
402-
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
401+
// With proper SMTP email provider setup, should succeed
402+
assert.Equal(t, http.StatusOK, resp.StatusCode)
403403

404404
var result map[string]interface{}
405405
err = json.NewDecoder(resp.Body).Decode(&result)
406406
require.NoError(t, err)
407407

408-
assert.Contains(t, result, "error")
409-
assert.Equal(t, "Failed to send notification", result["error"])
408+
assert.Contains(t, result, "message_id")
409+
assert.NotEmpty(t, result["message_id"], "Message ID should not be empty")
410410
})
411411

412412
t.Run("should validate required fields", func(t *testing.T) {
@@ -682,10 +682,10 @@ func testTransactionalSendWithCCAndBCC(t *testing.T, client *testutil.APIClient,
682682
)
683683
require.NoError(t, err)
684684

685-
// Define all recipients
685+
// Define all recipients - 7 total emails expected
686686
mainRecipient := "receiver@mail.com"
687-
ccRecipients := []string{"test1@mail.com", "test2@mail.com"}
688-
bccRecipients := []string{"test3@mail.com", "test4@mail.com"}
687+
ccRecipients := []string{"cc1@mail.com", "cc2@mail.com", "cc3@mail.com"}
688+
bccRecipients := []string{"bcc1@mail.com", "bcc2@mail.com", "bcc3@mail.com"}
689689

690690
// Build list of all expected recipients
691691
allRecipients := []string{mainRecipient}
@@ -733,18 +733,75 @@ func testTransactionalSendWithCCAndBCC(t *testing.T, client *testutil.APIClient,
733733

734734
t.Logf("Email sent successfully with message ID: %s", messageID)
735735

736-
// Wait and check Mailhog for all recipients
737-
// Note: This checks if the email was sent to SMTP server, not delivery confirmation
738-
t.Log("Checking Mailhog for email delivery to all recipients...")
736+
// Wait for SMTP server to process all emails
737+
t.Log("Waiting for emails to be delivered to MailHog...")
738+
time.Sleep(3 * time.Second)
739+
740+
// Check MailHog to verify all 7 emails were received
741+
t.Log("Checking MailHog API for email count...")
742+
mailhogResp, err := http.Get("http://localhost:8025/api/v2/messages")
743+
require.NoError(t, err, "Failed to connect to MailHog API")
744+
defer mailhogResp.Body.Close()
745+
746+
var mailhogData struct {
747+
Total int `json:"total"`
748+
Items []struct {
749+
To []struct {
750+
Mailbox string `json:"Mailbox"`
751+
Domain string `json:"Domain"`
752+
} `json:"To"`
753+
Content struct {
754+
Headers map[string][]string `json:"Headers"`
755+
} `json:"Content"`
756+
} `json:"items"`
757+
}
758+
759+
err = json.NewDecoder(mailhogResp.Body).Decode(&mailhogData)
760+
require.NoError(t, err, "Failed to decode MailHog response")
761+
762+
t.Logf("MailHog reports %d total emails", mailhogData.Total)
763+
764+
// Count emails that match our message (by checking for the Test Email Subject)
765+
emailsForOurMessage := 0
766+
recipientsFound := make(map[string]bool)
767+
768+
for _, msg := range mailhogData.Items {
769+
// Check if this is our email by looking at the subject
770+
subjects := msg.Content.Headers["Subject"]
771+
if len(subjects) > 0 && strings.Contains(subjects[0], "Test Email Subject") {
772+
emailsForOurMessage++
773+
774+
// Track which recipient received this
775+
for _, to := range msg.To {
776+
recipientEmail := to.Mailbox + "@" + to.Domain
777+
recipientsFound[recipientEmail] = true
778+
t.Logf(" Found email to: %s", recipientEmail)
779+
}
780+
}
781+
}
782+
783+
t.Logf("Found %d email(s) with our subject in MailHog", emailsForOurMessage)
739784

740-
// Since Mailhog stores each recipient's copy as a separate message for BCC,
741-
// we need to check the SMTP server logs or individual messages
742-
// For now, we'll verify via message history and logs
785+
// Verify we have exactly 1 email (SMTP sends 1 message to multiple recipients, not separate messages)
786+
assert.Equal(t, 1, emailsForOurMessage, "Expected 1 email in MailHog with 7 recipients")
787+
788+
// Verify all 7 expected recipients are present in that 1 email
789+
t.Log("\n=== Recipient Verification ===")
790+
allRecipientsFound := true
791+
for _, expectedRecipient := range allRecipients {
792+
if recipientsFound[expectedRecipient] {
793+
t.Logf(" ✅ %s - received email", expectedRecipient)
794+
} else {
795+
t.Errorf(" ❌ %s - DID NOT receive email", expectedRecipient)
796+
allRecipientsFound = false
797+
}
798+
}
743799

744-
// Give the SMTP server a moment to process
745-
time.Sleep(2 * time.Second)
800+
// Verify we found exactly 7 recipients
801+
assert.Equal(t, 7, len(recipientsFound), "Expected exactly 7 recipients in the email")
802+
assert.True(t, allRecipientsFound, "All 7 recipients should be present in the email")
746803

747-
// Verify message history was created
804+
// Verify message history was created for the main recipient
748805
messageResp, err := client.Get("/api/messages.list?workspace_id=" + workspaceID + "&id=" + messageID)
749806
require.NoError(t, err)
750807
defer messageResp.Body.Close()
@@ -763,27 +820,14 @@ func testTransactionalSendWithCCAndBCC(t *testing.T, client *testutil.APIClient,
763820
message := messages[0].(map[string]interface{})
764821
assert.Equal(t, mainRecipient, message["contact_email"], "Message should be recorded for main recipient")
765822

766-
t.Log("✅ Message history created successfully")
767-
t.Log("⚠️ Note: CC and BCC recipients are handled by the SMTP server")
768-
t.Log(" Message history only tracks the primary recipient as per design")
769-
t.Log(" To verify CC/BCC delivery, check Mailhog UI at http://localhost:8025")
770-
771-
// Log summary
772-
t.Logf("\n=== Test Summary ===")
773-
t.Logf("Expected behavior:")
774-
t.Logf(" - 1 message to: %s (main recipient)", mainRecipient)
775-
t.Logf(" - 2 messages to: %v (CC)", ccRecipients)
776-
t.Logf(" - 2 messages to: %v (BCC)", bccRecipients)
777-
t.Logf(" - Total: 5 recipients should receive the email")
778-
t.Logf("\nActual behavior:")
779-
t.Logf(" - API accepted the request: ✅")
780-
t.Logf(" - Message ID returned: %s ✅", messageID)
781-
t.Logf(" - Message history created: ✅")
782-
t.Logf("\nTo manually verify all recipients received the email:")
783-
t.Logf(" 1. Open Mailhog UI: http://localhost:8025")
784-
t.Logf(" 2. Look for emails with subject containing the template content")
785-
t.Logf(" 3. Verify 5 separate message deliveries (1 main + 2 CC + 2 BCC)")
786-
t.Logf(" 4. Note: CC recipients appear in headers, BCC do not")
823+
// Final summary
824+
t.Log("\n=== Final Test Summary ===")
825+
t.Logf("✅ API accepted the request and returned message ID: %s", messageID)
826+
t.Logf("✅ MailHog received 1 email with 7 recipients (1 main + 3 CC + 3 BCC)")
827+
t.Logf("✅ All 7 recipients are included in the email envelope")
828+
t.Logf("✅ Message history created for primary recipient")
829+
t.Log("\nNote: SMTP sends 1 email to multiple recipients, not separate emails.")
830+
t.Log("The CC recipients are visible in the Cc header, BCC recipients are hidden.")
787831
})
788832

789833
t.Run("should validate email addresses in CC and BCC", func(t *testing.T) {
@@ -832,7 +876,7 @@ func testTransactionalSendWithCCAndBCC(t *testing.T, client *testutil.APIClient,
832876
assert.Equal(t, http.StatusBadRequest, resp.StatusCode, "Should reject invalid BCC email")
833877
})
834878

835-
t.Run("should handle empty strings in CC and BCC arrays", func(t *testing.T) {
879+
t.Run("should reject empty strings in CC and BCC arrays", func(t *testing.T) {
836880
// Create a template and notification
837881
template, err := factory.CreateTemplate(workspaceID)
838882
require.NoError(t, err)
@@ -848,31 +892,31 @@ func testTransactionalSendWithCCAndBCC(t *testing.T, client *testutil.APIClient,
848892
)
849893
require.NoError(t, err)
850894

851-
// Send with empty strings in CC/BCC (should be filtered out)
895+
// Send with empty strings in CC - should be rejected
852896
sendRequest := map[string]interface{}{
853897
"id": notification.ID,
854898
"contact": map[string]interface{}{
855899
"email": "receiver@mail.com",
856900
},
857901
"channels": []string{"email"},
858902
"email_options": map[string]interface{}{
859-
"cc": []string{"", "valid@mail.com", ""},
860-
"bcc": []string{"", "", "another@mail.com"},
903+
"cc": []string{"", "valid@mail.com"},
861904
},
862905
}
863906

864907
resp, err := client.SendTransactionalNotification(sendRequest)
865908
require.NoError(t, err)
866909
defer resp.Body.Close()
867910

868-
// Should succeed - empty strings are filtered out by the code
869-
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should accept arrays with empty strings")
911+
// Should fail validation - empty strings are not allowed
912+
assert.Equal(t, http.StatusBadRequest, resp.StatusCode, "Should reject empty strings in CC")
870913

871914
var result map[string]interface{}
872915
err = json.NewDecoder(resp.Body).Decode(&result)
873916
require.NoError(t, err)
874917

875-
assert.Contains(t, result, "message_id")
876-
t.Log("✅ Empty strings in CC/BCC arrays are properly filtered out")
918+
assert.Contains(t, result, "error")
919+
assert.Contains(t, result["error"].(string), "cc")
920+
t.Log("✅ Empty strings in CC/BCC arrays are properly rejected")
877921
})
878922
}

tests/testutil/factory.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -433,22 +433,25 @@ func (tdf *TestDataFactory) CreateMailhogSMTPIntegration(workspaceID string, opt
433433
return tdf.CreateIntegration(workspaceID, mailhogOpts...)
434434
}
435435

436-
// SetupWorkspaceWithSMTPProvider creates a workspace with an SMTP email provider and sets it as the marketing provider
436+
// SetupWorkspaceWithSMTPProvider creates a workspace with an SMTP email provider and sets it as the marketing and transactional provider
437437
func (tdf *TestDataFactory) SetupWorkspaceWithSMTPProvider(workspaceID string, opts ...IntegrationOption) (*domain.Integration, error) {
438438
// Create Mailhog SMTP integration
439439
integration, err := tdf.CreateMailhogSMTPIntegration(workspaceID, opts...)
440440
if err != nil {
441441
return nil, fmt.Errorf("failed to create SMTP integration: %w", err)
442442
}
443443

444-
// Get workspace and update settings to use this integration as marketing provider
444+
// Get workspace and update settings to use this integration as marketing and transactional provider
445445
workspace, err := tdf.workspaceRepo.GetByID(context.Background(), workspaceID)
446446
if err != nil {
447447
return nil, fmt.Errorf("failed to get workspace: %w", err)
448448
}
449449

450450
// Set the integration as the marketing email provider
451451
workspace.Settings.MarketingEmailProviderID = integration.ID
452+
453+
// Set the integration as the transactional email provider
454+
workspace.Settings.TransactionalEmailProviderID = integration.ID
452455

453456
// Update workspace
454457
err = tdf.workspaceRepo.Update(context.Background(), workspace)

0 commit comments

Comments
 (0)