feat: implement email provider testing functionality and corresponding unit tests

This commit is contained in:
GitHub Actions
2026-03-08 16:14:08 +00:00
parent 3201830405
commit d1baf6f1b0
4 changed files with 228 additions and 0 deletions
@@ -306,6 +306,23 @@ func (h *NotificationProviderHandler) Test(c *gin.Context) {
return
}
// Email providers use global SMTP + recipients from the URL field; they don't require a saved provider ID.
if providerType == "email" {
provider := models.NotificationProvider{
ID: strings.TrimSpace(req.ID),
Name: req.Name,
Type: req.Type,
URL: req.URL,
}
if err := h.service.TestEmailProvider(provider); err != nil {
code, category, message := classifyProviderTestFailure(err)
respondSanitizedProviderError(c, http.StatusBadRequest, code, category, message)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Test notification sent"})
return
}
providerID := strings.TrimSpace(req.ID)
if providerID == "" {
respondSanitizedProviderError(c, http.StatusBadRequest, "MISSING_PROVIDER_ID", "validation", "Trusted provider ID is required for test dispatch")
@@ -510,3 +510,74 @@ func TestNotificationProviderHandler_Create_ResponseHasHasToken(t *testing.T) {
assert.Equal(t, true, raw["has_token"])
assert.NotContains(t, w.Body.String(), "app-token-123")
}
func TestNotificationProviderHandler_Test_Email_NoMailService_Returns400(t *testing.T) {
r, _ := setupNotificationProviderTest(t)
// mailService is nil in test setup — email test should return 400 (not MISSING_PROVIDER_ID)
payload := map[string]interface{}{
"type": "email",
"url": "user@example.com",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestNotificationProviderHandler_Test_Email_EmptyURL_Returns400(t *testing.T) {
r, _ := setupNotificationProviderTest(t)
payload := map[string]interface{}{
"type": "email",
"url": "",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestNotificationProviderHandler_Test_Email_DoesNotRequireProviderID(t *testing.T) {
r, _ := setupNotificationProviderTest(t)
// No ID field — email path must not return MISSING_PROVIDER_ID
payload := map[string]interface{}{
"type": "email",
"url": "user@example.com",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
var resp map[string]interface{}
_ = json.Unmarshal(w.Body.Bytes(), &resp)
assert.NotEqual(t, "MISSING_PROVIDER_ID", resp["code"])
}
func TestNotificationProviderHandler_Test_NonEmail_StillRequiresProviderID(t *testing.T) {
r, _ := setupNotificationProviderTest(t)
payload := map[string]interface{}{
"type": "discord",
"url": "https://discord.com/api/webhooks/123/abc",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
var resp map[string]interface{}
_ = json.Unmarshal(w.Body.Bytes(), &resp)
assert.Equal(t, "MISSING_PROVIDER_ID", resp["code"])
}
@@ -560,6 +560,37 @@ func (s *NotificationService) TestProvider(provider models.NotificationProvider)
return s.sendJSONPayload(context.Background(), provider, data)
}
// TestEmailProvider sends a test email to the recipients configured in provider.URL.
// It bypasses the JSON-template path used by TestProvider and uses the SMTP mail service directly.
func (s *NotificationService) TestEmailProvider(provider models.NotificationProvider) error {
if s.mailService == nil || !s.mailService.IsConfigured() {
return fmt.Errorf("email service is not configured; configure SMTP settings before testing email providers")
}
rawRecipients := strings.Split(provider.URL, ",")
recipients := make([]string, 0, len(rawRecipients))
for _, r := range rawRecipients {
if trimmed := strings.TrimSpace(r); trimmed != "" {
recipients = append(recipients, trimmed)
}
}
if len(recipients) == 0 {
return fmt.Errorf("no recipients configured; add at least one recipient email address")
}
data := EmailTemplateData{
EventType: "test",
Title: "Test Notification",
Message: "This is a test notification from Charon. If you received this email, your email notification provider is configured correctly.",
Timestamp: time.Now().Format(time.RFC3339),
}
htmlBody, renderErr := s.mailService.RenderNotificationEmail("email_system_event.html", data)
if renderErr != nil {
htmlBody = "<strong>Test Notification</strong><br>This is a test notification from Charon. If you received this email, your email notification provider is configured correctly."
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return s.mailService.SendEmail(ctx, recipients, "[Charon Test] Test Notification", htmlBody)
}
// ListTemplates returns all external notification templates stored in the database.
func (s *NotificationService) ListTemplates() ([]models.NotificationTemplate, error) {
var list []models.NotificationTemplate
@@ -2879,3 +2879,112 @@ func TestDispatchEmail_TemplateFallback(t *testing.T) {
assert.Contains(t, mock.calls[0].body, "<strong>Fallback Title</strong>")
assert.Contains(t, mock.calls[0].body, "Fallback Message")
}
// --- TestEmailProvider unit tests ---
func TestEmailProvider_MailServiceNil(t *testing.T) {
db := setupNotificationTestDB(t)
svc := NewNotificationService(db, nil)
p := models.NotificationProvider{Name: "test-email", Type: "email", URL: "a@b.com"}
err := svc.TestEmailProvider(p)
require.Error(t, err)
assert.Contains(t, err.Error(), "email service is not configured")
}
func TestEmailProvider_MailServiceNotConfigured(t *testing.T) {
db := setupNotificationTestDB(t)
mock := &mockMailService{isConfigured: false}
svc := NewNotificationService(db, mock)
p := models.NotificationProvider{Name: "test-email", Type: "email", URL: "a@b.com"}
err := svc.TestEmailProvider(p)
require.Error(t, err)
assert.Contains(t, err.Error(), "email service is not configured")
}
func TestEmailProvider_EmptyURL(t *testing.T) {
db := setupNotificationTestDB(t)
mock := &mockMailService{isConfigured: true}
svc := NewNotificationService(db, mock)
p := models.NotificationProvider{Name: "test-email", Type: "email", URL: ""}
err := svc.TestEmailProvider(p)
require.Error(t, err)
assert.Contains(t, err.Error(), "no recipients configured")
assert.Zero(t, mock.callCount())
}
func TestEmailProvider_BlankWhitespaceURL(t *testing.T) {
db := setupNotificationTestDB(t)
mock := &mockMailService{isConfigured: true}
svc := NewNotificationService(db, mock)
p := models.NotificationProvider{Name: "test-email", Type: "email", URL: " , , "}
err := svc.TestEmailProvider(p)
require.Error(t, err)
assert.Contains(t, err.Error(), "no recipients configured")
}
func TestEmailProvider_ValidRecipient(t *testing.T) {
db := setupNotificationTestDB(t)
mock := &mockMailService{isConfigured: true}
svc := NewNotificationService(db, mock)
p := models.NotificationProvider{Name: "test-email", Type: "email", URL: "user@example.com"}
err := svc.TestEmailProvider(p)
require.NoError(t, err)
require.Equal(t, 1, mock.callCount())
call := mock.firstCall()
assert.Equal(t, []string{"user@example.com"}, call.to)
assert.Equal(t, "[Charon Test] Test Notification", call.subject)
}
func TestEmailProvider_MultipleRecipients(t *testing.T) {
db := setupNotificationTestDB(t)
mock := &mockMailService{isConfigured: true}
svc := NewNotificationService(db, mock)
p := models.NotificationProvider{Name: "test-email", Type: "email", URL: "a@b.com, c@d.com , e@f.com"}
err := svc.TestEmailProvider(p)
require.NoError(t, err)
require.Equal(t, 1, mock.callCount())
assert.Equal(t, []string{"a@b.com", "c@d.com", "e@f.com"}, mock.firstCall().to)
}
func TestEmailProvider_SendError(t *testing.T) {
db := setupNotificationTestDB(t)
mock := &mockMailService{isConfigured: true, sendEmailErr: fmt.Errorf("smtp: connection refused")}
svc := NewNotificationService(db, mock)
p := models.NotificationProvider{Name: "test-email", Type: "email", URL: "a@b.com"}
err := svc.TestEmailProvider(p)
require.Error(t, err)
assert.Contains(t, err.Error(), "smtp")
assert.Equal(t, 1, mock.callCount())
}
func TestEmailProvider_TemplateFallback(t *testing.T) {
db := setupNotificationTestDB(t)
mock := &mockMailService{isConfigured: true, renderErr: fmt.Errorf("template not found")}
svc := NewNotificationService(db, mock)
p := models.NotificationProvider{Name: "test-email", Type: "email", URL: "a@b.com"}
err := svc.TestEmailProvider(p)
require.NoError(t, err)
require.Equal(t, 1, mock.callCount())
assert.Contains(t, mock.firstCall().body, "<strong>Test Notification</strong>")
}
func TestEmailProvider_UsesRenderedTemplate(t *testing.T) {
db := setupNotificationTestDB(t)
rendered := "<html><body>Rendered test email</body></html>"
mock := &mockMailService{isConfigured: true, renderResult: rendered}
svc := NewNotificationService(db, mock)
p := models.NotificationProvider{Name: "test-email", Type: "email", URL: "a@b.com"}
err := svc.TestEmailProvider(p)
require.NoError(t, err)
require.Equal(t, 1, mock.callCount())
assert.Equal(t, rendered, mock.firstCall().body)
}