82c67e32e1
ONE setting (window start W) drives every nightly leg at fixed, never-stored offsets: DB dump at W, tier-2 at W+60m, off-box at W+105m (wrap-safe). Precedence settings > controller.yaml db_dump_schedule > 02:30. - scheduler.UpdateDaily: retime a daily job at runtime (no restart) via a per-job buffered resched chan + a select case in runDailyJob. - new pure package internal/backupwindow (LegTimes/GateWindow/EffectiveWindow). - quiesce disk-tier window gate: scheduled cycles run only inside [W+2h,W+6h) with a safety valve (age>cadence+24h runs regardless); manual TriggerNow never gated. Backend.Due now also returns the backup age (from the agent's own /backup/due). - backup page: Mentési időablak card (time input + derived leg/gate rows); POST /backups/window validates -> saves -> UpdateDaily x3 -> flash. Tests: 5 groups, all red-proofed. Agent/cadence//backup/due untouched.
97 lines
3.3 KiB
Go
97 lines
3.3 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/scheduler"
|
|
)
|
|
|
|
// schedWith3Legs builds a real scheduler carrying the three daily legs at their default times, so
|
|
// UpdateDaily has jobs to find.
|
|
func schedWith3Legs() *scheduler.Scheduler {
|
|
s := scheduler.New(log.New(io.Discard, "", 0))
|
|
noop := func(context.Context) error { return nil }
|
|
s.Daily("db-dump", "02:30", noop)
|
|
s.Daily("tier2-backup", "03:30", noop)
|
|
s.Daily("offbox-backup", "04:15", noop)
|
|
return s
|
|
}
|
|
|
|
func legTimes(t *testing.T, sch *scheduler.Scheduler, name string) string {
|
|
t.Helper()
|
|
for _, j := range sch.GetJobs() {
|
|
if j.Name == name {
|
|
return j.Schedule
|
|
}
|
|
}
|
|
t.Fatalf("job %q not found", name)
|
|
return ""
|
|
}
|
|
|
|
// Group E — a valid save writes the setting, fans the three legs out via UpdateDaily, and redirects
|
|
// with a success flash. Red-proof: skip the Valid() check in the handler and an invalid value would
|
|
// be written — TestBackupWindowSave_Invalid then fails.
|
|
func TestBackupWindowSave_Valid(t *testing.T) {
|
|
s := testServer(t)
|
|
s.scheduler = schedWith3Legs()
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/backups/window", strings.NewReader("window_start=22:00"))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rec := httptest.NewRecorder()
|
|
s.backupWindowSaveHandler(rec, req)
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("status = %d, want 303", rec.Code)
|
|
}
|
|
if loc := rec.Header().Get("Location"); !strings.Contains(loc, "flash=") || strings.Contains(loc, "flash_error=") {
|
|
t.Errorf("redirect Location = %q, want a success flash", loc)
|
|
}
|
|
if got := s.settings.GetBackupWindowStart(); got != "22:00" {
|
|
t.Errorf("settings BackupWindowStart = %q, want 22:00", got)
|
|
}
|
|
// The three legs must have been rescheduled to W / W+60m / W+105m.
|
|
if got := legTimes(t, s.scheduler, "db-dump"); got != "22:00" {
|
|
t.Errorf("db-dump = %q, want 22:00", got)
|
|
}
|
|
if got := legTimes(t, s.scheduler, "tier2-backup"); got != "23:00" {
|
|
t.Errorf("tier2-backup = %q, want 23:00", got)
|
|
}
|
|
if got := legTimes(t, s.scheduler, "offbox-backup"); got != "23:45" {
|
|
t.Errorf("offbox-backup = %q, want 23:45", got)
|
|
}
|
|
}
|
|
|
|
// Group E — an invalid time stores nothing, leaves the legs untouched, and redirects with an error
|
|
// flash.
|
|
func TestBackupWindowSave_Invalid(t *testing.T) {
|
|
s := testServer(t)
|
|
s.scheduler = schedWith3Legs()
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/backups/window", strings.NewReader("window_start=2500"))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rec := httptest.NewRecorder()
|
|
s.backupWindowSaveHandler(rec, req)
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("status = %d, want 303", rec.Code)
|
|
}
|
|
if loc := rec.Header().Get("Location"); !strings.Contains(loc, "flash_error=") {
|
|
t.Errorf("redirect Location = %q, want an error flash", loc)
|
|
}
|
|
if got := s.settings.GetBackupWindowStart(); got != "" {
|
|
t.Errorf("settings BackupWindowStart = %q, want empty (nothing stored)", got)
|
|
}
|
|
if got := legTimes(t, s.scheduler, "db-dump"); got != "02:30" {
|
|
t.Errorf("db-dump = %q, want 02:30 (unchanged)", got)
|
|
}
|
|
if got := legTimes(t, s.scheduler, "offbox-backup"); got != "04:15" {
|
|
t.Errorf("offbox-backup = %q, want 04:15 (unchanged)", got)
|
|
}
|
|
}
|