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