Unifies the two previously independent email subsystems — MailService (net/smtp transport) and NotificationService (HTTP-based providers) — so email can participate in the notification dispatch pipeline. Key changes: - SendEmail signature updated to accept context.Context and []string recipients to enable timeout propagation and multi-recipient dispatch - NotificationService.dispatchEmail() wires MailService as a first-class provider type with IsConfigured() guard and 30s context timeout - 'email' added to isSupportedNotificationProviderType() and supportsJSONTemplates() returns false for email (plain/HTML only) - settings_handler.go test-email endpoint updated to new SendEmail API - Frontend: 'email' added to provider type union in notifications.ts, Notifications.tsx shows recipient field and hides URL/token fields for email providers - All existing tests updated to match new SendEmail signature - New tests added covering dispatchEmail paths, IsConfigured guards, recipient validation, and context timeout behaviour Also fixes confirmed false-positive CodeQL go/email-injection alerts: - smtp.SendMail, sendSSL w.Write, and sendSTARTTLS w.Write sites now carry inline codeql[go/email-injection] annotations as required by the CodeQL same-line suppression spec; preceding-line annotations silently no-op in current CodeQL versions - auth_handler.go c.SetCookie annotated for intentional Secure=false on local non-HTTPS loopback (go/cookie-secure-not-set warning only) Closes part of #800
435 lines
12 KiB
Go
435 lines
12 KiB
Go
package handlers_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"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 setupTestDB(t *testing.T) *gorm.DB {
|
|
db := handlers.OpenTestDB(t)
|
|
|
|
// Auto migrate all models that handlers depend on
|
|
_ = db.AutoMigrate(
|
|
&models.ProxyHost{},
|
|
&models.Location{},
|
|
&models.RemoteServer{},
|
|
&models.ImportSession{},
|
|
&models.Notification{},
|
|
&models.NotificationProvider{},
|
|
)
|
|
|
|
return db
|
|
}
|
|
|
|
func TestRemoteServerHandler_List(t *testing.T) {
|
|
t.Parallel()
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
|
|
// Create test server
|
|
server := &models.RemoteServer{
|
|
UUID: uuid.NewString(),
|
|
Name: "Test Server",
|
|
Provider: "docker",
|
|
Host: "localhost",
|
|
Port: 8080,
|
|
Enabled: true,
|
|
}
|
|
db.Create(server)
|
|
|
|
ns := services.NewNotificationService(db, nil)
|
|
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
|
|
router := gin.New()
|
|
handler.RegisterRoutes(router.Group("/api/v1"))
|
|
|
|
// Test List
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/remote-servers", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var servers []models.RemoteServer
|
|
err := json.Unmarshal(w.Body.Bytes(), &servers)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, servers, 1)
|
|
assert.Equal(t, "Test Server", servers[0].Name)
|
|
}
|
|
|
|
func TestRemoteServerHandler_Create(t *testing.T) {
|
|
t.Parallel()
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
|
|
ns := services.NewNotificationService(db, nil)
|
|
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
|
|
router := gin.New()
|
|
handler.RegisterRoutes(router.Group("/api/v1"))
|
|
|
|
// Test Create
|
|
serverData := map[string]any{
|
|
"name": "New Server",
|
|
"provider": "generic",
|
|
"host": "192.168.1.100",
|
|
"port": 3000,
|
|
"enabled": true,
|
|
}
|
|
body, _ := json.Marshal(serverData)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/remote-servers", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
var server models.RemoteServer
|
|
err := json.Unmarshal(w.Body.Bytes(), &server)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "New Server", server.Name)
|
|
assert.NotEmpty(t, server.UUID)
|
|
}
|
|
|
|
func TestRemoteServerHandler_TestConnection(t *testing.T) {
|
|
t.Parallel()
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
|
|
// Create test server
|
|
server := &models.RemoteServer{
|
|
UUID: uuid.NewString(),
|
|
Name: "Test Server",
|
|
Provider: "docker",
|
|
Host: "localhost",
|
|
Port: 99999, // Invalid port to test failure
|
|
Enabled: true,
|
|
}
|
|
db.Create(server)
|
|
|
|
ns := services.NewNotificationService(db, nil)
|
|
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
|
|
router := gin.New()
|
|
handler.RegisterRoutes(router.Group("/api/v1"))
|
|
|
|
// Test connection
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/remote-servers/"+server.UUID+"/test", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var result map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &result)
|
|
assert.NoError(t, err)
|
|
assert.False(t, result["reachable"].(bool))
|
|
assert.NotEmpty(t, result["error"])
|
|
}
|
|
|
|
func TestRemoteServerHandler_Get(t *testing.T) {
|
|
t.Parallel()
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
|
|
// Create test server
|
|
server := &models.RemoteServer{
|
|
UUID: uuid.NewString(),
|
|
Name: "Test Server",
|
|
Provider: "docker",
|
|
Host: "localhost",
|
|
Port: 8080,
|
|
Enabled: true,
|
|
}
|
|
db.Create(server)
|
|
|
|
ns := services.NewNotificationService(db, nil)
|
|
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
|
|
router := gin.New()
|
|
handler.RegisterRoutes(router.Group("/api/v1"))
|
|
|
|
// Test Get
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/remote-servers/"+server.UUID, http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var fetched models.RemoteServer
|
|
err := json.Unmarshal(w.Body.Bytes(), &fetched)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, server.UUID, fetched.UUID)
|
|
}
|
|
|
|
func TestRemoteServerHandler_Update(t *testing.T) {
|
|
t.Parallel()
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
|
|
// Create test server
|
|
server := &models.RemoteServer{
|
|
UUID: uuid.NewString(),
|
|
Name: "Test Server",
|
|
Provider: "docker",
|
|
Host: "localhost",
|
|
Port: 8080,
|
|
Enabled: true,
|
|
}
|
|
db.Create(server)
|
|
|
|
ns := services.NewNotificationService(db, nil)
|
|
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
|
|
router := gin.New()
|
|
handler.RegisterRoutes(router.Group("/api/v1"))
|
|
|
|
// Test Update
|
|
updateData := map[string]any{
|
|
"name": "Updated Server",
|
|
"provider": "generic",
|
|
"host": "10.0.0.1",
|
|
"port": 9000,
|
|
"enabled": false,
|
|
}
|
|
body, _ := json.Marshal(updateData)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("PUT", "/api/v1/remote-servers/"+server.UUID, bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var updated models.RemoteServer
|
|
err := json.Unmarshal(w.Body.Bytes(), &updated)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Updated Server", updated.Name)
|
|
assert.Equal(t, "generic", updated.Provider)
|
|
assert.False(t, updated.Enabled)
|
|
}
|
|
|
|
func TestRemoteServerHandler_Delete(t *testing.T) {
|
|
t.Parallel()
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
|
|
// Create test server
|
|
server := &models.RemoteServer{
|
|
UUID: uuid.NewString(),
|
|
Name: "Test Server",
|
|
Provider: "docker",
|
|
Host: "localhost",
|
|
Port: 8080,
|
|
Enabled: true,
|
|
}
|
|
db.Create(server)
|
|
|
|
ns := services.NewNotificationService(db, nil)
|
|
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
|
|
router := gin.New()
|
|
handler.RegisterRoutes(router.Group("/api/v1"))
|
|
|
|
// Test Delete
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("DELETE", "/api/v1/remote-servers/"+server.UUID, http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusNoContent, w.Code)
|
|
|
|
// Verify Delete
|
|
w2 := httptest.NewRecorder()
|
|
req2, _ := http.NewRequest("GET", "/api/v1/remote-servers/"+server.UUID, http.NoBody)
|
|
router.ServeHTTP(w2, req2)
|
|
|
|
assert.Equal(t, http.StatusNotFound, w2.Code)
|
|
}
|
|
|
|
func TestProxyHostHandler_List(t *testing.T) {
|
|
t.Parallel()
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
|
|
// Create test proxy host
|
|
host := &models.ProxyHost{
|
|
UUID: uuid.NewString(),
|
|
Name: "Test Host",
|
|
DomainNames: "test.local",
|
|
ForwardScheme: "http",
|
|
ForwardHost: "localhost",
|
|
ForwardPort: 3000,
|
|
Enabled: true,
|
|
}
|
|
db.Create(host)
|
|
|
|
ns := services.NewNotificationService(db, nil)
|
|
handler := handlers.NewProxyHostHandler(db, nil, ns, nil)
|
|
router := gin.New()
|
|
handler.RegisterRoutes(router.Group("/api/v1"))
|
|
|
|
// Test List
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/proxy-hosts", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var hosts []models.ProxyHost
|
|
err := json.Unmarshal(w.Body.Bytes(), &hosts)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, hosts, 1)
|
|
assert.Equal(t, "Test Host", hosts[0].Name)
|
|
}
|
|
|
|
func TestProxyHostHandler_Create(t *testing.T) {
|
|
t.Parallel()
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
|
|
ns := services.NewNotificationService(db, nil)
|
|
handler := handlers.NewProxyHostHandler(db, nil, ns, nil)
|
|
router := gin.New()
|
|
handler.RegisterRoutes(router.Group("/api/v1"))
|
|
|
|
// Test Create
|
|
hostData := map[string]any{
|
|
"name": "New Host",
|
|
"domain_names": "new.local",
|
|
"forward_scheme": "http",
|
|
"forward_host": "192.168.1.200",
|
|
"forward_port": 8080,
|
|
"enabled": true,
|
|
}
|
|
body, _ := json.Marshal(hostData)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/proxy-hosts", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
var host models.ProxyHost
|
|
err := json.Unmarshal(w.Body.Bytes(), &host)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "New Host", host.Name)
|
|
assert.Equal(t, "new.local", host.DomainNames)
|
|
assert.NotEmpty(t, host.UUID)
|
|
}
|
|
|
|
func TestProxyHostHandler_PartialUpdate_DoesNotWipeFields(t *testing.T) {
|
|
t.Parallel()
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
|
|
// Seed a proxy host
|
|
original := &models.ProxyHost{
|
|
UUID: uuid.NewString(),
|
|
Name: "Bazarr",
|
|
DomainNames: "bazarr.example.com",
|
|
ForwardScheme: "http",
|
|
ForwardHost: "10.0.0.20",
|
|
ForwardPort: 6767,
|
|
Enabled: true,
|
|
}
|
|
db.Create(original)
|
|
|
|
ns := services.NewNotificationService(db, nil)
|
|
handler := handlers.NewProxyHostHandler(db, nil, ns, nil)
|
|
router := gin.New()
|
|
handler.RegisterRoutes(router.Group("/api/v1"))
|
|
|
|
// Perform partial update: only toggle enabled=false
|
|
body := bytes.NewBufferString(`{"enabled": false}`)
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("PUT", "/api/v1/proxy-hosts/"+original.UUID, body)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var updated models.ProxyHost
|
|
err := json.Unmarshal(w.Body.Bytes(), &updated)
|
|
assert.NoError(t, err)
|
|
|
|
// Validate that only 'enabled' changed; other fields remain intact
|
|
assert.Equal(t, false, updated.Enabled)
|
|
assert.Equal(t, "Bazarr", updated.Name)
|
|
assert.Equal(t, "bazarr.example.com", updated.DomainNames)
|
|
assert.Equal(t, "http", updated.ForwardScheme)
|
|
assert.Equal(t, "10.0.0.20", updated.ForwardHost)
|
|
assert.Equal(t, 6767, updated.ForwardPort)
|
|
|
|
// Fetch via GET to ensure DB persisted state correctly
|
|
w2 := httptest.NewRecorder()
|
|
req2, _ := http.NewRequest("GET", "/api/v1/proxy-hosts/"+original.UUID, http.NoBody)
|
|
router.ServeHTTP(w2, req2)
|
|
assert.Equal(t, http.StatusOK, w2.Code)
|
|
|
|
var fetched models.ProxyHost
|
|
err = json.Unmarshal(w2.Body.Bytes(), &fetched)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, false, fetched.Enabled)
|
|
assert.Equal(t, "Bazarr", fetched.Name)
|
|
assert.Equal(t, "bazarr.example.com", fetched.DomainNames)
|
|
assert.Equal(t, 6767, fetched.ForwardPort)
|
|
}
|
|
|
|
func TestHealthHandler(t *testing.T) {
|
|
t.Parallel()
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
router := gin.New()
|
|
router.GET("/health", handlers.HealthHandler)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/health", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var result map[string]string
|
|
err := json.Unmarshal(w.Body.Bytes(), &result)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "ok", result["status"])
|
|
}
|
|
|
|
func TestRemoteServerHandler_Errors(t *testing.T) {
|
|
t.Parallel()
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
|
|
ns := services.NewNotificationService(db, nil)
|
|
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
|
|
router := gin.New()
|
|
handler.RegisterRoutes(router.Group("/api/v1"))
|
|
|
|
// Get non-existent
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/remote-servers/non-existent", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
|
|
// Update non-existent
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("PUT", "/api/v1/remote-servers/non-existent", strings.NewReader(`{}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
|
|
// Delete non-existent
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("DELETE", "/api/v1/remote-servers/non-existent", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
}
|