// ========================================================================== // 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 Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.States { public class PersistenceEventSourcingTests { private readonly string key = Guid.NewGuid().ToString(); private readonly IEventDataFormatter eventDataFormatter = A.Fake(); private readonly IEventStore eventStore = A.Fake(); private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IPubSub pubSub = new InMemoryPubSub(true); private readonly IServiceProvider services = A.Fake(); private readonly ISnapshotStore snapshotStore = A.Fake>(); private readonly IStreamNameResolver streamNameResolver = A.Fake(); private readonly IStore sut; public PersistenceEventSourcingTests() { A.CallTo(() => services.GetService(typeof(ISnapshotStore))) .Returns(snapshotStore); A.CallTo(() => streamNameResolver.GetStreamName(typeof(object), key)) .Returns(key); sut = new Store(eventStore, 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(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", 0, new EventData()); A.CallTo(() => eventStore.QueryAsync(key, 0)) .Returns(new List { storedEvent }); A.CallTo(() => eventDataFormatter.Parse(storedEvent.Data, true)) .Throws(new TypeNameNotFoundException()); var persistedEvents = new List(); var persistence = sut.WithEventSourcing(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 = (object)null; var persistedEvents = new List(); var persistence = sut.WithSnapshotsAndEventSourcing(key, 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 = (object)null; var persistedEvents = new List(); var persistence = sut.WithSnapshotsAndEventSourcing(key, 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 = (object)null; var persistedEvents = new List(); var persistence = sut.WithSnapshotsAndEventSourcing(key, 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(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(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 = (object)null; var persistedEvents = new List(); var persistence = sut.WithSnapshotsAndEventSourcing(key, 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 = (object)null; var persistedEvents = new List(); var persistence = sut.WithSnapshotsAndEventSourcing(key, x => persistedState = x, x => persistedEvents.Add(x.Payload)); await persistence.ReadAsync(); } [Fact] public async Task Should_write_to_store_with_previous_position() { SetupEventStore(3); var persistedEvents = new List(); var persistence = sut.WithEventSourcing(key, x => persistedEvents.Add(x.Payload)); await persistence.ReadAsync(); await persistence.WriteEventsAsync(new[] { new MyEvent(), new MyEvent() }.Select(Envelope.Create)); await persistence.WriteEventsAsync(new[] { new MyEvent(), new MyEvent() }.Select(Envelope.Create)); A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 2))) .MustHaveHappened(); A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 4, A>.That.Matches(x => x.Count == 2))) .MustHaveHappened(); } [Fact] public async Task Should_wrap_exception_when_writing_to_store_with_previous_position() { SetupEventStore(3); var persistedEvents = new List(); var persistence = sut.WithEventSourcing(key, x => persistedEvents.Add(x.Payload)); await persistence.ReadAsync(); A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 2))) .Throws(new WrongEventVersionException(1, 1)); await Assert.ThrowsAsync(() => persistence.WriteEventsAsync(new[] { new MyEvent(), new MyEvent() }.Select(Envelope.Create))); } 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 = 0, int readPosition = 0) { var eventsStored = new List(); var i = eventOffset; foreach (var @event in events) { var eventData = new EventData(); var eventStored = new StoredEvent(i.ToString(), i, eventData); eventsStored.Add(eventStored); A.CallTo(() => eventDataFormatter.Parse(eventData, true)) .Returns(new Envelope(@event)); i++; } A.CallTo(() => eventStore.QueryAsync(key, readPosition)) .Returns(eventsStored); } } }