diff --git a/backend/internal/api/handlers/notification_provider_handler_test.go b/backend/internal/api/handlers/notification_provider_handler_test.go index e75de4ac..a3fcc88d 100644 --- a/backend/internal/api/handlers/notification_provider_handler_test.go +++ b/backend/internal/api/handlers/notification_provider_handler_test.go @@ -668,3 +668,35 @@ func TestNotificationProviderHandler_List_TelegramNeverExposesBotToken(t *testin _, hasTokenField := raw[0]["token"] assert.False(t, hasTokenField, "raw token field must not appear in JSON response") } + +func TestNotificationProviderHandler_Test_TelegramTokenRejected(t *testing.T) { + r, _ := setupNotificationProviderTest(t) + + payload := map[string]any{ + "type": "telegram", + "token": "bot123:TOKEN", + } + body, _ := json.Marshal(payload) + req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer(body)) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "TOKEN_WRITE_ONLY") +} + +func TestNotificationProviderHandler_Test_PushoverTokenRejected(t *testing.T) { + r, _ := setupNotificationProviderTest(t) + + payload := map[string]any{ + "type": "pushover", + "token": "app-token-abc", + } + body, _ := json.Marshal(payload) + req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer(body)) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "TOKEN_WRITE_ONLY") +} diff --git a/backend/internal/notifications/router_test.go b/backend/internal/notifications/router_test.go index 0d4ea894..4a7fa17d 100644 --- a/backend/internal/notifications/router_test.go +++ b/backend/internal/notifications/router_test.go @@ -86,3 +86,39 @@ func TestRouter_ShouldUseNotify_WebhookServiceFlag(t *testing.T) { t.Fatalf("expected notify routing disabled for webhook when FlagWebhookServiceEnabled is false") } } + +func TestRouter_ShouldUseNotify_SlackServiceFlag(t *testing.T) { + router := NewRouter() + + flags := map[string]bool{ + FlagNotifyEngineEnabled: true, + FlagSlackServiceEnabled: true, + } + + if !router.ShouldUseNotify("slack", flags) { + t.Fatalf("expected notify routing enabled for slack when FlagSlackServiceEnabled is true") + } + + flags[FlagSlackServiceEnabled] = false + if router.ShouldUseNotify("slack", flags) { + t.Fatalf("expected notify routing disabled for slack when FlagSlackServiceEnabled is false") + } +} + +func TestRouter_ShouldUseNotify_PushoverServiceFlag(t *testing.T) { + router := NewRouter() + + flags := map[string]bool{ + FlagNotifyEngineEnabled: true, + FlagPushoverServiceEnabled: true, + } + + if !router.ShouldUseNotify("pushover", flags) { + t.Fatalf("expected notify routing enabled for pushover when FlagPushoverServiceEnabled is true") + } + + flags[FlagPushoverServiceEnabled] = false + if router.ShouldUseNotify("pushover", flags) { + t.Fatalf("expected notify routing disabled for pushover when FlagPushoverServiceEnabled is false") + } +} diff --git a/backend/internal/services/notification_service_test.go b/backend/internal/services/notification_service_test.go index 17b64180..6c84b784 100644 --- a/backend/internal/services/notification_service_test.go +++ b/backend/internal/services/notification_service_test.go @@ -3849,3 +3849,32 @@ func TestIsDispatchEnabled_PushoverDisabledByFlag(t *testing.T) { assert.False(t, svc.isDispatchEnabled("pushover")) } + +func TestPushoverDispatch_DefaultBaseURL(t *testing.T) { + db := setupNotificationTestDB(t) + svc := NewNotificationService(db, nil) + // Reset the test seam to "" so the defensive 'if pushoverBase == ""' path executes, + // setting it to the production URL "https://api.pushover.net". + svc.pushoverAPIBaseURL = "" + + provider := models.NotificationProvider{ + Type: "pushover", + Token: "test-token", + URL: "test-user-key", + Template: "minimal", + } + data := map[string]any{ + "Title": "Test", + "Message": "Hello", + "Time": time.Now().Format(time.RFC3339), + "EventType": "test", + } + + // Pre-cancel the context so the HTTP send fails immediately. + // The defensive path (assigning the production base URL) still executes before any I/O. + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + err := svc.sendJSONPayload(ctx, provider, data) + require.Error(t, err) +}