From 9c108f2a44169d04dd6ce8b1ed3d37b62ad76b64 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 14 Nov 2017 18:12:02 +0100 Subject: [PATCH] Tests for orleans stuff. --- .../Rules/MongoRuleEntity.cs | 41 -- .../Rules/MongoRuleRepository.cs | 90 ---- .../MongoRuleRepository_EventHandling.cs | 97 ----- .../Rules/Repositories/IRuleRepository.cs | 21 - .../Rules/RuleEnqueuer.cs | 13 +- .../Grains/Implementations/AppStateGrain.cs | 2 - .../Grains/Implementations/AppUserGrain.cs | 2 - .../State/Orleans/OrleansAppProvider.cs | 94 ++-- .../Implementations/XmlRepositoryGrain.cs | 5 +- .../MongoDb/JsonBsonConverter.cs | 19 +- .../Implementation/EventConsumerGrain.cs | 17 +- .../EventConsumerRegistryGrain.cs | 24 +- .../Json/Orleans/IJsonValue.cs | 2 + src/Squidex.Infrastructure/Json/Orleans/J.cs | 10 +- .../Json/Orleans/JsonExternalSerializer.cs | 19 +- src/Squidex/AppServices.cs | 1 - src/Squidex/Config/Domain/PubSubServices.cs | 41 -- src/Squidex/Config/Domain/StoreServices.cs | 5 - .../Controllers/Api/Rules/RulesController.cs | 11 +- src/Squidex/appsettings.json | 20 - tests/RunCoverage.ps1 | 2 +- .../CQRS/Events/CompoundEventConsumerTests.cs | 11 + .../Grains/EventConsumerBootstrapTests.cs | 58 +++ .../Events/Grains/EventConsumerGrainTests.cs | 404 ++++++++++++++++++ .../Grains/EventConsumerRegistryGrainTests.cs | 165 +++++++ .../Grains/OrleansEventNotifierTests.cs | 41 ++ .../Orleans/JsonExternalSerializerTests.cs | 132 ++++++ .../TestHelpers/JsonHelper.cs | 6 +- 28 files changed, 974 insertions(+), 379 deletions(-) delete mode 100644 src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs delete mode 100644 src/Squidex/Config/Domain/PubSubServices.cs create mode 100644 tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/EventConsumerBootstrapTests.cs create mode 100644 tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/EventConsumerGrainTests.cs create mode 100644 tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/EventConsumerRegistryGrainTests.cs create mode 100644 tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/OrleansEventNotifierTests.cs create mode 100644 tests/Squidex.Infrastructure.Tests/Json/Orleans/JsonExternalSerializerTests.cs diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs deleted file mode 100644 index be9369804..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs +++ /dev/null @@ -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; } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs deleted file mode 100644 index ab9ff0204..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs +++ /dev/null @@ -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, IRuleRepository, IEventConsumer - { - private static readonly List EmptyRules = new List(); - private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); - private Dictionary> inMemoryRules; - - public MongoRuleRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "Projections_Rules"; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection) - { - return Task.WhenAll(collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId))); - } - - public async Task> QueryByAppAsync(Guid appId) - { - var entities = - await Collection.Find(x => x.AppId == appId) - .ToListAsync(); - - return entities.OfType().ToList(); - } - - public async Task> 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>(); - - var webhooks = - await Collection.Find(new BsonDocument()) - .ToListAsync(); - - foreach (var webhook in webhooks) - { - inMemoryRules.GetOrAddNew(webhook.AppId).Add(webhook); - } - } - } - finally - { - lockObject.Release(); - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs deleted file mode 100644 index 26be2002a..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs +++ /dev/null @@ -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 @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); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs deleted file mode 100644 index 5f24b1a4e..000000000 --- a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs +++ /dev/null @@ -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> QueryByAppAsync(Guid appId); - - Task> QueryCachedByAppAsync(Guid appId); - } -} diff --git a/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs index 01870edb7..5629f61e7 100644 --- a/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs @@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Read.Rules public sealed class RuleEnqueuer : IEventConsumer { private readonly IRuleEventRepository ruleEventRepository; - private readonly IRuleRepository ruleRepository; + private readonly IAppProvider appProvider; private readonly RuleService ruleService; public string Name @@ -33,17 +33,18 @@ namespace Squidex.Domain.Apps.Read.Rules } public RuleEnqueuer( - IRuleEventRepository ruleEventRepository, - IRuleRepository ruleRepository, + IRuleEventRepository ruleEventRepository, IAppProvider appProvider, RuleService ruleService) { Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); - Guard.NotNull(ruleRepository, nameof(ruleRepository)); Guard.NotNull(ruleService, nameof(ruleService)); + Guard.NotNull(appProvider, nameof(appProvider)); + this.ruleEventRepository = ruleEventRepository; - this.ruleRepository = ruleRepository; this.ruleService = ruleService; + + this.appProvider = appProvider; } public Task ClearAsync() @@ -55,7 +56,7 @@ namespace Squidex.Domain.Apps.Read.Rules { if (@event.Payload is AppEvent appEvent) { - var rules = await ruleRepository.QueryCachedByAppAsync(appEvent.AppId.Id); + var rules = await appProvider.GetRulesAsync(appEvent.AppId.Name); foreach (var ruleEntity in rules) { diff --git a/src/Squidex.Domain.Apps.Read/State/Orleans/Grains/Implementations/AppStateGrain.cs b/src/Squidex.Domain.Apps.Read/State/Orleans/Grains/Implementations/AppStateGrain.cs index e7e910c87..ba964bfbb 100644 --- a/src/Squidex.Domain.Apps.Read/State/Orleans/Grains/Implementations/AppStateGrain.cs +++ b/src/Squidex.Domain.Apps.Read/State/Orleans/Grains/Implementations/AppStateGrain.cs @@ -10,7 +10,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Orleans; -using Orleans.Providers; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Rules; @@ -21,7 +20,6 @@ using Squidex.Infrastructure.Json.Orleans; namespace Squidex.Domain.Apps.Read.State.Orleans.Grains.Implementations { - [StorageProvider(ProviderName = "Default")] public sealed class AppStateGrain : Grain, IAppStateGrain { private readonly FieldRegistry fieldRegistry; diff --git a/src/Squidex.Domain.Apps.Read/State/Orleans/Grains/Implementations/AppUserGrain.cs b/src/Squidex.Domain.Apps.Read/State/Orleans/Grains/Implementations/AppUserGrain.cs index 10c0eff38..a474d8f6b 100644 --- a/src/Squidex.Domain.Apps.Read/State/Orleans/Grains/Implementations/AppUserGrain.cs +++ b/src/Squidex.Domain.Apps.Read/State/Orleans/Grains/Implementations/AppUserGrain.cs @@ -10,11 +10,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Orleans; -using Orleans.Providers; namespace Squidex.Domain.Apps.Read.State.Orleans.Grains.Implementations { - [StorageProvider(ProviderName = "Default")] public sealed class AppUserGrain : Grain, IAppUserGrain { public Task AddAppAsync(string appName) diff --git a/src/Squidex.Domain.Apps.Read/State/Orleans/OrleansAppProvider.cs b/src/Squidex.Domain.Apps.Read/State/Orleans/OrleansAppProvider.cs index 210fc614c..44ed20870 100644 --- a/src/Squidex.Domain.Apps.Read/State/Orleans/OrleansAppProvider.cs +++ b/src/Squidex.Domain.Apps.Read/State/Orleans/OrleansAppProvider.cs @@ -1,5 +1,5 @@ // ========================================================================== -// OrleansApps.cs +// OrleansAppProvider.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -16,74 +16,114 @@ using Squidex.Domain.Apps.Read.Rules; using Squidex.Domain.Apps.Read.Schemas; using Squidex.Domain.Apps.Read.State.Orleans.Grains; using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; namespace Squidex.Domain.Apps.Read.State.Orleans { public sealed class OrleansAppProvider : IAppProvider { private readonly IGrainFactory factory; + private readonly ISemanticLog log; - public OrleansAppProvider(IGrainFactory factory) + public OrleansAppProvider(IGrainFactory factory, ISemanticLog log) { Guard.NotNull(factory, nameof(factory)); + Guard.NotNull(log, nameof(log)); this.factory = factory; + + this.log = log; } public async Task GetAppAsync(string appName) { - var result = await factory.GetGrain(appName).GetAppAsync(); - - return result.Value; + using (log.MeasureTrace(w => w + .WriteProperty("module", nameof(OrleansAppProvider)) + .WriteProperty("method", nameof(GetAppAsync)))) + { + var result = await factory.GetGrain(appName).GetAppAsync(); + + return result.Value; + } } public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id) { - var result = await factory.GetGrain(appName).GetAppWithSchemaAsync(id); - - return result.Value; + using (log.MeasureTrace(w => w + .WriteProperty("module", nameof(OrleansAppProvider)) + .WriteProperty("method", nameof(GetAppWithSchemaAsync)))) + { + var result = await factory.GetGrain(appName).GetAppWithSchemaAsync(id); + + return result.Value; + } } public async Task> GetRulesAsync(string appName) { - var result = await factory.GetGrain(appName).GetRulesAsync(); - - return result.Value; + using (log.MeasureTrace(w => w + .WriteProperty("module", nameof(OrleansAppProvider)) + .WriteProperty("method", nameof(GetRulesAsync)))) + { + var result = await factory.GetGrain(appName).GetRulesAsync(); + + return result.Value; + } } public async Task GetSchemaAsync(string appName, Guid id, bool provideDeleted = false) { - var result = await factory.GetGrain(appName).GetSchemaAsync(id, provideDeleted); - - return result.Value; + using (log.MeasureTrace(w => w + .WriteProperty("module", nameof(OrleansAppProvider)) + .WriteProperty("method", nameof(GetSchemaAsync)))) + { + var result = await factory.GetGrain(appName).GetSchemaAsync(id, provideDeleted); + + return result.Value; + } } public async Task GetSchemaAsync(string appName, string name, bool provideDeleted = false) { - var result = await factory.GetGrain(appName).GetSchemaAsync(name, provideDeleted); - - return result.Value; + using (log.MeasureTrace(w => w + .WriteProperty("module", nameof(OrleansAppProvider)) + .WriteProperty("method", nameof(GetSchemaAsync)))) + { + var result = await factory.GetGrain(appName).GetSchemaAsync(name, provideDeleted); + + return result.Value; + } } public async Task> GetSchemasAsync(string appName) { - var result = await factory.GetGrain(appName).GetSchemasAsync(); - - return result.Value; + using (log.MeasureTrace(w => w + .WriteProperty("module", nameof(OrleansAppProvider)) + .WriteProperty("method", nameof(GetSchemasAsync)))) + { + var result = await factory.GetGrain(appName).GetSchemasAsync(); + + return result.Value; + } } public async Task> GetUserApps(string userId) { - var schemaIds = await factory.GetGrain(userId).GetSchemaNamesAsync(); + using (log.MeasureTrace(w => w + .WriteProperty("module", nameof(OrleansAppProvider)) + .WriteProperty("method", nameof(GetUserApps)))) + { + var schemaIds = await factory.GetGrain(userId).GetSchemaNamesAsync(); - var tasks = - schemaIds - .Select(x => factory.GetGrain(x)) - .Select(x => x.GetAppAsync()); + var tasks = + schemaIds + .Select(x => factory.GetGrain(x)) + .Select(x => x.GetAppAsync()); - var apps = await Task.WhenAll(tasks); + var apps = await Task.WhenAll(tasks); - return apps.Select(a => a.Value).Where(a => a != null).ToList(); + return apps.Select(a => a.Value).Where(a => a != null).ToList(); + } } } } diff --git a/src/Squidex.Domain.Users/DataProtection/Orleans/Grains/Implementations/XmlRepositoryGrain.cs b/src/Squidex.Domain.Users/DataProtection/Orleans/Grains/Implementations/XmlRepositoryGrain.cs index 21af2a50f..370005645 100644 --- a/src/Squidex.Domain.Users/DataProtection/Orleans/Grains/Implementations/XmlRepositoryGrain.cs +++ b/src/Squidex.Domain.Users/DataProtection/Orleans/Grains/Implementations/XmlRepositoryGrain.cs @@ -10,12 +10,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Orleans; -using Orleans.Providers; -using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Users.DataProtection.Orleans.Grains.Implementations { - [StorageProvider(ProviderName = "Default")] public sealed class XmlRepositoryGrain : Grain>, IXmlRepositoryGrain { public Task GetAllElementsAsync() @@ -27,7 +24,7 @@ namespace Squidex.Domain.Users.DataProtection.Orleans.Grains.Implementations { State[friendlyName] = element; - return TaskHelper.Done; + return WriteStateAsync(); } } } diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonConverter.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonConverter.cs index 100318609..ff12de09d 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonConverter.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonConverter.cs @@ -20,7 +20,7 @@ namespace Squidex.Infrastructure.MongoDb foreach (var property in source) { - var key = property.Key.Replace("$", "§"); + var key = ReplaceFirstCharacter(property.Key, '$', '§'); result.Add(key, property.Value.ToBson()); } @@ -34,7 +34,7 @@ namespace Squidex.Infrastructure.MongoDb foreach (var property in source) { - var key = property.Name.Replace("§", "$"); + var key = ReplaceFirstCharacter(property.Name, '§', '$'); result.Add(key, property.Value.ToJson()); } @@ -133,5 +133,20 @@ namespace Squidex.Infrastructure.MongoDb throw new NotSupportedException($"Cannot convert {source.GetType()} to Json."); } + + private static string ReplaceFirstCharacter(string value, char toReplace, char replacement) + { + if (value.Length == 0 || value[0] != toReplace) + { + return value; + } + + if (value.Length == 1) + { + return toReplace.ToString(); + } + + return replacement + value.Substring(1); + } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerGrain.cs b/src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerGrain.cs index cfd475a79..a25690da3 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerGrain.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerGrain.cs @@ -10,13 +10,13 @@ using System; using System.Threading.Tasks; using Orleans; using Orleans.Concurrency; -using Orleans.Providers; +using Orleans.Core; +using Orleans.Runtime; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation { - [StorageProvider(ProviderName = "Default")] public class EventConsumerGrain : Grain, IEventSubscriber, IEventConsumerGrain { private readonly EventDataFormatter eventFormatter; @@ -32,6 +32,19 @@ namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation EventConsumerFactory eventConsumerFactory, IEventStore eventStore, ISemanticLog log) + : this(eventFormatter, eventConsumerFactory, eventStore, log, null, null, null) + { + } + + protected EventConsumerGrain( + EventDataFormatter eventFormatter, + EventConsumerFactory eventConsumerFactory, + IEventStore eventStore, + ISemanticLog log, + IGrainIdentity identity, + IGrainRuntime runtime, + IStorage storage) + : base(identity, runtime, storage) { Guard.NotNull(log, nameof(log)); Guard.NotNull(eventStore, nameof(eventStore)); diff --git a/src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerRegistryGrain.cs b/src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerRegistryGrain.cs index eae74b083..cd9c3bcf1 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerRegistryGrain.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerRegistryGrain.cs @@ -13,26 +13,41 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Orleans; using Orleans.Concurrency; +using Orleans.Core; using Orleans.Runtime; -using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation { - public sealed class EventConsumerRegistryGrain : Grain, IEventConsumerRegistryGrain, IRemindable + public class EventConsumerRegistryGrain : Grain, IEventConsumerRegistryGrain, IRemindable { private readonly IEnumerable eventConsumers; public EventConsumerRegistryGrain(IEnumerable eventConsumers) + : this(eventConsumers, null, null) + { + } + + protected EventConsumerRegistryGrain( + IEnumerable eventConsumers, + IGrainIdentity identity, + IGrainRuntime runtime) + : base(identity, runtime) { Guard.NotNull(eventConsumers, nameof(eventConsumers)); this.eventConsumers = eventConsumers; } + public Task ReceiveReminder(string reminderName, TickStatus status) + { + return ActivateAsync(null); + } + public override Task OnActivateAsync() { DelayDeactivation(TimeSpan.FromDays(1)); + RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); RegisterTimer(x => ActivateAsync(null), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); return Task.FromResult(true); @@ -59,11 +74,6 @@ namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation return Task.WhenAll(tasks).ContinueWith(x => new Immutable>(x.Result.Select(r => r.Value).ToList())); } - public Task ReceiveReminder(string reminderName, TickStatus status) - { - return TaskHelper.Done; - } - public Task ResetAsync(string consumerName) { var eventConsumer = GrainFactory.GetGrain(consumerName); diff --git a/src/Squidex.Infrastructure/Json/Orleans/IJsonValue.cs b/src/Squidex.Infrastructure/Json/Orleans/IJsonValue.cs index fd1ba7c05..f2d8ab41f 100644 --- a/src/Squidex.Infrastructure/Json/Orleans/IJsonValue.cs +++ b/src/Squidex.Infrastructure/Json/Orleans/IJsonValue.cs @@ -11,5 +11,7 @@ namespace Squidex.Infrastructure.Json.Orleans public interface IJsonValue { object Value { get; } + + bool IsImmutable { get; } } } diff --git a/src/Squidex.Infrastructure/Json/Orleans/J.cs b/src/Squidex.Infrastructure/Json/Orleans/J.cs index 97256ee7f..c137b6a8d 100644 --- a/src/Squidex.Infrastructure/Json/Orleans/J.cs +++ b/src/Squidex.Infrastructure/Json/Orleans/J.cs @@ -14,21 +14,29 @@ namespace Squidex.Infrastructure.Json.Orleans public struct J : IJsonValue { private readonly T value; + private readonly bool isImmutable; public T Value { get { return value; } } + bool IJsonValue.IsImmutable + { + get { return isImmutable; } + } + object IJsonValue.Value { get { return Value; } } [JsonConstructor] - public J(T value) + public J(T value, bool isImmutable = false) { this.value = value; + + this.isImmutable = isImmutable; } public static implicit operator T(J value) diff --git a/src/Squidex.Infrastructure/Json/Orleans/JsonExternalSerializer.cs b/src/Squidex.Infrastructure/Json/Orleans/JsonExternalSerializer.cs index f14070130..80566b15d 100644 --- a/src/Squidex.Infrastructure/Json/Orleans/JsonExternalSerializer.cs +++ b/src/Squidex.Infrastructure/Json/Orleans/JsonExternalSerializer.cs @@ -38,7 +38,24 @@ namespace Squidex.Infrastructure.Json.Orleans public object DeepCopy(object source, ICopyContext context) { - return source != null ? JObject.FromObject(source, serializer).ToObject(source.GetType(), serializer) : null; + var jsonValue = source as IJsonValue; + + if (jsonValue == null) + { + return null; + } + else if (jsonValue.IsImmutable) + { + return jsonValue; + } + else if (jsonValue.Value == null) + { + return jsonValue; + } + else + { + return JObject.FromObject(source, serializer).ToObject(source.GetType(), serializer); + } } public object Deserialize(Type expectedType, IDeserializationContext context) diff --git a/src/Squidex/AppServices.cs b/src/Squidex/AppServices.cs index 05d969b3f..730541815 100644 --- a/src/Squidex/AppServices.cs +++ b/src/Squidex/AppServices.cs @@ -33,7 +33,6 @@ namespace Squidex services.AddMyIdentityServer(); services.AddMyInfrastructureServices(config); services.AddMyMvc(); - services.AddMyPubSubServices(config); services.AddMyReadServices(config); services.AddMySerializers(); services.AddMyStoreServices(config); diff --git a/src/Squidex/Config/Domain/PubSubServices.cs b/src/Squidex/Config/Domain/PubSubServices.cs deleted file mode 100644 index dc251043e..000000000 --- a/src/Squidex/Config/Domain/PubSubServices.cs +++ /dev/null @@ -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() - .As(); - }, - ["Redis"] = () => - { - var configuration = config.GetRequiredValue("pubsub:redis:configuration"); - - var redis = Singletons.GetOrAddLazy(configuration, s => ConnectionMultiplexer.Connect(s)); - - services.AddSingleton(c => new RedisPubSub(redis, c.GetRequiredService())) - .As() - .As(); - } - }); - } - } -} diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index d05763f0a..7eddb48ad 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -89,11 +89,6 @@ namespace Squidex.Config.Domain .As() .As() .As(); - - services.AddSingleton(c => new MongoRuleRepository(mongoDatabase)) - .As() - .As() - .As(); } }); } diff --git a/src/Squidex/Controllers/Api/Rules/RulesController.cs b/src/Squidex/Controllers/Api/Rules/RulesController.cs index 100bb3633..ab4d1fab5 100644 --- a/src/Squidex/Controllers/Api/Rules/RulesController.cs +++ b/src/Squidex/Controllers/Api/Rules/RulesController.cs @@ -14,6 +14,7 @@ using NodaTime; using NSwag.Annotations; using Squidex.Controllers.Api.Rules.Models; using Squidex.Controllers.Api.Rules.Models.Converters; +using Squidex.Domain.Apps.Read; using Squidex.Domain.Apps.Read.Rules.Repositories; using Squidex.Domain.Apps.Write.Rules.Commands; using Squidex.Infrastructure.CQRS.Commands; @@ -32,15 +33,15 @@ namespace Squidex.Controllers.Api.Rules [MustBeAppDeveloper] public sealed class RulesController : ControllerBase { - private readonly IRuleRepository rulesRepository; + private readonly IAppProvider appProvider; private readonly IRuleEventRepository ruleEventsRepository; - public RulesController(ICommandBus commandBus, - IRuleRepository rulesRepository, + public RulesController(ICommandBus commandBus, IAppProvider appProvider, IRuleEventRepository ruleEventsRepository) : base(commandBus) { - this.rulesRepository = rulesRepository; + this.appProvider = appProvider; + this.ruleEventsRepository = ruleEventsRepository; } @@ -58,7 +59,7 @@ namespace Squidex.Controllers.Api.Rules [ApiCosts(1)] public async Task GetRules(string app) { - var rules = await rulesRepository.QueryByAppAsync(App.Id); + var rules = await appProvider.GetRulesAsync(AppName); var response = rules.Select(r => r.ToModel()); diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index 2709ed69d..941f5d20d 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -29,26 +29,6 @@ "human": true }, - /* - * The pub sub mechanmism distributes messages between the nodes. - */ - "pubSub": { - /* - * Define the type of the read store. - * - * Supported: InMemory (for single node only), Redis (for cluster) - */ - "type": "InMemory", - "redis": { - /* - * Connection string to your redis server. - * - * Read More: https://github.com/ServiceStack/ServiceStack.Redis#redis-connection-strings - */ - "configuration": "localhost:6379,resolveDns=1" - } - }, - "assetStore": { /* * Define the type of the read store. diff --git a/tests/RunCoverage.ps1 b/tests/RunCoverage.ps1 index 9a3b56d23..3607e486f 100644 --- a/tests/RunCoverage.ps1 +++ b/tests/RunCoverage.ps1 @@ -26,7 +26,7 @@ if ($all -Or $infrastructure) { -register:user ` -target:"C:\Program Files\dotnet\dotnet.exe" ` -targetargs:"test $folderWorking\Squidex.Infrastructure.Tests\Squidex.Infrastructure.Tests.csproj" ` - -filter:"+[Squidex.Infrastructure*]*" ` + -filter:"+[Squidex.Infrastructure*]* -[Squidex.Infrastructure*]*CodeGen*" ` -skipautoprops ` -output:"$folderWorking\$folderReports\Infrastructure.xml" ` -oldStyle diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/CompoundEventConsumerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/CompoundEventConsumerTests.cs index 1c44e943f..09176575e 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/CompoundEventConsumerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/CompoundEventConsumerTests.cs @@ -51,6 +51,17 @@ namespace Squidex.Infrastructure.CQRS.Events Assert.Equal("(filter1)|(filter2)", sut.EventsFilter); } + [Fact] + public void Should_return_compound_filter_from_array() + { + A.CallTo(() => consumer1.EventsFilter).Returns("filter1"); + A.CallTo(() => consumer2.EventsFilter).Returns("filter2"); + + var sut = new CompoundEventConsumer(new[] { consumer1, consumer2 }); + + Assert.Equal("(filter1)|(filter2)", sut.EventsFilter); + } + [Fact] public void Should_ignore_empty_filters() { diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/EventConsumerBootstrapTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/EventConsumerBootstrapTests.cs new file mode 100644 index 000000000..9e94d3831 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/EventConsumerBootstrapTests.cs @@ -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(); + private readonly IProviderRuntime runtime = A.Fake(); + private readonly EventConsumerBootstrap sut = new EventConsumerBootstrap(); + + public EventConsumerBootstrapTests() + { + var factory = A.Fake(); + + A.CallTo(() => factory.GetGrain("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(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/EventConsumerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/EventConsumerGrainTests.cs new file mode 100644 index 000000000..7cfc09960 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/EventConsumerGrainTests.cs @@ -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 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(); + private readonly IEventStore eventStore = A.Fake(); + private readonly IEventSubscription eventSubscription = A.Fake(); + private readonly ISemanticLog log = A.Fake(); + private readonly IEventSubscriber sutSubscriber; + private readonly IStorage storage = A.Fake>(); + private readonly EventDataFormatter formatter = A.Fake(); + private readonly EventData eventData = new EventData(); + private readonly Envelope envelope = new Envelope(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.Ignored, A.Ignored, A.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(s => state = s)); + + sut = new MyEventConsumerActor( + formatter, + factory, + eventStore, + log, + A.Fake(), + A.Fake(), + 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.Ignored, A.Ignored, A.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.Ignored, A.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.Ignored, A.Ignored, state.Position)) + .MustHaveHappened(Repeated.Exactly.Once); + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.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()); + + 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()); + + 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(), @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(), 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.Ignored, A.Ignored, A.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); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/EventConsumerRegistryGrainTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/EventConsumerRegistryGrainTests.cs new file mode 100644 index 000000000..1dbba926d --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/EventConsumerRegistryGrainTests.cs @@ -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 eventConsumers, + IGrainIdentity identity, + IGrainRuntime runtime) + : base(eventConsumers, identity, runtime) + { + } + } + + private readonly IEventConsumer consumerA = A.Fake(); + private readonly IEventConsumer consumerB = A.Fake(); + private readonly IEventConsumerGrain grainA = A.Fake(); + private readonly IEventConsumerGrain grainB = A.Fake(); + private readonly MyEventConsumerRegistryGrain sut; + + public EventConsumerRegistryGrainTests() + { + var grainRuntime = A.Fake(); + var grainFactory = A.Fake(); + + A.CallTo(() => grainFactory.GetGrain("a", null)).Returns(grainA); + A.CallTo(() => grainFactory.GetGrain("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(), 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( + new EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" })); + + A.CallTo(() => grainB.GetStateAsync()) + .Returns(new Immutable( + new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" })); + + var infos = await sut.GetConsumersAsync(); + + infos.Value.ShouldBeEquivalentTo( + new List + { + new EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" }, + new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" } + }); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/OrleansEventNotifierTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/OrleansEventNotifierTests.cs new file mode 100644 index 000000000..325b97b05 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/OrleansEventNotifierTests.cs @@ -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(); + private readonly OrleansEventNotifier sut; + + public OrleansEventNotifierTests() + { + var factory = A.Fake(); + + A.CallTo(() => factory.GetGrain("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(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Json/Orleans/JsonExternalSerializerTests.cs b/tests/Squidex.Infrastructure.Tests/Json/Orleans/JsonExternalSerializerTests.cs new file mode 100644 index 000000000..40ece3532 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Json/Orleans/JsonExternalSerializerTests.cs @@ -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))); + Assert.True(sut.IsSupportedType(typeof(J>))); + + Assert.False(sut.IsSupportedType(typeof(int))); + Assert.False(sut.IsSupportedType(typeof(List))); + } + + [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>(null); + var copy = (J>)sut.DeepCopy(value, null); + + Assert.Null(copy.Value); + } + + [Fact] + public void Should_not_copy_immutable_values() + { + var value = new J>(new List { 1, 2, 3 }, true); + var copy = (J>)sut.DeepCopy(value, null); + + Assert.Same(value.Value, copy.Value); + } + + [Fact] + public void Should_copy_non_immutable_values() + { + var value = new J>(new List { 1, 2, 3 }); + var copy = (J>)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>(new List { 1, 2, 3 }); + + var writtenLength = 0; + var writtenBuffer = (byte[])null; + + var writer = A.Fake(); + var writerContext = new SerializationContext(null) { StreamWriter = writer }; + + A.CallTo(() => writer.Write(A.Ignored)) + .Invokes(new Action(x => writtenLength = x)); + + A.CallTo(() => writer.Write(A.Ignored)) + .Invokes(new Action(x => writtenBuffer = x)); + + sut.Serialize(value, writerContext, value.GetType()); + + var reader = A.Fake(); + var readerContext = new DeserializationContext(null) { StreamReader = reader }; + + A.CallTo(() => reader.ReadInt()) + .Returns(writtenLength); + + A.CallTo(() => reader.ReadBytes(writtenLength)) + .Returns(writtenBuffer); + + var copy = (J>)sut.Deserialize(value.GetType(), readerContext); + + Assert.Equal(value.Value, copy.Value); + Assert.NotSame(value.Value, copy.Value); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs index 35e3d55da..38e0abdf7 100644 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs @@ -38,7 +38,7 @@ namespace Squidex.Infrastructure.TestHelpers public static T SerializeAndDeserializeAndReturn(this T value, JsonConverter converter) { - var serializerSettings = CreateSettings(converter); + var serializerSettings = CreateSettings(converter); var result = JsonConvert.SerializeObject(Tuple.Create(value), serializerSettings); var output = JsonConvert.DeserializeObject>(result, serializerSettings); @@ -48,12 +48,12 @@ namespace Squidex.Infrastructure.TestHelpers public static void DoesNotDeserialize(string value, JsonConverter converter) { - var serializerSettings = CreateSettings(converter); + var serializerSettings = CreateSettings(converter); Assert.ThrowsAny(() => JsonConvert.DeserializeObject>($"{{ \"Item1\": \"{value}\" }}", serializerSettings)); } - private static JsonSerializerSettings CreateSettings(JsonConverter converter) + private static JsonSerializerSettings CreateSettings(JsonConverter converter) { var serializerSettings = new JsonSerializerSettings();