323 lines
11 KiB
Go
323 lines
11 KiB
Go
package delivery
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
)
|
|
|
|
// capturedDeclaration is a node's declaration as the controller's `plan` verb answered it on 2026-10-24 at 16:19
|
|
// (testdata/declaration-<node>.json.gz; the resources carrying a sealed value or a secret taken out, and the filled
|
|
// files that named them), never typed by hand.
|
|
type capturedDeclaration struct {
|
|
Builds map[string]string `json:"builds"`
|
|
Resources []json.RawMessage `json:"resources"`
|
|
Filled []string `json:"filled_files"`
|
|
Provisioner string `json:"provisioner_account"`
|
|
}
|
|
|
|
func readCaptured(t *testing.T, node string) capturedDeclaration {
|
|
t.Helper()
|
|
f, err := os.Open("testdata/declaration-" + node + ".json.gz")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
z, err := gzip.NewReader(f)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := io.ReadAll(z)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var c capturedDeclaration
|
|
if err := json.Unmarshal(raw, &c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// fpOfBuild stands for each module's fingerprint in a captured declaration, which names builds, not fingerprints.
|
|
func fpOfBuild(build string) string {
|
|
var b strings.Builder
|
|
for b.Len() < 64 {
|
|
b.WriteString(fmt.Sprintf("%x", []byte(build)))
|
|
}
|
|
return SourcePrefix2 + b.String()[:64]
|
|
}
|
|
|
|
func splitCaptured(t *testing.T, node string, c capturedDeclaration) map[string]Composed {
|
|
t.Helper()
|
|
fps := map[string]string{}
|
|
for m, b := range c.Builds {
|
|
fps[m] = fpOfBuild(b)
|
|
}
|
|
// mesh-wireguard's resources are composed with no build of it named; its fingerprint is the indexer's, as every
|
|
// module's is, and stood in for here by its name.
|
|
for _, r := range c.Resources {
|
|
var head struct {
|
|
Module string `json:"module"`
|
|
}
|
|
_ = json.Unmarshal(r, &head)
|
|
if _, ok := fps[head.Module]; head.Module != "" && !ok {
|
|
fps[head.Module] = fpOfBuild(head.Module)
|
|
}
|
|
}
|
|
split, err := SplitComposed(node, c.Resources, fps, c.Filled, NodeOwn{Provisioner: c.Provisioner})
|
|
if err != nil {
|
|
t.Fatalf("SplitComposed(%s): %v", node, err)
|
|
}
|
|
return split
|
|
}
|
|
|
|
func canonical(t *testing.T, raw json.RawMessage) string {
|
|
t.Helper()
|
|
var v any
|
|
d := json.NewDecoder(bytes.NewReader(raw))
|
|
d.UseNumber()
|
|
if err := d.Decode(&v); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, _ := json.Marshal(v)
|
|
return string(out)
|
|
}
|
|
|
|
// 1943/AC2: each module's slice of a captured declaration of each node, and the node's own resources, round-trip
|
|
// through the composition's encoding with the same digest; together they are the declaration's resources, each once,
|
|
// each in the composition of the module it says.
|
|
func TestCompositionsOfACapturedDeclaration(t *testing.T) {
|
|
t.Parallel()
|
|
for _, node := range []string{"ace", "g14"} {
|
|
t.Run(node, func(t *testing.T) {
|
|
t.Parallel()
|
|
oneCapturedDeclaration(t, node)
|
|
})
|
|
}
|
|
}
|
|
|
|
func oneCapturedDeclaration(t *testing.T, node string) {
|
|
{
|
|
c := readCaptured(t, node)
|
|
split := splitCaptured(t, node, c)
|
|
modules := map[string]bool{}
|
|
for m := range c.Builds {
|
|
modules[m] = true
|
|
}
|
|
var union []string
|
|
for key, comp := range split {
|
|
n, module, fp, isNode, err := ParseComposedKey(key)
|
|
if err != nil || n != node || comp.Node != node || comp.Module != module || comp.FP != fp {
|
|
t.Fatalf("%s: key %q holds %s/%s/%s (%v)", node, key, comp.Node, comp.Module, comp.FP, err)
|
|
}
|
|
if !isNode && !modules[module] && module != "mesh-wireguard" {
|
|
t.Errorf("%s: a composition of %s, which the declaration names no build of", node, module)
|
|
}
|
|
raw, err := EncodeComposed(comp)
|
|
if err != nil {
|
|
t.Fatalf("%s: EncodeComposed(%s): %v", node, key, err)
|
|
}
|
|
back, err := DecodeComposed(raw)
|
|
if err != nil {
|
|
t.Fatalf("%s: DecodeComposed(%s): %v", node, key, err)
|
|
}
|
|
if !SameComposed(back, comp) {
|
|
t.Errorf("%s: %s did not round-trip", node, key)
|
|
}
|
|
// SameComposed encodes both, so the digest of what was read back, a hash of its encoding, is the one written.
|
|
if d, err := ComposedDigestOf(back); err != nil || !digestForm.MatchString(d) {
|
|
t.Errorf("%s: %s digests %q (%v)", node, key, d, err)
|
|
}
|
|
for _, r := range back.Resources {
|
|
var head struct {
|
|
Module string `json:"module"`
|
|
}
|
|
_ = json.Unmarshal(r, &head)
|
|
if head.Module != module {
|
|
t.Errorf("%s: %s holds a resource of %q", node, key, head.Module)
|
|
}
|
|
union = append(union, canonical(t, r))
|
|
}
|
|
}
|
|
var want []string
|
|
for _, r := range c.Resources {
|
|
want = append(want, canonical(t, r))
|
|
}
|
|
sort.Strings(union)
|
|
sort.Strings(want)
|
|
if strings.Join(union, "\n") != strings.Join(want, "\n") {
|
|
t.Errorf("%s: the compositions hold %d resources, the declaration %d, or they differ", node, len(union), len(want))
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1943/AC2: one changed byte of a resource is another digest; the same composition written with its keys in another
|
|
// order is the same digest.
|
|
func TestComposedDigestFollowsTheContent(t *testing.T) {
|
|
c := readCaptured(t, "g14")
|
|
split := splitCaptured(t, "g14", c)
|
|
var comp Composed
|
|
for _, v := range split {
|
|
if v.Module != "" && len(v.Resources) > 1 {
|
|
comp = v
|
|
break
|
|
}
|
|
}
|
|
d1, _ := ComposedDigestOf(comp)
|
|
var m map[string]json.RawMessage
|
|
_ = json.Unmarshal(comp.Resources[0], &m)
|
|
var keys []string
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Sort(sort.Reverse(sort.StringSlice(keys)))
|
|
var parts []string
|
|
for _, k := range keys {
|
|
parts = append(parts, fmt.Sprintf("%q : %s", k, m[k]))
|
|
}
|
|
reordered := json.RawMessage("{ " + strings.Join(parts, ",\n ") + " }")
|
|
shuffled := comp
|
|
shuffled.Resources = append([]json.RawMessage{reordered}, comp.Resources[1:]...)
|
|
if d2, _ := ComposedDigestOf(shuffled); d2 != d1 {
|
|
t.Errorf("the same resources in another order and spacing digest %s and %s", d1, d2)
|
|
}
|
|
changed := comp
|
|
changed.Resources = append([]json.RawMessage{json.RawMessage(strings.Replace(canonical(t, comp.Resources[0]), `"id":"`, `"id":"x`, 1))}, comp.Resources[1:]...)
|
|
if d3, _ := ComposedDigestOf(changed); d3 == d1 {
|
|
t.Errorf("a changed resource kept its digest")
|
|
}
|
|
revised := comp
|
|
revised.Data = !comp.Data
|
|
if d4, _ := ComposedDigestOf(revised); d4 == d1 {
|
|
t.Errorf("a changed data flag kept its digest")
|
|
}
|
|
}
|
|
|
|
// 1943/AC2: a file over 64 KiB is carried by digest and size, its content handed back to be put in the node's object
|
|
// bucket; a composition that carries such a file whole is refused.
|
|
func TestAComposedFileOver64KiBIsCarriedByDigest(t *testing.T) {
|
|
c := readCaptured(t, "g14")
|
|
split := splitCaptured(t, "g14", c)
|
|
var comp Composed
|
|
var at int
|
|
for _, v := range split {
|
|
for i, r := range v.Resources {
|
|
var f map[string]any
|
|
_ = json.Unmarshal(r, &f)
|
|
if f["type"] == "file" && v.Module != "" && f["bytes"] == nil && f["into"] == nil {
|
|
comp, at = v, i
|
|
break
|
|
}
|
|
}
|
|
if comp.Module != "" {
|
|
break
|
|
}
|
|
}
|
|
var f map[string]any
|
|
_ = json.Unmarshal(comp.Resources[at], &f)
|
|
// Grown past the threshold from a captured file: no captured file of this node is over 64 KiB.
|
|
body := strings.Repeat("x", declaration.FileReferenceThreshold+1)
|
|
f["content"] = body
|
|
big, _ := json.Marshal(f)
|
|
comp.Resources = append([]json.RawMessage{}, comp.Resources...)
|
|
comp.Resources[at] = big
|
|
if _, err := EncodeComposed(comp); err == nil || !strings.Contains(err.Error(), "by digest") {
|
|
t.Fatalf("a file over 64 KiB carried whole = %v, want refused", err)
|
|
}
|
|
byDigest, objects, err := comp.ByDigest()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var ref map[string]any
|
|
_ = json.Unmarshal(byDigest.Resources[at], &ref)
|
|
digest := declaration.DigestOf([]byte(body))
|
|
if ref["digest"] != digest || ref["size"] != float64(len(body)) || ref["content"] != nil {
|
|
t.Errorf("the file became %v, want digest %s and size %d", ref, digest, len(body))
|
|
}
|
|
if string(objects[digest]) != body || len(objects) != 1 {
|
|
t.Errorf("the objects handed back are %d, want the file's content under its digest", len(objects))
|
|
}
|
|
if _, err := EncodeComposed(byDigest); err != nil {
|
|
t.Errorf("the composition by digest is refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// 1943/AC2: an unknown version is refused as one; a module's composition carrying the node's own parts, a resource of
|
|
// another module, a resource its kind refuses, or no fingerprint, is refused, naming why.
|
|
func TestComposedRefusals(t *testing.T) {
|
|
fp := fp2of('a')
|
|
file := json.RawMessage(`{"id":"m.f","type":"file","module":"m","path":"/etc/m","content":"x"}`)
|
|
ok := Composed{Node: "ace", Module: "m", FP: fp, Resources: []json.RawMessage{file}}
|
|
raw, err := EncodeComposed(ok)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := DecodeComposed(bytes.Replace(raw, []byte(`"v":1`), []byte(`"v":2`), 1)); !errors.Is(err, ErrUnknownVersion) {
|
|
t.Errorf("version 2 = %v, want ErrUnknownVersion", err)
|
|
}
|
|
refusals := []struct {
|
|
c Composed
|
|
why string
|
|
}{
|
|
{Composed{Node: "ace", Module: "m", Resources: []json.RawMessage{file}}, "fingerprint"},
|
|
{Composed{Node: "", Module: "m", FP: fp, Resources: []json.RawMessage{file}}, "node name is empty"},
|
|
{Composed{Node: "ace", Module: "other", FP: fp, Resources: []json.RawMessage{file}}, `of "m"`},
|
|
{Composed{Node: "ace", Module: "m", FP: fp, Resources: []json.RawMessage{file}, Own: NodeOwn{Provisioner: "p"}}, "node's own"},
|
|
{Composed{Node: "ace", FP: fp, Resources: []json.RawMessage{}}, "names no fingerprint"},
|
|
{Composed{Node: "ace", Resources: []json.RawMessage{file}}, `of "m"`},
|
|
{Composed{Node: "ace", Module: "m", FP: fp, Resources: []json.RawMessage{json.RawMessage(`{"id":"m.f","type":"file","module":"m","path":"/etc/m","content":"x","image":"y"}`)}}, `"image"`},
|
|
{Composed{Node: "ace", Module: "m", FP: fp, Resources: []json.RawMessage{json.RawMessage(`{"id":"m.a","type":"action","module":"m"}`)}}, "action"},
|
|
}
|
|
for _, r := range refusals {
|
|
if _, err := EncodeComposed(r.c); err == nil || !strings.Contains(err.Error(), r.why) {
|
|
t.Errorf("EncodeComposed(%s/%s) = %v, want a refusal saying %q", r.c.Node, r.c.Module, err, r.why)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1943/AC2 (issue 1150): a module whose name holds a dot keys its have, its want and its composition escaped, and the
|
|
// parsers give its name back; a name with a wildcard, whitespace or an escape of its own is still refused.
|
|
func TestADottedModuleKeysEscaped(t *testing.T) {
|
|
fp := fp2of('f')
|
|
have, err := HaveKey("ace", "novox.be")
|
|
if err != nil || have != "ace.novox=2ebe" {
|
|
t.Fatalf("HaveKey = %q, %v", have, err)
|
|
}
|
|
if n, m, isNode, err := ParseHaveKey(have); err != nil || n != "ace" || m != "novox.be" || isNode {
|
|
t.Errorf("ParseHaveKey(%q) = %q, %q, %v, %v", have, n, m, isNode, err)
|
|
}
|
|
want, err := WantKey("ace", "novox.be")
|
|
if err != nil || want != "want.ace.novox=2ebe" {
|
|
t.Fatalf("WantKey = %q, %v", want, err)
|
|
}
|
|
if n, m, err := ParseWantKey(want); err != nil || n != "ace" || m != "novox.be" {
|
|
t.Errorf("ParseWantKey(%q) = %q, %q, %v", want, n, m, err)
|
|
}
|
|
comp, err := ComposedKey("ace", "novox.be", fp)
|
|
if err != nil || comp != "ace.novox=2ebe."+strings.Repeat("f", 64) {
|
|
t.Fatalf("ComposedKey = %q, %v", comp, err)
|
|
}
|
|
if n, m, back, _, err := ParseComposedKey(comp); err != nil || n != "ace" || m != "novox.be" || back != fp {
|
|
t.Errorf("ParseComposedKey(%q) = %q, %q, %q, %v", comp, n, m, back, err)
|
|
}
|
|
for _, bad := range []string{"m*", "m x", "m=2e", "m>", ".m", "m.", "a..b"} {
|
|
if _, err := HaveKey("ace", bad); err == nil {
|
|
t.Errorf("HaveKey(ace, %q) accepted", bad)
|
|
}
|
|
}
|
|
for _, bad := range []string{"ace.m=2", "ace.m=2E", "ace.m=41", "ace.=6eode"} {
|
|
if _, _, _, err := ParseHaveKey(bad); err == nil {
|
|
t.Errorf("ParseHaveKey(%q) accepted", bad)
|
|
}
|
|
}
|
|
}
|