Files
felhom-controller/controller/cmd/controller/r475_wiring_test.go
T

125 lines
3.9 KiB
Go

package main
import (
"go/ast"
"go/parser"
"go/token"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)
// R-475 — the update reads EVERY backup tier, through the one adapter.
func TestR475_TierConstantsAgree(t *testing.T) {
if stacks.UpdateTierLocal != backup.UpdateTierLocal ||
stacks.UpdateTierSecondDrive != backup.UpdateTierSecondDrive ||
stacks.UpdateTierOffsite != backup.UpdateTierOffsite {
t.Fatal("stacks and backup number the tiers differently — a hold would name the wrong copy")
}
}
// adapterMethodSelectors returns the selector names used in updateGuardsAdapter.<name>'s body.
func adapterMethodSelectors(t *testing.T, name string) string {
t.Helper()
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "main.go", nil, 0)
if err != nil {
t.Fatal(err)
}
for _, d := range f.Decls {
fn, ok := d.(*ast.FuncDecl)
if !ok || fn.Recv == nil || fn.Name.Name != name || fn.Body == nil {
continue
}
star, ok := fn.Recv.List[0].Type.(*ast.StarExpr)
if !ok {
continue
}
if id, ok := star.X.(*ast.Ident); !ok || id.Name != "updateGuardsAdapter" {
continue
}
var names []string
ast.Inspect(fn.Body, func(n ast.Node) bool {
if sel, ok := n.(*ast.SelectorExpr); ok {
names = append(names, sel.Sel.Name)
}
return true
})
return " " + strings.Join(names, " ") + " "
}
t.Fatalf("updateGuardsAdapter.%s not found in main.go", name)
return ""
}
func TestR475_AdapterReadsEveryTier(t *testing.T) {
rp := adapterMethodSelectors(t, "RestorePoints")
if !strings.Contains(rp, " UpdateRestorePoints ") {
t.Errorf("the adapter must read every tier through backup.UpdateRestorePoints; selectors:%s", rp)
}
if strings.Contains(rp, " Tier2UnitRestorePoint ") {
t.Error("the Tier-2-only predicate must not come back into the update path (R-475)")
}
if cb := adapterMethodSelectors(t, "CanBackUp"); !strings.Contains(cb, " CanBackUpApp ") {
t.Errorf("CanBackUp must ask backup.CanBackUpApp; selectors:%s", cb)
}
// R-659 (v0.268.0) CHANGED THIS ON PURPOSE: the hold no longer names the precondition copy the
// adapter is handed (round 11: that copy was one the restore refuses). The tier, and R-479's
// "what the copy holds", are now chosen inside backup.HoldAfterFailedUpdateWhole from every tier —
// pinned there by TestR659_TheHoldNamesOnlyAWholeCopy (which asserts the holds-phrase too).
if h := adapterMethodSelectors(t, "HoldAfterFailedUpdate"); !strings.Contains(h, " HoldAfterFailedUpdateWhole ") {
t.Errorf("the hold must be recorded through HoldAfterFailedUpdateWhole (R-659); selectors:%s", h)
}
}
// R-490 — /api/system/info reaches the API router; the web layer's /api/system/ prefix used to eat it.
//
// COMPANION RED-PROOF (REPORT.md): delete the exact-path mount — this fails.
func TestR490_SystemInfoIsMountedAheadOfTheWebPrefix(t *testing.T) {
src, err := parser.ParseFile(token.NewFileSet(), "main.go", nil, 0)
if err != nil {
t.Fatal(err)
}
var exact, prefix bool
ast.Inspect(src, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok || len(call.Args) < 2 {
return true
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok || sel.Sel.Name != "Handle" {
return true
}
lit, ok := call.Args[0].(*ast.BasicLit)
if !ok {
return true
}
switch lit.Value {
case `"/api/system/info"`:
exact = strings.Contains(fmtNode(call.Args[1]), "apiRouter")
case `"/api/system/"`:
prefix = true
}
return true
})
if !prefix {
t.Fatal("the /api/system/ prefix mount is gone — this test's premise moved")
}
if !exact {
t.Error("/api/system/info must be mounted on the API router ahead of the web layer's /api/system/ prefix (R-490)")
}
}
func fmtNode(n ast.Node) string {
var names []string
ast.Inspect(n, func(x ast.Node) bool {
if id, ok := x.(*ast.Ident); ok {
names = append(names, id.Name)
}
return true
})
return strings.Join(names, " ")
}