Files
mesh-host/internal/compose/resolution/settings_test.go
T
jschoubben d68e241724 Take the catalogue's composition rules into the node-engine
ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk
carries no composition rule. The catalogue functions only the
node-engine's composition reaches (Resolve, Resolution's Compose,
Declaration, DataItems, LeftOut, Rules and the rest, the settings,
placeholder and template refusals, the rendering helpers) are copied
with their tests into internal/compose/resolution; a method of a
mesh-sdk shape becomes a function taking it first. The composition
calls its own copies, and the captured catalogue its tests read lives
here now, shared by mesh-compose's tests.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:15:55 +01:00

107 lines
4.2 KiB
Go

package resolution
import (
"strings"
"testing"
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
)
func file(content string, protected ...string) map[string]any {
r := map[string]any{"id": "conf", "type": "file", "path": "/etc/thing.json",
"merge": mcat.MergeJSON, "content": content}
if len(protected) > 0 {
var as []any
for _, p := range protected {
as = append(as, p)
}
r["protected"] = as
}
return r
}
func TestAProtectedKeyIsRefusedNotIgnored(t *testing.T) {
// A setting quietly dropped is somebody believing they changed something. Refusing says so
// while they are looking at it.
_, err := ApplySettings(file(`{"socket":"/run/thing.sock","log":"info"}`, "socket"),
[]mcat.Layer{{From: "node", Values: map[string]any{"socket": "/tmp/mine.sock"}}})
if err == nil {
t.Fatal("a protected key was overridden")
}
if !strings.Contains(err.Error(), "socket") || !strings.Contains(err.Error(), "not settable") {
t.Errorf("the refusal does not say which key or why: %v", err)
}
}
func TestTheSameSettingsAlwaysProduceTheSameBytes(t *testing.T) {
// A file whose lines move for no reason makes every reconcile look like a change, and a
// service reflecting it would restart for ever.
//
// Note what this defends: Go's JSON encoder sorts map keys, so the stability comes from the
// standard library and this passes with the merging removed. It is worth keeping as the thing
// that would catch a move to an encoder that does not sort — but it is not evidence about the
// code below it, and it was checked.
resource := file(`{"b":2,"a":1,"c":3}`)
layer := mcat.Layer{From: "node", Values: map[string]any{"z": 26.0, "a": 100.0}}
first, err := ApplySettings(resource, []mcat.Layer{layer})
if err != nil {
t.Fatal(err)
}
for i := 0; i < 5; i++ {
again, err := ApplySettings(resource, []mcat.Layer{layer})
if err != nil {
t.Fatal(err)
}
if again["content"] != first["content"] {
t.Fatal("the same settings produced different bytes")
}
}
}
func TestAFileThatDoesNotMergeIsLeftAlone(t *testing.T) {
plain := map[string]any{"id": "conf", "type": "file", "path": "/etc/thing",
"content": "not structured at all\n"}
out, err := ApplySettings(plain, []mcat.Layer{{From: "node", Values: map[string]any{"x": 1}}})
if err != nil {
t.Fatal(err)
}
if out["content"] != "not structured at all\n" {
t.Errorf("a file with no merge rule was changed: %v", out["content"])
}
}
func TestContentThatIsNotJSONIsRefusedWhereSomebodyIsLooking(t *testing.T) {
// Rather than on the machine, at apply time, as a file the program cannot read.
_, err := ApplySettings(file(`this is not json`), nil)
if err == nil {
t.Fatal("a module claiming to merge as JSON shipped something else and was accepted")
}
}
// No layer replaces a value the vault fills, at any depth (novox/hq issue 610): searxng's server.secret_key
// sits under `server`, a key its file takes, and a layer reaching it was let through by a top-level-only check.
func TestNoLayerReplacesAVaultSecretAtAnyDepth(t *testing.T) {
file := map[string]any{"id": "settings", "type": "file", "merge": mcat.MergeJSON,
"content": `{"server": {"secret_key": "${secret:secret}", "port": 8080}, "ui": {"theme": "simple"}, "token": "${secret:token}"}`}
layer := func(values map[string]any) []mcat.Layer { return []mcat.Layer{{From: "anchor", Values: values}} }
merged, err := ApplySettings(file, layer(map[string]any{"server": map[string]any{"port": float64(9000)}, "ui": map[string]any{"theme": "dark"}}))
if err != nil {
t.Fatalf("a key beside the secret was refused: %v", err)
}
if c := merged["content"].(string); !strings.Contains(c, `"secret_key": "${secret:secret}"`) || !strings.Contains(c, `"port": 9000`) {
t.Fatalf("the secret or the setting did not land as meant:\n%s", c)
}
for name, values := range map[string]map[string]any{
"the nested secret": {"server": map[string]any{"secret_key": "chosen"}},
"the whole map holding it": {"server": "elsewhere"},
"a top-level secret": {"token": "chosen"},
"the secret, emptied": {"server": map[string]any{"secret_key": ""}},
} {
if _, err := ApplySettings(file, layer(values)); err == nil || !strings.Contains(err.Error(), "vault") {
t.Errorf("%s: %v, want a refusal naming the vault", name, err)
}
}
}