From 07cdda6a009cf51231504df517c77cd939e51363 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 3 Jul 2018 13:26:46 +0200 Subject: [PATCH] New domain object type. --- ...ain.cs => LogSnapshotDomainObjectGrain.cs} | 8 +- .../Commands/DomainObjectGrainTests.cs | 26 +- .../LogSnapshotDomainObjectGrainTests.cs | 300 ++++++++++++++++++ .../TestHelpers/MyDomainObject.cs | 91 ++++++ .../TestHelpers/MyDomainState.cs | 4 +- 5 files changed, 415 insertions(+), 14 deletions(-) rename src/Squidex.Infrastructure/Commands/{MultiSnapshotDomainObjectGrain.cs => LogSnapshotDomainObjectGrain.cs} (90%) create mode 100644 tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs create mode 100644 tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs diff --git a/src/Squidex.Infrastructure/Commands/MultiSnapshotDomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs similarity index 90% rename from src/Squidex.Infrastructure/Commands/MultiSnapshotDomainObjectGrain.cs rename to src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs index 44aa4c731..487c8e74d 100644 --- a/src/Squidex.Infrastructure/Commands/MultiSnapshotDomainObjectGrain.cs +++ b/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs @@ -59,7 +59,7 @@ namespace Squidex.Infrastructure.Commands var snapshot = OnEvent(@event); snapshot.Version = NewVersion + 1; - snapshots.Add(OnEvent(@event)); + snapshots.Add(snapshot); } protected sealed override Task ReadAsync(Type type, Guid id) @@ -73,16 +73,16 @@ namespace Squidex.Infrastructure.Commands { if (events.Length > 0) { - var snaphosts = store.GetSnapshotStore(); + var persistedSnapshots = store.GetSnapshotStore(); await persistence.WriteEventsAsync(events); - await snaphosts.WriteAsync(Id, Snapshot, previousVersion, previousVersion + events.Length); + await persistedSnapshots.WriteAsync(Id, Snapshot, previousVersion, previousVersion + events.Length); } } protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) { - while (snapshots.Count > previousVersion) + while (snapshots.Count > previousVersion + 2) { snapshots.RemoveAt(snapshots.Count - 1); } diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs index 34818e231..487ae93f5 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs @@ -126,9 +126,9 @@ namespace Squidex.Infrastructure.Commands { await SetupEmptyAsync(); - var result = await sut.ExecuteAsync(C(new CreateAuto { Value = 5 })); + var result = await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); - A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 5))) + A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 4))) .MustHaveHappened(); A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) .MustHaveHappened(); @@ -136,7 +136,9 @@ namespace Squidex.Infrastructure.Commands Assert.True(result.Value is EntityCreatedResult); Assert.Empty(sut.GetUncomittedEvents()); - Assert.Equal(5, sut.Snapshot.Value); + + Assert.Equal(4, sut.Snapshot.Value); + Assert.Equal(0, sut.Snapshot.Version); } [Fact] @@ -144,9 +146,9 @@ namespace Squidex.Infrastructure.Commands { await SetupCreatedAsync(); - var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = 5 })); + var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); - A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 5))) + A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 8))) .MustHaveHappened(); A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) .MustHaveHappened(); @@ -154,7 +156,9 @@ namespace Squidex.Infrastructure.Commands Assert.True(result.Value is EntitySavedResult); Assert.Empty(sut.GetUncomittedEvents()); - Assert.Equal(5, sut.Snapshot.Value); + + Assert.Equal(8, sut.Snapshot.Value); + Assert.Equal(1, sut.Snapshot.Version); } [Fact] @@ -212,7 +216,9 @@ namespace Squidex.Infrastructure.Commands await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); Assert.Empty(sut.GetUncomittedEvents()); - Assert.Equal(0, sut.Snapshot.Value); + + Assert.Equal(0, sut.Snapshot.Value); + Assert.Equal(-1, sut.Snapshot.Version); } [Fact] @@ -226,14 +232,16 @@ namespace Squidex.Infrastructure.Commands await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); Assert.Empty(sut.GetUncomittedEvents()); - Assert.Equal(0, sut.Snapshot.Value); + + Assert.Equal(4, sut.Snapshot.Value); + Assert.Equal(0, sut.Snapshot.Version); } private async Task SetupCreatedAsync() { await sut.OnActivateAsync(id); - await sut.ExecuteAsync(C(new CreateAuto())); + await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); } private static J C(IAggregateCommand command) diff --git a/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs new file mode 100644 index 000000000..8fc1270d8 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs @@ -0,0 +1,300 @@ +// ========================================================================== +// 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 FluentAssertions; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Commands +{ + public class MultiSnapshotDomainObjectGrainTests + { + private readonly IStore store = A.Fake>(); + private readonly ISnapshotStore snapshotStore = A.Fake>(); + private readonly IPersistence persistence = A.Fake(); + private readonly Guid id = Guid.NewGuid(); + private readonly MyDomainObject sut; + + public sealed class ValueChanged : IEvent + { + public int Value { get; set; } + } + + public sealed class CreateAuto : MyCommand + { + public int Value { get; set; } + } + + public sealed class CreateCustom : MyCommand + { + public int Value { get; set; } + } + + public sealed class UpdateAuto : MyCommand + { + public int Value { get; set; } + } + + public sealed class UpdateCustom : MyCommand + { + public int Value { get; set; } + } + + public sealed class MyDomainObject : MultiSnapshotDomainObjectGrain + { + public MyDomainObject(IStore store) + : base(store, A.Dummy()) + { + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + switch (command) + { + case CreateAuto createAuto: + return CreateAsync(createAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case CreateCustom createCustom: + return CreateReturnAsync(createCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "CREATED"; + }); + + case UpdateAuto updateAuto: + return UpdateAsync(updateAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case UpdateCustom updateCustom: + return UpdateReturnAsync(updateCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "UPDATED"; + }); + } + + return Task.FromResult(null); + } + + protected override MyDomainState OnEvent(Envelope @event) + { + return new MyDomainState { Value = ((ValueChanged)@event.Payload).Value }; + } + } + + public MultiSnapshotDomainObjectGrainTests() + { + A.CallTo(() => store.WithEventSourcing(typeof(MyDomainObject), id, A, Task>>.Ignored)) + .Returns(persistence); + + A.CallTo(() => store.GetSnapshotStore()) + .Returns(snapshotStore); + + sut = new MyDomainObject(store); + } + + [Fact] + public async Task Should_get_latestet_version_when_requesting_state_with_any() + { + await SetupUpdatedAsync(); + + var result = sut.GetSnapshot(EtagVersion.Any); + + result.Should().BeEquivalentTo(new MyDomainState { Value = 8, Version = 1 }); + } + + [Fact] + public async Task Should_get_empty_version_when_requesting_state_with_empty_version() + { + await SetupUpdatedAsync(); + + var result = sut.GetSnapshot(EtagVersion.Empty); + + result.Should().BeEquivalentTo(new MyDomainState { Value = 0, Version = -1 }); + } + + [Fact] + public async Task Should_get_specific_version_when_requesting_state_with_specific_version() + { + await SetupUpdatedAsync(); + + sut.GetSnapshot(0).Should().BeEquivalentTo(new MyDomainState { Value = 4, Version = 0 }); + sut.GetSnapshot(1).Should().BeEquivalentTo(new MyDomainState { Value = 8, Version = 1 }); + } + + [Fact] + public async Task Should_get_null_state_when_requesting_state_with_invalid_version() + { + await SetupUpdatedAsync(); + + Assert.Null(sut.GetSnapshot(-3)); + Assert.Null(sut.GetSnapshot(2)); + } + + [Fact] + public void Should_instantiate() + { + Assert.Equal(EtagVersion.Empty, sut.Version); + } + + [Fact] + public async Task Should_write_state_and_events_when_created() + { + await SetupEmptyAsync(); + + var result = await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); + + A.CallTo(() => snapshotStore.WriteAsync(id, A.That.Matches(x => x.Value == 4), -1, 0)) + .MustHaveHappened(); + A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) + .MustHaveHappened(); + + Assert.True(result.Value is EntityCreatedResult); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(4, sut.Snapshot.Value); + Assert.Equal(0, sut.Snapshot.Version); + } + + [Fact] + public async Task Should_write_state_and_events_when_updated() + { + await SetupCreatedAsync(); + + var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); + + A.CallTo(() => snapshotStore.WriteAsync(id, A.That.Matches(x => x.Value == 8), 0, 1)) + .MustHaveHappened(); + A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) + .MustHaveHappened(); + + Assert.True(result.Value is EntitySavedResult); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(8, sut.Snapshot.Value); + Assert.Equal(1, sut.Snapshot.Version); + } + + [Fact] + public async Task Should_throw_exception_when_already_created() + { + await SetupCreatedAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); + } + + [Fact] + public async Task Should_throw_exception_when_not_created() + { + await SetupEmptyAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); + } + + [Fact] + public async Task Should_return_custom_result_on_create() + { + await SetupEmptyAsync(); + + var result = await sut.ExecuteAsync(C(new CreateCustom())); + + Assert.Equal("CREATED", result.Value); + } + + [Fact] + public async Task Should_return_custom_result_on_update() + { + await SetupCreatedAsync(); + + var result = await sut.ExecuteAsync(C(new UpdateCustom())); + + Assert.Equal("UPDATED", result.Value); + } + + [Fact] + public async Task Should_throw_exception_when_other_verison_expected() + { + await SetupCreatedAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateCustom { ExpectedVersion = 3 }))); + } + + [Fact] + public async Task Should_reset_state_when_writing_snapshot_for_create_failed() + { + await SetupEmptyAsync(); + + A.CallTo(() => snapshotStore.WriteAsync(A.Ignored, A.Ignored, -1, 0)) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(0, sut.Snapshot.Value); + Assert.Equal(-1, sut.Snapshot.Version); + } + + [Fact] + public async Task Should_reset_state_when_writing_snapshot_for_update_failed() + { + await SetupCreatedAsync(); + + A.CallTo(() => snapshotStore.WriteAsync(A.Ignored, A.Ignored, 0, 1)) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(4, sut.Snapshot.Value); + Assert.Equal(0, sut.Snapshot.Version); + } + + private async Task SetupCreatedAsync() + { + await sut.OnActivateAsync(id); + + await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); + } + + private async Task SetupUpdatedAsync() + { + await SetupCreatedAsync(); + + await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); + } + + private async Task SetupEmptyAsync() + { + await sut.OnActivateAsync(id); + } + + private static J C(IAggregateCommand command) + { + return command.AsJ(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs new file mode 100644 index 000000000..8c50065a5 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs @@ -0,0 +1,91 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.TestHelpers +{ + public sealed class MyDomainObject : DomainObjectGrain + { + public sealed class ValueChanged : IEvent + { + public int Value { get; set; } + } + + public sealed class CreateAuto : MyCommand + { + public int Value { get; set; } + } + + public sealed class CreateCustom : MyCommand + { + public int Value { get; set; } + } + + public sealed class UpdateAuto : MyCommand + { + public int Value { get; set; } + } + + public sealed class UpdateCustom : MyCommand + { + public int Value { get; set; } + } + + public MyDomainObject(IStore store) + : base(store, A.Dummy()) + { + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + switch (command) + { + case CreateAuto createAuto: + return CreateAsync(createAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case CreateCustom createCustom: + return CreateReturnAsync(createCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "CREATED"; + }); + + case UpdateAuto updateAuto: + return UpdateAsync(updateAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case UpdateCustom updateCustom: + return UpdateReturnAsync(updateCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "UPDATED"; + }); + } + + return Task.FromResult(null); + } + + protected override MyDomainState OnEvent(Envelope @event) + { + return new MyDomainState { Value = ((ValueChanged)@event.Payload).Value }; + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs index 5792385b9..2ca95b098 100644 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs @@ -9,8 +9,10 @@ using Squidex.Infrastructure.Commands; namespace Squidex.Infrastructure.TestHelpers { - public class MyDomainState : IDomainState + public sealed class MyDomainState : IDomainState { public long Version { get; set; } + + public int Value { get; set; } } }