package main import ( "context" "errors" "reflect" "strings" "testing" "github.com/novox/mesh-controller/internal/link" ) // recordingDelivery is a delivery that writes down what was done, in order, and fails where told. type recordingDelivery struct { did []string grantErr error declareErr error } func (r *recordingDelivery) grant(_ context.Context, sending []readyNode) error { for _, s := range sending { r.did = append(r.did, "grant "+s.node) } return r.grantErr } func (r *recordingDelivery) declare(_ context.Context, s readyNode, _ []byte) (string, error) { if r.declareErr != nil { return "", r.declareErr } r.did = append(r.did, "declare "+s.node) return "digest-" + s.node, nil } func ready(names ...string) []readyNode { var out []readyNode for _, n := range names { out = append(out, readyNode{node: n, declared: sendable{Resources: []map[string]any{{"id": "x"}}}}) } return out } // novox/hq issue 249: the grants that come with a module's new declarations are issued before any // machine is sent the code that uses them — except the machine holding the bus, whose declaration // carries the controller's own right to issue them, and goes first. func TestGrantsAreIssuedBeforeTheDeclarations(t *testing.T) { d := &recordingDelivery{} digests, err := deliver(t.Context(), d, "", ready("anchor", "laptop")) if err != nil { t.Fatal(err) } want := []string{"grant anchor", "grant laptop", "declare anchor", "declare laptop"} if !reflect.DeepEqual(d.did, want) { t.Fatalf("delivered in the order %v, wanted %v", d.did, want) } if digests["anchor"] != "digest-anchor" || digests["laptop"] != "digest-laptop" { t.Fatalf("the digests sent were not answered: %v", digests) } held := &recordingDelivery{} if _, err := deliver(t.Context(), held, "broker", ready("broker", "anchor")); err != nil { t.Fatal(err) } want = []string{"declare broker", "grant broker", "grant anchor", "declare anchor"} if !reflect.DeepEqual(held.did, want) { t.Fatalf("with the bus's machine in the send: %v, wanted %v", held.did, want) } } // A grant that cannot be issued holds back the machines it concerns and is an error the caller // retries on — never "until the next push" — and the bus's own machine is sent regardless, so the // grant that would let the controller issue memberships is never held behind them. func TestAGrantThatFailsHoldsBackWhatItConcerns(t *testing.T) { // A failure naming no machine (the buckets): everything but the bus's machine. d := &recordingDelivery{grantErr: errors.New("the bus refused the bucket")} _, err := deliver(t.Context(), d, "broker", ready("broker", "anchor", "laptop")) if err == nil || !errors.Is(err, errGrants) || !strings.Contains(err.Error(), "the bus refused the bucket") || !strings.Contains(err.Error(), "anchor, laptop not sent") { t.Fatalf("a failed grant was not said as the send's failure: %v", err) } if want := []string{"declare broker", "grant broker", "grant anchor", "grant laptop"}; !reflect.DeepEqual(d.did, want) { t.Fatalf("delivered %v, wanted the bus's machine alone", d.did) } // A membership that failed for one machine: that machine alone. one := &recordingDelivery{grantErr: &grantsRefused{nodes: map[string]error{"laptop": errors.New("no")}}} _, err = deliver(t.Context(), one, "", ready("anchor", "laptop")) if !errors.Is(err, errGrants) || !strings.Contains(err.Error(), "laptop not sent") { t.Fatalf("one machine's refused membership was not said: %v", err) } if want := []string{"grant anchor", "grant laptop", "declare anchor"}; !reflect.DeepEqual(one.did, want) { t.Fatalf("delivered %v, wanted anchor sent and laptop held back", one.did) } // The announced upgrade that hit it is asked again. if !errors.Is(askAgainOnGrants(err), link.ErrTryAgain) { t.Fatal("an announcement whose send stopped at its grants is not asked again") } if other := errors.New("laptop could not be resolved"); errors.Is(askAgainOnGrants(other), link.ErrTryAgain) { t.Fatal("any failure is asked again, not only a grant's") } // Nothing to send is nothing granted either. none := &recordingDelivery{grantErr: errors.New("never asked")} if _, err := deliver(t.Context(), none, "", nil); err != nil || len(none.did) != 0 { t.Fatalf("an empty send granted or failed: %v %v", none.did, err) } } // Whether the bus's machine goes first is read from the user list alone, by its digest. func TestTheBusMachineIsBehindByItsUserListAlone(t *testing.T) { list := "users: [a, b]" if userListBehind(list, digestOf([]byte(list))) { t.Fatal("the list it was sent reads as behind") } if !userListBehind(list, digestOf([]byte("users: [a]"))) || !userListBehind(list, "") { t.Fatal("a changed or never-sent list reads as current") } if userListBehind("", "") { t.Fatal("a machine sent no list reads as behind") } } // The machine holding the bus goes first: its declaration carries the user list the new grants are // checked against. Among the machines it is moved to the front; not among them it is added only // when it is behind. func TestTheMachineHoldingTheBusIsSentFirst(t *testing.T) { for _, c := range []struct { what string names []string holder string behind bool want []string }{ {"among them", []string{"ace", "g14", "novox"}, "novox", false, []string{"novox", "ace", "g14"}}, {"not among them, behind", []string{"ace", "g14"}, "novox", true, []string{"novox", "ace", "g14"}}, {"not among them, current", []string{"ace", "g14"}, "novox", false, []string{"ace", "g14"}}, {"nothing holds the bus", []string{"ace", "g14"}, "", true, []string{"ace", "g14"}}, {"only it", []string{"novox"}, "novox", false, []string{"novox"}}, } { if got := brokerFirst(c.names, c.holder, c.behind); !reflect.DeepEqual(got, c.want) { t.Errorf("%s: sent in the order %v, wanted %v", c.what, got, c.want) } } }