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.'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, " ") }