package pubsub_test import ( "context" "errors" "fmt" "testing" "time" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/internal/pubsub" "github.com/tendermint/tendermint/internal/pubsub/query" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/types" ) const ( clientID = "test-client" ) // pubstring is a trivial implementation of the EventData interface for // string-valued test data. type pubstring string func (pubstring) TypeTag() string { return "pubstring" } func TestSubscribeWithArgs(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() logger := log.NewNopLogger() s := newTestServer(ctx, t, logger) t.Run("DefaultLimit", func(t *testing.T) { sub := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{ ClientID: clientID, Query: query.All, })) require.Equal(t, 1, s.NumClients()) require.Equal(t, 1, s.NumClientSubscriptions(clientID)) require.NoError(t, s.Publish(ctx, pubstring("Ka-Zar"))) sub.mustReceive(ctx, pubstring("Ka-Zar")) }) t.Run("PositiveLimit", func(t *testing.T) { sub := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{ ClientID: clientID + "-2", Query: query.All, Limit: 10, })) require.NoError(t, s.Publish(ctx, pubstring("Aggamon"))) sub.mustReceive(ctx, pubstring("Aggamon")) }) } func TestObserver(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() logger := log.NewNopLogger() s := newTestServer(ctx, t, logger) done := make(chan struct{}) var got interface{} require.NoError(t, s.Observe(ctx, func(msg pubsub.Message) error { defer close(done) got = msg.Data() return nil })) const input = pubstring("Lions and tigers and bears, oh my!") require.NoError(t, s.Publish(ctx, input)) <-done require.Equal(t, got, input) } func TestObserverErrors(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() logger := log.NewNopLogger() s := newTestServer(ctx, t, logger) require.Error(t, s.Observe(ctx, nil, query.All)) require.NoError(t, s.Observe(ctx, func(pubsub.Message) error { return nil })) require.Error(t, s.Observe(ctx, func(pubsub.Message) error { return nil }, query.All)) } func TestPublishDoesNotBlock(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() logger := log.NewNopLogger() s := newTestServer(ctx, t, logger) sub := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{ ClientID: clientID, Query: query.All, })) published := make(chan struct{}) go func() { defer close(published) require.NoError(t, s.Publish(ctx, pubstring("Quicksilver"))) require.NoError(t, s.Publish(ctx, pubstring("Asylum"))) require.NoError(t, s.Publish(ctx, pubstring("Ivan"))) }() select { case <-published: sub.mustReceive(ctx, pubstring("Quicksilver")) sub.mustFail(ctx, pubsub.ErrTerminated) case <-time.After(3 * time.Second): t.Fatal("Publishing should not have blocked") } } func TestSubscribeErrors(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() logger := log.NewNopLogger() s := newTestServer(ctx, t, logger) t.Run("NegativeLimitErr", func(t *testing.T) { _, err := s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{ ClientID: clientID, Query: query.All, Limit: -5, }) require.Error(t, err) }) } func TestSlowSubscriber(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() logger := log.NewNopLogger() s := newTestServer(ctx, t, logger) sub := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{ ClientID: clientID, Query: query.All, })) require.NoError(t, s.Publish(ctx, pubstring("Fat Cobra"))) require.NoError(t, s.Publish(ctx, pubstring("Viper"))) require.NoError(t, s.Publish(ctx, pubstring("Black Panther"))) // We had capacity for one item, so we should get that item, but after that // the subscription should have been terminated by the publisher. sub.mustReceive(ctx, pubstring("Fat Cobra")) sub.mustFail(ctx, pubsub.ErrTerminated) } func TestDifferentClients(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() logger := log.NewNopLogger() s := newTestServer(ctx, t, logger) sub1 := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{ ClientID: "client-1", Query: query.MustCompile(`tm.events.type='NewBlock'`), })) events := []abci.Event{{ Type: "tm.events", Attributes: []abci.EventAttribute{{Key: "type", Value: "NewBlock"}}, }} require.NoError(t, s.PublishWithEvents(ctx, pubstring("Iceman"), events)) sub1.mustReceive(ctx, pubstring("Iceman")) sub2 := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{ ClientID: "client-2", Query: query.MustCompile(`tm.events.type='NewBlock' AND abci.account.name='Igor'`), })) events = []abci.Event{ { Type: "tm.events", Attributes: []abci.EventAttribute{{Key: "type", Value: "NewBlock"}}, }, { Type: "abci.account", Attributes: []abci.EventAttribute{{Key: "name", Value: "Igor"}}, }, } require.NoError(t, s.PublishWithEvents(ctx, pubstring("Ultimo"), events)) sub1.mustReceive(ctx, pubstring("Ultimo")) sub2.mustReceive(ctx, pubstring("Ultimo")) sub3 := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{ ClientID: "client-3", Query: query.MustCompile( `tm.events.type='NewRoundStep' AND abci.account.name='Igor' AND abci.invoice.number = 10`), })) events = []abci.Event{{ Type: "tm.events", Attributes: []abci.EventAttribute{{Key: "type", Value: "NewRoundStep"}}, }} require.NoError(t, s.PublishWithEvents(ctx, pubstring("Valeria Richards"), events)) sub3.mustTimeOut(ctx, 100*time.Millisecond) } func TestSubscribeDuplicateKeys(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() logger := log.NewNopLogger() s := newTestServer(ctx, t, logger) testCases := []struct { query string expected types.EventData }{ {`withdraw.rewards='17'`, pubstring("Iceman")}, {`withdraw.rewards='22'`, pubstring("Iceman")}, {`withdraw.rewards='1' AND withdraw.rewards='22'`, pubstring("Iceman")}, {`withdraw.rewards='100'`, nil}, } for i, tc := range testCases { id := fmt.Sprintf("client-%d", i) q := query.MustCompile(tc.query) t.Run(id, func(t *testing.T) { sub := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{ ClientID: id, Query: q, })) events := []abci.Event{ { Type: "transfer", Attributes: []abci.EventAttribute{ {Key: "sender", Value: "foo"}, {Key: "sender", Value: "bar"}, {Key: "sender", Value: "baz"}, }, }, { Type: "withdraw", Attributes: []abci.EventAttribute{ {Key: "rewards", Value: "1"}, {Key: "rewards", Value: "17"}, {Key: "rewards", Value: "22"}, }, }, } require.NoError(t, s.PublishWithEvents(ctx, pubstring("Iceman"), events)) if tc.expected != nil { sub.mustReceive(ctx, tc.expected) } else { sub.mustTimeOut(ctx, 100*time.Millisecond) } }) } } func TestClientSubscribesTwice(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() logger := log.NewNopLogger() s := newTestServer(ctx, t, logger) q := query.MustCompile(`tm.events.type='NewBlock'`) events := []abci.Event{{ Type: "tm.events", Attributes: []abci.EventAttribute{{Key: "type", Value: "NewBlock"}}, }} sub1 := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{ ClientID: clientID, Query: q, })) require.NoError(t, s.PublishWithEvents(ctx, pubstring("Goblin Queen"), events)) sub1.mustReceive(ctx, pubstring("Goblin Queen")) // Subscribing a second time with the same client ID and query fails. { sub2, err := s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{ ClientID: clientID, Query: q, }) require.Error(t, err) require.Nil(t, sub2) } // The attempt to re-subscribe does not disrupt the existing sub. require.NoError(t, s.PublishWithEvents(ctx, pubstring("Spider-Man"), events)) sub1.mustReceive(ctx, pubstring("Spider-Man")) } func TestUnsubscribe(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() logger := log.NewNopLogger() s := newTestServer(ctx, t, logger) sub := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{ ClientID: clientID, Query: query.MustCompile(`tm.events.type='NewBlock'`), })) // Removing the subscription we just made should succeed. require.NoError(t, s.Unsubscribe(ctx, pubsub.UnsubscribeArgs{ Subscriber: clientID, Query: query.MustCompile(`tm.events.type='NewBlock'`), })) // Publishing should still work. require.NoError(t, s.Publish(ctx, pubstring("Nick Fury"))) // The unsubscribed subscriber should report as such. sub.mustFail(ctx, pubsub.ErrUnsubscribed) } func TestClientUnsubscribesTwice(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() logger := log.NewNopLogger() s := newTestServer(ctx, t, logger) newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{ ClientID: clientID, Query: query.MustCompile(`tm.events.type='NewBlock'`), })) require.NoError(t, s.Unsubscribe(ctx, pubsub.UnsubscribeArgs{ Subscriber: clientID, Query: query.MustCompile(`tm.events.type='NewBlock'`), })) require.ErrorIs(t, s.Unsubscribe(ctx, pubsub.UnsubscribeArgs{ Subscriber: clientID, Query: query.MustCompile(`tm.events.type='NewBlock'`), }), pubsub.ErrSubscriptionNotFound) require.ErrorIs(t, s.UnsubscribeAll(ctx, clientID), pubsub.ErrSubscriptionNotFound) } func TestResubscribe(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() logger := log.NewNopLogger() s := newTestServer(ctx, t, logger) args := pubsub.SubscribeArgs{ ClientID: clientID, Query: query.All, } newTestSub(t).must(s.SubscribeWithArgs(ctx, args)) require.NoError(t, s.Unsubscribe(ctx, pubsub.UnsubscribeArgs{ Subscriber: clientID, Query: query.All, })) sub := newTestSub(t).must(s.SubscribeWithArgs(ctx, args)) require.NoError(t, s.Publish(ctx, pubstring("Cable"))) sub.mustReceive(ctx, pubstring("Cable")) } func TestUnsubscribeAll(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() logger := log.NewNopLogger() s := newTestServer(ctx, t, logger) sub1 := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{ ClientID: clientID, Query: query.MustCompile(`tm.events.type='NewBlock'`), })) sub2 := newTestSub(t).must(s.SubscribeWithArgs(ctx, pubsub.SubscribeArgs{ ClientID: clientID, Query: query.MustCompile(`tm.events.type='NewBlockHeader'`), })) require.NoError(t, s.UnsubscribeAll(ctx, clientID)) require.NoError(t, s.Publish(ctx, pubstring("Nick Fury"))) sub1.mustFail(ctx, pubsub.ErrUnsubscribed) sub2.mustFail(ctx, pubsub.ErrUnsubscribed) } func TestBufferCapacity(t *testing.T) { logger := log.NewNopLogger() s := pubsub.NewServer(logger, pubsub.BufferCapacity(2)) require.Equal(t, 2, s.BufferCapacity()) ctx, cancel := context.WithCancel(context.Background()) defer cancel() require.NoError(t, s.Publish(ctx, pubstring("Nighthawk"))) require.NoError(t, s.Publish(ctx, pubstring("Sage"))) ctx, cancel = context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() require.ErrorIs(t, s.Publish(ctx, pubstring("Ironclad")), context.DeadlineExceeded) } func newTestServer(ctx context.Context, t testing.TB, logger log.Logger) *pubsub.Server { t.Helper() s := pubsub.NewServer(logger) require.NoError(t, s.Start(ctx)) t.Cleanup(s.Wait) return s } type testSub struct { t testing.TB *pubsub.Subscription } func newTestSub(t testing.TB) *testSub { return &testSub{t: t} } func (s *testSub) must(sub *pubsub.Subscription, err error) *testSub { s.t.Helper() require.NoError(s.t, err) require.NotNil(s.t, sub) s.Subscription = sub return s } func (s *testSub) mustReceive(ctx context.Context, want types.EventData) { s.t.Helper() got, err := s.Next(ctx) require.NoError(s.t, err) require.Equal(s.t, want, got.Data()) } func (s *testSub) mustTimeOut(ctx context.Context, dur time.Duration) { s.t.Helper() tctx, cancel := context.WithTimeout(ctx, dur) defer cancel() got, err := s.Next(tctx) if !errors.Is(err, context.DeadlineExceeded) { s.t.Errorf("Next: got (%+v, %v), want %v", got, err, context.DeadlineExceeded) } } func (s *testSub) mustFail(ctx context.Context, want error) { s.t.Helper() got, err := s.Next(ctx) if err == nil && want != nil { s.t.Fatalf("Next: got (%+v, %v), want error %v", got, err, want) } require.ErrorIs(s.t, err, want) }