// ========================================================================== // StateSnapshotTests.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== using System; using System.Collections.Generic; using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Squidex.Infrastructure.EventSourcing; using Xunit; #pragma warning disable RECS0002 // Convert anonymous method to method group namespace Squidex.Infrastructure.States { public class StateSnapshotTests : IDisposable { private class MyStatefulObject : IStatefulObject { private IPersistence persistence; private int state; public long ExpectedVersion { get; set; } = EtagVersion.Any; public long Version { get { return persistence.Version; } } public int State { get { return state; } } public Task ActivateAsync(string key, IStore store) { persistence = store.WithSnapshots(key, s => state = s); return persistence.ReadAsync(ExpectedVersion); } public void SetState(int value) { state = value; } public Task WriteStateAsync() { return persistence.WriteSnapshotAsync(state); } } private readonly string key = Guid.NewGuid().ToString(); private readonly MyStatefulObject statefulObject = new MyStatefulObject(); 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 StateFactory sut; public StateSnapshotTests() { A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .Returns(statefulObject); A.CallTo(() => services.GetService(typeof(ISnapshotStore))) .Returns(snapshotStore); sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver); sut.Initialize(); } public void Dispose() { sut.Dispose(); } [Fact] public async Task Should_read_from_store() { statefulObject.ExpectedVersion = 1; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, 1)); var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.NotNull(cache.Get(key)); Assert.Equal(123, statefulObject.State); } [Fact] public async Task Should_set_to_empty_when_store_returns_not_found() { A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, EtagVersion.NotFound)); var actualObject = await sut.GetSingleAsync(key); Assert.Equal(-1, statefulObject.Version); Assert.Equal( 0, statefulObject.State); } [Fact] public async Task Should_throw_exception_if_not_found() { statefulObject.ExpectedVersion = 0; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((0, EtagVersion.Empty)); await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] public async Task Should_throw_exception_if_other_version_found() { statefulObject.ExpectedVersion = 1; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2)); await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] public async Task Should_not_throw_exception_if_noting_expected() { A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((0, EtagVersion.Empty)); await sut.GetSingleAsync(key); } [Fact] public async Task Should_provide_state_from_services_and_add_to_cache() { var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.NotNull(cache.Get(key)); } [Fact] public async Task Should_serve_next_request_from_cache() { var actualObject1 = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject1); Assert.NotNull(cache.Get(key)); var actualObject2 = await sut.GetSingleAsync(key); A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .MustHaveHappened(Repeated.Exactly.Once); } [Fact] public async Task Should_not_serve_next_request_from_cache_when_detached() { var actualObject1 = await sut.CreateAsync(key); Assert.Same(statefulObject, actualObject1); Assert.Null(cache.Get(key)); var actualObject2 = await sut.CreateAsync(key); A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .MustHaveHappened(Repeated.Exactly.Twice); } [Fact] public async Task Should_write_to_store_with_previous_version() { InvalidateMessage message = null; pubSub.Subscribe(m => { message = m; }); A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, 13)); var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.Equal(123, statefulObject.State); statefulObject.SetState(456); await statefulObject.WriteStateAsync(); A.CallTo(() => snapshotStore.WriteAsync(key, 456, 13, 14)) .MustHaveHappened(); Assert.NotNull(message); Assert.Equal(key, message.Key); } [Fact] public async Task Should_wrap_exception_when_writing_to_store_with_previous_version() { A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, 13)); A.CallTo(() => snapshotStore.WriteAsync(key, 123, 13, 14)) .Throws(new InconsistentStateException(1, 1, new InvalidOperationException())); var actualObject = await sut.GetSingleAsync(key); await Assert.ThrowsAsync(() => statefulObject.WriteStateAsync()); } [Fact] public async Task Should_remove_from_cache_when_invalidation_message_received() { var actualObject = await sut.GetSingleAsync(key); await InvalidateCacheAsync(); Assert.False(cache.TryGetValue(key, out var t)); } [Fact] public async Task Should_remove_from_cache_when_write_failed() { A.CallTo(() => snapshotStore.WriteAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) .Throws(new InvalidOperationException()); var actualObject = await sut.GetSingleAsync(key); await Assert.ThrowsAsync(() => statefulObject.WriteStateAsync()); Assert.False(cache.TryGetValue(key, out var t)); } [Fact] public async Task Should_return_same_instance_for_parallel_requests() { A.CallTo(() => snapshotStore.ReadAsync(key)) .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => (1, 1L))); var tasks = new List>(); for (var i = 0; i < 1000; i++) { tasks.Add(Task.Run(() => sut.GetSingleAsync(key))); } var retrievedStates = await Task.WhenAll(tasks); foreach (var retrievedState in retrievedStates) { Assert.Same(retrievedStates[0], retrievedState); } A.CallTo(() => snapshotStore.ReadAsync(key)) .MustHaveHappened(Repeated.Exactly.Once); } private async Task RemoveFromCacheAsync() { cache.Remove(key); await Task.Delay(400); } private async Task InvalidateCacheAsync() { pubSub.Publish(new InvalidateMessage { Key = key }, true); await Task.Delay(400); } } }