Files

131 lines
5.8 KiB
Go

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)
}
}