Files
mesh-controller/cmd/mesh-controller/reissue_verified_sender_test.go
T
jschoubben 87410d79ed
mesh/merge-gate head 87410d79edd6 merged into trunk e231185572ba — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 87410d79edd6 merged into trunk e231185572ba — pass: its merge-check.sh passed
mesh/delivery delivered
Issue a verified sender again only over the membership that was read, so a newer push is not overwritten (issue 930)
2026-10-16 17:46:18 +02:00

188 lines
7.7 KiB
Go

package main
import (
"encoding/json"
"errors"
"strings"
"testing"
"github.com/nats-io/nats.go"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/link"
"github.com/novox/mesh-controller/internal/testbus"
)
func verifiedSenderRecords(free map[string]bool) (broker.Records, broker.Declared) {
tg := broker.Seat{Name: "channel", Scope: "mesh", Accepts: []string{"show"}, Kinded: true, Kind: "telegram",
DeclaredBy: "messenger", Capabilities: []string{"choice", "verified-sender"}}
router := broker.Declared{Module: "messenger", RunsAs: "messenger", Holds: []broker.Seat{{Name: "operator-channel",
Scope: "mesh", Accepts: []string{"ask"}}}, Uses: []broker.Seat{{Name: "channel", Scope: "mesh", Kinded: true,
DeclaredBy: "messenger"}}}
return broker.Records{Nodes: []string{"novox"}, RootFree: free, Assigned: map[string][]broker.Declared{
"novox": {router, {Module: "telegram", RunsAs: "telegram", Holds: []broker.Seat{tg}}}}}, router
}
type fakeBus struct {
last map[string][]byte
published map[string][]byte
seq map[string]uint64
// between runs once, after a read and before the write: a push issuing meanwhile.
between func()
}
func (f *fakeBus) get(node, module string) ([]byte, uint64, error) {
if b, ok := f.last[node+"/"+module]; ok {
return b, f.seq[node+"/"+module], nil
}
return nil, 0, nats.ErrMsgNotFound
}
func (f *fakeBus) put(node, module string, body []byte, seq uint64) error {
if f.between != nil {
f.between()
f.between = nil
}
if seq != f.seq[node+"/"+module] {
return errors.New("nats: wrong last sequence")
}
f.published[node+"/"+module] = body
f.last[node+"/"+module] = body
f.seq[node+"/"+module]++
return nil
}
func hasVerified(t *testing.T, membership []byte) bool {
t.Helper()
var m broker.Membership
if err := json.Unmarshal(membership, &m); err != nil {
t.Fatal(err)
}
for _, k := range m.SeatTraffic.Kinds {
if k.Kind == "telegram" {
return strings.Contains(strings.Join(k.Capabilities, ","), "verified-sender")
}
}
t.Fatal("telegram not in the router's membership")
return false
}
// A membership composed while the judgement failed is issued again once it passes, with the verified sender
// (novox/hq issue 930); while it fails, nothing is given back (ADR 0259 §8).
func TestAMembershipThatLostItsVerifiedSenderGetsItBackWhenTheJudgementPasses(t *testing.T) {
failed, router := verifiedSenderRecords(nil)
whereFailed := broker.PlacementsOf(failed, nil)
bus := &fakeBus{last: map[string][]byte{}, published: map[string][]byte{}, seq: map[string]uint64{}}
body, _ := json.Marshal(broker.MembershipFor("novox", router, whereFailed))
bus.last["novox/messenger"] = body
if hasVerified(t, body) {
t.Fatal("composed with a failed judgement, and the verified sender is kept")
}
say := func(string, ...any) {}
if n := reissueVerifiedSenders(failed, whereFailed, bus.get, bus.put, say); n != 0 || len(bus.published) != 0 {
t.Fatalf("issued %d while the judgement still fails", n)
}
passing, _ := verifiedSenderRecords(map[string]bool{"novox": true})
wherePassing := broker.PlacementsOf(passing, nil)
if n := reissueVerifiedSenders(passing, wherePassing, bus.get, bus.put, say); n != 1 {
t.Fatalf("issued %d memberships after the judgement passed, want 1", n)
}
if !hasVerified(t, bus.published["novox/messenger"]) {
t.Fatal("the membership issued again lacks the verified sender")
}
if n := reissueVerifiedSenders(passing, wherePassing, bus.get, bus.put, say); n != 0 {
t.Fatalf("issued %d again with nothing changed", n)
}
}
// What a composition drops it says, naming the kind, the machine and the judgement's reason.
func TestADroppedVerifiedSenderIsSaidWithItsReason(t *testing.T) {
records, _ := verifiedSenderRecords(nil)
where := broker.PlacementsOf(records, nil)
lines := droppedVerifiedSenders(records, where, map[string]string{"novox": "not measured: no verb heard"})
if len(lines) != 1 || !strings.Contains(lines[0], "telegram") || !strings.Contains(lines[0], "novox") ||
!strings.Contains(lines[0], "not measured: no verb heard") {
t.Fatalf("said %q", lines)
}
records, _ = verifiedSenderRecords(map[string]bool{"novox": true})
if lines := droppedVerifiedSenders(records, broker.PlacementsOf(records, nil), nil); len(lines) != 0 {
t.Fatalf("said %q of a kind that kept its verified sender", lines)
}
}
func TestAMembershipNeverIssuedIsLeftToItsOwnSend(t *testing.T) {
records, _ := verifiedSenderRecords(map[string]bool{"novox": true})
bus := &fakeBus{last: map[string][]byte{}, published: map[string][]byte{}, seq: map[string]uint64{}}
var said []string
n := reissueVerifiedSenders(records, broker.PlacementsOf(records, nil), bus.get, bus.put,
func(f string, a ...any) { said = append(said, f) })
if n != 0 || len(said) != 0 {
t.Fatalf("issued %d, said %v", n, said)
}
}
// **On a real bus** (novox/hq issue 930): a router's membership issued without the verified sender because the
// judgement failed is, once the judgement passes, published again to the AssignmentsStream with it in its seat
// traffic's kinds, and a second pass publishes nothing.
func TestNatsAMembershipIssuedWithoutItsVerifiedSenderIsIssuedAgainWhenTheJudgementPasses(t *testing.T) {
js, err := broker.Dial(testbus.URL(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(js.Close)
if err := broker.AssertMeshStreams(js); err != nil {
t.Fatal(err)
}
bus := link.OverNATS{Conn: js.Conn(), JS: js.Context()}
failed, router := verifiedSenderRecords(nil)
body, err := json.Marshal(broker.MembershipFor("novox", router, broker.PlacementsOf(failed, nil)))
if err != nil {
t.Fatal(err)
}
if err := bus.PublishMembership(t.Context(), "novox", "messenger", body); err != nil {
t.Fatal(err)
}
issued := func() []byte {
m, err := js.Context().GetLastMsg(broker.AssignmentsStream, broker.MembershipSubject("novox", "messenger"))
if err != nil {
t.Fatal(err)
}
return m.Data
}
if hasVerified(t, issued()) {
t.Fatal("issued with a failed judgement, and the verified sender is kept: the test proves nothing")
}
if n := reissueVerifiedSendersOn(t.Context(), bus, failed, broker.PlacementsOf(failed, nil)); n != 0 {
t.Fatalf("issued %d memberships while the judgement still fails", n)
}
passing, _ := verifiedSenderRecords(map[string]bool{"novox": true})
where := broker.PlacementsOf(passing, nil)
if n := reissueVerifiedSendersOn(t.Context(), bus, passing, where); n != 1 {
t.Fatalf("issued %d memberships after the judgement passed, want 1", n)
}
if !hasVerified(t, issued()) {
t.Fatal("the membership on the bus lacks the verified sender after the judgement passed")
}
if n := reissueVerifiedSendersOn(t.Context(), bus, passing, where); n != 0 {
t.Fatalf("a second pass issued %d memberships", n)
}
}
// A push issuing a newer membership between the self-check's read and its write is not overwritten (issue 930).
func TestANewerMembershipIssuedMeanwhileIsNotOverwritten(t *testing.T) {
failed, router := verifiedSenderRecords(nil)
bus := &fakeBus{last: map[string][]byte{}, published: map[string][]byte{}, seq: map[string]uint64{}}
old, _ := json.Marshal(broker.MembershipFor("novox", router, broker.PlacementsOf(failed, nil)))
bus.last["novox/messenger"], bus.seq["novox/messenger"] = old, 1
newer := []byte(`{"node":"novox","module":"messenger","newer":true}`)
bus.between = func() { bus.last["novox/messenger"], bus.seq["novox/messenger"] = newer, 2 }
passing, _ := verifiedSenderRecords(map[string]bool{"novox": true})
if n := reissueVerifiedSenders(passing, broker.PlacementsOf(passing, nil), bus.get, bus.put, func(string, ...any) {}); n != 0 {
t.Fatalf("issued %d over a newer membership", n)
}
if string(bus.last["novox/messenger"]) != string(newer) || len(bus.published) != 0 {
t.Fatalf("the newer membership was overwritten: %s", bus.last["novox/messenger"])
}
}