From 6e7828b90cb84016d0e4f7b7bfd6d55429230828 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 24 Aug 2022 21:58:43 +0200 Subject: [PATCH] Schema triggers (#912) * Fix to schema triggers * Improve tests. --- .../Assets/AssetChangedTriggerHandler.cs | 3 + .../Comments/CommentTriggerHandler.cs | 33 +++--- .../Contents/ContentChangedTriggerHandler.cs | 3 + .../Rules/ManualTriggerHandler.cs | 3 +- .../Rules/RuleEnqueuer.cs | 2 +- .../Schemas/SchemaChangedTriggerHandler.cs | 3 +- .../TestHelpers/TestUtils.cs | 25 +++++ .../Assets/AssetChangedTriggerHandlerTests.cs | 24 ++-- .../Comments/CommentTriggerHandlerTests.cs | 12 +- .../ContentChangedTriggerHandlerTests.cs | 22 ++-- .../Rules/ManualTriggerHandlerTests.cs | 16 ++- .../Rules/RuleEnqueuerTests.cs | 21 ++++ .../UsageTracking/UsageTriggerHandlerTests.cs | 3 +- .../SchemaChangedTriggerHandlerTests.cs | 23 ++-- .../TestSuite.ApiTests/RuleRunnerTests.cs | 103 +++++++++++++++++- .../shared/forms/array-editor.component.html | 6 +- .../shared/forms/array-editor.component.scss | 18 +++ .../shared/forms/array-editor.component.ts | 3 + .../shared/forms/assets-editor.component.html | 6 +- .../shared/forms/assets-editor.component.scss | 30 ++--- .../shared/forms/assets-editor.component.ts | 3 + .../shared/forms/field-editor.component.html | 7 +- .../shared/forms/field-editor.component.scss | 1 + .../references-editor.component.html | 2 +- .../references-editor.component.scss | 23 ++-- .../references/references-editor.component.ts | 3 + frontend/src/app/theme/_mixins.scss | 2 +- frontend/src/app/theme/_vars.scss | 1 - 28 files changed, 314 insertions(+), 87 deletions(-) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs index 5af7b6758..1267b81f3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs @@ -86,6 +86,9 @@ namespace Squidex.Domain.Apps.Entities.Assets result.AssetType = asset.Type; } + // Use the concrete event to map properties that are not part of app event. + SimpleMapper.Map(assetEvent, result); + switch (@event.Payload) { case AssetCreated: diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs index c1edfa6ef..679e19609 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs @@ -42,26 +42,31 @@ namespace Squidex.Domain.Apps.Entities.Comments { var commentCreated = (CommentCreated)@event.Payload; - if (commentCreated.Mentions?.Length > 0) + if (!(commentCreated.Mentions?.Length > 0)) { - var users = await userResolver.QueryManyAsync(commentCreated.Mentions, ct); + yield break; + } + + var users = await userResolver.QueryManyAsync(commentCreated.Mentions, ct); + + if (users.Count <= 0) + { + yield break; + } - if (users.Count > 0) + foreach (var user in users.Values) + { + var enrichedEvent = new EnrichedCommentEvent { - foreach (var user in users.Values) - { - var enrichedEvent = new EnrichedCommentEvent - { - MentionedUser = user - }; + MentionedUser = user + }; - enrichedEvent.Name = "UserMentioned"; + enrichedEvent.Name = "UserMentioned"; - SimpleMapper.Map(commentCreated, enrichedEvent); + // Use the concrete event to map properties that are not part of app event. + SimpleMapper.Map(commentCreated, enrichedEvent); - yield return enrichedEvent; - } - } + yield return enrichedEvent; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs index 8f6d00eee..bf5b50189 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs @@ -93,6 +93,9 @@ namespace Squidex.Domain.Apps.Entities.Contents SimpleMapper.Map(content, result); } + // Use the concrete event to map properties that are not part of app event. + SimpleMapper.Map(contentEvent, result); + switch (@event.Payload) { case ContentCreated: diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs index 8a0f08525..7ef0181c0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs @@ -30,7 +30,8 @@ namespace Squidex.Domain.Apps.Entities.Rules { var result = new EnrichedManualEvent(); - SimpleMapper.Map(@event.Payload, result); + // Use the concrete event to map properties that are not part of app event. + SimpleMapper.Map((RuleManuallyTriggered)@event.Payload, result); await Task.Yield(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs index f62f50142..11b1d7c71 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs @@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Rules await foreach (var job in jobs) { // We do not want to handle disabled rules in the normal flow. - if (job.Job != null && job.SkipReason == SkipReason.None) + if (job.Job != null && job.SkipReason is SkipReason.None or SkipReason.Failed) { await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs index f1f9e3b38..2eda50aa0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs @@ -38,7 +38,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas { var result = new EnrichedSchemaEvent(); - SimpleMapper.Map(@event.Payload, result); + // Use the concrete event to map properties that are not part of app event. + SimpleMapper.Map((SchemaEvent)@event.Payload, result); switch (@event.Payload) { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs index 6929bb286..56be531d2 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs @@ -24,6 +24,7 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Json; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas.Json; +using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Json; @@ -183,5 +184,29 @@ namespace Squidex.Domain.Apps.Core.TestHelpers return DefaultSerializer.Serialize(document, true); } + + public static TEvent CreateEvent(Action? init = null) where TEvent : IEvent, new() + { + var result = new TEvent(); + + if (result is SquidexEvent squidexEvent) + { + squidexEvent.Actor = new RefToken(RefTokenType.Client, "my-client"); + } + + if (result is AppEvent appEvent) + { + appEvent.AppId = NamedId.Of(DomainId.NewGuid(), "my-app"); + } + + if (result is SchemaEvent schemaEvent) + { + schemaEvent.SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + } + + init?.Invoke(result); + + return result; + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs index 742d15d95..2876a54a8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs @@ -11,6 +11,7 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Assets; @@ -45,10 +46,10 @@ namespace Squidex.Domain.Apps.Entities.Assets public static IEnumerable TestEvents() { - yield return new object[] { new AssetCreated(), EnrichedAssetEventType.Created }; - yield return new object[] { new AssetUpdated(), EnrichedAssetEventType.Updated }; - yield return new object[] { new AssetAnnotated(), EnrichedAssetEventType.Annotated }; - yield return new object[] { new AssetDeleted(), EnrichedAssetEventType.Deleted }; + yield return new object[] { TestUtils.CreateEvent(), EnrichedAssetEventType.Created }; + yield return new object[] { TestUtils.CreateEvent(), EnrichedAssetEventType.Updated }; + yield return new object[] { TestUtils.CreateEvent(), EnrichedAssetEventType.Annotated }; + yield return new object[] { TestUtils.CreateEvent(), EnrichedAssetEventType.Deleted }; } [Fact] @@ -99,9 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Assets [MemberData(nameof(TestEvents))] public async Task Should_create_enriched_events(AssetEvent @event, EnrichedAssetEventType type) { - var ctx = Context(); - - @event.AppId = ctx.AppId; + var ctx = Context(appId: @event.AppId); var envelope = Envelope.Create(@event).SetEventStreamNumber(12); @@ -110,9 +109,12 @@ namespace Squidex.Domain.Apps.Entities.Assets var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); - var enrichedEvent = result.Single() as EnrichedAssetEvent; + var enrichedEvent = (EnrichedAssetEvent)result.Single(); - Assert.Equal(type, enrichedEvent!.Type); + Assert.Equal(type, enrichedEvent.Type); + Assert.Equal(@event.Actor, enrichedEvent.Actor); + Assert.Equal(@event.AppId, enrichedEvent.AppId); + Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id); } [Fact] @@ -175,13 +177,13 @@ namespace Squidex.Domain.Apps.Entities.Assets } } - private static RuleContext Context(RuleTrigger? trigger = null) + private static RuleContext Context(RuleTrigger? trigger = null, NamedId? appId = null) { trigger ??= new AssetChangedTriggerV2(); return new RuleContext { - AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), + AppId = appId ?? NamedId.Of(DomainId.NewGuid(), "my-app"), Rule = new Rule(trigger, A.Fake()), RuleId = DomainId.NewGuid() }; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs index 624fa84c4..82b6b21c6 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs @@ -77,11 +77,12 @@ namespace Squidex.Domain.Apps.Entities.Comments var userIds = users.Select(x => x.Id).ToArray(); var @event = new CommentCreated { Mentions = userIds }; + var envelope = Envelope.Create(@event); A.CallTo(() => userResolver.QueryManyAsync(userIds, default)) .Returns(users.ToDictionary(x => x.Id)); - var result = await sut.CreateEnrichedEventsAsync(Envelope.Create(@event), ctx, default).ToListAsync(); + var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); Assert.Equal(2, result.Count); @@ -106,8 +107,9 @@ namespace Squidex.Domain.Apps.Entities.Comments var userIds = users.Select(x => x.Id).ToArray(); var @event = new CommentCreated { Mentions = userIds }; + var envelope = Envelope.Create(@event); - var result = await sut.CreateEnrichedEventsAsync(Envelope.Create(@event), ctx, default).ToListAsync(); + var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); Assert.Empty(result); } @@ -118,8 +120,9 @@ namespace Squidex.Domain.Apps.Entities.Comments var ctx = Context(); var @event = new CommentCreated { Mentions = null }; + var envelope = Envelope.Create(@event); - var result = await sut.CreateEnrichedEventsAsync(Envelope.Create(@event), ctx, default).ToListAsync(); + var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); Assert.Empty(result); @@ -133,8 +136,9 @@ namespace Squidex.Domain.Apps.Entities.Comments var ctx = Context(); var @event = new CommentCreated { Mentions = Array.Empty() }; + var envelope = Envelope.Create(@event); - var result = await sut.CreateEnrichedEventsAsync(Envelope.Create(@event), ctx, default).ToListAsync(); + var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); Assert.Empty(result); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs index 6a434b2c1..907f994d1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -20,6 +20,7 @@ using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; using Xunit; namespace Squidex.Domain.Apps.Entities.Contents @@ -50,12 +51,12 @@ namespace Squidex.Domain.Apps.Entities.Contents public static IEnumerable TestEvents() { - yield return new object[] { new ContentCreated(), EnrichedContentEventType.Created }; - yield return new object[] { new ContentUpdated(), EnrichedContentEventType.Updated }; - yield return new object[] { new ContentDeleted(), EnrichedContentEventType.Deleted }; - yield return new object[] { new ContentStatusChanged { Change = StatusChange.Change }, EnrichedContentEventType.StatusChanged }; - yield return new object[] { new ContentStatusChanged { Change = StatusChange.Published }, EnrichedContentEventType.Published }; - yield return new object[] { new ContentStatusChanged { Change = StatusChange.Unpublished }, EnrichedContentEventType.Unpublished }; + yield return new object[] { TestUtils.CreateEvent(), EnrichedContentEventType.Created }; + yield return new object[] { TestUtils.CreateEvent(), EnrichedContentEventType.Updated }; + yield return new object[] { TestUtils.CreateEvent(), EnrichedContentEventType.Deleted }; + yield return new object[] { TestUtils.CreateEvent(x => x.Change = StatusChange.Change), EnrichedContentEventType.StatusChanged }; + yield return new object[] { TestUtils.CreateEvent(x => x.Change = StatusChange.Published), EnrichedContentEventType.Published }; + yield return new object[] { TestUtils.CreateEvent(x => x.Change = StatusChange.Unpublished), EnrichedContentEventType.Unpublished }; } [Fact] @@ -188,13 +189,18 @@ namespace Squidex.Domain.Apps.Entities.Contents var envelope = Envelope.Create(@event).SetEventStreamNumber(12); A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 12, ct)) - .Returns(new ContentEntity { AppId = ctx.AppId, SchemaId = schemaMatch }); + .Returns(SimpleMapper.Map(@event, new ContentEntity())); var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); - var enrichedEvent = result.Single() as EnrichedContentEvent; + var enrichedEvent = (EnrichedContentEvent)result.Single(); Assert.Equal(type, enrichedEvent!.Type); + Assert.Equal(@event.Actor, enrichedEvent.Actor); + Assert.Equal(@event.AppId, enrichedEvent.AppId); + Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id); + Assert.Equal(@event.SchemaId, enrichedEvent.SchemaId); + Assert.Equal(@event.SchemaId.Id, enrichedEvent.SchemaId.Id); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs index 90ff8c68c..50f365cfb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs @@ -5,8 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using FakeItEasy; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; +using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Rules; using Squidex.Infrastructure; @@ -36,11 +38,16 @@ namespace Squidex.Domain.Apps.Entities.Rules [Fact] public async Task Should_create_event_with_name() { - var @event = new RuleManuallyTriggered(); + var @event = TestUtils.CreateEvent(); + var envelope = Envelope.Create(@event); + + var result = await sut.CreateEnrichedEventsAsync(envelope, default, default).ToListAsync(); - var result = await sut.CreateEnrichedEventsAsync(Envelope.Create(@event), default, default).ToListAsync(); + var enrichedEvent = (EnrichedManualEvent)result.Single(); - Assert.NotEmpty(result); + Assert.Equal(@event.Actor, enrichedEvent.Actor); + Assert.Equal(@event.AppId, enrichedEvent.AppId); + Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id); } [Fact] @@ -49,8 +56,9 @@ namespace Squidex.Domain.Apps.Entities.Rules var actor = RefToken.User("me"); var @event = new RuleManuallyTriggered { Actor = actor }; + var envelope = Envelope.Create(@event); - var result = await sut.CreateEnrichedEventsAsync(Envelope.Create(@event), default, default).ToListAsync(); + var result = await sut.CreateEnrichedEventsAsync(envelope, default, default).ToListAsync(); Assert.Equal(actor, ((EnrichedUserEventBase)result.Single()).Actor); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs index 0d6a840ef..1b30f1321 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -135,6 +135,27 @@ namespace Squidex.Domain.Apps.Entities.Rules .MustHaveHappened(); } + [Fact] + public async Task Should_update_repository_if_enqueing_broken_job() + { + var @event = Envelope.Create(new ContentCreated { AppId = appId }); + + var rule = CreateRule(); + + var job = new RuleJob + { + Created = now + }; + + A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default)) + .Returns(new List { new JobResult { Job = job, SkipReason = SkipReason.Failed } }.ToAsyncEnumerable()); + + await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); + + A.CallTo(() => ruleEventRepository.EnqueueAsync(job, (Exception?)null, default)) + .MustHaveHappened(); + } + [Fact] public async Task Should_update_repository_with_jobs_from_service() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs index baae0d82e..d49597529 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs @@ -46,8 +46,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking var ctx = Context(); var @event = new AppUsageExceeded { CallsCurrent = 80, CallsLimit = 120 }; + var envelope = Envelope.Create(@event); - var result = await sut.CreateEnrichedEventsAsync(Envelope.Create(@event), ctx, default).ToListAsync(); + var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); var enrichedEvent = result.Single() as EnrichedUsageExceededEvent; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs index 5a85d0ab0..4966784c8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs @@ -11,6 +11,8 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Schemas; @@ -38,11 +40,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas public static IEnumerable TestEvents() { - yield return new object[] { new SchemaCreated(), EnrichedSchemaEventType.Created }; - yield return new object[] { new SchemaUpdated(), EnrichedSchemaEventType.Updated }; - yield return new object[] { new SchemaDeleted(), EnrichedSchemaEventType.Deleted }; - yield return new object[] { new SchemaPublished(), EnrichedSchemaEventType.Published }; - yield return new object[] { new SchemaUnpublished(), EnrichedSchemaEventType.Unpublished }; + yield return new object[] { TestUtils.CreateEvent(), EnrichedSchemaEventType.Created }; + yield return new object[] { TestUtils.CreateEvent(), EnrichedSchemaEventType.Updated }; + yield return new object[] { TestUtils.CreateEvent(), EnrichedSchemaEventType.Deleted }; + yield return new object[] { TestUtils.CreateEvent(), EnrichedSchemaEventType.Published }; + yield return new object[] { TestUtils.CreateEvent(), EnrichedSchemaEventType.Unpublished }; } [Fact] @@ -67,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas [MemberData(nameof(TestEvents))] public async Task Should_create_enriched_events(SchemaEvent @event, EnrichedSchemaEventType type) { - var ctx = Context(); + var ctx = Context(appId: @event.AppId); var envelope = Envelope.Create(@event).SetEventStreamNumber(12); @@ -76,6 +78,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas var enrichedEvent = result.Single() as EnrichedSchemaEvent; Assert.Equal(type, enrichedEvent!.Type); + Assert.Equal(@event.Actor, enrichedEvent.Actor); + Assert.Equal(@event.AppId, enrichedEvent.AppId); + Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id); + Assert.Equal(@event.SchemaId, enrichedEvent.SchemaId); + Assert.Equal(@event.SchemaId.Id, enrichedEvent.SchemaId.Id); } [Fact] @@ -151,13 +158,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas } } - private static RuleContext Context(RuleTrigger? trigger = null) + private static RuleContext Context(RuleTrigger? trigger = null, NamedId? appId = null) { trigger ??= new SchemaChangedTrigger(); return new RuleContext { - AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), + AppId = appId ?? NamedId.Of(DomainId.NewGuid(), "my-app"), Rule = new Rule(trigger, A.Fake()), RuleId = DomainId.NewGuid() }; diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs index 993611883..bd1bac361 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs @@ -31,7 +31,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_run_rules() + public async Task Should_run_rules_on_content_change() { // STEP 0: Create app. await CreateAppAsync(); @@ -77,6 +77,94 @@ namespace TestSuite.ApiTests Assert.Single(eventsRule.Items); } + [Fact] + public async Task Should_run_rules_on_asset_change() + { + // STEP 0: Create app. + await CreateAppAsync(); + + + // STEP 1: Start webhook session + var (url, sessionId) = await webhookCatcher.CreateSessionAsync(); + + + // STEP 2: Create rule + var createRule = new CreateRuleDto + { + Action = new WebhookRuleActionDto + { + Method = WebhookMethod.POST, + Payload = null, + PayloadType = null, + Url = new Uri(url) + }, + Trigger = new AssetChangedRuleTriggerDto() + }; + + var rule = await _.Rules.PostRuleAsync(appName, createRule); + + + // STEP 3: Create test asset + await CreateAssetAsync(); + + // Get requests. + var requests = await webhookCatcher.WaitForRequestsAsync(sessionId, TimeSpan.FromMinutes(2)); + + Assert.Contains(requests, x => x.Method == "POST" && x.Content.Contains("logo-squared", StringComparison.OrdinalIgnoreCase)); + + + // STEP 4: Get events + var eventsAll = await _.Rules.GetEventsAsync(appName, rule.Id); + var eventsRule = await _.Rules.GetEventsAsync(appName); + + Assert.Single(eventsAll.Items); + Assert.Single(eventsRule.Items); + } + + [Fact] + public async Task Should_run_rules_on_schema_change() + { + // STEP 0: Create app. + await CreateAppAsync(); + + + // STEP 1: Start webhook session + var (url, sessionId) = await webhookCatcher.CreateSessionAsync(); + + + // STEP 2: Create rule + var createRule = new CreateRuleDto + { + Action = new WebhookRuleActionDto + { + Method = WebhookMethod.POST, + Payload = null, + PayloadType = null, + Url = new Uri(url) + }, + Trigger = new SchemaChangedRuleTriggerDto() + }; + + var rule = await _.Rules.PostRuleAsync(appName, createRule); + + + // STEP 3: Create test schema + await TestEntity.CreateSchemaAsync(_.Schemas, appName, schemaName); + + // Get requests. + var requests = await webhookCatcher.WaitForRequestsAsync(sessionId, TimeSpan.FromMinutes(2)); + + Assert.Contains(requests, x => x.Method == "POST" && x.Content.Contains(schemaName, StringComparison.OrdinalIgnoreCase)); + + + // STEP 4: Get events + var eventsAll = await _.Rules.GetEventsAsync(appName, rule.Id); + var eventsRule = await _.Rules.GetEventsAsync(appName); + + Assert.Single(eventsAll.Items); + Assert.Single(eventsRule.Items); + } + [Fact] public async Task Should_run_rule_manually() { @@ -182,6 +270,19 @@ namespace TestSuite.ApiTests }); } + private async Task CreateAssetAsync() + { + // Upload a test asset + var fileInfo = new FileInfo("Assets/logo-squared.png"); + + await using (var stream = fileInfo.OpenRead()) + { + var upload = new FileParameter(stream, fileInfo.Name, "image/png"); + + await _.Assets.PostAssetAsync(appName, file: upload); + } + } + private async Task CreateAppAsync() { var createRequest = new CreateAppDto diff --git a/frontend/src/app/features/content/shared/forms/array-editor.component.html b/frontend/src/app/features/content/shared/forms/array-editor.component.html index 08a507741..6ae26c21a 100644 --- a/frontend/src/app/features/content/shared/forms/array-editor.component.html +++ b/frontend/src/app/features/content/shared/forms/array-editor.component.html @@ -1,5 +1,5 @@ -
-
+
-
+
diff --git a/frontend/src/app/features/content/shared/forms/array-editor.component.scss b/frontend/src/app/features/content/shared/forms/array-editor.component.scss index d9546e750..17456fe8f 100644 --- a/frontend/src/app/features/content/shared/forms/array-editor.component.scss +++ b/frontend/src/app/features/content/shared/forms/array-editor.component.scss @@ -23,6 +23,24 @@ virtual-scroller { background: $color-border-lighter; margin: 0; margin-bottom: 1rem; + overflow-y: auto; + + &.expanded { + @include absolute(3rem, 1.25rem, 2.375rem + 1rem + .75rem, 1.25rem); + margin: 0; + + virtual-scroller { + @include absolute(0, 0, 0, 0); + height: 100%; + } + } +} + +.array-buttons { + &.expanded { + @include absolute(null, 1.25rem, .75rem, 1.25rem); + max-height: none; + } } .drag-container { diff --git a/frontend/src/app/features/content/shared/forms/array-editor.component.ts b/frontend/src/app/features/content/shared/forms/array-editor.component.ts index 46ccac253..f677c71aa 100644 --- a/frontend/src/app/features/content/shared/forms/array-editor.component.ts +++ b/frontend/src/app/features/content/shared/forms/array-editor.component.ts @@ -35,6 +35,9 @@ export class ArrayEditorComponent implements OnChanges { @Input() public isComparing = false; + @Input() + public isExpanded = false; + @Input() public canUnset?: boolean | null; diff --git a/frontend/src/app/features/content/shared/forms/assets-editor.component.html b/frontend/src/app/features/content/shared/forms/assets-editor.component.html index 06ebd134e..67563ca28 100644 --- a/frontend/src/app/features/content/shared/forms/assets-editor.component.html +++ b/frontend/src/app/features/content/shared/forms/assets-editor.component.html @@ -1,4 +1,8 @@ -
+
diff --git a/frontend/src/app/features/content/shared/forms/assets-editor.component.scss b/frontend/src/app/features/content/shared/forms/assets-editor.component.scss index 49146cb54..e859021a7 100644 --- a/frontend/src/app/features/content/shared/forms/assets-editor.component.scss +++ b/frontend/src/app/features/content/shared/forms/assets-editor.component.scss @@ -1,19 +1,23 @@ @import 'mixins'; @import 'vars'; -.disabled { - pointer-events: none; +.assets-container { + background: $color-background; + max-height: 26.5rem; + min-height: 26.5rem; + overflow-x: hidden; + overflow-y: scroll; + padding: 1rem; + padding-bottom: 0; + + &.expanded { + @include absolute(3rem, 1.25rem, .75rem, 1.25rem); + max-height: none; + } } -.assets { - &-container { - background: $color-background; - height: $asset-height + 2rem; - overflow-x: hidden; - overflow-y: scroll; - padding: 1rem; - padding-bottom: 0; - } +.disabled { + pointer-events: none; } .list-view { @@ -26,10 +30,6 @@ min-height: 1px; } -.unrow { - display: block; -} - .drop-area { @include truncate-nowidth; border: 2px dashed darken($color-border, 10%); diff --git a/frontend/src/app/features/content/shared/forms/assets-editor.component.ts b/frontend/src/app/features/content/shared/forms/assets-editor.component.ts index a0b3d3911..ea39f02f9 100644 --- a/frontend/src/app/features/content/shared/forms/assets-editor.component.ts +++ b/frontend/src/app/features/content/shared/forms/assets-editor.component.ts @@ -50,6 +50,9 @@ export class AssetsEditorComponent extends StatefulControlComponent - + @@ -88,6 +89,7 @@ [formModel]="$any(formModel)" [formContext]="formContext" [isComparing]="isComparing" + [isExpanded]="isExpanded" [language]="language" [languages]="languages"> @@ -124,9 +126,10 @@ diff --git a/frontend/src/app/features/content/shared/forms/field-editor.component.scss b/frontend/src/app/features/content/shared/forms/field-editor.component.scss index b70c8115a..fe992d934 100644 --- a/frontend/src/app/features/content/shared/forms/field-editor.component.scss +++ b/frontend/src/app/features/content/shared/forms/field-editor.component.scss @@ -20,6 +20,7 @@ overflow-x: hidden; overflow-y: auto; padding: .75rem 1.25rem; + padding-top: 1.5rem; z-index: 500; } } diff --git a/frontend/src/app/features/content/shared/references/references-editor.component.html b/frontend/src/app/features/content/shared/references/references-editor.component.html index 1119acb23..b3b56846e 100644 --- a/frontend/src/app/features/content/shared/references/references-editor.component.html +++ b/frontend/src/app/features/content/shared/references/references-editor.component.html @@ -1,4 +1,4 @@ -
diff --git a/frontend/src/app/features/content/shared/references/references-editor.component.scss b/frontend/src/app/features/content/shared/references/references-editor.component.scss index 931a4aa3c..3724d1d9d 100644 --- a/frontend/src/app/features/content/shared/references/references-editor.component.scss +++ b/frontend/src/app/features/content/shared/references/references-editor.component.scss @@ -1,6 +1,20 @@ @import 'mixins'; @import 'vars'; +.references-container { + background: $color-background; + max-height: 20rem; + min-height: 6rem; + overflow-x: hidden; + overflow-y: auto; + padding: 1rem; + + &.expanded { + @include absolute(3rem, 1.25rem, .75rem, 1.25rem); + max-height: none; + } +} + .disabled { pointer-events: none; } @@ -13,15 +27,6 @@ text-align: center; } -.references-container { - background: $color-background; - max-height: 20rem; - min-height: 6rem; - overflow-x: hidden; - overflow-y: auto; - padding: 1rem; -} - .drop-area { border: 2px dashed darken($color-border, 10%); border-radius: $border-radius; diff --git a/frontend/src/app/features/content/shared/references/references-editor.component.ts b/frontend/src/app/features/content/shared/references/references-editor.component.ts index fd82f43e9..59af78a5c 100644 --- a/frontend/src/app/features/content/shared/references/references-editor.component.ts +++ b/frontend/src/app/features/content/shared/references/references-editor.component.ts @@ -44,6 +44,9 @@ export class ReferencesEditorComponent extends StatefulControlComponent