Files
felhom-controller/controller/internal/api/r474_remove_wiring_test.go
T
admin d698ce343b
gates / gates (push) Successful in 14s
controller v0.242.0: a removed app is listed with its kept backup; five small ones (R-487 R-491 R-490 R-489 R-476 R-456)
R-487: the local backup lists are keyed on the drives, not on what is
deployed — a removed app whose unit was kept is listed with the restore
that reinstalls it, the picker answers for it, and the restore opens the
unit where it sits. R-491: a removal clears the app's update hold.
R-490: /api/system/info reaches the API router and reads the default
storage path. R-489: volumes_removed is the real before/after difference,
[] when none. R-476: a Tier-2 copy is dated by its data, not its manifest.
R-456: the boot-orphan rule is pinned. Every fix red-proofed.
2026-09-13 22:50:18 +02:00

122 lines
3.8 KiB
Go

package api
import (
"encoding/json"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
"go/ast"
"go/parser"
"go/token"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// R-474 — the remove handler really deletes the whole unit, the Tier-2 mirror and the prefs.
func TestR474_RemoveHandlerDeletesUnitMirrorAndPrefs(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "router.go", nil, 0)
if err != nil {
t.Fatal(err)
}
var sel []string
for _, d := range f.Decls {
fn, ok := d.(*ast.FuncDecl)
if !ok || fn.Name.Name != "removeStack" || fn.Body == nil {
continue
}
ast.Inspect(fn.Body, func(n ast.Node) bool {
if s, ok := n.(*ast.SelectorExpr); ok {
sel = append(sel, s.Sel.Name)
}
return true
})
}
joined := " " + strings.Join(sel, " ") + " "
for _, want := range []string{"RecoveryUnitPath", "Tier2MirrorDirsForApp", "RemoveTier2Mirrors", "DeleteAppBackupPrefs"} {
if !strings.Contains(joined, " "+want+" ") {
t.Errorf("removeStack must call %s", want)
}
}
}
// R-486 — removing an app with its backups KEPT keeps its Tier-2 record. The only call that may
// forget the record is DeleteAppBackupPrefs, which the R-474 branch guards on remove_backups.
//
// COMPANION RED-PROOF (REPORT.md): put `r.sett.SetCrossDriveConfig(name, nil)` back into
// removeStack unconditionally — this fails.
func TestR486_RemovalKeepsTheTier2RecordUnlessBackupsGo(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "router.go", nil, 0)
if err != nil {
t.Fatal(err)
}
for _, d := range f.Decls {
fn, ok := d.(*ast.FuncDecl)
if !ok || fn.Name.Name != "removeStack" || fn.Body == nil {
continue
}
ast.Inspect(fn.Body, func(n ast.Node) bool {
if s, ok := n.(*ast.SelectorExpr); ok && s.Sel.Name == "SetCrossDriveConfig" {
t.Errorf("removeStack calls SetCrossDriveConfig at line %d — a removal with backups kept would forget the mirror it kept (R-486)", fset.Position(s.Pos()).Line)
}
return true
})
}
}
// R-491 — a removal clears the app's UPDATE hold (and only that kind).
//
// COMPANION RED-PROOF (REPORT.md): remove the ClearUpdateHold call from removeStack — this fails.
func TestR491_RemovalClearsTheUpdateHold(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "router.go", nil, 0)
if err != nil {
t.Fatal(err)
}
found := false
for _, d := range f.Decls {
fn, ok := d.(*ast.FuncDecl)
if !ok || fn.Name.Name != "removeStack" || fn.Body == nil {
continue
}
ast.Inspect(fn.Body, func(n ast.Node) bool {
if s, ok := n.(*ast.SelectorExpr); ok && s.Sel.Name == "ClearUpdateHold" {
found = true
}
return true
})
}
if !found {
t.Error("removeStack must clear the app's update hold (R-491), or a reinstall starts held")
}
}
// R-490 — with a default storage path registered, system-info reports the drive (the fallback every
// other reader already had).
func TestR490_SystemInfoFallsBackToTheDefaultStoragePath(t *testing.T) {
r, sett, _, _ := newSlice4Router(t)
r.sett = sett // the fixture router carries no settings; system-info reads the default path from them
if err := sett.AddStoragePath(settings.StoragePath{Path: t.TempDir(), Label: "HDD", Schedulable: true, IsDefault: true}); err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/api/system/info", nil))
if w.Code != http.StatusOK {
t.Fatalf("status %d: %s", w.Code, w.Body.String())
}
var resp struct {
Data struct {
System struct {
HDDConfigured bool `json:"hdd_configured"`
} `json:"system"`
} `json:"data"`
}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if !resp.Data.System.HDDConfigured {
t.Errorf("hdd_configured must be true with a default storage path registered; body %s", w.Body.String())
}
}