Files
felhom-controller/controller/internal/backupwindow/backupwindow_test.go
T
admin 82c67e32e1 v0.168.0: customer-configurable backup window (Mentési időablak)
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.
2026-07-24 20:55:44 +02:00

70 lines
2.9 KiB
Go

package backupwindow
import "testing"
// Group A — LegTimes derives the three nightly legs at fixed offsets, wrap-safe across midnight.
// Red-proof: drop the modulo in FmtHHMM → the 23:30 case yields "24:30"/"25:15" and fails.
func TestLegTimes(t *testing.T) {
cases := []struct{ start, db, tier2, offbox string }{
{"02:30", "02:30", "03:30", "04:15"}, // the default window
{"23:30", "23:30", "00:30", "01:15"}, // wraps past midnight
{"22:00", "22:00", "23:00", "23:45"},
{"00:00", "00:00", "01:00", "01:45"},
{"2:30", "02:30", "03:30", "04:15"}, // normalizes a missing leading zero
}
for _, c := range cases {
db, tier2, offbox := LegTimes(c.start)
if db != c.db || tier2 != c.tier2 || offbox != c.offbox {
t.Errorf("LegTimes(%q) = (%q,%q,%q), want (%q,%q,%q)", c.start, db, tier2, offbox, c.db, c.tier2, c.offbox)
}
}
}
// Group A — invalid input is rejected (LegTimes → empty; Valid → error). Callers pass a value
// already resolved through EffectiveWindow, so an empty result is never rendered.
func TestLegTimes_InvalidRejected(t *testing.T) {
for _, bad := range []string{"25:77", "2200", "", "ab:cd", "24:00", "12:60", "-1:00"} {
db, tier2, offbox := LegTimes(bad)
if db != "" || tier2 != "" || offbox != "" {
t.Errorf("LegTimes(%q) = (%q,%q,%q), want all empty (rejected)", bad, db, tier2, offbox)
}
if Valid(bad) == nil {
t.Errorf("Valid(%q) = nil, want an error", bad)
}
}
if Valid("02:30") != nil {
t.Error("Valid(02:30) returned an error for a well-formed time")
}
}
// Group A — the whole-guest gate span is [W+2h, W+6h), wrap-safe.
func TestGateWindow(t *testing.T) {
if from, to := GateWindow("02:30"); from != "04:30" || to != "08:30" {
t.Errorf("GateWindow(02:30) = (%q,%q), want (04:30,08:30)", from, to)
}
if from, to := GateWindow("23:00"); from != "01:00" || to != "05:00" {
t.Errorf("GateWindow(23:00) = (%q,%q), want (01:00,05:00) — must wrap", from, to)
}
if from, to := GateWindow("bad"); from != "" || to != "" {
t.Errorf("GateWindow(bad) = (%q,%q), want empties", from, to)
}
}
// Group B — precedence: a valid settings value wins over a valid yaml value, which wins over the
// "02:30" default; an empty/corrupt settings value falls through the chain.
func TestEffectiveWindow(t *testing.T) {
cases := []struct{ settingsVal, yamlVal, want string }{
{"22:00", "02:30", "22:00"}, // settings wins over yaml
{"", "03:00", "03:00"}, // yaml when settings empty
{"", "", "02:30"}, // default when both empty
{"garbage", "03:00", "03:00"}, // corrupt settings → fall through to yaml
{"garbage", "nope", "02:30"}, // both invalid → default
{"22:00", "", "22:00"}, // settings valid, yaml empty
}
for _, c := range cases {
if got := EffectiveWindow(c.settingsVal, c.yamlVal); got != c.want {
t.Errorf("EffectiveWindow(%q,%q) = %q, want %q", c.settingsVal, c.yamlVal, got, c.want)
}
}
}