package nodeengineclient import ( "errors" "fmt" "reflect" "strings" "testing" seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-sdk/go/delivery" ) // The verb compose-candidate's ask and answer (ADR 0593 §7, §9): each round-trips, and a reader refuses what it could // not take, naming it. func TestComposeCandidateRoundTripsAndRefuses(t *testing.T) { key, err := delivery.CompositionKey("mesh-sdk#7", "0123456789ab") if err != nil { t.Fatal(err) } raw, err := EncodeAsk(seat.ComposeCandidateInput{Candidate: key, Input: true}) if err != nil { t.Fatal(err) } if a, err := DecodeAsk(raw); err != nil || a.Candidate != key || !a.Input { t.Fatalf("ask round trip: %v, %+v", err, a) } if _, err := DecodeAsk([]byte(`{"candidate":"` + key + `","other":1}`)); err == nil { t.Error("an ask with an unknown key was read") } if _, err := EncodeAsk(seat.ComposeCandidateInput{Candidate: "nope"}); err == nil { t.Error("an ask naming no candidate key was encoded") } good := seat.ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: Refused, Took: 812, Own: seat.Module{Module: Own, State: ModuleComposed, Digest: "sha256:00"}, Modules: []seat.Module{ {Module: "netcheck", Fp: "src2:x", State: ModuleComposed, Digest: "sha256:01", Changed: true}, {Module: "route-proxy", State: ModuleRefused, Words: "the setting domain has no value"}, {Module: "zz", State: ModuleHeld, Words: "held before"}, }, Input: map[string]any{"machines": []any{"ace"}}} raw, err = EncodeAnswer(good) if err != nil { t.Fatal(err) } back, err := DecodeAnswer(raw) if err != nil || back.Verdict != Refused || len(back.Modules) != 3 || !reflect.DeepEqual(back.Input, good.Input) { t.Fatalf("answer round trip: %v, %+v", err, back) } if _, err := DecodeAnswer([]byte(strings.Replace(string(raw), `"took"`, `"other":1,"took"`, 1))); err == nil { t.Error("an answer with an unknown key was read") } if _, err := DecodeAnswer([]byte(strings.Replace(string(raw), `"v":1`, `"v":2`, 1))); !errors.Is(err, ErrUnknownVersion) { t.Errorf("an answer at another version read as %v", err) } numbered := strings.Replace(string(raw), `"machines":["ace"]`, `"bound":12345678901234567890`, 1) if back, err := DecodeAnswer([]byte(numbered)); err != nil || fmt.Sprint(back.Input["bound"]) != "12345678901234567890" { t.Errorf("an open input's number did not keep its digits: %v, %+v", err, back.Input) } whole := seat.ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: Refused, Refused: "the bound passed"} unready := seat.ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: Unready, Refused: "the agent is starting"} notJudged := seat.ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: NotJudged, Refused: "its node-engine sends no running state"} for _, a := range []seat.ComposeCandidateAnswer{whole, unready, notJudged} { if raw, err := EncodeAnswer(a); err != nil { t.Errorf("a %s answer was not encoded: %v", a.Verdict, err) } else if back, err := DecodeAnswer(raw); err != nil || back.Verdict != a.Verdict { t.Errorf("a %s answer read back as %+v, %v", a.Verdict, back, err) } } for name, c := range map[string]struct { from seat.ComposeCandidateAnswer mutate func(a *seat.ComposeCandidateAnswer) }{ "verdict not the modules'": {good, func(a *seat.ComposeCandidateAnswer) { a.Verdict = Composed }}, "unknown state": {good, func(a *seat.ComposeCandidateAnswer) { a.Modules[0].State = "fine" }}, "composed without digest": {good, func(a *seat.ComposeCandidateAnswer) { a.Modules[0].Digest = "" }}, "refused without words": {good, func(a *seat.ComposeCandidateAnswer) { a.Modules[1].Words = "" }}, "unsorted": {good, func(a *seat.ComposeCandidateAnswer) { a.Modules[0], a.Modules[1] = a.Modules[1], a.Modules[0] }}, "own misnamed": {good, func(a *seat.ComposeCandidateAnswer) { a.Own.Module = "ace" }}, "whole refusal with modules": {good, func(a *seat.ComposeCandidateAnswer) { a.Refused = "the bound passed" }}, "node with a dot": {good, func(a *seat.ComposeCandidateAnswer) { a.Node = "a.b" }}, "negative took": {good, func(a *seat.ComposeCandidateAnswer) { a.Took = -1 }}, "unready saying no why": {unready, func(a *seat.ComposeCandidateAnswer) { a.Refused = "" }}, "unready naming modules": {unready, func(a *seat.ComposeCandidateAnswer) { a.Modules = good.Modules }}, "not judged saying no why": {notJudged, func(a *seat.ComposeCandidateAnswer) { a.Refused = "" }}, "not judged naming modules": {notJudged, func(a *seat.ComposeCandidateAnswer) { a.Modules = good.Modules }}, } { a := c.from a.Modules = append([]seat.Module(nil), c.from.Modules...) c.mutate(&a) if _, err := EncodeAnswer(a); err == nil { t.Errorf("%s: encoded", name) } } }