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).
186 lines
7.0 KiB
Go
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")
|
|
}
|