mirror of https://github.com/Squidex/squidex.git
Browse Source
* Flow fixes. * Fix tests * Fix test * Another attempt. * Fix trigger URL * Fix testspull/1221/head
committed by
GitHub
72 changed files with 1286 additions and 517 deletions
@ -1,85 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using Squidex.Domain.Apps.Core.Rules; |
|
||||
using Squidex.Domain.Apps.Core.Rules.Triggers; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.Translations; |
|
||||
using Squidex.Infrastructure.Validation; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards; |
|
||||
|
|
||||
public sealed class RuleTriggerValidator(DomainId appId, IAppProvider appProvider, CancellationToken ct) |
|
||||
: IRuleTriggerVisitor<Task<IEnumerable<ValidationError>>> |
|
||||
{ |
|
||||
public static Task<IEnumerable<ValidationError>> ValidateAsync(DomainId appId, RuleTrigger trigger, IAppProvider appProvider, |
|
||||
CancellationToken ct) |
|
||||
{ |
|
||||
Guard.NotNull(trigger); |
|
||||
Guard.NotNull(appProvider); |
|
||||
|
|
||||
var visitor = new RuleTriggerValidator(appId, appProvider, ct); |
|
||||
|
|
||||
return trigger.Accept(visitor); |
|
||||
} |
|
||||
|
|
||||
public Task<IEnumerable<ValidationError>> Visit(CommentTrigger trigger) |
|
||||
{ |
|
||||
return Task.FromResult(Enumerable.Empty<ValidationError>()); |
|
||||
} |
|
||||
|
|
||||
public Task<IEnumerable<ValidationError>> Visit(AssetChangedTriggerV2 trigger) |
|
||||
{ |
|
||||
return Task.FromResult(Enumerable.Empty<ValidationError>()); |
|
||||
} |
|
||||
|
|
||||
public Task<IEnumerable<ValidationError>> Visit(ManualTrigger trigger) |
|
||||
{ |
|
||||
return Task.FromResult(Enumerable.Empty<ValidationError>()); |
|
||||
} |
|
||||
|
|
||||
public Task<IEnumerable<ValidationError>> Visit(SchemaChangedTrigger trigger) |
|
||||
{ |
|
||||
return Task.FromResult(Enumerable.Empty<ValidationError>()); |
|
||||
} |
|
||||
|
|
||||
public Task<IEnumerable<ValidationError>> Visit(UsageTrigger trigger) |
|
||||
{ |
|
||||
var errors = new List<ValidationError>(); |
|
||||
|
|
||||
if (trigger.NumDays is < 1 or > 30) |
|
||||
{ |
|
||||
errors.Add(new ValidationError(Not.Between(nameof(trigger.NumDays), 1, 30), nameof(trigger.NumDays))); |
|
||||
} |
|
||||
|
|
||||
return Task.FromResult<IEnumerable<ValidationError>>(errors); |
|
||||
} |
|
||||
|
|
||||
public async Task<IEnumerable<ValidationError>> Visit(ContentChangedTriggerV2 trigger) |
|
||||
{ |
|
||||
var errors = new List<ValidationError>(); |
|
||||
|
|
||||
if (trigger.Schemas == null) |
|
||||
{ |
|
||||
return errors; |
|
||||
} |
|
||||
|
|
||||
foreach (var schema in trigger.Schemas) |
|
||||
{ |
|
||||
if (schema.SchemaId == DomainId.Empty) |
|
||||
{ |
|
||||
errors.Add(new ValidationError(Not.Defined("SchemaId"), nameof(trigger.Schemas))); |
|
||||
} |
|
||||
else if (await appProvider.GetSchemaAsync(appId, schema.SchemaId, false, ct) == null) |
|
||||
{ |
|
||||
errors.Add(new ValidationError(T.Get("schemas.notFoundId", new { id = schema.SchemaId }), nameof(trigger.Schemas))); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return errors; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,26 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Rules; |
||||
|
using Squidex.Flows; |
||||
|
using Squidex.Flows.Internal; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Validation; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules; |
||||
|
|
||||
|
public interface IRuleValidator |
||||
|
{ |
||||
|
Task ValidateTriggerAsync(RuleTrigger trigger, DomainId appId, AddValidation addError, |
||||
|
CancellationToken ct = default); |
||||
|
|
||||
|
Task ValidateFlowAsync(FlowDefinition flow, AddValidation addError, |
||||
|
CancellationToken ct = default); |
||||
|
|
||||
|
Task ValidateStepAsync(FlowStep step, AddValidation addError, |
||||
|
CancellationToken ct = default); |
||||
|
} |
||||
@ -0,0 +1,141 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.HandleRules; |
||||
|
using Squidex.Domain.Apps.Core.Rules; |
||||
|
using Squidex.Domain.Apps.Core.Rules.Triggers; |
||||
|
using Squidex.Flows; |
||||
|
using Squidex.Flows.Internal; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Translations; |
||||
|
using Squidex.Infrastructure.Validation; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules; |
||||
|
|
||||
|
public sealed class RuleValidator(IFlowManager<FlowEventContext> flowManager, IAppProvider appProvider) : IRuleValidator |
||||
|
{ |
||||
|
public async Task ValidateTriggerAsync(RuleTrigger trigger, DomainId appId, AddValidation addError, |
||||
|
CancellationToken ct = default) |
||||
|
{ |
||||
|
Guard.NotNull(trigger); |
||||
|
Guard.NotNull(addError); |
||||
|
|
||||
|
var context = new TriggerValidationContext(appId, addError, appProvider, ct); |
||||
|
|
||||
|
await trigger.Accept(RuleTriggerValidator.Instance, context); |
||||
|
} |
||||
|
|
||||
|
public async Task ValidateFlowAsync(FlowDefinition flow, AddValidation addError, |
||||
|
CancellationToken ct = default) |
||||
|
{ |
||||
|
Guard.NotNull(flow); |
||||
|
Guard.NotNull(addError); |
||||
|
|
||||
|
await flowManager.ValidateAsync(flow, ConvertError(addError), ct); |
||||
|
} |
||||
|
|
||||
|
public async Task ValidateStepAsync(FlowStep step, AddValidation addError, |
||||
|
CancellationToken ct = default) |
||||
|
{ |
||||
|
Guard.NotNull(step); |
||||
|
Guard.NotNull(addError); |
||||
|
|
||||
|
await flowManager.ValidateAsync(step, ConvertError(addError), ct); |
||||
|
} |
||||
|
|
||||
|
private static AddError ConvertError(AddValidation addError) |
||||
|
{ |
||||
|
return new AddError((path, type, message) => |
||||
|
{ |
||||
|
switch (type) |
||||
|
{ |
||||
|
case ValidationErrorType.NoSteps: |
||||
|
addError(T.Get("rules.validation.noSteps"), path); |
||||
|
break; |
||||
|
case ValidationErrorType.NoStartStep: |
||||
|
addError(T.Get("rules.validation.noStartStep"), path); |
||||
|
break; |
||||
|
case ValidationErrorType.InvalidNextStepId: |
||||
|
addError(T.Get("rules.validation.invalidNextStepId"), path); |
||||
|
break; |
||||
|
case ValidationErrorType.InvalidStepId: |
||||
|
addError(T.Get("rules.validation.invalidStepId"), path); |
||||
|
break; |
||||
|
case ValidationErrorType.InvalidProperty when !string.IsNullOrWhiteSpace(message): |
||||
|
addError(message, path); |
||||
|
break; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private sealed class RuleTriggerValidator : IRuleTriggerVisitor<ValueTask<None>, TriggerValidationContext> |
||||
|
{ |
||||
|
public static readonly RuleTriggerValidator Instance = new RuleTriggerValidator(); |
||||
|
|
||||
|
public ValueTask<None> Visit(CommentTrigger trigger, TriggerValidationContext args) |
||||
|
{ |
||||
|
return default; |
||||
|
} |
||||
|
|
||||
|
public ValueTask<None> Visit(AssetChangedTriggerV2 trigger, TriggerValidationContext args) |
||||
|
{ |
||||
|
return default; |
||||
|
} |
||||
|
|
||||
|
public ValueTask<None> Visit(ManualTrigger trigger, TriggerValidationContext args) |
||||
|
{ |
||||
|
return default; |
||||
|
} |
||||
|
|
||||
|
public ValueTask<None> Visit(SchemaChangedTrigger trigger, TriggerValidationContext args) |
||||
|
{ |
||||
|
return default; |
||||
|
} |
||||
|
|
||||
|
public ValueTask<None> Visit(UsageTrigger trigger, TriggerValidationContext args) |
||||
|
{ |
||||
|
if (trigger.NumDays is < 1 or > 30) |
||||
|
{ |
||||
|
args.AddError(Not.Between(nameof(trigger.NumDays), 1, 30), nameof(trigger.NumDays)); |
||||
|
} |
||||
|
|
||||
|
return default; |
||||
|
} |
||||
|
|
||||
|
public async ValueTask<None> Visit(ContentChangedTriggerV2 trigger, TriggerValidationContext args) |
||||
|
{ |
||||
|
if (trigger.Schemas == null) |
||||
|
{ |
||||
|
return None.Value; |
||||
|
} |
||||
|
|
||||
|
foreach (var schema in trigger.Schemas) |
||||
|
{ |
||||
|
if (schema.SchemaId == DomainId.Empty) |
||||
|
{ |
||||
|
args.AddError(Not.Defined("SchemaId"), nameof(trigger.Schemas)); |
||||
|
} |
||||
|
else if (await args.AppProvider.GetSchemaAsync(args.AppId, schema.SchemaId, false, args.CancellationToken) == null) |
||||
|
{ |
||||
|
args.AddError(T.Get("schemas.notFoundId", new { id = schema.SchemaId }), nameof(trigger.Schemas)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return None.Value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
||||
|
#pragma warning disable RECS0082 // Parameter has the same name as a member and hides it
|
||||
|
private record struct TriggerValidationContext( |
||||
|
DomainId AppId, |
||||
|
AddValidation AddError, |
||||
|
IAppProvider AppProvider, |
||||
|
CancellationToken CancellationToken); |
||||
|
#pragma warning restore RECS0082 // Parameter has the same name as a member and hides it
|
||||
|
#pragma warning restore SA1313 // Parameter names should begin with lower-case letter
|
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Entities.Rules.Commands; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
using Squidex.Web; |
||||
|
|
||||
|
namespace Squidex.Areas.Api.Controllers.Rules.Models; |
||||
|
|
||||
|
[OpenApiRequest] |
||||
|
public class TriggerRuleDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The optional value to send to the flow.
|
||||
|
/// </summary>
|
||||
|
public JsonValue Value { get; set; } |
||||
|
|
||||
|
public TriggerRule ToCommand(DomainId id) |
||||
|
{ |
||||
|
return SimpleMapper.Map(this, new TriggerRule { RuleId = id }); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,106 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.HandleRules; |
||||
|
using Squidex.Domain.Apps.Core.Rules.Triggers; |
||||
|
using Squidex.Domain.Apps.Core.TestHelpers; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Flows; |
||||
|
using Squidex.Flows.Internal; |
||||
|
using Squidex.Infrastructure.Validation; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules; |
||||
|
|
||||
|
public class RuleValidatorTests : GivenContext, IClassFixture<TranslationsFixture> |
||||
|
{ |
||||
|
private readonly IFlowManager<FlowEventContext> flowManager = A.Fake<IFlowManager<FlowEventContext>>(); |
||||
|
private readonly RuleValidator sut; |
||||
|
|
||||
|
private sealed record TestFlowStep : FlowStep |
||||
|
{ |
||||
|
public override ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
||||
|
CancellationToken ct) |
||||
|
{ |
||||
|
return default; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public RuleValidatorTests() |
||||
|
{ |
||||
|
sut = new RuleValidator(flowManager, AppProvider); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_validate_trigger() |
||||
|
{ |
||||
|
var trigger = new UsageTrigger { NumDays = 50 }; |
||||
|
|
||||
|
var errors = new List<ValidationError>(); |
||||
|
|
||||
|
await sut.ValidateTriggerAsync( |
||||
|
trigger, |
||||
|
AppId.Id, |
||||
|
(message, properties) => errors.Add(new ValidationError(message, properties)), |
||||
|
CancellationToken); |
||||
|
|
||||
|
errors.Should().BeEquivalentTo( |
||||
|
[ |
||||
|
new ValidationError("Num days must be between 1 and 30.", "NumDays"), |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_validate_step() |
||||
|
{ |
||||
|
var step = new TestFlowStep(); |
||||
|
|
||||
|
A.CallTo(() => flowManager.ValidateAsync(step, A<AddError>._, CancellationToken)) |
||||
|
.Invokes(x => |
||||
|
{ |
||||
|
x.GetArgument<AddError>(1)?.Invoke("Path", ValidationErrorType.InvalidProperty, "Invalid property."); |
||||
|
}); |
||||
|
|
||||
|
var errors = new List<ValidationError>(); |
||||
|
|
||||
|
await sut.ValidateStepAsync( |
||||
|
step, |
||||
|
(message, properties) => errors.Add(new ValidationError(message, properties)), |
||||
|
CancellationToken); |
||||
|
|
||||
|
errors.Should().BeEquivalentTo( |
||||
|
[ |
||||
|
new ValidationError("Invalid property.", "Path"), |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(ValidationErrorType.NoSteps, "Flow has no step")] |
||||
|
[InlineData(ValidationErrorType.NoStartStep, "Flow has no start step")] |
||||
|
[InlineData(ValidationErrorType.InvalidNextStepId, "Invalid next step ID")] |
||||
|
[InlineData(ValidationErrorType.InvalidStepId, "Invalid step ID")] |
||||
|
public async Task Should_validate_flow(ValidationErrorType type, string expectedMessage) |
||||
|
{ |
||||
|
var flow = new FlowDefinition(); |
||||
|
|
||||
|
A.CallTo(() => flowManager.ValidateAsync(flow, A<AddError>._, CancellationToken)) |
||||
|
.Invokes(x => |
||||
|
{ |
||||
|
x.GetArgument<AddError>(1)?.Invoke("Path", type); |
||||
|
}); |
||||
|
|
||||
|
var errors = new List<ValidationError>(); |
||||
|
|
||||
|
await sut.ValidateFlowAsync(flow, |
||||
|
(message, properties) => errors.Add(new ValidationError(message, properties)), |
||||
|
CancellationToken); |
||||
|
|
||||
|
errors.Should().BeEquivalentTo( |
||||
|
[ |
||||
|
new ValidationError(expectedMessage, "Path"), |
||||
|
]); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,84 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.ClientLibrary; |
||||
|
using TestSuite.Fixtures; |
||||
|
|
||||
|
#pragma warning disable SA1300 // Element should begin with upper-case letter
|
||||
|
|
||||
|
namespace TestSuite.ApiTests; |
||||
|
|
||||
|
public class RuleValidationTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFixture> |
||||
|
{ |
||||
|
public CreatedAppFixture _ { get; } = fixture; |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_get_steps() |
||||
|
{ |
||||
|
var steps = await _.Client.Rules.GetStepsAsync(); |
||||
|
|
||||
|
Assert.NotEmpty(steps); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_get_event_schemas() |
||||
|
{ |
||||
|
var schema = await _.Client.Rules.GetEventSchemaAsync("EnrichedContentEvent"); |
||||
|
|
||||
|
Assert.NotNull(schema); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_get_event_types() |
||||
|
{ |
||||
|
var eventTypes = await _.Client.Rules.GetEventTypesAsync(); |
||||
|
|
||||
|
Assert.NotEmpty(eventTypes); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_validate_valid_trigger() |
||||
|
{ |
||||
|
var trigger = new UsageRuleTriggerDto { Limit = 500, NumDays = 5 }; |
||||
|
|
||||
|
await _.Client.Rules.ValidateRuleAsync(trigger); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_validate_invalid_trigger() |
||||
|
{ |
||||
|
var trigger = new UsageRuleTriggerDto { Limit = 500, NumDays = 50 }; |
||||
|
|
||||
|
var ex = await Assert.ThrowsAnyAsync<SquidexException>(async () => |
||||
|
{ |
||||
|
await _.Client.Rules.ValidateRuleAsync(trigger); |
||||
|
}); |
||||
|
|
||||
|
Assert.Equal(400, ex.StatusCode); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_validate_valid_step() |
||||
|
{ |
||||
|
var trigger = new WebhookFlowStepDto { Url = new Uri("https://squidex.io") }; |
||||
|
|
||||
|
await _.Client.Rules.ValidateStepAsync(trigger); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_validate_invalid_step() |
||||
|
{ |
||||
|
var trigger = new WebhookFlowStepDto(); |
||||
|
|
||||
|
var ex = await Assert.ThrowsAnyAsync<SquidexException>(async () => |
||||
|
{ |
||||
|
await _.Client.Rules.ValidateStepAsync(trigger); |
||||
|
}); |
||||
|
|
||||
|
Assert.Equal(400, ex.StatusCode); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue