betterbird declares a preference and owns a mergeable file that names no keys, and the catalogue can only name them once this controller runs: the refusal made the catalogue check fail against catalogue main. It follows once mesh-catalog names the keys.
197 lines
8.0 KiB
Go
197 lines
8.0 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// twoFiles is a module that owns two configurable files, which issue 168 said no module could:
|
|
// every key of every layer reached both.
|
|
func twoFiles(app, sidecar []any) Manifest {
|
|
appFile := map[string]any{"id": "app", "type": "file", "path": "/etc/app.json", "merge": MergeJSON,
|
|
"content": `{"server":{"port":8080},"search":{"safe":1}}`}
|
|
sideFile := map[string]any{"id": "sidecar", "type": "file", "path": "/etc/sidecar.json", "merge": MergeJSON,
|
|
"content": `{"url":"http://127.0.0.1:8080"}`}
|
|
if app != nil {
|
|
appFile[TakesSettingsField] = app
|
|
}
|
|
if sidecar != nil {
|
|
sideFile[TakesSettingsField] = sidecar
|
|
}
|
|
return Manifest{Module: "search", Resources: []map[string]any{
|
|
appFile, sideFile,
|
|
{"id": "env", "type": "file", "path": "/etc/search.env", "content": "SITE=${setting:site-name}\n"},
|
|
}}
|
|
}
|
|
|
|
func settledContent(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)
|
|
}
|
|
if _, kept := out[TakesSettingsField]; kept {
|
|
t.Errorf("%v still carries %s; the host knows no such field", out["id"], TakesSettingsField)
|
|
}
|
|
var parsed map[string]any
|
|
if err := json.Unmarshal([]byte(out["content"].(string)), &parsed); err != nil {
|
|
t.Fatalf("%v is not JSON after settling: %v", out["id"], err)
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func TestEachMergeableFileTakesOnlyTheKeysItNames(t *testing.T) {
|
|
// novox/hq ADR 0326 §1, issue 168: two configurable files, each taking its own keys. A key for
|
|
// the other file, for a ${setting:…} elsewhere or for the mesh itself lands in neither.
|
|
m := twoFiles([]any{"server", "search"}, []any{"url"})
|
|
layers := []Layer{
|
|
{From: "the mesh", Values: map[string]any{
|
|
"server": map[string]any{"base_url": "https://search.example.tld/"},
|
|
"url": "http://127.0.0.1:9090", "site-name": "Search",
|
|
"endpoints": map[string]any{"web": map[string]any{"reach": "public"}}}},
|
|
{From: "node-a", Values: map[string]any{"search": map[string]any{"safe": 0.0}}},
|
|
}
|
|
|
|
app := settledContent(t, m.Resources[0], layers)
|
|
for _, stray := range []string{"url", "site-name", "endpoints"} {
|
|
if _, in := app[stray]; in {
|
|
t.Errorf("app took %q, which it does not name: %v", stray, app)
|
|
}
|
|
}
|
|
server, _ := app["server"].(map[string]any)
|
|
if server["base_url"] != "https://search.example.tld/" || server["port"] != 8080.0 {
|
|
t.Errorf("app's server block is %v; the setting should merge into the file's own", server)
|
|
}
|
|
if search, _ := app["search"].(map[string]any); search["safe"] != 0.0 {
|
|
t.Errorf("app's search block is %v; the node's layer should win", search)
|
|
}
|
|
|
|
side := settledContent(t, m.Resources[1], layers)
|
|
if side["url"] != "http://127.0.0.1:9090" {
|
|
t.Errorf("sidecar's url is %v; it names url", side["url"])
|
|
}
|
|
if len(side) != 1 {
|
|
t.Errorf("sidecar took keys it does not name: %v", side)
|
|
}
|
|
}
|
|
|
|
func TestAnEmptyListTakesNoSetting(t *testing.T) {
|
|
// ADR 0164 §12: a file that names no setting takes none.
|
|
m := twoFiles([]any{"server"}, []any{})
|
|
side := settledContent(t, m.Resources[1], []Layer{{From: "the mesh", Values: map[string]any{
|
|
"server": map[string]any{"port": 1.0}, "url": "x"}}})
|
|
if len(side) != 1 || side["url"] != "http://127.0.0.1:8080" {
|
|
t.Errorf("a file taking none was changed: %v", side)
|
|
}
|
|
}
|
|
|
|
func TestAFileThatNamesNothingKeepsTodaysForm(t *testing.T) {
|
|
// ADR 0326 §3, ADR 0164 §15: a module none of whose files names keys is left as it was.
|
|
m := twoFiles(nil, nil)
|
|
app := settledContent(t, m.Resources[0], []Layer{{From: "the mesh", Values: map[string]any{"url": "x"}}})
|
|
if app["url"] != "x" {
|
|
t.Errorf("a silent file should still take every key until its module names them: %v", app)
|
|
}
|
|
if unused := UnusedSettings(m, []Layer{{From: "the mesh", Values: map[string]any{"anything": 1}}}); len(unused) != 0 {
|
|
t.Errorf("today's form has nothing stray, and said: %v", unused)
|
|
}
|
|
}
|
|
|
|
func TestAKeyNoFileNamesIsStray(t *testing.T) {
|
|
// ADR 0326 §5: once the files name their keys, a key none of them names — and nothing else
|
|
// takes — is stray, and is said with what each file takes.
|
|
m := twoFiles([]any{"server", "search"}, []any{"url"})
|
|
unused := UnusedSettings(m, []Layer{{From: "the mesh", Values: map[string]any{
|
|
"server": 1, "url": "x", "site-name": "Search", "ui": map[string]any{"theme": "dark"}}}})
|
|
if len(unused) != 1 || !strings.Contains(unused[0], `"ui"`) {
|
|
t.Fatalf("only ui reaches nothing; named: %v", unused)
|
|
}
|
|
if !strings.Contains(unused[0], "app takes server, search") || !strings.Contains(unused[0], "sidecar takes url") {
|
|
t.Errorf("the stray key's message does not say what each file takes: %s", unused[0])
|
|
}
|
|
}
|
|
|
|
func TestOneNamingFileMakesEveryMergeableFileName(t *testing.T) {
|
|
// ADR 0326 §2: a module with one naming and one silent mergeable file is refused, naming the
|
|
// silent one — the silent one would otherwise still take every key.
|
|
m := twoFiles([]any{"server"}, nil)
|
|
problems := TakenSettingsProblems(m)
|
|
if len(problems) != 1 || !strings.Contains(problems[0], "sidecar merges and names no takes-settings") {
|
|
t.Fatalf("problems: %v", problems)
|
|
}
|
|
if err := JudgeSettings(m, nil, false); err == nil || !strings.Contains(err.Error(), "sidecar") {
|
|
t.Errorf("composing it should refuse it too, and said: %v", err)
|
|
}
|
|
raw, err := json.Marshal(map[string]any{"module": m.Module, "version": "1", "resources": m.Resources})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := ParseManifest(raw); err == nil || !strings.Contains(err.Error(), "sidecar merges and names no") {
|
|
t.Errorf("the module check does not say it: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTakesSettingsIsRefusedWhereItCannotMeanAnything(t *testing.T) {
|
|
// ADR 0326 §4.
|
|
cases := map[string]struct {
|
|
resource map[string]any
|
|
says string
|
|
}{
|
|
"on a file that does not merge": {
|
|
map[string]any{"id": "plain", "type": "file", "path": "/x", "content": "x",
|
|
TakesSettingsField: []any{"a"}},
|
|
"does not merge"},
|
|
"not a list": {
|
|
map[string]any{"id": "conf", "type": "file", "path": "/x", "merge": MergeJSON, "content": "{}",
|
|
TakesSettingsField: "server"},
|
|
"is a list of setting keys"},
|
|
"a mesh word": {
|
|
map[string]any{"id": "conf", "type": "file", "path": "/x", "merge": MergeJSON, "content": "{}",
|
|
TakesSettingsField: []any{"endpoints"}},
|
|
"the mesh reads"},
|
|
"a protected key": {
|
|
map[string]any{"id": "conf", "type": "file", "path": "/x", "merge": MergeJSON, "content": "{}",
|
|
"protected": []any{"secret"}, TakesSettingsField: []any{"secret"}},
|
|
"keeps it as protected"},
|
|
"named twice": {
|
|
map[string]any{"id": "conf", "type": "file", "path": "/x", "merge": MergeJSON, "content": "{}",
|
|
TakesSettingsField: []any{"a", "a"}},
|
|
"twice"},
|
|
"not a key": {
|
|
map[string]any{"id": "conf", "type": "file", "path": "/x", "merge": MergeJSON, "content": "{}",
|
|
TakesSettingsField: []any{"two words"}},
|
|
"not a usable setting key"},
|
|
}
|
|
for name, c := range cases {
|
|
problems := TakenSettingsProblems(Manifest{Module: "m", Resources: []map[string]any{c.resource}})
|
|
if !containsText(problems, c.says) {
|
|
t.Errorf("%s: want a problem saying %q, got %v", name, c.says, problems)
|
|
}
|
|
}
|
|
// And a file that does not merge is refused where it is settled, not handed to the host.
|
|
plain := map[string]any{"id": "plain", "type": "file", "path": "/x", "content": "x",
|
|
TakesSettingsField: []any{"a"}}
|
|
if _, err := ApplySettings(plain, nil); err == nil {
|
|
t.Error("a file that does not merge and names keys was settled")
|
|
}
|
|
}
|
|
|
|
func TestASoftwareSpelledKeyIsUsable(t *testing.T) {
|
|
// A file's keys are its software's: nodered's timeZone is not lower case and is still a key.
|
|
m := Manifest{Module: "flows", Resources: []map[string]any{{"id": "settings", "type": "file",
|
|
"path": "/x", "merge": MergeJSON, "content": "{}", TakesSettingsField: []any{"timeZone"}}}}
|
|
if problems := TakenSettingsProblems(m); len(problems) != 0 {
|
|
t.Errorf("timeZone was refused: %v", problems)
|
|
}
|
|
}
|
|
|
|
func containsText(list []string, text string) bool {
|
|
for _, s := range list {
|
|
if strings.Contains(s, text) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|