d698ce343b
gates / gates (push) Successful in 14s
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.
122 lines
3.8 KiB
Go
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())
|
|
}
|
|
}
|