Files
mesh-sdk/go/catalogue/settings_test.go
T

195 lines
7.4 KiB
Go

package catalogue
import (
"encoding/json"
"strings"
"testing"
)
func file(content string, protected ...string) map[string]any {
r := map[string]any{"id": "conf", "type": "file", "path": "/etc/thing.json",
"merge": MergeJSON, "content": content}
if len(protected) > 0 {
var as []any
for _, p := range protected {
as = append(as, p)
}
r["protected"] = as
}
return r
}
func merged(t *testing.T, resource map[string]any, layers ...Layer) map[string]any {
t.Helper()
out, err := ApplySettings(resource, layers)
if err != nil {
t.Fatal(err)
}
var parsed map[string]any
if err := json.Unmarshal([]byte(out["content"].(string)), &parsed); err != nil {
t.Fatalf("the merged file is not JSON: %v", err)
}
return parsed
}
func TestASettingBeatsTheModulesDefault(t *testing.T) {
// The whole rule. A setting is a statement about that key made deliberately; the default was
// only ever what to do in the absence of one. So there is nothing to resolve.
got := merged(t, file(`{"port":8080,"log":"info"}`),
Layer{From: "node", Values: map[string]any{"port": 9090.0}})
if got["port"] != 9090.0 {
t.Errorf("port is %v; the setting should have won", got["port"])
}
if got["log"] != "info" {
t.Errorf("log is %v; a key nobody set should keep the module's value", got["log"])
}
}
func TestUpstreamKeepsTheKeysNobodySet(t *testing.T) {
// The point of merging rather than replacing: the module can change its half freely and only
// the keys somebody actually cares about are pinned.
got := merged(t, file(`{"port":8080,"log":"info","workers":4}`),
Layer{From: "node", Values: map[string]any{"log": "debug"}})
if got["port"] != 8080.0 || got["workers"] != 4.0 {
t.Errorf("the module's other keys did not survive: %v", got)
}
}
func TestTheNodeBeatsTheMesh(t *testing.T) {
// Two layers, in order. A node that differs is expressed by differing, rather than by
// repeating everything the rest of the mesh already says.
got := merged(t, file(`{"log":"info"}`),
Layer{From: "mesh", Values: map[string]any{"log": "warn", "workers": 8.0}},
Layer{From: "node", Values: map[string]any{"log": "debug"}})
if got["log"] != "debug" {
t.Errorf("log is %v; the node's setting should have won", got["log"])
}
if got["workers"] != 8.0 {
t.Errorf("workers is %v; a mesh-wide setting the node did not touch should stand", got["workers"])
}
}
func TestNestedBlocksMergeRatherThanReplace(t *testing.T) {
// Setting one field of a block must not delete its siblings, or every setting would have to
// restate the whole block and would then pin all of it against upstream.
got := merged(t, file(`{"http":{"gzip":"off","timeout":30,"port":80}}`),
Layer{From: "node", Values: map[string]any{
"http": map[string]any{"gzip": "on"}}})
block := got["http"].(map[string]any)
if block["gzip"] != "on" {
t.Errorf("gzip is %v", block["gzip"])
}
if block["timeout"] != 30.0 || block["port"] != 80.0 {
t.Errorf("the block's other fields were lost: %v", block)
}
}
func TestAListIsReplacedWholeNotMerged(t *testing.T) {
// A list that merged element-wise could neither be shortened nor reordered, and there is no
// correct guess about which element is "the same one".
got := merged(t, file(`{"hosts":["a","b","c"]}`),
Layer{From: "node", Values: map[string]any{"hosts": []any{"x"}}})
hosts := got["hosts"].([]any)
if len(hosts) != 1 || hosts[0] != "x" {
t.Errorf("hosts is %v; a list is replaced whole", hosts)
}
}
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"),
[]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 TestSettingsAroundAProtectedKeyStillApply(t *testing.T) {
got := merged(t, file(`{"socket":"/run/thing.sock","log":"info"}`, "socket"),
Layer{From: "node", Values: map[string]any{"log": "debug"}})
if got["log"] != "debug" {
t.Errorf("log is %v", got["log"])
}
}
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 := Layer{From: "node", Values: map[string]any{"z": 26.0, "a": 100.0}}
first, err := ApplySettings(resource, []Layer{layer})
if err != nil {
t.Fatal(err)
}
for i := 0; i < 5; i++ {
again, err := ApplySettings(resource, []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, []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": MergeJSON,
"content": `{"server": {"secret_key": "${secret:secret}", "port": 8080}, "ui": {"theme": "simple"}, "token": "${secret:token}"}`}
layer := func(values map[string]any) []Layer { return []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)
}
}
}