203 lines
7.5 KiB
Go
203 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/net/dns/dnsmessage"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// The self-check (novox/hq to-be 45 §4): a probe that fails raises its condition, one that cannot run
|
|
// raises probe-failed for itself and is never a pass, and every run ends with its heartbeat.
|
|
|
|
// withProbes runs the test with a registry of its own.
|
|
func withProbes(t *testing.T, probes ...probe) {
|
|
t.Helper()
|
|
before := probeRegistry
|
|
probeRegistry = probes
|
|
t.Cleanup(func() { probeRegistry = before })
|
|
}
|
|
|
|
// **The registry says what each probe asserts**, and a probe not built says why and when.
|
|
func TestTheRegistryIsTheDesignsLiveForm(t *testing.T) {
|
|
seen := map[string]bool{}
|
|
for _, p := range probeRegistry {
|
|
if seen[p.ID] {
|
|
t.Errorf("%s twice", p.ID)
|
|
}
|
|
seen[p.ID] = true
|
|
if p.Asserts == "" || p.From == "" || p.Kind == "" {
|
|
t.Errorf("%s does not say what it asserts, where from, or what it raises", p.ID)
|
|
}
|
|
if (p.run == nil) != (p.Deferred != "") || (p.Deferred != "" && p.Phase <= 1) {
|
|
t.Errorf("%s is run and deferred, or neither, or deferred out of Phase 1: %+v", p.ID, p)
|
|
}
|
|
}
|
|
for _, id := range []string{"D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "D10"} {
|
|
if !seen[id] {
|
|
t.Errorf("to-be 45 §4 has %s and the registry does not", id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **The doctor and the watchdogs watch each other**: watchdogs that stopped are DW; a self-check that
|
|
// stopped is S10 (signals_test.go).
|
|
func TestWatchdogsThatStoppedAreSaid(t *testing.T) {
|
|
w := &watchdogs{started: time.Now().Add(-time.Hour)}
|
|
d := &doctor{watchdogs: w, host: "anchor"}
|
|
got, err := probeWatchdogs(t.Context(), d)
|
|
if err != nil || len(got) != 1 || got[0].Severity != conditions.Urgent {
|
|
t.Fatalf("%+v %v", got, err)
|
|
}
|
|
w.ticked = time.Now()
|
|
if got, _ := probeWatchdogs(t.Context(), d); len(got) != 0 {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
}
|
|
|
|
// **D1 composes every machine of a healthy mesh and the host's own validator takes each.**
|
|
|
|
// **D1 names a machine nothing can be sent to**, and the network that cannot be computed for it.
|
|
|
|
// **D2: a resolver answering NXDOMAIN for IPv6 is wrong** — musl takes it as no such name (issue 262).
|
|
func TestAResolverAnsweringNoSuchNameForIPv6IsWrong(t *testing.T) {
|
|
answerAs := func(rcode dnsmessage.RCode) string {
|
|
conn, err := net.ListenPacket("udp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = conn.Close() })
|
|
go func() {
|
|
buf := make([]byte, 1500)
|
|
for {
|
|
n, from, err := conn.ReadFrom(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var q dnsmessage.Message
|
|
if q.Unpack(buf[:n]) != nil {
|
|
continue
|
|
}
|
|
reply := dnsmessage.Message{Header: dnsmessage.Header{ID: q.ID, Response: true}, Questions: q.Questions}
|
|
if q.Questions[0].Type == dnsmessage.TypeA {
|
|
reply.Answers = []dnsmessage.Resource{{Header: dnsmessage.ResourceHeader{Name: q.Questions[0].Name,
|
|
Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET}, Body: &dnsmessage.AResource{A: [4]byte{10, 77, 0, 1}}}}
|
|
} else {
|
|
reply.RCode = rcode
|
|
}
|
|
packed, _ := reply.Pack()
|
|
_, _ = conn.WriteTo(packed, from)
|
|
}
|
|
}()
|
|
_, port, _ := net.SplitHostPort(conn.LocalAddr().String())
|
|
return port
|
|
}
|
|
before := resolverPort
|
|
t.Cleanup(func() { resolverPort = before })
|
|
|
|
resolverPort = answerAs(dnsmessage.RCodeSuccess)
|
|
v4, rcode, err := askResolver(t.Context(), "127.0.0.1", "anchor.internal", dnsmessage.TypeA)
|
|
if err != nil || rcode != dnsmessage.RCodeSuccess || !slices.Equal(v4, []string{"10.77.0.1"}) {
|
|
t.Fatalf("%v %v %v", v4, rcode, err)
|
|
}
|
|
v6, rcode, err := askResolver(t.Context(), "127.0.0.1", "anchor.internal", dnsmessage.TypeAAAA)
|
|
if err != nil || rcode != dnsmessage.RCodeSuccess || len(v6) != 0 {
|
|
t.Fatalf("NODATA read as %v %v %v", v6, rcode, err)
|
|
}
|
|
resolverPort = answerAs(dnsmessage.RCodeNameError)
|
|
if _, rcode, _ := askResolver(t.Context(), "127.0.0.1", "anchor.internal", dnsmessage.TypeAAAA); rcode != dnsmessage.RCodeNameError {
|
|
t.Fatalf("NXDOMAIN read as %v", rcode)
|
|
}
|
|
}
|
|
|
|
// **D6, D7: what the controller defines is what it finds**, and a consumer deleted or a stream
|
|
// redefined is said — against a real bus, raised by the same derivation the controller starts with.
|
|
|
|
// **S9 hears the bus**: a consumer that gives up on a message, and one deleted, as the server says.
|
|
|
|
// **D10 compares a node-engine with what it is delivered as, not with its commit** (2026-10-06: every
|
|
// machine read as behind right after a push sent it the current build — it says the digest-named
|
|
// directory it runs from, and the mesh holds a commit).
|
|
func TestANodeEngineIsJudgedByTheVersionItIsDeliveredAs(t *testing.T) {
|
|
m := catalogue.Manifest{Module: "mesh-host", Resources: []map[string]any{
|
|
{"id": "launcher", "type": "file", "path": "/usr/lib/nox-mesh-host/launch"},
|
|
{"id": "host", "type": "archive", "path": "/usr/lib/nox-mesh-host/versions/31045596c83a"},
|
|
{"id": "unfilled", "type": "archive", "path": "/usr/lib/x/versions/${version}"},
|
|
}}
|
|
delivered := deliveredVersions(m)
|
|
if !slices.Equal(delivered, []string{"31045596c83a"}) {
|
|
t.Fatalf("%v", delivered)
|
|
}
|
|
commit := "1545b00a9f0c"
|
|
for _, c := range []struct {
|
|
reported string
|
|
behind bool
|
|
}{
|
|
{"31045596c83a", false}, // the live case: current, and was called behind
|
|
{"0123456789ab", true}, // another delivery
|
|
{"1545b00a", false}, // placed by hand, stamped with the commit
|
|
{"", false}, // not said
|
|
} {
|
|
if got := engineBehind(c.reported, delivered, commit); got != c.behind {
|
|
t.Errorf("%q behind = %v, want %v", c.reported, got, c.behind)
|
|
}
|
|
}
|
|
if engineBehind("31045596c83a", nil, commit) {
|
|
t.Error("behind a mesh that holds no delivered build")
|
|
}
|
|
}
|
|
|
|
// **Every seat verb a probe calls is one it declares, and one the controller is granted** — derived
|
|
// from the registry, so a probe added with a question the bus would refuse fails here, not live.
|
|
func TestEverySeatVerbAProbeAsksIsGranted(t *testing.T) {
|
|
granted, err := broker.PermissionsFor(broker.Principal{Kind: broker.KindController, PasswordHash: "x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
asked := 0
|
|
for _, p := range probeRegistry {
|
|
for _, v := range p.Asks {
|
|
asked++
|
|
subject := link.NodeSeatToolSubject(v.Seat, v.Verb, "anchor")
|
|
if !slices.ContainsFunc(granted.Publish, func(pattern string) bool { return subjectMatches(pattern, subject) }) {
|
|
t.Errorf("%s asks %s.%s and the controller may not publish %s", p.ID, v.Seat, v.Verb, subject)
|
|
}
|
|
if !slices.Contains(broker.VerbsTheSelfCheckAsks, v) {
|
|
t.Errorf("%s asks %s.%s, which broker.VerbsTheSelfCheckAsks does not name", p.ID, v.Seat, v.Verb)
|
|
}
|
|
}
|
|
}
|
|
if asked == 0 {
|
|
t.Fatal("no probe asks a seat verb: D8 lost its declaration")
|
|
}
|
|
// And a probe asking what it did not declare is refused before anything is sent.
|
|
ctx := context.WithValue(t.Context(), probeAsksKey{}, probe{ID: "DX"})
|
|
if _, err := askSeatTool(ctx, nil, "node-intrusion-prevention", "banned", "anchor"); err == nil ||
|
|
!strings.Contains(err.Error(), "does not declare") {
|
|
t.Fatalf("an undeclared question was asked: %v", err)
|
|
}
|
|
}
|
|
|
|
// subjectMatches is the bus's matching of a permission pattern against a subject.
|
|
func subjectMatches(pattern, subject string) bool {
|
|
p, s := strings.Split(pattern, "."), strings.Split(subject, ".")
|
|
for i, tok := range p {
|
|
if tok == ">" {
|
|
return len(s) > i
|
|
}
|
|
if i >= len(s) || (tok != "*" && tok != s[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return len(p) == len(s)
|
|
}
|