Files
mesh-controller/internal/catalogue/config_rename_test.go
T
mesh-tools 97a2d841c7 Land feature 57's controller work on its release branch
The controller plans config, templates and moved providers' provisions,
hands bus accounts to the vault and judges the new manifest words, on the
feature's SDK and node-engine validator (feature 57, config and provisioning).
2026-10-15 21:27:33 +00:00

186 lines
7.0 KiB
Go

package catalogue
import (
"encoding/json"
"strings"
"testing"
)
// A takes-config entry may lay a value under the name the file's software reads (issue 751): the key stays a
// config key, lower-case, and the file names its own variable, such as ATLASSIAN_URL.
// renaming is inConfigForm with its rules file taking `url` as NOTIFIER_URL, a value the code reading the file
// falls back on when the file lacks it.
func renaming() map[string]any {
m := inConfigForm()
m["config"].(map[string]any)["url"] = map[string]any{"type": "string",
"description": "the address the notifier posts to, a URL; unset, the notifier's own environment says it",
"default": ""}
resourceOf(m, "rules")["takes-config"] = []any{"position", "theme",
map[string]any{"key": "url", "as": "NOTIFIER_URL"}}
return m
}
// rulesComposed is the rules file's content as the module's defaults and these layers make it.
func rulesComposed(t *testing.T, m Manifest, layers ...Layer) string {
t.Helper()
all := layers
if d, ok := Defaults(m); ok {
all = append([]Layer{d}, layers...)
}
for _, r := range m.Resources {
if r["id"] != "rules" {
continue
}
out, err := ApplySettings(r, all)
if err != nil {
t.Fatal(err)
}
if _, left := out[TakesSettingsField]; left {
t.Fatalf("the composed file still says %s: %v", TakesSettingsField, out)
}
return out["content"].(string)
}
t.Fatal("no rules file")
return ""
}
func TestATakenValueIsLaidUnderTheFilesOwnName(t *testing.T) {
m, err := parseRaw(t, renaming())
if err != nil {
t.Fatalf("a renamed takes-config entry was refused: %v", err)
}
got := rulesComposed(t, m, Layer{From: "ace", Values: map[string]any{"url": "https://hooks.example.test/n"}})
var doc map[string]any
if err := json.Unmarshal([]byte(got), &doc); err != nil {
t.Fatal(err)
}
if doc["NOTIFIER_URL"] != "https://hooks.example.test/n" {
t.Fatalf("url was not laid as NOTIFIER_URL: %s", got)
}
if _, said := doc["url"]; said {
t.Fatalf("url was laid under the config key too: %s", got)
}
}
func TestAnUnsetRenamedValueLeavesTheFileAsItWas(t *testing.T) {
plain := inConfigForm()
base, err := parseRaw(t, plain)
if err != nil {
t.Fatal(err)
}
m, err := parseRaw(t, renaming())
if err != nil {
t.Fatal(err)
}
want := rulesComposed(t, base)
if got := rulesComposed(t, m); got != want {
t.Fatalf("with no layer the file is\n%s\nwanted, as without the entry,\n%s", got, want)
}
other := Layer{From: "ace", Values: map[string]any{"theme": "light"}}
if got, want := rulesComposed(t, m, other), rulesComposed(t, base, other); got != want {
t.Fatalf("with url unset the file is\n%s\nwanted\n%s", got, want)
}
}
// A layer said before its module moved names the file's own spelling, such as nodered's timeZone on one machine.
// Until K5 moves the layer and renames the key, that spelling still reaches the file, so the move changes no
// machine's file; the config key wins where one layer says both.
func TestALayerStillSayingTheFilesNameReachesItUntilItsMove(t *testing.T) {
m, err := parseRaw(t, renaming())
if err != nil {
t.Fatal(err)
}
for _, said := range []string{"url", "NOTIFIER_URL"} {
if stray := UnusedSettings(m, []Layer{{From: "ace", Values: map[string]any{said: "https://x.test"}}}); len(stray) > 0 {
t.Fatalf("%s, which the rules file takes, is said stray: %v", said, stray)
}
got := rulesComposed(t, m, Layer{From: "ace", Values: map[string]any{said: "https://x.test"}})
var doc map[string]any
if err := json.Unmarshal([]byte(got), &doc); err != nil {
t.Fatal(err)
}
if doc["NOTIFIER_URL"] != "https://x.test" || doc["url"] != nil {
t.Fatalf("a layer saying %s made the file %s", said, got)
}
}
both := rulesComposed(t, m, Layer{From: "ace", Values: map[string]any{"url": "https://key.test", "NOTIFIER_URL": "https://old.test"}})
if !strings.Contains(both, "https://key.test") || strings.Contains(both, "old.test") {
t.Fatalf("where one layer says both, the config key did not win: %s", both)
}
later := rulesComposed(t, m, Layer{From: "mesh", Values: map[string]any{"url": "https://mesh.test"}},
Layer{From: "ace", Values: map[string]any{"NOTIFIER_URL": "https://ace.test"}})
if !strings.Contains(later, "https://ace.test") {
t.Fatalf("a later layer's old spelling did not beat an earlier layer's key: %s", later)
}
// Who may set it is the same under either spelling: the rules file is trusted, so both are the terminal's.
terminal := TerminalKeys(m)
for _, k := range []string{"url", "NOTIFIER_URL"} {
if !listHolds(terminal, k) {
t.Fatalf("%s is not the terminal's in a trusted file: %v", k, terminal)
}
}
}
func listHolds(list []string, s string) bool {
for _, x := range list {
if x == s {
return true
}
}
return false
}
func TestARenamedEntryIsWrittenBackAsItWasSaid(t *testing.T) {
m, err := parseRaw(t, renaming())
if err != nil {
t.Fatal(err)
}
raw, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(raw), `"takes-config":["position","theme",{"as":"NOTIFIER_URL","key":"url"}]`) {
t.Fatalf("written back as %s", raw)
}
if _, err := ParseManifest(raw); err != nil {
t.Fatalf("read again: %v", err)
}
}
func TestARenamedEntryIsRefusedWhereItCouldNotHold(t *testing.T) {
entry := func(m map[string]any, e any) map[string]any {
resourceOf(m, "rules")["takes-config"] = []any{"position", "theme", e}
return m
}
refused(t, entry(renaming(), map[string]any{"key": "url"}), "notifier's rules", `"key" and "as"`)
refused(t, entry(renaming(), map[string]any{"key": "url", "as": "NOTIFIER_URL", "when": "set"}), `"key" and "as"`)
refused(t, entry(renaming(), map[string]any{"key": "URL", "as": "NOTIFIER_URL"}), "not a usable key")
refused(t, entry(renaming(), map[string]any{"key": "url", "as": "NOTIFIER URL"}), "not a usable name")
refused(t, entry(renaming(), map[string]any{"key": "url", "as": "endpoints"}), `"endpoints"`, "the mesh reads")
refused(t, entry(renaming(), map[string]any{"key": "url", "as": "theme"}), `"theme"`, "twice")
refused(t, entry(renaming(), map[string]any{"key": "theme", "as": "THEME"}), `"theme"`, "twice")
refused(t, entry(renaming(), map[string]any{"key": "nothing", "as": "NOTHING"}), `"nothing"`, "declares no such value")
kept := entry(renaming(), map[string]any{"key": "url", "as": "NOTIFIER_URL"})
resourceOf(kept, "rules")["protected"] = []any{"NOTIFIER_URL"}
refused(t, kept, `"NOTIFIER_URL"`, "protected")
// Laid only when set, so the code's own fallback holds: never required.
required := renaming()
declaration(required, "url")["required"] = true
delete(declaration(required, "url"), "default")
refused(t, required, `"url"`, "only when it is set", "never required")
}
func TestARenamedEntryIsTheConfigFormsOnly(t *testing.T) {
m := map[string]any{
"module": "notifier", "version": "1",
"resources": []any{
map[string]any{"id": "rules", "type": "file", "path": "/x/rules.json", "merge": "json", "content": "{}",
"takes-settings": []any{map[string]any{"key": "url", "as": "NOTIFIER_URL"}}},
},
}
refused(t, m, "notifier's rules", "config form")
}