package smtp2shoutrrr import ( "context" "fmt" "io" "net/http" "net/http/httptest" "net/smtp" "testing" "time" "github.com/stretchr/testify/require" ) func TestEmailForwarding(t *testing.T) { // Start mock ntfy server notifications := make([]string, 0) mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } // Read body and log body, err := io.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } notifications = append(notifications, string(body)) w.WriteHeader(http.StatusOK) })) defer mockServer.Close() // Configure the SMTP server config := &Config{ Port: 2525, Username: "testuser", Password: "testpass", Recipients: []ConfigRecipient{ { Addresses: []string{"test@example.com"}, Target: "generic+" + mockServer.URL + "/?template=json", }, }, } ctx := context.Background() smtpServer := NewSMTPServer(config) // Start the server go func() { if err := smtpServer.Start(ctx); err != nil { t.Errorf("failed to start server: %v", err) } }() // Give the server time to start time.Sleep(100 * time.Millisecond) defer func() { if err := smtpServer.Stop(ctx); err != nil { t.Errorf("failed to stop server: %v", err) } }() // Send test email auth := smtp.PlainAuth("", config.Username, config.Password, "localhost") err := smtp.SendMail( fmt.Sprintf("localhost:%d", config.Port), auth, "sender@example.com", []string{"test@example.com"}, []byte("Subject: Test Email\r\n\r\nThis is a test email body"), ) require.NoError(t, err) // Give some time for the notification to be processed time.Sleep(100 * time.Millisecond) // Verify the notification was received require.Len(t, notifications, 1) require.Contains(t, notifications[0], "This is a test email body") } func TestMultipleTargets(t *testing.T) { // Track notifications received notifications := make([]string, 0) mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } body, err := io.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } notifications = append(notifications, string(body)) w.WriteHeader(http.StatusOK) })) defer mockServer.Close() // Configure with multiple targets config := &Config{ Port: 2526, Username: "testuser", Password: "testpass", Recipients: []ConfigRecipient{ { Addresses: []string{"test@example.com"}, Targets: []string{ "generic+" + mockServer.URL + "/?template=json&id=1", "generic+" + mockServer.URL + "/?template=json&id=2", }, }, }, } ctx := context.Background() smtpServer := NewSMTPServer(config) go func() { if err := smtpServer.Start(ctx); err != nil { t.Errorf("failed to start server: %v", err) } }() time.Sleep(100 * time.Millisecond) defer func() { if err := smtpServer.Stop(ctx); err != nil { t.Errorf("failed to stop server: %v", err) } }() // Send test email auth := smtp.PlainAuth("", config.Username, config.Password, "localhost") err := smtp.SendMail( fmt.Sprintf("localhost:%d", config.Port), auth, "sender@example.com", []string{"test@example.com"}, []byte("Subject: Multiple Targets Test\r\n\r\nTest body for multiple targets"), ) require.NoError(t, err) time.Sleep(100 * time.Millisecond) // Verify both targets received notification require.Len(t, notifications, 2) require.Contains(t, notifications[0], "Test body for multiple targets") require.Contains(t, notifications[1], "Test body for multiple targets") } func TestTargetAndTargetsMerging(t *testing.T) { // Track notifications and targets hit notificationCount := 0 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } _, _ = io.ReadAll(r.Body) notificationCount++ w.WriteHeader(http.StatusOK) })) defer mockServer.Close() // Configure with both Target (deprecated) and Targets config := &Config{ Port: 2527, Username: "testuser", Password: "testpass", Recipients: []ConfigRecipient{ { Addresses: []string{"test@example.com"}, Target: "generic+" + mockServer.URL + "/?template=json&id=legacy", Targets: []string{ "generic+" + mockServer.URL + "/?template=json&id=new1", "generic+" + mockServer.URL + "/?template=json&id=new2", }, }, }, } ctx := context.Background() smtpServer := NewSMTPServer(config) go func() { if err := smtpServer.Start(ctx); err != nil { t.Errorf("failed to start server: %v", err) } }() time.Sleep(100 * time.Millisecond) defer func() { if err := smtpServer.Stop(ctx); err != nil { t.Errorf("failed to stop server: %v", err) } }() // Send test email auth := smtp.PlainAuth("", config.Username, config.Password, "localhost") err := smtp.SendMail( fmt.Sprintf("localhost:%d", config.Port), auth, "sender@example.com", []string{"test@example.com"}, []byte("Subject: Merging Test\r\n\r\nTest body"), ) require.NoError(t, err) time.Sleep(100 * time.Millisecond) // Verify all 3 targets received notification (1 from Target + 2 from Targets) require.Equal(t, 3, notificationCount) } func TestPartialTargetFailure(t *testing.T) { // Track successful notifications successCount := 0 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } _, _ = io.ReadAll(r.Body) // Fail if URL path contains "fail" if r.URL.Path == "/fail" { w.WriteHeader(http.StatusInternalServerError) return } successCount++ w.WriteHeader(http.StatusOK) })) defer mockServer.Close() // Configure with mix of good and bad targets config := &Config{ Port: 2528, Username: "testuser", Password: "testpass", Recipients: []ConfigRecipient{ { Addresses: []string{"test@example.com"}, Targets: []string{ "generic+" + mockServer.URL + "/success1?template=json", "generic+" + mockServer.URL + "/fail?template=json", "generic+" + mockServer.URL + "/success2?template=json", }, }, }, } ctx := context.Background() smtpServer := NewSMTPServer(config) go func() { if err := smtpServer.Start(ctx); err != nil { t.Errorf("failed to start server: %v", err) } }() time.Sleep(100 * time.Millisecond) defer func() { if err := smtpServer.Stop(ctx); err != nil { t.Errorf("failed to stop server: %v", err) } }() // Send test email auth := smtp.PlainAuth("", config.Username, config.Password, "localhost") err := smtp.SendMail( fmt.Sprintf("localhost:%d", config.Port), auth, "sender@example.com", []string{"test@example.com"}, []byte("Subject: Partial Failure Test\r\n\r\nTest body"), ) require.NoError(t, err) time.Sleep(100 * time.Millisecond) // Verify that 2 out of 3 targets succeeded require.Equal(t, 2, successCount) } func TestInvalidTargetURLs(t *testing.T) { // Track notifications notifications := make([]string, 0) mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } body, _ := io.ReadAll(r.Body) notifications = append(notifications, string(body)) w.WriteHeader(http.StatusOK) })) defer mockServer.Close() // Configure with invalid and valid URLs config := &Config{ Port: 2529, Username: "testuser", Password: "testpass", Recipients: []ConfigRecipient{ { Addresses: []string{"test@example.com"}, Targets: []string{ "://invalid-url", // Invalid URL "generic+" + mockServer.URL + "/?template=json&id=1", // Valid URL "ht!tp://bad url with spaces", // Invalid URL "generic+" + mockServer.URL + "/?template=json&id=2", // Valid URL }, }, }, } ctx := context.Background() smtpServer := NewSMTPServer(config) go func() { if err := smtpServer.Start(ctx); err != nil { t.Errorf("failed to start server: %v", err) } }() time.Sleep(100 * time.Millisecond) defer func() { if err := smtpServer.Stop(ctx); err != nil { t.Errorf("failed to stop server: %v", err) } }() // Send test email auth := smtp.PlainAuth("", config.Username, config.Password, "localhost") err := smtp.SendMail( fmt.Sprintf("localhost:%d", config.Port), auth, "sender@example.com", []string{"test@example.com"}, []byte("Subject: Invalid URLs Test\r\n\r\nTest body"), ) require.NoError(t, err) time.Sleep(100 * time.Millisecond) // Verify only valid URLs received notifications (2 out of 4) require.Len(t, notifications, 2) }