// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschränkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FakeItEasy; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.States { public class PersistenceEventSourcingTests { private readonly string key = Guid.NewGuid().ToString(); private readonly IEventEnricher eventEnricher = A.Fake>(); private readonly IEventDataFormatter eventDataFormatter = A.Fake(); private readonly IEventStore eventStore = A.Fake(); private readonly IServiceProvider services = A.Fake(); private readonly ISnapshotStore snapshotStore = A.Fake>(); private readonly ISnapshotStore snapshotStoreNone = A.Fake>(); private readonly IStreamNameResolver streamNameResolver = A.Fake(); private readonly IStore sut; public PersistenceEventSourcingTests() { A.CallTo(() => services.GetService(typeof(ISnapshotStore))) .Returns(snapshotStore); A.CallTo(() => services.GetService(typeof(ISnapshotStore))) .Returns(snapshotStoreNone); A.CallTo(() => streamNameResolver.GetStreamName(None.Type, key)) .Returns(key); sut = new Store(eventStore, eventEnricher, eventDataFormatter, services, streamNameResolver); } [Fact] public async Task Should_read_from_store() { var event1 = new MyEvent(); var event2 = new MyEvent(); SetupEventStore(event1, event2); var persistedEvents = new List(); var persistence = sut.WithEventSourcing(None.Type, key, x => persistedEvents.Add(x.Payload)); await persistence.ReadAsync(); Assert.Equal(persistedEvents.ToArray(), new[] { event1, event2 }); } [Fact] public async Task Should_ignore_old_events() { var storedEvent = new StoredEvent("1", "1", 0, new EventData("Type", new EnvelopeHeaders(), "Payload")); A.CallTo(() => eventStore.QueryAsync(key, 0)) .Returns(new List { storedEvent }); A.CallTo(() => eventDataFormatter.Parse(storedEvent.Data, null)) .Throws(new TypeNameNotFoundException()); var persistedEvents = new List(); var persistence = sut.WithEventSourcing(None.Type, key, x => persistedEvents.Add(x.Payload)); await persistence.ReadAsync(); Assert.Empty(persistedEvents); Assert.Equal(0, persistence.Version); } [Fact] public async Task Should_read_status_from_snapshot() { A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); SetupEventStore(3, 2); var persistedState = -1; var persistedEvents = new List(); var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, (int x) => persistedState = x, x => persistedEvents.Add(x.Payload)); await persistence.ReadAsync(); A.CallTo(() => eventStore.QueryAsync(key, 3)) .MustHaveHappened(); } [Fact] public async Task Should_throw_exception_if_events_are_older_than_snapshot() { A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); SetupEventStore(3, 0, 3); var persistedState = -1; var persistedEvents = new List(); var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, (int x) => persistedState = x, x => persistedEvents.Add(x.Payload)); await Assert.ThrowsAsync(() => persistence.ReadAsync()); } [Fact] public async Task Should_throw_exception_if_events_have_gaps_to_snapshot() { A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); SetupEventStore(3, 4, 3); var persistedState = -1; var persistedEvents = new List(); var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, (int x) => persistedState = x, x => persistedEvents.Add(x.Payload)); await Assert.ThrowsAsync(() => persistence.ReadAsync()); } [Fact] public async Task Should_throw_exception_if_not_found() { SetupEventStore(0); var persistedEvents = new List(); var persistence = sut.WithEventSourcing(None.Type, key, x => persistedEvents.Add(x.Payload)); await Assert.ThrowsAsync(() => persistence.ReadAsync(1)); } [Fact] public async Task Should_throw_exception_if_other_version_found() { SetupEventStore(3); var persistedEvents = new List(); var persistence = sut.WithEventSourcing(None.Type, key, x => persistedEvents.Add(x.Payload)); await Assert.ThrowsAsync(() => persistence.ReadAsync(1)); } [Fact] public async Task Should_throw_exception_if_other_version_found_from_snapshot() { A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); SetupEventStore(0); var persistedState = -1; var persistedEvents = new List(); var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, (int x) => persistedState = x, x => persistedEvents.Add(x.Payload)); await Assert.ThrowsAsync(() => persistence.ReadAsync(1)); } [Fact] public async Task Should_not_throw_exception_if_nothing_expected() { SetupEventStore(0); var persistedState = -1; var persistedEvents = new List(); var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, (int x) => persistedState = x, x => persistedEvents.Add(x.Payload)); await persistence.ReadAsync(); } [Fact] public async Task Should_write_to_store_with_previous_version() { SetupEventStore(3); var persistedEvents = new List(); var persistence = sut.WithEventSourcing(None.Type, key, x => persistedEvents.Add(x.Payload)); await persistence.ReadAsync(); await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 1))) .MustHaveHappened(); A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 3, A>.That.Matches(x => x.Count == 1))) .MustHaveHappened(); A.CallTo(() => eventEnricher.Enrich(A>.Ignored, key)) .MustHaveHappenedTwiceExactly(); } [Fact] public async Task Should_write_events_to_store_with_empty_version() { var persistence = sut.WithEventSourcing(None.Type, key, null); await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, EtagVersion.Empty, A>.That.Matches(x => x.Count == 1))) .MustHaveHappened(); } [Fact] public async Task Should_wrap_exception_when_writing_to_store_with_previous_version() { SetupEventStore(3); var persistedEvents = new List(); var persistence = sut.WithEventSourcing(None.Type, key, x => persistedEvents.Add(x.Payload)); await persistence.ReadAsync(); A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 1))) .Throws(new WrongEventVersionException(1, 1)); await Assert.ThrowsAsync(() => persistence.WriteEventAsync(Envelope.Create(new MyEvent()))); } [Fact] public async Task Should_delete_events_but_not_snapshot_when_deleted_snapshot_only() { var persistence = sut.WithEventSourcing(None.Type, key, null); await persistence.DeleteAsync(); A.CallTo(() => eventStore.DeleteStreamAsync(key)) .MustHaveHappened(); A.CallTo(() => snapshotStore.RemoveAsync(key)) .MustNotHaveHappened(); } [Fact] public async Task Should_delete_events_and_snapshot_when_deleted() { var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, null, null); await persistence.DeleteAsync(); A.CallTo(() => eventStore.DeleteStreamAsync(key)) .MustHaveHappened(); A.CallTo(() => snapshotStore.RemoveAsync(key)) .MustHaveHappened(); } private void SetupEventStore(int count, int eventOffset = 0, int readPosition = 0) { SetupEventStore(Enumerable.Repeat(0, count).Select(x => new MyEvent()).ToArray(), eventOffset, readPosition); } private void SetupEventStore(params MyEvent[] events) { SetupEventStore(events, 0, 0); } private void SetupEventStore(MyEvent[] events, int eventOffset, int readPosition = 0) { var eventsStored = new List(); var i = eventOffset; foreach (var @event in events) { var eventData = new EventData("Type", new EnvelopeHeaders(), "Payload"); var eventStored = new StoredEvent(i.ToString(), i.ToString(), i, eventData); eventsStored.Add(eventStored); A.CallTo(() => eventDataFormatter.Parse(eventData, null)) .Returns(new Envelope(@event)); i++; } A.CallTo(() => eventStore.QueryAsync(key, readPosition)) .Returns(eventsStored); } } }