Per-machine and mesh-wide settings could be set only from the controller's command line. The settings verb runs settings set|clear, passing the values inline, which the command now accepts as well as a file (novox/hq issue 198).
174 lines
6.8 KiB
Go
174 lines
6.8 KiB
Go
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)
|
|
}
|
|
}
|
|
|
|
// `settings` is `settings set|clear` at a shell, with the values passed inline (novox/hq issue 198).
|
|
func TestSettingsSetsOrClearsALayer(t *testing.T) {
|
|
argv, err := argvFor("settings", map[string]any{"module": "dnsmasq", "values": `{"a":1}`, "node": "ace"})
|
|
if err != nil || strings.Join(argv, " ") != `settings set dnsmasq {"a":1} --node ace` {
|
|
t.Fatalf("set on a machine: %v %v", argv, err)
|
|
}
|
|
argv, _ = argvFor("settings", map[string]any{"module": "dnsmasq", "clear": "true"})
|
|
if strings.Join(argv, " ") != "settings clear dnsmasq" {
|
|
t.Fatalf("clear for the mesh: %v", argv)
|
|
}
|
|
argv, _ = argvFor("settings", map[string]any{"module": "dnsmasq"})
|
|
if strings.Join(argv, " ") != "settings set dnsmasq" {
|
|
t.Fatalf("a set with no values falls to the command's usage: %v", argv)
|
|
}
|
|
}
|
|
|
|
// `issue` is `module issue` at a shell: the module and the machine, and nothing that would push. A
|
|
// module's bus account was mintable only from the controller's command line, so an agent working
|
|
// through the tools could not finish a rollout that gave a module one (novox/hq issue 191).
|
|
func TestIssueGivesAModuleItsAccountOnAMachine(t *testing.T) {
|
|
argv, err := argvFor("issue", map[string]any{"node": "ace", "module": "route-proxy"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Join(argv, " ") != "module issue route-proxy --node ace" {
|
|
t.Fatalf("issue runs %v", argv)
|
|
}
|
|
if _, err := argvFor("issue", map[string]any{"module": "route-proxy"}); err == nil {
|
|
t.Error("an account was issued without saying which machine reads it")
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|