Skip to content

Commit

Permalink
Merge pull request #367 from maxekman/345-nil-checks
Browse files Browse the repository at this point in the history
Fix / Add nil checks for commands/events
  • Loading branch information
maxekman committed Nov 29, 2021
2 parents bc7e608 + 3d00387 commit b6067b2
Show file tree
Hide file tree
Showing 11 changed files with 104 additions and 4 deletions.
16 changes: 16 additions & 0 deletions command_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,20 @@
package eventhorizon

import (
"errors"
"reflect"
"time"

"github.com/looplab/eventhorizon/uuid"
)

var (
// ErrMissingCommand is when there is no command to be handled.
ErrMissingCommand = errors.New("missing command")
// ErrMissingAggregateID is when a command is missing an aggregate ID.
ErrMissingAggregateID = errors.New("missing aggregate ID")
)

// IsZeroer is used to check if a type is zero-valued, and in that case
// is not allowed to be used in a command. See CheckCommand.
type IsZeroer interface {
Expand All @@ -39,6 +47,14 @@ func (c *CommandFieldError) Error() string {

// CheckCommand checks a command for errors.
func CheckCommand(cmd Command) error {
if cmd == nil {
return ErrMissingCommand
}

if cmd.AggregateID() == uuid.Nil {
return ErrMissingAggregateID
}

rv := reflect.Indirect(reflect.ValueOf(cmd))
rt := rv.Type()

Expand Down
13 changes: 13 additions & 0 deletions command_check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package eventhorizon

import (
"errors"
"testing"
"time"

Expand All @@ -28,6 +29,18 @@ func TestCheckCommand(t *testing.T) {
t.Error("there should be no error:", err)
}

// Missing command.
err = CheckCommand(nil)
if !errors.Is(err, ErrMissingCommand) {
t.Error("there should be a missing command error:", err)
}

// Missing Aggregate ID.
err = CheckCommand(&TestCommandUUIDValue{})
if !errors.Is(err, ErrMissingAggregateID) {
t.Error("there should be a missing aggregate ID error:", err)
}

// Missing required UUID value.
err = CheckCommand(&TestCommandUUIDValue{TestID: uuid.New()})
if err == nil || err.Error() != "missing field: Content" {
Expand Down
3 changes: 1 addition & 2 deletions commandhandler/aggregate/commandhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ func NewCommandHandler(t eh.AggregateType, store eh.AggregateStore) (*CommandHan
// HandleCommand handles a command with the registered aggregate.
// Returns ErrAggregateNotFound if no aggregate could be found.
func (h *CommandHandler) HandleCommand(ctx context.Context, cmd eh.Command) error {
err := eh.CheckCommand(cmd)
if err != nil {
if err := eh.CheckCommand(cmd); err != nil {
return err
}

Expand Down
4 changes: 4 additions & 0 deletions commandhandler/bus/commandhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ func NewCommandHandler() *CommandHandler {

// HandleCommand handles a command with a handler capable of handling it.
func (h *CommandHandler) HandleCommand(ctx context.Context, cmd eh.Command) error {
if err := eh.CheckCommand(cmd); err != nil {
return err
}

h.handlersMu.RLock()
defer h.handlersMu.RUnlock()

Expand Down
4 changes: 4 additions & 0 deletions eventhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ package eventhorizon

import (
"context"
"errors"
"reflect"
"runtime"
"strings"
)

// ErrMissingEvent is when there is no event to be handled.
var ErrMissingEvent = errors.New("missing event")

// EventHandlerType is the type of an event handler, used as its unique identifier.
type EventHandlerType string

Expand Down
7 changes: 7 additions & 0 deletions eventhandler/projector/eventhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,13 @@ func (h *EventHandler) HandlerType() eh.EventHandlerType {
// It will try to find the correct version of the model, waiting for it the projector
// has the WithWait option set.
func (h *EventHandler) HandleEvent(ctx context.Context, event eh.Event) error {
if event == nil {
return &Error{
Err: eh.ErrMissingEvent,
Projector: h.projector.ProjectorType().String(),
}
}

// Used to retry once in case of a version mismatch.
triedOnce := false
retryOnce:
Expand Down
18 changes: 18 additions & 0 deletions eventhandler/projector/eventhandler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,24 @@ func TestEventHandler_UpdateModelAfterDelete(t *testing.T) {
}
}

func TestEventHandler_MissingEventError(t *testing.T) {
repo := &mocks.Repo{}
projector := &TestProjector{}
handler := NewEventHandler(projector, repo)
handler.SetEntityFactory(func() eh.Entity {
return &mocks.SimpleModel{}
})

ctx := context.Background()

err := handler.HandleEvent(ctx, nil)

projectError := &Error{}
if !errors.As(err, &projectError) || !errors.Is(err, eh.ErrMissingEvent) {
t.Error("there should be an error:", err)
}
}

func TestEventHandler_LoadError(t *testing.T) {
repo := &mocks.Repo{}
projector := &TestProjector{}
Expand Down
7 changes: 7 additions & 0 deletions eventhandler/saga/eventhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ func (h *EventHandler) HandlerType() eh.EventHandlerType {

// HandleEvent implements the HandleEvent method of the eventhorizon.EventHandler interface.
func (h *EventHandler) HandleEvent(ctx context.Context, event eh.Event) error {
if event == nil {
return &Error{
Err: eh.ErrMissingEvent,
Saga: h.saga.SagaType().String(),
}
}

// Run the saga which can issue commands on the provided command handler.
if err := h.saga.RunSaga(ctx, event, h.commandHandler); err != nil {
return &Error{
Expand Down
18 changes: 18 additions & 0 deletions eventhandler/saga/eventhandler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package saga

import (
"context"
"errors"
"reflect"
"testing"
"time"
Expand Down Expand Up @@ -52,6 +53,23 @@ func TestEventHandler(t *testing.T) {
}
}

func TestEventHandler_MissingEventError(t *testing.T) {
commandHandler := &mocks.CommandHandler{
Commands: []eh.Command{},
}
saga := &TestSaga{}
handler := NewEventHandler(saga, commandHandler)

ctx := context.Background()

err := handler.HandleEvent(ctx, nil)

projectError := &Error{}
if !errors.As(err, &projectError) || !errors.Is(err, eh.ErrMissingEvent) {
t.Error("there should be an error:", err)
}
}

const (
TestSagaType Type = "TestSaga"
)
Expand Down
4 changes: 4 additions & 0 deletions eventhandler/waiter/eventhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ func (h *EventHandler) HandlerType() eh.EventHandlerType {
// HandleEvent implements the HandleEvent method of the eventhorizon.EventHandler interface.
// It forwards events to the waiters so that they can match the events.
func (h *EventHandler) HandleEvent(ctx context.Context, event eh.Event) error {
if event == nil {
return eh.ErrMissingEvent
}

h.inbox <- event

return nil
Expand Down
14 changes: 12 additions & 2 deletions eventhandler/waiter/eventhandler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,21 @@ import (
func TestEventHandler(t *testing.T) {
h := NewEventHandler()

if err := h.HandleEvent(context.Background(), nil); !errors.Is(err, eh.ErrMissingEvent) {
t.Error("there should be a missing event error:", err)
}

// Event should match when waiting.
timestamp := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
expectedEvent := eh.NewEvent(mocks.EventType, nil, timestamp,
eh.ForAggregate(mocks.AggregateType, uuid.New(), 1))

go func() {
time.Sleep(time.Millisecond)
h.HandleEvent(context.Background(), expectedEvent)

if err := h.HandleEvent(context.Background(), expectedEvent); err != nil {
t.Error("there should be no error:", err)
}
}()

l := h.Listen(func(event eh.Event) bool {
Expand Down Expand Up @@ -67,7 +74,10 @@ func TestEventHandler(t *testing.T) {

go func() {
time.Sleep(time.Millisecond)
h.HandleEvent(context.Background(), otherEvent)

if err := h.HandleEvent(context.Background(), otherEvent); err != nil {
t.Error("there should be no error:", err)
}
}()

ctx, cancel = context.WithTimeout(context.Background(), 10*time.Millisecond)
Expand Down

0 comments on commit b6067b2

Please sign in to comment.