Files
mesh-sdk/go/catalogue/config_rename_test.go
T
jschoubben 68aaf235d4 Take the judge out of mesh-sdk: it is mesh-delivery's now (ADR 0578 §4, §7)
Issue 1585, S19: the strict parser, every manifest problem function and the judge's tables (192 declarations,
about 5,200 lines) and their 244 tests leave; composition keeps its own refusals and the helpers it uses. The
catalogue's tests decode the manifests they build. ParseManifest, ParseManifestWith, ParseManifestForInputs,
CatalogueProblems and the rest are gone; mesh-delivery-indexer/judge holds them.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:36:24 +01:00

150 lines
5.1 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 := decodeForTest(raw); err != nil {
t.Fatalf("read again: %v", err)
}
}