package main import ( "context" "go/ast" "go/parser" "go/token" "testing" "github.com/carlosframework/platform/internal/alerting" "github.com/carlosframework/platform/internal/console" "github.com/carlosframework/platform/internal/moves" ) // The Phase 1 moves switch reaches console.Config.MovesEnabled through // applyMovesSwitch, and only the on-values turn it on. func TestApplyMovesSwitchSetsTheConfigField(t *testing.T) { for _, tc := range []struct { val string want bool }{ {"", false}, {"on", true}, {"1", true}, {"true", true}, {"off", false}, {"yes", false}, } { var cfg console.Config applyMovesSwitch(&cfg, func(k string) string { if k != moves.SwitchEnv { t.Fatalf("read %q, want only %s", k, moves.SwitchEnv) } return tc.val }) if cfg.MovesEnabled != tc.want { t.Errorf("%s=%q: MovesEnabled = %v, want %v", moves.SwitchEnv, tc.val, cfg.MovesEnabled, tc.want) } } } // serve() is not callable from a test (it opens a database, a bucket and a // listener), so this parses main.go and checks three things: serve() calls // applyMovesSwitch(&X, os.Getenv); the same X is the config handed to // console.NewServer; and the switch call comes before the NewServer call. func TestServeAppliesTheMovesSwitchFromTheEnvironment(t *testing.T) { f, err := parser.ParseFile(token.NewFileSet(), "main.go", nil, 0) if err != nil { t.Fatal(err) } var serve *ast.FuncDecl for _, d := range f.Decls { if fn, ok := d.(*ast.FuncDecl); ok && fn.Name.Name == "serve" { serve = fn } } if serve == nil { t.Fatal("no serve() in main.go") } var applies, news []*ast.CallExpr ast.Inspect(serve.Body, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return true } switch fun := call.Fun.(type) { case *ast.Ident: if fun.Name == "applyMovesSwitch" { applies = append(applies, call) } case *ast.SelectorExpr: if pkg, ok := fun.X.(*ast.Ident); ok && pkg.Name == "console" && fun.Sel.Name == "NewServer" { news = append(news, call) } } return true }) if len(applies) != 1 || len(news) != 1 { t.Fatalf("serve() has %d applyMovesSwitch calls and %d console.NewServer calls, want one of each", len(applies), len(news)) } apply, ns := applies[0], news[0] if len(apply.Args) != 2 || len(ns.Args) < 1 { t.Fatal("applyMovesSwitch wants (&cfg, os.Getenv) and console.NewServer wants (cfg, store)") } if sel, ok := apply.Args[1].(*ast.SelectorExpr); !ok || !isIdent(sel.X, "os") || sel.Sel.Name != "Getenv" { t.Fatal("applyMovesSwitch is not given os.Getenv; the switch would not read the real environment") } addr, ok := apply.Args[0].(*ast.UnaryExpr) if !ok || addr.Op != token.AND { t.Fatal("applyMovesSwitch's first argument is not &X") } x, ok := addr.X.(*ast.Ident) if !ok { t.Fatal("applyMovesSwitch's first argument is not the address of a plain variable") } if !isIdent(ns.Args[0], x.Name) { t.Fatalf("console.NewServer is not handed %s, the config applyMovesSwitch changed", x.Name) } if apply.Pos() >= ns.Pos() { t.Fatal("applyMovesSwitch runs after console.NewServer; the server would be built with the switch off") } } func isIdent(e ast.Expr, name string) bool { id, ok := e.(*ast.Ident) return ok && id.Name == name } // The console pages from its move pass (moves Plan 5 Task 7, delta M14): // armMovesPaging arms alerting's sinks from the agent's variables on a // host-claim console, with or without moves; serve() // calls it with os.Getenv before ConfigureClaimRuntime, so the recovery // pass never runs unarmed. func TestArmMovesPagingWithClaims(t *testing.T) { env := func(k string) string { if k == "CARLOS_ALERT_WEBHOOK" { return "http://example.test/hook" } return "" } for _, tc := range []struct{ claim, moves, want bool }{{false, false, false}, {true, false, true}, {false, true, false}, {true, true, true}} { cfg := console.Config{HostClaim: tc.claim, MovesEnabled: tc.moves} if got := armMovesPaging(context.Background(), cfg, env); got != tc.want { t.Errorf("claim %v moves %v: armed %v, want %v", tc.claim, tc.moves, got, tc.want) } } t.Cleanup(func() { alerting.Arm(nil) }) f, err := parser.ParseFile(token.NewFileSet(), "main.go", nil, 0) if err != nil { t.Fatal(err) } var armed, configured token.Pos for _, d := range f.Decls { fn, ok := d.(*ast.FuncDecl) if !ok || fn.Name.Name != "serve" { continue } ast.Inspect(fn.Body, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return true } switch fun := call.Fun.(type) { case *ast.Ident: if fun.Name == "armMovesPaging" && len(call.Args) == 3 { if sel, ok := call.Args[2].(*ast.SelectorExpr); ok && isIdent(sel.X, "os") && sel.Sel.Name == "Getenv" { armed = call.Pos() } } case *ast.SelectorExpr: if fun.Sel.Name == "ConfigureClaimRuntime" { configured = call.Pos() } } return true }) } if armed == 0 || configured == 0 || armed > configured { t.Fatalf("serve() must call armMovesPaging(ctx, cfg, os.Getenv) before ConfigureClaimRuntime (armed %v, configured %v)", armed, configured) } }