diff --git a/src/BullOak.Repositories.EventStore.Test.Integration/Components/IHoldHigherOrder.cs b/src/BullOak.Repositories.EventStore.Test.Integration/Components/IHoldHigherOrder.cs index 05d1ded..149a7a8 100644 --- a/src/BullOak.Repositories.EventStore.Test.Integration/Components/IHoldHigherOrder.cs +++ b/src/BullOak.Repositories.EventStore.Test.Integration/Components/IHoldHigherOrder.cs @@ -4,6 +4,6 @@ public interface IHoldHigherOrder { int HigherOrder { get; set; } - bool Visility { get; set; } + bool Visibility { get; set; } } } diff --git a/src/BullOak.Repositories.EventStore.Test.Integration/Components/StateApplier.cs b/src/BullOak.Repositories.EventStore.Test.Integration/Components/StateApplier.cs index 57623df..df181c7 100644 --- a/src/BullOak.Repositories.EventStore.Test.Integration/Components/StateApplier.cs +++ b/src/BullOak.Repositories.EventStore.Test.Integration/Components/StateApplier.cs @@ -24,7 +24,7 @@ public IHoldHigherOrder Apply(IHoldHigherOrder state, EntitySoftDeleted @event) public IHoldHigherOrder Apply(IHoldHigherOrder state, VisibilityEnabledEvent @event) { - state.Visility = true; + state.Visibility = true; return state; } diff --git a/src/BullOak.Repositories.EventStore.Test.Integration/Contexts/EventStoreIntegrationContext.cs b/src/BullOak.Repositories.EventStore.Test.Integration/Contexts/EventStoreIntegrationContext.cs index adf8cc3..11007a8 100644 --- a/src/BullOak.Repositories.EventStore.Test.Integration/Contexts/EventStoreIntegrationContext.cs +++ b/src/BullOak.Repositories.EventStore.Test.Integration/Contexts/EventStoreIntegrationContext.cs @@ -20,6 +20,7 @@ internal class EventStoreIntegrationContext private static readonly IntegrationTestsSettings testsSettings = new IntegrationTestsSettings(); private readonly EventStoreRepository repository; public readonly EventStoreReadOnlyRepository readOnlyRepository; + public readonly EventStoreEventReaderRepository readEventsRepository; private static IEventStoreConnection connection; private static IDisposable eventStoreIsolation; @@ -41,6 +42,7 @@ public EventStoreIntegrationContext(PassThroughValidator validator) repository = new EventStoreRepository(validator, configuration, GetConnection(), DateTimeProvider); readOnlyRepository = new EventStoreReadOnlyRepository(configuration, GetConnection()); + readEventsRepository = new EventStoreEventReaderRepository(configuration, GetConnection()); } private static IEventStoreConnection GetConnection() @@ -86,12 +88,12 @@ public async Task AppendEventsToCurrentStream(string id, IMyEvent[] events) } } + public Task HardDeleteStream(string id, int expectedVersion = -1) + => GetConnection().DeleteStreamAsync(id, expectedVersion, hardDelete: true); + public Task SoftDeleteStream(string id) => repository.SoftDelete(id); - public Task HardDeleteStream(string id) - => GetConnection().DeleteStreamAsync(id, -1, true); - public Task SoftDeleteByEvent(string id) => repository.SoftDeleteByEvent(id); diff --git a/src/BullOak.Repositories.EventStore.Test.Integration/Contexts/TestDataContext.cs b/src/BullOak.Repositories.EventStore.Test.Integration/Contexts/TestDataContext.cs index f0cddee..49f9b9c 100644 --- a/src/BullOak.Repositories.EventStore.Test.Integration/Contexts/TestDataContext.cs +++ b/src/BullOak.Repositories.EventStore.Test.Integration/Contexts/TestDataContext.cs @@ -11,10 +11,14 @@ internal class TestDataContext internal string CurrentStreamId { get; set; } + internal int CurrentVersion { get; set; } = -1; + internal Guid RawStreamId { get; set; } public Exception RecordedException { get; internal set; } public IHoldHigherOrder LatestLoadedState { get; internal set; } + public IEnumerable LatestReadEvents { get; internal set; } + public Dictionary> NamedSessions { get; internal set; } = new Dictionary>(); public Dictionary> NamedSessionsExceptions { get; internal set; } = @@ -29,6 +33,7 @@ internal void ResetStream(string categoryName = null) RawStreamId = Guid.NewGuid(); StreamIdPrefix = !string.IsNullOrEmpty(categoryName) ? $"{StreamIdPrefix}_{categoryName}" : StreamIdPrefix; CurrentStreamId = $"{StreamIdPrefix}-{RawStreamId}"; + CurrentVersion = -1; } } } diff --git a/src/BullOak.Repositories.EventStore.Test.Integration/Specification/ReadEventsSpecs.feature b/src/BullOak.Repositories.EventStore.Test.Integration/Specification/ReadEventsSpecs.feature new file mode 100644 index 0000000..d559f19 --- /dev/null +++ b/src/BullOak.Repositories.EventStore.Test.Integration/Specification/ReadEventsSpecs.feature @@ -0,0 +1,66 @@ +Feature: ReadEventsSpecs + In order to support stream migration + As a user of this library + I want to be able to read all events from a particular stream + +Scenario: Read all events from a stream + Given a new stream + And 3 new events + And I try to save the new events in the stream through their interface + When I read all the events back from the stream + Then the load process should succeed + And I should see all the events + +Scenario: Read events after a hard delete should return empty events + Given a new stream + And 3 new events + And I try to save the new events in the stream through their interface + When I hard-delete the stream + And I read all the events back from the stream + Then the stream should be empty + +Scenario: Read events after a soft delete should return empty events + Given a new stream + And 3 new events + And I try to save the new events in the stream through their interface + When I soft-delete the stream + And I read all the events back from the stream + Then the stream should be empty + +Scenario: Read events after a soft delete by event should return empty events + Given a new stream + And 3 new events + And I try to save the new events in the stream through their interface + When I soft-delete-by-event the stream + And I read all the events back from the stream + Then the stream should be empty + +Scenario: Read events after a soft delete by event with subsequant events appended should return only appended events + Given a new stream + And 3 new events + And I try to save the new events in the stream through their interface + And I soft-delete-by-event the stream + And 5 new events + And I try to save the new events in the stream through their interface + When I read all the events back from the stream + Then the load process should succeed + And I should see all the appended events only + +Scenario: Read events after a soft delete by custom event should return empty events + Given a new stream + And 3 new events + And I try to save the new events in the stream through their interface + When I soft-delete-by-custom-event the stream + And I read all the events back from the stream + Then the stream should be empty + +Scenario: Read events after a soft delete by custom event with subsequant events appended should return only appended events + Given a new stream + And 3 new events + And I try to save the new events in the stream through their interface + And I soft-delete-by-custom-event the stream + And 5 new events + And I try to save the new events in the stream through their interface + When I read all the events back from the stream + Then the load process should succeed + And I should see all the appended events only diff --git a/src/BullOak.Repositories.EventStore.Test.Integration/Specification/ReadModelSpecs.feature b/src/BullOak.Repositories.EventStore.Test.Integration/Specification/ReadModelSpecs.feature index e6b55d6..edcd7d9 100644 --- a/src/BullOak.Repositories.EventStore.Test.Integration/Specification/ReadModelSpecs.feature +++ b/src/BullOak.Repositories.EventStore.Test.Integration/Specification/ReadModelSpecs.feature @@ -66,4 +66,4 @@ Scenario: Reconstitute state based on category with two event types up to a give And I update the state of visible to be enabled as of '2020-09-22 11:10:00' When I load all my entities as of '2020-09-20 11:10:00' for the streams category Then the load process should succeed - And the visibilty should be disabled + And the visibility should be disabled diff --git a/src/BullOak.Repositories.EventStore.Test.Integration/Specification/ReadModelSpecs.feature.cs b/src/BullOak.Repositories.EventStore.Test.Integration/Specification/ReadModelSpecs.feature.cs index 2f58d35..1e264b8 100644 --- a/src/BullOak.Repositories.EventStore.Test.Integration/Specification/ReadModelSpecs.feature.cs +++ b/src/BullOak.Repositories.EventStore.Test.Integration/Specification/ReadModelSpecs.feature.cs @@ -375,7 +375,7 @@ public virtual void ReconstituteStateBasedOnCategoryWithTwoEventTypesUpToAGivenD testRunner.Then("the load process should succeed", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden #line 69 - testRunner.And("the visibilty should be disabled", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); + testRunner.And("the visibility should be disabled", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); #line hidden } this.ScenarioCleanup(); diff --git a/src/BullOak.Repositories.EventStore.Test.Integration/StepDefinitions/StreamStepsDefinitions.cs b/src/BullOak.Repositories.EventStore.Test.Integration/StepDefinitions/StreamStepsDefinitions.cs index c5af224..d248aab 100644 --- a/src/BullOak.Repositories.EventStore.Test.Integration/StepDefinitions/StreamStepsDefinitions.cs +++ b/src/BullOak.Repositories.EventStore.Test.Integration/StepDefinitions/StreamStepsDefinitions.cs @@ -78,7 +78,7 @@ public async Task GivenIUpdateTheStateOfVisibleToBeEnabled(DateTime datetime) testDataContext.LastGeneratedEvents = new List{ visibleEvent }; session.AddEvent(visibleEvent); - await session.SaveChanges(); + testDataContext.CurrentVersion = await session.SaveChanges(); } } @@ -114,7 +114,7 @@ public async Task GivenITryToSaveTheNewEventsInTheStreamThroughTheirInterface() }); } - await session.SaveChanges(); + testDataContext.CurrentVersion = await session.SaveChanges(); } }); } @@ -141,12 +141,16 @@ public async Task WhenITryToSaveTheNewEventsInTheStream() } [Given(@"I soft-delete the stream")] + [When(@"I soft-delete the stream")] public Task GivenISoft_DeleteTheStream() => eventStoreContainer.SoftDeleteStream(testDataContexts.First().CurrentStreamId); [Given(@"I hard-delete the stream")] + [When(@"I hard-delete the stream")] public Task GivenIHard_DeleteTheStream() - => eventStoreContainer.HardDeleteStream(testDataContexts.First().CurrentStreamId); + => eventStoreContainer.HardDeleteStream( + testDataContexts.First().CurrentStreamId, + testDataContexts.First().CurrentVersion); [Given(@"I soft-delete-by-event the stream")] [When(@"I soft-delete-by-event the stream")] @@ -182,10 +186,10 @@ public void ThenHighOrderPropertyForStreamShouldBe(int streamNumber, int highest States.ElementAt(--streamNumber).state.HigherOrder.Should().Be(highestOrderValue); } - [Then(@"the visibilty should be disabled")] - public void ThenTheVisibiltyShouldBeEnabled() + [Then(@"the visibility should be disabled")] + public void ThenTheVisibilityShouldBeEnabled() { - testDataContexts.First().LatestLoadedState.Visility.Should().BeFalse(); + testDataContexts.First().LatestLoadedState.Visibility.Should().BeFalse(); } [Then(@"the save process should fail")] @@ -244,7 +248,7 @@ public async Task WhenITryToSave(string sessionName) { testDataContext.NamedSessionsExceptions.Add(sessionName, new List()); } - var recordedException = await Record.ExceptionAsync(() => testDataContext.NamedSessions[sessionName].SaveChanges()); + var recordedException = await Record.ExceptionAsync(async () => testDataContext.CurrentVersion = await testDataContext.NamedSessions[sessionName].SaveChanges()); if (recordedException != null) { testDataContext.NamedSessionsExceptions[sessionName].Add(recordedException); @@ -273,6 +277,34 @@ public void ThenTheSaveProcessShouldFailFor(string sessionName) testDataContexts.First().NamedSessionsExceptions[sessionName].Should().NotBeEmpty(); } + [When(@"I read all the events back from the stream")] + public async Task WhenIReadAllTheEventsBackFromTheStream() + { + var testDataContext = testDataContexts.First(); + + if (testDataContext.RecordedException != null) return; + + testDataContext.RecordedException = await Record.ExceptionAsync(async () => + { + var itemWithTypes = await eventStoreContainer.readEventsRepository.ReadFrom(testDataContext.CurrentStreamId.ToString()); + testDataContext.LatestReadEvents = itemWithTypes; + }); + } + + [Then(@"I should see all the events")] + [Then(@"I should see all the appended events only")] + public void ThenIShouldSeeAllTheEvents() + { + testDataContexts.First().LatestReadEvents.Select(x => x.instance).Should().BeEquivalentTo(testDataContexts.First().LastGeneratedEvents); + } + + [Then(@"the stream should be empty")] + public void ThenTheStreamShouldBeEmpty() + { + testDataContexts.First().LatestReadEvents.Should().BeEmpty(); + } + + private void AddStream() { var testDataContext = new TestDataContext(); diff --git a/src/BullOak.Repositories.EventStore/EventStoreEventReaderRepository.cs b/src/BullOak.Repositories.EventStore/EventStoreEventReaderRepository.cs new file mode 100644 index 0000000..e936a01 --- /dev/null +++ b/src/BullOak.Repositories.EventStore/EventStoreEventReaderRepository.cs @@ -0,0 +1,29 @@ +namespace BullOak.Repositories.EventStore +{ + using BullOak.Repositories.EventStore.Streams; + using global::EventStore.ClientAPI; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + + public class EventStoreEventReaderRepository : IReadEvents + { + private readonly IHoldAllConfiguration configs; + private readonly EventReader reader; + + public EventStoreEventReaderRepository(IHoldAllConfiguration configs, IEventStoreConnection connection) + { + this.configs = configs ?? throw new ArgumentNullException(nameof(configs)); + + reader = new EventReader(connection, configs); + } + + public async Task> ReadFrom(TId id) + { + var streamData = await reader.ReadFrom(new ReadStreamBackwardsStrategy(id.ToString())); + + return streamData.results.Select(x => x.Event); ; + } + } +} diff --git a/src/BullOak.Repositories.EventStore/EventStoreReadOnlyRepository.cs b/src/BullOak.Repositories.EventStore/EventStoreReadOnlyRepository.cs index fda706c..7984d59 100644 --- a/src/BullOak.Repositories.EventStore/EventStoreReadOnlyRepository.cs +++ b/src/BullOak.Repositories.EventStore/EventStoreReadOnlyRepository.cs @@ -14,7 +14,7 @@ public class EventStoreReadOnlyRepository : IReadQueryModels + { + Task> ReadFrom(TId id); + } +}