diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs index b93a45a25..049de10ab 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs @@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules { internal sealed class ClientPool { - 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> 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; diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs index ec4d672da..616db38f7 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs +++ b/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 { diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs new file mode 100644 index 000000000..3597d90c0 --- /dev/null +++ b/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 @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 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; + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs index 6d2e7961d..1346f9b80 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs @@ -14,6 +14,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules { public interface IEventEnricher { - Task EnrichAsync(Envelope @event); + Task EnrichAsync(EnrichedEvent enrichedEvent, Envelope @event); } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs index 5b27a05b9..b7cd99f60 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs +++ b/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 CreateEnrichedEventAsync(Envelope @event); + bool Trigger(EnrichedEvent @event, RuleTrigger trigger); - bool Trigger(IEvent @event, RuleTrigger trigger, Guid ruleId); + bool Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId); } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs index b7a0050b7..a1f5e3b7b 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/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(); + 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(); - 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) diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs index 7e62cad86..d5f8d47df 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs +++ b/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 : 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 IRuleTriggerHandler.CreateEnrichedEventAsync(Envelope @event) + { + return await CreateEnrichedEventAsync(@event.To()); + } + 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 CreateEnrichedEventAsync(Envelope @event); + protected abstract bool Trigger(TEnrichedEvent @event, TTrigger trigger); protected virtual bool Trigger(TEvent @event, TTrigger trigger, Guid ruleId) diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs deleted file mode 100644 index e75e41b8a..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs +++ /dev/null @@ -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 - { - 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); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs new file mode 100644 index 000000000..7c23af11e --- /dev/null +++ b/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 + { + 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 CreateEnrichedEventAsync(Envelope @event) + { + var result = new EnrichedAssetEvent(); + + var asset = + (await grainFactory + .GetGrain(@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); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs similarity index 51% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs rename to src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs index 1c95e6734..60726b883 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs +++ b/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 { 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 CreateEnrichedEventAsync(Envelope @event) + { + var result = new EnrichedContentEvent(); + + var content = + (await grainFactory + .GetGrain(@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) diff --git a/src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs b/src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs deleted file mode 100644 index 803d90884..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs +++ /dev/null @@ -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 EnrichAsync(Envelope @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 @event) - { - var asset = - (await grainFactory - .GetGrain(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 @event) - { - var content = - (await grainFactory - .GetGrain(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 @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 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; - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs new file mode 100644 index 000000000..3556a8f47 --- /dev/null +++ b/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(SingleGrain.Id); + } + + public async Task HandleAsync(CommandContext context, Func 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(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs index 1ebc89e37..c1cda0185 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ b/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(@event)); } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/UsageTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs similarity index 51% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/UsageTriggerHandler.cs rename to src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs index 71ed317f4..f5b17df98 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/UsageTriggerHandler.cs +++ b/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 { + private const string EventName = "Usage exceeeded"; + + protected override Task CreateEnrichedEventAsync(Envelope @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; } } } diff --git a/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs b/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs index dc2e05ad2..d4139ae71 100644 --- a/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs +++ b/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs @@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Events return headers.GetGuid(SquidexHeaders.AppId); } - public static Envelope SetAppId(this Envelope envelope, Guid value) where T : class + public static Envelope SetAppId(this Envelope envelope, Guid value) where T : class, IEvent { envelope.Headers.Add(SquidexHeaders.AppId, value.ToString()); diff --git a/src/Squidex.Infrastructure/EventSourcing/Envelope.cs b/src/Squidex.Infrastructure/EventSourcing/Envelope.cs index aa4af3878..458fc7991 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Envelope.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Envelope.cs @@ -12,12 +12,12 @@ namespace Squidex.Infrastructure.EventSourcing { public static class Envelope { - public static Envelope Create(TPayload payload) where TPayload : IEvent + public static Envelope Create(TPayload payload) where TPayload : class, IEvent { var eventId = Guid.NewGuid(); var envelope = - new Envelope(payload) + new Envelope(payload) .SetEventId(eventId) .SetTimestamp(SystemClock.Instance.GetCurrentInstant()); diff --git a/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs b/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs index e08a6ec4f..82719fe96 100644 --- a/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs +++ b/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs @@ -20,7 +20,7 @@ namespace Squidex.Infrastructure.EventSourcing return headers.GetString(CommonHeaders.EventNumber); } - public static Envelope SetEventPosition(this Envelope envelope, string value) where T : class + public static Envelope SetEventPosition(this Envelope 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 SetEventStreamNumber(this Envelope envelope, long value) where T : class + public static Envelope SetEventStreamNumber(this Envelope 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 SetCommitId(this Envelope envelope, Guid value) where T : class + public static Envelope SetCommitId(this Envelope 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 SetAggregateId(this Envelope envelope, Guid value) where T : class + public static Envelope SetAggregateId(this Envelope 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 SetEventId(this Envelope envelope, Guid value) where T : class + public static Envelope SetEventId(this Envelope 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 SetTimestamp(this Envelope envelope, Instant value) where T : class + public static Envelope SetTimestamp(this Envelope envelope, Instant value) where T : class, IEvent { envelope.Headers.Add(CommonHeaders.Timestamp, value.ToString()); diff --git a/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs b/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs index de6ac36ae..ba1b59a34 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs @@ -7,7 +7,7 @@ namespace Squidex.Infrastructure.EventSourcing { - public class Envelope where T : class + public class Envelope 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 To() where TOther : class + public Envelope To() where TOther : class, IEvent { return new Envelope(payload as TOther, headers.Clone()); } + + public static implicit operator Envelope(Envelope source) + { + return source == null ? source : new Envelope(source.payload, source.headers); + } } } diff --git a/src/Squidex.Infrastructure/InstantExtensions.cs b/src/Squidex.Infrastructure/InstantExtensions.cs new file mode 100644 index 000000000..3b9e37d8b --- /dev/null +++ b/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()); + } + } +} diff --git a/src/Squidex.Infrastructure/Log/IArrayWriter.cs b/src/Squidex.Infrastructure/Log/IArrayWriter.cs index 8ec8b803a..d80dae6ed 100644 --- a/src/Squidex.Infrastructure/Log/IArrayWriter.cs +++ b/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 objectWriter); } diff --git a/src/Squidex.Infrastructure/Log/IObjectWriter.cs b/src/Squidex.Infrastructure/Log/IObjectWriter.cs index fc133875e..b1f7cbad1 100644 --- a/src/Squidex.Infrastructure/Log/IObjectWriter.cs +++ b/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 objectWriter); diff --git a/src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs b/src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs index 491f32964..80ad22c26 100644 --- a/src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs +++ b/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) { diff --git a/src/Squidex.Infrastructure/Log/JsonLogWriter.cs b/src/Squidex.Infrastructure/Log/JsonLogWriter.cs index b90cf64eb..413dbd136 100644 --- a/src/Squidex.Infrastructure/Log/JsonLogWriter.cs +++ b/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; } diff --git a/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs b/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs index f1ab37473..2e1b6bf30 100644 --- a/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs +++ b/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 timestamp; + private readonly IClock clock; - public TimestampLogAppender() - : this(() => DateTime.UtcNow) + public TimestampLogAppender(IClock clock = null) { - } - - public TimestampLogAppender(Func 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()); } } } diff --git a/src/Squidex.Infrastructure/RetryWindow.cs b/src/Squidex.Infrastructure/RetryWindow.cs index 0541b90db..ed155d2d8 100644 --- a/src/Squidex.Infrastructure/RetryWindow.cs +++ b/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 retries = new Queue(); + private readonly Queue retries = new Queue(); + 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); } } } diff --git a/src/Squidex.Infrastructure/States/StoreExtensions.cs b/src/Squidex.Infrastructure/States/StoreExtensions.cs index 789c94ce1..8fd4d1dbd 100644 --- a/src/Squidex.Infrastructure/States/StoreExtensions.cs +++ b/src/Squidex.Infrastructure/States/StoreExtensions.cs @@ -17,11 +17,6 @@ namespace Squidex.Infrastructure.States return persistence.WriteEventsAsync(new[] { @event }); } - public static Task WriteEventAsync(this IPersistence persistence, IEvent @event) - { - return persistence.WriteEventsAsync(new[] { Envelope.Create(@event) }); - } - public static Task ClearSnapshotsAsync(this IStore store) { return store.GetSnapshotStore().ClearAsync(); diff --git a/src/Squidex/Config/Domain/RuleServices.cs b/src/Squidex/Config/Domain/RuleServices.cs index 0b30fda37..f178db4de 100644 --- a/src/Squidex/Config/Domain/RuleServices.cs +++ b/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; diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs index fff6720c4..13a33e087 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs +++ b/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)); } } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs index 53bcf4106..3e5712af7 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs +++ b/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); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs index 537de9762..e4d693829 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs +++ b/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>.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>.Ignored)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.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>.Ignored)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.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>.Ignored)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.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>.Ignored)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.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.Ignored, A.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.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>.Ignored)) + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.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.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.Ignored, rule.Trigger)) - .Returns(false); + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) + .Returns(Task.FromResult(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>.That.Matches(x => x.Payload == @event.Payload))) + .Returns(enrichedEvent); - A.CallTo(() => ruleActionHandler.CreateJobAsync(A.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>.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.Ignored, rule.Trigger)) + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, rule.Trigger, ruleId)) + A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger)) .Returns(true); + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) + .Returns(enrichedEvent); + A.CallTo(() => ruleActionHandler.CreateJobAsync(A.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>.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()); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs index 6ae3fd19b..dfb26997c 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs +++ b/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) diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerTests.cs similarity index 70% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerTests.cs index 3ea7cdeb2..19b3f2fd7 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs +++ b/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(); + private readonly IGrainFactory grainFactory = A.Fake(); private readonly IRuleTriggerHandler sut; public AssetChangedTriggerTests() { - sut = new AssetChangedTriggerHandler(scriptEngine); + sut = new AssetChangedTriggerHandler(scriptEngine, grainFactory); A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) .Returns(true); @@ -34,6 +42,33 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers .Returns(false); } + public static IEnumerable 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(@event).SetEventStreamNumber(12); + + var assetGrain = A.Fake(); + + A.CallTo(() => grainFactory.GetGrain(@event.AssetId, null)) + .Returns(assetGrain); + + A.CallTo(() => assetGrain.GetStateAsync(12)) + .Returns(A.Fake().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() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs index cb665e45a..fd142c7f2 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs +++ b/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.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() + Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() }; ValidationAssert.Throws(() => GuardComments.CanUpdate(events, command), @@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards var events = new List> { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() + Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() }; Assert.Throws(() => GuardComments.CanUpdate(events, command)); @@ -86,8 +86,8 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards var events = new List> { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To(), - Envelope.Create(new CommentDeleted { CommentId = commentId }).To() + Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To(), + Envelope.Create(new CommentDeleted { CommentId = commentId }).To() }; Assert.Throws(() => GuardComments.CanUpdate(events, command)); @@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards var events = new List> { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() + Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() }; GuardComments.CanUpdate(events, command); @@ -115,7 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards var events = new List> { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() + Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() }; Assert.Throws(() => GuardComments.CanDelete(events, command)); @@ -140,8 +140,8 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards var events = new List> { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To(), - Envelope.Create(new CommentDeleted { CommentId = commentId }).To() + Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }), + Envelope.Create(new CommentDeleted { CommentId = commentId }) }; Assert.Throws(() => GuardComments.CanDelete(events, command)); @@ -155,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards var events = new List> { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() + Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() }; GuardComments.CanDelete(events, command); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerTests.cs similarity index 78% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerTests.cs index 467cb9378..a2c601af5 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs +++ b/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(); + private readonly IGrainFactory grainFactory = A.Fake(); private readonly IRuleTriggerHandler sut; private readonly Guid ruleId = Guid.NewGuid(); private static readonly NamedId 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.Ignored, "true")) .Returns(true); @@ -43,6 +49,36 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers .Returns(false); } + public static IEnumerable 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(@event).SetEventStreamNumber(12); + + var contentGrain = A.Fake(); + + A.CallTo(() => grainFactory.GetGrain(@event.ContentId, null)) + .Returns(contentGrain); + + A.CallTo(() => contentGrain.GetStateAsync(12)) + .Returns(A.Fake().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() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index 6988c899d..7e1639b8e 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/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 { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs index 68a3a2eb8..ee1183609 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs +++ b/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(); private readonly ISemanticLog log = A.Dummy(); private readonly IRuleEventRepository ruleEventRepository = A.Fake(); - private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); private readonly RuleService ruleService = A.Fake(); 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; diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs index a38e3a2da..b8fcb0d48 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ b/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(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") }); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/UsageTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs similarity index 76% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/UsageTriggerHandlerTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs index def006016..b8ca9421b 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/UsageTriggerHandlerTests.cs +++ b/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(@event)); + + Assert.Equal(@event.CallsCurrent, result.CallsCurrent); + Assert.Equal(@event.CallsLimit, result.CallsLimit); + } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs index 7137b4527..6cfa3feef 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs +++ b/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)); } } } \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs index 491f9e7d1..4bd06870e 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs @@ -17,31 +17,33 @@ namespace Squidex.Infrastructure.Commands { private readonly IClock clock = A.Fake(); private readonly ICommandBus commandBus = A.Dummy(); + 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(), commandBus)); - A.CallTo(() => clock.GetCurrentInstant()).MustNotHaveHappened(); + A.CallTo(() => clock.GetCurrentInstant()) + .MustNotHaveHappened(); } } } diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs index 594564274..9494ff7b7 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs +++ b/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(new MyEvent()); var sut = new CompoundEventConsumer("consumer-name", consumer1, consumer2); diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs index 74529224f..ead614257 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs +++ b/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(), commitId); + var eventData = sut.ToEventData(inputEvent, commitId); var outputEvent = sut.Parse(eventData).To(); @@ -64,7 +64,7 @@ namespace Squidex.Infrastructure.EventSourcing { var inputEvent = new Envelope(new MyOldEvent { MyProperty = "My-Property" }); - var eventData = sut.ToEventData(inputEvent.To(), Guid.NewGuid()); + var eventData = sut.ToEventData(inputEvent, Guid.NewGuid()); var outputEvent = sut.Parse(eventData).To(); @@ -76,7 +76,7 @@ namespace Squidex.Infrastructure.EventSourcing { var inputEvent = new Envelope(new MyOldEvent { MyProperty = "My-Property" }); - var eventData = sut.ToEventData(inputEvent.To(), Guid.NewGuid(), false); + var eventData = sut.ToEventData(inputEvent, Guid.NewGuid(), false); var outputEvent = sut.Parse(eventData).To(); diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs index 464585379..dbba8b617 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs @@ -13,7 +13,11 @@ namespace Squidex.Infrastructure.EventSourcing { public class EnvelopeExtensionsTests { - private readonly Envelope sut = new Envelope(string.Empty); + private readonly Envelope sut = new Envelope(new MyEvent()); + + public sealed class MyEvent : IEvent + { + } [Fact] public void Should_set_and_get_timestamp() diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs index f9d3a75e5..8a573c40f 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs +++ b/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(new MyEvent { Value = 1 }); + var value = Envelope.Create(new MyEvent { Value = 1 }); var deserialized = value.SerializeAndDeserialize(); - Assert.Equal(1, deserialized.To().Payload.Value); + Assert.Equal(1, deserialized.Payload.Value); } } } diff --git a/tests/Squidex.Infrastructure.Tests/InstantExtensions.cs b/tests/Squidex.Infrastructure.Tests/InstantExtensions.cs new file mode 100644 index 000000000..eb30be491 --- /dev/null +++ b/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()); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs b/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs index aba2d8269..6ef80c6fd 100644 --- a/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs +++ b/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(); diff --git a/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs b/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs index 4ea3d7c0f..aee6e6966 100644 --- a/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs +++ b/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(); diff --git a/tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs b/tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs index 069be90c7..5d3b00d6c 100644 --- a/tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs +++ b/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] diff --git a/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs b/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs index 7f82046ff..cd5685739 100644 --- a/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs +++ b/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(); - 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); } diff --git a/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs b/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs index a32164846..dcb6a2322 100644 --- a/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs +++ b/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(); + + 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()); } } } diff --git a/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs b/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs index 686a57c95..5e44de36f 100644 --- a/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs +++ b/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.Ignored, key, 2, A>.That.Matches(x => x.Count == 2))) + A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 1))) .MustHaveHappened(); - A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 4, A>.That.Matches(x => x.Count == 2))) + A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 3, A>.That.Matches(x => x.Count == 1))) .MustHaveHappened(); } @@ -202,10 +202,10 @@ namespace Squidex.Infrastructure.States await persistence.ReadAsync(); - A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 2))) + A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 1))) .Throws(new WrongEventVersionException(1, 1)); - await Assert.ThrowsAsync(() => persistence.WriteEventsAsync(new[] { new MyEvent(), new MyEvent() }.Select(Envelope.Create))); + await Assert.ThrowsAsync(() => persistence.WriteEventAsync(Envelope.Create(new MyEvent()))); } [Fact]