package catalogue import ( "encoding/json" "sort" "strings" "testing" ) // issue 900, novox/hq ADR 0394 §1: node-files is a node seat whose verbs say the commands they replace. func TestNodeFilesIsANodeSeatWithEveryVerbOfADR0394(t *testing.T) { s, ok := SeatNamed(FilesSeat) if !ok { t.Fatalf("the mesh defines no seat %s", FilesSeat) } if s.Scope != ScopeNode || s.Delivers != "" || !strings.Contains(s.Decision, "ADR 0394") { t.Errorf("%s: scope %q, delivers %q, decision %q", FilesSeat, s.Scope, s.Delivers, s.Decision) } have := map[string]bool{} replaced := map[string]string{} for _, v := range s.Serves { have[v.Name] = true if v.Optional { t.Errorf("%s.%s is optional", FilesSeat, v.Name) } // upload and download (ADR 0468 §9, issue 1718) move bytes through the object store: no command is theirs. if len(v.Replaces) == 0 && v.Name != "upload" && v.Name != "download" { t.Errorf("%s.%s says nothing it replaces", FilesSeat, v.Name) } for _, c := range v.Replaces { replaced[c] = v.Name } } for _, name := range []string{"stat", "readlink", "list", "read", "search", "account", "write", "copy", "move", "delete", "mkdir", "chmod", "chown", "upload", "download", "trash", "restore"} { if !have[name] { t.Errorf("%s serves no verb %s", FilesSeat, name) } } for command, verb := range map[string]string{"cat": "read", "tee": "write", "cp": "copy", "mv": "move", "rm": "delete", "mkdir -p": "mkdir", "chmod": "chmod", "chgrp": "chown"} { if replaced[command] != verb { t.Errorf("%q is replaced by %q, want %s.%s", command, replaced[command], FilesSeat, verb) } } } // issue 900, ADR 0394 §2: a holder of node-files runs as the tool runner's account, so it declares no runs-as. func TestAHolderOfNodeFilesDeclaringRunsAsIsRefused(t *testing.T) { p := FilesHolderProblems(Manifest{Module: "files", RunsAs: "mesh-files", Claims: []Claim{{Name: FilesSeat}}}) if len(p) != 1 || !strings.Contains(p[0], "0394") || !strings.Contains(p[0], "runs-as") { t.Errorf("a holder declaring runs-as was not refused: %v", p) } if p := FilesHolderProblems(Manifest{Module: "files", Claims: []Claim{{Name: FilesSeat}}}); len(p) != 0 { t.Errorf("a holder declaring none was refused: %v", p) } if p := FilesHolderProblems(Manifest{Module: "other", RunsAs: "x"}); len(p) != 0 { t.Errorf("a non-holder was judged: %v", p) } } // issue 900, ADR 0394 §4: the holder is told the secret places and the paths modules own on its node. func TestTheHolderOfNodeFilesIsToldSecretPlacesAndOwnedPaths(t *testing.T) { files := Manifest{Module: "files", Version: "1", Claims: []Claim{{Name: FilesSeat}}, OwnSecrets: OwnSecrets{"broker": {Path: "/var/lib/files/broker"}}} host := Manifest{Module: "hostnames", Version: "1", Claims: []Claim{{Name: "node-hostname"}}, Resources: []map[string]any{{"id": "hosts", "type": "file", "path": "/etc/hosts"}}} app := Manifest{Module: "app", Version: "1", Resources: []map[string]any{{"id": "conf", "type": "file", "path": "/etc/app/app.conf"}}} r := Resolution{Node: "anchor", Modules: []Manifest{files, host, app}} words, err := r.filesWords(files, nil, Rendering{}) if err != nil { t.Fatal(err) } var places []string if err := json.Unmarshal([]byte(words[ReadsSecretPlaces]), &places); err != nil || len(places) == 0 { t.Errorf("no secret places: %v %v", err, words) } var owned map[string]string if err := json.Unmarshal([]byte(words[FilesOwnedPaths]), &owned); err != nil { t.Fatalf("no owned paths: %v (%v)", err, words) } if !strings.HasPrefix(owned["/etc/hosts"], "hostnames") { t.Errorf("/etc/hosts is owned by %q, want hostnames", owned["/etc/hosts"]) } if !strings.HasPrefix(owned["/etc/app/app.conf"], "app") { t.Errorf("a placed file is owned by %q, want app", owned["/etc/app/app.conf"]) } if w, _ := r.filesWords(app, nil, Rendering{}); w[FilesOwnedPaths] != "" { t.Error("a module not holding node-files is told the owned paths") } } // issue 900: restore, like every write verb, takes a reason and an override. func TestNodeFilesRestoreTakesIdAndReason(t *testing.T) { s, _ := SeatNamed(FilesSeat) for _, v := range s.Serves { if v.Name != "restore" { continue } props, _ := v.Input["properties"].(map[string]any) for _, p := range []string{"id", "reason", "override"} { if _, has := props[p]; !has { t.Errorf("restore has no input %s", p) } } if req, _ := v.Input["required"].([]string); strings.Join(req, ",") != "id,reason" { t.Errorf("restore requires %v, want id and reason", req) } return } t.Error("no restore verb") } // 1718/AC1: node-files names upload and download, in mesh-files' order, before trash. func TestNodeFilesNamesUploadAndDownloadBeforeTrash(t *testing.T) { s, _ := SeatNamed(FilesSeat) var names []string for _, v := range s.Serves { names = append(names, v.Name) } joined := strings.Join(names, ",") if !strings.Contains(joined, "chown,upload,download,trash") { t.Errorf("1718/AC1: node-files' verbs are %s, want upload and download between chown and trash", joined) } } // 1718/AC2: a holder serving every verb including upload and download is accepted; one naming a verb outside the // contract is still refused, and one leaving upload out is refused. func TestAHolderOfNodeFilesServesUploadAndDownloadAndNothingElse(t *testing.T) { s, _ := SeatNamed(FilesSeat) var verbs []string for _, v := range s.Serves { verbs = append(verbs, v.Name) } hold := func(serves []string) error { return CanHold(Manifest{Module: "mesh-files", Version: "1", Claims: []Claim{{Name: FilesSeat, Scope: ScopeNode, Serves: serves}}}, s) } if err := hold(verbs); err != nil { t.Errorf("1718/AC2: a holder serving every verb was refused: %v", err) } if err := hold(append(append([]string{}, verbs...), "sync")); err == nil || !strings.Contains(err.Error(), "does not promise") { t.Errorf("1718/AC2: a verb outside the contract was accepted: %v", err) } var without []string for _, v := range verbs { if v != "upload" { without = append(without, v) } } if err := hold(without); err == nil || !strings.Contains(err.Error(), "upload") { t.Errorf("1718/AC2: a holder leaving upload out was accepted: %v", err) } } // 1718/AC3: upload requires path and bucket; download requires bucket, key, path and reason; sha256, key (upload) // and override are plain strings, not true/false switches. func TestNodeFilesUploadAndDownloadInputs(t *testing.T) { s, _ := SeatNamed(FilesSeat) want := map[string]struct { required string strs []string }{ "upload": {"path,bucket", []string{"path", "bucket", "key"}}, "download": {"bucket,key,path,reason", []string{"bucket", "key", "path", "sha256", "reason", "override"}}, } seen := 0 for _, v := range s.Serves { w, ok := want[v.Name] if !ok { continue } seen++ req, _ := v.Input["required"].([]string) got := append([]string{}, req...) sortStrings(got) exp := strings.Split(w.required, ",") sortStrings(exp) if strings.Join(got, ",") != strings.Join(exp, ",") { t.Errorf("1718/AC3: %s requires %v, want %s", v.Name, req, w.required) } props, _ := v.Input["properties"].(map[string]any) if len(props) != len(w.strs) { t.Errorf("1718/AC3: %s has %d inputs, want %v", v.Name, len(props), w.strs) } for _, name := range w.strs { p, has := props[name].(map[string]any) if !has { t.Errorf("1718/AC3: %s has no input %s", v.Name, name) continue } if _, enum := p["enum"]; enum || p["type"] != "string" { t.Errorf("1718/AC3: %s input %s is %v, want a plain string", v.Name, name, p) } } } if seen != 2 { t.Errorf("1718/AC3: found %d of upload and download", seen) } } func sortStrings(s []string) { sort.Strings(s) }