Ctrl K
ComunidadGuías…

internal/server/fallback_route_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// SPDX-License-Identifier: Apache-2.0

package server

import (
	"encoding/json"
	"strings"
	"testing"

	"github.com/X4Applegate/caddyui/internal/models"
)

// v2.59.4 (issue #123): a request that matches no host used to get an empty
// 200 OK from Caddy (a fallback existed only when custom 404 HTML was saved).
// Both listeners now end with a real 404.

func lastRouteJSON(t *testing.T, routes []any) string {
	t.Helper()
	if len(routes) == 0 {
		t.Fatal("no routes")
	}
	b, _ := json.Marshal(routes[len(routes)-1])
	return string(b)
}

func proxyRow(domain string, forceSSL bool) models.ProxyHost {
	return models.ProxyHost{ID: 1, Domains: domain, ForwardScheme: "http", ForwardHost: "10.0.0.5", ForwardPort: 80, Enabled: true, SSLEnabled: true, SSLForced: forceSSL}
}

func TestDefaultFallbackIsAnErrorHandler404OnBothListeners(t *testing.T) {
	s := newHTTPOnlyTestServer(t)
	hosts := []models.ProxyHost{proxyRow("app.example.com", false)}

	https := s.withFallbackRoute(s.buildMergedRoutes(hosts, nil, nil))
	httpR := s.buildHTTPRoutes(hosts, nil, nil)
	for name, routes := range map[string][]any{"HTTPS": https, "HTTP": httpR} {
		last := lastRouteJSON(t, routes)
		if !strings.Contains(last, `"handler":"error"`) || !strings.Contains(last, `"status_code":404`) {
			t.Errorf("%s: the last route is not a 404 fallback: %s", name, last)
		}
		if strings.Contains(last, `"match"`) {
			t.Errorf("%s: the fallback must match every request: %s", name, last)
		}
	}
}

func TestCustomFallbackHTMLStillWinsAndStaysA404(t *testing.T) {
	s := newHTTPOnlyTestServer(t)
	if err := models.SetSetting(s.DB, settingCatchAll404HTML, "<h1>nope</h1>"); err != nil {
		t.Fatal(err)
	}
	hosts := []models.ProxyHost{proxyRow("app.example.com", false)}
	for name, routes := range map[string][]any{
		"HTTPS": s.withFallbackRoute(s.buildMergedRoutes(hosts, nil, nil)),
		"HTTP":  s.buildHTTPRoutes(hosts, nil, nil),
	} {
		last := lastRouteJSON(t, routes)
		if !strings.Contains(last, `\u003ch1\u003enope\u003c/h1\u003e`) || !strings.Contains(last, `"status_code":404`) || !strings.Contains(last, "static_response") {
			t.Errorf("%s: custom fallback page lost: %s", name, last)
		}
	}
}

// A forced-SSL host must still be redirected, not answered by the fallback:
// the redirect has to sit BEFORE the catch-all on :80.
func TestHTTPSRedirectStaysAheadOfTheFallbackOnPlainHTTP(t *testing.T) {
	s := newHTTPOnlyTestServer(t)
	routes := s.buildHTTPRoutes([]models.ProxyHost{proxyRow("secure.example.com", true)}, nil, nil)
	if len(routes) != 2 {
		t.Fatalf("routes = %d, want redirect + fallback: %v", len(routes), routes)
	}
	first, _ := json.Marshal(routes[0])
	if !strings.Contains(string(first), "secure.example.com") || !strings.Contains(string(first), "Location") {
		t.Errorf("first route is not the HTTPS redirect: %s", first)
	}
	if last := lastRouteJSON(t, routes); !strings.Contains(last, `"handler":"error"`) {
		t.Errorf("the fallback is not last: %s", last)
	}
}

// With nothing configured CaddyUI must keep owning nothing: no fallback route,
// no taking over of :80 / :443 for an empty server.
func TestNoFallbackWhenThereAreNoRoutes(t *testing.T) {
	s := newHTTPOnlyTestServer(t)
	if r := s.withFallbackRoute(nil); len(r) != 0 {
		t.Errorf("a fallback was added to an empty route list: %v", r)
	}
	if r := s.buildHTTPRoutes(nil, nil, nil); len(r) != 0 {
		t.Errorf("an empty :80 server got routes: %v", r)
	}
}

func TestFallbackSitsAfterGlobalMaintenanceAndRoutes(t *testing.T) {
	s := newHTTPOnlyTestServer(t)
	if err := models.SetSetting(s.DB, settingGlobalMaintenance, "1"); err != nil {
		t.Fatal(err)
	}
	routes := s.withFallbackRoute(s.buildMergedRoutes([]models.ProxyHost{proxyRow("app.example.com", false)}, nil, nil))
	first, _ := json.Marshal(routes[0])
	if !strings.Contains(string(first), "Maintenance") {
		t.Errorf("maintenance is no longer first: %s", first)
	}
	if last := lastRouteJSON(t, routes); !strings.Contains(last, `"handler":"error"`) {
		t.Errorf("fallback not last: %s", last)
	}
}

// End to end through syncCaddy: what is written to the live Caddy.
func TestSyncWritesTheFallbackToBothServers(t *testing.T) {
	f := newL4Fleet(t)
	f.createProxy(t, "app.example.com", nil)
	f.fSource.mu.Lock()
	defer f.fSource.mu.Unlock()
	var srv0, plain string
	for _, w := range f.fSource.writes {
		switch {
		case strings.HasPrefix(w, "PATCH /config/apps/http/servers/srv0/routes"):
			srv0 = w
		case strings.Contains(w, "/servers/caddyui_http") && !strings.HasPrefix(w, "DELETE"):
			plain = w
		}
	}
	for name, w := range map[string]string{"srv0": srv0, "caddyui_http": plain} {
		if !strings.Contains(w, `"handler":"error"`) || !strings.Contains(w, `"status_code":404`) {
			t.Errorf("%s was written without the 404 fallback: %.300s", name, w)
		}
	}
}