Files
Charon/backend/internal/caddy/config_test.go

439 lines
14 KiB
Go

package caddy
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
)
func TestGenerateConfig_Empty(t *testing.T) {
config, err := GenerateConfig([]models.ProxyHost{}, "/tmp/caddy-data", "admin@example.com", "", "", false, nil, nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, config)
require.NotNil(t, config.Apps.HTTP)
require.Empty(t, config.Apps.HTTP.Servers)
}
func TestGenerateConfig_SingleHost(t *testing.T) {
hosts := []models.ProxyHost{
{
UUID: "test-uuid",
Name: "Media",
DomainNames: "media.example.com",
ForwardScheme: "http",
ForwardHost: "media",
ForwardPort: 32400,
SSLForced: true,
WebsocketSupport: false,
Enabled: true,
},
}
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, nil, nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, config)
require.NotNil(t, config.Apps.HTTP)
require.Len(t, config.Apps.HTTP.Servers, 1)
server := config.Apps.HTTP.Servers["cpm_server"]
require.NotNil(t, server)
require.Contains(t, server.Listen, ":80")
require.Contains(t, server.Listen, ":443")
require.Len(t, server.Routes, 1)
route := server.Routes[0]
require.Len(t, route.Match, 1)
require.Equal(t, []string{"media.example.com"}, route.Match[0].Host)
require.Len(t, route.Handle, 1)
require.True(t, route.Terminal)
handler := route.Handle[0]
require.Equal(t, "reverse_proxy", handler["handler"])
}
func TestGenerateConfig_MultipleHosts(t *testing.T) {
hosts := []models.ProxyHost{
{
UUID: "uuid-1",
DomainNames: "site1.example.com",
ForwardHost: "app1",
ForwardPort: 8080,
Enabled: true,
},
{
UUID: "uuid-2",
DomainNames: "site2.example.com",
ForwardHost: "app2",
ForwardPort: 8081,
Enabled: true,
},
}
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, nil, nil, nil, nil)
require.NoError(t, err)
require.Len(t, config.Apps.HTTP.Servers["cpm_server"].Routes, 2)
}
func TestGenerateConfig_WebSocketEnabled(t *testing.T) {
hosts := []models.ProxyHost{
{
UUID: "uuid-ws",
DomainNames: "ws.example.com",
ForwardHost: "wsapp",
ForwardPort: 3000,
WebsocketSupport: true,
Enabled: true,
},
}
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, nil, nil, nil, nil)
require.NoError(t, err)
route := config.Apps.HTTP.Servers["cpm_server"].Routes[0]
handler := route.Handle[0]
// Check WebSocket headers are present
require.NotNil(t, handler["headers"])
}
func TestGenerateConfig_EmptyDomain(t *testing.T) {
hosts := []models.ProxyHost{
{
UUID: "bad-uuid",
DomainNames: "",
ForwardHost: "app",
ForwardPort: 8080,
Enabled: true,
},
}
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, nil, nil, nil, nil)
require.NoError(t, err)
// Should produce empty routes (or just catch-all if frontendDir was set, but it's empty here)
require.Empty(t, config.Apps.HTTP.Servers["cpm_server"].Routes)
}
func TestGenerateConfig_Logging(t *testing.T) {
hosts := []models.ProxyHost{}
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, nil, nil, nil, nil)
require.NoError(t, err)
// Verify logging configuration
require.NotNil(t, config.Logging)
require.NotNil(t, config.Logging.Logs)
require.NotNil(t, config.Logging.Logs["access"])
require.Equal(t, "INFO", config.Logging.Logs["access"].Level)
require.Contains(t, config.Logging.Logs["access"].Writer.Filename, "access.log")
require.Equal(t, 10, config.Logging.Logs["access"].Writer.RollSize)
require.Equal(t, 5, config.Logging.Logs["access"].Writer.RollKeep)
require.Equal(t, 7, config.Logging.Logs["access"].Writer.RollKeepDays)
}
func TestGenerateConfig_Advanced(t *testing.T) {
hosts := []models.ProxyHost{
{
UUID: "advanced-uuid",
Name: "Advanced",
DomainNames: "advanced.example.com",
ForwardScheme: "http",
ForwardHost: "advanced",
ForwardPort: 8080,
SSLForced: true,
HSTSEnabled: true,
HSTSSubdomains: true,
BlockExploits: true,
Enabled: true,
Locations: []models.Location{
{
Path: "/api",
ForwardHost: "api-service",
ForwardPort: 9000,
},
},
},
}
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, nil, nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, config)
server := config.Apps.HTTP.Servers["cpm_server"]
require.NotNil(t, server)
// Should have 2 routes: 1 for location /api, 1 for main domain
require.Len(t, server.Routes, 2)
// Check Location Route (should be first as it is more specific)
locRoute := server.Routes[0]
require.Equal(t, []string{"/api", "/api/*"}, locRoute.Match[0].Path)
require.Equal(t, []string{"advanced.example.com"}, locRoute.Match[0].Host)
// Check Main Route
mainRoute := server.Routes[1]
require.Nil(t, mainRoute.Match[0].Path) // No path means all paths
require.Equal(t, []string{"advanced.example.com"}, mainRoute.Match[0].Host)
// Check HSTS and BlockExploits handlers in main route
// Handlers are: [HSTS, BlockExploits, ReverseProxy]
// But wait, BlockExploitsHandler implementation details?
// Let's just check count for now or inspect types if possible.
// Based on code:
// handlers = append(handlers, HeaderHandler(...)) // HSTS
// handlers = append(handlers, BlockExploitsHandler()) // BlockExploits
// mainHandlers = append(handlers, ReverseProxyHandler(...))
require.Len(t, mainRoute.Handle, 3)
// Check HSTS
hstsHandler := mainRoute.Handle[0]
require.Equal(t, "headers", hstsHandler["handler"])
}
func TestGenerateConfig_ACMEStaging(t *testing.T) {
hosts := []models.ProxyHost{
{
UUID: "test-uuid",
DomainNames: "test.example.com",
ForwardHost: "app",
ForwardPort: 8080,
Enabled: true,
},
}
// Test with staging enabled
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "letsencrypt", true, nil, nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, config.Apps.TLS)
require.NotNil(t, config.Apps.TLS.Automation)
require.Len(t, config.Apps.TLS.Automation.Policies, 1)
issuers := config.Apps.TLS.Automation.Policies[0].IssuersRaw
require.Len(t, issuers, 1)
acmeIssuer := issuers[0].(map[string]interface{})
require.Equal(t, "acme", acmeIssuer["module"])
require.Equal(t, "admin@example.com", acmeIssuer["email"])
require.Equal(t, "https://acme-staging-v02.api.letsencrypt.org/directory", acmeIssuer["ca"])
// Test with staging disabled (production)
config, err = GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "letsencrypt", false, nil, nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, config.Apps.TLS)
require.NotNil(t, config.Apps.TLS.Automation)
require.Len(t, config.Apps.TLS.Automation.Policies, 1)
issuers = config.Apps.TLS.Automation.Policies[0].IssuersRaw
require.Len(t, issuers, 1)
acmeIssuer = issuers[0].(map[string]interface{})
require.Equal(t, "acme", acmeIssuer["module"])
require.Equal(t, "admin@example.com", acmeIssuer["email"])
_, hasCA := acmeIssuer["ca"]
require.False(t, hasCA, "Production mode should not set ca field (uses default)")
// We can't easily check the map content without casting, but we know it's there.
}
func TestGenerateSecurityApp(t *testing.T) {
t.Run("empty inputs", func(t *testing.T) {
app := generateSecurityApp(nil, nil, nil)
require.NotNil(t, app)
require.NotNil(t, app.Authentication)
require.NotNil(t, app.Authentication.Portals)
require.NotNil(t, app.Authorization)
require.NotNil(t, app.Authorization.Policies)
})
t.Run("with local users", func(t *testing.T) {
users := []models.AuthUser{
{Username: "admin", Email: "admin@example.com", PasswordHash: "hash123", Enabled: true},
{Username: "user", Email: "user@example.com", PasswordHash: "hash456", Enabled: true},
}
app := generateSecurityApp(users, nil, nil)
require.NotNil(t, app)
portal := app.Authentication.Portals["cpmp_portal"]
require.NotNil(t, portal)
require.Equal(t, "cpmp_portal", portal.Name)
require.Len(t, portal.Backends, 1)
require.Equal(t, "local", portal.Backends[0].Name)
require.Equal(t, "local", portal.Backends[0].Method)
})
t.Run("with disabled users", func(t *testing.T) {
users := []models.AuthUser{
{Username: "active", Email: "active@example.com", PasswordHash: "hash", Enabled: true},
{Username: "inactive", Email: "inactive@example.com", PasswordHash: "hash", Enabled: false},
}
app := generateSecurityApp(users, nil, nil)
portal := app.Authentication.Portals["cpmp_portal"]
config := portal.Backends[0].Config["users"].([]map[string]interface{})
// Only enabled user should be in config
require.Len(t, config, 1)
require.Equal(t, "active", config[0]["username"])
})
t.Run("with oauth providers", func(t *testing.T) {
providers := []models.AuthProvider{
{
Name: "Google",
Type: "google",
Enabled: true,
ClientID: "google-client-id",
ClientSecret: "google-secret",
Scopes: "openid,profile,email",
},
{
Name: "GitHub",
Type: "github",
Enabled: true,
ClientID: "github-client-id",
ClientSecret: "github-secret",
},
}
app := generateSecurityApp(nil, providers, nil)
portal := app.Authentication.Portals["cpmp_portal"]
require.Len(t, portal.Backends, 2)
googleBackend := portal.Backends[0]
require.Equal(t, "Google", googleBackend.Name)
require.Equal(t, "oauth2", googleBackend.Method)
require.Equal(t, "google", googleBackend.Realm)
require.Equal(t, "google-client-id", googleBackend.Config["client_id"])
})
t.Run("with disabled providers", func(t *testing.T) {
providers := []models.AuthProvider{
{Name: "Active", Type: "oidc", Enabled: true, ClientID: "id", ClientSecret: "secret"},
{Name: "Inactive", Type: "oidc", Enabled: false, ClientID: "id2", ClientSecret: "secret2"},
}
app := generateSecurityApp(nil, providers, nil)
portal := app.Authentication.Portals["cpmp_portal"]
require.Len(t, portal.Backends, 1)
require.Equal(t, "Active", portal.Backends[0].Name)
})
t.Run("with authorization policies", func(t *testing.T) {
policies := []models.AuthPolicy{
{
Name: "admin_policy",
Enabled: true,
AllowedRoles: "admin,super",
AllowedUsers: "user1,user2",
RequireMFA: true,
},
{
Name: "user_policy",
Enabled: true,
AllowedRoles: "user",
},
}
app := generateSecurityApp(nil, nil, policies)
require.Len(t, app.Authorization.Policies, 2)
adminPolicy := app.Authorization.Policies["admin_policy"]
require.NotNil(t, adminPolicy)
require.Equal(t, []string{"admin", "super"}, adminPolicy.AllowedRoles)
require.Equal(t, []string{"user1", "user2"}, adminPolicy.AllowedUsers)
require.True(t, adminPolicy.RequireMFA)
userPolicy := app.Authorization.Policies["user_policy"]
require.NotNil(t, userPolicy)
require.Equal(t, []string{"user"}, userPolicy.AllowedRoles)
require.False(t, userPolicy.RequireMFA)
})
t.Run("with disabled policies", func(t *testing.T) {
policies := []models.AuthPolicy{
{Name: "active", Enabled: true},
{Name: "inactive", Enabled: false},
}
app := generateSecurityApp(nil, nil, policies)
require.Len(t, app.Authorization.Policies, 1)
require.NotNil(t, app.Authorization.Policies["active"])
})
t.Run("provider with custom URLs", func(t *testing.T) {
providers := []models.AuthProvider{
{
Name: "Custom OIDC",
Type: "oidc",
Enabled: true,
ClientID: "client-id",
ClientSecret: "secret",
IssuerURL: "https://issuer.example.com",
AuthURL: "https://auth.example.com/authorize",
TokenURL: "https://auth.example.com/token",
},
}
app := generateSecurityApp(nil, providers, nil)
portal := app.Authentication.Portals["cpmp_portal"]
backend := portal.Backends[0]
require.Equal(t, "https://issuer.example.com", backend.Config["base_auth_url"])
require.Equal(t, "https://auth.example.com/authorize", backend.Config["authorization_url"])
require.Equal(t, "https://auth.example.com/token", backend.Config["token_url"])
})
}
func TestConvertAuthUsersToConfig(t *testing.T) {
t.Run("empty users", func(t *testing.T) {
result := convertAuthUsersToConfig(nil)
require.Empty(t, result)
})
t.Run("filters disabled users", func(t *testing.T) {
users := []models.AuthUser{
{Username: "active", Email: "active@example.com", PasswordHash: "hash1", Enabled: true},
{Username: "disabled", Email: "disabled@example.com", PasswordHash: "hash2", Enabled: false},
}
result := convertAuthUsersToConfig(users)
require.Len(t, result, 1)
require.Equal(t, "active", result[0]["username"])
})
t.Run("includes user details", func(t *testing.T) {
users := []models.AuthUser{
{
Username: "testuser",
Email: "test@example.com",
PasswordHash: "bcrypt-hash",
Name: "Test User",
Roles: "admin,editor",
Enabled: true,
},
}
result := convertAuthUsersToConfig(users)
require.Len(t, result, 1)
userConfig := result[0]
require.Equal(t, "testuser", userConfig["username"])
require.Equal(t, "test@example.com", userConfig["email"])
require.Equal(t, "bcrypt-hash", userConfig["password"])
require.Equal(t, "Test User", userConfig["name"])
require.Equal(t, []string{"admin", "editor"}, userConfig["roles"])
})
t.Run("omits empty name", func(t *testing.T) {
users := []models.AuthUser{
{Username: "noname", Email: "noname@example.com", PasswordHash: "hash", Enabled: true},
}
result := convertAuthUsersToConfig(users)
_, hasName := result[0]["name"]
require.False(t, hasName)
})
t.Run("omits empty roles", func(t *testing.T) {
users := []models.AuthUser{
{Username: "noroles", Email: "noroles@example.com", PasswordHash: "hash", Enabled: true},
}
result := convertAuthUsersToConfig(users)
_, hasRoles := result[0]["roles"]
require.False(t, hasRoles)
})
}