package catalogue import ( "encoding/json" "os" "path/filepath" "strings" "testing" ) // The list of predefined keys (novox/hq ADR 0348 §7, design 57 package K2, issue 661): read strictly, and every // manifest naming a key judged against the list of the tree it is read in. // theFirstKeys is the catalogue's first list: font-size and agent-managed-settings. const theFirstKeys = `{ "font-size": {"type": "number", "description": "the size text is shown in, in points", "minimum": 6, "maximum": 72}, "agent-managed-settings": {"type": "object", "description": "the managed settings of every agent session, a JSON object", "trusted": true} }` func TestTheFirstListIsRead(t *testing.T) { list, err := ParsePredefined([]byte(theFirstKeys)) if err != nil { t.Fatal(err) } if got := strings.Join(list.Keys(), ","); got != "agent-managed-settings,font-size" { t.Fatalf("keys %s", got) } if k := list["font-size"]; k.Type != ConfigNumber || *k.Minimum != 6 || *k.Maximum != 72 || k.Trusted { t.Fatalf("font-size read as %+v", k) } if k := list["agent-managed-settings"]; k.Type != ConfigObject || !k.Trusted { t.Fatalf("agent-managed-settings read as %+v", k) } // One list is one text, however it was written. a, _ := list.Canonical() again, _ := ParsePredefined(a) b, _ := again.Canonical() if string(a) != string(b) { t.Fatalf("canonical text moved:\n%s\n%s", a, b) } } func TestAListIsRefusedWholeForAnyWrongKey(t *testing.T) { for _, c := range []struct{ file, says string }{ {`[]`, "not a JSON object of keys"}, {`null`, "not a JSON object of keys"}, {`{"font-size": {"type": "number", "description": "points", "default": 10}}`, `unknown field "default"`}, {`{"font-size": {"description": "points"}}`, "no type"}, {`{"font-size": {"type": "decimal", "description": "points"}}`, `type "decimal"`}, {`{"font-size": {"type": "number"}}`, "no description"}, {`{"accent": {"type": "object", "description": "colours"}}`, "not trusted"}, {`{"font-size": {"type": "number", "description": "points", "minimum": 9, "maximum": 6}}`, "above its maximum"}, {`{"accent": {"type": "string", "description": "a name", "pattern": "("}}`, "not a regular expression"}, {`{"accent": {"type": "boolean", "description": "on or off", "minimum": 1}}`, "minimum or maximum is for a number"}, {`{"Font Size": {"type": "number", "description": "points"}}`, "not a usable key"}, {`{"places": {"type": "string", "description": "where"}}`, "placement"}, {`{"puid": {"type": "number", "description": "an id"}}`, "placement"}, } { if _, err := ParsePredefined([]byte(c.file)); err == nil || !strings.Contains(err.Error(), c.says) { t.Errorf("%s: wanted a refusal saying %q, got %v", c.file, c.says, err) } } } // fallingBackTo is inConfigForm with its font size falling back to a predefined key. func fallingBackTo(t *testing.T, key string) []byte { t.Helper() m := inConfigForm() declaration(m, "font-size")["predefined"] = key raw, err := json.Marshal(m) if err != nil { t.Fatal(err) } return raw } // A manifest naming font-size registers against the list; one naming a key not on it, or of another type, is // refused — judged against the list given, whatever the mesh published. func TestAManifestIsJudgedAgainstTheListOfItsTree(t *testing.T) { withPredefined(t, nil) list, err := ParsePredefined([]byte(theFirstKeys)) if err != nil { t.Fatal(err) } if _, err := ParseManifestWith(fallingBackTo(t, "font-size"), list); err != nil { t.Fatalf("a value falling back to font-size is refused: %v", err) } if _, err := ParseManifestWith(fallingBackTo(t, "font-scale"), list); err == nil || !strings.Contains(err.Error(), `"font-scale", which is not on the list`) { t.Fatalf("a key not on the list: %v", err) } m := inConfigForm() declaration(m, "theme")["predefined"] = "font-size" raw, _ := json.Marshal(m) if _, err := ParseManifestWith(raw, list); err == nil || !strings.Contains(err.Error(), "which is a number") { t.Fatalf("a key of another type: %v", err) } // Where no tree is read, the published list judges: none published refuses. if _, err := ParseManifest(fallingBackTo(t, "font-size")); err == nil || !strings.Contains(err.Error(), "is not present") { t.Fatalf("judged against no list: %v", err) } withPredefined(t, list) if _, err := ParseManifest(fallingBackTo(t, "font-size")); err != nil { t.Fatalf("judged against the published list: %v", err) } } // A template's text filled from the tree keeps the list the manifest was judged against: read again, it is not // judged against the published one. func TestTemplatesFilledKeepTheListTheManifestWasJudgedAgainst(t *testing.T) { withPredefined(t, nil) list, _ := ParsePredefined([]byte(theFirstKeys)) dir := t.TempDir() if err := os.MkdirAll(filepath.Join(dir, TemplatesDir), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, TemplatesDir, "rc"), []byte("font = ${config:font-size}\n"), 0o644); err != nil { t.Fatal(err) } m := inConfigForm() declaration(m, "font-size")["predefined"] = "font-size" r := resourceOf(m, "configuration") delete(r, "content") r["template"] = TemplatesDir + "/rc" m["build"] = map[string]any{"artifacts": []any{map[string]any{"name": "files", "kind": "archive", "from": "files"}}} declaration(m, "width")["runtime"] = true declaration(m, "sender")["runtime"] = true raw, _ := json.Marshal(m) parsed, err := ParseManifestWith(raw, list) if err != nil { t.Fatal(err) } if _, err := WithTemplatesFrom(parsed, dir); err != nil { t.Fatalf("filled from the tree, it was judged against another list: %v", err) } } // module check reads the list at the root of the repository above the manifest; no repository, no list. func TestTheListIsFoundAtTheRepositoryRootAboveAManifest(t *testing.T) { root := t.TempDir() module := filepath.Join(root, "modules", "dunst") if err := os.MkdirAll(module, 0o755); err != nil { t.Fatal(err) } if list, err := PredefinedAbove(module); err != nil || list != nil { t.Fatalf("outside a repository: %v %v", list, err) } if err := os.Mkdir(filepath.Join(root, ".git"), 0o755); err != nil { t.Fatal(err) } if list, err := PredefinedAbove(module); err != nil || list != nil { t.Fatalf("a repository without the file: %v %v", list, err) } if err := os.WriteFile(filepath.Join(root, PredefinedFile), []byte(theFirstKeys), 0o644); err != nil { t.Fatal(err) } list, err := PredefinedAbove(module) if err != nil || len(list) != 2 { t.Fatalf("the list above: %v %v", list, err) } if err := os.WriteFile(filepath.Join(root, PredefinedFile), []byte(`{"font-size": {}}`), 0o644); err != nil { t.Fatal(err) } if _, err := PredefinedAbove(module); err == nil { t.Fatal("a wrong list above was read") } if err := os.Remove(filepath.Join(root, PredefinedFile)); err != nil { t.Fatal(err) } if err := os.Symlink("/etc/hostname", filepath.Join(root, PredefinedFile)); err == nil { if _, err := PredefinedAbove(module); err == nil || !strings.Contains(err.Error(), "not a plain file") { t.Fatalf("a link read as the list: %v", err) } } }