package bus_test import ( "encoding/json" "errors" "fmt" "sync" "testing" "time" "github.com/novox/mesh-tools/node-tools/internal/bus" mt "github.com/novox/mesh-tools/node-tools/internal/meshtest" ) // streamMesh is a mesh with the mail module's own stream MAIL, and a connection on anchor following the // memberships of the modules named. func streamMesh(t *testing.T, memberships ...bus.Membership) (*mt.Mesh, *bus.Conn) { t.Helper() mesh := mt.New(t) mesh.ModuleStream(t, "mail", "MAIL") for _, m := range memberships { mesh.Issue(t, m) } conn, err := bus.Connect(bus.Credential{URL: mt.URL(t), Module: "node-tools", Node: "anchor"}) if err != nil { t.Fatal(err) } conn.Logf = func(string, ...any) {} t.Cleanup(conn.Close) for _, m := range memberships { conn.Follow(m.Module) } mt.Until(t, func() error { for _, m := range memberships { if conn.Membership(m.Module) == nil { return errors.New(m.Module + " not issued yet") } } return nil }) return mesh, conn } // novox/hq ADR 0363 §2, §8 (issue 725): an event one of the module's streams carries is published on that // stream's subject and kept by that stream alone; any other event of it goes to EVENTS as before. func TestAnEventADeclaredStreamCarriesIsPublishedOnItsSubjectAlone(t *testing.T) { mail := mt.MembershipOf("mail", "anchor", true, nil) mail.Streams = []bus.StreamIssued{{Stream: "MAIL", Events: []string{"mail.*.message.added"}, Subject: "mesh.stream.mail.event.{event}"}} mesh, conn := streamMesh(t, mail) landed := mesh.NextEvent(t, "mesh.stream.mail.event.>") if err := conn.PublishAs("mail", bus.Envelope{Key: "mail.home.message.added", Body: json.RawMessage(`{"id":1}`)}); err != nil { t.Fatal(err) } select { case got := <-landed: if got != "mesh.stream.mail.event.mail.home.message.added" { t.Errorf("published on %s", got) } case <-time.After(5 * time.Second): t.Fatal("the event did not land on its stream's subject") } if err := conn.PublishAs("mail", bus.Envelope{Key: "status", Body: json.RawMessage(`{}`)}); err != nil { t.Fatal(err) } if n := mesh.Messages(t, "MAIL"); n != 1 { t.Errorf("MAIL holds %d messages, want the one event it carries", n) } if n := mesh.Messages(t, "EVENTS"); n != 1 { t.Errorf("EVENTS holds %d messages, want only the event no stream carries", n) } } // novox/hq ADR 0363 §8 (issue 725): a module hears, beside EVENTS, every module stream its membership names, // one event at a time, with the key it reads on EVENTS; a membership naming none hears EVENTS alone. func TestAModuleHearsEveryStreamItsMembershipNames(t *testing.T) { reader := mt.MembershipOf("reader", "anchor", false, nil) mesh, conn := streamMesh(t, reader) mesh.Consumer(t, "anchor", "reader", []string{"mesh.mod.alpha.event.>"}, 5*time.Second) mesh.ConsumerOn(t, "MAIL", "anchor", "reader", []string{"mesh.stream.mail.event.mail.home.>"}, 5*time.Second) var mu sync.Mutex heard := map[string]int{} handling := 0 stop, err := conn.ConsumeAs("reader", func(env bus.Envelope) error { mu.Lock() handling++ if handling > 1 { t.Error("two events were handed to the module at once") } mu.Unlock() time.Sleep(50 * time.Millisecond) mu.Lock() handling-- heard[env.Key]++ mu.Unlock() return nil }) if err != nil { t.Fatal(err) } t.Cleanup(stop) count := func(key string) int { mu.Lock() defer mu.Unlock() return heard[key] } // Its membership names no stream: EVENTS alone, as before. mesh.Emit(t, "mesh.mod.alpha.event.happened", map[string]any{"n": 1}) mesh.Emit(t, "mesh.stream.mail.event.mail.home.message.added", map[string]any{"n": 1}) mt.Until(t, func() error { if count("alpha.happened") != 1 { return errors.New("the event on EVENTS was not heard") } return nil }) if _, notYet := mesh.PendingOn(t, "MAIL", "anchor", "reader"); notYet != 1 || count("mail.mail.home.message.added") != 0 { t.Fatalf("a module whose membership names no stream heard MAIL (%d not yet delivered)", notYet) } // Its membership now names its consumer on MAIL: bound, and what waited there is heard. reader.Hears = []bus.HeardIssued{{Stream: "MAIL", Consumer: "anchor_reader"}} mesh.Issue(t, reader) mt.Until(t, func() error { if count("mail.mail.home.message.added") != 1 { return errors.New("the event on MAIL was not heard") } return nil }) for n := 2; n <= 4; n++ { mesh.Emit(t, "mesh.mod.alpha.event.happened", map[string]any{"n": n}) mesh.Emit(t, "mesh.stream.mail.event.mail.home.message.added", map[string]any{"n": n}) } mt.Until(t, func() error { if a, m := count("alpha.happened"), count("mail.mail.home.message.added"); a != 4 || m != 4 { return fmt.Errorf("heard %d on EVENTS and %d on MAIL, want 4 and 4", a, m) } for _, stream := range []string{"EVENTS", "MAIL"} { if ack, notYet := mesh.PendingOn(t, stream, "anchor", "reader"); ack != 0 || notYet != 0 { return fmt.Errorf("%s still holds %d unacknowledged, %d undelivered", stream, ack, notYet) } } return nil }) // A membership that stops naming it stops what it reads there. reader.Hears = nil mesh.Issue(t, reader) mt.Until(t, func() error { if mesh.Consumers(t, "MAIL") != 1 { return errors.New("MAIL's consumer is gone") } mesh.Emit(t, "mesh.stream.mail.event.mail.home.message.added", map[string]any{"n": 0}) time.Sleep(300 * time.Millisecond) if _, notYet := mesh.PendingOn(t, "MAIL", "anchor", "reader"); notYet == 0 { return errors.New("MAIL is still read") } return nil }) } // novox/hq ADR 0363 §6 (issue 725): a consumer the membership names before the controller made it is bound once // it exists, without the module's events on EVENTS waiting for it. func TestAConsumerNamedBeforeItExistsIsBoundOnceItDoes(t *testing.T) { restore := bus.RebindEvery bus.RebindEvery = 200 * time.Millisecond t.Cleanup(func() { bus.RebindEvery = restore }) reader := mt.MembershipOf("reader", "anchor", false, nil) reader.Hears = []bus.HeardIssued{{Stream: "MAIL", Consumer: "anchor_reader"}} mesh, conn := streamMesh(t, reader) mesh.Consumer(t, "anchor", "reader", []string{"mesh.mod.alpha.event.>"}, 5*time.Second) var mu sync.Mutex heard := map[string]int{} stop, err := conn.ConsumeAs("reader", func(env bus.Envelope) error { mu.Lock() heard[env.Key]++ mu.Unlock() return nil }) if err != nil { t.Fatalf("a consumer on a module stream not made yet kept the module from hearing EVENTS: %v", err) } t.Cleanup(stop) mesh.Emit(t, "mesh.mod.alpha.event.happened", map[string]any{}) mesh.ConsumerOn(t, "MAIL", "anchor", "reader", []string{"mesh.stream.mail.event.>"}, 5*time.Second) mesh.Emit(t, "mesh.stream.mail.event.mail.home.message.added", map[string]any{}) mt.Until(t, func() error { mu.Lock() defer mu.Unlock() if heard["alpha.happened"] != 1 || heard["mail.mail.home.message.added"] != 1 { return fmt.Errorf("heard %v", heard) } return nil }) }