package catalogue import ( "strings" "testing" ) // Defends novox/hq ADR 0263: a machine's shares and the shares a machine mounts are two node seats, each // with its verbs required of every holder, each delivering nothing — `nfs-share` is provided at the mesh's // scope by the holder of node-nfs-server, and a node seat cannot answer for a provision at the mesh's. func TestTheSharesAndMountsAreNodeSeatsWithTheirVerbs(t *testing.T) { for seat, want := range map[string]string{ NFSServerSeat: "exports clients test reload adopt", MountsSeat: "list test mount unmount adopt", } { s, ok := SeatNamed(seat) if !ok { t.Fatalf("%s is not in the mesh's set", seat) } if s.Scope != ScopeNode || s.Decision != "novox/hq ADR 0263" || s.Delivers != "" || s.Replicated { t.Errorf("%s is %+v; a node seat under ADR 0263 that delivers nothing", seat, s) } var got []string for _, v := range s.Serves { got = append(got, v.Name) if v.Optional { t.Errorf("%s.%s is optional; its first holder serves it", seat, v.Name) } if v.Description == "" || v.Input["type"] != "object" || len(v.Replaces) == 0 { t.Errorf("%s.%s has no description, no object schema or says it replaces nothing", seat, v.Name) } } if strings.Join(got, " ") != want { t.Errorf("%s serves %v, not %s", seat, got, want) } } } // Both acts that take over what a person wrote by hand are dry runs unless confirmed, and name the path. func TestTheAdoptVerbsAreDryRunsUnlessConfirmed(t *testing.T) { for seat, key := range map[string]string{NFSServerSeat: "path", MountsSeat: "mountpoint"} { s, _ := SeatNamed(seat) for _, v := range s.Serves { if v.Name != "adopt" { continue } props, _ := v.Input["properties"].(map[string]any) confirm, _ := props["confirm"].(map[string]any) if confirm == nil || confirm["enum"] == nil { t.Errorf("%s.adopt has no confirm switch: %v", seat, v.Input) } if req, _ := v.Input["required"].([]string); len(req) != 1 || req[0] != key { t.Errorf("%s.adopt requires %v, not %q alone", seat, v.Input["required"], key) } if !strings.Contains(v.Description, "Without confirm, says what it would do and changes nothing") { t.Errorf("%s.adopt does not say a call without confirm changes nothing: %s", seat, v.Description) } } } } // A holder claiming the seat at a node with every verb holds it; one naming a verb the seat does not // promise, or leaving one out, is refused — as the catalogue's nfs-server and mounts modules claim them. func TestAHolderOfTheShareSeatsServesEveryVerbAndNothingElse(t *testing.T) { cases := []struct { seat, module string verbs []string }{ {NFSServerSeat, "nfs-server", []string{"exports", "clients", "test", "reload", "adopt"}}, {MountsSeat, "mounts", []string{"list", "test", "mount", "unmount", "adopt"}}, } for _, c := range cases { seat, _ := SeatNamed(c.seat) holder := Manifest{Module: c.module, Version: "1", Claims: []Claim{{Name: c.seat, Scope: ScopeNode, Serves: c.verbs}}} if c.seat == NFSServerSeat { holder.Provides = []Offer{{Name: "nfs-share", Scope: ScopeMesh}} } if err := CanHold(holder, seat); err != nil { t.Errorf("%s serving every verb is refused: %v", c.module, err) } typo := holder typo.Claims = []Claim{{Name: c.seat, Scope: ScopeNode, Serves: append(append([]string{}, c.verbs...), "export")}} if err := CanHold(typo, seat); err == nil || !strings.Contains(err.Error(), "does not promise") { t.Errorf("%s naming a verb the seat does not promise was accepted: %v", c.module, err) } short := holder short.Claims = []Claim{{Name: c.seat, Scope: ScopeNode, Serves: c.verbs[:len(c.verbs)-1]}} if err := CanHold(short, seat); err == nil || !strings.Contains(err.Error(), "adopt") { t.Errorf("%s leaving adopt out was accepted: %v", c.module, err) } mesh := holder mesh.Claims = []Claim{{Name: c.seat, Scope: ScopeMesh, Serves: c.verbs}} if err := CanHold(mesh, seat); err == nil { t.Errorf("%s claiming a node seat at the mesh's scope was accepted", c.module) } } } // What each verb replaces is what an agent would type over ssh to read or change a share by hand. func TestTheShareVerbsSayWhatTheyReplace(t *testing.T) { want := map[string]string{ NFSServerSeat + ".exports": "exportfs -v", NFSServerSeat + ".adopt": "zfs set sharenfs=off", MountsSeat + ".list": "cat /etc/fstab", MountsSeat + ".adopt": "sed -i /etc/fstab", } for _, s := range DefaultSeats() { for _, v := range s.Serves { cmd, ok := want[s.Name+"."+v.Name] if !ok { continue } delete(want, s.Name+"."+v.Name) found := false for _, r := range v.Replaces { found = found || r == cmd } if !found { t.Errorf("%s.%s does not say it replaces %q: %v", s.Name, v.Name, cmd, v.Replaces) } } } for verb := range want { t.Errorf("%s is not a verb of the compiled seats", verb) } } // The confirm switch is the seat's string "true", as every verb's switch is, and its description says so: // a holder handed the boolean reading of "true to change it" would treat the string as a dry run. func TestTheConfirmSwitchIsTheStringTrue(t *testing.T) { for _, seat := range []string{NFSServerSeat, MountsSeat} { s, _ := SeatNamed(seat) for _, v := range s.Serves { props, _ := v.Input["properties"].(map[string]any) confirm, _ := props["confirm"].(map[string]any) if confirm == nil { continue } if confirm["type"] != "string" { t.Errorf("%s.%s confirm is %v, not the string switch", seat, v.Name, confirm["type"]) } if d, _ := confirm["description"].(string); !strings.Contains(d, `"true"`) { t.Errorf("%s.%s confirm does not name the string \"true\": %q", seat, v.Name, d) } } } } // reload has the kernel read the export files; the module's process writes its file. The description // must not promise a write it does not do. func TestReloadSaysWhatItDoes(t *testing.T) { s, _ := SeatNamed(NFSServerSeat) for _, v := range s.Serves { if v.Name == "reload" && (strings.Contains(v.Description, "Write this machine's export file") || !strings.Contains(v.Description, "exportfs")) { t.Errorf("reload's description promises a write or does not name exportfs: %s", v.Description) } } } // A share is exported to each node the server grants it to and that asks for it, at that node's own // address (novox/hq ADR 0263 rule 5) — never to the private network's whole range. The verbs that // describe the export say so, and do not promise the range. func TestTheExportVerbsDescribePerNodeAddresses(t *testing.T) { s, _ := SeatNamed(NFSServerSeat) for _, v := range s.Serves { if v.Name != "exports" && v.Name != "test" { continue } if strings.Contains(v.Description, "private network's range") { t.Errorf("%s still describes the export as the private network's range: %s", v.Name, v.Description) } if !strings.Contains(v.Description, "address") { t.Errorf("%s does not say the export is to each node's address: %s", v.Name, v.Description) } } }