Ctrl K
ComunidadGuías…

internal/server/settings_sections_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 (
	"context"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

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

func postSettingsPage(t *testing.T, s *Server, form url.Values) *httptest.ResponseRecorder {
	t.Helper()
	req := httptest.NewRequest(http.MethodPost, "/settings", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req = req.WithContext(context.WithValue(req.Context(), auth.ContextUserKey, &models.User{ID: 1, Email: "admin@example.com", IsAdmin: true, Role: models.RoleAdmin}))
	rec := httptest.NewRecorder()
	s.postSettings(rec, req)
	return rec
}

func setting(t *testing.T, s *Server, key string) string {
	t.Helper()
	v, err := models.GetSetting(s.DB, key)
	if err != nil {
		t.Fatal(err)
	}
	return v
}

// v2.44.0: a page saves its own keys and leaves every other page's values
// alone; the legacy whole-form post (no page) still saves everything.
func TestSettingsPagesSaveOnlyTheirOwnKeys(t *testing.T) {
	s, _ := newPreflightTestServer(t)
	seed := map[string]string{
		settingSiteTitle:              "Old title",
		settingTimezone:               "Europe/Paris",
		settingSMTPHost:               "mail.example.test",
		settingNotifyWebhookURL:       "https://hooks.example.test/x",
		settingAnalyticsIngestTarget:  "10.8.0.1:9019",
		settingAnalyticsRetentionDays: "45",
		settingAdminAllowlist:         "10.0.0.0/8",
		settingAIProvider:             "anthropic",
		settingServerIP:               "203.0.113.5",
		settingClientIPHeaders:        "CF-Connecting-IP",
	}
	for k, v := range seed {
		if err := models.SetSetting(s.DB, k, v); err != nil {
			t.Fatal(err)
		}
	}

	// General page: only General keys change.
	rec := postSettingsPage(t, s, url.Values{"settings_section": {"general"}, "site_title": {"New title"}, "timezone": {"UTC"}, "auto_sync_hours": {"6"}})
	if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/settings/general?saved=1" {
		t.Fatalf("general save → %d %s", rec.Code, rec.Header().Get("Location"))
	}
	if got := setting(t, s, settingSiteTitle); got != "New title" {
		t.Errorf("site title = %q", got)
	}
	if got := setting(t, s, settingTimezone); got != "UTC" {
		t.Errorf("timezone = %q", got)
	}
	for _, k := range []string{settingSMTPHost, settingNotifyWebhookURL, settingAnalyticsIngestTarget, settingAnalyticsRetentionDays, settingAdminAllowlist, settingAIProvider, settingServerIP, settingClientIPHeaders} {
		if got := setting(t, s, k); got != seed[k] {
			t.Errorf("General save changed %s: %q → %q", k, seed[k], got)
		}
	}

	// Analytics page: its keys change, General and the rest stay.
	rec = postSettingsPage(t, s, url.Values{"settings_section": {"analytics"}, "analytics_enabled": {"0", "1"}, "analytics_ingest_target": {"caddyui:9019"}, "analytics_retention_days": {"14"}})
	if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/settings/analytics?saved=1" {
		t.Fatalf("analytics save → %d %s", rec.Code, rec.Header().Get("Location"))
	}
	if got := setting(t, s, settingAnalyticsIngestTarget); got != "caddyui:9019" {
		t.Errorf("analytics target = %q", got)
	}
	if got := setting(t, s, settingAnalyticsRetentionDays); got != "14" {
		t.Errorf("retention = %q", got)
	}
	if got := setting(t, s, settingSiteTitle); got != "New title" {
		t.Errorf("Analytics save changed the site title: %q", got)
	}
	if got := setting(t, s, settingSMTPHost); got != seed[settingSMTPHost] {
		t.Errorf("Analytics save changed SMTP host: %q", got)
	}

	// Security page owns client_ip_headers even though integrations bundle it.
	rec = postSettingsPage(t, s, url.Values{"settings_section": {"security"}, "client_ip_headers": {"X-Real-IP"}, "admin_allowlist": {"192.168.0.0/16"}})
	if rec.Code != http.StatusSeeOther {
		t.Fatalf("security save → %d %s", rec.Code, rec.Body.String())
	}
	if got := setting(t, s, settingClientIPHeaders); got != "X-Real-IP" {
		t.Errorf("client_ip_headers = %q", got)
	}
	if got := setting(t, s, settingAdminAllowlist); got != "192.168.0.0/16" {
		t.Errorf("admin allowlist = %q", got)
	}
	if got := setting(t, s, settingServerIP); got != seed[settingServerIP] {
		t.Errorf("Security save changed the DNS server IP: %q", got)
	}

	// v2.53.0: extra certificate read roots are persisted normalised — one
	// absolute path per line, de-duplicated, with unusable entries dropped.
	rec = postSettingsPage(t, s, url.Values{
		"settings_section":       {"security"},
		"client_ip_headers":      {"X-Real-IP"},
		"certificate_read_roots": {"/opt/tls, /mnt/certs\n/opt/tls\n/\nrelative/dir\n  "},
	})
	if rec.Code != http.StatusSeeOther {
		t.Fatalf("security save → %d %s", rec.Code, rec.Body.String())
	}
	if got := setting(t, s, settingCertificateReadRoots); got != "/mnt/certs\n/opt/tls" {
		t.Errorf("certificate read roots = %q", got)
	}
	// An empty textarea clears the extras rather than being ignored.
	rec = postSettingsPage(t, s, url.Values{"settings_section": {"security"}, "client_ip_headers": {"X-Real-IP"}, "certificate_read_roots": {""}})
	if rec.Code != http.StatusSeeOther {
		t.Fatalf("security save → %d", rec.Code)
	}
	if got := setting(t, s, settingCertificateReadRoots); got != "" {
		t.Errorf("empty textarea should clear the extras, got %q", got)
	}

	// Notifications page: SMTP changes; a blank password keeps the stored one.
	if err := models.SetSetting(s.DB, settingSMTPPassword, "secret"); err != nil {
		t.Fatal(err)
	}
	rec = postSettingsPage(t, s, url.Values{"settings_section": {"notifications"}, "smtp_host": {"smtp.new.test"}, "smtp_password": {""}, "webhook_url": {""}})
	if rec.Code != http.StatusSeeOther {
		t.Fatalf("notifications save → %d", rec.Code)
	}
	if got := setting(t, s, settingSMTPHost); got != "smtp.new.test" || setting(t, s, settingSMTPPassword) != "secret" || setting(t, s, settingNotifyWebhookURL) != "" {
		t.Errorf("notifications save: host=%q password kept=%v webhook=%q", got, setting(t, s, settingSMTPPassword) == "secret", setting(t, s, settingNotifyWebhookURL))
	}
	if got := setting(t, s, settingAnalyticsIngestTarget); got != "caddyui:9019" {
		t.Errorf("Notifications save changed the analytics target: %q", got)
	}

	// Legacy whole-form post (no page): everything it carries is saved, as before.
	rec = postSettingsPage(t, s, url.Values{"site_title": {"Legacy"}, "analytics_ingest_target": {"legacy:9019"}})
	if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/settings?saved=1" {
		t.Fatalf("legacy save → %d %s", rec.Code, rec.Header().Get("Location"))
	}
	if setting(t, s, settingSiteTitle) != "Legacy" || setting(t, s, settingAnalyticsIngestTarget) != "legacy:9019" {
		t.Errorf("legacy save should write every page's keys")
	}

	// Unknown page slug → treated as legacy; unknown GET page → 404.
	if settingsSectionSlug("bogus") != "" || settingsSectionSlug(" DNS ") != "dns" {
		t.Errorf("slug validation")
	}
	req := httptest.NewRequest(http.MethodGet, "/settings/bogus", nil)
	rec = httptest.NewRecorder()
	// chi URL params are not set outside a router; exercise the slug check directly.
	if settingsSectionLabel("bogus") != "Settings" || settingsSectionLabel("dns") != "DNS" {
		t.Errorf("labels")
	}
	_ = req
	for anchor, page := range settingsAnchorSection {
		if settingsSectionSlug(page) == "" {
			t.Errorf("anchor %s points at unknown page %q", anchor, page)
		}
	}
	for key, page := range settingsKeySection {
		if settingsSectionSlug(page) == "" {
			t.Errorf("key %s owned by unknown page %q", key, page)
		}
	}
}