Files
mesh-controller/internal/catalogue/config_rename_test.go
T

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")
}