package catalogue import ( "encoding/json" "strings" "testing" ) // A preference has a default in the definition; the mesh's layer, then the node's, override it; a key // with a default is not stray; and a value that is the operator's still has none (novox/hq ADR 0262). func notifier() Manifest { return Manifest{Module: "notifier", Settings: map[string]SettingDeclaration{ "font-size": {Kind: KindPreference, Default: float64(10), Why: "readable at a scale of one"}, "width": {Kind: KindPreference, Default: float64(250), Why: "fits a title of forty characters"}, }, Resources: []map[string]any{{"id": "configuration", "type": "file", "path": "/x/notifierrc", "content": "font = Inter ${setting:font-size}\nwidth = ${setting:width}\n"}}, } } func parsed(t *testing.T, m Manifest) error { t.Helper() raw, err := json.Marshal(m) if err != nil { t.Fatal(err) } _, err = ParseManifest(raw) return err } func TestAnUnsetPreferenceTakesItsDefault(t *testing.T) { m := notifier() for _, layers := range [][]Layer{nil, {{From: MeshWideLayer, Values: map[string]any{}}}} { file := map[string]any{} for k, v := range m.Resources[0] { file[k] = v } if err := settingInto(file, WithDefaults(m, layers), m.Module); err != nil { t.Fatal(err) } if file["content"] != "font = Inter 10\nwidth = 250\n" { t.Fatalf("filled as %q", file["content"]) } } if err := JudgeSettings(m, nil, false); err != nil { t.Fatalf("a module whose every key has a default does not compose with no layer: %v", err) } } func TestTheNodeOverTheMeshOverTheDefault(t *testing.T) { m := notifier() layers := []Layer{ {From: MeshWideLayer, Values: map[string]any{"width": float64(300)}}, {From: "laptop", Values: map[string]any{"font-size": float64(13), "width": float64(340)}}, } file := map[string]any{"type": "file", "content": m.Resources[0]["content"]} if err := settingInto(file, WithDefaults(m, layers), m.Module); err != nil { t.Fatal(err) } if file["content"] != "font = Inter 13\nwidth = 340\n" { t.Fatalf("filled as %q", file["content"]) } file = map[string]any{"type": "file", "content": m.Resources[0]["content"]} if err := settingInto(file, WithDefaults(m, layers[:1]), m.Module); err != nil { t.Fatal(err) } if file["content"] != "font = Inter 10\nwidth = 300\n" { t.Fatalf("the mesh's layer over the default filled as %q", file["content"]) } } // Composed for a machine, the way a push writes it: the default reaches the file, and the node's // layer overrides it. func TestAComposedMachineGetsTheDefaultAndTheNodesValue(t *testing.T) { r := anAdoptedAnchor() r.Modules = append(r.Modules, notifier()) with := anchorRendering(false) composed, err := r.Compose(with) if err != nil { t.Fatal(err) } if why, left := composed.LeftOut["notifier"]; left { t.Fatalf("the notifier was left out: %s", why) } if c := byID(composed.Resources)["notifier.configuration"]["content"]; c != "font = Inter 10\nwidth = 250\n" { t.Fatalf("composed with no layer as %q", c) } with.Settings["notifier"] = []Layer{{From: "anchor", Values: map[string]any{"font-size": float64(13)}}} if composed, err = r.Compose(with); err != nil { t.Fatal(err) } if c := byID(composed.Resources)["notifier.configuration"]["content"]; c != "font = Inter 13\nwidth = 250\n" { t.Fatalf("composed with the node's font size as %q", c) } } // Setting a key the module gives a default is not refused as reaching nothing: it overrides the // default, which is how a running module gains a setting with no gap between. func TestAKeyWithADefaultIsNotStray(t *testing.T) { m := notifier() m.Resources[0]["content"] = "font = Inter ${setting:font-size}\nwidth = ${setting:width}\n" stray := UnusedSettings(m, []Layer{{From: "laptop", Values: map[string]any{"font-size": float64(13), "colour": "red"}}}) joined := strings.Join(stray, "; ") if strings.Contains(joined, `"font-size"`) || !strings.Contains(joined, `"colour"`) { t.Fatalf("stray: %s", joined) } } // A default fills ${setting:…} only: it never becomes a key of a mergeable file (issue 168). func TestADefaultIsNotMergedIntoAJSONFile(t *testing.T) { m := notifier() m.Resources = append(m.Resources, map[string]any{"id": "other", "type": "file", "path": "/x/other.json", "merge": MergeJSON, "content": `{"keep": 1}`}) out, err := ApplySettings(m.Resources[1], WithDefaults(m, nil)) if err != nil { t.Fatal(err) } if strings.Contains(out["content"].(string), "font-size") { t.Fatalf("a default reached a mergeable file: %s", out["content"]) } } // A value that is the operator's has no default: no layer setting it is refused by name, as before. func TestAnOperatorsValueWithoutADefaultIsStillRefused(t *testing.T) { m := notifier() m.Resources[0]["content"] = "font = Inter ${setting:font-size}\nwidth = ${setting:width}\nfrom = ${setting:domain}\n" err := JudgeSettings(m, nil, false) if err == nil || !strings.Contains(err.Error(), "${setting:domain}") { t.Fatalf("judged %v", err) } if strings.Contains(err.Error(), "font-size") { t.Fatalf("a default was named as set today: %v", err) } } func TestTheParserTakesAPreferenceAndRefusesTheRest(t *testing.T) { if err := parsed(t, notifier()); err != nil { t.Fatalf("a preference with a default was refused: %v", err) } for _, c := range []struct { name string key string d SettingDeclaration refuse string }{ {"no kind", "font-size", SettingDeclaration{Default: float64(10), Why: "x"}, `only a "preference" has a default`}, {"another kind", "font-size", SettingDeclaration{Kind: "operator", Default: float64(10), Why: "x"}, `only a "preference"`}, {"no default", "font-size", SettingDeclaration{Kind: KindPreference, Why: "x"}, "no default"}, {"an empty default", "font-size", SettingDeclaration{Kind: KindPreference, Default: " ", Why: "x"}, "an empty default"}, {"an object", "font-size", SettingDeclaration{Kind: KindPreference, Default: map[string]any{"a": 1.0}, Why: "x"}, "a string, a number or a boolean"}, {"no why", "font-size", SettingDeclaration{Kind: KindPreference, Default: float64(10)}, "no why"}, {"the operator's", "mail-domain", SettingDeclaration{Kind: KindPreference, Default: "example.tld", Why: "x"}, "is the operator's value"}, {"a secret", "api-token", SettingDeclaration{Kind: KindPreference, Default: "x", Why: "x"}, "is the operator's value"}, {"the mesh's word", PortsSetting, SettingDeclaration{Kind: KindPreference, Default: float64(1), Why: "x"}, "the mesh reads"}, {"read by nothing", "height", SettingDeclaration{Kind: KindPreference, Default: float64(300), Why: "x"}, "reaches nothing"}, } { m := notifier() m.Resources[0]["content"] = m.Resources[0]["content"].(string) + "x = ${setting:" + c.key + "}\n" if c.name == "read by nothing" { m.Resources[0]["content"] = "font = Inter ${setting:font-size}\nwidth = ${setting:width}\n" } m.Settings[c.key] = c.d err := parsed(t, m) if err == nil || !strings.Contains(err.Error(), c.refuse) { t.Errorf("%s: parsed %v, want %q", c.name, err, c.refuse) } } } func TestEveryValueSaysWhereItCameFrom(t *testing.T) { m := notifier() m.Resources[0]["content"] = m.Resources[0]["content"].(string) + "x = ${setting:position}\n" got := Effective(m, []Layer{ {From: MeshWideLayer, Values: map[string]any{"width": float64(300), "position": "top-right"}}, {From: "laptop", Values: map[string]any{"font-size": float64(13)}}, }) want := map[string]string{"font-size": "laptop", "position": MeshWideLayer, "width": MeshWideLayer} if len(got) != 3 { t.Fatalf("effective: %+v", got) } for _, s := range got { if s.From != want[s.Key] { t.Errorf("%s from %q, want %q", s.Key, s.From, want[s.Key]) } } if got[0].Key != "font-size" || got[0].Value != float64(13) || got[0].Default != float64(10) { t.Fatalf("font-size: %+v", got[0]) } only := Effective(m, nil) if only[0].From != DefaultLayer || only[0].Value != float64(10) { t.Fatalf("with no layer: %+v", only) } }