internal/server/settings_sections_test.go
Rama main · réplica de lectura del repositorio original.
// 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)
}
}
}