package catalogue import ( "strings" "testing" ) // The list of predefined keys is read strictly: a type and a why for each key (novox/hq issue 630). func TestThePredefinedListIsReadStrictly(t *testing.T) { list, err := ParsePredefined([]byte(`{"font-size": {"type": "number", "why": "the size text is drawn at"}, "theme": {"type": "string", "why": "light or dark"}}`)) if err != nil || len(list) != 2 || list["font-size"].Type != TypeNumber { t.Fatalf("a good list: %v %v", list, err) } for raw, want := range map[string]string{ `{"font-size": {"type": "integer", "why": "x"}}`: `the type "integer"`, `{"font-size": {"type": "number"}}`: "no why", `{"Font Size": {"type": "number", "why": "x"}}`: "not a usable key", `{"font-size": {"type": "number", "why": "x", "a": 1}}`: "unknown field", `{"ports": {"type": "number", "why": "x"}}`: "the mesh reads itself", } { if _, err := ParsePredefined([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("%s: %v, want %q", raw, err, want) } } } // A manifest declares preferences with a type, a default of that type, a why and maybe a predefined key; // a `settings` entry of kind preference is read as one, its type from its default. func TestAManifestDeclaresPreferences(t *testing.T) { file := `"resources": [{"id": "rc", "type": "file", "path": "/etc/rc", "mode": "0644", "trusted": false, "content": "size = ${setting:font-size} width = ${setting:width}"}]` m, err := ParseManifest([]byte(`{"module": "dunst", "version": "1", "preferences": {"font-size": {"type": "number", "default": 10, "predefined": "font-size", "why": "100 DPI"}}, "settings": {"width": {"kind": "preference", "default": 250, "why": "forty characters"}}, ` + file + `}`)) if err != nil { t.Fatal(err) } prefs := PreferencesOf(m) if prefs["font-size"].Predefined != "font-size" || prefs["width"].Type != TypeNumber || prefs["width"].Default != float64(250) { t.Fatalf("the preferences: %+v", prefs) } if got := PredefinedNamed(m); len(got) != 1 || got[0] != "font-size" { t.Fatalf("the predefined keys named: %v", got) } for body, want := range map[string]string{ `"preferences": {"font-size": {"type": "number", "default": "10", "why": "x"}, "width": {"type": "number", "default": 1, "why": "x"}}`: `its default "10" is a string`, `"preferences": {"font-size": {"type": "float", "default": 10, "why": "x"}, "width": {"type": "number", "default": 1, "why": "x"}}`: `the type is "float"`, `"preferences": {"font-size": {"type": "number", "why": "x"}, "width": {"type": "number", "default": 1, "why": "x"}}`: "no default", `"preferences": {"font-size": {"type": "number", "default": 10}, "width": {"type": "number", "default": 1, "why": "x"}}`: "no why", } { if _, err := ParseManifest([]byte(`{"module": "dunst", "version": "1", ` + body + `, ` + file + `}`)); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("%s: %v, want %q", body, err, want) } } } // A preference falls back only to a key on the list, with the list's type. func TestAPreferencesPredefinedKeyIsOnTheListWithItsType(t *testing.T) { m := Manifest{Module: "dunst", Preferences: map[string]Preference{ "font-size": {Type: TypeNumber, Default: float64(10), Predefined: "font-size", Why: "x"}}} if p := PredefinedProblems(m, Predefined{"font-size": {Type: TypeNumber, Why: "x"}}); len(p) != 0 { t.Fatalf("on the list: %v", p) } if p := PredefinedProblems(m, Predefined{"theme": {Type: TypeString, Why: "x"}}); len(p) != 1 || !strings.Contains(p[0], "not on the catalogue's list") || !strings.Contains(p[0], "theme") { t.Fatalf("not on the list: %v", p) } if p := PredefinedProblems(m, Predefined{"font-size": {Type: TypeString, Why: "x"}}); len(p) != 1 || !strings.Contains(p[0], "is a string") { t.Fatalf("another type: %v", p) } } // Mesh and node config fill `${setting:}` between the default and the module's own layer, and — like // the default — never reach a mergeable file. func TestConfigLayersFillAPlaceholderAndNoMergeableFile(t *testing.T) { m := Manifest{Module: "dunst", Preferences: map[string]Preference{ "font-size": {Type: TypeNumber, Default: float64(10), Predefined: "font-size", Why: "x"}, "width": {Type: TypeNumber, Default: float64(250), Why: "x"}}} fill := func(layers []Layer) string { t.Helper() r := map[string]any{"id": "rc", "type": "file", "content": "font ${setting:font-size} width ${setting:width}"} if err := settingInto(r, WithDefaults(m, layers), m.Module); err != nil { t.Fatal(err) } return r["content"].(string) } mesh := map[string]any{"font-size": float64(11), "width": float64(999)} node := map[string]any{"font-size": float64(13)} if got := fill(nil); got != "font 10 width 250" { t.Fatalf("the defaults: %s", got) } if got := fill(ConfigLayers(m, mesh, nil, "g14")); got != "font 11 width 250" { t.Fatalf("mesh config, which a preference without predefined does not read: %s", got) } both := ConfigLayers(m, mesh, node, "g14") if got := fill(both); got != "font 13 width 250" { t.Fatalf("node config over mesh config: %s", got) } own := append(both, Layer{From: "g14", Values: map[string]any{"font-size": float64(15)}}) if got := fill(own); got != "font 15 width 250" { t.Fatalf("the module's own value over both: %s", got) } sources := map[string]string{} for _, s := range Effective(m, both) { sources[s.Key] = s.From } if sources["font-size"] != NodeConfigLayer("g14") || sources["width"] != DefaultLayer { t.Fatalf("where each comes from: %v", sources) } merged, err := ApplySettings(map[string]any{"id": "conf", "type": "file", "merge": MergeJSON, "content": "{}"}, both) if err != nil || merged["content"] != "{}\n" { t.Fatalf("config reached a mergeable file: %v %v", merged["content"], err) } }