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
107 lines
4.2 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|