Headless CMS and Content Managment Hub
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

483 lines
17 KiB

// ==========================================================================
// 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.Threading.Tasks;
using FakeItEasy;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.TestHelpers;
using Xunit;
namespace Squidex.Infrastructure.Commands
{
public class DomainObjectTests
{
private readonly IPersistenceFactory<MyDomainState> persistenceFactory = A.Fake<IPersistenceFactory<MyDomainState>>();
private readonly IPersistence<MyDomainState> persistence = A.Fake<IPersistence<MyDomainState>>();
private readonly DomainId id = DomainId.NewGuid();
private readonly MyDomainObject sut;
public DomainObjectTests()
{
sut = new MyDomainObject(persistenceFactory);
}
[Fact]
public void Should_instantiate()
{
Assert.Equal(EtagVersion.Empty, sut.Version);
AssertSnapshot(sut.Snapshot, 0);
}
[Fact]
public async Task Should_write_state_and_events_when_created()
{
SetupEmpty();
var result = await sut.ExecuteAsync(new CreateAuto { Value = 4 });
A.CallTo(() => persistence.WriteSnapshotAsync(A<MyDomainState>.That.Matches(x => x.Value == 4)))
.MustHaveHappened();
A.CallTo(() => persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1)))
.MustHaveHappened();
A.CallTo(() => persistence.ReadAsync(A<long>._))
.MustNotHaveHappened();
Assert.Equal(CommandResult.Empty(id, 0, EtagVersion.Empty), result);
Assert.Empty(sut.GetUncomittedEvents());
AssertSnapshot(sut.Snapshot, 4);
}
[Fact]
public async Task Should_recreate_with_create_command_when_deleted_before()
{
sut.Recreate = true;
SetupCreated(2);
SetupDeleted();
A.CallTo(() => persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._))
.Throws(new InconsistentStateException(2, -1)).Once();
var result = await sut.ExecuteAsync(new CreateAuto { Value = 4 });
A.CallTo(() => persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1)))
.MustHaveHappenedANumberOfTimesMatching(x => x == 3);
A.CallTo(() => persistence.ReadAsync(A<long>._))
.MustHaveHappened();
Assert.Equal(CommandResult.Empty(id, 2, 1), result);
Assert.Empty(sut.GetUncomittedEvents());
AssertSnapshot(sut.Snapshot, 4);
}
[Fact]
public async Task Should_throw_exception_when_recreation_with_create_command_not_allowed()
{
sut.Recreate = false;
SetupCreated(2);
SetupDeleted();
A.CallTo(() => persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._))
.Throws(new InconsistentStateException(2, -1)).Once();
await Assert.ThrowsAsync<DomainObjectConflictException>(() => sut.ExecuteAsync(new CreateAuto { Value = 4 }));
}
[Fact]
public async Task Should_recreate_with_upsert_command_when_deleted_before()
{
sut.Recreate = true;
SetupCreated(2);
SetupDeleted();
A.CallTo(() => persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._))
.Throws(new InconsistentStateException(2, -1)).Once();
var result = await sut.ExecuteAsync(new Upsert { Value = 4 });
A.CallTo(() => persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1)))
.MustHaveHappenedANumberOfTimesMatching(x => x == 3);
A.CallTo(() => persistence.ReadAsync(A<long>._))
.MustHaveHappened();
Assert.Equal(CommandResult.Empty(id, 2, 1), result);
Assert.Empty(sut.GetUncomittedEvents());
AssertSnapshot(sut.Snapshot, 4);
}
[Fact]
public async Task Should_throw_exception_when_recreation_with_upsert_command_not_allowed()
{
sut.Recreate = false;
SetupCreated(2);
SetupDeleted();
A.CallTo(() => persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._))
.Throws(new InconsistentStateException(2, -1)).Once();
await Assert.ThrowsAsync<DomainObjectDeletedException>(() => sut.ExecuteAsync(new Upsert { Value = 4 }));
}
[Fact]
public async Task Should_write_state_and_events_when_updated_after_creation()
{
SetupEmpty();
await sut.ExecuteAsync(new CreateAuto { Value = 4 });
var result = await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 });
A.CallTo(() => persistence.WriteSnapshotAsync(A<MyDomainState>.That.Matches(x => x.Value == 8)))
.MustHaveHappened();
A.CallTo(() => persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1)))
.MustHaveHappened();
A.CallTo(() => persistence.ReadAsync(A<long>._))
.MustNotHaveHappened();
Assert.Equal(CommandResult.Empty(id, 1, 0), result);
Assert.Empty(sut.GetUncomittedEvents());
AssertSnapshot(sut.Snapshot, 8);
}
[Fact]
public async Task Should_write_state_and_events_when_updated()
{
SetupCreated(4);
var result = await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 });
A.CallTo(() => persistence.WriteSnapshotAsync(A<MyDomainState>.That.Matches(x => x.Value == 8)))
.MustHaveHappened();
A.CallTo(() => persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1)))
.MustHaveHappened();
A.CallTo(() => persistence.ReadAsync(A<long>._))
.MustHaveHappenedOnceExactly();
Assert.Equal(CommandResult.Empty(id, 1, 0), result);
Assert.Empty(sut.GetUncomittedEvents());
AssertSnapshot(sut.Snapshot, 8);
}
[Fact]
public async Task Should_not_load_on_create()
{
SetupEmpty();
await sut.ExecuteAsync(new CreateAuto());
A.CallTo(() => persistence.ReadAsync(A<long>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_load_once_on_update()
{
SetupCreated(4);
await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 });
await sut.ExecuteAsync(new UpdateAuto { Value = 9, ExpectedVersion = 1 });
A.CallTo(() => persistence.ReadAsync(A<long>._))
.MustHaveHappenedOnceExactly();
Assert.Empty(sut.GetUncomittedEvents());
AssertSnapshot(sut.Snapshot, 9);
}
[Fact]
public async Task Should_rebuild_state()
{
SetupCreated(4);
await sut.RebuildStateAsync();
A.CallTo(() => persistence.WriteSnapshotAsync(A<MyDomainState>.That.Matches(x => x.Value == 4)))
.MustHaveHappened();
A.CallTo(() => persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_throw_on_rebuild_when_no_event_found()
{
SetupEmpty();
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.RebuildStateAsync());
}
[Fact]
public async Task Should_throw_exception_on_create_command_is_rejected_due_to_version_conflict()
{
SetupEmpty();
A.CallTo(() => persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._))
.Throws(new InconsistentStateException(4, EtagVersion.Empty));
await Assert.ThrowsAsync<DomainObjectConflictException>(() => sut.ExecuteAsync(new CreateAuto()));
}
[Fact]
public async Task Should_throw_exception_when_create_command_is_invoked_for_loaded_and_created_object()
{
await sut.ExecuteAsync(new CreateAuto());
await Assert.ThrowsAsync<DomainObjectConflictException>(() => sut.ExecuteAsync(new CreateAuto()));
}
[Fact]
public async Task Should_throw_exception_when_create_command_not_accepted()
{
SetupEmpty();
await Assert.ThrowsAsync<DomainException>(() => sut.ExecuteAsync(new CreateAuto { Value = 99 }));
}
[Fact]
public async Task Should_return_custom_result_on_create()
{
SetupEmpty();
var result = await sut.ExecuteAsync(new CreateCustom());
Assert.Equal(new CommandResult(id, 0, EtagVersion.Empty, "CREATED"), result);
}
[Fact]
public async Task Should_throw_exception_when_update_command_invoked_for_empty_object()
{
SetupEmpty();
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.ExecuteAsync(new UpdateAuto()));
}
[Fact]
public async Task Should_throw_exception_when_update_command_not_accepted()
{
SetupCreated(4);
await Assert.ThrowsAsync<DomainException>(() => sut.ExecuteAsync(new UpdateAuto { Value = 99 }));
}
[Fact]
public async Task Should_return_custom_result_on_update()
{
SetupCreated(4);
var result = await sut.ExecuteAsync(new UpdateCustom());
Assert.Equal(new CommandResult(id, 1, 0, "UPDATED"), result);
}
[Fact]
public async Task Should_throw_exception_when_other_verison_expected()
{
SetupCreated(4);
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.ExecuteAsync(new UpdateCustom { ExpectedVersion = 3 }));
}
[Fact]
public async Task Should_not_update_when_snapshot_is_not_changed()
{
SetupCreated(4);
var result = await sut.ExecuteAsync(new UpdateAuto { Value = MyDomainState.Unchanged });
Assert.Equal(CommandResult.Empty(id, 0, 0), result);
Assert.Empty(sut.GetUncomittedEvents());
AssertSnapshot(sut.Snapshot, 4);
}
[Fact]
public async Task Should_reset_state_when_writing_snapshot_for_create_failed()
{
SetupEmpty();
A.CallTo(() => persistence.WriteSnapshotAsync(A<MyDomainState>._))
.Throws(new InvalidOperationException());
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.ExecuteAsync(new CreateAuto()));
Assert.Empty(sut.GetUncomittedEvents());
AssertSnapshot(sut.Snapshot, 0);
}
[Fact]
public async Task Should_reset_state_when_writing_snapshot_for_update_failed()
{
SetupCreated(4);
A.CallTo(() => persistence.WriteSnapshotAsync(A<MyDomainState>._))
.Throws(new InvalidOperationException());
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.ExecuteAsync(new UpdateAuto()));
Assert.Empty(sut.GetUncomittedEvents());
AssertSnapshot(sut.Snapshot, 4);
}
[Fact]
public async Task Should_write_events_to_delete_stream_on_delete()
{
SetupCreated(4);
SetupDeleted();
var deleteStream = A.Fake<IPersistence<MyDomainState>>();
A.CallTo(() => persistenceFactory.WithEventSourcing(typeof(MyDomainObject), DomainId.Combine(id, DomainId.Create("deleted")), null))
.Returns(deleteStream);
await sut.ExecuteAsync(new DeletePermanent());
AssertSnapshot(sut.Snapshot, 0, false);
A.CallTo(() => persistence.DeleteAsync())
.MustHaveHappened();
A.CallTo(() => persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._))
.MustHaveHappenedOnceExactly();
A.CallTo(() => deleteStream.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._))
.MustHaveHappened();
}
[Fact]
public async Task Should_get_old_versions_when_cached()
{
sut.VersionsToKeep = int.MaxValue;
SetupEmpty();
await sut.ExecuteAsync(new CreateAuto { Value = 3 });
await sut.ExecuteAsync(new UpdateAuto { Value = 4 });
var version_Empty = await sut.GetSnapshotAsync(EtagVersion.Empty);
var version_0 = await sut.GetSnapshotAsync(0);
var version_1 = await sut.GetSnapshotAsync(1);
Assert.Empty(sut.GetUncomittedEvents());
AssertSnapshot(version_Empty, 0);
AssertSnapshot(version_0, 3);
AssertSnapshot(version_1, 4);
A.CallTo(() => persistenceFactory.WithEventSourcing(typeof(MyDomainObject), id, A<HandleEvent>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_get_old_versions_from_query_when_not_cached()
{
sut.VersionsToKeep = 1;
SetupEmpty();
SetupLoaded();
await sut.ExecuteAsync(new CreateAuto { Value = 3 });
await sut.ExecuteAsync(new UpdateAuto { Value = 4 });
var version_Empty = await sut.GetSnapshotAsync(EtagVersion.Empty);
var version_0 = await sut.GetSnapshotAsync(0);
var version_1 = await sut.GetSnapshotAsync(1);
Assert.Empty(sut.GetUncomittedEvents());
AssertSnapshot(version_Empty, 0);
AssertSnapshot(version_0, 3);
AssertSnapshot(version_1, 4);
A.CallTo(() => persistenceFactory.WithEventSourcing(typeof(MyDomainObject), id, A<HandleEvent>._))
.MustHaveHappened();
}
private static void AssertSnapshot(MyDomainState state, int value, bool isDeleted = false)
{
Assert.Equal(new MyDomainState { Value = value, IsDeleted = isDeleted }, state);
}
private void SetupDeleted()
{
sut.ExecuteAsync(new Delete()).Wait();
}
private void SetupCreated(int value)
{
var handleEvent = new HandleEvent(_ => true);
var version = -1;
A.CallTo(() => persistence.ReadAsync(-2))
.Invokes(() =>
{
version = 0;
handleEvent(Envelope.Create(new ValueChanged { Value = value }));
});
A.CallTo(() => persistenceFactory.WithSnapshotsAndEventSourcing(typeof(MyDomainObject), id, A<HandleSnapshot<MyDomainState>>._, A<HandleEvent>._))
.Invokes(args =>
{
handleEvent = args.GetArgument<HandleEvent>(3)!;
})
.Returns(persistence);
A.CallTo(() => persistence.Version)
.ReturnsLazily(() => version);
sut.Setup(id);
}
private void SetupLoaded()
{
var handleEvent = new HandleEvent(_ => true);
var @events = new List<Envelope<IEvent>>();
A.CallTo(() => persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._))
.Invokes(c => @events.AddRange(c.GetArgument<IReadOnlyList<Envelope<IEvent>>>(0)!));
var eventsPersistence = A.Fake<IPersistence<MyDomainState>>();
A.CallTo(() => persistenceFactory.WithEventSourcing(typeof(MyDomainObject), id, A<HandleEvent>._))
.Invokes(args =>
{
handleEvent = args.GetArgument<HandleEvent>(2)!;
})
.Returns(eventsPersistence);
A.CallTo(() => eventsPersistence.ReadAsync(EtagVersion.Any))
.Invokes(_ =>
{
foreach (var @event in events)
{
handleEvent(@event);
}
});
}
private void SetupEmpty()
{
A.CallTo(() => persistenceFactory.WithSnapshotsAndEventSourcing(typeof(MyDomainObject), id, A<HandleSnapshot<MyDomainState>>._, A<HandleEvent>._))
.Returns(persistence);
A.CallTo(() => persistence.Version)
.Returns(-1);
sut.Setup(id);
}
}
}