703 lines
22 KiB
Go
703 lines
22 KiB
Go
package handlers_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/api/handlers"
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
"github.com/Wikid82/charon/backend/internal/services"
|
|
)
|
|
|
|
func setupNotificationProviderTest(t *testing.T) (*gin.Engine, *gorm.DB) {
|
|
t.Helper()
|
|
db := handlers.OpenTestDB(t)
|
|
require.NoError(t, db.AutoMigrate(&models.NotificationProvider{}, &models.Notification{}))
|
|
|
|
service := services.NewNotificationService(db, nil)
|
|
handler := handlers.NewNotificationProviderHandler(service)
|
|
|
|
r := gin.Default()
|
|
r.Use(func(c *gin.Context) {
|
|
c.Set("role", "admin")
|
|
c.Set("userID", uint(1))
|
|
c.Next()
|
|
})
|
|
api := r.Group("/api/v1")
|
|
providers := api.Group("/notifications/providers")
|
|
providers.GET("", handler.List)
|
|
providers.POST("/preview", handler.Preview)
|
|
providers.POST("", handler.Create)
|
|
providers.PUT("/:id", handler.Update)
|
|
providers.DELETE("/:id", handler.Delete)
|
|
providers.POST("/test", handler.Test)
|
|
api.GET("/notifications/templates", handler.Templates)
|
|
|
|
return r, db
|
|
}
|
|
|
|
func TestNotificationProviderHandler_CRUD(t *testing.T) {
|
|
r, db := setupNotificationProviderTest(t)
|
|
|
|
// 1. Create
|
|
provider := models.NotificationProvider{
|
|
Name: "Test Discord",
|
|
Type: "discord",
|
|
URL: "https://discord.com/api/webhooks/...",
|
|
}
|
|
body, _ := json.Marshal(provider)
|
|
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer(body))
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
var created models.NotificationProvider
|
|
err := json.Unmarshal(w.Body.Bytes(), &created)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, provider.Name, created.Name)
|
|
assert.NotEmpty(t, created.ID)
|
|
|
|
// 2. List
|
|
req, _ = http.NewRequest("GET", "/api/v1/notifications/providers", http.NoBody)
|
|
w = httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var list []models.NotificationProvider
|
|
err = json.Unmarshal(w.Body.Bytes(), &list)
|
|
require.NoError(t, err)
|
|
assert.Len(t, list, 1)
|
|
|
|
// 3. Update
|
|
created.Name = "Updated Discord"
|
|
body, _ = json.Marshal(created)
|
|
req, _ = http.NewRequest("PUT", "/api/v1/notifications/providers/"+created.ID, bytes.NewBuffer(body))
|
|
w = httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var updated models.NotificationProvider
|
|
err = json.Unmarshal(w.Body.Bytes(), &updated)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Updated Discord", updated.Name)
|
|
|
|
// Verify in DB
|
|
var dbProvider models.NotificationProvider
|
|
db.First(&dbProvider, "id = ?", created.ID)
|
|
assert.Equal(t, "Updated Discord", dbProvider.Name)
|
|
|
|
// 4. Delete
|
|
req, _ = http.NewRequest("DELETE", "/api/v1/notifications/providers/"+created.ID, http.NoBody)
|
|
w = httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Verify Delete
|
|
var count int64
|
|
db.Model(&models.NotificationProvider{}).Count(&count)
|
|
assert.Equal(t, int64(0), count)
|
|
}
|
|
|
|
func TestNotificationProviderHandler_Templates(t *testing.T) {
|
|
r, _ := setupNotificationProviderTest(t)
|
|
|
|
req, _ := http.NewRequest("GET", "/api/v1/notifications/templates", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var templates []map[string]string
|
|
err := json.Unmarshal(w.Body.Bytes(), &templates)
|
|
require.NoError(t, err)
|
|
assert.Len(t, templates, 3)
|
|
}
|
|
|
|
func TestNotificationProviderHandler_Test(t *testing.T) {
|
|
r, db := setupNotificationProviderTest(t)
|
|
|
|
stored := models.NotificationProvider{
|
|
ID: "trusted-provider-id",
|
|
Name: "Stored Provider",
|
|
Type: "discord",
|
|
URL: "invalid-url",
|
|
Enabled: true,
|
|
}
|
|
require.NoError(t, db.Create(&stored).Error)
|
|
|
|
payload := map[string]any{
|
|
"id": stored.ID,
|
|
"type": "discord",
|
|
"url": "https://discord.com/api/webhooks/123/override",
|
|
}
|
|
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(), "PROVIDER_TEST_URL_INVALID")
|
|
}
|
|
|
|
func TestNotificationProviderHandler_Test_RequiresTrustedProviderID(t *testing.T) {
|
|
r, _ := setupNotificationProviderTest(t)
|
|
|
|
payload := map[string]any{
|
|
"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))
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
assert.Contains(t, w.Body.String(), "MISSING_PROVIDER_ID")
|
|
}
|
|
|
|
func TestNotificationProviderHandler_Test_ReturnsNotFoundForUnknownProvider(t *testing.T) {
|
|
r, _ := setupNotificationProviderTest(t)
|
|
|
|
payload := map[string]any{
|
|
"id": "missing-provider-id",
|
|
}
|
|
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.StatusNotFound, w.Code)
|
|
assert.Contains(t, w.Body.String(), "PROVIDER_NOT_FOUND")
|
|
}
|
|
|
|
func TestNotificationProviderHandler_Errors(t *testing.T) {
|
|
r, _ := setupNotificationProviderTest(t)
|
|
|
|
// Create Invalid JSON
|
|
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer([]byte("invalid")))
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
|
|
// Update Invalid JSON
|
|
req, _ = http.NewRequest("PUT", "/api/v1/notifications/providers/123", bytes.NewBuffer([]byte("invalid")))
|
|
w = httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
|
|
// Test Invalid JSON
|
|
req, _ = http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer([]byte("invalid")))
|
|
w = httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestNotificationProviderHandler_InvalidCustomTemplate_Rejects(t *testing.T) {
|
|
r, _ := setupNotificationProviderTest(t)
|
|
|
|
// Create with invalid custom template should return 400
|
|
provider := models.NotificationProvider{
|
|
Name: "Bad",
|
|
Type: "discord",
|
|
URL: "https://discord.com/api/webhooks/123/abc",
|
|
Template: "custom",
|
|
Config: `{"broken": "{{.Title"}`,
|
|
}
|
|
body, _ := json.Marshal(provider)
|
|
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer(body))
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
|
|
// Create valid and then attempt update to invalid custom template
|
|
provider = models.NotificationProvider{
|
|
Name: "Good",
|
|
Type: "discord",
|
|
URL: "https://discord.com/api/webhooks/456/def",
|
|
Template: "minimal",
|
|
}
|
|
body, _ = json.Marshal(provider)
|
|
req, _ = http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer(body))
|
|
w = httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
var created models.NotificationProvider
|
|
_ = json.Unmarshal(w.Body.Bytes(), &created)
|
|
|
|
created.Template = "custom"
|
|
created.Config = `{"broken": "{{.Title"}`
|
|
body, _ = json.Marshal(created)
|
|
req, _ = http.NewRequest("PUT", "/api/v1/notifications/providers/"+created.ID, bytes.NewBuffer(body))
|
|
w = httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestNotificationProviderHandler_Preview(t *testing.T) {
|
|
r, _ := setupNotificationProviderTest(t)
|
|
|
|
// Minimal template preview
|
|
provider := models.NotificationProvider{
|
|
Type: "discord",
|
|
URL: "https://discord.com/api/webhooks/123/abc",
|
|
Template: "minimal",
|
|
}
|
|
body, _ := json.Marshal(provider)
|
|
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/preview", bytes.NewBuffer(body))
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var resp map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, resp, "rendered")
|
|
assert.Contains(t, resp, "parsed")
|
|
|
|
// Invalid template should not succeed
|
|
provider.Config = `{"broken": "{{.Title"}`
|
|
provider.Template = "custom"
|
|
body, _ = json.Marshal(provider)
|
|
req, _ = http.NewRequest("POST", "/api/v1/notifications/providers/preview", bytes.NewBuffer(body))
|
|
w = httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestNotificationProviderHandler_CreateRejectsDiscordIPHost(t *testing.T) {
|
|
r, _ := setupNotificationProviderTest(t)
|
|
|
|
provider := models.NotificationProvider{
|
|
Name: "Discord IP",
|
|
Type: "discord",
|
|
URL: "https://203.0.113.10/api/webhooks/123456/token_abc",
|
|
}
|
|
body, _ := json.Marshal(provider)
|
|
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer(body))
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
assert.Contains(t, w.Body.String(), "PROVIDER_VALIDATION_FAILED")
|
|
assert.Contains(t, w.Body.String(), "validation")
|
|
}
|
|
|
|
func TestNotificationProviderHandler_CreateAcceptsDiscordHostname(t *testing.T) {
|
|
r, _ := setupNotificationProviderTest(t)
|
|
|
|
provider := models.NotificationProvider{
|
|
Name: "Discord Host",
|
|
Type: "discord",
|
|
URL: "https://discord.com/api/webhooks/123456/token_abc",
|
|
}
|
|
body, _ := json.Marshal(provider)
|
|
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer(body))
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
}
|
|
|
|
func TestNotificationProviderHandler_CreateIgnoresServerManagedMigrationFields(t *testing.T) {
|
|
r, db := setupNotificationProviderTest(t)
|
|
|
|
payload := map[string]any{
|
|
"name": "Create Ignore Migration",
|
|
"type": "discord",
|
|
"url": "https://discord.com/api/webhooks/123/abc",
|
|
"template": "minimal",
|
|
"enabled": true,
|
|
"notify_proxy_hosts": true,
|
|
"notify_remote_servers": true,
|
|
"notify_domains": true,
|
|
"notify_certs": true,
|
|
"notify_uptime": true,
|
|
"engine": "notify_v1",
|
|
"service_config": `{"token":"attacker"}`,
|
|
"migration_state": "migrated",
|
|
"migration_error": "client-value",
|
|
"legacy_url": "https://malicious.example",
|
|
"last_migrated_at": "2020-01-01T00:00:00Z",
|
|
"id": "client-controlled-id",
|
|
}
|
|
|
|
body, _ := json.Marshal(payload)
|
|
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
var created models.NotificationProvider
|
|
err := json.Unmarshal(w.Body.Bytes(), &created)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, created.ID)
|
|
assert.NotEqual(t, "client-controlled-id", created.ID)
|
|
|
|
var dbProvider models.NotificationProvider
|
|
err = db.First(&dbProvider, "id = ?", created.ID).Error
|
|
require.NoError(t, err)
|
|
assert.Empty(t, dbProvider.Engine)
|
|
assert.Empty(t, dbProvider.ServiceConfig)
|
|
assert.Empty(t, dbProvider.MigrationState)
|
|
assert.Empty(t, dbProvider.MigrationError)
|
|
assert.Empty(t, dbProvider.LegacyURL)
|
|
assert.Nil(t, dbProvider.LastMigratedAt)
|
|
}
|
|
|
|
func TestNotificationProviderHandler_UpdatePreservesServerManagedMigrationFields(t *testing.T) {
|
|
r, db := setupNotificationProviderTest(t)
|
|
|
|
now := time.Now().UTC().Round(time.Second)
|
|
original := models.NotificationProvider{
|
|
Name: "Original",
|
|
Type: "discord",
|
|
URL: "https://discord.com/api/webhooks/123/abc",
|
|
Template: "minimal",
|
|
Enabled: true,
|
|
NotifyProxyHosts: true,
|
|
NotifyRemoteServers: true,
|
|
NotifyDomains: true,
|
|
NotifyCerts: true,
|
|
NotifyUptime: true,
|
|
Engine: "notify_v1",
|
|
ServiceConfig: `{"token":"server"}`,
|
|
MigrationState: "migrated",
|
|
MigrationError: "",
|
|
LegacyURL: "discord://legacy",
|
|
LastMigratedAt: &now,
|
|
}
|
|
require.NoError(t, db.Create(&original).Error)
|
|
|
|
payload := map[string]any{
|
|
"name": "Updated Name",
|
|
"type": "discord",
|
|
"url": "https://discord.com/api/webhooks/456/def",
|
|
"template": "minimal",
|
|
"enabled": false,
|
|
"notify_proxy_hosts": false,
|
|
"notify_remote_servers": false,
|
|
"notify_domains": false,
|
|
"notify_certs": false,
|
|
"notify_uptime": false,
|
|
"engine": "legacy",
|
|
"service_config": `{"token":"client-overwrite"}`,
|
|
"migration_state": "failed",
|
|
"migration_error": "client-error",
|
|
"legacy_url": "https://attacker.example",
|
|
"last_migrated_at": "1999-01-01T00:00:00Z",
|
|
}
|
|
|
|
body, _ := json.Marshal(payload)
|
|
req, _ := http.NewRequest("PUT", "/api/v1/notifications/providers/"+original.ID, bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var dbProvider models.NotificationProvider
|
|
require.NoError(t, db.First(&dbProvider, "id = ?", original.ID).Error)
|
|
assert.Equal(t, "Updated Name", dbProvider.Name)
|
|
assert.Equal(t, "notify_v1", dbProvider.Engine)
|
|
assert.Equal(t, `{"token":"server"}`, dbProvider.ServiceConfig)
|
|
assert.Equal(t, "migrated", dbProvider.MigrationState)
|
|
assert.Equal(t, "", dbProvider.MigrationError)
|
|
assert.Equal(t, "discord://legacy", dbProvider.LegacyURL)
|
|
require.NotNil(t, dbProvider.LastMigratedAt)
|
|
assert.Equal(t, now, dbProvider.LastMigratedAt.UTC().Round(time.Second))
|
|
}
|
|
|
|
func TestNotificationProviderHandler_List_ReturnsHasTokenTrue(t *testing.T) {
|
|
r, db := setupNotificationProviderTest(t)
|
|
|
|
p := models.NotificationProvider{
|
|
ID: "tok-true",
|
|
Name: "Gotify With Token",
|
|
Type: "gotify",
|
|
URL: "https://gotify.example.com",
|
|
Token: "secret-app-token",
|
|
}
|
|
require.NoError(t, db.Create(&p).Error)
|
|
|
|
req, _ := http.NewRequest("GET", "/api/v1/notifications/providers", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var raw []map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &raw))
|
|
require.Len(t, raw, 1)
|
|
assert.Equal(t, true, raw[0]["has_token"])
|
|
}
|
|
|
|
func TestNotificationProviderHandler_List_ReturnsHasTokenFalse(t *testing.T) {
|
|
r, db := setupNotificationProviderTest(t)
|
|
|
|
p := models.NotificationProvider{
|
|
ID: "tok-false",
|
|
Name: "Discord No Token",
|
|
Type: "discord",
|
|
URL: "https://discord.com/api/webhooks/123/abc",
|
|
}
|
|
require.NoError(t, db.Create(&p).Error)
|
|
|
|
req, _ := http.NewRequest("GET", "/api/v1/notifications/providers", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var raw []map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &raw))
|
|
require.Len(t, raw, 1)
|
|
assert.Equal(t, false, raw[0]["has_token"])
|
|
}
|
|
|
|
func TestNotificationProviderHandler_List_NeverExposesRawToken(t *testing.T) {
|
|
r, db := setupNotificationProviderTest(t)
|
|
|
|
p := models.NotificationProvider{
|
|
ID: "tok-hidden",
|
|
Name: "Secret Gotify",
|
|
Type: "gotify",
|
|
URL: "https://gotify.example.com",
|
|
Token: "super-secret-value",
|
|
}
|
|
require.NoError(t, db.Create(&p).Error)
|
|
|
|
req, _ := http.NewRequest("GET", "/api/v1/notifications/providers", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.NotContains(t, w.Body.String(), "super-secret-value")
|
|
|
|
var raw []map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &raw))
|
|
require.Len(t, raw, 1)
|
|
_, hasTokenField := raw[0]["token"]
|
|
assert.False(t, hasTokenField, "raw token field must not appear in JSON response")
|
|
}
|
|
|
|
func TestNotificationProviderHandler_Create_ResponseHasHasToken(t *testing.T) {
|
|
r, _ := setupNotificationProviderTest(t)
|
|
|
|
payload := map[string]interface{}{
|
|
"name": "New Gotify",
|
|
"type": "gotify",
|
|
"url": "https://gotify.example.com",
|
|
"token": "app-token-123",
|
|
"template": "minimal",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
var raw map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &raw))
|
|
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"])
|
|
}
|
|
|
|
func TestNotificationProviderHandler_Create_Telegram(t *testing.T) {
|
|
r, _ := setupNotificationProviderTest(t)
|
|
|
|
payload := map[string]interface{}{
|
|
"name": "My Telegram Bot",
|
|
"type": "telegram",
|
|
"url": "123456789",
|
|
"token": "bot123456789:ABCdefGHIjklMNOpqrSTUvwxYZ",
|
|
"template": "minimal",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
var raw map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &raw))
|
|
assert.Equal(t, "telegram", raw["type"])
|
|
assert.Equal(t, true, raw["has_token"])
|
|
// Token must never appear in response
|
|
assert.NotContains(t, w.Body.String(), "bot123456789:ABCdefGHIjklMNOpqrSTUvwxYZ")
|
|
}
|
|
|
|
func TestNotificationProviderHandler_Update_TelegramTokenPreservation(t *testing.T) {
|
|
r, db := setupNotificationProviderTest(t)
|
|
|
|
p := models.NotificationProvider{
|
|
ID: "tg-preserve",
|
|
Name: "Telegram Bot",
|
|
Type: "telegram",
|
|
URL: "123456789",
|
|
Token: "original-bot-token",
|
|
}
|
|
require.NoError(t, db.Create(&p).Error)
|
|
|
|
// Update without token — token should be preserved
|
|
payload := map[string]interface{}{
|
|
"name": "Updated Telegram Bot",
|
|
"type": "telegram",
|
|
"url": "987654321",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req, _ := http.NewRequest("PUT", "/api/v1/notifications/providers/tg-preserve", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Verify token was preserved in DB
|
|
var dbProvider models.NotificationProvider
|
|
require.NoError(t, db.Where("id = ?", "tg-preserve").First(&dbProvider).Error)
|
|
assert.Equal(t, "original-bot-token", dbProvider.Token)
|
|
assert.Equal(t, "987654321", dbProvider.URL)
|
|
}
|
|
|
|
func TestNotificationProviderHandler_List_TelegramNeverExposesBotToken(t *testing.T) {
|
|
r, db := setupNotificationProviderTest(t)
|
|
|
|
p := models.NotificationProvider{
|
|
ID: "tg-secret",
|
|
Name: "Secret Telegram",
|
|
Type: "telegram",
|
|
URL: "123456789",
|
|
Token: "bot999:SECRETTOKEN",
|
|
}
|
|
require.NoError(t, db.Create(&p).Error)
|
|
|
|
req, _ := http.NewRequest("GET", "/api/v1/notifications/providers", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.NotContains(t, w.Body.String(), "bot999:SECRETTOKEN")
|
|
assert.NotContains(t, w.Body.String(), "api.telegram.org")
|
|
|
|
var raw []map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &raw))
|
|
require.Len(t, raw, 1)
|
|
assert.Equal(t, true, raw[0]["has_token"])
|
|
_, 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")
|
|
}
|