268 lines
11 KiB
Go
268 lines
11 KiB
Go
package seatlist
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
|
)
|
|
|
|
// fakeOwner is the seat mesh-catalog: its verb answers from its table and the events are delivered by the test.
|
|
type fakeOwner struct {
|
|
table map[string]string // seat -> definition JSON, trunk
|
|
down bool
|
|
gate chan struct{} // when set, an ask waits for it to be closed
|
|
asked []string // "key seat"
|
|
handlers map[string]func(string, json.RawMessage) error
|
|
}
|
|
|
|
func (f *fakeOwner) Ask(key string, body any) (json.RawMessage, error) {
|
|
if f.gate != nil {
|
|
<-f.gate
|
|
}
|
|
args, _ := body.(map[string]any)
|
|
seat, _ := args["seat"].(string)
|
|
f.asked = append(f.asked, key+" "+seat)
|
|
if f.down {
|
|
return nil, errors.New("no answer")
|
|
}
|
|
if seat != "" {
|
|
raw, ok := f.table[seat]
|
|
if !ok {
|
|
return json.Marshal(Answer{Seat: seat})
|
|
}
|
|
return json.Marshal(Answer{Seat: seat, Found: true, Entry: &Entry{Seat: seat, Source: SourceTrunk, Digest: digest(raw), Definition: json.RawMessage(raw)}})
|
|
}
|
|
l := Listing{Revision: 7}
|
|
for seat, raw := range f.table {
|
|
l.Seats = append(l.Seats, Entry{Seat: seat, Source: SourceTrunk, Digest: digest(raw)})
|
|
}
|
|
return json.Marshal(l)
|
|
}
|
|
|
|
func digest(raw string) string { return fmt.Sprintf("%d", len(raw)) }
|
|
|
|
func (f *fakeOwner) Subscribe(pattern string, h func(string, json.RawMessage) error) error {
|
|
if f.handlers == nil {
|
|
f.handlers = map[string]func(string, json.RawMessage) error{}
|
|
}
|
|
f.handlers[pattern] = h
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeOwner) say(event, seat, source string) error {
|
|
b, _ := json.Marshal(Changed{Seat: seat, Source: source, Digest: "d"})
|
|
return f.handlers[Pattern(event)](Pattern(event), b)
|
|
}
|
|
|
|
func def(seat, scope, verb string) string {
|
|
return `{"format":1,"seat":"` + seat + `","scope":"` + scope + `","grain":"seat-list","domain": "Core", "decision": "ADR 1",` +
|
|
`"verbs":[{"name":"` + verb + `","input":{"type":"object"}}]}`
|
|
}
|
|
|
|
func names(r *Reader) string {
|
|
var out []string
|
|
for _, s := range r.Seats() {
|
|
out = append(out, s.Name)
|
|
}
|
|
return strings.Join(out, ",")
|
|
}
|
|
|
|
// ADR 0636 §1, §3, §4: the seat list is read through the seat's verb, starting from the bootstrap, and follows its two events.
|
|
func TestTheSeatListIsReadFromTheSeatAndFollowsItsEvents(t *testing.T) {
|
|
boot, err := seatdef.Parse([]byte(def("boot-seat", "mesh", "go")))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
owner := &fakeOwner{down: true, table: map[string]string{
|
|
"node-b": def("node-b", "node", "put"), "mesh-a": def("mesh-a", "mesh", "get"), "boot-seat": def("boot-seat", "mesh", "other")}}
|
|
var told int
|
|
r, err := Start(Config{Bus: owner, Bootstrap: []seatdef.Definition{boot}, OnChange: func([]catalogue.Seat) { told++ }})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
<-r.Ready()
|
|
if owner.handlers[Pattern(ChangedEvent)] == nil || owner.handlers[Pattern(RemovedEvent)] == nil || Pattern(ChangedEvent) != "mesh-catalog.seat-definition-changed" {
|
|
t.Fatalf("subscribed to %v", owner.handlers)
|
|
}
|
|
if owner.asked[0] != "seat:mesh-catalog.seat-definitions " {
|
|
t.Fatalf("first asked %q: the seat's verb, never a module", owner.asked[0])
|
|
}
|
|
steps := []struct {
|
|
name string
|
|
act func() error
|
|
want string
|
|
err string
|
|
asks int // asks the step made
|
|
}{
|
|
{"the seat has not answered: the bootstrap stands", func() error { return nil }, "boot-seat", "", 0},
|
|
{"an event while the seat is down is offered again", func() error { return owner.say(ChangedEvent, "mesh-a", SourceTrunk) }, "boot-seat", "no answer", 1},
|
|
{"the seat answers: its seats in reading order, its definition replacing the bootstrap's, each fetched once",
|
|
func() error { owner.down = false; return r.Reload() }, "boot-seat,mesh-a,node-b", "", 4},
|
|
{"a read again fetches nothing whose digest it holds", func() error { return r.Reload() }, "boot-seat,mesh-a,node-b", "", 1},
|
|
{"a moved definition is asked again", func() error {
|
|
owner.table["mesh-a"] = def("mesh-a", "mesh", "renamed")
|
|
return owner.say(ChangedEvent, "mesh-a", SourceTrunk)
|
|
}, "boot-seat,mesh-a,node-b", "", 1},
|
|
{"a definition that does not parse leaves the one that stands", func() error {
|
|
owner.table["mesh-a"] = `{"format":1,"seat":"mesh-a","verbs":"no"}`
|
|
return owner.say(ChangedEvent, "mesh-a", SourceTrunk)
|
|
}, "boot-seat,mesh-a,node-b", "", 1},
|
|
{"a seat the seat removes goes", func() error { delete(owner.table, "node-b"); return owner.say(RemovedEvent, "node-b", SourceTrunk) }, "boot-seat,mesh-a", "", 0},
|
|
{"a pull request's definition is not held and not asked", func() error { return owner.say(ChangedEvent, "mesh-z", "pull") }, "boot-seat,mesh-a", "", 0},
|
|
{"a listing that drops a trunk seat drops it", func() error { delete(owner.table, "mesh-a"); return r.Reload() }, "boot-seat", "", 1},
|
|
}
|
|
for _, s := range steps {
|
|
before := len(owner.asked)
|
|
err := s.act()
|
|
if (err == nil) != (s.err == "") || (err != nil && !strings.Contains(err.Error(), s.err)) {
|
|
t.Fatalf("%s: error %v, want %q", s.name, err, s.err)
|
|
}
|
|
if got := names(r); got != s.want {
|
|
t.Fatalf("%s: seats %q, want %q", s.name, got, s.want)
|
|
}
|
|
if got := len(owner.asked) - before; got != s.asks {
|
|
t.Fatalf("%s: asked %d times (%v), want %d", s.name, got, owner.asked[before:], s.asks)
|
|
}
|
|
}
|
|
if st := r.Status(); len(st.Refused) != 0 || !r.Has("boot-seat") || r.Has("mesh-a") {
|
|
t.Fatalf("status %+v, has mesh-a %v", st, r.Has("mesh-a"))
|
|
}
|
|
if told < 5 {
|
|
t.Fatalf("OnChange was told %d times", told)
|
|
}
|
|
}
|
|
|
|
func TestStartRefusesWhatCannotRead(t *testing.T) {
|
|
if _, err := Start(Config{}); err == nil {
|
|
t.Fatal("started without a bus")
|
|
}
|
|
}
|
|
|
|
// ADR 0636 §1, §2: before the seat is heard the reader holds the bootstrapped definitions, says it waits and lets a host act
|
|
// on the modules those definitions name only.
|
|
func TestTheBootstrapIsHeldAndNothingNewIsActedOnUntilTheSeatAnswers(t *testing.T) {
|
|
boot := Bootstrapped()
|
|
held := map[string]bool{}
|
|
for _, d := range boot {
|
|
held[d.Seat] = true
|
|
}
|
|
for _, seat := range []string{"mesh-catalog", "mesh-conditions", "operator-channel", "node-engine", "mesh-broker", "mesh-config"} {
|
|
if !held[seat] {
|
|
t.Fatalf("the bootstrap lacks %s (ADR 0636 §2)", seat)
|
|
}
|
|
}
|
|
owner := &fakeOwner{down: true}
|
|
r, err := Start(Config{Bus: owner, Bootstrap: boot})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
<-r.Ready()
|
|
if got := len(r.Definitions()); got != len(boot) {
|
|
t.Fatalf("the reader holds %d definitions, the bootstrap has %d", got, len(boot))
|
|
}
|
|
st := r.Status()
|
|
if st.Heard || len(st.Waiting) != len(boot) || st.LastError == "" || st.Health() == "" {
|
|
t.Fatalf("status %+v: want the seat not heard, every bootstrapped seat waiting, the error and the health said", st)
|
|
}
|
|
acts := []struct {
|
|
seats []string
|
|
want bool
|
|
}{
|
|
{[]string{"node-engine"}, true},
|
|
{[]string{"mesh-catalog", "mesh-config"}, true},
|
|
{[]string{"node-engine", "node-files"}, false},
|
|
{[]string{"node-files"}, false},
|
|
{nil, false},
|
|
}
|
|
for _, a := range acts {
|
|
if got := r.MayAct(a.seats...); got != a.want {
|
|
t.Fatalf("waiting: MayAct(%v) = %v, want %v", a.seats, got, a.want)
|
|
}
|
|
}
|
|
owner.down = false
|
|
owner.table = map[string]string{"node-b": def("node-b", "node", "put")}
|
|
if err := r.Reload(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st := r.Status(); !st.Heard || len(st.Waiting) != len(boot) || st.LastError != "" || st.Revision != 7 {
|
|
t.Fatalf("status after the seat answered %+v: want heard, the bootstrap's seats it does not hold still waiting, no error, revision 7", st)
|
|
}
|
|
if !r.MayAct("node-files") || !r.MayAct() {
|
|
t.Fatal("after the seat answered, a host acts on any module")
|
|
}
|
|
}
|
|
|
|
// ADR 0636 §7: the two tools read what is held and read again now, the second to its callers only.
|
|
func TestTheTwoToolsReadTheHeldListAndReadItAgainForTheirCallers(t *testing.T) {
|
|
owner := &fakeOwner{down: true, table: map[string]string{"node-b": def("node-b", "node", "put")}}
|
|
r, err := Start(Config{Bus: owner, Bootstrap: Bootstrapped()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
<-r.Ready()
|
|
callers := []string{stdio.AnyPerson, "mesh-healers"}
|
|
tools := Tools(r, "seat_list", "seat_list_again", callers)
|
|
out, err := tools[0].Run(map[string]any{})
|
|
if err != nil || out.(map[string]any)["health"] == "" || !strings.Contains(fmt.Sprint(out.(map[string]any)["status"]), "bootstrap") {
|
|
t.Fatalf("read: %v %v", out, err)
|
|
}
|
|
for caller, allowed := range map[string]bool{"person.jo": true, "ace.mesh-healers": true, "ace.mesh-other": false, "person.al": true, stdio.Unproven: false} {
|
|
if err := Refusal("seat_list_again", callers, caller); (err == nil) != allowed {
|
|
t.Fatalf("%s: %v, allowed %v", caller, err, allowed)
|
|
}
|
|
}
|
|
if _, err := tools[1].Run(map[string]any{}); err == nil || !strings.Contains(err.Error(), "refused") || len(owner.asked) != 1 {
|
|
t.Fatalf("a call without a proven caller read the list again: %v, asks %v", err, owner.asked)
|
|
}
|
|
owner.down = false
|
|
if err := r.Reload(); err != nil || !r.Status().Heard || len(r.Status().Held) != len(Bootstrapped())+1 {
|
|
t.Fatalf("again: %v %+v", err, r.Status())
|
|
}
|
|
}
|
|
|
|
// A reader started while the seat was silent is heard at the seat's changed event: the whole list is read then (ADR 0636 §1).
|
|
func TestAReaderStartedWhileTheSeatWasSilentIsHeardAtItsChangedEvent(t *testing.T) {
|
|
owner := &fakeOwner{down: true}
|
|
r, err := Start(Config{Bus: owner, Bootstrap: Bootstrapped()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
<-r.Ready()
|
|
if r.Status().Heard || r.MayAct("node-files") {
|
|
t.Fatal("the silent seat must leave the reader unheard")
|
|
}
|
|
owner.down = false
|
|
owner.table = map[string]string{"node-a": def("node-a", "node", "put"), "node-b": def("node-b", "node", "put")}
|
|
if err := owner.say(ChangedEvent, "node-a", SourceTrunk); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st := r.Status(); !st.Heard || st.LastError != "" || !r.MayAct("node-files") || !r.Has("node-a") || !r.Has("node-b") {
|
|
t.Fatalf("after the changed event: %+v, node-b held %v: want heard and the whole list read", st, r.Has("node-b"))
|
|
}
|
|
}
|
|
|
|
// ADR 0636 §1: Start returns at once whatever the seat does; until the first read has ended the reader acts on nothing new
|
|
// and says it is reading.
|
|
func TestStartReturnsAtOnceAndTheReaderSaysItIsReadingUntilTheSeatAnswers(t *testing.T) {
|
|
owner := &fakeOwner{gate: make(chan struct{}), table: map[string]string{"node-b": def("node-b", "node", "put")}}
|
|
r, err := Start(Config{Bus: owner, Bootstrap: Bootstrapped()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st := r.Status()
|
|
if !st.Reading || st.Heard || !strings.Contains(st.Health(), "first read") || r.MayAct("node-b") || !r.MayAct("node-engine") {
|
|
t.Fatalf("while the seat is slow: %+v", st)
|
|
}
|
|
close(owner.gate)
|
|
<-r.Ready()
|
|
if st := r.Status(); st.Reading || !st.Heard || !r.MayAct("node-b") {
|
|
t.Fatalf("after the read: %+v", st)
|
|
}
|
|
}
|