Files
Charon/backend/internal/caddy/validator_emergency_test.go
GitHub Actions 3169b05156 fix: skip incomplete system log viewer tests
- Marked 12 tests as skip pending feature implementation
- Features tracked in GitHub issue #686 (system log viewer feature completion)
- Tests cover sorting by timestamp/level/method/URI/status, pagination controls, filtering by text/level, download functionality
- Unblocks Phase 2 at 91.7% pass rate to proceed to Phase 3 security enforcement validation
- TODO comments in code reference GitHub #686 for feature completion tracking
- Tests skipped: Pagination (3), Search/Filter (2), Download (2), Sorting (1), Log Display (4)
2026-02-09 21:55:55 +00:00

288 lines
7.1 KiB
Go

package caddy
import (
"testing"
"github.com/stretchr/testify/require"
)
// TestValidate_EmergencyPlusMainPattern tests the core fix:
// Allow duplicate host when one has path matchers (emergency) and one doesn't (main)
func TestValidate_EmergencyPlusMainPattern(t *testing.T) {
tests := []struct {
name string
routes []*Route
expectError bool
errorText string
}{
{
name: "Emergency+Main Pattern (ALLOWED)",
routes: []*Route{
{
// Emergency route WITH paths
Match: []Match{{
Host: []string{"test.com"},
Path: []string{"/api/v1/emergency/*", "/emergency/*"},
}},
Handle: []Handler{
ReverseProxyHandler("app:8080", false, "none", true),
},
},
{
// Main route WITHOUT paths
Match: []Match{{
Host: []string{"test.com"},
}},
Handle: []Handler{
ReverseProxyHandler("app:8080", false, "none", true),
},
},
},
expectError: false,
},
{
name: "Reverse Order: Main+Emergency (ALLOWED)",
routes: []*Route{
{
// Main route WITHOUT paths (comes first)
Match: []Match{{
Host: []string{"example.com"},
}},
Handle: []Handler{
ReverseProxyHandler("app:8080", false, "none", true),
},
},
{
// Emergency route WITH paths (comes second)
Match: []Match{{
Host: []string{"example.com"},
Path: []string{"/emergency/*"},
}},
Handle: []Handler{
ReverseProxyHandler("app:8080", false, "none", true),
},
},
},
expectError: false,
},
{
name: "Duplicate Hosts With Same Paths (REJECTED)",
routes: []*Route{
{
Match: []Match{{
Host: []string{"test.com"},
Path: []string{"/api/*"},
}},
Handle: []Handler{
ReverseProxyHandler("app1:8080", false, "none", true),
},
},
{
Match: []Match{{
Host: []string{"test.com"},
Path: []string{"/admin/*"}, // Different paths, but both have paths
}},
Handle: []Handler{
ReverseProxyHandler("app2:8080", false, "none", true),
},
},
},
expectError: true,
errorText: "duplicate host with paths",
},
{
name: "Duplicate Hosts Without Paths (REJECTED)",
routes: []*Route{
{
Match: []Match{{
Host: []string{"test.com"},
}},
Handle: []Handler{
ReverseProxyHandler("app1:8080", false, "none", true),
},
},
{
Match: []Match{{
Host: []string{"test.com"}, // Same host, both without paths
}},
Handle: []Handler{
ReverseProxyHandler("app2:8080", false, "none", true),
},
},
},
expectError: true,
errorText: "duplicate host without paths",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := &Config{
Apps: Apps{
HTTP: &HTTPApp{
Servers: map[string]*Server{
"test": {
Listen: []string{":80"},
Routes: tt.routes,
},
},
},
},
}
err := Validate(config)
if tt.expectError {
require.Error(t, err, "Expected validation to fail")
if tt.errorText != "" {
require.Contains(t, err.Error(), tt.errorText)
}
} else {
require.NoError(t, err, "Expected validation to pass")
}
})
}
}
// TestValidate_MultipleHostsWithEmergencyPattern tests scalability:
// Verify validator handles 5, 10, and 18+ hosts with emergency+main pattern
func TestValidate_MultipleHostsWithEmergencyPattern(t *testing.T) {
hostCounts := []int{5, 10, 18}
for _, count := range hostCounts {
t.Run("RouteCount_"+string(rune(count+'0')), func(t *testing.T) {
routes := make([]*Route, 0, count*2) // 2 routes per host
for i := 0; i < count; i++ {
hostname := "host" + string(rune(i+'0')) + ".example.com"
// Emergency route
routes = append(routes, &Route{
Match: []Match{{
Host: []string{hostname},
Path: []string{"/api/v1/emergency/*", "/emergency/*"},
}},
Handle: []Handler{
ReverseProxyHandler("app:8080", false, "none", true),
},
})
// Main route
routes = append(routes, &Route{
Match: []Match{{
Host: []string{hostname},
}},
Handle: []Handler{
ReverseProxyHandler("app:8080", false, "none", true),
},
})
}
config := &Config{
Apps: Apps{
HTTP: &HTTPApp{
Servers: map[string]*Server{
"test": {
Listen: []string{":80"},
Routes: routes,
},
},
},
},
}
err := Validate(config)
require.NoError(t, err, "Expected validation to pass for %d hosts", count)
})
}
}
// TestValidate_RouteOrdering verifies route ordering is preserved
func TestValidate_RouteOrdering(t *testing.T) {
// Emergency route should be checked BEFORE main route
// This test ensures validator doesn't impose ordering constraints
config := &Config{
Apps: Apps{
HTTP: &HTTPApp{
Servers: map[string]*Server{
"test": {
Listen: []string{":80"},
Routes: []*Route{
{
// Route 0: Emergency (with paths) - should match /emergency/* first
Match: []Match{{
Host: []string{"test.com"},
Path: []string{"/emergency/*"},
}},
Handle: []Handler{
Handler{
"handler": "static_response",
"body": "Emergency bypass",
},
},
Terminal: true,
},
{
// Route 1: Main (no paths) - matches everything else
Match: []Match{{
Host: []string{"test.com"},
}},
Handle: []Handler{
ReverseProxyHandler("app:8080", false, "none", true),
},
Terminal: true,
},
},
},
},
},
},
}
err := Validate(config)
require.NoError(t, err, "Route ordering should be preserved")
}
// TestValidate_CaseInsensitiveHosts tests that host comparison is case-sensitive
// This is intentional - DNS is case-insensitive, but Caddy handles normalization
func TestValidate_CaseSensitiveHostnames(t *testing.T) {
// Note: This test documents current behavior.
// The validator treats "Test.com" and "test.com" as DIFFERENT strings.
// This is acceptable because config.go normalizes all hostnames to lowercase
// BEFORE calling the validator. By the time validator sees them, they're already
// normalized, so this scenario doesn't occur in production.
config := &Config{
Apps: Apps{
HTTP: &HTTPApp{
Servers: map[string]*Server{
"test": {
Listen: []string{":80"},
Routes: []*Route{
{
Match: []Match{{
Host: []string{"Test.com"}, // Uppercase T
}},
Handle: []Handler{
ReverseProxyHandler("app1:8080", false, "none", true),
},
},
{
Match: []Match{{
Host: []string{"test.com"}, // Lowercase t
}},
Handle: []Handler{
ReverseProxyHandler("app2:8080", false, "none", true),
},
},
},
},
},
},
},
}
// Current behavior: validator treats these as different hosts (case-sensitive string comparison)
// This is fine because config.go normalizes all domains to lowercase before validation
err := Validate(config)
require.NoError(t, err, "Validator treats different case as different hosts (config.go normalizes before validation)")
}