package nodeengineclient import ( "errors" "strings" "testing" "time" seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-sdk/go/keys" ) const digestA = "sha256:0000000000000000000000000000000000000000000000000000000000000001" func TestHaveCodec(t *testing.T) { good := seat.HaveByNodeModuleValue{Phase: PhaseRunning, Health: HealthOK, Composed: digestA, Fp: "src2:x"} waiting := seat.HaveByNodeModuleValue{Phase: PhaseWaitingYes, Health: HealthOK, Judgement: JudgedRecreates, Waits: digestA} pull := &seat.PullWait{Image: "i", Holder: "h"} tests := map[string]struct { have seat.HaveByNodeModuleValue bad string }{ "running": {good, ""}, "waiting for a yes": {waiting, ""}, "unknown phase": {seat.HaveByNodeModuleValue{Phase: "gone", Health: HealthOK}, "phase"}, "unknown health": {seat.HaveByNodeModuleValue{Phase: PhaseRunning, Health: "fine"}, "health"}, "judgement outside a yes": {seat.HaveByNodeModuleValue{Phase: PhaseRunning, Health: HealthOK, Judgement: JudgedCannot}, "judgement"}, "a yes without a digest": {seat.HaveByNodeModuleValue{Phase: PhaseWaitingYes, Health: HealthOK, Judgement: JudgedCannot, Waits: "x"}, "sha256"}, "pull wait on a running": {seat.HaveByNodeModuleValue{Phase: PhaseRunning, Health: HealthOK, PullWait: pull}, "pull"}, "pull wait without holder": {seat.HaveByNodeModuleValue{Phase: PhaseFailed, Health: HealthFailed, PullWait: &seat.PullWait{Image: "i"}}, "holder"}, "bad composed digest": {seat.HaveByNodeModuleValue{Phase: PhaseRunning, Health: HealthOK, Composed: "abc"}, "sha256"}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { raw, err := EncodeHave(tc.have) if tc.bad != "" { if err == nil || !strings.Contains(err.Error(), tc.bad) { t.Fatalf("encode: %v, want it to name %q", err, tc.bad) } return } if err != nil { t.Fatal(err) } back, err := DecodeHave(raw) if err != nil { t.Fatal(err) } tc.have.V = keys.Version if !SameHave(back, tc.have) { t.Fatalf("round trip differs: %+v", back) } }) } if _, err := DecodeHave([]byte(`{"v":2,"phase":"running","health":"ok"}`)); !errors.Is(err, keys.ErrUnknownVersion) { t.Fatalf("version 2: %v", err) } if _, err := DecodeHave([]byte(`{"v":1,"phase":"running","health":"ok","extra":1}`)); err == nil { t.Fatal("an unknown field is read") } } func TestSameHaveComparesTimesAsInstants(t *testing.T) { at := time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC) zone := time.FixedZone("x", 3600) mk := func(tm time.Time) seat.HaveByNodeModuleValue { return seat.HaveByNodeModuleValue{Phase: PhaseFailed, Health: HealthFailed, PullWait: &seat.PullWait{Image: "i", Holder: "h", Began: tm}, UserList: &seat.UserListWritten{Source: "s", At: tm}} } if !SameHave(mk(at), mk(at.In(zone))) { t.Fatal("the same instant in another zone differs") } if SameHave(mk(at), mk(at.Add(time.Second))) { t.Fatal("another instant is the same") } } func TestHaveNodeCodec(t *testing.T) { n := seat.HaveByNodeNodeValue{Composed: digestA, Composes: ComposesOwn, Candidates: map[string]seat.CandidateAnswer{"g": {Proposal: "p", Verdict: seat.CandidateAnswerVerdictSame}}} raw, err := EncodeHaveNode(n) if err != nil { t.Fatal(err) } back, err := DecodeHaveNode(raw) if err != nil { t.Fatal(err) } n.V = keys.Version if !SameHaveNode(back, n) { t.Fatalf("round trip differs: %+v", back) } for name, bad := range map[string]seat.HaveByNodeNodeValue{ "composes": {Composes: "other"}, "no group": {Candidates: map[string]seat.CandidateAnswer{"": {Verdict: "same"}}}, "a verdict": {Candidates: map[string]seat.CandidateAnswer{"g": {Verdict: "maybe"}}}, } { if _, err := EncodeHaveNode(bad); err == nil { t.Errorf("%s: encoded", name) } } a, b := seat.NodeRecord{Node: "n", V: 1}, seat.NodeRecord{Node: "n", V: 2} if !SameNodeRecord(a, b) { t.Error("a record differs by its version") } } func TestHealthAndDataCodecs(t *testing.T) { at := time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC) h := seat.HealthByNodeValue{Node: "n", At: at, Fallback: &seat.LoginFallback{Node: "n", From: "old", Why: "w", At: at}} raw, err := EncodeHealth(h) if err != nil { t.Fatal(err) } back, err := DecodeHealth(raw) h.V = keys.Version if err != nil || !SameHealth(back, h) { t.Fatalf("round trip: %v %+v", err, back) } later := h later.At = at.Add(time.Hour) if !SameHealth(h, later) { t.Error("a report differs by its time of writing") } for name, bad := range map[string]seat.HealthByNodeValue{ "no node": {}, "another node": {Node: "n", Fallback: &seat.LoginFallback{Node: "m", From: "o", Why: "w", At: at}}, "no time": {Node: "n", Fallback: &seat.LoginFallback{Node: "n", From: "o", Why: "w"}}, "no login from": {Node: "n", Fallback: &seat.LoginFallback{Node: "n", Why: "w", At: at}}, } { if _, err := EncodeHealth(bad); err == nil { t.Errorf("%s: encoded", name) } } if _, err := DecodeHealth([]byte(`{"v":2,"node":"n"}`)); !errors.Is(err, keys.ErrUnknownVersion) { t.Errorf("version 2: %v", err) } d := seat.DataByNodeModuleValue{Node: "n", Module: "m", Digest: digestA} raw, err = EncodeData(d) if err != nil || !strings.Contains(string(raw), `"items":[]`) { t.Fatalf("data: %v %s", err, raw) } if _, err := EncodeData(seat.DataByNodeModuleValue{Node: "n", Module: "node"}); err == nil { t.Error("the module name node keys") } if k, err := HaveNodeKey("n"); err != nil || k != "n.node" { t.Errorf("node key %q %v", k, err) } } // The lists a node's facts carry (windows, busy, strays, filters) are arrays on the bus, as the engine has always written // them; the contract once said objects and refused every real value. func TestHaveNodeReadsTheListsTheEngineWrites(t *testing.T) { raw := []byte(`{"v":1,"windows":[{"step":"backup"}],"busy":[{"id":"x"}],"strays":[{"kind":"container"}],"filters":[{"name":"f"}]}`) n, err := DecodeHaveNode(raw) if err != nil { t.Fatal(err) } if len(n.Windows) != 1 || len(n.Busy) != 1 || len(n.Strays) != 1 || len(n.Filters) != 1 { t.Fatalf("lists lost: %+v", n) } }