523 lines
13 KiB
Go
523 lines
13 KiB
Go
package cqrs_test
|
|
|
|
import (
|
|
"context"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ThreeDotsLabs/watermill"
|
|
"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 TestCommandProcessorConfig_Validate(t *testing.T) {
|
|
testCases := []struct {
|
|
Name string
|
|
ModifyValidConfig func(*cqrs.CommandProcessorConfig)
|
|
ExpectedErr error
|
|
}{
|
|
{
|
|
Name: "valid_config",
|
|
ModifyValidConfig: nil,
|
|
ExpectedErr: nil,
|
|
},
|
|
{
|
|
Name: "missing_Marshaler",
|
|
ModifyValidConfig: func(c *cqrs.CommandProcessorConfig) {
|
|
c.Marshaler = nil
|
|
},
|
|
ExpectedErr: errors.Errorf("missing Marshaler"),
|
|
},
|
|
{
|
|
Name: "missing_SubscriberConstructor",
|
|
ModifyValidConfig: func(c *cqrs.CommandProcessorConfig) {
|
|
c.SubscriberConstructor = nil
|
|
},
|
|
ExpectedErr: errors.Errorf("missing SubscriberConstructor"),
|
|
},
|
|
{
|
|
Name: "missing_GenerateHandlerSubscribeTopic",
|
|
ModifyValidConfig: func(c *cqrs.CommandProcessorConfig) {
|
|
c.GenerateSubscribeTopic = nil
|
|
},
|
|
ExpectedErr: errors.Errorf("missing GenerateSubscribeTopic"),
|
|
},
|
|
}
|
|
for i := range testCases {
|
|
tc := testCases[i]
|
|
|
|
t.Run(tc.Name, func(t *testing.T) {
|
|
validConfig := cqrs.CommandProcessorConfig{
|
|
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
|
|
return "", nil
|
|
},
|
|
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (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 TestNewCommandProcessor(t *testing.T) {
|
|
config := cqrs.CommandProcessorConfig{
|
|
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
|
|
return "", nil
|
|
},
|
|
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
|
|
return nil, nil
|
|
},
|
|
Marshaler: cqrs.JSONMarshaler{},
|
|
}
|
|
require.NoError(t, config.Validate())
|
|
|
|
router, err := message.NewRouter(message.RouterConfig{}, watermill.NewStdLogger(false, false))
|
|
require.NoError(t, err)
|
|
|
|
cp, err := cqrs.NewCommandProcessorWithConfig(router, config)
|
|
assert.NotNil(t, cp)
|
|
assert.NoError(t, err)
|
|
|
|
config.SubscriberConstructor = nil
|
|
require.Error(t, config.Validate())
|
|
|
|
cp, err = cqrs.NewCommandProcessorWithConfig(router, config)
|
|
assert.Nil(t, cp)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
type nonPointerCommandHandler struct {
|
|
}
|
|
|
|
func (nonPointerCommandHandler) HandlerName() string {
|
|
return "nonPointerCommandHandler"
|
|
}
|
|
|
|
func (nonPointerCommandHandler) NewCommand() interface{} {
|
|
return TestCommand{}
|
|
}
|
|
|
|
func (nonPointerCommandHandler) Handle(ctx context.Context, cmd interface{}) error {
|
|
panic("not implemented")
|
|
}
|
|
|
|
func TestCommandProcessor_non_pointer_command(t *testing.T) {
|
|
ts := NewTestServices()
|
|
|
|
handler := nonPointerCommandHandler{}
|
|
|
|
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
|
|
require.NoError(t, err)
|
|
|
|
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(
|
|
router,
|
|
cqrs.CommandProcessorConfig{
|
|
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
|
|
return "", nil
|
|
},
|
|
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
|
|
return nil, nil
|
|
},
|
|
Marshaler: ts.Marshaler,
|
|
Logger: ts.Logger,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
err = commandProcessor.AddHandlers(handler)
|
|
assert.IsType(t, cqrs.NonPointerError{}, errors.Cause(err))
|
|
}
|
|
|
|
// TestCommandProcessor_multiple_same_command_handlers checks, that we don't register multiple handlers for the same command.
|
|
func TestCommandProcessor_multiple_same_command_handlers(t *testing.T) {
|
|
ts := NewTestServices()
|
|
|
|
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
|
|
require.NoError(t, err)
|
|
|
|
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(
|
|
router,
|
|
cqrs.CommandProcessorConfig{
|
|
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
|
|
return "", nil
|
|
},
|
|
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
|
|
return nil, nil
|
|
},
|
|
Marshaler: ts.Marshaler,
|
|
Logger: ts.Logger,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
err = commandProcessor.AddHandlers(
|
|
&CaptureCommandHandler{},
|
|
&CaptureCommandHandler{},
|
|
)
|
|
require.Error(t, err)
|
|
assert.EqualValues(t, cqrs.DuplicateCommandHandlerError{CommandName: "cqrs_test.TestCommand"}, err)
|
|
assert.Equal(t, "command handler for command cqrs_test.TestCommand already exists", err.Error())
|
|
}
|
|
|
|
type mockSubscriber struct {
|
|
MessagesToSend []*message.Message
|
|
WaitForAckBeforeSendingNext bool
|
|
|
|
out chan *message.Message
|
|
}
|
|
|
|
func (m *mockSubscriber) Subscribe(ctx context.Context, topic string) (<-chan *message.Message, error) {
|
|
m.out = make(chan *message.Message)
|
|
|
|
go func() {
|
|
for _, msg := range m.MessagesToSend {
|
|
m.out <- msg
|
|
|
|
if m.WaitForAckBeforeSendingNext {
|
|
<-msg.Acked()
|
|
}
|
|
}
|
|
}()
|
|
|
|
return m.out, nil
|
|
}
|
|
|
|
func (m mockSubscriber) Close() error {
|
|
close(m.out)
|
|
return nil
|
|
}
|
|
|
|
func TestCommandProcessor_AckCommandHandlingErrors_option_true(t *testing.T) {
|
|
logger := watermill.NewCaptureLogger()
|
|
|
|
marshaler := cqrs.JSONMarshaler{}
|
|
|
|
msgToSend, err := marshaler.Marshal(&TestCommand{ID: "1"})
|
|
require.NoError(t, err)
|
|
|
|
mockSub := &mockSubscriber{
|
|
MessagesToSend: []*message.Message{
|
|
msgToSend,
|
|
},
|
|
}
|
|
|
|
router, err := message.NewRouter(message.RouterConfig{}, logger)
|
|
require.NoError(t, err)
|
|
|
|
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(
|
|
router,
|
|
cqrs.CommandProcessorConfig{
|
|
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
|
|
return "commands", nil
|
|
},
|
|
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
|
|
return mockSub, nil
|
|
},
|
|
Marshaler: marshaler,
|
|
Logger: logger,
|
|
AckCommandHandlingErrors: true,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
expectedErr := errors.New("test error")
|
|
|
|
err = commandProcessor.AddHandlers(cqrs.NewCommandHandler(
|
|
"handler", func(ctx context.Context, cmd *TestCommand) error {
|
|
return expectedErr
|
|
}),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
go func() {
|
|
err := router.Run(context.Background())
|
|
assert.NoError(t, err)
|
|
}()
|
|
|
|
<-router.Running()
|
|
|
|
select {
|
|
case <-msgToSend.Acked():
|
|
// ok
|
|
case <-msgToSend.Nacked():
|
|
// nack received
|
|
t.Fatal("nack received, message should be acked")
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatal("timeout waiting for ack")
|
|
}
|
|
|
|
// it's pretty important to not ack message silently, so let's assert if it's logged properly
|
|
expectedLogMessage := watermill.CapturedMessage{
|
|
Level: watermill.ErrorLogLevel,
|
|
Fields: map[string]any{
|
|
"command_handler_name": "handler",
|
|
"topic": "commands",
|
|
},
|
|
Msg: "Error when handling command, acking (AckCommandHandlingErrors is enabled)",
|
|
Err: expectedErr,
|
|
}
|
|
assert.True(
|
|
t,
|
|
logger.Has(expectedLogMessage),
|
|
"expected log message not found, logs: %#v",
|
|
logger.Captured(),
|
|
)
|
|
}
|
|
|
|
func TestCommandProcessor_AckCommandHandlingErrors_option_false(t *testing.T) {
|
|
logger := watermill.NewCaptureLogger()
|
|
|
|
marshaler := cqrs.JSONMarshaler{}
|
|
|
|
msgToSend, err := marshaler.Marshal(&TestCommand{ID: "1"})
|
|
require.NoError(t, err)
|
|
|
|
mockSub := &mockSubscriber{
|
|
MessagesToSend: []*message.Message{
|
|
msgToSend,
|
|
},
|
|
}
|
|
|
|
router, err := message.NewRouter(message.RouterConfig{}, logger)
|
|
require.NoError(t, err)
|
|
|
|
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(
|
|
router,
|
|
cqrs.CommandProcessorConfig{
|
|
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
|
|
return "commands", nil
|
|
},
|
|
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
|
|
return mockSub, nil
|
|
},
|
|
Marshaler: marshaler,
|
|
Logger: logger,
|
|
AckCommandHandlingErrors: false,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
expectedErr := errors.New("test error")
|
|
|
|
err = commandProcessor.AddHandlers(cqrs.NewCommandHandler(
|
|
"handler", func(ctx context.Context, cmd *TestCommand) error {
|
|
return expectedErr
|
|
}),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
go func() {
|
|
err := router.Run(context.Background())
|
|
assert.NoError(t, err)
|
|
}()
|
|
|
|
<-router.Running()
|
|
|
|
select {
|
|
case <-msgToSend.Acked():
|
|
// nack received
|
|
t.Fatal("ack received, message should be nacked")
|
|
case <-msgToSend.Nacked():
|
|
// ok
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatal("timeout waiting for ack")
|
|
}
|
|
}
|
|
|
|
func TestNewCommandProcessor_OnHandle(t *testing.T) {
|
|
ts := NewTestServices()
|
|
|
|
msg1, err := ts.Marshaler.Marshal(&TestCommand{ID: "1"})
|
|
require.NoError(t, err)
|
|
|
|
msg2, err := ts.Marshaler.Marshal(&TestCommand{ID: "2"})
|
|
require.NoError(t, err)
|
|
|
|
mockSub := &mockSubscriber{
|
|
MessagesToSend: []*message.Message{
|
|
msg1,
|
|
msg2,
|
|
},
|
|
}
|
|
|
|
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
|
|
require.NoError(t, err)
|
|
|
|
handlerCalled := 0
|
|
|
|
defer func() {
|
|
// for msg 1 we are not calling handler - but returning before
|
|
assert.Equal(t, 1, handlerCalled)
|
|
}()
|
|
|
|
handler := cqrs.NewCommandHandler("test", func(ctx context.Context, cmd *TestCommand) error {
|
|
handlerCalled++
|
|
return nil
|
|
})
|
|
|
|
onHandleCalled := int64(0)
|
|
|
|
config := cqrs.CommandProcessorConfig{
|
|
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
|
|
return "commands", nil
|
|
},
|
|
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
|
|
return mockSub, nil
|
|
},
|
|
OnHandle: func(params cqrs.CommandProcessorOnHandleParams) error {
|
|
atomic.AddInt64(&onHandleCalled, 1)
|
|
|
|
assert.IsType(t, &TestCommand{}, params.Command)
|
|
assert.Equal(t, "cqrs_test.TestCommand", params.CommandName)
|
|
assert.Equal(t, handler, params.Handler)
|
|
|
|
if params.Command.(*TestCommand).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.Command)
|
|
},
|
|
Marshaler: ts.Marshaler,
|
|
Logger: ts.Logger,
|
|
}
|
|
cp, err := cqrs.NewCommandProcessorWithConfig(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)
|
|
}
|
|
|
|
func TestCommandProcessor_AddHandlersToRouter_without_disableRouterAutoAddHandlers(t *testing.T) {
|
|
ts := NewTestServices()
|
|
|
|
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
|
|
require.NoError(t, err)
|
|
|
|
cp, err := cqrs.NewCommandProcessorWithConfig(
|
|
router,
|
|
cqrs.CommandProcessorConfig{
|
|
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
|
|
return "commands", nil
|
|
},
|
|
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
|
|
return ts.CommandsPubSub, nil
|
|
},
|
|
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 NewCommandProcessor")
|
|
}
|
|
|
|
func TestCommandProcessor_original_msg_set_to_ctx(t *testing.T) {
|
|
logger := watermill.NewCaptureLogger()
|
|
|
|
marshaler := cqrs.JSONMarshaler{}
|
|
|
|
msgToSend, err := marshaler.Marshal(&TestCommand{ID: "1"})
|
|
require.NoError(t, err)
|
|
|
|
mockSub := &mockSubscriber{
|
|
MessagesToSend: []*message.Message{
|
|
msgToSend,
|
|
},
|
|
}
|
|
|
|
router, err := message.NewRouter(message.RouterConfig{}, logger)
|
|
require.NoError(t, err)
|
|
|
|
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(
|
|
router,
|
|
cqrs.CommandProcessorConfig{
|
|
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
|
|
return "commands", nil
|
|
},
|
|
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
|
|
return mockSub, nil
|
|
},
|
|
Marshaler: marshaler,
|
|
Logger: logger,
|
|
AckCommandHandlingErrors: true,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
var msgFromCtx *message.Message
|
|
|
|
err = commandProcessor.AddHandlers(cqrs.NewCommandHandler(
|
|
"handler", func(ctx context.Context, cmd *TestCommand) 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 <-msgToSend.Acked():
|
|
// ok
|
|
case <-msgToSend.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, msgToSend, msgFromCtx)
|
|
}
|