mirror of https://github.com/Squidex/squidex.git
31 changed files with 460 additions and 342 deletions
@ -0,0 +1,17 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ResetConsumerMessage.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure.Actors; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages |
||||
|
{ |
||||
|
[TypeName(nameof(ResetConsumerMessage))] |
||||
|
public sealed class ResetConsumerMessage : IMessage |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -1,9 +0,0 @@ |
|||||
using Squidex.Infrastructure.Actors; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages |
|
||||
{ |
|
||||
[TypeName(nameof(ResetReceiverMessage))] |
|
||||
public sealed class ResetReceiverMessage : IMessage |
|
||||
{ |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,17 @@ |
|||||
|
// ==========================================================================
|
||||
|
// StartConsumerMessage.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure.Actors; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages |
||||
|
{ |
||||
|
[TypeName(nameof(StartConsumerMessage))] |
||||
|
public sealed class StartConsumerMessage : IMessage |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -1,9 +0,0 @@ |
|||||
using Squidex.Infrastructure.Actors; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages |
|
||||
{ |
|
||||
[TypeName(nameof(StartReceiverMessage))] |
|
||||
public sealed class StartReceiverMessage : IMessage |
|
||||
{ |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,19 @@ |
|||||
|
// ==========================================================================
|
||||
|
// StopConsumerMessage.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure.Actors; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages |
||||
|
{ |
||||
|
[TypeName(nameof(StopConsumerMessage))] |
||||
|
public sealed class StopConsumerMessage : IMessage |
||||
|
{ |
||||
|
public Exception Exception { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -1,11 +0,0 @@ |
|||||
using System; |
|
||||
using Squidex.Infrastructure.Actors; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages |
|
||||
{ |
|
||||
[TypeName(nameof(StopReceiverMessage))] |
|
||||
public sealed class StopReceiverMessage : IMessage |
|
||||
{ |
|
||||
public Exception Exception { get; set; } |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,82 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ActorRemoteTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using FluentAssertions; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Actors |
||||
|
{ |
||||
|
public class ActorRemoteTests |
||||
|
{ |
||||
|
[TypeName(nameof(SuccessMessage))] |
||||
|
public class SuccessMessage : IMessage |
||||
|
{ |
||||
|
public int Counter { get; set; } |
||||
|
} |
||||
|
|
||||
|
private sealed class MyActor : Actor |
||||
|
{ |
||||
|
public List<IMessage> Invokes { get; } = new List<IMessage>(); |
||||
|
|
||||
|
protected override Task OnMessage(IMessage message) |
||||
|
{ |
||||
|
Invokes.Add(message); |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private readonly MyActor actor = new MyActor(); |
||||
|
private readonly TypeNameRegistry registry = new TypeNameRegistry(); |
||||
|
private readonly RemoteActors actors; |
||||
|
private readonly IActor remoteActor; |
||||
|
|
||||
|
public ActorRemoteTests() |
||||
|
{ |
||||
|
registry.Map(typeof(SuccessMessage)); |
||||
|
|
||||
|
actors = new RemoteActors(new DefaultRemoteActorChannel(new InMemoryPubSub(), registry)); |
||||
|
actors.Connect("my", actor); |
||||
|
|
||||
|
remoteActor = actors.Get("my"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_when_stopping_remote_actor() |
||||
|
{ |
||||
|
Assert.Throws<NotSupportedException>(() => remoteActor.StopAsync().Forget()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_when_sending_exception_to_remote_actor() |
||||
|
{ |
||||
|
Assert.Throws<NotSupportedException>(() => remoteActor.SendAsync(new InvalidOperationException()).Forget()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_handle_messages_sequentially() |
||||
|
{ |
||||
|
remoteActor.SendAsync(new SuccessMessage { Counter = 1 }).Forget(); |
||||
|
remoteActor.SendAsync(new SuccessMessage { Counter = 2 }).Forget(); |
||||
|
remoteActor.SendAsync(new SuccessMessage { Counter = 3 }).Forget(); |
||||
|
|
||||
|
await actor.StopAsync(); |
||||
|
|
||||
|
actor.Invokes.ShouldBeEquivalentTo(new List<object> |
||||
|
{ |
||||
|
new SuccessMessage { Counter = 1 }, |
||||
|
new SuccessMessage { Counter = 2 }, |
||||
|
new SuccessMessage { Counter = 3 } |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,130 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ActorTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using FluentAssertions; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Actors |
||||
|
{ |
||||
|
public class ActorTests |
||||
|
{ |
||||
|
public class SuccessMessage : IMessage |
||||
|
{ |
||||
|
public int Counter { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class FailedMessage : IMessage |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
private sealed class MyActor : Actor |
||||
|
{ |
||||
|
public List<object> Invokes { get; } = new List<object>(); |
||||
|
|
||||
|
protected override Task OnStop() |
||||
|
{ |
||||
|
Invokes.Add(true); |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
} |
||||
|
|
||||
|
protected override Task OnError(Exception exception) |
||||
|
{ |
||||
|
Invokes.Add(exception); |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
} |
||||
|
|
||||
|
protected override Task OnMessage(IMessage message) |
||||
|
{ |
||||
|
if (message is FailedMessage) |
||||
|
{ |
||||
|
throw new InvalidOperationException(); |
||||
|
} |
||||
|
|
||||
|
Invokes.Add(message); |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private readonly MyActor sut = new MyActor(); |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_invoke_with_exception() |
||||
|
{ |
||||
|
sut.SendAsync(new InvalidOperationException()).Forget(); |
||||
|
|
||||
|
await sut.StopAsync(); |
||||
|
|
||||
|
Assert.True(sut.Invokes[0] is InvalidOperationException); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_handle_messages_sequentially() |
||||
|
{ |
||||
|
sut.SendAsync(new SuccessMessage { Counter = 1 }).Forget(); |
||||
|
sut.SendAsync(new SuccessMessage { Counter = 2 }).Forget(); |
||||
|
sut.SendAsync(new SuccessMessage { Counter = 3 }).Forget(); |
||||
|
|
||||
|
await sut.StopAsync(); |
||||
|
|
||||
|
sut.Invokes.ShouldBeEquivalentTo(new List<object> |
||||
|
{ |
||||
|
new SuccessMessage { Counter = 1 }, |
||||
|
new SuccessMessage { Counter = 2 }, |
||||
|
new SuccessMessage { Counter = 3 }, |
||||
|
true |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_raise_error_event_when_event_handling_failed() |
||||
|
{ |
||||
|
sut.SendAsync(new FailedMessage()).Forget(); |
||||
|
sut.SendAsync(new SuccessMessage { Counter = 2 }).Forget(); |
||||
|
sut.SendAsync(new SuccessMessage { Counter = 3 }).Forget(); |
||||
|
|
||||
|
await sut.StopAsync(); |
||||
|
|
||||
|
Assert.True(sut.Invokes[0] is InvalidOperationException); |
||||
|
|
||||
|
sut.Invokes.Skip(1).ShouldBeEquivalentTo(new List<object> |
||||
|
{ |
||||
|
new SuccessMessage { Counter = 2 }, |
||||
|
new SuccessMessage { Counter = 3 }, |
||||
|
true |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_handle_messages_after_stop() |
||||
|
{ |
||||
|
sut.SendAsync(new SuccessMessage { Counter = 1 }).Forget(); |
||||
|
|
||||
|
sut.StopAsync().Forget(); |
||||
|
|
||||
|
sut.SendAsync(new SuccessMessage { Counter = 2 }).Forget(); |
||||
|
sut.SendAsync(new SuccessMessage { Counter = 3 }).Forget(); |
||||
|
sut.SendAsync(new InvalidOperationException()).Forget(); |
||||
|
|
||||
|
await sut.StopAsync(); |
||||
|
|
||||
|
sut.Invokes.ShouldBeEquivalentTo(new List<object> |
||||
|
{ |
||||
|
new SuccessMessage { Counter = 1 }, |
||||
|
true |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,216 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// EventReceiverTests.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Threading.Tasks; |
|
||||
using FakeItEasy; |
|
||||
using Squidex.Infrastructure.Log; |
|
||||
using Squidex.Infrastructure.Tasks; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.CQRS.Events |
|
||||
{ |
|
||||
public class EventReceiverTests |
|
||||
{ |
|
||||
public sealed class MyEvent : IEvent |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
private sealed class MyEventConsumerInfo : IEventConsumerInfo |
|
||||
{ |
|
||||
public bool IsStopped { get; set; } |
|
||||
public bool IsResetting { get; set; } |
|
||||
public string Name { get; set; } |
|
||||
public string Error { get; set; } |
|
||||
public string Position { get; set; } |
|
||||
} |
|
||||
|
|
||||
private sealed class MyEventSubscription : IEventSubscription |
|
||||
{ |
|
||||
private readonly IEnumerable<StoredEvent> storedEvents; |
|
||||
private bool isDisposed; |
|
||||
|
|
||||
public MyEventSubscription(IEnumerable<StoredEvent> storedEvents) |
|
||||
{ |
|
||||
this.storedEvents = storedEvents; |
|
||||
} |
|
||||
|
|
||||
public async Task SubscribeAsync(Func<StoredEvent, Task> onNext, Func<Exception, Task> onError) |
|
||||
{ |
|
||||
foreach (var storedEvent in storedEvents) |
|
||||
{ |
|
||||
if (isDisposed) |
|
||||
{ |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
await onNext(storedEvent); |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
await onError(ex); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
isDisposed = true; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private sealed class MyEventStore : IEventStore |
|
||||
{ |
|
||||
private readonly IEnumerable<StoredEvent> storedEvents; |
|
||||
|
|
||||
public MyEventStore(IEnumerable<StoredEvent> storedEvents) |
|
||||
{ |
|
||||
this.storedEvents = storedEvents; |
|
||||
} |
|
||||
|
|
||||
public IEventSubscription CreateSubscription(string streamFilter = null, string position = null) |
|
||||
{ |
|
||||
return new MyEventSubscription(storedEvents); |
|
||||
} |
|
||||
|
|
||||
public Task<IReadOnlyList<StoredEvent>> GetEventsAsync(string streamName) |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
|
|
||||
public Task AppendEventsAsync(Guid commitId, string streamName, ICollection<EventData> events) |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
|
|
||||
public Task AppendEventsAsync(Guid commitId, string streamName, int expectedVersion, ICollection<EventData> events) |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private readonly IEventConsumerInfoRepository eventConsumerInfoRepository = A.Fake<IEventConsumerInfoRepository>(); |
|
||||
private readonly IEventConsumer eventConsumer = A.Fake<IEventConsumer>(); |
|
||||
private readonly ISemanticLog log = A.Fake<ISemanticLog>(); |
|
||||
private readonly EventDataFormatter formatter = A.Fake<EventDataFormatter>(); |
|
||||
private readonly EventData eventData1 = new EventData(); |
|
||||
private readonly EventData eventData2 = new EventData(); |
|
||||
private readonly EventData eventData3 = new EventData(); |
|
||||
private readonly Envelope<IEvent> envelope1 = new Envelope<IEvent>(new MyEvent()); |
|
||||
private readonly Envelope<IEvent> envelope2 = new Envelope<IEvent>(new MyEvent()); |
|
||||
private readonly Envelope<IEvent> envelope3 = new Envelope<IEvent>(new MyEvent()); |
|
||||
private readonly EventReceiver sut; |
|
||||
private readonly MyEventConsumerInfo consumerInfo = new MyEventConsumerInfo(); |
|
||||
private readonly string consumerName; |
|
||||
|
|
||||
public EventReceiverTests() |
|
||||
{ |
|
||||
var events = new[] |
|
||||
{ |
|
||||
new StoredEvent("3", 3, eventData1), |
|
||||
new StoredEvent("4", 4, eventData2), |
|
||||
new StoredEvent("5", 5, eventData3) |
|
||||
}; |
|
||||
|
|
||||
consumerName = eventConsumer.GetType().Name; |
|
||||
|
|
||||
var eventStore = new MyEventStore(events); |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.Name).Returns(consumerName); |
|
||||
A.CallTo(() => eventConsumerInfoRepository.FindAsync(consumerName)).Returns(consumerInfo); |
|
||||
|
|
||||
A.CallTo(() => formatter.Parse(eventData1, true)).Returns(envelope1); |
|
||||
A.CallTo(() => formatter.Parse(eventData2, true)).Returns(envelope2); |
|
||||
A.CallTo(() => formatter.Parse(eventData3, true)).Returns(envelope3); |
|
||||
|
|
||||
sut = new EventReceiver(formatter, eventStore, eventConsumerInfoRepository, log); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_only_connect_once() |
|
||||
{ |
|
||||
sut.Subscribe(eventConsumer); |
|
||||
sut.Subscribe(eventConsumer); |
|
||||
sut.Refresh(); |
|
||||
sut.Dispose(); |
|
||||
|
|
||||
A.CallTo(() => eventConsumerInfoRepository.CreateAsync(consumerName)).MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_subscribe_to_consumer_and_handle_events() |
|
||||
{ |
|
||||
consumerInfo.Position = "2"; |
|
||||
|
|
||||
sut.Subscribe(eventConsumer); |
|
||||
sut.Refresh(); |
|
||||
sut.Dispose(); |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.On(envelope1)).MustHaveHappened(); |
|
||||
A.CallTo(() => eventConsumer.On(envelope2)).MustHaveHappened(); |
|
||||
A.CallTo(() => eventConsumer.On(envelope3)).MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_abort_if_handling_failed() |
|
||||
{ |
|
||||
consumerInfo.Position = "2"; |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.On(envelope1)).Returns(TaskHelper.True); |
|
||||
A.CallTo(() => eventConsumer.On(envelope2)).Throws(new InvalidOperationException()); |
|
||||
|
|
||||
sut.Subscribe(eventConsumer); |
|
||||
sut.Refresh(); |
|
||||
sut.Dispose(); |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.On(envelope1)).MustHaveHappened(); |
|
||||
A.CallTo(() => eventConsumer.On(envelope2)).MustHaveHappened(); |
|
||||
A.CallTo(() => eventConsumer.On(envelope3)).MustNotHaveHappened(); |
|
||||
|
|
||||
A.CallTo(() => eventConsumerInfoRepository.StopAsync(consumerName, A<string>.Ignored)).MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_abort_if_serialization_failed() |
|
||||
{ |
|
||||
consumerInfo.Position = "2"; |
|
||||
|
|
||||
A.CallTo(() => formatter.Parse(eventData2, true)).Throws(new InvalidOperationException()); |
|
||||
|
|
||||
sut.Subscribe(eventConsumer); |
|
||||
sut.Refresh(); |
|
||||
sut.Dispose(); |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.On(envelope1)).MustHaveHappened(); |
|
||||
A.CallTo(() => eventConsumer.On(envelope2)).MustNotHaveHappened(); |
|
||||
A.CallTo(() => eventConsumer.On(envelope3)).MustNotHaveHappened(); |
|
||||
|
|
||||
A.CallTo(() => eventConsumerInfoRepository.StopAsync(consumerName, A<string>.Ignored)).MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_reset_if_requested() |
|
||||
{ |
|
||||
consumerInfo.IsResetting = true; |
|
||||
consumerInfo.Position = "2"; |
|
||||
|
|
||||
sut.Subscribe(eventConsumer); |
|
||||
sut.Refresh(); |
|
||||
sut.Dispose(); |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.On(envelope1)).MustHaveHappened(); |
|
||||
A.CallTo(() => eventConsumer.On(envelope2)).MustHaveHappened(); |
|
||||
A.CallTo(() => eventConsumer.On(envelope3)).MustHaveHappened(); |
|
||||
|
|
||||
A.CallTo(() => eventConsumer.ClearAsync()).MustHaveHappened(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue