package nodeengineclient import ( "encoding/json" "fmt" "os" "strings" "testing" seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" ) // captured is resources of ace's declaration of 2026-10-24 (the controller's `plan` verb answered it; testdata/captured-ace.json // keeps three modules of it, never typed by hand), composed here by module under a stand-in fingerprint. func captured(t *testing.T) (account string, byModule map[string][]json.RawMessage) { t.Helper() raw, err := os.ReadFile("testdata/captured-ace.json") if err != nil { t.Fatal(err) } var c struct { Account string `json:"provisioner_account"` Resources []json.RawMessage `json:"resources"` } if err := json.Unmarshal(raw, &c); err != nil { t.Fatal(err) } byModule = map[string][]json.RawMessage{} for _, r := range c.Resources { var h struct { Module string `json:"module"` } _ = json.Unmarshal(r, &h) byModule[h.Module] = append(byModule[h.Module], r) } return c.Account, byModule } func fp(n int) string { return SourcePrefix + fmt.Sprintf("%064x", n) } // TestCompositionDigests: a composition's digest is the one the first writer computed (the values are the digests // mesh-sdk's delivery package gave for the same compositions when it moved here), whatever order the contract's generated // types sort their fields in, and the plan is no part of it. func TestCompositionDigests(t *testing.T) { account, by := captured(t) own := seat.NodeOwn{ Adoption: &seat.Adoption{Taken: []string{"avahi"}, Untaken: map[string][]string{"plex": {"plex.bundle-code"}}}, Identities: []seat.DeclaredIdentity{{Module: "plex", Name: "plex", Uid: 1001, Gid: 1001, Groups: []int64{1002}, Runner: "plex"}}, Groups: []seat.DeclaredGroup{{Name: "mesh-media", Gid: 1002}}, Libraries: []seat.DeclaredLibrary{{Library: "movies", Root: "/srv/media/movies", Group: "mesh-media", Gid: 1002, Taken: true, Paths: []string{"/srv/media/movies"}}}, ProvisionerAccount: "mesh-provisioner-client", } cases := []struct { name string c Composition digest string }{ {"a node's own, the account only", Composition{Node: "ace", Own: seat.NodeOwn{ProvisionerAccount: account}}, "sha256:17fde3a215d522619c18bc80b8c236975e25b2d2980a984aebd7c18376b095a2"}, {"a node's own, every part", Composition{Node: "ace", Own: own}, "sha256:b8ac7ace490a3c483c3ae18a9cda58e607aa6b69167fa274c062949c7b34fdbf"}, {"one package", Composition{Node: "ace", Module: "buildah", FP: fp(7), Resources: by["buildah"]}, "sha256:84e480a7a62333960b3e4be21a7548bbaa09572ef73ca8fb9dc6dfd1228aeb6b"}, {"a service and a file", Composition{Node: "ace", Module: "mesh-wireguard", FP: fp(14), Resources: by["mesh-wireguard"]}, "sha256:d6edec2474f606503e55d4ee2e0b4f5d1ffc25d016e4bc595e86ea67fe205b8c"}, {"a plan beside the resources", Composition{Node: "ace", Module: "hostname", FP: fp(8), Resources: by["hostname"], Plan: json.RawMessage(`{"files":[{"path":"/etc/hostname"}]}`)}, "sha256:49e17d6c471ee65cdef745ca3ac5cea4d37b6a100e11d8434dc64a6e2949e57e"}, } for _, c := range cases { got, err := ComposedDigest(c.c) if err != nil || got != c.digest { t.Errorf("%s: digest %s, %v; want %s", c.name, got, err, c.digest) } raw, err := EncodeComposition(c.c) if err != nil { t.Errorf("%s: %v", c.name, err) continue } back, err := DecodeComposition(raw) if err != nil { t.Errorf("%s: %v", c.name, err) continue } if !SameComposition(c.c, back) { t.Errorf("%s: reads back as another composition", c.name) } again, _ := ComposedDigest(back) if again != c.digest { t.Errorf("%s: read back, its digest is %s", c.name, again) } } a := Composition{Node: "ace", Module: "hostname", FP: fp(8), Resources: by["hostname"], Plan: json.RawMessage(`{"a":1}`)} b := a b.Plan = json.RawMessage(`{"a":2}`) da, _ := ComposedDigest(a) db, _ := ComposedDigest(b) if da != db || SameComposition(a, b) { t.Error("a plan changes the composition's encoding and not its digest") } } // TestCompositionRefusals: what the codec refuses. func TestCompositionRefusals(t *testing.T) { _, by := captured(t) ok := Composition{Node: "ace", Module: "buildah", FP: fp(7), Resources: by["buildah"]} cases := []struct { name string c Composition want string }{ {"an old-form fingerprint", Composition{Node: "ace", Module: "buildah", FP: "src1:abc", Resources: ok.Resources}, "new form"}, {"a resource of another module", Composition{Node: "ace", Module: "hostname", FP: fp(8), Resources: ok.Resources}, "is of"}, {"the node's own parts on a module", Composition{Node: "ace", Module: "buildah", FP: fp(7), Own: seat.NodeOwn{ProvisionerAccount: "x"}}, "node's own parts"}, {"a refusal that holds resources", Composition{Node: "ace", Module: "buildah", FP: fp(7), Resources: ok.Resources, Refused: "no secret"}, "refused and holds"}, {"the node's own with a fingerprint", Composition{Node: "ace", FP: fp(7)}, "names no fingerprint"}, {"a node name with a dot", Composition{Node: "a.ce"}, "node"}, } for _, c := range cases { if _, err := EncodeComposition(c.c); err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("%s: %v, want an error saying %q", c.name, err, c.want) } } if _, err := ComposedDigest(Composition{Node: "ace", Module: "buildah", FP: fp(7), Refused: "no secret"}); err == nil { t.Error("a refused composition has a digest") } if _, err := DecodeComposition([]byte(`{"v":2,"node":"ace","resources":[]}`)); err == nil { t.Error("a composition of version 2 is read") } if _, err := DecodeComposition([]byte(`{"v":1,"node":"ace","resources":[],"extra":1}`)); err == nil { t.Error("a composition with a field it does not know is read") } if _, err := DecodeComposition([]byte(`{"v":1,"node":"ace","resources":[],"signature":"x"}`)); err != nil { t.Errorf("a signed composition of the ended controller is refused: %v", err) } }