Files
mesh-sdk/go/catalogue/groups_test.go
T
jschoubben f0f66d5b8d
mesh/merge-gate head f0f66d5b8d42 merged into trunk 66e8fdaa62e2 — pass, with a note: builds mesh-sdk (+41 dependent(s)) → ace, g14, novox, shanks; no bus …
mesh/repo-check head f0f66d5b8d42 merged into trunk 66e8fdaa62e2 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery delivered
A container may add supplementary groups, and a group may be a config value filled at composition (issue 1636, ADR 0480 clause 7)
2026-10-21 14:01:19 +02:00

119 lines
5.0 KiB
Go

package catalogue
import (
"reflect"
"strings"
"testing"
)
// A container's supplementary groups may be an operator's value (novox/hq ADR 0480 clause 7, ADR 0348 §11,
// issue 1636): `groups: ["${config:media-gid}"]`, each element a whole placeholder or a whole number, filled at
// composition with the number the config value holds.
func aContainerWithGroups(groups ...any) map[string]any {
return map[string]any{"id": "server", "type": "container", "name": "n8n", "image": "x@sha256:" + strings.Repeat("a", 64),
"groups": groups}
}
// 1636/AC3: a manifest container with groups: ["${setting:media-gid}"] resolves to the number the setting says.
func TestAContainersGroupIsFilledFromTheSetting(t *testing.T) {
r := aContainerWithGroups("${setting:media-gid}", float64(2001))
layers := []Layer{{From: "ace", Values: map[string]any{"media-gid": float64(2000)}}}
if err := settingInto(r, layers, "n8n"); err != nil {
t.Fatal(err)
}
if want := []any{2000, 2001}; !reflect.DeepEqual(r["groups"], want) {
t.Fatalf("composed as %#v, want %#v", r["groups"], want)
}
// A value the operator wrote as text is read as the number it spells.
r = aContainerWithGroups("${setting:media-gid}")
if err := settingInto(r, []Layer{{From: "ace", Values: map[string]any{"media-gid": "2000"}}}, "n8n"); err != nil {
t.Fatal(err)
}
if want := []any{2000}; !reflect.DeepEqual(r["groups"], want) {
t.Fatalf("composed as %#v, want %#v", r["groups"], want)
}
}
// 1636/AC3: an unset value refuses the module by name, and the key and the remedy are named; nothing is dropped.
func TestAnUnsetGroupRefusesTheModuleByName(t *testing.T) {
r := aContainerWithGroups("${setting:media-gid}")
err := settingInto(r, nil, "n8n")
if err == nil || !strings.Contains(err.Error(), "n8n") || !strings.Contains(err.Error(), "media-gid") {
t.Fatalf("want a refusal naming n8n and media-gid, got %v", err)
}
if got, _ := r["groups"].([]any); len(got) != 1 || got[0] != "${setting:media-gid}" {
t.Fatalf("the resource was changed: %#v", r["groups"])
}
}
// 1636/AC2: a value that is not a whole number above zero cannot be a group.
func TestAGroupThatIsNotAWholeNumberAboveZeroIsRefused(t *testing.T) {
for _, bad := range []any{"media", "-1", "0", float64(0), float64(2000.5), "2000:2000", "", true} {
r := aContainerWithGroups("${setting:media-gid}")
err := settingInto(r, []Layer{{From: "ace", Values: map[string]any{"media-gid": bad}}}, "n8n")
if err == nil || !strings.Contains(err.Error(), "media-gid") {
t.Errorf("value %#v: want a refusal naming the key, got %v", bad, err)
}
}
}
// A manifest in the config form registers with the group a config value fills, and the value is placed (a container's
// groups count as a place for it); a placeholder in any other shape is refused where the manifest is read.
func TestAManifestWhoseContainerTakesItsGroupFromConfigRegisters(t *testing.T) {
m := map[string]any{
"module": "n8n", "version": "1",
"config": map[string]any{"media-gid": map[string]any{"type": "number", "minimum": 1, "required": true,
"description": "the group that owns the media library on this machine"}},
"resources": []any{aContainerWithGroups("${config:media-gid}")},
}
if _, err := parseRaw(t, m); err != nil {
t.Fatalf("refused: %v", err)
}
for _, shape := range []string{"x${config:media-gid}", "${config:media-gid}:1", "${config:other}"} {
m["resources"] = []any{aContainerWithGroups(shape)}
if _, err := parseRaw(t, m); err == nil {
t.Errorf("groups %q registered", shape)
}
}
}
// 1636/AC3 composed for a machine, the way a push writes it: n8n's group is the number the machine's config value
// says, written as a number, and the module is left out by name when the machine says none.
func TestAComposedMachineGetsTheGroupTheConfigValueNames(t *testing.T) {
manifest, err := parseRaw(t, map[string]any{
"module": "n8n", "version": "1",
"config": map[string]any{"media-gid": map[string]any{"type": "number", "minimum": 1, "required": true,
"description": "the group that owns the media library on this machine"}},
"resources": []any{aContainerWithGroups("${config:media-gid}")},
})
if err != nil {
t.Fatal(err)
}
r := anAdoptedAnchor()
r.Modules = append(r.Modules, manifest)
with := anchorRendering(false)
with.Settings["n8n"] = []Layer{{From: "anchor", Values: map[string]any{"media-gid": float64(2000)}}}
composed, err := r.Compose(with)
if err != nil {
t.Fatal(err)
}
if why, left := composed.LeftOut["n8n"]; left {
t.Fatalf("left out: %s", why)
}
if got := byID(composed.Resources)["n8n.server"]["groups"]; !reflect.DeepEqual(got, []any{2000}) {
t.Fatalf("composed groups %#v", got)
}
without := anchorRendering(false)
composed, err = r.Compose(without)
if err == nil {
why, left := composed.LeftOut["n8n"]
if !left || !strings.Contains(why, "media-gid") {
t.Fatalf("a machine that says no group kept n8n or left it out without the key: %q (%v)", why, left)
}
} else if !strings.Contains(err.Error(), "media-gid") {
t.Fatalf("refused without the key: %v", err)
}
}