// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschränkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using System.Threading.Tasks; using FakeItEasy; using NodaTime; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Xunit; #pragma warning disable xUnit2009 // Do not use boolean check to check for string equality namespace Squidex.Domain.Apps.Core.Operations.HandleRules { public class RuleServiceTests { private readonly IRuleTriggerHandler ruleTriggerHandler = A.Fake(); private readonly IRuleActionHandler ruleActionHandler = A.Fake(); private readonly IEventEnricher eventEnricher = A.Fake(); private readonly IClock clock = A.Fake(); private readonly string actionData = "{\"value\":10}"; private readonly string actionDump = "MyDump"; private readonly string actionName = "ValidAction"; private readonly string actionDescription = "MyDescription"; private readonly Guid ruleId = Guid.NewGuid(); private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); private readonly RuleService sut; public sealed class InvalidEvent : IEvent { } public sealed class InvalidAction : RuleAction { } public sealed class ValidAction : RuleAction { public int Value { get; set; } } public sealed class ValidData { public int Value { get; set; } } public sealed class InvalidTrigger : RuleTrigger { public override T Accept(IRuleTriggerVisitor visitor) { return default(T); } } public RuleServiceTests() { typeNameRegistry.Map(typeof(ContentCreated)); typeNameRegistry.Map(typeof(ValidAction), actionName); A.CallTo(() => eventEnricher.EnrichAsync(A>.Ignored)) .Returns(new EnrichedContentEvent { AppId = appId }); A.CallTo(() => ruleActionHandler.ActionType) .Returns(typeof(ValidAction)); A.CallTo(() => ruleActionHandler.DataType) .Returns(typeof(ValidData)); A.CallTo(() => ruleTriggerHandler.TriggerType) .Returns(typeof(ContentChangedTriggerV2)); sut = new RuleService(new[] { ruleTriggerHandler }, new[] { ruleActionHandler }, eventEnricher, TestUtils.DefaultSerializer, clock, typeNameRegistry); } [Fact] public async Task Should_not_create_if_rule_disabled() { var rule = ValidRule().Disable(); var ruleEvent = Envelope.Create(new ContentCreated()); var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); Assert.Null(job); A.CallTo(() => eventEnricher.EnrichAsync(A>.Ignored)) .MustNotHaveHappened(); } [Fact] public async Task Should_not_create_job_for_invalid_event() { var rule = ValidRule(); var ruleEvent = Envelope.Create(new InvalidEvent()); var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); Assert.Null(job); A.CallTo(() => eventEnricher.EnrichAsync(A>.Ignored)) .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 job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); Assert.Null(job); A.CallTo(() => eventEnricher.EnrichAsync(A>.Ignored)) .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 job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); Assert.Null(job); A.CallTo(() => eventEnricher.EnrichAsync(A>.Ignored)) .MustNotHaveHappened(); } [Fact] 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)) .Returns(false); var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); Assert.Null(job); A.CallTo(() => eventEnricher.EnrichAsync(A>.Ignored)) .MustNotHaveHappened(); } [Fact] public async Task Should_not_create_job_if_not_triggered() { var rule = ValidRule(); var ruleEvent = Envelope.Create(new ContentCreated()); A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, rule.Trigger, ruleId)) .Returns(true); A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, rule.Trigger)) .Returns(false); var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); Assert.Null(job); } [Fact] public async Task Should_not_create_job_if_too_old() { var ruleEvent = new ContentCreated { SchemaId = schemaId, AppId = appId }; var now = SystemClock.Instance.GetCurrentInstant(); var rule = ValidRule(); var ruleEnvelope = Envelope.Create(ruleEvent); ruleEnvelope.SetTimestamp(now.Minus(Duration.FromDays(3))); A.CallTo(() => clock.GetCurrentInstant()) .Returns(now); A.CallTo(() => ruleActionHandler.CreateJobAsync(A.Ignored, rule.Action)) .Returns((actionDescription, actionData)); var job = await sut.CreateJobAsync(rule, ruleId, ruleEnvelope); 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 rule = ValidRule(); var ruleEnvelope = Envelope.Create(ruleEvent); ruleEnvelope.SetTimestamp(now); A.CallTo(() => clock.GetCurrentInstant()) .Returns(now); A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, rule.Trigger)) .Returns(true); A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, rule.Trigger, ruleId)) .Returns(true); A.CallTo(() => ruleActionHandler.CreateJobAsync(A.Ignored, rule.Action)) .Returns((actionDescription, new ValidData { Value = 10 })); var job = await sut.CreateJobAsync(rule, ruleId, ruleEnvelope); Assert.Equal(actionData, job.ActionData); Assert.Equal(actionName, job.ActionName); Assert.Equal(actionDescription, job.Description); Assert.Equal(now, job.Created); Assert.Equal(now.Plus(Duration.FromDays(2)), job.Expires); Assert.Equal(ruleEvent.AppId.Id, job.AppId); Assert.NotEqual(Guid.Empty, job.JobId); } [Fact] public async Task Should_return_succeeded_job_with_full_dump_when_handler_returns_no_exception() { A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10))) .Returns((actionDump, null)); var result = await sut.InvokeAsync(actionName, actionData); Assert.Equal(RuleResult.Success, result.Result); Assert.True(result.Elapsed >= TimeSpan.Zero); Assert.StartsWith(actionDump, result.Dump, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task Should_return_failed_job_with_full_dump_when_handler_returns_exception() { A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10))) .Returns((actionDump, new InvalidOperationException())); var result = await sut.InvokeAsync(actionName, actionData); Assert.Equal(RuleResult.Failed, result.Result); Assert.True(result.Elapsed >= TimeSpan.Zero); Assert.True(result.Dump.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); } [Fact] public async Task Should_return_timedout_job_with_full_dump_when_exception_from_handler_indicates_timeout() { A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10))) .Returns((actionDump, new TimeoutException())); var result = await sut.InvokeAsync(actionName, actionData); Assert.Equal(RuleResult.Timeout, result.Result); Assert.True(result.Elapsed >= TimeSpan.Zero); Assert.True(result.Dump.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); Assert.True(result.Dump.IndexOf("Action timed out.", StringComparison.OrdinalIgnoreCase) >= 0); } [Fact] public async Task Should_create_exception_details_when_job_to_execute_failed() { var ruleError = new InvalidOperationException(); A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10))) .Throws(ruleError); var result = await sut.InvokeAsync(actionName, actionData); Assert.Equal((ruleError.ToString(), RuleResult.Failed, TimeSpan.Zero), result); } private static Rule ValidRule() { return new Rule(new ContentChangedTriggerV2(), new ValidAction()); } } }