mirror of https://github.com/Squidex/squidex.git
28 changed files with 974 additions and 379 deletions
@ -1,41 +0,0 @@ |
|||
// ==========================================================================
|
|||
// MongoRuleEntity.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using MongoDB.Bson.Serialization.Attributes; |
|||
using Squidex.Domain.Apps.Core.Rules; |
|||
using Squidex.Domain.Apps.Read.Rules; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.MongoDb; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.MongoDb.Rules |
|||
{ |
|||
public class MongoRuleEntity : MongoEntity, IRuleEntity |
|||
{ |
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public Guid AppId { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public RefToken CreatedBy { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public RefToken LastModifiedBy { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public long Version { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
[BsonJson] |
|||
public Rule Rule { get; set; } |
|||
} |
|||
} |
|||
@ -1,90 +0,0 @@ |
|||
// ==========================================================================
|
|||
// MongoRuleRepository.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using MongoDB.Bson; |
|||
using MongoDB.Driver; |
|||
using Squidex.Domain.Apps.Read.Rules; |
|||
using Squidex.Domain.Apps.Read.Rules.Repositories; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.CQRS.Events; |
|||
using Squidex.Infrastructure.MongoDb; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.MongoDb.Rules |
|||
{ |
|||
public partial class MongoRuleRepository : MongoRepositoryBase<MongoRuleEntity>, IRuleRepository, IEventConsumer |
|||
{ |
|||
private static readonly List<IRuleEntity> EmptyRules = new List<IRuleEntity>(); |
|||
private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); |
|||
private Dictionary<Guid, List<IRuleEntity>> inMemoryRules; |
|||
|
|||
public MongoRuleRepository(IMongoDatabase database) |
|||
: base(database) |
|||
{ |
|||
} |
|||
|
|||
protected override string CollectionName() |
|||
{ |
|||
return "Projections_Rules"; |
|||
} |
|||
|
|||
protected override Task SetupCollectionAsync(IMongoCollection<MongoRuleEntity> collection) |
|||
{ |
|||
return Task.WhenAll(collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId))); |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IRuleEntity>> QueryByAppAsync(Guid appId) |
|||
{ |
|||
var entities = |
|||
await Collection.Find(x => x.AppId == appId) |
|||
.ToListAsync(); |
|||
|
|||
return entities.OfType<IRuleEntity>().ToList(); |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IRuleEntity>> QueryCachedByAppAsync(Guid appId) |
|||
{ |
|||
await EnsureRulesLoadedAsync(); |
|||
|
|||
return inMemoryRules.GetOrDefault(appId)?.ToList() ?? EmptyRules; |
|||
} |
|||
|
|||
private async Task EnsureRulesLoadedAsync() |
|||
{ |
|||
if (inMemoryRules == null) |
|||
{ |
|||
try |
|||
{ |
|||
await lockObject.WaitAsync(); |
|||
|
|||
if (inMemoryRules == null) |
|||
{ |
|||
inMemoryRules = new Dictionary<Guid, List<IRuleEntity>>(); |
|||
|
|||
var webhooks = |
|||
await Collection.Find(new BsonDocument()) |
|||
.ToListAsync(); |
|||
|
|||
foreach (var webhook in webhooks) |
|||
{ |
|||
inMemoryRules.GetOrAddNew(webhook.AppId).Add(webhook); |
|||
} |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
lockObject.Release(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,97 +0,0 @@ |
|||
// ==========================================================================
|
|||
// MongoRuleRepository_EventHandling.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using MongoDB.Driver; |
|||
using Squidex.Domain.Apps.Events.Rules; |
|||
using Squidex.Domain.Apps.Events.Rules.Utils; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.CQRS.Events; |
|||
using Squidex.Infrastructure.Dispatching; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.MongoDb.Rules |
|||
{ |
|||
public partial class MongoRuleRepository |
|||
{ |
|||
public string Name |
|||
{ |
|||
get { return GetType().Name; } |
|||
} |
|||
|
|||
public string EventsFilter |
|||
{ |
|||
get { return "^rule-"; } |
|||
} |
|||
|
|||
public Task On(Envelope<IEvent> @event) |
|||
{ |
|||
return this.DispatchActionAsync(@event.Payload, @event.Headers); |
|||
} |
|||
|
|||
protected async Task On(RuleCreated @event, EnvelopeHeaders headers) |
|||
{ |
|||
await EnsureRulesLoadedAsync(); |
|||
|
|||
await Collection.CreateAsync(@event, headers, w => |
|||
{ |
|||
w.Rule = RuleEventDispatcher.Create(@event); |
|||
|
|||
inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); |
|||
inMemoryRules.GetOrAddNew(w.AppId).Add(w); |
|||
}); |
|||
} |
|||
|
|||
protected async Task On(RuleUpdated @event, EnvelopeHeaders headers) |
|||
{ |
|||
await EnsureRulesLoadedAsync(); |
|||
|
|||
await Collection.UpdateAsync(@event, headers, w => |
|||
{ |
|||
w.Rule.Apply(@event); |
|||
|
|||
inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); |
|||
inMemoryRules.GetOrAddNew(w.AppId).Add(w); |
|||
}); |
|||
} |
|||
|
|||
protected async Task On(RuleEnabled @event, EnvelopeHeaders headers) |
|||
{ |
|||
await EnsureRulesLoadedAsync(); |
|||
|
|||
await Collection.UpdateAsync(@event, headers, w => |
|||
{ |
|||
w.Rule.Apply(@event); |
|||
|
|||
inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); |
|||
inMemoryRules.GetOrAddNew(w.AppId).Add(w); |
|||
}); |
|||
} |
|||
|
|||
protected async Task On(RuleDisabled @event, EnvelopeHeaders headers) |
|||
{ |
|||
await EnsureRulesLoadedAsync(); |
|||
|
|||
await Collection.UpdateAsync(@event, headers, w => |
|||
{ |
|||
w.Rule.Apply(@event); |
|||
|
|||
inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); |
|||
inMemoryRules.GetOrAddNew(w.AppId).Add(w); |
|||
}); |
|||
} |
|||
|
|||
protected async Task On(RuleDeleted @event, EnvelopeHeaders headers) |
|||
{ |
|||
await EnsureRulesLoadedAsync(); |
|||
|
|||
inMemoryRules.GetOrAddNew(@event.AppId.Id).RemoveAll(x => x.Id == @event.RuleId); |
|||
|
|||
await Collection.DeleteManyAsync(x => x.Id == @event.RuleId); |
|||
} |
|||
} |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
// ==========================================================================
|
|||
// IRuleRepository.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.Rules.Repositories |
|||
{ |
|||
public interface IRuleRepository |
|||
{ |
|||
Task<IReadOnlyList<IRuleEntity>> QueryByAppAsync(Guid appId); |
|||
|
|||
Task<IReadOnlyList<IRuleEntity>> QueryCachedByAppAsync(Guid appId); |
|||
} |
|||
} |
|||
@ -1,41 +0,0 @@ |
|||
// ==========================================================================
|
|||
// PubSubServices.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.Extensions.Configuration; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Log; |
|||
using StackExchange.Redis; |
|||
|
|||
namespace Squidex.Config.Domain |
|||
{ |
|||
public static class PubSubServices |
|||
{ |
|||
public static void AddMyPubSubServices(this IServiceCollection services, IConfiguration config) |
|||
{ |
|||
config.ConfigureByOption("pubSub:type", new Options |
|||
{ |
|||
["InMemory"] = () => |
|||
{ |
|||
services.AddSingleton<InMemoryPubSub>() |
|||
.As<IPubSub>(); |
|||
}, |
|||
["Redis"] = () => |
|||
{ |
|||
var configuration = config.GetRequiredValue("pubsub:redis:configuration"); |
|||
|
|||
var redis = Singletons<IConnectionMultiplexer>.GetOrAddLazy(configuration, s => ConnectionMultiplexer.Connect(s)); |
|||
|
|||
services.AddSingleton(c => new RedisPubSub(redis, c.GetRequiredService<ISemanticLog>())) |
|||
.As<IPubSub>() |
|||
.As<IExternalSystem>(); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
// ==========================================================================
|
|||
// EventConsumerBootstrapTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Orleans; |
|||
using Orleans.Providers; |
|||
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Infrastructure.CQRS.Events.Grains |
|||
{ |
|||
public sealed class EventConsumerBootstrapTests |
|||
{ |
|||
private readonly IEventConsumerRegistryGrain registry = A.Fake<IEventConsumerRegistryGrain>(); |
|||
private readonly IProviderRuntime runtime = A.Fake<IProviderRuntime>(); |
|||
private readonly EventConsumerBootstrap sut = new EventConsumerBootstrap(); |
|||
|
|||
public EventConsumerBootstrapTests() |
|||
{ |
|||
var factory = A.Fake<IGrainFactory>(); |
|||
|
|||
A.CallTo(() => factory.GetGrain<IEventConsumerRegistryGrain>("Default", null)) |
|||
.Returns(registry); |
|||
|
|||
A.CallTo(() => runtime.GrainFactory) |
|||
.Returns(factory); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_do_nothing_on_close() |
|||
{ |
|||
await sut.Close(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_set_name_on_init() |
|||
{ |
|||
await sut.Init("MyName", runtime, null); |
|||
|
|||
Assert.Equal("MyName", sut.Name); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_activate_registry_on_init() |
|||
{ |
|||
await sut.Init("MyName", runtime, null); |
|||
|
|||
A.CallTo(() => registry.ActivateAsync(null)) |
|||
.MustHaveHappened(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,404 @@ |
|||
// ==========================================================================
|
|||
// EventConsumerGrainTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Orleans.Core; |
|||
using Orleans.Runtime; |
|||
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation; |
|||
using Squidex.Infrastructure.Log; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Infrastructure.CQRS.Events.Grains |
|||
{ |
|||
public class EventConsumerGrainTests |
|||
{ |
|||
public sealed class MyEvent : IEvent |
|||
{ |
|||
} |
|||
|
|||
public sealed class MyEventConsumerActor : EventConsumerGrain |
|||
{ |
|||
public MyEventConsumerActor( |
|||
EventDataFormatter formatter, |
|||
EventConsumerFactory eventConsumerFactory, |
|||
IEventStore eventStore, |
|||
ISemanticLog log, |
|||
IGrainIdentity identity, |
|||
IGrainRuntime runtime, |
|||
IStorage<EventConsumerGrainState> storage) |
|||
: base(formatter, eventConsumerFactory, eventStore, log, identity, runtime, storage) |
|||
{ |
|||
} |
|||
|
|||
protected override IEventSubscription CreateSubscription(IEventStore eventStore, string streamFilter, string position) |
|||
{ |
|||
return eventStore.CreateSubscription(this, streamFilter, position); |
|||
} |
|||
} |
|||
|
|||
private readonly IEventConsumer eventConsumer = A.Fake<IEventConsumer>(); |
|||
private readonly IEventStore eventStore = A.Fake<IEventStore>(); |
|||
private readonly IEventSubscription eventSubscription = A.Fake<IEventSubscription>(); |
|||
private readonly ISemanticLog log = A.Fake<ISemanticLog>(); |
|||
private readonly IEventSubscriber sutSubscriber; |
|||
private readonly IStorage<EventConsumerGrainState> storage = A.Fake<IStorage<EventConsumerGrainState>>(); |
|||
private readonly EventDataFormatter formatter = A.Fake<EventDataFormatter>(); |
|||
private readonly EventData eventData = new EventData(); |
|||
private readonly Envelope<IEvent> envelope = new Envelope<IEvent>(new MyEvent()); |
|||
private readonly EventConsumerFactory factory; |
|||
private readonly MyEventConsumerActor sut; |
|||
private readonly string consumerName; |
|||
private EventConsumerGrainState state = new EventConsumerGrainState(); |
|||
|
|||
public EventConsumerGrainTests() |
|||
{ |
|||
factory = new EventConsumerFactory(x => eventConsumer); |
|||
|
|||
state.Position = Guid.NewGuid().ToString(); |
|||
consumerName = eventConsumer.GetType().Name; |
|||
|
|||
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored)).Returns(eventSubscription); |
|||
A.CallTo(() => eventConsumer.Name).Returns(consumerName); |
|||
|
|||
A.CallTo(() => formatter.Parse(eventData, true)).Returns(envelope); |
|||
|
|||
A.CallTo(() => storage.State).ReturnsLazily(() => state); |
|||
A.CallToSet(() => storage.State).Invokes(new Action<EventConsumerGrainState>(s => state = s)); |
|||
|
|||
sut = new MyEventConsumerActor( |
|||
formatter, |
|||
factory, |
|||
eventStore, |
|||
log, |
|||
A.Fake<IGrainIdentity>(), |
|||
A.Fake<IGrainRuntime>(), |
|||
storage); |
|||
|
|||
sutSubscriber = sut; |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_subscribe_to_event_store_when_stopped_in_db() |
|||
{ |
|||
state.IsStopped = true; |
|||
|
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
|
|||
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_subscribe_to_event_store_when_not_stopped_in_db() |
|||
{ |
|||
state.Position = "123"; |
|||
|
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
|
|||
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, "123")) |
|||
.MustHaveHappened(Repeated.Exactly.Once); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_stop_subscription_when_stopped() |
|||
{ |
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
await sut.StopAsync(); |
|||
await sut.StopAsync(); |
|||
|
|||
A.CallTo(() => eventSubscription.StopAsync()) |
|||
.MustHaveHappened(Repeated.Exactly.Once); |
|||
|
|||
Assert.True(state.IsStopped); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_reset_consumer_when_resetting() |
|||
{ |
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
await sut.StopAsync(); |
|||
await sut.ResetAsync(); |
|||
|
|||
A.CallTo(() => eventConsumer.ClearAsync()) |
|||
.MustHaveHappened(Repeated.Exactly.Once); |
|||
|
|||
A.CallTo(() => eventSubscription.StopAsync()) |
|||
.MustHaveHappened(Repeated.Exactly.Once); |
|||
|
|||
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, state.Position)) |
|||
.MustHaveHappened(Repeated.Exactly.Once); |
|||
|
|||
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, null)) |
|||
.MustHaveHappened(Repeated.Exactly.Once); |
|||
|
|||
Assert.False(state.IsStopped); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_unsubscribe_from_subscription_when_closed() |
|||
{ |
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
|
|||
await OnClosedAsync(eventSubscription); |
|||
|
|||
A.CallTo(() => eventSubscription.StopAsync()) |
|||
.MustHaveHappened(); |
|||
|
|||
Assert.False(state.IsStopped); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_unsubscribe_from_subscription_when_closed_call_is_from_another_subscription() |
|||
{ |
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
|
|||
await OnClosedAsync(A.Fake<IEventSubscription>()); |
|||
|
|||
A.CallTo(() => eventSubscription.StopAsync()) |
|||
.MustNotHaveHappened(); |
|||
|
|||
Assert.False(state.IsStopped); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_unsubscribe_from_subscription_when_not_running() |
|||
{ |
|||
state.IsStopped = true; |
|||
|
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
|
|||
await OnClosedAsync(A.Fake<IEventSubscription>()); |
|||
|
|||
A.CallTo(() => storage.WriteStateAsync()) |
|||
.MustNotHaveHappened(); |
|||
|
|||
Assert.True(state.IsStopped); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_invoke_and_update_position_when_event_received() |
|||
{ |
|||
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
|||
|
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
|
|||
await OnEventAsync(eventSubscription, @event); |
|||
|
|||
A.CallTo(() => eventConsumer.On(envelope)) |
|||
.MustHaveHappened(Repeated.Exactly.Once); |
|||
|
|||
Assert.Equal(@event.EventPosition, state.Position); |
|||
|
|||
var info = await sut.GetStateAsync(); |
|||
|
|||
Assert.Equal(@event.EventPosition, info.Value.Position); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_ignore_old_events() |
|||
{ |
|||
A.CallTo(() => formatter.Parse(eventData, true)) |
|||
.Throws(new TypeNameNotFoundException()); |
|||
|
|||
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
|||
|
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
|
|||
await OnEventAsync(eventSubscription, @event); |
|||
|
|||
A.CallTo(() => eventConsumer.On(envelope)) |
|||
.MustNotHaveHappened(); |
|||
|
|||
Assert.Equal(@event.EventPosition, state.Position); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_invoke_and_update_position_when_event_is_from_another_subscription() |
|||
{ |
|||
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
|||
|
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
|
|||
await OnEventAsync(A.Fake<IEventSubscription>(), @event); |
|||
|
|||
A.CallTo(() => eventConsumer.On(envelope)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_make_error_handling_when_exception_is_from_another_subscription() |
|||
{ |
|||
var ex = new InvalidOperationException(); |
|||
|
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
|
|||
await OnErrorAsync(A.Fake<IEventSubscription>(), ex); |
|||
|
|||
Assert.False(state.IsStopped); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_stop_if_subscription_failed() |
|||
{ |
|||
var ex = new InvalidOperationException(); |
|||
|
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
|
|||
await OnErrorAsync(eventSubscription, ex); |
|||
|
|||
A.CallTo(() => eventSubscription.StopAsync()) |
|||
.MustHaveHappened(Repeated.Exactly.Once); |
|||
|
|||
Assert.True(state.IsStopped); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_stop_if_subscription_failed_and_ignore_error_on_unsubscribe() |
|||
{ |
|||
A.CallTo(() => eventSubscription.StopAsync()) |
|||
.Throws(new InvalidOperationException()); |
|||
|
|||
var ex = new InvalidOperationException(); |
|||
|
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
|
|||
await OnErrorAsync(eventSubscription, ex); |
|||
|
|||
Assert.True(state.IsStopped); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_stop_if_resetting_failed() |
|||
{ |
|||
var ex = new InvalidOperationException(); |
|||
|
|||
A.CallTo(() => eventConsumer.ClearAsync()) |
|||
.Throws(ex); |
|||
|
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
|
|||
await sut.ResetAsync(); |
|||
|
|||
A.CallTo(() => eventSubscription.StopAsync()) |
|||
.MustHaveHappened(Repeated.Exactly.Once); |
|||
|
|||
Assert.True(state.IsStopped); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_stop_if_handling_failed() |
|||
{ |
|||
var ex = new InvalidOperationException(); |
|||
|
|||
A.CallTo(() => eventConsumer.On(envelope)) |
|||
.Throws(ex); |
|||
|
|||
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
|||
|
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
|
|||
await OnEventAsync(eventSubscription, @event); |
|||
|
|||
A.CallTo(() => eventConsumer.On(envelope)) |
|||
.MustHaveHappened(); |
|||
|
|||
A.CallTo(() => eventSubscription.StopAsync()) |
|||
.MustHaveHappened(Repeated.Exactly.Once); |
|||
|
|||
Assert.True(state.IsStopped); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_stop_if_deserialization_failed() |
|||
{ |
|||
var ex = new InvalidOperationException(); |
|||
|
|||
A.CallTo(() => formatter.Parse(eventData, true)) |
|||
.Throws(ex); |
|||
|
|||
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
|||
|
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
|
|||
await OnEventAsync(eventSubscription, @event); |
|||
|
|||
A.CallTo(() => eventConsumer.On(envelope)) |
|||
.MustNotHaveHappened(); |
|||
|
|||
A.CallTo(() => eventSubscription.StopAsync()) |
|||
.MustHaveHappened(Repeated.Exactly.Once); |
|||
|
|||
Assert.True(state.IsStopped); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_start_after_stop_when_handling_failed() |
|||
{ |
|||
var exception = new InvalidOperationException(); |
|||
|
|||
A.CallTo(() => eventConsumer.On(envelope)) |
|||
.Throws(exception); |
|||
|
|||
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); |
|||
|
|||
await sut.OnActivateAsync(); |
|||
await sut.ActivateAsync(); |
|||
|
|||
await OnEventAsync(eventSubscription, @event); |
|||
|
|||
Assert.True(state.IsStopped); |
|||
|
|||
await sut.StartAsync(); |
|||
await sut.StartAsync(); |
|||
|
|||
A.CallTo(() => eventConsumer.On(envelope)) |
|||
.MustHaveHappened(); |
|||
|
|||
A.CallTo(() => eventSubscription.StopAsync()) |
|||
.MustHaveHappened(Repeated.Exactly.Once); |
|||
|
|||
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored)) |
|||
.MustHaveHappened(Repeated.Exactly.Twice); |
|||
|
|||
Assert.False(state.IsStopped); |
|||
} |
|||
|
|||
private Task OnErrorAsync(IEventSubscription subscriber, Exception ex) |
|||
{ |
|||
return sutSubscriber.OnErrorAsync(subscriber, ex); |
|||
} |
|||
|
|||
private Task OnEventAsync(IEventSubscription subscriber, StoredEvent ev) |
|||
{ |
|||
return sutSubscriber.OnEventAsync(subscriber, ev); |
|||
} |
|||
|
|||
private Task OnClosedAsync(IEventSubscription subscriber) |
|||
{ |
|||
return sutSubscriber.OnClosedAsync(subscriber); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,165 @@ |
|||
// ==========================================================================
|
|||
// EventConsumerRegistryGrainTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using FluentAssertions; |
|||
using Orleans; |
|||
using Orleans.Concurrency; |
|||
using Orleans.Core; |
|||
using Orleans.Runtime; |
|||
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains; |
|||
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Infrastructure.CQRS.Events.Grains |
|||
{ |
|||
public class EventConsumerRegistryGrainTests |
|||
{ |
|||
public class MyEventConsumerRegistryGrain : EventConsumerRegistryGrain |
|||
{ |
|||
public MyEventConsumerRegistryGrain( |
|||
IEnumerable<IEventConsumer> eventConsumers, |
|||
IGrainIdentity identity, |
|||
IGrainRuntime runtime) |
|||
: base(eventConsumers, identity, runtime) |
|||
{ |
|||
} |
|||
} |
|||
|
|||
private readonly IEventConsumer consumerA = A.Fake<IEventConsumer>(); |
|||
private readonly IEventConsumer consumerB = A.Fake<IEventConsumer>(); |
|||
private readonly IEventConsumerGrain grainA = A.Fake<IEventConsumerGrain>(); |
|||
private readonly IEventConsumerGrain grainB = A.Fake<IEventConsumerGrain>(); |
|||
private readonly MyEventConsumerRegistryGrain sut; |
|||
|
|||
public EventConsumerRegistryGrainTests() |
|||
{ |
|||
var grainRuntime = A.Fake<IGrainRuntime>(); |
|||
var grainFactory = A.Fake<IGrainFactory>(); |
|||
|
|||
A.CallTo(() => grainFactory.GetGrain<IEventConsumerGrain>("a", null)).Returns(grainA); |
|||
A.CallTo(() => grainFactory.GetGrain<IEventConsumerGrain>("b", null)).Returns(grainB); |
|||
A.CallTo(() => grainRuntime.GrainFactory).Returns(grainFactory); |
|||
|
|||
A.CallTo(() => consumerA.Name).Returns("a"); |
|||
A.CallTo(() => consumerA.EventsFilter).Returns("^a-"); |
|||
|
|||
A.CallTo(() => consumerB.Name).Returns("b"); |
|||
A.CallTo(() => consumerB.EventsFilter).Returns("^b-"); |
|||
|
|||
sut = new MyEventConsumerRegistryGrain(new[] { consumerA, consumerB }, A.Fake<IGrainIdentity>(), grainRuntime); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_activate_all_grains_on_activate() |
|||
{ |
|||
await sut.OnActivateAsync(); |
|||
|
|||
A.CallTo(() => grainA.ActivateAsync()) |
|||
.MustNotHaveHappened(); |
|||
|
|||
A.CallTo(() => grainB.ActivateAsync()) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_activate_all_grains_on_reminder() |
|||
{ |
|||
await sut.ReceiveReminder(null, default(TickStatus)); |
|||
|
|||
A.CallTo(() => grainA.ActivateAsync()) |
|||
.MustHaveHappened(); |
|||
|
|||
A.CallTo(() => grainB.ActivateAsync()) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_activate_all_grains_on_activate_with_null() |
|||
{ |
|||
await sut.ActivateAsync(null); |
|||
|
|||
A.CallTo(() => grainA.ActivateAsync()) |
|||
.MustHaveHappened(); |
|||
|
|||
A.CallTo(() => grainB.ActivateAsync()) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_activate_matching_grains_when_stream_name_defined() |
|||
{ |
|||
await sut.ActivateAsync("a-123"); |
|||
|
|||
A.CallTo(() => grainA.ActivateAsync()) |
|||
.MustHaveHappened(); |
|||
|
|||
A.CallTo(() => grainB.ActivateAsync()) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_start_matching_grain() |
|||
{ |
|||
await sut.StartAsync("a"); |
|||
|
|||
A.CallTo(() => grainA.StartAsync()) |
|||
.MustHaveHappened(); |
|||
|
|||
A.CallTo(() => grainB.StartAsync()) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_stop_matching_grain() |
|||
{ |
|||
await sut.StopAsync("b"); |
|||
|
|||
A.CallTo(() => grainA.StopAsync()) |
|||
.MustNotHaveHappened(); |
|||
|
|||
A.CallTo(() => grainB.StopAsync()) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_reset_matching_grain() |
|||
{ |
|||
await sut.ResetAsync("b"); |
|||
|
|||
A.CallTo(() => grainA.ResetAsync()) |
|||
.MustNotHaveHappened(); |
|||
|
|||
A.CallTo(() => grainB.ResetAsync()) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_fetch_infos_from_all_grains() |
|||
{ |
|||
A.CallTo(() => grainA.GetStateAsync()) |
|||
.Returns(new Immutable<EventConsumerInfo>( |
|||
new EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" })); |
|||
|
|||
A.CallTo(() => grainB.GetStateAsync()) |
|||
.Returns(new Immutable<EventConsumerInfo>( |
|||
new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" })); |
|||
|
|||
var infos = await sut.GetConsumersAsync(); |
|||
|
|||
infos.Value.ShouldBeEquivalentTo( |
|||
new List<EventConsumerInfo> |
|||
{ |
|||
new EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" }, |
|||
new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" } |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
// ==========================================================================
|
|||
// OrleansEventNotifierTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using FakeItEasy; |
|||
using Orleans; |
|||
using Squidex.Infrastructure.CQRS.Events.Orleans; |
|||
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Infrastructure.CQRS.Events.Grains |
|||
{ |
|||
public class OrleansEventNotifierTests |
|||
{ |
|||
private readonly IEventConsumerRegistryGrain registry = A.Fake<IEventConsumerRegistryGrain>(); |
|||
private readonly OrleansEventNotifier sut; |
|||
|
|||
public OrleansEventNotifierTests() |
|||
{ |
|||
var factory = A.Fake<IGrainFactory>(); |
|||
|
|||
A.CallTo(() => factory.GetGrain<IEventConsumerRegistryGrain>("Default", null)) |
|||
.Returns(registry); |
|||
|
|||
sut = new OrleansEventNotifier(factory); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_activate_registry_with_stream_name() |
|||
{ |
|||
sut.NotifyEventsStored("my-stream"); |
|||
|
|||
A.CallTo(() => registry.ActivateAsync("my-stream")) |
|||
.MustHaveHappened(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,132 @@ |
|||
// ==========================================================================
|
|||
// JsonExternalSerializerTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using FakeItEasy; |
|||
using Newtonsoft.Json; |
|||
using Orleans.Serialization; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Infrastructure.Json.Orleans |
|||
{ |
|||
public class JsonExternalSerializerTests |
|||
{ |
|||
/* |
|||
class Context : ISerializationContext |
|||
{ |
|||
public IBinaryTokenStreamWriter StreamWriter => throw new NotImplementedException(); |
|||
|
|||
public int CurrentOffset => throw new NotImplementedException(); |
|||
|
|||
public IServiceProvider ServiceProvider => throw new NotImplementedException(); |
|||
|
|||
public object AdditionalContext => throw new NotImplementedException(); |
|||
|
|||
public int CheckObjectWhileSerializing(object raw) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
public void RecordObject(object original, int offset) |
|||
{ |
|||
} |
|||
|
|||
public void SerializeInner(object obj, Type expected) |
|||
{ |
|||
} |
|||
}*/ |
|||
|
|||
private readonly JsonExternalSerializer sut = new JsonExternalSerializer(JsonSerializer.CreateDefault()); |
|||
|
|||
public JsonExternalSerializerTests() |
|||
{ |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_serialize_js_only() |
|||
{ |
|||
Assert.True(sut.IsSupportedType(typeof(J<int>))); |
|||
Assert.True(sut.IsSupportedType(typeof(J<List<int>>))); |
|||
|
|||
Assert.False(sut.IsSupportedType(typeof(int))); |
|||
Assert.False(sut.IsSupportedType(typeof(List<int>))); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_copy_null() |
|||
{ |
|||
var value = (string)null; |
|||
var copy = sut.DeepCopy(value, null); |
|||
|
|||
Assert.Null(copy); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_copy_null_json() |
|||
{ |
|||
var value = new J<List<int>>(null); |
|||
var copy = (J<List<int>>)sut.DeepCopy(value, null); |
|||
|
|||
Assert.Null(copy.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_copy_immutable_values() |
|||
{ |
|||
var value = new J<List<int>>(new List<int> { 1, 2, 3 }, true); |
|||
var copy = (J<List<int>>)sut.DeepCopy(value, null); |
|||
|
|||
Assert.Same(value.Value, copy.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_copy_non_immutable_values() |
|||
{ |
|||
var value = new J<List<int>>(new List<int> { 1, 2, 3 }); |
|||
var copy = (J<List<int>>)sut.DeepCopy(value, null); |
|||
|
|||
Assert.Equal(value.Value, copy.Value); |
|||
Assert.NotSame(value.Value, copy.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_serialize_and_deserialize_value() |
|||
{ |
|||
var value = new J<List<int>>(new List<int> { 1, 2, 3 }); |
|||
|
|||
var writtenLength = 0; |
|||
var writtenBuffer = (byte[])null; |
|||
|
|||
var writer = A.Fake<IBinaryTokenStreamWriter>(); |
|||
var writerContext = new SerializationContext(null) { StreamWriter = writer }; |
|||
|
|||
A.CallTo(() => writer.Write(A<int>.Ignored)) |
|||
.Invokes(new Action<int>(x => writtenLength = x)); |
|||
|
|||
A.CallTo(() => writer.Write(A<byte[]>.Ignored)) |
|||
.Invokes(new Action<byte[]>(x => writtenBuffer = x)); |
|||
|
|||
sut.Serialize(value, writerContext, value.GetType()); |
|||
|
|||
var reader = A.Fake<IBinaryTokenStreamReader>(); |
|||
var readerContext = new DeserializationContext(null) { StreamReader = reader }; |
|||
|
|||
A.CallTo(() => reader.ReadInt()) |
|||
.Returns(writtenLength); |
|||
|
|||
A.CallTo(() => reader.ReadBytes(writtenLength)) |
|||
.Returns(writtenBuffer); |
|||
|
|||
var copy = (J<List<int>>)sut.Deserialize(value.GetType(), readerContext); |
|||
|
|||
Assert.Equal(value.Value, copy.Value); |
|||
Assert.NotSame(value.Value, copy.Value); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue