Adding "not running what the mesh would send it" to `status` and not to the board would have left two answers to one question with a person in front of each — which is the single thing this page's design forbids, introduced by the change that was supposed to make the question answerable. The published JSON carries it as well, so the page, the command and anything built against either say the same thing from the same read. Additive, because that shape is hard to change once anything is built against it. Never told stays separate from out of date on the page as it is everywhere else: same remedy, and nobody has ever asked that machine to be anything.
139 lines
4.9 KiB
Go
139 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-control/internal/inventory"
|
|
)
|
|
|
|
// The shape something other than a person reads.
|
|
//
|
|
// Hard to change once anything is built against it, so it stays close to what the domain already
|
|
// calls things and carries no summary field that would have to be kept true.
|
|
|
|
func statusOf(t *testing.T, wrong []inventory.Doing, nodes, quiet []inventory.Node,
|
|
behind map[string][]string, sources map[string]inventory.Source) map[string]any {
|
|
t.Helper()
|
|
body, err := statusAsJSON(wrong, nodes, quiet, behind, sources, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var parsed map[string]any
|
|
if err := json.Unmarshal(body, &parsed); err != nil {
|
|
t.Fatalf("what a board would read is not JSON: %v", err)
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func TestRefusedAndFailedStayDistinctAllTheWayOut(t *testing.T) {
|
|
// They are fixed in different places, so one word for both would send half the readers of a
|
|
// page to the wrong one.
|
|
got := statusOf(t,
|
|
[]inventory.Doing{
|
|
{Node: "one", Outcome: inventory.OutcomeRefused, Refused: "a file needs a path"},
|
|
{Node: "two", Outcome: inventory.OutcomeFailed, Applied: 3,
|
|
Failed: []inventory.FailedResource{{ID: "shell.pkg", Error: "target not found"}}},
|
|
},
|
|
[]inventory.Node{{Name: "one"}, {Name: "two"}}, nil, nil, nil)
|
|
|
|
wrong, _ := got["wrong"].([]any)
|
|
if len(wrong) != 2 {
|
|
t.Fatalf("got %v", got["wrong"])
|
|
}
|
|
first, _ := wrong[0].(map[string]any)
|
|
if first["outcome"] != inventory.OutcomeRefused || first["refused"] == nil {
|
|
t.Fatalf("a refusal did not survive: %v", first)
|
|
}
|
|
second, _ := wrong[1].(map[string]any)
|
|
if second["outcome"] != inventory.OutcomeFailed {
|
|
t.Fatalf("a failure did not survive: %v", second)
|
|
}
|
|
if second["applied"] != float64(3) {
|
|
// "Three of eight" and "none of eight" are different machines.
|
|
t.Fatalf("what did apply was not carried: %v", second)
|
|
}
|
|
}
|
|
|
|
func TestAMachineThatNeverSpokeSaysSoByOmission(t *testing.T) {
|
|
// Never heard from and quiet for a while are different situations, and a zero time would read
|
|
// as a date in 1970 on any page that formatted it.
|
|
got := statusOf(t, nil,
|
|
[]inventory.Node{{Name: "silent"}, {Name: "away", LastSeen: time.Now().Add(-3 * time.Hour)}},
|
|
[]inventory.Node{{Name: "silent"}, {Name: "away", LastSeen: time.Now().Add(-3 * time.Hour)}},
|
|
nil, nil)
|
|
|
|
quiet, _ := got["quiet"].([]any)
|
|
if len(quiet) != 2 {
|
|
t.Fatalf("got %v", got["quiet"])
|
|
}
|
|
never, _ := quiet[0].(map[string]any)
|
|
if _, said := never["lastSeen"]; said {
|
|
t.Fatalf("a machine that never spoke carries a time: %v", never)
|
|
}
|
|
away, _ := quiet[1].(map[string]any)
|
|
if _, said := away["lastSeen"]; !said {
|
|
t.Fatalf("a machine that has been quiet carries no time: %v", away)
|
|
}
|
|
}
|
|
|
|
func TestNothingWrongIsAnEmptyListRatherThanNothing(t *testing.T) {
|
|
// A page distinguishing "no machines are wrong" from "this field is missing" would have to
|
|
// handle both, and null is the one that gets forgotten.
|
|
got := statusOf(t, nil, []inventory.Node{{Name: "a", LastSeen: time.Now()}}, nil, nil, nil)
|
|
for _, key := range []string{"wrong", "quiet", "behind"} {
|
|
list, ok := got[key].([]any)
|
|
if !ok {
|
|
t.Fatalf("%q is %T, not a list", key, got[key])
|
|
}
|
|
if len(list) != 0 {
|
|
t.Fatalf("%q is not empty: %v", key, list)
|
|
}
|
|
}
|
|
// And how many machines there are, so a reader can tell "none wrong" from "none at all".
|
|
if got["machines"] != float64(1) {
|
|
t.Fatalf("got %v", got["machines"])
|
|
}
|
|
}
|
|
|
|
func TestWhatIsBehindNamesTheMachinesRunningTheOldOne(t *testing.T) {
|
|
// A module being out of date is a fact about the catalogue; machines running the old one is
|
|
// the thing with consequences.
|
|
got := statusOf(t, nil, nil, nil,
|
|
map[string][]string{"shell": {"workstation", "laptop"}},
|
|
map[string]inventory.Source{"shell": {BuiltFrom: "aaaaaaa1", Head: "bbbbbbb2"}})
|
|
|
|
behind, _ := got["behind"].([]any)
|
|
if len(behind) != 1 {
|
|
t.Fatalf("got %v", got["behind"])
|
|
}
|
|
row, _ := behind[0].(map[string]any)
|
|
if row["module"] != "shell" || row["builtFrom"] != "aaaaaaa1" || row["head"] != "bbbbbbb2" {
|
|
t.Fatalf("got %v", row)
|
|
}
|
|
on, _ := row["on"].([]any)
|
|
if len(on) != 2 {
|
|
t.Fatalf("the machines running the old one are not named: %v", row)
|
|
}
|
|
}
|
|
|
|
func TestNoSecretIsInWhatABoardReads(t *testing.T) {
|
|
// Everything here comes from the mesh's own records, which hold no readable secret — but a
|
|
// shape a page is built against is exactly where one would eventually be added for
|
|
// convenience, so this says it out loud.
|
|
body, err := statusAsJSON(
|
|
[]inventory.Doing{{Node: "a", Outcome: inventory.OutcomeRefused,
|
|
Refused: "resource \"x\": a file needs a path"}},
|
|
[]inventory.Node{{Name: "a"}}, nil, nil, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, word := range []string{"password", "secret", "sealed", "credential", "token"} {
|
|
if strings.Contains(strings.ToLower(string(body)), word) {
|
|
t.Fatalf("what a board reads carries a %q field:\n%s", word, body)
|
|
}
|
|
}
|
|
}
|