Files
2026-09-15 21:27:46 +05:00

242 lines
6.8 KiB
Go

package middleware_test
import (
"context"
stdErrors "errors"
"testing"
"time"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/ThreeDotsLabs/watermill/message/router/middleware"
"github.com/ThreeDotsLabs/watermill/message/subscriber"
"github.com/ThreeDotsLabs/watermill/pubsub/gochannel"
)
const topic = "testing_poison_queue_topic"
// TestPoisonQueue_handler_ok simulates the situation when the message is processed correctly
// We expect that all messages pass through the middleware unaffected and the poison queue catches no messages.
func TestPoisonQueue_handler_ok(t *testing.T) {
poisonPublisher := mockPublisher{behaviour: BehaviourAlwaysOK}
poisonQueue, err := middleware.PoisonQueue(&poisonPublisher, topic)
require.NoError(t, err)
poisonQueueWithFilter, err := middleware.PoisonQueueWithFilter(&poisonPublisher, topic, func(err error) bool {
return true
})
require.NoError(t, err)
testCases := []struct {
Name string
Middleware message.HandlerMiddleware
}{
{
Name: "PoisonQueue",
Middleware: poisonQueue,
},
{
Name: "PoisonQueueWithFilter",
Middleware: poisonQueueWithFilter,
},
}
for _, c := range testCases {
t.Run(c.Name, func(t *testing.T) {
produced, err := c.Middleware(handlerFuncAlwaysOK)(
message.NewMessage("uuid", nil),
)
assert.NoError(t, err)
assert.Equal(t, handlerFuncAlwaysOKMessages, produced)
assert.Empty(t, poisonPublisher.PopMessages())
})
}
}
func TestPoisonQueue_handler_failing(t *testing.T) {
poisonPublisher := mockPublisher{behaviour: BehaviourAlwaysOK}
poisonQueue, err := middleware.PoisonQueue(&poisonPublisher, topic)
require.NoError(t, err)
poisonQueueWithFilter, err := middleware.PoisonQueueWithFilter(&poisonPublisher, topic, func(err error) bool {
return true
})
require.NoError(t, err)
testCases := []struct {
Name string
Middleware message.HandlerMiddleware
}{
{
Name: "PoisonQueue",
Middleware: poisonQueue,
},
{
Name: "PoisonQueueWithFilter",
Middleware: poisonQueueWithFilter,
},
}
for _, c := range testCases {
t.Run(c.Name, func(t *testing.T) {
msg := message.NewMessage("uuid", []byte("payload"))
produced, err := c.Middleware(handlerFuncAlwaysFailing)(
msg,
)
// the middleware itself should not fail; the publisher is working OK, so no error is passed down the chain
assert.NoError(t, err)
// but no messages should be passed
assert.Empty(t, produced)
// the original message should end up in the poison queue
poisonMsgs := poisonPublisher.PopMessages()
require.Len(t, poisonMsgs, 1)
assert.Equal(t, msg.Payload, poisonMsgs[0].Payload)
// there should be additional metadata telling why the message was poisoned
// it should be the error that the handler failed with
assert.Equal(t, errFailed.Error(), poisonMsgs[0].Metadata.Get(middleware.ReasonForPoisonedKey))
})
}
}
func TestPoisonQueue_context_values(t *testing.T) {
pubSub := gochannel.NewGoChannel(
gochannel.Config{Persistent: true},
watermill.NewStdLogger(true, true),
)
logger := watermill.NewStdLogger(true, true)
router, err := message.NewRouter(message.RouterConfig{}, logger)
require.NoError(t, err)
pq, err := middleware.PoisonQueue(pubSub, "poison_queue")
require.NoError(t, err)
router.AddMiddleware(pq)
router.AddConsumerHandler("handler_name", "test", pubSub, func(msg *message.Message) error {
return errors.New("error")
})
go func() {
require.NoError(t, router.Run(context.Background()))
}()
require.NoError(t, err)
defer func() { _ = router.Close() }()
select {
case <-router.Running():
// ok
case <-time.After(time.Second):
t.Fatal("waiting for router timeout")
}
err = pubSub.Publish("test", message.NewMessage("1", nil))
require.NoError(t, err)
msgs, err := pubSub.Subscribe(context.Background(), "poison_queue")
require.NoError(t, err)
messages, all := subscriber.BulkRead(msgs, 1, time.Second)
require.True(t, all, "no messages received")
assert.Equal(t, "handler_name", messages[0].Metadata[middleware.PoisonedHandlerKey])
assert.Equal(t, "gochannel.GoChannel", messages[0].Metadata[middleware.PoisonedSubscriberKey])
assert.Equal(t, "test", messages[0].Metadata[middleware.PoisonedTopicKey])
assert.Equal(t, "error", messages[0].Metadata[middleware.ReasonForPoisonedKey])
}
func TestPoisonQueue_handler_failing_publisher_failing(t *testing.T) {
poisonPublisher := mockPublisher{behaviour: BehaviourAlwaysFail}
poisonQueue, err := middleware.PoisonQueue(&poisonPublisher, topic)
require.NoError(t, err)
poisonQueueWithFilter, err := middleware.PoisonQueueWithFilter(&poisonPublisher, topic, func(err error) bool {
return true
})
require.NoError(t, err)
testCases := []struct {
Name string
Middleware message.HandlerMiddleware
}{
{
Name: "PoisonQueue",
Middleware: poisonQueue,
},
{
Name: "PoisonQueueWithFilter",
Middleware: poisonQueueWithFilter,
},
}
for _, c := range testCases {
t.Run(c.Name, func(t *testing.T) {
msg := message.NewMessage("uuid", nil)
produced, err := poisonQueue(handlerFuncAlwaysFailing)(
msg,
)
// publisher failed, can't hide the error anymore
// Instead of checking the specific error, we check if the error.Is() is the same as the one we expect
assert.ErrorIs(t, err, errFailed)
// can't really expect any produced messages
assert.Empty(t, produced)
// nor poison messages
assert.Empty(t, poisonPublisher.PopMessages())
})
}
}
func TestPoisonQueueWithFilter_poison_queue(t *testing.T) {
poisonPublisher := mockPublisher{behaviour: BehaviourAlwaysOK}
poisonQueueErr := errors.New("poison queue err")
msg := message.NewMessage("uuid", []byte("payload"))
poisonQueue, err := middleware.PoisonQueueWithFilter(&poisonPublisher, topic, func(err error) bool {
return stdErrors.Is(err, poisonQueueErr)
})
require.NoError(t, err)
_, err = poisonQueue(func(msg *message.Message) (messages []*message.Message, e error) {
return nil, poisonQueueErr
})(msg)
assert.NoError(t, err)
require.Len(t, poisonPublisher.PopMessages(), 1)
}
func TestPoisonQueueWithFilter_non_poison_queue(t *testing.T) {
poisonPublisher := mockPublisher{behaviour: BehaviourAlwaysOK}
nonPoisonQueueErr := errors.New("non poison queue err")
msg := message.NewMessage("uuid", []byte("payload"))
poisonQueue, err := middleware.PoisonQueueWithFilter(&poisonPublisher, topic, func(err error) bool {
return !stdErrors.Is(err, nonPoisonQueueErr)
})
require.NoError(t, err)
_, err = poisonQueue(func(msg *message.Message) (messages []*message.Message, e error) {
return nil, nonPoisonQueueErr
})(msg)
assert.Error(t, err)
require.Len(t, poisonPublisher.PopMessages(), 0)
}