func TestCommandBus(t *testing.T) { bus := NewCommandBus() if bus == nil { t.Fatal("there should be a bus") } t.Log("handle with no handler") command1 := &testutil.TestCommand{eh.NewUUID(), "command1"} err := bus.HandleCommand(command1) if err != eh.ErrHandlerNotFound { t.Error("there should be a ErrHandlerNotFound error:", err) } t.Log("set handler") handler := &TestCommandHandler{} err = bus.SetHandler(handler, testutil.TestCommandType) if err != nil { t.Error("there should be no error:", err) } t.Log("handle with handler") err = bus.HandleCommand(command1) if err != nil { t.Error("there should be no error:", err) } if handler.command != command1 { t.Error("the handled command should be correct:", handler.command) } err = bus.SetHandler(handler, testutil.TestCommandType) if err != eh.ErrHandlerAlreadySet { t.Error("there should be a ErrHandlerAlreadySet error:", err) } }
func TestEventStore(t *testing.T) { config := &EventStoreConfig{ Table: "eventhorizonTest-" + eh.NewUUID().String(), Region: "eu-west-1", } store, err := NewEventStore(config) if err != nil { t.Fatal("there should be no error:", err) } if store == nil { t.Fatal("there should be a store") } t.Log("creating table:", config.Table) if err := store.CreateTable(); err != nil { t.Fatal("could not create table:", err) } defer func() { t.Log("deleting table:", store.config.Table) if err := store.DeleteTable(); err != nil { t.Fatal("could not delete table: ", err) } }() // Run the actual test suite. testutil.EventStoreCommonTests(t, store) }
func TestEventBus(t *testing.T) { bus := NewEventBus() if bus == nil { t.Fatal("there should be a bus") } observer := testutil.NewMockEventObserver() bus.AddObserver(observer) t.Log("publish event without handler") event1 := &testutil.TestEvent{eh.NewUUID(), "event1"} bus.PublishEvent(event1) if !reflect.DeepEqual(observer.Events, []eh.Event{event1}) { t.Error("the observed events should be correct:", observer.Events) } t.Log("publish event") handler := testutil.NewMockEventHandler("testHandler") bus.AddHandler(handler, testutil.TestEventType) bus.PublishEvent(event1) if !reflect.DeepEqual(handler.Events, []eh.Event{event1}) { t.Error("the handler events should be correct:", handler.Events) } if !reflect.DeepEqual(observer.Events, []eh.Event{event1, event1}) { t.Error("the observed events should be correct:", observer.Events) } t.Log("publish another event") bus.AddHandler(handler, testutil.TestEventOtherType) event2 := &testutil.TestEventOther{eh.NewUUID(), "event2"} bus.PublishEvent(event2) if !reflect.DeepEqual(handler.Events, []eh.Event{event1, event2}) { t.Error("the handler events should be correct:", handler.Events) } if !reflect.DeepEqual(observer.Events, []eh.Event{event1, event1, event2}) { t.Error("the observed events should be correct:", observer.Events) } }
func TestReadRepository(t *testing.T) { // Support Wercker testing with MongoDB. host := os.Getenv("MONGO_PORT_27017_TCP_ADDR") port := os.Getenv("MONGO_PORT_27017_TCP_PORT") url := "localhost" if host != "" && port != "" { url = host + ":" + port } repo, err := NewReadRepository(url, "test", "testutil.TestModel") if err != nil { t.Error("there should be no error:", err) } if repo == nil { t.Error("there should be a repository") } repo.SetModel(func() interface{} { return &testutil.TestModel{} }) defer repo.Close() defer func() { t.Log("clearing collection") if err = repo.Clear(); err != nil { t.Fatal("there should be no error:", err) } }() // TODO: Share these tests between implementations. t.Log("FindAll with no items") result, err := repo.FindAll() if err != nil { t.Error("there should be no error:", err) } if len(result) != 0 { t.Error("there should be no items:", len(result)) } t.Log("Save one item") model1 := &testutil.TestModel{eh.NewUUID(), "model1", time.Now().Round(time.Millisecond)} if err = repo.Save(model1.ID, model1); err != nil { t.Error("there should be no error:", err) } model, err := repo.Find(model1.ID) if err != nil { t.Error("there should be no error:", err) } if !reflect.DeepEqual(model, model1) { t.Error("the item should be correct:", model) } t.Log("Save and overwrite with same ID") model1Alt := &testutil.TestModel{model1.ID, "model1Alt", time.Now().Round(time.Millisecond)} if err = repo.Save(model1Alt.ID, model1Alt); err != nil { t.Error("there should be no error:", err) } model, err = repo.Find(model1Alt.ID) if err != nil { t.Error("there should be no error:", err) } if !reflect.DeepEqual(model, model1Alt) { t.Error("the item should be correct:", model) } t.Log("FindAll with one item") result, err = repo.FindAll() if err != nil { t.Error("there should be no error:", err) } if len(result) != 1 { t.Error("there should be one item:", len(result)) } if !reflect.DeepEqual(result[0], model1Alt) { t.Error("the item should be correct:", model) } t.Log("Save with another ID") model2 := &testutil.TestModel{eh.NewUUID(), "model2", time.Now().Round(time.Millisecond)} if err = repo.Save(model2.ID, model2); err != nil { t.Error("there should be no error:", err) } model, err = repo.Find(model2.ID) if err != nil { t.Error("there should be no error:", err) } if !reflect.DeepEqual(model, model2) { t.Error("the item should be correct:", model) } t.Log("FindAll with two items") result, err = repo.FindAll() if err != nil { t.Error("there should be no error:", err) } if len(result) != 2 { t.Error("there should be two items:", len(result)) } if (!reflect.DeepEqual(result[0], model1Alt) || !reflect.DeepEqual(result[1], model2)) && (!reflect.DeepEqual(result[0], model2) || !reflect.DeepEqual(result[1], model1Alt)) { t.Error("the items should be correct:", result) } t.Log("FindCustom by content") result, err = repo.FindCustom(func(c *mgo.Collection) *mgo.Query { return c.Find(bson.M{"content": "model1Alt"}) }) if len(result) != 1 { t.Error("there should be one item:", len(result)) } if !reflect.DeepEqual(result[0], model1Alt) { t.Error("the item should be correct:", model) } t.Log("FindCustom with no query") result, err = repo.FindCustom(func(c *mgo.Collection) *mgo.Query { return nil }) if err == nil || err != ErrInvalidQuery { t.Error("there should be a invalid query error:", err) } count := 0 t.Log("FindCustom with query execution in the callback") _, err = repo.FindCustom(func(c *mgo.Collection) *mgo.Query { if count, err = c.Count(); err != nil { t.Error("there should be no error:", err) } // Be sure to return nil to not execute the query again in FindCustom. return nil }) if err == nil || err != ErrInvalidQuery { t.Error("there should be a invalid query error:", err) } if count != 2 { t.Error("the count should be correct:", count) } t.Log("Remove one item") err = repo.Remove(model1Alt.ID) if err != nil { t.Error("there should be no error:", err) } result, err = repo.FindAll() if err != nil { t.Error("there should be no error:", err) } if len(result) != 1 { t.Error("there should be one item:", len(result)) } if !reflect.DeepEqual(result[0], model2) { t.Error("the item should be correct:", result[0]) } t.Log("Remove non-existing item") err = repo.Remove(model1Alt.ID) if err != eh.ErrModelNotFound { t.Error("there should be a ErrModelNotFound error:", err) } }
func EventStoreCommonTests(t *testing.T, store eh.EventStore) []eh.Event { savedEvents := []eh.Event{} t.Log("save no events") err := store.Save([]eh.Event{}, 0) if err != eh.ErrNoEventsToAppend { t.Error("there shoud be a ErrNoEventsToAppend error:", err) } t.Log("save event, version 1") id, _ := eh.ParseUUID("c1138e5f-f6fb-4dd0-8e79-255c6c8d3756") event1 := &TestEvent{id, "event1"} err = store.Save([]eh.Event{event1}, 0) if err != nil { t.Error("there should be no error:", err) } savedEvents = append(savedEvents, event1) t.Log("save event, version 2") err = store.Save([]eh.Event{event1}, 1) if err != nil { t.Error("there should be no error:", err) } savedEvents = append(savedEvents, event1) t.Log("save event, version 3") event2 := &TestEvent{id, "event2"} err = store.Save([]eh.Event{event2}, 2) if err != nil { t.Error("there should be no error:", err) } savedEvents = append(savedEvents, event2) t.Log("save multiple events, version 4, 5 and 6") err = store.Save([]eh.Event{event1, event2, event1}, 3) if err != nil { t.Error("there should be no error:", err) } savedEvents = append(savedEvents, event1, event2, event1) t.Log("save event for another aggregate") id2, _ := eh.ParseUUID("c1138e5e-f6fb-4dd0-8e79-255c6c8d3756") event3 := &TestEvent{id2, "event3"} err = store.Save([]eh.Event{event3}, 0) if err != nil { t.Error("there should be no error:", err) } savedEvents = append(savedEvents, event3) t.Log("load events for non-existing aggregate") events, err := store.Load(eh.NewUUID()) if err != nil { t.Error("there should be no error:", err) } if len(events) != 0 { t.Error("there should be no loaded events:", eventsToString(events)) } t.Log("load events") events, err = store.Load(id) if err != nil { t.Error("there should be no error:", err) } if !reflect.DeepEqual(events, []eh.Event{ event1, // Version 1 event1, // Version 2 event2, // Version 3 event1, event2, event1, // Version 4, 5 and 6 }) { t.Error("the loaded events should be correct:", eventsToString(events)) } t.Log("load events for another aggregate") events, err = store.Load(id2) if err != nil { t.Error("there should be no error:", err) } if !reflect.DeepEqual(events, []eh.Event{event3}) { t.Error("the loaded events should be correct:", eventsToString(events)) } return savedEvents }
func Example() { // Support Wercker testing with MongoDB. host := os.Getenv("MONGO_PORT_27017_TCP_ADDR") port := os.Getenv("MONGO_PORT_27017_TCP_PORT") url := "localhost" if host != "" && port != "" { url = host + ":" + port } // Create the event store. eventStore, err := eventstore.NewEventStore(url, "demo") if err != nil { log.Fatalf("could not create event store: %s", err) } // Create the event bus that distributes events. eventBus := eventbus.NewEventBus() eventBus.AddObserver(&domain.Logger{}) // Create the aggregate repository. repository, err := eh.NewEventSourcingRepository(eventStore, eventBus) if err != nil { log.Fatalf("could not create repository: %s", err) } // Create the aggregate command handler. handler, err := eh.NewAggregateCommandHandler(repository) if err != nil { log.Fatalf("could not create command handler: %s", err) } // Register the domain aggregates with the dispather. Remember to check for // errors here in a real app! handler.SetAggregate(domain.InvitationAggregateType, domain.CreateInviteCommand) handler.SetAggregate(domain.InvitationAggregateType, domain.AcceptInviteCommand) handler.SetAggregate(domain.InvitationAggregateType, domain.DeclineInviteCommand) // Create the command bus and register the handler for the commands. commandBus := commandbus.NewCommandBus() commandBus.SetHandler(handler, domain.CreateInviteCommand) commandBus.SetHandler(handler, domain.AcceptInviteCommand) commandBus.SetHandler(handler, domain.DeclineInviteCommand) // Create and register a read model for individual invitations. invitationRepository, err := readrepository.NewReadRepository(url, "demo", "invitations") if err != nil { log.Fatalf("could not create invitation repository: %s", err) } invitationRepository.SetModel(func() interface{} { return &domain.Invitation{} }) invitationProjector := domain.NewInvitationProjector(invitationRepository) eventBus.AddHandler(invitationProjector, domain.InviteCreatedEvent) eventBus.AddHandler(invitationProjector, domain.InviteAcceptedEvent) eventBus.AddHandler(invitationProjector, domain.InviteDeclinedEvent) // Create and register a read model for a guest list. eventID := eh.NewUUID() guestListRepository, err := readrepository.NewReadRepository(url, "demo", "guest_lists") if err != nil { log.Fatalf("could not create guest list repository: %s", err) } guestListRepository.SetModel(func() interface{} { return &domain.GuestList{} }) guestListProjector := domain.NewGuestListProjector(guestListRepository, eventID) eventBus.AddHandler(guestListProjector, domain.InviteCreatedEvent) eventBus.AddHandler(guestListProjector, domain.InviteAcceptedEvent) eventBus.AddHandler(guestListProjector, domain.InviteDeclinedEvent) // Clear DB collections. eventStore.Clear() invitationRepository.Clear() guestListRepository.Clear() // Issue some invitations and responses. // Note that Athena tries to decline the event, but that is not allowed // by the domain logic in InvitationAggregate. The result is that she is // still accepted. athenaID := eh.NewUUID() commandBus.HandleCommand(&domain.CreateInvite{InvitationID: athenaID, Name: "Athena", Age: 42}) commandBus.HandleCommand(&domain.AcceptInvite{InvitationID: athenaID}) err = commandBus.HandleCommand(&domain.DeclineInvite{InvitationID: athenaID}) if err != nil { log.Printf("error: %s\n", err) } hadesID := eh.NewUUID() commandBus.HandleCommand(&domain.CreateInvite{InvitationID: hadesID, Name: "Hades"}) commandBus.HandleCommand(&domain.AcceptInvite{InvitationID: hadesID}) zeusID := eh.NewUUID() commandBus.HandleCommand(&domain.CreateInvite{InvitationID: zeusID, Name: "Zeus"}) commandBus.HandleCommand(&domain.DeclineInvite{InvitationID: zeusID}) // Read all invites. invitations, _ := invitationRepository.FindAll() for _, i := range invitations { if i, ok := i.(*domain.Invitation); ok { log.Printf("invitation: %s - %s\n", i.Name, i.Status) fmt.Printf("invitation: %s - %s\n", i.Name, i.Status) } } // Read the guest list. l, _ := guestListRepository.Find(eventID) if l, ok := l.(*domain.GuestList); ok { log.Printf("guest list: %d guests (%d accepted, %d declined)\n", l.NumGuests, l.NumAccepted, l.NumDeclined) fmt.Printf("guest list: %d guests (%d accepted, %d declined)\n", l.NumGuests, l.NumAccepted, l.NumDeclined) } // records := eventStore.FindAllEventRecords() // fmt.Printf("event records:\n") // for _, r := range records { // fmt.Printf("%#v\n", r) // } // Output: // invitation: Athena - accepted // invitation: Hades - accepted // invitation: Zeus - declined // guest list: 3 guests (2 accepted, 1 declined) }
func TestReadRepository(t *testing.T) { repo := NewReadRepository() if repo == nil { t.Error("there should be a repository") } // TODO: Share these tests between implementations. t.Log("FindAll with no items") result, err := repo.FindAll() if err != nil { t.Error("there should be no error:", err) } if len(result) != 0 { t.Error("there should be no items:", len(result)) } t.Log("Save one item") model1 := &testutil.TestModel{eh.NewUUID(), "model1", time.Now().Round(time.Millisecond)} if err = repo.Save(model1.ID, model1); err != nil { t.Error("there should be no error:", err) } model, err := repo.Find(model1.ID) if err != nil { t.Error("there should be no error:", err) } if !reflect.DeepEqual(model, model1) { t.Error("the item should be correct:", model) } t.Log("Save and overwrite with same ID") model1Alt := &testutil.TestModel{model1.ID, "model1Alt", time.Now().Round(time.Millisecond)} if err = repo.Save(model1Alt.ID, model1Alt); err != nil { t.Error("there should be no error:", err) } model, err = repo.Find(model1Alt.ID) if err != nil { t.Error("there should be no error:", err) } if !reflect.DeepEqual(model, model1Alt) { t.Error("the item should be correct:", model) } t.Log("FindAll with one item") result, err = repo.FindAll() if err != nil { t.Error("there should be no error:", err) } if len(result) != 1 { t.Error("there should be one item:", len(result)) } if !reflect.DeepEqual(result[0], model1Alt) { t.Error("the item should be correct:", model) } t.Log("Save with another ID") model2 := &testutil.TestModel{eh.NewUUID(), "model2", time.Now().Round(time.Millisecond)} if err = repo.Save(model2.ID, model2); err != nil { t.Error("there should be no error:", err) } model, err = repo.Find(model2.ID) if err != nil { t.Error("there should be no error:", err) } if !reflect.DeepEqual(model, model2) { t.Error("the item should be correct:", model) } t.Log("FindAll with two items") result, err = repo.FindAll() if err != nil { t.Error("there should be no error:", err) } if len(result) != 2 { t.Error("there should be two items:", len(result)) } if (!reflect.DeepEqual(result[0], model1Alt) || !reflect.DeepEqual(result[1], model2)) && (!reflect.DeepEqual(result[0], model2) || !reflect.DeepEqual(result[1], model1Alt)) { t.Error("the items should be correct:", result) } t.Log("Remove one item") err = repo.Remove(model1Alt.ID) if err != nil { t.Error("there should be no error:", err) } result, err = repo.FindAll() if err != nil { t.Error("there should be no error:", err) } if len(result) != 1 { t.Error("there should be one item:", len(result)) } if !reflect.DeepEqual(result[0], model2) { t.Error("the item should be correct:", result[0]) } t.Log("Remove non-existing item") err = repo.Remove(model1Alt.ID) if err != eh.ErrModelNotFound { t.Error("there should be a ErrModelNotFound error:", err) } }
func main() { // Create the event bus that distributes events. eventBus := eventhorizon.NewInternalEventBus() eventBus.AddGlobalHandler(&LoggerSubscriber{}) // Create the event store. eventStore, err := eventhorizon.NewMongoEventStore(eventBus, "localhost", "demo") if err != nil { log.Fatalf("could not create event store: %s", err) } eventStore.RegisterEventType(&InviteCreated{}, func() eventhorizon.Event { return &InviteCreated{} }) eventStore.RegisterEventType(&InviteAccepted{}, func() eventhorizon.Event { return &InviteAccepted{} }) eventStore.RegisterEventType(&InviteDeclined{}, func() eventhorizon.Event { return &InviteDeclined{} }) // Create the aggregate repository. repository, err := eventhorizon.NewCallbackRepository(eventStore) if err != nil { log.Fatalf("could not create repository: %s", err) } // Register an aggregate factory. repository.RegisterAggregate(&InvitationAggregate{}, func(id eventhorizon.UUID) eventhorizon.Aggregate { return &InvitationAggregate{ AggregateBase: eventhorizon.NewAggregateBase(id), } }, ) // Create the aggregate command handler. handler, err := eventhorizon.NewAggregateCommandHandler(repository) if err != nil { log.Fatalf("could not create command handler: %s", err) } // Register the domain aggregates with the dispather. Remember to check for // errors here in a real app! handler.SetAggregate(&InvitationAggregate{}, &CreateInvite{}) handler.SetAggregate(&InvitationAggregate{}, &AcceptInvite{}) handler.SetAggregate(&InvitationAggregate{}, &DeclineInvite{}) // Create the command bus and register the handler for the commands. commandBus := eventhorizon.NewInternalCommandBus() commandBus.SetHandler(handler, &CreateInvite{}) commandBus.SetHandler(handler, &AcceptInvite{}) commandBus.SetHandler(handler, &DeclineInvite{}) // Create and register a read model for individual invitations. invitationRepository, err := eventhorizon.NewMongoReadRepository("localhost", "demo", "invitations") if err != nil { log.Fatalf("could not create invitation repository: %s", err) } invitationRepository.SetModel(func() interface{} { return &Invitation{} }) invitationProjector := NewInvitationProjector(invitationRepository) eventBus.AddHandler(invitationProjector, &InviteCreated{}) eventBus.AddHandler(invitationProjector, &InviteAccepted{}) eventBus.AddHandler(invitationProjector, &InviteDeclined{}) // Create and register a read model for a guest list. eventID := eventhorizon.NewUUID() guestListRepository, err := eventhorizon.NewMongoReadRepository("localhost", "demo", "guest_lists") if err != nil { log.Fatalf("could not create guest list repository: %s", err) } guestListRepository.SetModel(func() interface{} { return &GuestList{} }) guestListProjector := NewGuestListProjector(guestListRepository, eventID) eventBus.AddHandler(guestListProjector, &InviteCreated{}) eventBus.AddHandler(guestListProjector, &InviteAccepted{}) eventBus.AddHandler(guestListProjector, &InviteDeclined{}) // Clear DB collections. eventStore.Clear() invitationRepository.Clear() guestListRepository.Clear() // Issue some invitations and responses. // Note that Athena tries to decline the event, but that is not allowed // by the domain logic in InvitationAggregate. The result is that she is // still accepted. athenaID := eventhorizon.NewUUID() commandBus.HandleCommand(&CreateInvite{InvitationID: athenaID, Name: "Athena", Age: 42}) commandBus.HandleCommand(&AcceptInvite{InvitationID: athenaID}) err = commandBus.HandleCommand(&DeclineInvite{InvitationID: athenaID}) if err != nil { fmt.Printf("error: %s\n", err) } hadesID := eventhorizon.NewUUID() commandBus.HandleCommand(&CreateInvite{InvitationID: hadesID, Name: "Hades"}) commandBus.HandleCommand(&AcceptInvite{InvitationID: hadesID}) zeusID := eventhorizon.NewUUID() commandBus.HandleCommand(&CreateInvite{InvitationID: zeusID, Name: "Zeus"}) commandBus.HandleCommand(&DeclineInvite{InvitationID: zeusID}) // Read all invites. invitations, _ := invitationRepository.FindAll() for _, i := range invitations { fmt.Printf("invitation: %#v\n", i) } // Read the guest list. guestList, _ := guestListRepository.Find(eventID) fmt.Printf("guest list: %#v\n", guestList) // records := eventStore.FindAllEventRecords() // fmt.Printf("event records:\n") // for _, r := range records { // fmt.Printf("%#v\n", r) // } }
func TestEventBus(t *testing.T) { // Support Wercker testing with MongoDB. host := os.Getenv("REDIS_PORT_6379_TCP_ADDR") port := os.Getenv("REDIS_PORT_6379_TCP_PORT") url := ":6379" if host != "" && port != "" { url = host + ":" + port } bus, err := NewEventBus("test", url, "") if err != nil { t.Fatal("there should be no error:", err) } if bus == nil { t.Fatal("there should be a bus") } defer bus.Close() observer := testutil.NewMockEventObserver() bus.AddObserver(observer) // Another bus to test the observer. bus2, err := NewEventBus("test", url, "") if err != nil { t.Fatal("there should be no error:", err) } defer bus2.Close() observer2 := testutil.NewMockEventObserver() bus2.AddObserver(observer2) t.Log("publish event without handler") event1 := &testutil.TestEvent{eh.NewUUID(), "event1"} bus.PublishEvent(event1) <-observer.Recv if !reflect.DeepEqual(observer.Events, []eh.Event{event1}) { t.Error("the observed events should be correct:", observer.Events) } <-observer2.Recv if !reflect.DeepEqual(observer2.Events, []eh.Event{event1}) { t.Error("the second observed events should be correct:", observer2.Events) } t.Log("publish event") handler := testutil.NewMockEventHandler("testHandler") bus.AddHandler(handler, testutil.TestEventType) bus.PublishEvent(event1) if !reflect.DeepEqual(handler.Events, []eh.Event{event1}) { t.Error("the handler events should be correct:", handler.Events) } <-observer.Recv if !reflect.DeepEqual(observer.Events, []eh.Event{event1, event1}) { t.Error("the observed events should be correct:", observer.Events) } <-observer2.Recv if !reflect.DeepEqual(observer2.Events, []eh.Event{event1, event1}) { t.Error("the second observed events should be correct:", observer2.Events) } t.Log("publish another event") bus.AddHandler(handler, testutil.TestEventOtherType) event2 := &testutil.TestEventOther{eh.NewUUID(), "event2"} bus.PublishEvent(event2) if !reflect.DeepEqual(handler.Events, []eh.Event{event1, event2}) { t.Error("the handler events should be correct:", handler.Events) } <-observer.Recv if !reflect.DeepEqual(observer.Events, []eh.Event{event1, event1, event2}) { t.Error("the observed events should be correct:", observer.Events) } <-observer2.Recv if !reflect.DeepEqual(observer2.Events, []eh.Event{event1, event1, event2}) { t.Error("the second observed events should be correct:", observer2.Events) } }