"Not waiting" says the declaration is current, not that the machine finished applying it: the sent digest is recorded at send. So a test that pushed, saw waiting clear, and asked the machine what it was running found containers that did not exist yet — the certificate fix made compositions stable, and the settling that used to fail first had been hiding the gap behind it. The mesh already held the missing half: every machine's last report, with its time. It just was not in the JSON. `reported` now sets each machine's last word beside when the current declaration went to it, and "has it caught up" becomes a comparison of two timestamps the mesh recorded itself — a report newer than the send means the machine acted on what was sent; older means it is still working, which waiting alone cannot distinguish.
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, 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, 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)
|
|
}
|
|
}
|
|
}
|