Browse Source

Refactorings.

pull/342/head
Sebastian Stehle 7 years ago
parent
commit
9b06a1a7ef
  1. 4
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs
  2. 4
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs
  3. 78
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs
  4. 2
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs
  5. 6
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs
  6. 34
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  7. 33
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs
  8. 32
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs
  9. 73
      src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs
  10. 64
      src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
  11. 207
      src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs
  12. 65
      src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs
  13. 3
      src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs
  14. 21
      src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs
  15. 2
      src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs
  16. 4
      src/Squidex.Infrastructure/EventSourcing/Envelope.cs
  17. 12
      src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs
  18. 9
      src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs
  19. 19
      src/Squidex.Infrastructure/InstantExtensions.cs
  20. 5
      src/Squidex.Infrastructure/Log/IArrayWriter.cs
  21. 5
      src/Squidex.Infrastructure/Log/IObjectWriter.cs
  22. 3
      src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs
  23. 29
      src/Squidex.Infrastructure/Log/JsonLogWriter.cs
  24. 17
      src/Squidex.Infrastructure/Log/TimestampLogAppender.cs
  25. 21
      src/Squidex.Infrastructure/RetryWindow.cs
  26. 5
      src/Squidex.Infrastructure/States/StoreExtensions.cs
  27. 4
      src/Squidex/Config/Domain/RuleServices.cs
  28. 2
      tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs
  29. 4
      tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
  30. 131
      tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs
  31. 4
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs
  32. 41
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerTests.cs
  33. 18
      tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs
  34. 42
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerTests.cs
  35. 6
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  36. 17
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs
  37. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs
  38. 16
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs
  39. 3
      tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs
  40. 22
      tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs
  41. 2
      tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs
  42. 6
      tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs
  43. 6
      tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs
  44. 4
      tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs
  45. 23
      tests/Squidex.Infrastructure.Tests/InstantExtensions.cs
  46. 5
      tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs
  47. 6
      tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs
  48. 37
      tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs
  49. 10
      tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs
  50. 42
      tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs
  51. 12
      tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs

4
src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs

@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{
internal sealed class ClientPool<TKey, TClient>
{
private static readonly TimeSpan TTL = TimeSpan.FromMinutes(30);
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(30);
private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly Func<TKey, Task<TClient>> factory;
@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{
client = await factory(key);
memoryCache.Set(key, client, TTL);
memoryCache.Set(key, client, CacheDuration);
}
return client;

4
src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs

@ -9,9 +9,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
{
public sealed class EnrichedUsageExceededEvent : EnrichedEvent
{
public long Current { get; set; }
public long CallsCurrent { get; set; }
public long Limit { get; set; }
public long CallsLimit { get; set; }
public override long Partition
{

78
src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs

@ -0,0 +1,78 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Core.HandleRules
{
public sealed class EventEnricher : IEventEnricher
{
private static readonly TimeSpan UserCacheDuration = TimeSpan.FromMinutes(10);
private readonly IMemoryCache userCache;
private readonly IUserResolver userResolver;
public EventEnricher(IMemoryCache userCache, IUserResolver userResolver)
{
Guard.NotNull(userCache, nameof(userCache));
Guard.NotNull(userResolver, nameof(userResolver));
this.userCache = userCache;
this.userResolver = userResolver;
}
public async Task EnrichAsync(EnrichedEvent enrichedEvent, Envelope<AppEvent> @event)
{
enrichedEvent.Timestamp = @event.Headers.Timestamp();
if (enrichedEvent is EnrichedUserEvent userEvent)
{
if (@event.Payload is SquidexEvent squidexEvent)
{
userEvent.Actor = squidexEvent.Actor;
}
userEvent.User = await FindUserAsync(userEvent.Actor);
}
enrichedEvent.AppId = @event.Payload.AppId;
}
private Task<IUser> FindUserAsync(RefToken actor)
{
var key = $"EventEnrichers_Users_${actor.Identifier}";
return userCache.GetOrCreateAsync(key, async x =>
{
x.AbsoluteExpirationRelativeToNow = UserCacheDuration;
IUser user;
try
{
user = await userResolver.FindByIdOrEmailAsync(actor.Identifier);
}
catch
{
user = null;
}
if (user == null && actor.Type.Equals(RefTokenType.Client, StringComparison.OrdinalIgnoreCase))
{
user = new ClientUser(actor);
}
return user;
});
}
}
}

2
src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs

@ -14,6 +14,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{
public interface IEventEnricher
{
Task<EnrichedEvent> EnrichAsync(Envelope<AppEvent> @event);
Task EnrichAsync(EnrichedEvent enrichedEvent, Envelope<AppEvent> @event);
}
}

6
src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs

@ -6,8 +6,10 @@
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Core.HandleRules
@ -16,8 +18,10 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{
Type TriggerType { get; }
Task<EnrichedEvent> CreateEnrichedEventAsync(Envelope<AppEvent> @event);
bool Trigger(EnrichedEvent @event, RuleTrigger trigger);
bool Trigger(IEvent @event, RuleTrigger trigger, Guid ruleId);
bool Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId);
}
}

34
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs

@ -7,7 +7,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -71,6 +70,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return null;
}
var typed = @event.To<AppEvent>();
var actionType = rule.Action.GetType();
if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler))
@ -83,11 +84,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return null;
}
if (!triggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId))
{
return null;
}
var now = clock.GetCurrentInstant();
var eventTime =
@ -102,9 +98,21 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return null;
}
if (!triggerHandler.Trigger(typed.Payload, rule.Trigger, ruleId))
{
return null;
}
var appEventEnvelope = @event.To<AppEvent>();
var enrichedEvent = await eventEnricher.EnrichAsync(appEventEnvelope);
var enrichedEvent = await triggerHandler.CreateEnrichedEventAsync(appEventEnvelope);
if (enrichedEvent == null)
{
return null;
}
await eventEnricher.EnrichAsync(enrichedEvent, typed);
if (!triggerHandler.Trigger(enrichedEvent, rule.Trigger))
{
@ -137,7 +145,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
try
{
var actionType = typeNameRegistry.GetType(actionName);
var actionWatch = Stopwatch.StartNew();
var actionWatch = ValueStopwatch.StartNew();
var actionHandler = ruleActionHandlers[actionType];
@ -145,12 +153,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules
var result = await actionHandler.ExecuteJobAsync(deserialized);
actionWatch.Stop();
var elapsed = TimeSpan.FromMilliseconds(actionWatch.Stop());
var dumpBuilder = new StringBuilder(result.Dump);
dumpBuilder.AppendLine();
dumpBuilder.AppendFormat("Elapsed {0}.", actionWatch.Elapsed);
dumpBuilder.AppendFormat("Elapsed {0}.", elapsed);
dumpBuilder.AppendLine();
if (result.Exception is TimeoutException || result.Exception is OperationCanceledException)
@ -158,15 +166,15 @@ namespace Squidex.Domain.Apps.Core.HandleRules
dumpBuilder.AppendLine();
dumpBuilder.AppendLine("Action timed out.");
return (dumpBuilder.ToString(), RuleResult.Timeout, actionWatch.Elapsed);
return (dumpBuilder.ToString(), RuleResult.Timeout, elapsed);
}
else if (result.Exception != null)
{
return (dumpBuilder.ToString(), RuleResult.Failed, actionWatch.Elapsed);
return (dumpBuilder.ToString(), RuleResult.Failed, elapsed);
}
else
{
return (dumpBuilder.ToString(), RuleResult.Success, actionWatch.Elapsed);
return (dumpBuilder.ToString(), RuleResult.Success, elapsed);
}
}
catch (Exception ex)

33
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs

@ -6,15 +6,19 @@
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
#pragma warning disable IDE0019 // Use pattern matching
namespace Squidex.Domain.Apps.Core.HandleRules
{
public abstract class RuleTriggerHandler<TTrigger, TEvent, TEnrichedEvent> : IRuleTriggerHandler
where TTrigger : RuleTrigger
where TEvent : IEvent
where TEvent : AppEvent
where TEnrichedEvent : EnrichedEvent
{
public Type TriggerType
@ -22,16 +26,37 @@ namespace Squidex.Domain.Apps.Core.HandleRules
get { return typeof(TTrigger); }
}
async Task<EnrichedEvent> IRuleTriggerHandler.CreateEnrichedEventAsync(Envelope<AppEvent> @event)
{
return await CreateEnrichedEventAsync(@event.To<TEvent>());
}
bool IRuleTriggerHandler.Trigger(EnrichedEvent @event, RuleTrigger trigger)
{
return @event is TEnrichedEvent e && Trigger(e, (TTrigger)trigger);
var typed = @event as TEnrichedEvent;
if (typed != null)
{
return Trigger(typed, (TTrigger)trigger);
}
return false;
}
bool IRuleTriggerHandler.Trigger(IEvent @event, RuleTrigger trigger, Guid ruleId)
bool IRuleTriggerHandler.Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId)
{
return @event is TEvent e && Trigger(e, (TTrigger)trigger, ruleId);
var typed = @event as TEvent;
if (typed != null)
{
return Trigger(typed, (TTrigger)trigger, ruleId);
}
return false;
}
protected abstract Task<TEnrichedEvent> CreateEnrichedEventAsync(Envelope<TEvent> @event);
protected abstract bool Trigger(TEnrichedEvent @event, TTrigger trigger);
protected virtual bool Trigger(TEvent @event, TTrigger trigger, Guid ruleId)

32
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs

@ -1,32 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.HandleRules.Triggers
{
public sealed class AssetChangedTriggerHandler : RuleTriggerHandler<AssetChangedTriggerV2, AssetEvent, EnrichedAssetEvent>
{
private readonly IScriptEngine scriptEngine;
public AssetChangedTriggerHandler(IScriptEngine scriptEngine)
{
Guard.NotNull(scriptEngine, nameof(scriptEngine));
this.scriptEngine = scriptEngine;
}
protected override bool Trigger(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger)
{
return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition);
}
}
}

73
src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs

@ -0,0 +1,73 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetChangedTriggerHandler : RuleTriggerHandler<AssetChangedTriggerV2, AssetEvent, EnrichedAssetEvent>
{
private readonly IScriptEngine scriptEngine;
private readonly IGrainFactory grainFactory;
public AssetChangedTriggerHandler(IScriptEngine scriptEngine, IGrainFactory grainFactory)
{
Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(grainFactory, nameof(grainFactory));
this.scriptEngine = scriptEngine;
this.grainFactory = grainFactory;
}
protected override async Task<EnrichedAssetEvent> CreateEnrichedEventAsync(Envelope<AssetEvent> @event)
{
var result = new EnrichedAssetEvent();
var asset =
(await grainFactory
.GetGrain<IAssetGrain>(@event.Payload.AssetId)
.GetStateAsync(@event.Headers.EventStreamNumber())).Value;
SimpleMapper.Map(asset, result);
switch (@event.Payload)
{
case AssetCreated _:
result.Type = EnrichedAssetEventType.Created;
break;
case AssetRenamed _:
result.Type = EnrichedAssetEventType.Renamed;
break;
case AssetUpdated _:
result.Type = EnrichedAssetEventType.Updated;
break;
case AssetDeleted _:
result.Type = EnrichedAssetEventType.Deleted;
break;
}
result.Name = $"Asset{result.Type}";
return result;
}
protected override bool Trigger(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger)
{
return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition);
}
}
}

64
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs → src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs

@ -6,23 +6,83 @@
// ==========================================================================
using System;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.HandleRules.Triggers
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentChangedTriggerHandler : RuleTriggerHandler<ContentChangedTriggerV2, ContentEvent, EnrichedContentEvent>
{
private readonly IScriptEngine scriptEngine;
private readonly IGrainFactory grainFactory;
public ContentChangedTriggerHandler(IScriptEngine scriptEngine)
public ContentChangedTriggerHandler(IScriptEngine scriptEngine, IGrainFactory grainFactory)
{
Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(grainFactory, nameof(grainFactory));
this.scriptEngine = scriptEngine;
this.grainFactory = grainFactory;
}
protected override async Task<EnrichedContentEvent> CreateEnrichedEventAsync(Envelope<ContentEvent> @event)
{
var result = new EnrichedContentEvent();
var content =
(await grainFactory
.GetGrain<IContentGrain>(@event.Payload.ContentId)
.GetStateAsync(@event.Headers.EventStreamNumber())).Value;
SimpleMapper.Map(content, result);
result.Data = content.Data ?? content.DataDraft;
switch (@event.Payload)
{
case ContentCreated _:
result.Type = EnrichedContentEventType.Created;
break;
case ContentDeleted _:
result.Type = EnrichedContentEventType.Deleted;
break;
case ContentChangesPublished _:
case ContentUpdated _:
result.Type = EnrichedContentEventType.Updated;
break;
case ContentStatusChanged contentStatusChanged:
switch (contentStatusChanged.Change)
{
case StatusChange.Published:
result.Type = EnrichedContentEventType.Published;
break;
case StatusChange.Unpublished:
result.Type = EnrichedContentEventType.Unpublished;
break;
case StatusChange.Archived:
result.Type = EnrichedContentEventType.Archived;
break;
case StatusChange.Restored:
result.Type = EnrichedContentEventType.Restored;
break;
}
break;
}
result.Name = $"{content.SchemaId.Name.ToPascalCase()}{result.Type}";
return result;
}
protected override bool Trigger(ContentEvent @event, ContentChangedTriggerV2 trigger, Guid ruleId)

207
src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs

@ -1,207 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Orleans;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Entities.Rules
{
public sealed class EventEnricher : IEventEnricher
{
private static readonly TimeSpan UserCacheDuration = TimeSpan.FromMinutes(10);
private readonly IGrainFactory grainFactory;
private readonly IMemoryCache userCache;
private readonly IUserResolver userResolver;
public EventEnricher(IGrainFactory grainFactory, IMemoryCache userCache, IUserResolver userResolver)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
Guard.NotNull(userCache, nameof(userCache));
Guard.NotNull(userResolver, nameof(userResolver));
this.grainFactory = grainFactory;
this.userCache = userCache;
this.userResolver = userResolver;
}
public async Task<EnrichedEvent> EnrichAsync(Envelope<AppEvent> @event)
{
Guard.NotNull(@event, nameof(@event));
switch (@event.Payload)
{
case ContentEvent contentEvent:
{
var result = new EnrichedContentEvent();
await Task.WhenAll(
EnrichContentAsync(result, contentEvent, @event),
EnrichDefaultAsync(result, @event));
return result;
}
case AssetEvent assetEvent:
{
var result = new EnrichedAssetEvent();
await Task.WhenAll(
EnrichAssetAsync(result, assetEvent, @event),
EnrichDefaultAsync(result, @event));
return result;
}
case AppUsageExceeded usageExceeded:
{
var result = new EnrichedUsageExceededEvent { Current = usageExceeded.CallsCurrent, Limit = usageExceeded.CallsLimit };
await EnrichDefaultAsync(result, @event);
return result;
}
}
return null;
}
private async Task EnrichAssetAsync(EnrichedAssetEvent result, AssetEvent assetEvent, Envelope<AppEvent> @event)
{
var asset =
(await grainFactory
.GetGrain<IAssetGrain>(assetEvent.AssetId)
.GetStateAsync(@event.Headers.EventStreamNumber())).Value;
SimpleMapper.Map(asset, result);
switch (assetEvent)
{
case AssetCreated _:
result.Type = EnrichedAssetEventType.Created;
break;
case AssetRenamed _:
result.Type = EnrichedAssetEventType.Renamed;
break;
case AssetUpdated _:
result.Type = EnrichedAssetEventType.Updated;
break;
case AssetDeleted _:
result.Type = EnrichedAssetEventType.Deleted;
break;
}
result.Name = $"Asset{result.Type}";
}
private async Task EnrichContentAsync(EnrichedContentEvent result, ContentEvent contentEvent, Envelope<AppEvent> @event)
{
var content =
(await grainFactory
.GetGrain<IContentGrain>(contentEvent.ContentId)
.GetStateAsync(@event.Headers.EventStreamNumber())).Value;
SimpleMapper.Map(content, result);
result.Data = content.Data ?? content.DataDraft;
switch (contentEvent)
{
case ContentCreated _:
result.Type = EnrichedContentEventType.Created;
break;
case ContentDeleted _:
result.Type = EnrichedContentEventType.Deleted;
break;
case ContentChangesPublished _:
case ContentUpdated _:
result.Type = EnrichedContentEventType.Updated;
break;
case ContentStatusChanged contentStatusChanged:
switch (contentStatusChanged.Change)
{
case StatusChange.Published:
result.Type = EnrichedContentEventType.Published;
break;
case StatusChange.Unpublished:
result.Type = EnrichedContentEventType.Unpublished;
break;
case StatusChange.Archived:
result.Type = EnrichedContentEventType.Archived;
break;
case StatusChange.Restored:
result.Type = EnrichedContentEventType.Restored;
break;
}
break;
}
result.Name = $"{content.SchemaId.Name.ToPascalCase()}{result.Type}";
}
private async Task EnrichDefaultAsync(EnrichedEvent result, Envelope<AppEvent> @event)
{
result.Timestamp = @event.Headers.Timestamp();
if (result is EnrichedUserEvent userEvent)
{
if (@event.Payload is SquidexEvent squidexEvent)
{
userEvent.Actor = squidexEvent.Actor;
}
userEvent.User = await FindUserAsync(userEvent.Actor);
}
if (@event.Payload is AppEvent appEvent)
{
result.AppId = appEvent.AppId;
}
}
private Task<IUser> FindUserAsync(RefToken actor)
{
var key = $"EventEnrichers_Users_${actor.Identifier}";
return userCache.GetOrCreateAsync(key, async x =>
{
x.AbsoluteExpirationRelativeToNow = UserCacheDuration;
IUser user;
try
{
user = await userResolver.FindByIdOrEmailAsync(actor.Identifier);
}
catch
{
user = null;
}
if (user == null && actor.Type.Equals(RefTokenType.Client, StringComparison.OrdinalIgnoreCase))
{
user = new ClientUser(actor);
}
return user;
});
}
}
}

65
src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs

@ -0,0 +1,65 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Orleans;
namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
{
public sealed class UsageTrackerCommandMiddleware : ICommandMiddleware
{
private readonly IUsageTrackerGrain usageTrackerGrain;
public UsageTrackerCommandMiddleware(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
usageTrackerGrain = grainFactory.GetGrain<IUsageTrackerGrain>(SingleGrain.Id);
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
switch (context.Command)
{
case DeleteRule deleteRule:
await usageTrackerGrain.RemoveTargetAsync(deleteRule.RuleId);
break;
case EnableRule enableRule:
await usageTrackerGrain.ActivateTargetAsync(enableRule.RuleId);
break;
case DisableRule disableRule:
await usageTrackerGrain.DeactivateTargetAsync(disableRule.RuleId);
break;
case CreateRule createRule:
{
if (createRule.Trigger is UsageTrigger createdTrigger)
{
await usageTrackerGrain.AddTargetAsync(createRule.RuleId, createRule.AppId, createdTrigger.Limit);
}
break;
}
case UpdateRule ruleUpdated:
if (ruleUpdated.Trigger is UsageTrigger updatedTrigger)
{
await usageTrackerGrain.UpdateTargetAsync(ruleUpdated.RuleId, updatedTrigger.Limit);
}
break;
}
await next();
}
}
}

3
src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs

@ -12,6 +12,7 @@ using Orleans;
using Orleans.Runtime;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.UsageTracking;
@ -82,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
RuleId = kvp.Key
};
await Persistence.WriteEventAsync(@event);
await Persistence.WriteEventAsync(Envelope.Create<IEvent>(@event));
}
}
}

21
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/UsageTriggerHandler.cs → src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs

@ -5,17 +5,34 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Core.HandleRules.Triggers
namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
{
public sealed class UsageTriggerHandler : RuleTriggerHandler<UsageTrigger, AppUsageExceeded, EnrichedUsageExceededEvent>
{
private const string EventName = "Usage exceeeded";
protected override Task<EnrichedUsageExceededEvent> CreateEnrichedEventAsync(Envelope<AppUsageExceeded> @event)
{
var result = new EnrichedUsageExceededEvent
{
CallsCurrent = @event.Payload.CallsCurrent,
CallsLimit = @event.Payload.CallsLimit,
Name = EventName
};
return Task.FromResult(result);
}
protected override bool Trigger(EnrichedUsageExceededEvent @event, UsageTrigger trigger)
{
return @event.Limit == trigger.Limit;
return @event.CallsLimit == trigger.Limit;
}
}
}

2
src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs

@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Events
return headers.GetGuid(SquidexHeaders.AppId);
}
public static Envelope<T> SetAppId<T>(this Envelope<T> envelope, Guid value) where T : class
public static Envelope<T> SetAppId<T>(this Envelope<T> envelope, Guid value) where T : class, IEvent
{
envelope.Headers.Add(SquidexHeaders.AppId, value.ToString());

4
src/Squidex.Infrastructure/EventSourcing/Envelope.cs

@ -12,12 +12,12 @@ namespace Squidex.Infrastructure.EventSourcing
{
public static class Envelope
{
public static Envelope<IEvent> Create<TPayload>(TPayload payload) where TPayload : IEvent
public static Envelope<TPayload> Create<TPayload>(TPayload payload) where TPayload : class, IEvent
{
var eventId = Guid.NewGuid();
var envelope =
new Envelope<IEvent>(payload)
new Envelope<TPayload>(payload)
.SetEventId(eventId)
.SetTimestamp(SystemClock.Instance.GetCurrentInstant());

12
src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs

@ -20,7 +20,7 @@ namespace Squidex.Infrastructure.EventSourcing
return headers.GetString(CommonHeaders.EventNumber);
}
public static Envelope<T> SetEventPosition<T>(this Envelope<T> envelope, string value) where T : class
public static Envelope<T> SetEventPosition<T>(this Envelope<T> envelope, string value) where T : class, IEvent
{
envelope.Headers.Add(CommonHeaders.EventNumber, value);
@ -32,7 +32,7 @@ namespace Squidex.Infrastructure.EventSourcing
return headers.GetLong(CommonHeaders.EventStreamNumber);
}
public static Envelope<T> SetEventStreamNumber<T>(this Envelope<T> envelope, long value) where T : class
public static Envelope<T> SetEventStreamNumber<T>(this Envelope<T> envelope, long value) where T : class, IEvent
{
envelope.Headers.Add(CommonHeaders.EventStreamNumber, value);
@ -44,7 +44,7 @@ namespace Squidex.Infrastructure.EventSourcing
return headers.GetGuid(CommonHeaders.CommitId);
}
public static Envelope<T> SetCommitId<T>(this Envelope<T> envelope, Guid value) where T : class
public static Envelope<T> SetCommitId<T>(this Envelope<T> envelope, Guid value) where T : class, IEvent
{
envelope.Headers.Add(CommonHeaders.CommitId, value.ToString());
@ -56,7 +56,7 @@ namespace Squidex.Infrastructure.EventSourcing
return headers.GetGuid(CommonHeaders.AggregateId);
}
public static Envelope<T> SetAggregateId<T>(this Envelope<T> envelope, Guid value) where T : class
public static Envelope<T> SetAggregateId<T>(this Envelope<T> envelope, Guid value) where T : class, IEvent
{
envelope.Headers.Add(CommonHeaders.AggregateId, value.ToString());
@ -68,7 +68,7 @@ namespace Squidex.Infrastructure.EventSourcing
return headers.GetGuid(CommonHeaders.EventId);
}
public static Envelope<T> SetEventId<T>(this Envelope<T> envelope, Guid value) where T : class
public static Envelope<T> SetEventId<T>(this Envelope<T> envelope, Guid value) where T : class, IEvent
{
envelope.Headers.Add(CommonHeaders.EventId, value.ToString());
@ -80,7 +80,7 @@ namespace Squidex.Infrastructure.EventSourcing
return headers.GetInstant(CommonHeaders.Timestamp);
}
public static Envelope<T> SetTimestamp<T>(this Envelope<T> envelope, Instant value) where T : class
public static Envelope<T> SetTimestamp<T>(this Envelope<T> envelope, Instant value) where T : class, IEvent
{
envelope.Headers.Add(CommonHeaders.Timestamp, value.ToString());

9
src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs

@ -7,7 +7,7 @@
namespace Squidex.Infrastructure.EventSourcing
{
public class Envelope<T> where T : class
public class Envelope<T> where T : class, IEvent
{
private readonly EnvelopeHeaders headers;
private readonly T payload;
@ -30,9 +30,14 @@ namespace Squidex.Infrastructure.EventSourcing
this.headers = headers ?? new EnvelopeHeaders();
}
public Envelope<TOther> To<TOther>() where TOther : class
public Envelope<TOther> To<TOther>() where TOther : class, IEvent
{
return new Envelope<TOther>(payload as TOther, headers.Clone());
}
public static implicit operator Envelope<IEvent>(Envelope<T> source)
{
return source == null ? source : new Envelope<IEvent>(source.payload, source.headers);
}
}
}

19
src/Squidex.Infrastructure/InstantExtensions.cs

@ -0,0 +1,19 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NodaTime;
namespace Squidex.Infrastructure
{
public static class InstantExtensions
{
public static Instant WithoutMs(this Instant value)
{
return Instant.FromUnixTimeSeconds(value.ToUnixTimeSeconds());
}
}
}

5
src/Squidex.Infrastructure/Log/IArrayWriter.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using NodaTime;
namespace Squidex.Infrastructure.Log
{
@ -21,9 +22,7 @@ namespace Squidex.Infrastructure.Log
IArrayWriter WriteValue(TimeSpan value);
IArrayWriter WriteValue(DateTime value);
IArrayWriter WriteValue(DateTimeOffset value);
IArrayWriter WriteValue(Instant value);
IArrayWriter WriteObject(Action<IObjectWriter> objectWriter);
}

5
src/Squidex.Infrastructure/Log/IObjectWriter.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using NodaTime;
namespace Squidex.Infrastructure.Log
{
@ -21,9 +22,7 @@ namespace Squidex.Infrastructure.Log
IObjectWriter WriteProperty(string property, TimeSpan value);
IObjectWriter WriteProperty(string property, DateTime value);
IObjectWriter WriteProperty(string property, DateTimeOffset value);
IObjectWriter WriteProperty(string property, Instant value);
IObjectWriter WriteObject(string property, Action<IObjectWriter> objectWriter);

3
src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs

@ -10,6 +10,7 @@ using System.Collections.Concurrent;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using NodaTime;
namespace Squidex.Infrastructure.Log.Internal
{
@ -70,7 +71,7 @@ namespace Squidex.Infrastructure.Log.Internal
AutoFlush = true
};
writer.WriteLine($"--- Started Logging {DateTime.UtcNow} ---", 1);
writer.WriteLine($"--- Started Logging {SystemClock.Instance.GetCurrentInstant()} ---", 1);
}
catch (Exception ex)
{

29
src/Squidex.Infrastructure/Log/JsonLogWriter.cs

@ -6,9 +6,9 @@
// ==========================================================================
using System;
using System.Globalization;
using System.IO;
using Newtonsoft.Json;
using NodaTime;
namespace Squidex.Infrastructure.Log
{
@ -73,23 +73,16 @@ namespace Squidex.Infrastructure.Log
return this;
}
IArrayWriter IArrayWriter.WriteValue(DateTime value)
IArrayWriter IArrayWriter.WriteValue(Instant value)
{
jsonWriter.WriteValue(value.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture));
return this;
}
IArrayWriter IArrayWriter.WriteValue(DateTimeOffset value)
{
jsonWriter.WriteValue(value.ToString("o", CultureInfo.InvariantCulture));
jsonWriter.WriteValue(value.ToString());
return this;
}
IArrayWriter IArrayWriter.WriteValue(TimeSpan value)
{
jsonWriter.WriteValue(value);
jsonWriter.WriteValue(value.ToString());
return this;
}
@ -126,18 +119,10 @@ namespace Squidex.Infrastructure.Log
return this;
}
IObjectWriter IObjectWriter.WriteProperty(string property, DateTime value)
IObjectWriter IObjectWriter.WriteProperty(string property, Instant value)
{
jsonWriter.WritePropertyName(Format(property));
jsonWriter.WriteValue(value.ToString("o", CultureInfo.InvariantCulture));
return this;
}
IObjectWriter IObjectWriter.WriteProperty(string property, DateTimeOffset value)
{
jsonWriter.WritePropertyName(Format(property));
jsonWriter.WriteValue(value.ToString("o", CultureInfo.InvariantCulture));
jsonWriter.WriteValue(value.ToString());
return this;
}
@ -145,7 +130,7 @@ namespace Squidex.Infrastructure.Log
IObjectWriter IObjectWriter.WriteProperty(string property, TimeSpan value)
{
jsonWriter.WritePropertyName(Format(property));
jsonWriter.WriteValue(value);
jsonWriter.WriteValue(value.ToString());
return this;
}

17
src/Squidex.Infrastructure/Log/TimestampLogAppender.cs

@ -5,29 +5,22 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using NodaTime;
namespace Squidex.Infrastructure.Log
{
public sealed class TimestampLogAppender : ILogAppender
{
private readonly Func<DateTime> timestamp;
private readonly IClock clock;
public TimestampLogAppender()
: this(() => DateTime.UtcNow)
public TimestampLogAppender(IClock clock = null)
{
}
public TimestampLogAppender(Func<DateTime> timestamp)
{
Guard.NotNull(timestamp, nameof(timestamp));
this.timestamp = timestamp;
this.clock = clock ?? SystemClock.Instance;
}
public void Append(IObjectWriter writer)
{
writer.WriteProperty("timestamp", timestamp());
writer.WriteProperty("timestamp", clock.GetCurrentInstant());
}
}
}

21
src/Squidex.Infrastructure/RetryWindow.cs

@ -7,19 +7,23 @@
using System;
using System.Collections.Generic;
using NodaTime;
namespace Squidex.Infrastructure
{
public sealed class RetryWindow
{
private readonly TimeSpan windowDuration;
private readonly Duration windowDuration;
private readonly int windowSize;
private readonly Queue<DateTime> retries = new Queue<DateTime>();
private readonly Queue<Instant> retries = new Queue<Instant>();
private readonly IClock clock;
public RetryWindow(TimeSpan windowDuration, int windowSize)
public RetryWindow(TimeSpan windowDuration, int windowSize, IClock clock = null)
{
this.windowDuration = windowDuration;
this.windowDuration = Duration.FromTimeSpan(windowDuration);
this.windowSize = windowSize + 1;
this.clock = clock ?? SystemClock.Instance;
}
public void Reset()
@ -29,19 +33,16 @@ namespace Squidex.Infrastructure
public bool CanRetryAfterFailure()
{
return CanRetryAfterFailure(DateTime.UtcNow);
}
var now = clock.GetCurrentInstant();
public bool CanRetryAfterFailure(DateTime utcNow)
{
retries.Enqueue(utcNow);
retries.Enqueue(now);
while (retries.Count > windowSize)
{
retries.Dequeue();
}
return retries.Count < windowSize || (retries.Count > 0 && (utcNow - retries.Peek()) > windowDuration);
return retries.Count < windowSize || (retries.Count > 0 && (now - retries.Peek()) > windowDuration);
}
}
}

5
src/Squidex.Infrastructure/States/StoreExtensions.cs

@ -17,11 +17,6 @@ namespace Squidex.Infrastructure.States
return persistence.WriteEventsAsync(new[] { @event });
}
public static Task WriteEventAsync<T>(this IPersistence<T> persistence, IEvent @event)
{
return persistence.WriteEventsAsync(new[] { Envelope.Create(@event) });
}
public static Task ClearSnapshotsAsync<TKey, TSnapshot>(this IStore<TKey> store)
{
return store.GetSnapshotStore<TSnapshot>().ClearAsync();

4
src/Squidex/Config/Domain/RuleServices.cs

@ -7,8 +7,10 @@
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.Triggers;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Rules.UsageTracking;
using Squidex.Extensions.Actions;
using Squidex.Infrastructure.EventSourcing;

2
tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs

@ -193,7 +193,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EnrichContent
private static Instant FutureDays(int days)
{
return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days));
return SystemClock.Instance.GetCurrentInstant().WithoutMs().Plus(Duration.FromDays(days));
}
}
}

4
tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs

@ -105,9 +105,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[InlineData("Script(`Date: ${formatDate(event.timestamp, 'yyyy-MM-dd')}, Full: ${formatDate(event.timestamp, 'yyyy-MM-dd-hh-mm-ss')}`)")]
public void Should_replace_timestamp_information_from_event(string script)
{
var now = DateTime.UtcNow;
var now = SystemClock.Instance.GetCurrentInstant();
var envelope = new EnrichedContentEvent { Timestamp = Instant.FromDateTimeUtc(now) };
var envelope = new EnrichedContentEvent { Timestamp = now };
var result = sut.Format(script, envelope);

131
tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs

@ -49,7 +49,6 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
public sealed class ValidAction : RuleAction
{
public int Value { get; set; }
}
public sealed class ValidData
@ -70,8 +69,8 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
typeNameRegistry.Map(typeof(ContentCreated));
typeNameRegistry.Map(typeof(ValidAction), actionName);
A.CallTo(() => eventEnricher.EnrichAsync(A<Envelope<AppEvent>>.Ignored))
.Returns(new EnrichedContentEvent { AppId = appId });
A.CallTo(() => clock.GetCurrentInstant())
.Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs());
A.CallTo(() => ruleActionHandler.ActionType)
.Returns(typeof(ValidAction));
@ -86,58 +85,67 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
}
[Fact]
public async Task Should_not_create_if_rule_disabled()
public async Task Should_not_create_job_if_rule_disabled()
{
var rule = ValidRule().Disable();
var ruleEvent = Envelope.Create(new ContentCreated());
var @event = Envelope.Create(new ContentCreated());
var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent);
var job = await sut.CreateJobAsync(ValidRule().Disable(), ruleId, @event);
Assert.Null(job);
A.CallTo(() => eventEnricher.EnrichAsync(A<Envelope<AppEvent>>.Ignored))
A.CallTo(() => ruleTriggerHandler.Trigger(A<AppEvent>.Ignored, A<RuleTrigger>.Ignored, ruleId))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_create_job_for_invalid_event()
{
var rule = ValidRule();
var ruleEvent = Envelope.Create(new InvalidEvent());
var @event = Envelope.Create(new InvalidEvent());
var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent);
var job = await sut.CreateJobAsync(ValidRule(), ruleId, @event);
Assert.Null(job);
A.CallTo(() => eventEnricher.EnrichAsync(A<Envelope<AppEvent>>.Ignored))
A.CallTo(() => ruleTriggerHandler.Trigger(A<AppEvent>.Ignored, A<RuleTrigger>.Ignored, ruleId))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_create_job_if_no_trigger_handler_registered()
{
var rule = new Rule(new InvalidTrigger(), new ValidAction());
var ruleEvent = Envelope.Create(new ContentCreated());
var @event = Envelope.Create(new ContentCreated());
var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent);
var job = await sut.CreateJobAsync(RuleInvalidTrigger(), ruleId, @event);
Assert.Null(job);
A.CallTo(() => eventEnricher.EnrichAsync(A<Envelope<AppEvent>>.Ignored))
A.CallTo(() => ruleTriggerHandler.Trigger(A<AppEvent>.Ignored, A<RuleTrigger>.Ignored, ruleId))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_create_job_if_no_action_handler_registered()
{
var rule = new Rule(new ContentChangedTriggerV2(), new InvalidAction());
var ruleEvent = Envelope.Create(new ContentCreated());
var @event = Envelope.Create(new ContentCreated());
var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent);
var job = await sut.CreateJobAsync(RuleInvalidAction(), ruleId, @event);
Assert.Null(job);
A.CallTo(() => eventEnricher.EnrichAsync(A<Envelope<AppEvent>>.Ignored))
A.CallTo(() => ruleTriggerHandler.Trigger(A<AppEvent>.Ignored, A<RuleTrigger>.Ignored, ruleId))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_create_job_if_too_old()
{
var @event = Envelope.Create(new ContentCreated()).SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3)));
var job = await sut.CreateJobAsync(ValidRule(), ruleId, @event);
Assert.Null(job);
A.CallTo(() => ruleTriggerHandler.Trigger(A<AppEvent>.Ignored, A<RuleTrigger>.Ignored, ruleId))
.MustNotHaveHappened();
}
@ -145,87 +153,85 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
public async Task Should_not_create_job_if_not_triggered_with_precheck()
{
var rule = ValidRule();
var ruleEvent = Envelope.Create(new ContentCreated());
A.CallTo(() => ruleTriggerHandler.Trigger(A<IEvent>.Ignored, rule.Trigger, ruleId))
var @event = Envelope.Create(new ContentCreated());
A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId))
.Returns(false);
var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent);
var job = await sut.CreateJobAsync(rule, ruleId, @event);
Assert.Null(job);
A.CallTo(() => eventEnricher.EnrichAsync(A<Envelope<AppEvent>>.Ignored))
A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A<Envelope<AppEvent>>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_create_job_if_not_triggered()
public async Task Should_not_create_job_if_enriched_event_not_created()
{
var rule = ValidRule();
var ruleEvent = Envelope.Create(new ContentCreated());
A.CallTo(() => ruleTriggerHandler.Trigger(A<IEvent>.Ignored, rule.Trigger, ruleId))
var @event = Envelope.Create(new ContentCreated());
A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId))
.Returns(true);
A.CallTo(() => ruleTriggerHandler.Trigger(A<EnrichedEvent>.Ignored, rule.Trigger))
.Returns(false);
A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A<Envelope<AppEvent>>.That.Matches(x => x.Payload == @event.Payload)))
.Returns(Task.FromResult<EnrichedEvent>(null));
var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent);
var job = await sut.CreateJobAsync(rule, ruleId, @event);
Assert.Null(job);
}
[Fact]
public async Task Should_not_create_job_if_too_old()
public async Task Should_not_create_job_if_not_triggered()
{
var ruleEvent = new ContentCreated { SchemaId = schemaId, AppId = appId };
var rule = ValidRule();
var now = SystemClock.Instance.GetCurrentInstant();
var enrichedEvent = new EnrichedContentEvent { AppId = appId };
var rule = ValidRule();
var ruleEnvelope = Envelope.Create(ruleEvent);
var @event = Envelope.Create(new ContentCreated());
ruleEnvelope.SetTimestamp(now.Minus(Duration.FromDays(3)));
A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId))
.Returns(true);
A.CallTo(() => clock.GetCurrentInstant())
.Returns(now);
A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A<Envelope<AppEvent>>.That.Matches(x => x.Payload == @event.Payload)))
.Returns(enrichedEvent);
A.CallTo(() => ruleActionHandler.CreateJobAsync(A<EnrichedEvent>.Ignored, rule.Action))
.Returns((actionDescription, actionData));
A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger))
.Returns(false);
var job = await sut.CreateJobAsync(rule, ruleId, ruleEnvelope);
var job = await sut.CreateJobAsync(rule, ruleId, @event);
Assert.Null(job);
A.CallTo(() => eventEnricher.EnrichAsync(A<Envelope<AppEvent>>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_create_job_if_triggered()
{
var ruleEvent = new ContentCreated { SchemaId = schemaId, AppId = appId };
var now = Instant.FromUnixTimeSeconds(SystemClock.Instance.GetCurrentInstant().ToUnixTimeSeconds());
var now = clock.GetCurrentInstant();
var rule = ValidRule();
var ruleEnvelope = Envelope.Create(ruleEvent);
ruleEnvelope.SetTimestamp(now);
var enrichedEvent = new EnrichedContentEvent { AppId = appId };
A.CallTo(() => clock.GetCurrentInstant())
.Returns(now);
var @event = Envelope.Create(new ContentCreated()).SetTimestamp(now);
A.CallTo(() => ruleTriggerHandler.Trigger(A<EnrichedEvent>.Ignored, rule.Trigger))
A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId))
.Returns(true);
A.CallTo(() => ruleTriggerHandler.Trigger(A<IEvent>.Ignored, rule.Trigger, ruleId))
A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger))
.Returns(true);
A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A<Envelope<AppEvent>>.That.Matches(x => x.Payload == @event.Payload)))
.Returns(enrichedEvent);
A.CallTo(() => ruleActionHandler.CreateJobAsync(A<EnrichedEvent>.Ignored, rule.Action))
.Returns((actionDescription, new ValidData { Value = 10 }));
var job = await sut.CreateJobAsync(rule, ruleId, ruleEnvelope);
var job = await sut.CreateJobAsync(rule, ruleId, @event);
Assert.Equal(actionData, job.ActionData);
Assert.Equal(actionName, job.ActionName);
@ -234,9 +240,12 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
Assert.Equal(now, job.Created);
Assert.Equal(now.Plus(Duration.FromDays(2)), job.Expires);
Assert.Equal(ruleEvent.AppId.Id, job.AppId);
Assert.Equal(enrichedEvent.AppId.Id, job.AppId);
Assert.NotEqual(Guid.Empty, job.JobId);
A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent, A<Envelope<AppEvent>>.That.Matches(x => x.Payload == @event.Payload)))
.MustHaveHappened();
}
[Fact]
@ -250,7 +259,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
Assert.Equal(RuleResult.Success, result.Result);
Assert.True(result.Elapsed >= TimeSpan.Zero);
Assert.StartsWith(actionDump, result.Dump, StringComparison.OrdinalIgnoreCase);
Assert.True(result.Dump.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase));
}
[Fact]
@ -296,6 +305,16 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
Assert.Equal((ruleError.ToString(), RuleResult.Failed, TimeSpan.Zero), result);
}
private static Rule RuleInvalidAction()
{
return new Rule(new ContentChangedTriggerV2(), new InvalidAction());
}
private static Rule RuleInvalidTrigger()
{
return new Rule(new InvalidTrigger(), new ValidAction());
}
private static Rule ValidRule()
{
return new Rule(new ContentChangedTriggerV2(), new ValidAction());

4
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs

@ -5,12 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
using NodaTime;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
@ -95,7 +95,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static Instant FutureDays(int days)
{
return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days));
return SystemClock.Instance.GetCurrentInstant().WithoutMs().Plus(Duration.FromDays(days));
}
private static IJsonValue CreateValue(Instant v)

41
tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs → tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerTests.cs

@ -6,26 +6,34 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Orleans;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.HandleRules.Triggers;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Orleans;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers
#pragma warning disable SA1401 // Fields must be private
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetChangedTriggerTests
{
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>();
private readonly IRuleTriggerHandler sut;
public AssetChangedTriggerTests()
{
sut = new AssetChangedTriggerHandler(scriptEngine);
sut = new AssetChangedTriggerHandler(scriptEngine, grainFactory);
A.CallTo(() => scriptEngine.Evaluate("event", A<object>.Ignored, "true"))
.Returns(true);
@ -34,6 +42,33 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers
.Returns(false);
}
public static IEnumerable<object[]> TestEvents = new[]
{
new object[] { new AssetCreated(), EnrichedAssetEventType.Created },
new object[] { new AssetUpdated(), EnrichedAssetEventType.Updated },
new object[] { new AssetRenamed(), EnrichedAssetEventType.Renamed },
new object[] { new AssetDeleted(), EnrichedAssetEventType.Deleted }
};
[Theory]
[MemberData(nameof(TestEvents))]
public async Task Should_enrich_events(AssetEvent @event, EnrichedAssetEventType type)
{
var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12);
var assetGrain = A.Fake<IAssetGrain>();
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(@event.AssetId, null))
.Returns(assetGrain);
A.CallTo(() => assetGrain.GetStateAsync(12))
.Returns(A.Fake<IAssetEntity>().AsJ());
var result = await sut.CreateEnrichedEventAsync(envelope);
Assert.Equal(type, ((EnrichedAssetEvent)result).Type);
}
[Fact]
public void Should_not_trigger_precheck_when_event_type_not_correct()
{

18
tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs

@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
};
ValidationAssert.Throws(() => GuardComments.CanUpdate(events, command),
@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
};
Assert.Throws<DomainException>(() => GuardComments.CanUpdate(events, command));
@ -86,8 +86,8 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>(),
Envelope.Create(new CommentDeleted { CommentId = commentId }).To<CommentsEvent>()
Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>(),
Envelope.Create<CommentsEvent>(new CommentDeleted { CommentId = commentId }).To<CommentsEvent>()
};
Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanUpdate(events, command));
@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
};
GuardComments.CanUpdate(events, command);
@ -115,7 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
};
Assert.Throws<DomainException>(() => GuardComments.CanDelete(events, command));
@ -140,8 +140,8 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>(),
Envelope.Create(new CommentDeleted { CommentId = commentId }).To<CommentsEvent>()
Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }),
Envelope.Create<CommentsEvent>(new CommentDeleted { CommentId = commentId })
};
Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanDelete(events, command));
@ -155,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
};
GuardComments.CanDelete(events, command);

42
tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs → tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerTests.cs

@ -8,25 +8,31 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using FakeItEasy;
using Orleans;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.HandleRules.Triggers;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Orleans;
using Xunit;
#pragma warning disable SA1401 // Fields must be private
#pragma warning disable RECS0070
namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentChangedTriggerTests
{
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>();
private readonly IRuleTriggerHandler sut;
private readonly Guid ruleId = Guid.NewGuid();
private static readonly NamedId<Guid> SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1");
@ -34,7 +40,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers
public ContentChangedTriggerTests()
{
sut = new ContentChangedTriggerHandler(scriptEngine);
sut = new ContentChangedTriggerHandler(scriptEngine, grainFactory);
A.CallTo(() => scriptEngine.Evaluate("event", A<object>.Ignored, "true"))
.Returns(true);
@ -43,6 +49,36 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers
.Returns(false);
}
public static IEnumerable<object[]> TestEvents = new[]
{
new object[] { new ContentCreated(), EnrichedContentEventType.Created },
new object[] { new ContentUpdated(), EnrichedContentEventType.Updated },
new object[] { new ContentDeleted(), EnrichedContentEventType.Deleted },
new object[] { new ContentStatusChanged { Change = StatusChange.Archived }, EnrichedContentEventType.Archived },
new object[] { new ContentStatusChanged { Change = StatusChange.Published }, EnrichedContentEventType.Published },
new object[] { new ContentStatusChanged { Change = StatusChange.Restored }, EnrichedContentEventType.Restored },
new object[] { new ContentStatusChanged { Change = StatusChange.Unpublished }, EnrichedContentEventType.Unpublished }
};
[Theory]
[MemberData(nameof(TestEvents))]
public async Task Should_enrich_events(ContentEvent @event, EnrichedContentEventType type)
{
var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12);
var contentGrain = A.Fake<IContentGrain>();
A.CallTo(() => grainFactory.GetGrain<IContentGrain>(@event.ContentId, null))
.Returns(contentGrain);
A.CallTo(() => contentGrain.GetStateAsync(12))
.Returns(A.Fake<IContentEntity>().AsJ());
var result = await sut.CreateEnrichedEventAsync(envelope);
Assert.Equal(type, ((EnrichedContentEvent)result).Type);
}
[Fact]
public void Should_not_trigger_precheck_when_event_type_not_correct()
{

6
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

@ -12,7 +12,7 @@ using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using NodaTime.Extensions;
using NodaTime;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
@ -99,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
protected static IContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null)
{
var now = DateTime.UtcNow.ToInstant();
var now = SystemClock.Instance.GetCurrentInstant();
data = data ??
new NamedContentData()
@ -159,7 +159,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
protected static IAssetEntity CreateAsset(Guid id)
{
var now = DateTime.UtcNow.ToInstant();
var now = SystemClock.Instance.GetCurrentInstant();
var asset = new FakeAssetEntity
{

17
tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs

@ -12,6 +12,7 @@ using NodaTime;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Xunit;
@ -24,19 +25,15 @@ namespace Squidex.Domain.Apps.Entities.Rules
private readonly IClock clock = A.Fake<IClock>();
private readonly ISemanticLog log = A.Dummy<ISemanticLog>();
private readonly IRuleEventRepository ruleEventRepository = A.Fake<IRuleEventRepository>();
private readonly Instant now = SystemClock.Instance.GetCurrentInstant();
private readonly RuleService ruleService = A.Fake<RuleService>();
private readonly RuleDequeuerGrain sut;
public RuleDequeuerTests()
{
A.CallTo(() => clock.GetCurrentInstant()).Returns(now);
A.CallTo(() => clock.GetCurrentInstant())
.Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs());
sut = new RuleDequeuerGrain(
ruleService,
ruleEventRepository,
log,
clock);
sut = new RuleDequeuerGrain(ruleService, ruleEventRepository, log, clock);
}
[Theory]
@ -63,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
if (minutes > 0)
{
nextCall = now.Plus(Duration.FromMinutes(minutes));
nextCall = clock.GetCurrentInstant().Plus(Duration.FromMinutes(minutes));
}
await sut.HandleAsync(@event);
@ -81,12 +78,12 @@ namespace Squidex.Domain.Apps.Entities.Rules
JobId = Guid.NewGuid(),
ActionData = actionData,
ActionName = actionName,
Created = now
Created = clock.GetCurrentInstant()
};
A.CallTo(() => @event.Id).Returns(Guid.NewGuid());
A.CallTo(() => @event.Job).Returns(job);
A.CallTo(() => @event.Created).Returns(now);
A.CallTo(() => @event.Created).Returns(clock.GetCurrentInstant());
A.CallTo(() => @event.NumCalls).Returns(numCalls);
return @event;

2
tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs

@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
[Fact]
public async Task Should_update_repositories_on_with_jobs_from_sender()
{
var @event = Envelope.Create(new ContentCreated { AppId = appId });
var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId });
var rule1 = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") });
var rule2 = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") });

16
tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/UsageTriggerHandlerTests.cs → tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs

@ -6,15 +6,16 @@
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.HandleRules.Triggers;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure.EventSourcing;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers
namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
{
public class UsageTriggerHandlerTests
{
@ -52,5 +53,16 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers
Assert.False(result);
}
[Fact]
public async Task Should_create_enriched_event()
{
var @event = new AppUsageExceeded { CallsCurrent = 80, CallsLimit = 120 };
var result = (EnrichedUsageExceededEvent)await sut.CreateEnrichedEventAsync(Envelope.Create<AppEvent>(@event));
Assert.Equal(@event.CallsCurrent, result.CallsCurrent);
Assert.Equal(@event.CallsLimit, result.CallsLimit);
}
}
}

3
tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
@ -119,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties
private static Instant FutureDays(int days)
{
return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days));
return SystemClock.Instance.GetCurrentInstant().WithoutMs().Plus(Duration.FromDays(days));
}
}
}

22
tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs

@ -17,31 +17,33 @@ namespace Squidex.Infrastructure.Commands
{
private readonly IClock clock = A.Fake<IClock>();
private readonly ICommandBus commandBus = A.Dummy<ICommandBus>();
private readonly EnrichWithTimestampCommandMiddleware sut;
[Fact]
public async Task Should_set_timestamp_for_timestamp_command()
public EnrichWithTimestampCommandMiddlewareTests()
{
var utc = Instant.FromUnixTimeSeconds(1000);
var sut = new EnrichWithTimestampCommandMiddleware(clock);
A.CallTo(() => clock.GetCurrentInstant())
.Returns(utc);
.Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs());
sut = new EnrichWithTimestampCommandMiddleware(clock);
}
[Fact]
public async Task Should_set_timestamp_for_timestamp_command()
{
var command = new MyCommand();
await sut.HandleAsync(new CommandContext(command, commandBus));
Assert.Equal(utc, command.Timestamp);
Assert.Equal(clock.GetCurrentInstant(), command.Timestamp);
}
[Fact]
public async Task Should_do_nothing_for_normal_command()
{
var sut = new EnrichWithTimestampCommandMiddleware(clock);
await sut.HandleAsync(new CommandContext(A.Dummy<ICommand>(), commandBus));
A.CallTo(() => clock.GetCurrentInstant()).MustNotHaveHappened();
A.CallTo(() => clock.GetCurrentInstant())
.MustNotHaveHappened();
}
}
}

2
tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs

@ -82,7 +82,7 @@ namespace Squidex.Infrastructure.EventSourcing
[Fact]
public async Task Should_invoke_all_consumers()
{
var @event = Envelope.Create(new MyEvent());
var @event = Envelope.Create<IEvent>(new MyEvent());
var sut = new CompoundEventConsumer("consumer-name", consumer1, consumer2);

6
tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs

@ -51,7 +51,7 @@ namespace Squidex.Infrastructure.EventSourcing
inputEvent.SetEventStreamNumber(1);
inputEvent.SetTimestamp(SystemClock.Instance.GetCurrentInstant());
var eventData = sut.ToEventData(inputEvent.To<IEvent>(), commitId);
var eventData = sut.ToEventData(inputEvent, commitId);
var outputEvent = sut.Parse(eventData).To<MyEvent>();
@ -64,7 +64,7 @@ namespace Squidex.Infrastructure.EventSourcing
{
var inputEvent = new Envelope<MyOldEvent>(new MyOldEvent { MyProperty = "My-Property" });
var eventData = sut.ToEventData(inputEvent.To<IEvent>(), Guid.NewGuid());
var eventData = sut.ToEventData(inputEvent, Guid.NewGuid());
var outputEvent = sut.Parse(eventData).To<MyEvent>();
@ -76,7 +76,7 @@ namespace Squidex.Infrastructure.EventSourcing
{
var inputEvent = new Envelope<MyOldEvent>(new MyOldEvent { MyProperty = "My-Property" });
var eventData = sut.ToEventData(inputEvent.To<IEvent>(), Guid.NewGuid(), false);
var eventData = sut.ToEventData(inputEvent, Guid.NewGuid(), false);
var outputEvent = sut.Parse(eventData).To<MyEvent>();

6
tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs

@ -13,7 +13,11 @@ namespace Squidex.Infrastructure.EventSourcing
{
public class EnvelopeExtensionsTests
{
private readonly Envelope<string> sut = new Envelope<string>(string.Empty);
private readonly Envelope<MyEvent> sut = new Envelope<MyEvent>(new MyEvent());
public sealed class MyEvent : IEvent
{
}
[Fact]
public void Should_set_and_get_timestamp()

4
tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs

@ -20,11 +20,11 @@ namespace Squidex.Infrastructure.EventSourcing
[Fact]
public void Should_serialize_and_deserialize()
{
var value = new Envelope<IEvent>(new MyEvent { Value = 1 });
var value = Envelope.Create(new MyEvent { Value = 1 });
var deserialized = value.SerializeAndDeserialize();
Assert.Equal(1, deserialized.To<MyEvent>().Payload.Value);
Assert.Equal(1, deserialized.Payload.Value);
}
}
}

23
tests/Squidex.Infrastructure.Tests/InstantExtensions.cs

@ -0,0 +1,23 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NodaTime;
using Xunit;
namespace Squidex.Infrastructure
{
public class InstantExtensions
{
[Fact]
public void Should_remove_ms_from_instant()
{
var source = Instant.FromUnixTimeMilliseconds((30 * 1000) + 100);
Assert.Equal(Instant.FromUnixTimeSeconds(30), source.WithoutMs());
}
}
}

5
tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using NodaTime;
using Squidex.Infrastructure.TestHelpers;
using Xunit;
@ -17,7 +16,7 @@ namespace Squidex.Infrastructure.Json
[Fact]
public void Should_serialize_and_deserialize()
{
var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date);
var value = Instant.FromUtc(2012, 12, 10, 9, 8, 45);
var serialized = value.SerializeAndDeserialize();
@ -27,7 +26,7 @@ namespace Squidex.Infrastructure.Json
[Fact]
public void Should_serialize_and_deserialize_nullable_with_value()
{
Instant? value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date);
Instant? value = Instant.FromUtc(2012, 12, 10, 9, 8, 45);
var serialized = value.SerializeAndDeserialize();

6
tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs

@ -42,7 +42,7 @@ namespace Squidex.Infrastructure.Json.Newtonsoft
[Fact]
public void Should_respect_property_converter()
{
var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date);
var value = Instant.FromUtc(2012, 12, 10, 9, 8, 45);
var serializerSettings = new JsonSerializerSettings
{
@ -57,7 +57,7 @@ namespace Squidex.Infrastructure.Json.Newtonsoft
[Fact]
public void Should_ignore_other_converters()
{
var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date);
var value = Instant.FromUtc(2012, 12, 10, 9, 8, 45);
var serializerSettings = new JsonSerializerSettings
{
@ -75,7 +75,7 @@ namespace Squidex.Infrastructure.Json.Newtonsoft
[Fact]
public void Should_serialize_and_deserialize_instant()
{
var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date);
var value = Instant.FromUtc(2012, 12, 10, 9, 8, 45);
var serialized = value.SerializeAndDeserialize();

37
tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs

@ -7,6 +7,7 @@
using System;
using Newtonsoft.Json;
using NodaTime;
using Xunit;
namespace Squidex.Infrastructure.Log
@ -48,31 +49,21 @@ namespace Squidex.Infrastructure.Log
}
[Fact]
public void Should_write_timespan_property()
public void Should_write_duration_property()
{
var result = sut.WriteProperty("property", new TimeSpan(1, 40, 30, 20, 100)).ToString();
var result = sut.WriteProperty("property", new TimeSpan(2, 16, 30, 20, 100)).ToString();
Assert.Equal(@"{""property"":""2.16:30:20.1000000""}", result);
}
[Fact]
public void Should_write_datetimeoffset_property()
{
var value = DateTimeOffset.UtcNow;
var result = sut.WriteProperty("property", value).ToString();
Assert.Equal($"{{\"property\":\"{value:o}\"}}", result);
}
[Fact]
public void Should_write_date_property()
{
var value = DateTime.UtcNow;
var value = Instant.FromUtc(2012, 11, 10, 9, 8, 45);
var result = sut.WriteProperty("property", value).ToString();
Assert.Equal($"{{\"property\":\"{value:o}\"}}", result);
Assert.Equal(@"{""property"":""2012-11-10T09:08:45Z""}", result);
}
[Fact]
@ -108,9 +99,9 @@ namespace Squidex.Infrastructure.Log
}
[Fact]
public void Should_write_timespan_value()
public void Should_write_duration_value()
{
var result = sut.WriteArray("property", a => a.WriteValue(new TimeSpan(1, 40, 30, 20, 100))).ToString();
var result = sut.WriteArray("property", a => a.WriteValue(new TimeSpan(2, 16, 30, 20, 100))).ToString();
Assert.Equal(@"{""property"":[""2.16:30:20.1000000""]}", result);
}
@ -123,24 +114,14 @@ namespace Squidex.Infrastructure.Log
Assert.Equal(@"{""property1"":[{""property2"":120}]}", result);
}
[Fact]
public void Should_write_datetimeoffset_value()
{
var value = DateTimeOffset.UtcNow;
var result = sut.WriteArray("property", a => a.WriteValue(value)).ToString();
Assert.Equal($"{{\"property\":[\"{value:o}\"]}}", result);
}
[Fact]
public void Should_write_date_value()
{
var value = DateTime.UtcNow;
var value = Instant.FromUtc(2012, 11, 10, 9, 8, 45);
var result = sut.WriteArray("property", a => a.WriteValue(value)).ToString();
Assert.Equal($"{{\"property\":[\"{value:yyyy-MM-ddTHH:mm:ssZ}\"]}}", result);
Assert.Equal(@"{""property"":[""2012-11-10T09:08:45Z""]}", result);
}
[Fact]

10
tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Linq;
using FakeItEasy;
using Microsoft.Extensions.Logging;
using NodaTime;
using Squidex.Infrastructure.Log.Adapter;
using Xunit;
@ -63,16 +64,19 @@ namespace Squidex.Infrastructure.Log
[Fact]
public void Should_log_timestamp()
{
var now = DateTime.UtcNow;
var clock = A.Fake<IClock>();
appenders.Add(new TimestampLogAppender(() => now));
A.CallTo(() => clock.GetCurrentInstant())
.Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs());
appenders.Add(new TimestampLogAppender(clock));
Log.LogFatal(w => { /* Do Nothing */ });
var expected =
LogTest(w => w
.WriteProperty("logLevel", "Fatal")
.WriteProperty("timestamp", now));
.WriteProperty("timestamp", clock.GetCurrentInstant()));
Assert.Equal(expected, output);
}

42
tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs

@ -6,20 +6,28 @@
// ==========================================================================
using System;
using FakeItEasy;
using NodaTime;
using Xunit;
namespace Squidex.Infrastructure
{
public class RetryWindowTests
{
private const int WindowSize = 5;
private readonly IClock clock = A.Fake<IClock>();
public RetryWindowTests()
{
A.CallTo(() => clock.GetCurrentInstant())
.Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs());
}
[Fact]
public void Should_allow_to_retry_after_reset()
{
var sut = new RetryWindow(TimeSpan.FromSeconds(1), WindowSize);
var sut = new RetryWindow(TimeSpan.FromSeconds(1), 5);
for (var i = 0; i < WindowSize * 2; i++)
for (var i = 0; i < 5 * 2; i++)
{
sut.CanRetryAfterFailure();
}
@ -34,19 +42,18 @@ namespace Squidex.Infrastructure
[InlineData(7)]
public void Should_not_allow_to_retry_after_many_errors(int errors)
{
var sut = new RetryWindow(TimeSpan.FromSeconds(1), WindowSize);
var now = DateTime.UtcNow;
var sut = new RetryWindow(TimeSpan.FromSeconds(1), 5, clock);
for (var i = 0; i < WindowSize; i++)
for (var i = 0; i < 5; i++)
{
Assert.True(sut.CanRetryAfterFailure(now));
Assert.True(sut.CanRetryAfterFailure());
}
var remaining = errors - WindowSize;
var remaining = errors - 5;
for (var i = 0; i < remaining; i++)
{
Assert.False(sut.CanRetryAfterFailure(now));
Assert.False(sut.CanRetryAfterFailure());
}
}
@ -57,12 +64,11 @@ namespace Squidex.Infrastructure
[InlineData(4)]
public void Should_allow_to_retry_after_few_errors(int errors)
{
var sut = new RetryWindow(TimeSpan.FromSeconds(1), WindowSize);
var now = DateTime.UtcNow;
var sut = new RetryWindow(TimeSpan.FromSeconds(1), 5, clock);
for (var i = 0; i < errors; i++)
{
Assert.True(sut.CanRetryAfterFailure(now));
Assert.True(sut.CanRetryAfterFailure());
}
}
@ -77,12 +83,18 @@ namespace Squidex.Infrastructure
[InlineData(8)]
public void Should_allow_to_retry_after_few_errors_in_window(int errors)
{
var sut = new RetryWindow(TimeSpan.FromSeconds(1), WindowSize);
var now = DateTime.UtcNow;
var sut = new RetryWindow(TimeSpan.FromSeconds(1), 5, clock);
var now = SystemClock.Instance.GetCurrentInstant();
A.CallTo(() => clock.GetCurrentInstant())
.ReturnsLazily(() => now);
for (var i = 0; i < errors; i++)
{
Assert.True(sut.CanRetryAfterFailure(now.AddMilliseconds(i * 300)));
now = now.Plus(Duration.FromMilliseconds(300));
Assert.True(sut.CanRetryAfterFailure());
}
}
}

12
tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs

@ -183,12 +183,12 @@ namespace Squidex.Infrastructure.States
await persistence.ReadAsync();
await persistence.WriteEventsAsync(new[] { new MyEvent(), new MyEvent() }.Select(Envelope.Create));
await persistence.WriteEventsAsync(new[] { new MyEvent(), new MyEvent() }.Select(Envelope.Create));
await persistence.WriteEventAsync(Envelope.Create(new MyEvent()));
await persistence.WriteEventAsync(Envelope.Create(new MyEvent()));
A.CallTo(() => eventStore.AppendAsync(A<Guid>.Ignored, key, 2, A<ICollection<EventData>>.That.Matches(x => x.Count == 2)))
A.CallTo(() => eventStore.AppendAsync(A<Guid>.Ignored, key, 2, A<ICollection<EventData>>.That.Matches(x => x.Count == 1)))
.MustHaveHappened();
A.CallTo(() => eventStore.AppendAsync(A<Guid>.Ignored, key, 4, A<ICollection<EventData>>.That.Matches(x => x.Count == 2)))
A.CallTo(() => eventStore.AppendAsync(A<Guid>.Ignored, key, 3, A<ICollection<EventData>>.That.Matches(x => x.Count == 1)))
.MustHaveHappened();
}
@ -202,10 +202,10 @@ namespace Squidex.Infrastructure.States
await persistence.ReadAsync();
A.CallTo(() => eventStore.AppendAsync(A<Guid>.Ignored, key, 2, A<ICollection<EventData>>.That.Matches(x => x.Count == 2)))
A.CallTo(() => eventStore.AppendAsync(A<Guid>.Ignored, key, 2, A<ICollection<EventData>>.That.Matches(x => x.Count == 1)))
.Throws(new WrongEventVersionException(1, 1));
await Assert.ThrowsAsync<DomainObjectVersionException>(() => persistence.WriteEventsAsync(new[] { new MyEvent(), new MyEvent() }.Select(Envelope.Create)));
await Assert.ThrowsAsync<DomainObjectVersionException>(() => persistence.WriteEventAsync(Envelope.Create(new MyEvent())));
}
[Fact]

Loading…
Cancel
Save