package delivery import ( "encoding/json" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" ) // The verb compose-candidate's ask and answer, and a composition that carries its plan or its refusal (ADR 0593 §6, §7): // each round-trips, and a reader refuses what it could not take, naming it. func TestComposeCandidateAndComposedPlanRoundTripAndRefuse(t *testing.T) { key, err := CompositionKey("mesh-sdk#7", "0123456789ab") if err != nil { t.Fatal(err) } raw, err := EncodeComposeCandidateAsk(ComposeCandidateAsk{Candidate: key, Input: true}) if err != nil { t.Fatal(err) } if a, err := DecodeComposeCandidateAsk(raw); err != nil || a.Candidate != key || !a.Input { t.Fatalf("ask round trip: %v, %+v", err, a) } if _, err := DecodeComposeCandidateAsk([]byte(`{"candidate":"` + key + `","other":1}`)); err == nil { t.Error("an ask with an unknown key was read") } if _, err := EncodeComposeCandidateAsk(ComposeCandidateAsk{Candidate: "nope"}); err == nil { t.Error("an ask naming no candidate key was encoded") } good := ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: ComposeCandidateRefused, Took: 812, Own: ModuleComposed{Module: "node", State: ModuleComposedState, Digest: "sha256:00"}, Modules: []ModuleComposed{ {Module: "netcheck", FP: "src2:x", State: ModuleComposedState, Digest: "sha256:01", Changed: true}, {Module: "route-proxy", State: ModuleRefusedState, Words: "the setting domain has no value"}, {Module: "zz", State: ModuleHeldState, Words: "held before"}, }, Input: json.RawMessage(`{"machines":["ace"]}`)} raw, err = EncodeComposeCandidateAnswer(good) if err != nil { t.Fatal(err) } back, err := DecodeComposeCandidateAnswer(raw) if err != nil || back.Verdict != ComposeCandidateRefused || len(back.Modules) != 3 || string(back.Input) != string(good.Input) { t.Fatalf("answer round trip: %v, %+v", err, back) } for name, mutate := range map[string]func(a *ComposeCandidateAnswer){ "verdict not the modules'": func(a *ComposeCandidateAnswer) { a.Verdict = ComposeCandidateComposed }, "unknown state": func(a *ComposeCandidateAnswer) { a.Modules[0].State = "fine" }, "composed without digest": func(a *ComposeCandidateAnswer) { a.Modules[0].Digest = "" }, "refused without words": func(a *ComposeCandidateAnswer) { a.Modules[1].Words = "" }, "unsorted": func(a *ComposeCandidateAnswer) { a.Modules[0], a.Modules[1] = a.Modules[1], a.Modules[0] }, "own misnamed": func(a *ComposeCandidateAnswer) { a.Own.Module = "ace" }, "whole refusal w/ modules": func(a *ComposeCandidateAnswer) { a.Refused = "the bound passed" }, } { a := good a.Modules = append([]ModuleComposed(nil), good.Modules...) mutate(&a) if _, err := EncodeComposeCandidateAnswer(a); err == nil { t.Errorf("%s: encoded", name) } } whole := ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: ComposeCandidateRefused, Refused: "the bound passed"} if _, err := EncodeComposeCandidateAnswer(whole); err != nil { t.Errorf("a whole refusal was not encoded: %v", err) } unready := ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: ComposeCandidateUnready, Refused: "the agent is starting"} if raw, err := EncodeComposeCandidateAnswer(unready); err != nil { t.Errorf("an unready answer was not encoded: %v", err) } else if back, err := DecodeComposeCandidateAnswer(raw); err != nil || back.Verdict != ComposeCandidateUnready { t.Errorf("an unready answer read back as %+v, %v", back, err) } unready.Refused = "" if _, err := EncodeComposeCandidateAnswer(unready); err == nil { t.Error("an unready answer saying no why was encoded") } unready.Refused, unready.Modules = "starting", good.Modules if _, err := EncodeComposeCandidateAnswer(unready); err == nil { t.Error("an unready answer naming modules was encoded") } if _, err := DecodeComposeCandidateAnswer([]byte(strings.Replace(string(raw), `"took"`, `"other":1,"took"`, 1))); err == nil { t.Error("an answer with an unknown key was read") } fp := "src2:" + strings.Repeat("a", 64) planned := Composed{Node: "ace", Module: "netcheck", FP: fp, Resources: []json.RawMessage{}, Plan: &catalogue.Plan{Files: []catalogue.PlanFile{}}} raw, err = EncodeComposed(planned) if err != nil { t.Fatal(err) } c, err := DecodeComposed(raw) if err != nil || c.Plan == nil { t.Fatalf("a composition's plan did not round-trip: %v, %+v", err, c) } d1, _ := ComposedDigestOf(planned) d0, _ := ComposedDigestOf(Composed{Node: "ace", Module: "netcheck", FP: fp, Resources: []json.RawMessage{}}) if d1 == "" || d1 != d0 { t.Error("the digest moves with the plan: it is the resources' alone") } if SameComposed(planned, Composed{Node: "ace", Module: "netcheck", FP: fp, Resources: []json.RawMessage{}}) { t.Error("a plan change is not a new composition value") } refused := Composed{Node: "ace", Module: "netcheck", FP: fp, Refused: "the setting domain has no value"} if raw, err = EncodeComposed(refused); err != nil { t.Fatal(err) } if c, err := DecodeComposed(raw); err != nil || c.Refused != refused.Refused { t.Fatalf("a refused composition did not round-trip: %v", err) } if _, err := ComposedDigestOf(refused); err == nil { t.Error("a refused composition has a digest") } refused.Data = true if _, err := EncodeComposed(refused); err == nil { t.Error("a refused composition holding data was encoded") } if _, err := EncodeComposed(Composed{Node: "ace", Resources: []json.RawMessage{}, Refused: "x"}); err == nil { t.Error("the node's own composition carried a refusal") } } // A kept import form is set only with imports, and in the fingerprint's form (ADR 0593 §4). func TestAKeptImportFormNeedsImports(t *testing.T) { form := SourcePrefix2 + strings.Repeat("b", 64) in := KeptInputs2{Tree: "t", Imports: []string{"git.novox.be/novox/mesh-sdk/go"}, ImportForm: form} m := Module{FP2: form, Manifest: strings.Repeat("c", 64), Inputs2: &in, Repository: "novox/x", Commit: "c"} if err := m.check(); err != nil && strings.Contains(err.Error(), "import form") { t.Fatalf("a good import form refused: %v", err) } for _, bad := range []KeptInputs2{{Tree: "t", ImportForm: form}, {Tree: "t", Imports: in.Imports, ImportForm: "src2:short"}} { m.Inputs2 = &bad if err := m.check(); err == nil || !strings.Contains(err.Error(), "import form") { t.Errorf("%+v: %v", bad, err) } } }