- 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)
288 lines
7.1 KiB
Go
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)")
|
|
}
|