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