package main import ( "strings" "testing" "github.com/novox/mesh-controller/internal/catalogue" ) // Every verb the mesh-controller seat declares is one this binary can run, with the arguments the // schema names and no other (novox/hq ADR 0154, ADR 0035). func TestEveryDeclaredVerbHasACommandLine(t *testing.T) { for _, v := range catalogue.ControllerVerbs { if v.Name == "tools" { continue } args := map[string]any{} props, _ := v.Input["properties"].(map[string]any) for name := range props { args[name] = "x" } argv, err := argvFor(v.Name, args) if err != nil { t.Errorf("%s: %v", v.Name, err) continue } if argv[0] == "" { t.Errorf("%s: empty command", v.Name) } } } // `builds` given a build's id reads that build's log from the bus rather than listing builds // (novox/hq ADR 0157). func TestBuildsWithAnIdReadsThatBuildsLog(t *testing.T) { argv, err := argvFor("builds", map[string]any{"log": "build-17"}) if err != nil { t.Fatal(err) } if strings.Join(argv, " ") != "builds --log build-17" { t.Fatalf("builds with a log id became %q", strings.Join(argv, " ")) } } // The build tool takes a repository as a URL or as its path on the forge holding the git seat, and // says which it was given, so the command reads the path as a seat source rather than handing it to // git as written (novox/hq issue 176). And it never waits: the id follows the build. func TestTheBuildToolTellsAForgePathFromAURL(t *testing.T) { argv, _ := argvFor("build", map[string]any{"repository": "novox/mesh-catalog", "path": "modules/x"}) if line := strings.Join(argv, " "); !strings.Contains(line, "--self") || !strings.Contains(line, "--wait 0") { t.Fatalf("a forge path is a seat source, not waited for; got %q", line) } argv, _ = argvFor("build", map[string]any{"repository": "https://example.tld/o/r.git"}) if line := strings.Join(argv, " "); strings.Contains(line, "--self") { t.Fatalf("a URL is cloned as given; got %q", line) } } // `rotate` is one verb with two shapes (ADR 0114, issue 180): a pair credential by provision, or a // module's own secret by machine, module and name. func TestRotateTakesAProvisionOrAnOwnSecret(t *testing.T) { argv, _ := argvFor("rotate", map[string]any{"provision": "postgres-database", "consumer": "ace"}) if strings.Join(argv, " ") != "rotate postgres-database --consumer ace" { t.Fatalf("a pair credential: %v", argv) } argv, _ = argvFor("rotate", map[string]any{"node": "ace", "module": "nodered", "secret": "api-token"}) if strings.Join(argv, " ") != "secret rotate ace nodered api-token" { t.Fatalf("an own secret: %v", argv) } argv, _ = argvFor("rotate", map[string]any{"node": "ace"}) if strings.Join(argv, " ") != "rotate" { t.Fatalf("half an own secret falls to the command's usage: %v", argv) } } // A required argument missing is refused in the verb's own words, before anything runs. func TestAVerbMissingWhatItNeedsIsRefused(t *testing.T) { if _, err := argvFor("node", map[string]any{}); err == nil || !strings.Contains(err.Error(), `node needs "node"`) { t.Fatalf("node without a machine was accepted: %v", err) } if _, err := argvFor("upgrade", map[string]any{}); err == nil { t.Fatal("a verb the seat does not serve was accepted") } } // A push and a build are sent, not waited for: the asker reads status, or the build's log by its // id, for what happened. A repository given as a forge path is said to be one (issue 176). func TestActsDoNotBlockTheCall(t *testing.T) { argv, _ := argvFor("push", map[string]any{"node": "one"}) if strings.Join(argv, " ") != "push one --wait 0" { t.Fatalf("push waits: %v", argv) } argv, _ = argvFor("build", map[string]any{"repository": "novox/x", "path": "modules/x"}) if strings.Join(argv, " ") != "build novox/x --wait 0 --self --path modules/x" { t.Fatalf("build: %v", argv) } } // What `tools` answers is the seats' records, with each verb's schema. func TestToolsAnswersTheSeatsRecords(t *testing.T) { handlers, err := seatToolHandlers() if err != nil { t.Fatal(err) } if len(handlers) != len(catalogue.ControllerVerbs) { t.Fatalf("%d handlers for %d verbs", len(handlers), len(catalogue.ControllerVerbs)) } answer := seatTools() seats, _ := answer["seats"].([]map[string]any) var found bool for _, s := range seats { if s["seat"] == catalogue.ControllerSeatName { found = true tools, _ := s["tools"].([]map[string]any) if len(tools) != len(catalogue.ControllerVerbs) || tools[0]["input"] == nil { t.Fatalf("the controller seat's tools are not listed in full: %v", tools) } } } if !found { t.Fatal("the mesh-controller seat is not in the listing") } } // A JSON verb's answer is parsed from what the command wrote to standard output alone; a warning it // printed beside the document does not take the document away. The test binary stands in for the // controller: `-test.run` with a name that matches nothing prints `ok` and a warning about no tests. func TestAJSONVerbsAnswerIsItsStandardOutput(t *testing.T) { jsonVerbs["-test.run"] = true t.Cleanup(func() { delete(jsonVerbs, "-test.run") }) answer, err := runVerb(t.Context(), []string{"-test.run", "TestAnswerEcho", "-test.v"}) if err != nil { t.Fatal(err) } if !answer.OK { t.Fatalf("the command failed: %s", answer.Output) } if !strings.Contains(answer.Output, "PASS") { t.Fatalf("stderr and stdout are both what the command said: %s", answer.Output) } }