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