1325 lines
41 KiB
Go
1325 lines
41 KiB
Go
package routes
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/config"
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func materializeRoutePath(path string) string {
|
|
segments := strings.Split(path, "/")
|
|
for i, segment := range segments {
|
|
if strings.HasPrefix(segment, ":") {
|
|
segments[i] = "1"
|
|
}
|
|
}
|
|
return strings.Join(segments, "/")
|
|
}
|
|
|
|
func TestRegister(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
// Use in-memory DB
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{
|
|
JWTSecret: "test-secret",
|
|
}
|
|
|
|
err = Register(router, db, cfg)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify some routes are registered
|
|
routes := router.Routes()
|
|
assert.NotEmpty(t, routes)
|
|
|
|
foundHealth := false
|
|
for _, r := range routes {
|
|
if r.Path == "/api/v1/health" {
|
|
foundHealth = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, foundHealth, "Health route should be registered")
|
|
}
|
|
|
|
func TestRegister_WithDevelopmentEnvironment(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_dev_env"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{
|
|
JWTSecret: "test-secret",
|
|
Environment: "development",
|
|
}
|
|
|
|
err = Register(router, db, cfg)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestRegister_WithProductionEnvironment(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_prod_env"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{
|
|
JWTSecret: "test-secret",
|
|
Environment: "production",
|
|
}
|
|
|
|
err = Register(router, db, cfg)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestRegister_AutoMigrateFailure(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
// Open a valid connection then close it to simulate migration failure
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_migrate_fail"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
// Close underlying SQL connection to force migration failure
|
|
sqlDB, err := db.DB()
|
|
require.NoError(t, err)
|
|
_ = sqlDB.Close()
|
|
|
|
cfg := config.Config{
|
|
JWTSecret: "test-secret",
|
|
}
|
|
|
|
err = Register(router, db, cfg)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "auto migrate")
|
|
}
|
|
|
|
func TestRegisterImportHandler(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_import"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
// RegisterImportHandler should not panic
|
|
RegisterImportHandler(router, db, cfg, "/usr/bin/caddy", "/tmp/imports", "/tmp/mount")
|
|
|
|
// Verify import routes exist
|
|
routes := router.Routes()
|
|
hasImportRoute := false
|
|
for _, r := range routes {
|
|
// Import routes are: /api/v1/import/status, /api/v1/import/preview, etc.
|
|
if r.Path == "/api/v1/import/status" || r.Path == "/api/v1/import/preview" || r.Path == "/api/v1/import/upload" {
|
|
hasImportRoute = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasImportRoute, "Import routes should be registered")
|
|
}
|
|
|
|
func TestRegister_RoutesRegistration(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{
|
|
JWTSecret: "test-secret",
|
|
}
|
|
|
|
err = Register(router, db, cfg)
|
|
require.NoError(t, err)
|
|
|
|
routes := router.Routes()
|
|
|
|
// Verify key routes are registered
|
|
expectedRoutes := []string{
|
|
"/api/v1/health",
|
|
"/metrics",
|
|
"/api/v1/auth/login",
|
|
"/api/v1/auth/register",
|
|
"/api/v1/setup",
|
|
}
|
|
|
|
routeMap := make(map[string]bool)
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = true
|
|
}
|
|
|
|
for _, expected := range expectedRoutes {
|
|
assert.True(t, routeMap[expected], "Route %s should be registered", expected)
|
|
}
|
|
}
|
|
|
|
func TestRegister_ProxyHostsRequireAuth(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
// Use in-memory DB
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_proxyhosts_auth"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/proxy-hosts", strings.NewReader(`{}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
assert.Contains(t, w.Body.String(), "Authorization header required")
|
|
}
|
|
|
|
func TestRegister_StateChangingRoutesDenyByDefaultWithExplicitAllowlist(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_mutation_auth_guard"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
mutatingMethods := map[string]bool{
|
|
http.MethodPost: true,
|
|
http.MethodPut: true,
|
|
http.MethodPatch: true,
|
|
http.MethodDelete: true,
|
|
}
|
|
|
|
publicMutationAllowlist := map[string]bool{
|
|
http.MethodPost + " /api/v1/auth/login": true,
|
|
http.MethodPost + " /api/v1/auth/register": true,
|
|
http.MethodPost + " /api/v1/setup": true,
|
|
http.MethodPost + " /api/v1/invite/accept": true,
|
|
http.MethodPost + " /api/v1/security/events": true,
|
|
http.MethodPost + " /api/v1/emergency/security-reset": true,
|
|
}
|
|
|
|
for _, route := range router.Routes() {
|
|
if !strings.HasPrefix(route.Path, "/api/v1/") {
|
|
continue
|
|
}
|
|
if !mutatingMethods[route.Method] {
|
|
continue
|
|
}
|
|
|
|
key := route.Method + " " + route.Path
|
|
if publicMutationAllowlist[key] {
|
|
continue
|
|
}
|
|
|
|
requestPath := materializeRoutePath(route.Path)
|
|
var body io.Reader = http.NoBody
|
|
if route.Method == http.MethodPost || route.Method == http.MethodPut || route.Method == http.MethodPatch {
|
|
body = strings.NewReader("{}")
|
|
}
|
|
|
|
req := httptest.NewRequest(route.Method, requestPath, body)
|
|
if route.Method == http.MethodPost || route.Method == http.MethodPut || route.Method == http.MethodPatch {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Contains(
|
|
t,
|
|
[]int{http.StatusUnauthorized, http.StatusForbidden},
|
|
w.Code,
|
|
"state-changing endpoint must deny unauthenticated access unless explicitly allowlisted: %s (materialized path: %s)",
|
|
key,
|
|
requestPath,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestRegister_DNSProviders_NotRegisteredWhenEncryptionKeyMissing(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_dnsproviders_missing"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret", EncryptionKey: ""}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
for _, r := range router.Routes() {
|
|
assert.NotContains(t, r.Path, "/api/v1/dns-providers")
|
|
}
|
|
}
|
|
|
|
func TestRegister_DNSProviders_NotRegisteredWhenEncryptionKeyInvalid(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_dnsproviders_invalid"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret", EncryptionKey: "not-base64"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
for _, r := range router.Routes() {
|
|
assert.NotContains(t, r.Path, "/api/v1/dns-providers")
|
|
}
|
|
}
|
|
|
|
func TestRegister_DNSProviders_RegisteredWhenEncryptionKeyValid(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_dnsproviders_valid"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
// 32-byte all-zero key in base64
|
|
cfg := config.Config{JWTSecret: "test-secret", EncryptionKey: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
paths := make(map[string]bool)
|
|
for _, r := range router.Routes() {
|
|
paths[r.Path] = true
|
|
}
|
|
|
|
assert.True(t, paths["/api/v1/dns-providers"], "dns providers list route should be registered")
|
|
assert.True(t, paths["/api/v1/dns-providers/types"], "dns providers types route should be registered")
|
|
}
|
|
|
|
func TestRegister_AllRoutesRegistered(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_all_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{
|
|
JWTSecret: "test-secret",
|
|
EncryptionKey: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
|
|
}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
routes := router.Routes()
|
|
routeMap := make(map[string][]string) // path -> methods
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = append(routeMap[r.Path], r.Method)
|
|
}
|
|
|
|
// Core routes
|
|
assert.Contains(t, routeMap, "/api/v1/health")
|
|
assert.Contains(t, routeMap, "/metrics")
|
|
|
|
// Auth routes
|
|
assert.Contains(t, routeMap, "/api/v1/auth/login")
|
|
assert.Contains(t, routeMap, "/api/v1/auth/register")
|
|
assert.Contains(t, routeMap, "/api/v1/auth/verify")
|
|
assert.Contains(t, routeMap, "/api/v1/auth/status")
|
|
assert.Contains(t, routeMap, "/api/v1/auth/logout")
|
|
assert.Contains(t, routeMap, "/api/v1/auth/me")
|
|
|
|
// User routes
|
|
assert.Contains(t, routeMap, "/api/v1/setup")
|
|
assert.Contains(t, routeMap, "/api/v1/invite/validate")
|
|
assert.Contains(t, routeMap, "/api/v1/invite/accept")
|
|
assert.Contains(t, routeMap, "/api/v1/users")
|
|
|
|
// Settings routes
|
|
assert.Contains(t, routeMap, "/api/v1/settings")
|
|
assert.Contains(t, routeMap, "/api/v1/settings/smtp")
|
|
|
|
// Security routes
|
|
assert.Contains(t, routeMap, "/api/v1/security/status")
|
|
assert.Contains(t, routeMap, "/api/v1/security/config")
|
|
assert.Contains(t, routeMap, "/api/v1/audit-logs")
|
|
|
|
// Notification routes
|
|
assert.Contains(t, routeMap, "/api/v1/notifications")
|
|
assert.Contains(t, routeMap, "/api/v1/notifications/providers")
|
|
|
|
// Uptime routes
|
|
assert.Contains(t, routeMap, "/api/v1/uptime/monitors")
|
|
|
|
// DNS Providers routes (when encryption key is set)
|
|
assert.Contains(t, routeMap, "/api/v1/dns-providers")
|
|
assert.Contains(t, routeMap, "/api/v1/dns-providers/types")
|
|
assert.Contains(t, routeMap, "/api/v1/dns-providers/:id/credentials")
|
|
|
|
// Admin routes - plugins should always be registered
|
|
assert.Contains(t, routeMap, "/api/v1/admin/plugins")
|
|
|
|
// CrowdSec routes
|
|
assert.Contains(t, routeMap, "/api/v1/admin/crowdsec/status")
|
|
assert.Contains(t, routeMap, "/api/v1/admin/crowdsec/start")
|
|
assert.Contains(t, routeMap, "/api/v1/admin/crowdsec/stop")
|
|
|
|
// Total route count should be substantial
|
|
assert.Greater(t, len(routes), 50, "Expected more than 50 routes to be registered")
|
|
}
|
|
|
|
func TestRegister_MiddlewareApplied(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_middleware"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
// Test that security headers middleware is applied
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/health", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Security headers should be present
|
|
assert.NotEmpty(t, w.Header().Get("X-Content-Type-Options"))
|
|
assert.NotEmpty(t, w.Header().Get("X-Frame-Options"))
|
|
|
|
// Response should be compressed (gzip middleware applied)
|
|
// Note: Only compressed if Accept-Encoding is set
|
|
req2 := httptest.NewRequest(http.MethodGet, "/api/v1/health", nil)
|
|
req2.Header.Set("Accept-Encoding", "gzip")
|
|
w2 := httptest.NewRecorder()
|
|
router.ServeHTTP(w2, req2)
|
|
// Check for gzip content encoding when response is large enough
|
|
assert.Equal(t, http.StatusOK, w2.Code)
|
|
}
|
|
|
|
func TestRegister_AuthenticatedRoutes(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_auth_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
// Test that protected routes require authentication
|
|
protectedPaths := []struct {
|
|
method string
|
|
path string
|
|
}{
|
|
{http.MethodGet, "/api/v1/backups"},
|
|
{http.MethodPost, "/api/v1/backups"},
|
|
{http.MethodGet, "/api/v1/logs"},
|
|
{http.MethodGet, "/api/v1/settings"},
|
|
{http.MethodGet, "/api/v1/notifications"},
|
|
{http.MethodGet, "/api/v1/users"},
|
|
{http.MethodGet, "/api/v1/auth/me"},
|
|
{http.MethodPost, "/api/v1/auth/logout"},
|
|
{http.MethodGet, "/api/v1/uptime/monitors"},
|
|
}
|
|
|
|
for _, tc := range protectedPaths {
|
|
t.Run(tc.method+"_"+tc.path, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(tc.method, tc.path, nil)
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code, "Route %s %s should require auth", tc.method, tc.path)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegister_StateChangingRoutesRequireAuthentication(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_mutating_auth_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
stateChangingPaths := []struct {
|
|
method string
|
|
path string
|
|
}{
|
|
{http.MethodPost, "/api/v1/backups"},
|
|
{http.MethodPost, "/api/v1/settings"},
|
|
{http.MethodPatch, "/api/v1/settings"},
|
|
{http.MethodPatch, "/api/v1/config"},
|
|
{http.MethodPost, "/api/v1/user/profile"},
|
|
{http.MethodPost, "/api/v1/remote-servers"},
|
|
{http.MethodPost, "/api/v1/remote-servers/test"},
|
|
{http.MethodPut, "/api/v1/remote-servers/1"},
|
|
{http.MethodDelete, "/api/v1/remote-servers/1"},
|
|
{http.MethodPost, "/api/v1/remote-servers/1/test"},
|
|
}
|
|
|
|
for _, tc := range stateChangingPaths {
|
|
t.Run(tc.method+"_"+tc.path, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(tc.method, tc.path, nil)
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code, "State-changing route %s %s should require auth", tc.method, tc.path)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegister_AdminRoutes(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_admin_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{
|
|
JWTSecret: "test-secret",
|
|
EncryptionKey: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
|
|
}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
// Admin routes should exist and require auth
|
|
adminPaths := []string{
|
|
"/api/v1/admin/plugins",
|
|
"/api/v1/admin/crowdsec/status",
|
|
}
|
|
|
|
for _, path := range adminPaths {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
router.ServeHTTP(w, req)
|
|
// Should require auth (401) not be missing (404)
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code, "Admin route %s should exist and require auth", path)
|
|
}
|
|
}
|
|
|
|
func TestRegister_PublicRoutes(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_public_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
// Public routes should be accessible without auth (route exists, not 404)
|
|
publicPaths := []struct {
|
|
method string
|
|
path string
|
|
}{
|
|
{http.MethodGet, "/api/v1/health"},
|
|
{http.MethodGet, "/metrics"},
|
|
{http.MethodGet, "/api/v1/setup"},
|
|
{http.MethodGet, "/api/v1/auth/status"},
|
|
}
|
|
|
|
for _, tc := range publicPaths {
|
|
t.Run(tc.method+"_"+tc.path, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(tc.method, tc.path, nil)
|
|
router.ServeHTTP(w, req)
|
|
// Should not be 404 (route exists)
|
|
assert.NotEqual(t, http.StatusNotFound, w.Code, "Public route %s %s should exist", tc.method, tc.path)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegister_HealthEndpoint(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_health_endpoint"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/health", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Contains(t, w.Body.String(), "status")
|
|
}
|
|
|
|
func TestRegister_MetricsEndpoint(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_metrics_endpoint"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
// Prometheus metrics format
|
|
assert.Contains(t, w.Header().Get("Content-Type"), "text/plain")
|
|
}
|
|
|
|
func TestRegister_DBHealthEndpoint(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_db_health"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/health/db", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should return OK or service unavailable, but not 404
|
|
assert.NotEqual(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func TestRegister_LoginEndpoint(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_login"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
// Test login endpoint exists and accepts POST
|
|
body := `{"username": "test", "password": "test"}`
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should not be 404 (route exists)
|
|
assert.NotEqual(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func TestRegister_SetupEndpoint(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_setup"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
// GET /setup should return setup status
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/setup", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Contains(t, w.Body.String(), "setup")
|
|
}
|
|
|
|
func TestRegister_WithEncryptionRoutes(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_encryption_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
// Set valid encryption key env var (32-byte key base64 encoded)
|
|
t.Setenv("CHARON_ENCRYPTION_KEY", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
|
|
|
|
cfg := config.Config{
|
|
JWTSecret: "test-secret",
|
|
EncryptionKey: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
|
|
}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
// Check if encryption routes are registered (may depend on env)
|
|
routes := router.Routes()
|
|
routeMap := make(map[string]bool)
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = true
|
|
}
|
|
|
|
// DNS providers should be registered with valid encryption key
|
|
assert.True(t, routeMap["/api/v1/dns-providers"])
|
|
assert.True(t, routeMap["/api/v1/dns-providers/types"])
|
|
}
|
|
|
|
func TestRegister_UptimeCheckEndpoint(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_uptime_check"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
// Uptime check route should exist and require auth
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/system/uptime/check", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should require auth
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
}
|
|
|
|
func TestRegister_CrowdSecRoutes(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_crowdsec_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
// CrowdSec routes should exist
|
|
routes := router.Routes()
|
|
routeMap := make(map[string]bool)
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = true
|
|
}
|
|
|
|
// CrowdSec management routes
|
|
assert.True(t, routeMap["/api/v1/admin/crowdsec/start"])
|
|
assert.True(t, routeMap["/api/v1/admin/crowdsec/stop"])
|
|
assert.True(t, routeMap["/api/v1/admin/crowdsec/status"])
|
|
assert.True(t, routeMap["/api/v1/admin/crowdsec/presets"])
|
|
assert.True(t, routeMap["/api/v1/admin/crowdsec/decisions"])
|
|
assert.True(t, routeMap["/api/v1/admin/crowdsec/ban"])
|
|
}
|
|
|
|
func TestRegister_SecurityRoutes(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_security_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
routes := router.Routes()
|
|
routeMap := make(map[string]bool)
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = true
|
|
}
|
|
|
|
// Security routes
|
|
assert.True(t, routeMap["/api/v1/security/status"])
|
|
assert.True(t, routeMap["/api/v1/security/config"])
|
|
assert.True(t, routeMap["/api/v1/security/enable"])
|
|
assert.True(t, routeMap["/api/v1/security/disable"])
|
|
assert.True(t, routeMap["/api/v1/security/decisions"])
|
|
assert.True(t, routeMap["/api/v1/security/rulesets"])
|
|
assert.True(t, routeMap["/api/v1/security/geoip/status"])
|
|
assert.True(t, routeMap["/api/v1/security/waf/exclusions"])
|
|
}
|
|
|
|
func TestRegister_AccessListRoutes(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_acl_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
routes := router.Routes()
|
|
routeMap := make(map[string]bool)
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = true
|
|
}
|
|
|
|
// Access List routes
|
|
assert.True(t, routeMap["/api/v1/access-lists"])
|
|
assert.True(t, routeMap["/api/v1/access-lists/:id"])
|
|
assert.True(t, routeMap["/api/v1/access-lists/:id/test"])
|
|
assert.True(t, routeMap["/api/v1/access-lists/templates"])
|
|
}
|
|
|
|
func TestRegister_CertificateRoutes(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_cert_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
routes := router.Routes()
|
|
routeMap := make(map[string]bool)
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = true
|
|
}
|
|
|
|
// Certificate routes
|
|
assert.True(t, routeMap["/api/v1/certificates"])
|
|
assert.True(t, routeMap["/api/v1/certificates/:id"])
|
|
}
|
|
|
|
// TestRegister_NilHandlers verifies registration behavior with minimal/nil components
|
|
func TestRegister_NilHandlers(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
// Create a minimal DB connection that will work
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_nil_handlers"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
// Config with minimal settings - no encryption key, no special features
|
|
cfg := config.Config{
|
|
JWTSecret: "test-secret",
|
|
Environment: "production",
|
|
EncryptionKey: "", // No encryption key - DNS providers won't be registered
|
|
}
|
|
|
|
err = Register(router, db, cfg)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify that routes still work without DNS provider features
|
|
routes := router.Routes()
|
|
routeMap := make(map[string]bool)
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = true
|
|
}
|
|
|
|
// Core routes should still be registered
|
|
assert.True(t, routeMap["/api/v1/health"])
|
|
assert.True(t, routeMap["/api/v1/auth/login"])
|
|
|
|
// DNS provider routes should NOT be registered (no encryption key)
|
|
assert.False(t, routeMap["/api/v1/dns-providers"])
|
|
}
|
|
|
|
// TestRegister_MiddlewareOrder verifies middleware is attached in correct order
|
|
func TestRegister_MiddlewareOrder(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_middleware_order"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{
|
|
JWTSecret: "test-secret",
|
|
Environment: "development",
|
|
}
|
|
|
|
err = Register(router, db, cfg)
|
|
require.NoError(t, err)
|
|
|
|
// Test that security headers are applied (they should come first)
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/health", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Security headers should be present regardless of response
|
|
assert.NotEmpty(t, w.Header().Get("X-Content-Type-Options"), "Security headers middleware should set X-Content-Type-Options")
|
|
assert.NotEmpty(t, w.Header().Get("X-Frame-Options"), "Security headers middleware should set X-Frame-Options")
|
|
|
|
// In development mode, CSP should be more permissive
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
}
|
|
|
|
// TestRegister_GzipCompression verifies gzip middleware is working
|
|
func TestRegister_GzipCompression(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_gzip"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
// Request with Accept-Encoding: gzip
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/health", nil)
|
|
req.Header.Set("Accept-Encoding", "gzip")
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Response should be OK (gzip will only compress if response is large enough)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
}
|
|
|
|
// TestRegister_CerberusMiddleware verifies Cerberus security middleware is applied
|
|
func TestRegister_CerberusMiddleware(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_cerberus_mw"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{
|
|
JWTSecret: "test-secret",
|
|
Security: config.SecurityConfig{
|
|
CerberusEnabled: true,
|
|
},
|
|
}
|
|
|
|
err = Register(router, db, cfg)
|
|
require.NoError(t, err)
|
|
|
|
// API routes should have Cerberus middleware applied
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/setup", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should still work (Cerberus allows normal requests)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
}
|
|
|
|
// TestRegister_FeatureFlagsEndpoint verifies feature flags endpoint is registered
|
|
func TestRegister_FeatureFlagsEndpoint(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_feature_flags"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
// Feature flags should require auth
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
}
|
|
|
|
// TestRegister_WebSocketRoutes verifies WebSocket routes are registered
|
|
func TestRegister_WebSocketRoutes(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_ws_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
routes := router.Routes()
|
|
routeMap := make(map[string]bool)
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = true
|
|
}
|
|
|
|
// WebSocket routes should be registered
|
|
assert.True(t, routeMap["/api/v1/logs/live"])
|
|
assert.True(t, routeMap["/api/v1/websocket/connections"])
|
|
assert.True(t, routeMap["/api/v1/websocket/stats"])
|
|
assert.True(t, routeMap["/api/v1/cerberus/logs/ws"])
|
|
}
|
|
|
|
// TestRegister_NotificationRoutes verifies all notification routes are registered
|
|
func TestRegister_NotificationRoutes(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_notification_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
routes := router.Routes()
|
|
routeMap := make(map[string]bool)
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = true
|
|
}
|
|
|
|
// Notification routes
|
|
assert.True(t, routeMap["/api/v1/notifications"])
|
|
assert.True(t, routeMap["/api/v1/notifications/:id/read"])
|
|
assert.True(t, routeMap["/api/v1/notifications/read-all"])
|
|
assert.True(t, routeMap["/api/v1/notifications/providers"])
|
|
assert.True(t, routeMap["/api/v1/notifications/providers/:id"])
|
|
assert.True(t, routeMap["/api/v1/notifications/templates"])
|
|
assert.True(t, routeMap["/api/v1/notifications/external-templates"])
|
|
assert.True(t, routeMap["/api/v1/notifications/external-templates/:id"])
|
|
}
|
|
|
|
// TestRegister_DomainRoutes verifies domain management routes
|
|
func TestRegister_DomainRoutes(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_domain_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
routes := router.Routes()
|
|
routeMap := make(map[string]bool)
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = true
|
|
}
|
|
|
|
// Domain routes
|
|
assert.True(t, routeMap["/api/v1/domains"])
|
|
assert.True(t, routeMap["/api/v1/domains/:id"])
|
|
}
|
|
|
|
// TestRegister_VerifyAuthEndpoint tests the verify endpoint for Caddy forward auth
|
|
func TestRegister_VerifyAuthEndpoint(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_verify_auth"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
// Verify endpoint is public (for Caddy forward auth)
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/verify", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should not be 404 (route exists) - will return 401 without valid session
|
|
assert.NotEqual(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
// TestRegister_SMTPRoutes verifies SMTP configuration routes
|
|
func TestRegister_SMTPRoutes(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_smtp_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
routes := router.Routes()
|
|
routeMap := make(map[string]bool)
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = true
|
|
}
|
|
|
|
// SMTP routes
|
|
assert.True(t, routeMap["/api/v1/settings/smtp"])
|
|
assert.True(t, routeMap["/api/v1/settings/smtp/test"])
|
|
assert.True(t, routeMap["/api/v1/settings/smtp/test-email"])
|
|
assert.True(t, routeMap["/api/v1/settings/validate-url"])
|
|
assert.True(t, routeMap["/api/v1/settings/test-url"])
|
|
}
|
|
|
|
// TestRegisterImportHandler_RoutesExist verifies import handler routes
|
|
func TestRegisterImportHandler_RoutesExist(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_import_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
RegisterImportHandler(router, db, cfg, "/usr/bin/caddy", "/tmp/imports", "/tmp/mount")
|
|
|
|
routes := router.Routes()
|
|
routeMap := make(map[string]bool)
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = true
|
|
}
|
|
|
|
// Import routes
|
|
assert.True(t, routeMap["/api/v1/import/status"] || routeMap["/api/v1/import/preview"] || routeMap["/api/v1/import/upload"],
|
|
"At least one import route should be registered")
|
|
}
|
|
|
|
// TestRegister_EncryptionRoutesWithValidKey verifies encryption management routes
|
|
func TestRegister_EncryptionRoutesWithValidKey(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_encryption_routes_valid"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
// Set the env var needed for rotation service
|
|
t.Setenv("CHARON_ENCRYPTION_KEY", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
|
|
|
|
// Valid 32-byte key in base64
|
|
cfg := config.Config{
|
|
JWTSecret: "test-secret",
|
|
EncryptionKey: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
|
|
}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
routes := router.Routes()
|
|
routeMap := make(map[string]bool)
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = true
|
|
}
|
|
|
|
// Encryption management routes should be registered (depends on rotation service init)
|
|
// Note: If rotation service init fails, these routes won't be registered
|
|
// We check if DNS provider routes are registered (which don't depend on rotation service)
|
|
assert.True(t, routeMap["/api/v1/dns-providers"])
|
|
assert.True(t, routeMap["/api/v1/dns-providers/types"])
|
|
|
|
// Encryption routes may or may not be registered depending on env setup
|
|
// Just verify the DNS providers are there when encryption key is valid
|
|
}
|
|
|
|
// TestRegister_WAFExclusionRoutes verifies WAF exclusion management routes
|
|
func TestRegister_WAFExclusionRoutes(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_waf_exclusion_routes"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
routes := router.Routes()
|
|
routeMap := make(map[string]bool)
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = true
|
|
}
|
|
|
|
// WAF exclusion routes
|
|
assert.True(t, routeMap["/api/v1/security/waf/exclusions"])
|
|
assert.True(t, routeMap["/api/v1/security/waf/exclusions/:rule_id"])
|
|
}
|
|
|
|
// TestRegister_BreakGlassRoute verifies break glass endpoint is registered
|
|
func TestRegister_BreakGlassRoute(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_breakglass_route"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
routes := router.Routes()
|
|
routeMap := make(map[string]bool)
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = true
|
|
}
|
|
|
|
// Break glass route
|
|
assert.True(t, routeMap["/api/v1/security/breakglass/generate"])
|
|
}
|
|
|
|
// TestRegister_RateLimitPresetsRoute verifies rate limit presets endpoint
|
|
func TestRegister_RateLimitPresetsRoute(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_ratelimit_presets"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
routes := router.Routes()
|
|
routeMap := make(map[string]bool)
|
|
for _, r := range routes {
|
|
routeMap[r.Path] = true
|
|
}
|
|
|
|
// Rate limit presets route
|
|
assert.True(t, routeMap["/api/v1/security/rate-limit/presets"])
|
|
}
|
|
|
|
// TestEmergencyEndpoint_BypassACL verifies emergency endpoint works when ACL is blocking
|
|
func TestEmergencyEndpoint_BypassACL(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
// Setup test database with ACL enabled
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_emergency_bypass_acl"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
// Set emergency token in env
|
|
t.Setenv("CHARON_EMERGENCY_TOKEN", "test-token-that-meets-minimum-length-requirement-32-chars")
|
|
|
|
// Register routes with security enabled
|
|
cfg := config.Config{
|
|
JWTSecret: "test-secret",
|
|
Security: config.SecurityConfig{
|
|
ACLMode: "enabled",
|
|
CerberusEnabled: true,
|
|
},
|
|
}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
// Note: We don't need to create ACL settings here because the emergency endpoint
|
|
// bypass happens at middleware level before Cerberus checks
|
|
|
|
// Test 1: Verify emergency endpoint exists
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
|
|
req.RemoteAddr = "127.0.0.1:12345"
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should not be 404 (route exists)
|
|
assert.NotEqual(t, http.StatusNotFound, w.Code, "Emergency endpoint should exist")
|
|
|
|
// Test 2: Emergency request with valid token should work
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
|
|
req.Header.Set("X-Emergency-Token", "test-token-that-meets-minimum-length-requirement-32-chars")
|
|
req.RemoteAddr = "127.0.0.1:12345"
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should succeed (even if ACL would normally block)
|
|
// Emergency handler returns 200 on success
|
|
assert.NotEqual(t, http.StatusForbidden, w.Code, "Emergency request should not be blocked by ACL")
|
|
assert.Equal(t, http.StatusOK, w.Code, "Emergency request should succeed")
|
|
}
|
|
|
|
// TestEmergencyBypass_MiddlewareOrder verifies emergency bypass is first in chain
|
|
func TestEmergencyBypass_MiddlewareOrder(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_emergency_mw_order"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
t.Setenv("CHARON_EMERGENCY_TOKEN", "test-token-that-meets-minimum-length-requirement-32-chars")
|
|
|
|
cfg := config.Config{
|
|
JWTSecret: "test-secret",
|
|
Security: config.SecurityConfig{
|
|
CerberusEnabled: true,
|
|
ManagementCIDRs: []string{"127.0.0.0/8"},
|
|
},
|
|
}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
// Request with emergency token should set bypass flag
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/health", nil)
|
|
req.Header.Set("X-Emergency-Token", "test-token-that-meets-minimum-length-requirement-32-chars")
|
|
req.RemoteAddr = "127.0.0.1:12345"
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should succeed - emergency bypass allows request through
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
}
|
|
|
|
// TestEmergencyBypass_InvalidToken verifies invalid tokens are rejected
|
|
func TestEmergencyBypass_InvalidToken(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_emergency_invalid_token"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
t.Setenv("CHARON_EMERGENCY_TOKEN", "test-token-that-meets-minimum-length-requirement-32-chars")
|
|
|
|
cfg := config.Config{
|
|
JWTSecret: "test-secret",
|
|
Security: config.SecurityConfig{
|
|
CerberusEnabled: true,
|
|
},
|
|
}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
// Request with WRONG emergency token
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
|
|
req.Header.Set("X-Emergency-Token", "wrong-token")
|
|
req.RemoteAddr = "127.0.0.1:12345"
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should not activate bypass (wrong token)
|
|
// Endpoint may still respond with proper error, but bypass flag should not be set
|
|
assert.NotEqual(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
// TestEmergencyBypass_UnauthorizedIP verifies IP restrictions work
|
|
func TestEmergencyBypass_UnauthorizedIP(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_emergency_unauthorized_ip"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
t.Setenv("CHARON_EMERGENCY_TOKEN", "test-token-that-meets-minimum-length-requirement-32-chars")
|
|
|
|
// Only allow 192.168.1.0/24
|
|
cfg := config.Config{
|
|
JWTSecret: "test-secret",
|
|
Security: config.SecurityConfig{
|
|
CerberusEnabled: true,
|
|
ManagementCIDRs: []string{"192.168.1.0/24"},
|
|
},
|
|
}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
// Request from public IP (not in management network)
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
|
|
req.Header.Set("X-Emergency-Token", "test-token-that-meets-minimum-length-requirement-32-chars")
|
|
req.RemoteAddr = "203.0.113.1:12345" // Public IP
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should not activate bypass (unauthorized IP)
|
|
assert.NotEqual(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func TestRegister_CreatesAccessLogFileForLogWatcher(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_test_access_log_create"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
logFilePath := filepath.Join(t.TempDir(), "logs", "access.log")
|
|
t.Setenv("CHARON_CADDY_ACCESS_LOG", logFilePath)
|
|
|
|
cfg := config.Config{JWTSecret: "test-secret"}
|
|
require.NoError(t, Register(router, db, cfg))
|
|
|
|
_, statErr := os.Stat(logFilePath)
|
|
assert.NoError(t, statErr)
|
|
}
|
|
|
|
func TestMigrateViewerToPassthrough(t *testing.T) {
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(&models.User{}))
|
|
|
|
// Seed a user with the legacy "viewer" role
|
|
viewer := models.User{
|
|
UUID: uuid.NewString(),
|
|
APIKey: uuid.NewString(),
|
|
Email: "viewer@example.com",
|
|
Role: models.UserRole("viewer"),
|
|
Enabled: true,
|
|
}
|
|
require.NoError(t, db.Create(&viewer).Error)
|
|
|
|
migrateViewerToPassthrough(db)
|
|
|
|
var updated models.User
|
|
require.NoError(t, db.First(&updated, viewer.ID).Error)
|
|
assert.Equal(t, models.RolePassthrough, updated.Role)
|
|
}
|