Files
felhom-controller/controller/internal/settings/settings_recovery_test.go
T
admin b0dd13154b v0.76.0: campaign-#3 hardening (settings .bak recovery, restore stack_name validation, quiesce marker quarantine)
S1: corrupt settings.json recovers from .bak / safe-defaults+preserve, no crash-loop.
F2: validStackName gates restore + export handlers (reject /,\,..,NUL traversal).
S3: corrupt quiesce marker logged + quarantined, not silently dropped.
Tests T-S1/F2/S3 + red-proofs. Agent/hub untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 23:34:33 +02:00

94 lines
2.9 KiB
Go

package settings
import (
"io"
"log"
"os"
"path/filepath"
"strings"
"testing"
)
func discardLog() *log.Logger { return log.New(io.Discard, "", 0) }
// T-S1a: a corrupt primary recovers from the last-known-good .bak (no error, no crash-loop).
// Companion: pre-fix Load (return error on parse) → err != nil → this fails.
func TestLoad_RecoversFromBak(t *testing.T) {
p := filepath.Join(t.TempDir(), "settings.json")
if err := os.WriteFile(p+".bak", []byte(`{"password_hash":"bak-hash"}`), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(`{"password_hash": BROKEN not json`), 0644); err != nil {
t.Fatal(err)
}
s, err := Load(p, discardLog())
if err != nil {
t.Fatalf("Load must not error (no crash-loop): %v", err)
}
if s.GetPasswordHash() != "bak-hash" {
t.Fatalf("want recovered hash bak-hash, got %q", s.GetPasswordHash())
}
if s.LoadWarning == "" {
t.Error("expected LoadWarning to be set after recovery")
}
if b, _ := os.ReadFile(p); !strings.Contains(string(b), "bak-hash") {
t.Error("primary should be re-promoted from .bak")
}
}
// T-S1b: unrecoverable (corrupt primary, no .bak) → safe defaults, NO error, corrupt file preserved.
func TestLoad_UnrecoverableNoBak(t *testing.T) {
p := filepath.Join(t.TempDir(), "settings.json")
if err := os.WriteFile(p, []byte(`{ BROKEN not json`), 0644); err != nil {
t.Fatal(err)
}
s, err := Load(p, discardLog())
if err != nil {
t.Fatalf("Load must not error (no crash-loop): %v", err)
}
if s == nil {
t.Fatal("expected non-nil safe-default settings")
}
if s.LoadWarning == "" {
t.Error("expected LoadWarning to be set")
}
if m, _ := filepath.Glob(p + ".corrupt-*"); len(m) == 0 {
t.Error("corrupt file must be preserved as *.corrupt-*")
}
if _, e := os.Stat(p); e == nil {
t.Error("original corrupt primary should have been renamed away")
}
}
// T-S1c: save() writes a .bak equal to the primary (last-known-good).
func TestSave_WritesBak(t *testing.T) {
p := filepath.Join(t.TempDir(), "settings.json")
s, err := Load(p, discardLog()) // no file → defaults
if err != nil {
t.Fatal(err)
}
if err := s.SetPasswordHash("h1"); err != nil { // SetPasswordHash persists via save()
t.Fatal(err)
}
prim, _ := os.ReadFile(p)
bak, e := os.ReadFile(p + ".bak")
if e != nil {
t.Fatalf(".bak missing after save: %v", e)
}
if string(prim) != string(bak) {
t.Error(".bak must equal the primary after save")
}
}
// T-S1d: happy path — valid primary loads normally, no warning (regression).
func TestLoad_HappyPath(t *testing.T) {
p := filepath.Join(t.TempDir(), "settings.json")
if err := os.WriteFile(p, []byte(`{"password_hash":"good"}`), 0644); err != nil {
t.Fatal(err)
}
s, err := Load(p, discardLog())
if err != nil || s.GetPasswordHash() != "good" || s.LoadWarning != "" {
t.Fatalf("happy load failed: err=%v hash=%q warn=%q", err, s.GetPasswordHash(), s.LoadWarning)
}
}