package compositions import ( "bytes" "encoding/json" "fmt" seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "slices" "strings" "testing" ) // What the node-engine finds when it compares the compositions it made with the ones it runs (issue 1126): the bus's user // list left out, a resource that differs named with both digests, a value sealed again judged by what it opens to, every // differing body shown with its secrets masked, and the paths found on the whole bodies. Moved from mesh-sdk's delivery // package with the code (ADR 0634 ยง1: only the node-engine compares compositions). func TestComparingCompositions(t *testing.T) { // CompareComposed: the bus's user list is left out on both sides; anything else that differs is named. file := func(id, content string) json.RawMessage { return json.RawMessage(`{"id":"` + id + `","type":"file","module":"nats","path":"/etc/` + id + `","mode":"0600","content":"` + content + `"}`) } fp := "src2:" + strings.Repeat("a", 64) comp := func(rs ...json.RawMessage) Composed { return Composed{Node: "novox", Module: "nats", FP: fp, Resources: rs} } for name, tc := range map[string]struct { composed, running Composed resource, why string }{ "user list alone": {comp(file("conf", "a"), file("nats.bus-users", "stand-in")), comp(file("conf", "a"), file("nats.bus-users", "real")), "", ""}, "a resource": {comp(file("conf", "a")), comp(file("conf", "b")), "conf", "the resource differs"}, "one side": {comp(file("conf", "a"), file("more", "x")), comp(file("conf", "a")), "more", "composed, and not in what runs"}, "order": {comp(file("conf", "a"), file("more", "x")), comp(file("more", "x"), file("conf", "a")), "", "the same resources in another order"}, } { c := CompareComposed(map[string]Composed{"novox.nats." + fp: tc.composed}, []Composed{tc.running}) if tc.why == "" { if len(c.Differs) != 0 || len(c.Same) != 1 { t.Errorf("%s: %+v", name, c) } } else if len(c.Differs) != 1 || c.Differs[0].Resource != tc.resource || c.Differs[0].Why != tc.why || c.Differs[0].Composed == "" || c.Differs[0].Running == "" || c.Differs[0].Composed == c.Differs[0].Running { t.Errorf("%s: differs %+v, want %s: %s with both digests", name, c.Differs, tc.resource, tc.why) } } // Issue 1126, the shadow shows its work: every differing resource with both bodies, each value a secret could be // in masked (never a secret in an answer), saying whether the two sides hold the same value. sealedFile := func(id, sealed, mode string) json.RawMessage { return json.RawMessage(`{"id":"` + id + `","type":"file","module":"nats","path":"/etc/` + id + `","mode":"` + mode + `","sealed":"` + sealed + `"}`) } // Issue 1126, C1: a sealed value is judged by the value it opens to at its own slot: b sealed again (box-2 and // box-3 open to one value) is no difference; d holds at its slot a box of another value, though that value is // the one b holds, and differs at its path `sealed`. sameBox := func(a, b string) (same, known bool) { return (a == "box-2" && b == "box-3") || a == b, true } shown := CompareComposedWith(map[string]Composed{"k": comp(sealedFile("a", "box-1", "0600"), sealedFile("b", "box-2", "0600"), file("c", "x"), sealedFile("d", "box-2", "0600"))}, []Composed{comp(sealedFile("a", "box-1", "0644"), sealedFile("b", "box-3", "0600"), file("c", "y"), sealedFile("d", "box-4", "0600"))}, sameBox) if len(shown.Differs) != 1 || len(shown.Differs[0].Bodies) != 3 { t.Fatalf("every differing resource is not shown, or b sealed again is shown: %+v", shown.Differs) } if c := CompareComposedWith(map[string]Composed{"k": comp(sealedFile("b", "box-2", "0600"))}, []Composed{comp(sealedFile("b", "box-3", "0600"))}, sameBox); len(c.Same) != 1 { t.Errorf("a value sealed again at its own slot is not the same: %+v", c) } for _, row := range []struct { id, has, hasNot string paths []string }{ {"a", `"mode":"0600"`, "box-1", []string{"mode"}}, {"c", `"content":"x"`, "", []string{"content"}}, {"d", "", "box-2", []string{"sealed"}}, } { var body seat.CandidateBodies for _, b := range shown.Differs[0].Bodies { if b.ID == row.id { body = b } } both := body.Composed + body.Running if !strings.Contains(both, row.has) || (row.hasNot != "" && strings.Contains(both, row.hasNot)) || !slices.Equal(body.Paths, row.paths) { t.Errorf("%s shown as %+v, want paths %v", row.id, body, row.paths) } } // Issue 1126, C1, the seed-7 probe: a file naming secrets carries each sealed value in its `secrets`, and each entry // is a slot of its own. The shape is intoFile's output, captured from catalogue for a file naming two secrets // (2026-10-28), with the module a composition names each resource of; only the boxes are replaced. withSecrets := func(db, key string) json.RawMessage { return json.RawMessage(`{"content":"[database]\npassword = ${secret:postgres-database}\n[security]\nsecret_key = ${secret:secret-key}\n",` + `"id":"grafana.ini","mode":"0600","module":"nats","path":"/etc/grafana/grafana.ini","secrets":{"postgres-database":"` + db + `","secret-key":"` + key + `"},"type":"file"}`) } if c := CompareComposedWith(map[string]Composed{"k": comp(withSecrets("box-2", "box-1"))}, []Composed{comp(withSecrets("box-3", "box-1"))}, sameBox); len(c.Same) != 1 { t.Errorf("a secret sealed again at its own entry is not the same: %+v", c) } if kept := KeepSeals(comp(withSecrets("box-2", "box-1")), SealsIn(comp(withSecrets("box-3", "box-1"))), sameBox); !bytes.Equal(kept.Resources[0], withSecrets("box-3", "box-1")) { t.Errorf("the box that runs is not kept byte for byte: %s", kept.Resources[0]) } if s := SealsIn(comp(withSecrets("box-2", "box-1"))); s[SealSlot("grafana.ini", SecretSlotField("postgres-database"))] != "box-2" || len(s) != 2 { t.Errorf("the secrets' slots are %v", s) } // Another value at one entry differs there alone, each entry masked by what it opens to. // box-2 and box-4 open to one value, so the box that runs is kept there; box-5 and box-6 are two values. mixed := CompareComposedWith(map[string]Composed{"k": comp(withSecrets("box-2", "box-5"))}, []Composed{comp(withSecrets("box-4", "box-6"))}, func(a, b string) (bool, bool) { return (a == "box-2" && b == "box-4") || a == b, true }) if len(mixed.Differs) != 1 || len(mixed.Differs[0].Bodies) != 1 { t.Fatalf("a secret of another value is not shown: %+v", mixed) } if body := mixed.Differs[0].Bodies[0]; !slices.Equal(body.Paths, []string{"secrets.secret-key"}) || !strings.Contains(body.Composed, `"secret-key":""`) || strings.Contains(body.Composed+body.Running, "box-") || !strings.Contains(body.Running, `"postgres-database":""`) { t.Errorf("a file's secrets shown as %+v", body) } if c, _ := MaskedPair(withSecrets("box-2", "box-5"), withSecrets("box-4", "box-6"), func(a, b string) (bool, bool) { return a == "box-2", true }); !strings.Contains(c, `"postgres-database":""`) || !strings.Contains(c, `"secret-key":""`) { t.Errorf("each secret is not masked by what it opens to: %s", c) } // Only a file's secrets are split by entry: a vault box (an object under `vault-sealed`) is one masked word and one // path, judged whole by what it opens to (rev-seed-same-3, round 1). box := func(ct string) json.RawMessage { return json.RawMessage(`{"id":"core-box","type":"file","module":"nats","path":"/etc/core","mode":"0600","vault-sealed":{"v":1,"eph":"e-` + ct + `","ct":"` + ct + `"}}`) } vault := CompareComposedWith(map[string]Composed{"k": comp(box("c1"))}, []Composed{comp(box("c2"))}, func(a, b string) (bool, bool) { return false, strings.Contains(a, `"ct":"c1"`) && strings.Contains(b, `"ct":"c2"`) }) if len(vault.Differs) != 1 || len(vault.Differs[0].Bodies) != 1 { t.Fatalf("a box of another value is not shown: %+v", vault) } if body := vault.Differs[0].Bodies[0]; !slices.Equal(body.Paths, []string{"vault-sealed"}) || !strings.Contains(body.Composed, `"vault-sealed":""`) || strings.Contains(body.Composed+body.Running, "c1") { t.Errorf("a vault box shown as %+v", body) } // Issue 1126, T1: the paths are found on the whole bodies, before they are cut; JSON in a string is walked into, a // key a secret could be under is named and never shown. long := strings.Repeat("x", MaxBody) conf := func(link, password string) json.RawMessage { inner, _ := json.Marshal(map[string]any{"pad": long, "links": []string{"a", link}, "password": password}) outer, _ := json.Marshal(map[string]any{"id": "conf", "type": "file", "module": "nats", "path": "/etc/conf", "mode": "0600", "content": string(inner)}) return outer } far := CompareComposedWith(map[string]Composed{"k": comp(conf("b", "hunter2"))}, []Composed{comp(conf("c", "hunter3"))}, nil) if len(far.Differs) != 1 || len(far.Differs[0].Bodies) != 1 || !slices.Equal(far.Differs[0].Bodies[0].Paths, []string{"content.links.1", "content.password"}) || !strings.Contains(far.Differs[0].Bodies[0].Composed, "cut at") || strings.Contains(fmt.Sprint(far), "hunter") { t.Errorf("a difference past the cut: %+v", far.Differs) } if c, r := MaskedPair(json.RawMessage(`{"env":{"DB_PASSWORD":"hunter2","PORT":"80"},"list":[{"token":"t1"}]}`), json.RawMessage(`{"env":{"DB_PASSWORD":"hunter3","PORT":"81"},"list":[{"token":"t1"}]}`), nil); strings.Contains(c+r, "hunter") || strings.Contains(c+r, `"t1"`) || !strings.Contains(r, `"PORT":"81"`) || !strings.Contains(c, "") || !strings.Contains(c, "") { t.Errorf("masked as %s and %s", c, r) } if c := CompareComposed(nil, []Composed{{Node: "novox", Resources: []json.RawMessage{}}}); len(c.Removed) != 1 || c.Removed[0] != "node" { t.Errorf("the node's own composition run and not composed is removed: %+v", c) } if c := CompareComposed(map[string]Composed{"k": comp(file("conf", "a"))}, nil); len(c.Added) != 1 || c.Added[0].Module != "nats" || c.Added[0].Composed == "" { t.Errorf("a composition not run is added: %+v", c) } }