internal/server/settings_backup_form_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"
"os"
"path/filepath"
"strings"
"testing"
"github.com/X4Applegate/caddyui/internal/auth"
"github.com/X4Applegate/caddyui/internal/models"
)
// Issue #166, server half. web.TestSettingsFormEnclosesEverySectionControl
// proves the Backup card's controls sit inside the settings form; these
// tests prove the round trip they feed actually works.
//
// Together they cover what a human verifies by hand: tick scheduled backups,
// set a directory/interval/keep count, save, reload, see the values persist,
// then press "Back up now" and get a backup file.
//
// Before the fix the browser sent none of this — the inputs belonged to no
// form at all — so the save was a no-op however correct the handler was.
// That is why the handler-level tests that already existed all passed while
// the feature was completely unreachable from the UI.
// TestBackupSectionSavesAndReloads posts exactly what the fixed form posts,
// then reads the settings back.
func TestBackupSectionSavesAndReloads(t *testing.T) {
s, _ := newPreflightTestServer(t)
dir := t.TempDir()
// Seed another page's value to confirm the backup save stays scoped to
// its own keys, like every other settings page.
if err := models.SetSetting(s.DB, settingSiteTitle, "Keep me"); err != nil {
t.Fatal(err)
}
rec := postSettingsPage(t, s, url.Values{
"settings_section": {"backup"},
"backup_schedule_enabled": {"on"},
"backup_schedule_dir": {dir},
"backup_schedule_interval_hours": {"12"},
"backup_schedule_keep": {"5"},
})
if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/settings/backup?saved=1" {
t.Fatalf("backup save → %d %s", rec.Code, rec.Header().Get("Location"))
}
// Reload. Persisting these four values is the exact thing issue #166
// reported as impossible.
cfg := s.backupScheduleConfig()
if !cfg.Enabled {
t.Error("scheduled backups did not stay enabled after save + reload")
}
if cfg.Dir != dir {
t.Errorf("backup dir = %q, want %q", cfg.Dir, dir)
}
if cfg.IntervalHours != 12 {
t.Errorf("interval = %d, want 12", cfg.IntervalHours)
}
if cfg.Keep != 5 {
t.Errorf("keep = %d, want 5", cfg.Keep)
}
// Scoped: the backup page does not touch another page's settings.
if got := setting(t, s, settingSiteTitle); got != "Keep me" {
t.Errorf("backup save changed the site title: %q", got)
}
// Unticking the checkbox clears the flag. A checkbox posts nothing when
// unchecked, so this is the path that has to turn the feature back off.
rec = postSettingsPage(t, s, url.Values{
"settings_section": {"backup"},
"backup_schedule_dir": {dir},
"backup_schedule_interval_hours": {"12"},
"backup_schedule_keep": {"5"},
})
if rec.Code != http.StatusSeeOther {
t.Fatalf("backup save (unticked) → %d", rec.Code)
}
if s.backupScheduleConfig().Enabled {
t.Error("unticking scheduled backups did not disable them")
}
}
// TestBackupRunUsesSavedDirectory covers the "Back up now" button, which is a
// submit inside the settings form with formaction="/settings/backup/run". It
// must still produce a backup once it is inside a real form.
func TestBackupRunUsesSavedDirectory(t *testing.T) {
s, _ := newPreflightTestServer(t)
dir := t.TempDir()
// With no directory saved, the button explains itself rather than failing
// silently or 500-ing.
rec := postBackupRunPage(t, s)
if rec.Code != http.StatusSeeOther {
t.Fatalf("backup run with no dir → %d", rec.Code)
}
if loc := rec.Header().Get("Location"); !strings.Contains(loc, "backuperr=") {
t.Errorf("expected a backuperr redirect when no directory is saved, got %q", loc)
}
// Save a directory through the settings form, exactly as the UI does.
rec = postSettingsPage(t, s, url.Values{
"settings_section": {"backup"},
"backup_schedule_enabled": {"on"},
"backup_schedule_dir": {dir},
"backup_schedule_interval_hours": {"24"},
"backup_schedule_keep": {"7"},
})
if rec.Code != http.StatusSeeOther {
t.Fatalf("backup save → %d", rec.Code)
}
// Now "Back up now" writes a real file into that directory.
rec = postBackupRunPage(t, s)
if rec.Code != http.StatusSeeOther {
t.Fatalf("backup run → %d %s", rec.Code, rec.Body.String())
}
loc := rec.Header().Get("Location")
if !strings.Contains(loc, "backupok=") {
t.Fatalf("expected a backupok redirect, got %q", loc)
}
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
var backups []string
for _, e := range entries {
if strings.HasPrefix(e.Name(), backupFilePrefix) && strings.HasSuffix(e.Name(), backupFileSuffix) {
backups = append(backups, e.Name())
}
}
if len(backups) != 1 {
t.Fatalf("expected exactly one backup file in %s, got %v", dir, entries)
}
info, err := os.Stat(filepath.Join(dir, backups[0]))
if err != nil {
t.Fatal(err)
}
if info.Size() == 0 {
t.Errorf("backup %s is empty", backups[0])
}
}
// postBackupRunPage drives postBackupRun the way the form's "Back up now"
// submit button does.
func postBackupRunPage(t *testing.T, s *Server) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodPost, "/settings/backup/run", strings.NewReader(""))
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.postBackupRun(rec, req)
return rec
}