feat: implement email provider testing functionality and corresponding unit tests
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user