Files
mesh-controller/internal/catalogue/settings_test.go
T
jschoubben 29be985c23 A served value or a contribution's may be the operator's: ${setting:…} fills there too, refused by name when unset
Issue 173's rule needs it: a mail provider serves its domain and an identity provider its issuer,
and neither is the definition's to state. Declared as ${setting:<key>}, filled from the layers after
the overrides; a key so asked for is not stray.
2026-09-30 22:34:43 +02:00

237 lines
9.2 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 TestSettingsThatReachNothingAreNamed(t *testing.T) {
// Somebody who misspells a module, or sets a key on one with nothing mergeable, has changed
// nothing — and would otherwise find out by the machine not behaving differently, which is
// the slowest way there is.
m := Manifest{Module: "thing", Resources: []map[string]any{
{"id": "conf", "type": "file", "path": "/etc/thing", "content": "plain"},
}}
unused := UnusedSettings(m, []Layer{{From: "node", Values: map[string]any{"port": 1}}})
if len(unused) != 1 || !strings.Contains(unused[0], "no file that merges it") {
t.Errorf("settings that reached nothing were not named: %v", unused)
}
}
func TestASettingLandsOnlyWhereSomethingDeclaresIt(t *testing.T) {
// novox/hq 04-ISSUES/173. A module that contributes a route and serves a provision takes a
// setting for a key either declares, and a setting for a key neither declares is stray — it
// would not reach the route or the served fact, so it must be said rather than dropped.
m := Manifest{Module: "mail",
Contributes: map[string]map[string]any{"reverse-proxy": {"host": "mail", "port": 8080}},
Serves: map[string]map[string]any{"smtp": {"host": "mail", "port": 25}},
Resources: []map[string]any{
{"id": "env", "type": "file", "path": "/etc/mail.env", "content": "SITE=${setting:sitename}\n"},
}}
layers := []Layer{{From: "the mesh", Values: map[string]any{
"host": "post", "sitename": "Mail", "website": "https://www.example.tld"}}}
unused := UnusedSettings(m, layers)
if len(unused) != 1 || !strings.Contains(unused[0], `"website"`) {
t.Errorf("only website reaches nothing; named: %v", unused)
}
}
func TestASettingOverridesAServedKeyAndAddsNone(t *testing.T) {
// What a consumer is told is the provider's contract. A setting made for one of the
// provider's files — its site name, its public address — is not part of it.
served, err := Settle(map[string]any{"host": "mail", "port": 25},
[]Layer{{From: "the mesh", Values: map[string]any{"host": "post", "sitename": "Mail"}}})
if err != nil {
t.Fatal(err)
}
if served["host"] != "post" {
t.Errorf("the setting did not override the served host: %v", served)
}
if _, leaked := served["sitename"]; leaked {
t.Errorf("a setting for a file reached the consumers: %v", served)
}
}
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")
}
}
func TestAServedValueMayBeTheOperators(t *testing.T) {
// A mail provider serves its domain and an identity provider its issuer; neither is the
// definition's to state (ADR 0155). Filled from the layers, refused by name when unset.
served, err := Settle(map[string]any{"port": 587, "domain": "${setting:domain}"},
[]Layer{{From: "the mesh", Values: map[string]any{"domain": "example.tld"}}})
if err != nil {
t.Fatal(err)
}
if served["domain"] != "example.tld" {
t.Errorf("the operator's value did not fill the served key: %v", served)
}
_, err = Settle(map[string]any{"domain": "${setting:domain}"}, nil)
if err == nil || !strings.Contains(err.Error(), `"domain"`) {
t.Errorf("a served value nothing sets must be refused by name; got %v", err)
}
m := Manifest{Module: "mail", Serves: map[string]map[string]any{"smtp": {"domain": "${setting:domain}"}}}
if unused := UnusedSettings(m, []Layer{{From: "the mesh", Values: map[string]any{"domain": "x"}}}); len(unused) != 0 {
t.Errorf("a setting a served fact asks for is not stray: %v", unused)
}
}