Files
upstream-watermill/components/cqrs/event_processor_test.go
2026-09-15 21:27:46 +05:00

540 lines
13 KiB
Go

package cqrs_test
import (
"context"
"fmt"
"sync/atomic"
"testing"
"time"
"github.com/pkg/errors"
"github.com/ThreeDotsLabs/watermill/components/cqrs"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEventProcessorConfig_Validate(t *testing.T) {
testCases := []struct {
Name string
ModifyValidConfig func(*cqrs.EventProcessorConfig)
ExpectedErr error
}{
{
Name: "valid_config",
ModifyValidConfig: nil,
ExpectedErr: nil,
},
{
Name: "missing_GenerateHandlerSubscribeTopic",
ModifyValidConfig: func(config *cqrs.EventProcessorConfig) {
config.GenerateSubscribeTopic = nil
},
ExpectedErr: fmt.Errorf("missing GenerateHandlerTopic"),
},
{
Name: "missing_marshaler",
ModifyValidConfig: func(config *cqrs.EventProcessorConfig) {
config.Marshaler = nil
},
ExpectedErr: fmt.Errorf("missing Marshaler"),
},
{
Name: "missing_subscriber_constructor",
ModifyValidConfig: func(config *cqrs.EventProcessorConfig) {
config.SubscriberConstructor = nil
},
ExpectedErr: fmt.Errorf("missing SubscriberConstructor"),
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.Name, func(t *testing.T) {
validConfig := cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return nil, nil
},
Marshaler: cqrs.JSONMarshaler{},
}
if tc.ModifyValidConfig != nil {
tc.ModifyValidConfig(&validConfig)
}
err := validConfig.Validate()
if tc.ExpectedErr == nil {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tc.ExpectedErr.Error())
}
})
}
}
func TestNewEventProcessor(t *testing.T) {
eventConfig := cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return nil, nil
},
Marshaler: cqrs.JSONMarshaler{},
}
require.NoError(t, eventConfig.Validate())
router, err := message.NewRouter(message.RouterConfig{}, nil)
require.NoError(t, err)
cp, err := cqrs.NewEventProcessorWithConfig(router, eventConfig)
assert.NotNil(t, cp)
assert.NoError(t, err)
eventConfig.SubscriberConstructor = nil
require.Error(t, eventConfig.Validate())
cp, err = cqrs.NewEventProcessorWithConfig(router, eventConfig)
assert.Nil(t, cp)
assert.Error(t, err)
}
type nonPointerEventProcessor struct {
}
func (nonPointerEventProcessor) HandlerName() string {
return "nonPointerEventProcessor"
}
func (nonPointerEventProcessor) NewEvent() interface{} {
return TestEvent{}
}
func (nonPointerEventProcessor) Handle(ctx context.Context, cmd interface{}) error {
panic("not implemented")
}
func TestEventProcessor_non_pointer_event(t *testing.T) {
ts := NewTestServices()
handler := nonPointerEventProcessor{}
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
eventProcessor, err := cqrs.NewEventProcessorWithConfig(
router,
cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return nil, nil
},
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = eventProcessor.AddHandlers(handler)
assert.IsType(t, cqrs.NonPointerError{}, errors.Cause(err))
}
type duplicateTestEventHandler1 struct{}
func (h duplicateTestEventHandler1) HandlerName() string {
return "duplicateTestEventHandler1"
}
func (duplicateTestEventHandler1) NewEvent() interface{} {
return &TestEvent{}
}
func (h *duplicateTestEventHandler1) Handle(ctx context.Context, event interface{}) error { return nil }
type duplicateTestEventHandler2 struct{}
func (h duplicateTestEventHandler2) HandlerName() string {
return "duplicateTestEventHandler2"
}
func (duplicateTestEventHandler2) NewEvent() interface{} {
return &TestEvent{}
}
func (h *duplicateTestEventHandler2) Handle(ctx context.Context, event interface{}) error { return nil }
func TestEventProcessor_multiple_same_event_handlers(t *testing.T) {
ts := NewTestServices()
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
eventProcessor, err := cqrs.NewEventProcessorWithConfig(
router,
cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return nil, nil
},
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = eventProcessor.AddHandlers(
&duplicateTestEventHandler1{},
&duplicateTestEventHandler2{},
)
require.NoError(t, err)
}
func TestNewEventProcessor_OnHandle(t *testing.T) {
ts := NewTestServices()
msg1, err := ts.Marshaler.Marshal(&TestEvent{ID: "1"})
require.NoError(t, err)
msg2, err := ts.Marshaler.Marshal(&TestEvent{ID: "2"})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msg1,
msg2,
},
}
handlerCalled := 0
defer func() {
// for msg 1 we are not calling handler - but returning before
assert.Equal(t, 1, handlerCalled)
}()
handler := cqrs.NewEventHandler("test", func(ctx context.Context, cmd *TestEvent) error {
handlerCalled++
return nil
})
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
onHandleCalled := int64(0)
config := cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "events", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return mockSub, nil
},
OnHandle: func(params cqrs.EventProcessorOnHandleParams) error {
atomic.AddInt64(&onHandleCalled, 1)
assert.IsType(t, &TestEvent{}, params.Event)
assert.Equal(t, "cqrs_test.TestEvent", params.EventName)
assert.Equal(t, handler, params.Handler)
if params.Event.(*TestEvent).ID == "1" {
assert.Equal(t, msg1, params.Message)
return errors.New("test error")
} else {
assert.Equal(t, msg2, params.Message)
}
return params.Handler.Handle(params.Message.Context(), params.Event)
},
Marshaler: ts.Marshaler,
Logger: ts.Logger,
}
cp, err := cqrs.NewEventProcessorWithConfig(router, config)
require.NoError(t, err)
err = cp.AddHandlers(handler)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msg1.Nacked():
// ok
case <-msg1.Acked():
// ack received
t.Fatal("ack received, message should be nacked")
}
select {
case <-msg2.Acked():
// ok
case <-msg2.Nacked():
// nack received
}
assert.EqualValues(t, 2, onHandleCalled)
}
type UnknownEvent struct {
}
func TestNewEventProcessor_AckOnUnknownEvent(t *testing.T) {
ts := NewTestServices()
msg, err := ts.Marshaler.Marshal(&UnknownEvent{})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msg,
},
}
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
cp, err := cqrs.NewEventProcessorWithConfig(
router,
cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "events", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return mockSub, nil
},
AckOnUnknownEvent: true,
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = cp.AddHandlers(
cqrs.NewEventHandler("test", func(ctx context.Context, cmd *TestEvent) error {
return nil
}),
)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msg.Acked():
// ok
case <-msg.Nacked():
// ack received
t.Fatal("ack received, message should be nacked")
}
}
func TestNewEventProcessor_AckOnUnknownEvent_disabled(t *testing.T) {
ts := NewTestServices()
msg, err := ts.Marshaler.Marshal(&UnknownEvent{})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msg,
},
}
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
cp, err := cqrs.NewEventProcessorWithConfig(
router,
cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "events", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return mockSub, nil
},
AckOnUnknownEvent: false,
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = cp.AddHandlers(
cqrs.NewEventHandler("test", func(ctx context.Context, cmd *TestEvent) error {
return nil
}),
)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msg.Nacked():
// ok
case <-msg.Acked():
// ack received
t.Fatal("ack received, message should be nacked")
}
}
func TestNewEventProcessor_backward_compatibility_of_AckOnUnknownEvent(t *testing.T) {
ts := NewTestServices()
msg, err := ts.Marshaler.Marshal(&UnknownEvent{})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msg,
},
}
cp, err := cqrs.NewEventProcessor(
[]cqrs.EventHandler{
cqrs.NewEventHandler("test", func(ctx context.Context, cmd *TestEvent) error {
return nil
}),
},
func(eventName string) string {
return "events"
},
func(handlerName string) (message.Subscriber, error) {
return mockSub, nil
},
ts.Marshaler,
ts.Logger,
)
require.NoError(t, err)
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
err = cp.AddHandlersToRouter(router)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msg.Acked():
// ok
case <-msg.Nacked():
// ack received
t.Fatal("ack received, message should be nacked")
}
}
func TestEventProcessor_AddHandlersToRouter_without_disableRouterAutoAddHandlers(t *testing.T) {
ts := NewTestServices()
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
cp, err := cqrs.NewEventProcessorWithConfig(
router,
cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "events", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return ts.EventsPubSub, nil
},
AckOnUnknownEvent: false,
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = cp.AddHandlersToRouter(router)
assert.ErrorContains(t, err, "AddHandlersToRouter should be called only when using deprecated NewEventProcessor")
}
func TestEventProcessor_original_msg_set_to_ctx(t *testing.T) {
ts := NewTestServices()
msg, err := ts.Marshaler.Marshal(&TestEvent{})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msg,
},
}
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
cp, err := cqrs.NewEventProcessorWithConfig(
router,
cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "events", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return mockSub, nil
},
AckOnUnknownEvent: true,
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
var msgFromCtx *message.Message
err = cp.AddHandlers(cqrs.NewEventHandler(
"handler", func(ctx context.Context, cmd *TestEvent) error {
msgFromCtx = cqrs.OriginalMessageFromCtx(ctx)
return nil
}),
)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msg.Acked():
// ok
case <-msg.Nacked():
// nack received
t.Fatal("nack received, message should be acked")
case <-time.After(1 * time.Second):
t.Fatal("timeout waiting for ack")
}
require.NotNil(t, msgFromCtx)
assert.Equal(t, msg, msgFromCtx)
}