Browse Source

Tests for orleans stuff.

pull/169/head
Sebastian Stehle 8 years ago
parent
commit
9c108f2a44
  1. 41
      src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs
  2. 90
      src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs
  3. 97
      src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs
  4. 21
      src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs
  5. 13
      src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs
  6. 2
      src/Squidex.Domain.Apps.Read/State/Orleans/Grains/Implementations/AppStateGrain.cs
  7. 2
      src/Squidex.Domain.Apps.Read/State/Orleans/Grains/Implementations/AppUserGrain.cs
  8. 94
      src/Squidex.Domain.Apps.Read/State/Orleans/OrleansAppProvider.cs
  9. 5
      src/Squidex.Domain.Users/DataProtection/Orleans/Grains/Implementations/XmlRepositoryGrain.cs
  10. 19
      src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonConverter.cs
  11. 17
      src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerGrain.cs
  12. 24
      src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerRegistryGrain.cs
  13. 2
      src/Squidex.Infrastructure/Json/Orleans/IJsonValue.cs
  14. 10
      src/Squidex.Infrastructure/Json/Orleans/J.cs
  15. 19
      src/Squidex.Infrastructure/Json/Orleans/JsonExternalSerializer.cs
  16. 1
      src/Squidex/AppServices.cs
  17. 41
      src/Squidex/Config/Domain/PubSubServices.cs
  18. 5
      src/Squidex/Config/Domain/StoreServices.cs
  19. 11
      src/Squidex/Controllers/Api/Rules/RulesController.cs
  20. 20
      src/Squidex/appsettings.json
  21. 2
      tests/RunCoverage.ps1
  22. 11
      tests/Squidex.Infrastructure.Tests/CQRS/Events/CompoundEventConsumerTests.cs
  23. 58
      tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/EventConsumerBootstrapTests.cs
  24. 404
      tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/EventConsumerGrainTests.cs
  25. 165
      tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/EventConsumerRegistryGrainTests.cs
  26. 41
      tests/Squidex.Infrastructure.Tests/CQRS/Events/Grains/OrleansEventNotifierTests.cs
  27. 132
      tests/Squidex.Infrastructure.Tests/Json/Orleans/JsonExternalSerializerTests.cs
  28. 6
      tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs

41
src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs

@ -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; }
}
}

90
src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs

@ -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();
}
}
}
}
}

97
src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs

@ -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);
}
}
}

21
src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs

@ -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);
}
}

13
src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs

@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Read.Rules
public sealed class RuleEnqueuer : IEventConsumer public sealed class RuleEnqueuer : IEventConsumer
{ {
private readonly IRuleEventRepository ruleEventRepository; private readonly IRuleEventRepository ruleEventRepository;
private readonly IRuleRepository ruleRepository; private readonly IAppProvider appProvider;
private readonly RuleService ruleService; private readonly RuleService ruleService;
public string Name public string Name
@ -33,17 +33,18 @@ namespace Squidex.Domain.Apps.Read.Rules
} }
public RuleEnqueuer( public RuleEnqueuer(
IRuleEventRepository ruleEventRepository, IRuleEventRepository ruleEventRepository, IAppProvider appProvider,
IRuleRepository ruleRepository,
RuleService ruleService) RuleService ruleService)
{ {
Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository));
Guard.NotNull(ruleRepository, nameof(ruleRepository));
Guard.NotNull(ruleService, nameof(ruleService)); Guard.NotNull(ruleService, nameof(ruleService));
Guard.NotNull(appProvider, nameof(appProvider));
this.ruleEventRepository = ruleEventRepository; this.ruleEventRepository = ruleEventRepository;
this.ruleRepository = ruleRepository;
this.ruleService = ruleService; this.ruleService = ruleService;
this.appProvider = appProvider;
} }
public Task ClearAsync() public Task ClearAsync()
@ -55,7 +56,7 @@ namespace Squidex.Domain.Apps.Read.Rules
{ {
if (@event.Payload is AppEvent appEvent) 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) foreach (var ruleEntity in rules)
{ {

2
src/Squidex.Domain.Apps.Read/State/Orleans/Grains/Implementations/AppStateGrain.cs

@ -10,7 +10,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Orleans.Providers;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Rules; using Squidex.Domain.Apps.Read.Rules;
@ -21,7 +20,6 @@ using Squidex.Infrastructure.Json.Orleans;
namespace Squidex.Domain.Apps.Read.State.Orleans.Grains.Implementations namespace Squidex.Domain.Apps.Read.State.Orleans.Grains.Implementations
{ {
[StorageProvider(ProviderName = "Default")]
public sealed class AppStateGrain : Grain<AppStateGrainState>, IAppStateGrain public sealed class AppStateGrain : Grain<AppStateGrainState>, IAppStateGrain
{ {
private readonly FieldRegistry fieldRegistry; private readonly FieldRegistry fieldRegistry;

2
src/Squidex.Domain.Apps.Read/State/Orleans/Grains/Implementations/AppUserGrain.cs

@ -10,11 +10,9 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Orleans.Providers;
namespace Squidex.Domain.Apps.Read.State.Orleans.Grains.Implementations namespace Squidex.Domain.Apps.Read.State.Orleans.Grains.Implementations
{ {
[StorageProvider(ProviderName = "Default")]
public sealed class AppUserGrain : Grain<AppUserGrainState>, IAppUserGrain public sealed class AppUserGrain : Grain<AppUserGrainState>, IAppUserGrain
{ {
public Task AddAppAsync(string appName) public Task AddAppAsync(string appName)

94
src/Squidex.Domain.Apps.Read/State/Orleans/OrleansAppProvider.cs

@ -1,5 +1,5 @@
// ========================================================================== // ==========================================================================
// OrleansApps.cs // OrleansAppProvider.cs
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // 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.Schemas;
using Squidex.Domain.Apps.Read.State.Orleans.Grains; using Squidex.Domain.Apps.Read.State.Orleans.Grains;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
namespace Squidex.Domain.Apps.Read.State.Orleans namespace Squidex.Domain.Apps.Read.State.Orleans
{ {
public sealed class OrleansAppProvider : IAppProvider public sealed class OrleansAppProvider : IAppProvider
{ {
private readonly IGrainFactory factory; 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(factory, nameof(factory));
Guard.NotNull(log, nameof(log));
this.factory = factory; this.factory = factory;
this.log = log;
} }
public async Task<IAppEntity> GetAppAsync(string appName) public async Task<IAppEntity> GetAppAsync(string appName)
{ {
var result = await factory.GetGrain<IAppStateGrain>(appName).GetAppAsync(); using (log.MeasureTrace(w => w
.WriteProperty("module", nameof(OrleansAppProvider))
return result.Value; .WriteProperty("method", nameof(GetAppAsync))))
{
var result = await factory.GetGrain<IAppStateGrain>(appName).GetAppAsync();
return result.Value;
}
} }
public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id) public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id)
{ {
var result = await factory.GetGrain<IAppStateGrain>(appName).GetAppWithSchemaAsync(id); using (log.MeasureTrace(w => w
.WriteProperty("module", nameof(OrleansAppProvider))
return result.Value; .WriteProperty("method", nameof(GetAppWithSchemaAsync))))
{
var result = await factory.GetGrain<IAppStateGrain>(appName).GetAppWithSchemaAsync(id);
return result.Value;
}
} }
public async Task<List<IRuleEntity>> GetRulesAsync(string appName) public async Task<List<IRuleEntity>> GetRulesAsync(string appName)
{ {
var result = await factory.GetGrain<IAppStateGrain>(appName).GetRulesAsync(); using (log.MeasureTrace(w => w
.WriteProperty("module", nameof(OrleansAppProvider))
return result.Value; .WriteProperty("method", nameof(GetRulesAsync))))
{
var result = await factory.GetGrain<IAppStateGrain>(appName).GetRulesAsync();
return result.Value;
}
} }
public async Task<ISchemaEntity> GetSchemaAsync(string appName, Guid id, bool provideDeleted = false) public async Task<ISchemaEntity> GetSchemaAsync(string appName, Guid id, bool provideDeleted = false)
{ {
var result = await factory.GetGrain<IAppStateGrain>(appName).GetSchemaAsync(id, provideDeleted); using (log.MeasureTrace(w => w
.WriteProperty("module", nameof(OrleansAppProvider))
return result.Value; .WriteProperty("method", nameof(GetSchemaAsync))))
{
var result = await factory.GetGrain<IAppStateGrain>(appName).GetSchemaAsync(id, provideDeleted);
return result.Value;
}
} }
public async Task<ISchemaEntity> GetSchemaAsync(string appName, string name, bool provideDeleted = false) public async Task<ISchemaEntity> GetSchemaAsync(string appName, string name, bool provideDeleted = false)
{ {
var result = await factory.GetGrain<IAppStateGrain>(appName).GetSchemaAsync(name, provideDeleted); using (log.MeasureTrace(w => w
.WriteProperty("module", nameof(OrleansAppProvider))
return result.Value; .WriteProperty("method", nameof(GetSchemaAsync))))
{
var result = await factory.GetGrain<IAppStateGrain>(appName).GetSchemaAsync(name, provideDeleted);
return result.Value;
}
} }
public async Task<List<ISchemaEntity>> GetSchemasAsync(string appName) public async Task<List<ISchemaEntity>> GetSchemasAsync(string appName)
{ {
var result = await factory.GetGrain<IAppStateGrain>(appName).GetSchemasAsync(); using (log.MeasureTrace(w => w
.WriteProperty("module", nameof(OrleansAppProvider))
return result.Value; .WriteProperty("method", nameof(GetSchemasAsync))))
{
var result = await factory.GetGrain<IAppStateGrain>(appName).GetSchemasAsync();
return result.Value;
}
} }
public async Task<List<IAppEntity>> GetUserApps(string userId) public async Task<List<IAppEntity>> GetUserApps(string userId)
{ {
var schemaIds = await factory.GetGrain<IAppUserGrain>(userId).GetSchemaNamesAsync(); using (log.MeasureTrace(w => w
.WriteProperty("module", nameof(OrleansAppProvider))
.WriteProperty("method", nameof(GetUserApps))))
{
var schemaIds = await factory.GetGrain<IAppUserGrain>(userId).GetSchemaNamesAsync();
var tasks = var tasks =
schemaIds schemaIds
.Select(x => factory.GetGrain<IAppStateGrain>(x)) .Select(x => factory.GetGrain<IAppStateGrain>(x))
.Select(x => x.GetAppAsync()); .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();
}
} }
} }
} }

5
src/Squidex.Domain.Users/DataProtection/Orleans/Grains/Implementations/XmlRepositoryGrain.cs

@ -10,12 +10,9 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Orleans.Providers;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Users.DataProtection.Orleans.Grains.Implementations namespace Squidex.Domain.Users.DataProtection.Orleans.Grains.Implementations
{ {
[StorageProvider(ProviderName = "Default")]
public sealed class XmlRepositoryGrain : Grain<Dictionary<string, string>>, IXmlRepositoryGrain public sealed class XmlRepositoryGrain : Grain<Dictionary<string, string>>, IXmlRepositoryGrain
{ {
public Task<string[]> GetAllElementsAsync() public Task<string[]> GetAllElementsAsync()
@ -27,7 +24,7 @@ namespace Squidex.Domain.Users.DataProtection.Orleans.Grains.Implementations
{ {
State[friendlyName] = element; State[friendlyName] = element;
return TaskHelper.Done; return WriteStateAsync();
} }
} }
} }

19
src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonConverter.cs

@ -20,7 +20,7 @@ namespace Squidex.Infrastructure.MongoDb
foreach (var property in source) foreach (var property in source)
{ {
var key = property.Key.Replace("$", "§"); var key = ReplaceFirstCharacter(property.Key, '$', '§');
result.Add(key, property.Value.ToBson()); result.Add(key, property.Value.ToBson());
} }
@ -34,7 +34,7 @@ namespace Squidex.Infrastructure.MongoDb
foreach (var property in source) foreach (var property in source)
{ {
var key = property.Name.Replace("§", "$"); var key = ReplaceFirstCharacter(property.Name, '§', '$');
result.Add(key, property.Value.ToJson()); result.Add(key, property.Value.ToJson());
} }
@ -133,5 +133,20 @@ namespace Squidex.Infrastructure.MongoDb
throw new NotSupportedException($"Cannot convert {source.GetType()} to Json."); 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);
}
} }
} }

17
src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerGrain.cs

@ -10,13 +10,13 @@ using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Orleans.Concurrency; using Orleans.Concurrency;
using Orleans.Providers; using Orleans.Core;
using Orleans.Runtime;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation
{ {
[StorageProvider(ProviderName = "Default")]
public class EventConsumerGrain : Grain<EventConsumerGrainState>, IEventSubscriber, IEventConsumerGrain public class EventConsumerGrain : Grain<EventConsumerGrainState>, IEventSubscriber, IEventConsumerGrain
{ {
private readonly EventDataFormatter eventFormatter; private readonly EventDataFormatter eventFormatter;
@ -32,6 +32,19 @@ namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation
EventConsumerFactory eventConsumerFactory, EventConsumerFactory eventConsumerFactory,
IEventStore eventStore, IEventStore eventStore,
ISemanticLog log) 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<EventConsumerGrainState> storage)
: base(identity, runtime, storage)
{ {
Guard.NotNull(log, nameof(log)); Guard.NotNull(log, nameof(log));
Guard.NotNull(eventStore, nameof(eventStore)); Guard.NotNull(eventStore, nameof(eventStore));

24
src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerRegistryGrain.cs

@ -13,26 +13,41 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Orleans.Concurrency; using Orleans.Concurrency;
using Orleans.Core;
using Orleans.Runtime; using Orleans.Runtime;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation
{ {
public sealed class EventConsumerRegistryGrain : Grain, IEventConsumerRegistryGrain, IRemindable public class EventConsumerRegistryGrain : Grain, IEventConsumerRegistryGrain, IRemindable
{ {
private readonly IEnumerable<IEventConsumer> eventConsumers; private readonly IEnumerable<IEventConsumer> eventConsumers;
public EventConsumerRegistryGrain(IEnumerable<IEventConsumer> eventConsumers) public EventConsumerRegistryGrain(IEnumerable<IEventConsumer> eventConsumers)
: this(eventConsumers, null, null)
{
}
protected EventConsumerRegistryGrain(
IEnumerable<IEventConsumer> eventConsumers,
IGrainIdentity identity,
IGrainRuntime runtime)
: base(identity, runtime)
{ {
Guard.NotNull(eventConsumers, nameof(eventConsumers)); Guard.NotNull(eventConsumers, nameof(eventConsumers));
this.eventConsumers = eventConsumers; this.eventConsumers = eventConsumers;
} }
public Task ReceiveReminder(string reminderName, TickStatus status)
{
return ActivateAsync(null);
}
public override Task OnActivateAsync() public override Task OnActivateAsync()
{ {
DelayDeactivation(TimeSpan.FromDays(1)); DelayDeactivation(TimeSpan.FromDays(1));
RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10));
RegisterTimer(x => ActivateAsync(null), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); RegisterTimer(x => ActivateAsync(null), null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
return Task.FromResult(true); return Task.FromResult(true);
@ -59,11 +74,6 @@ namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation
return Task.WhenAll(tasks).ContinueWith(x => new Immutable<List<EventConsumerInfo>>(x.Result.Select(r => r.Value).ToList())); return Task.WhenAll(tasks).ContinueWith(x => new Immutable<List<EventConsumerInfo>>(x.Result.Select(r => r.Value).ToList()));
} }
public Task ReceiveReminder(string reminderName, TickStatus status)
{
return TaskHelper.Done;
}
public Task ResetAsync(string consumerName) public Task ResetAsync(string consumerName)
{ {
var eventConsumer = GrainFactory.GetGrain<IEventConsumerGrain>(consumerName); var eventConsumer = GrainFactory.GetGrain<IEventConsumerGrain>(consumerName);

2
src/Squidex.Infrastructure/Json/Orleans/IJsonValue.cs

@ -11,5 +11,7 @@ namespace Squidex.Infrastructure.Json.Orleans
public interface IJsonValue public interface IJsonValue
{ {
object Value { get; } object Value { get; }
bool IsImmutable { get; }
} }
} }

10
src/Squidex.Infrastructure/Json/Orleans/J.cs

@ -14,21 +14,29 @@ namespace Squidex.Infrastructure.Json.Orleans
public struct J<T> : IJsonValue public struct J<T> : IJsonValue
{ {
private readonly T value; private readonly T value;
private readonly bool isImmutable;
public T Value public T Value
{ {
get { return value; } get { return value; }
} }
bool IJsonValue.IsImmutable
{
get { return isImmutable; }
}
object IJsonValue.Value object IJsonValue.Value
{ {
get { return Value; } get { return Value; }
} }
[JsonConstructor] [JsonConstructor]
public J(T value) public J(T value, bool isImmutable = false)
{ {
this.value = value; this.value = value;
this.isImmutable = isImmutable;
} }
public static implicit operator T(J<T> value) public static implicit operator T(J<T> value)

19
src/Squidex.Infrastructure/Json/Orleans/JsonExternalSerializer.cs

@ -38,7 +38,24 @@ namespace Squidex.Infrastructure.Json.Orleans
public object DeepCopy(object source, ICopyContext context) 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) public object Deserialize(Type expectedType, IDeserializationContext context)

1
src/Squidex/AppServices.cs

@ -33,7 +33,6 @@ namespace Squidex
services.AddMyIdentityServer(); services.AddMyIdentityServer();
services.AddMyInfrastructureServices(config); services.AddMyInfrastructureServices(config);
services.AddMyMvc(); services.AddMyMvc();
services.AddMyPubSubServices(config);
services.AddMyReadServices(config); services.AddMyReadServices(config);
services.AddMySerializers(); services.AddMySerializers();
services.AddMyStoreServices(config); services.AddMyStoreServices(config);

41
src/Squidex/Config/Domain/PubSubServices.cs

@ -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>();
}
});
}
}
}

5
src/Squidex/Config/Domain/StoreServices.cs

@ -89,11 +89,6 @@ namespace Squidex.Config.Domain
.As<IAssetRepository>() .As<IAssetRepository>()
.As<IAssetEventConsumer>() .As<IAssetEventConsumer>()
.As<IExternalSystem>(); .As<IExternalSystem>();
services.AddSingleton(c => new MongoRuleRepository(mongoDatabase))
.As<IRuleRepository>()
.As<IEventConsumer>()
.As<IExternalSystem>();
} }
}); });
} }

11
src/Squidex/Controllers/Api/Rules/RulesController.cs

@ -14,6 +14,7 @@ using NodaTime;
using NSwag.Annotations; using NSwag.Annotations;
using Squidex.Controllers.Api.Rules.Models; using Squidex.Controllers.Api.Rules.Models;
using Squidex.Controllers.Api.Rules.Models.Converters; using Squidex.Controllers.Api.Rules.Models.Converters;
using Squidex.Domain.Apps.Read;
using Squidex.Domain.Apps.Read.Rules.Repositories; using Squidex.Domain.Apps.Read.Rules.Repositories;
using Squidex.Domain.Apps.Write.Rules.Commands; using Squidex.Domain.Apps.Write.Rules.Commands;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
@ -32,15 +33,15 @@ namespace Squidex.Controllers.Api.Rules
[MustBeAppDeveloper] [MustBeAppDeveloper]
public sealed class RulesController : ControllerBase public sealed class RulesController : ControllerBase
{ {
private readonly IRuleRepository rulesRepository; private readonly IAppProvider appProvider;
private readonly IRuleEventRepository ruleEventsRepository; private readonly IRuleEventRepository ruleEventsRepository;
public RulesController(ICommandBus commandBus, public RulesController(ICommandBus commandBus, IAppProvider appProvider,
IRuleRepository rulesRepository,
IRuleEventRepository ruleEventsRepository) IRuleEventRepository ruleEventsRepository)
: base(commandBus) : base(commandBus)
{ {
this.rulesRepository = rulesRepository; this.appProvider = appProvider;
this.ruleEventsRepository = ruleEventsRepository; this.ruleEventsRepository = ruleEventsRepository;
} }
@ -58,7 +59,7 @@ namespace Squidex.Controllers.Api.Rules
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetRules(string app) public async Task<IActionResult> GetRules(string app)
{ {
var rules = await rulesRepository.QueryByAppAsync(App.Id); var rules = await appProvider.GetRulesAsync(AppName);
var response = rules.Select(r => r.ToModel()); var response = rules.Select(r => r.ToModel());

20
src/Squidex/appsettings.json

@ -29,26 +29,6 @@
"human": true "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": { "assetStore": {
/* /*
* Define the type of the read store. * Define the type of the read store.

2
tests/RunCoverage.ps1

@ -26,7 +26,7 @@ if ($all -Or $infrastructure) {
-register:user ` -register:user `
-target:"C:\Program Files\dotnet\dotnet.exe" ` -target:"C:\Program Files\dotnet\dotnet.exe" `
-targetargs:"test $folderWorking\Squidex.Infrastructure.Tests\Squidex.Infrastructure.Tests.csproj" ` -targetargs:"test $folderWorking\Squidex.Infrastructure.Tests\Squidex.Infrastructure.Tests.csproj" `
-filter:"+[Squidex.Infrastructure*]*" ` -filter:"+[Squidex.Infrastructure*]* -[Squidex.Infrastructure*]*CodeGen*" `
-skipautoprops ` -skipautoprops `
-output:"$folderWorking\$folderReports\Infrastructure.xml" ` -output:"$folderWorking\$folderReports\Infrastructure.xml" `
-oldStyle -oldStyle

11
tests/Squidex.Infrastructure.Tests/CQRS/Events/CompoundEventConsumerTests.cs

@ -51,6 +51,17 @@ namespace Squidex.Infrastructure.CQRS.Events
Assert.Equal("(filter1)|(filter2)", sut.EventsFilter); 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] [Fact]
public void Should_ignore_empty_filters() public void Should_ignore_empty_filters()
{ {

58
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<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();
}
}
}

404
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<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);
}
}
}

165
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<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" }
});
}
}
}

41
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<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();
}
}
}

132
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<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);
}
}
}

6
tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs

@ -38,7 +38,7 @@ namespace Squidex.Infrastructure.TestHelpers
public static T SerializeAndDeserializeAndReturn<T>(this T value, JsonConverter converter) public static T SerializeAndDeserializeAndReturn<T>(this T value, JsonConverter converter)
{ {
var serializerSettings = CreateSettings<T>(converter); var serializerSettings = CreateSettings(converter);
var result = JsonConvert.SerializeObject(Tuple.Create(value), serializerSettings); var result = JsonConvert.SerializeObject(Tuple.Create(value), serializerSettings);
var output = JsonConvert.DeserializeObject<Tuple<T>>(result, serializerSettings); var output = JsonConvert.DeserializeObject<Tuple<T>>(result, serializerSettings);
@ -48,12 +48,12 @@ namespace Squidex.Infrastructure.TestHelpers
public static void DoesNotDeserialize<T>(string value, JsonConverter converter) public static void DoesNotDeserialize<T>(string value, JsonConverter converter)
{ {
var serializerSettings = CreateSettings<T>(converter); var serializerSettings = CreateSettings(converter);
Assert.ThrowsAny<JsonException>(() => JsonConvert.DeserializeObject<Tuple<T>>($"{{ \"Item1\": \"{value}\" }}", serializerSettings)); Assert.ThrowsAny<JsonException>(() => JsonConvert.DeserializeObject<Tuple<T>>($"{{ \"Item1\": \"{value}\" }}", serializerSettings));
} }
private static JsonSerializerSettings CreateSettings<T>(JsonConverter converter) private static JsonSerializerSettings CreateSettings(JsonConverter converter)
{ {
var serializerSettings = new JsonSerializerSettings(); var serializerSettings = new JsonSerializerSettings();

Loading…
Cancel
Save