Browse Source

Exception handling for content validation.

pull/579/head
Sebastian 5 years ago
parent
commit
c517c272dd
  1. 1
      backend/i18n/source/backend_en.json
  2. 9
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs
  3. 14
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs
  4. 25
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs
  5. 35
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs
  6. 12
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  7. 3
      backend/src/Squidex.Shared/Texts.it.resx
  8. 3
      backend/src/Squidex.Shared/Texts.nl.resx
  9. 3
      backend/src/Squidex.Shared/Texts.resx
  10. 66
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs
  11. 49
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs
  12. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs

1
backend/i18n/source/backend_en.json

@ -147,6 +147,7 @@
"contents.validation.characterCount": "Must have exactly {count} character(s).",
"contents.validation.charactersBetween": "Must have between {min} and {max} character(s).",
"contents.validation.duplicates": "Must not contain duplicate values.",
"contents.validation.error": "Validation failed with internal error.",
"contents.validation.exactValue": "Must be exactly {value}.",
"contents.validation.extension": "Must be an allowed extension.",
"contents.validation.image": "Not an image.",

9
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs

@ -14,6 +14,7 @@ using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent.Validators;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Validation;
#pragma warning disable SA1028, IDE0004 // Code must not contain trailing whitespace
@ -25,6 +26,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
private readonly PartitionResolver partitionResolver;
private readonly ValidationContext context;
private readonly IEnumerable<IValidatorsFactory> factories;
private readonly ISemanticLog log;
private readonly ConcurrentBag<ValidationError> errors = new ConcurrentBag<ValidationError>();
public IReadOnlyCollection<ValidationError> Errors
@ -32,7 +34,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
get { return errors; }
}
public ContentValidator(PartitionResolver partitionResolver, ValidationContext context, IEnumerable<IValidatorsFactory> factories)
public ContentValidator(PartitionResolver partitionResolver, ValidationContext context, IEnumerable<IValidatorsFactory> factories, ISemanticLog log)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(factories, nameof(factories));
@ -40,6 +42,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
this.context = context;
this.factories = factories;
this.log = log;
this.partitionResolver = partitionResolver;
}
@ -72,7 +75,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{
Guard.NotNull(data, nameof(data));
var validator = new AggregateValidator(CreateContentValidators());
var validator = new AggregateValidator(CreateContentValidators(), log);
return validator.ValidateAsync(data, context, AddError);
}
@ -108,7 +111,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return new AggregateValidator(
CreateFieldValidators(field)
.Union(Enumerable.Repeat(
new ObjectValidator<IJsonValue>(fieldsValidators, isPartial, typeName), 1)));
new ObjectValidator<IJsonValue>(fieldsValidators, isPartial, typeName), 1)), log);
}
private IValidator CreateFieldValidator(IField field)

14
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs

@ -48,13 +48,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
ValidationMode mode = ValidationMode.Default)
{
AppId = appId;
ContentId = contentId;
IsOptional = isOptional;
Mode = mode;
Path = path;
Schema = schema;
SchemaId = schemaId;
IsOptional = isOptional;
Path = path;
}
public ValidationContext Optimized(bool isOptimized = true)
@ -69,14 +73,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return Clone(Path, IsOptional, mode);
}
public ValidationContext Optional(bool isOptional)
public ValidationContext Optional(bool fieldIsOptional)
{
if (IsOptional == isOptional)
if (IsOptional == fieldIsOptional)
{
return this;
}
return Clone(Path, isOptional, Mode);
return Clone(Path, fieldIsOptional, Mode);
}
public ValidationContext Nested(string property)

25
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs

@ -5,29 +5,44 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Translations;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class AggregateValidator : IValidator
{
private readonly IValidator[]? validators;
private readonly ISemanticLog log;
public AggregateValidator(IEnumerable<IValidator>? validators)
public AggregateValidator(IEnumerable<IValidator>? validators, ISemanticLog log)
{
this.validators = validators?.ToArray();
this.log = log;
}
public Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (validators?.Length > 0)
try
{
return Task.WhenAll(validators.Select(x => x.ValidateAsync(value, context, addError)));
if (validators?.Length > 0)
{
await Task.WhenAll(validators.Select(x => x.ValidateAsync(value, context, addError)));
}
}
catch (Exception ex)
{
log.LogError(ex, w => w
.WriteProperty("action", "validateField")
.WriteProperty("status", "Failed"));
return Task.CompletedTask;
addError(context.Path, T.Get("contents.validation.error"));
}
}
}
}

35
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs

@ -17,24 +17,24 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class FieldValidator : IValidator
{
private readonly IValidator[] validators;
private readonly IValidator[]? validators;
private readonly IField field;
public FieldValidator(IEnumerable<IValidator> validators, IField field)
public FieldValidator(IEnumerable<IValidator>? validators, IField field)
{
Guard.NotNull(field, nameof(field));
this.validators = validators.ToArray();
this.validators = validators?.ToArray();
this.field = field;
}
public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
{
var typedValue = value;
try
{
var typedValue = value;
if (value is IJsonValue jsonValue)
{
if (jsonValue.Type == JsonValueType.Null)
@ -55,22 +55,23 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
}
}
}
if (validators?.Length > 0)
{
var tasks = new List<Task>();
foreach (var validator in validators)
{
tasks.Add(validator.ValidateAsync(typedValue, context, addError));
}
await Task.WhenAll(tasks);
}
}
catch
{
addError(context.Path, T.Get("contents.validation.invalid"));
return;
}
if (validators?.Length > 0)
{
var tasks = new List<Task>();
foreach (var validator in validators)
{
tasks.Add(validator.ValidateAsync(typedValue, context, addError));
}
await Task.WhenAll(tasks);
}
}
}

12
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs

@ -18,6 +18,7 @@ using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Validation;
#pragma warning disable IDE0016 // Use 'throw' expression
@ -34,6 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
};
private readonly IScriptEngine scriptEngine;
private readonly ISemanticLog log;
private readonly IAppProvider appProvider;
private readonly IEnumerable<IValidatorsFactory> factories;
private ISchemaEntity schema;
@ -41,11 +43,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
private ContentCommand command;
private ValidationContext validationContext;
public ContentOperationContext(IAppProvider appProvider, IEnumerable<IValidatorsFactory> factories, IScriptEngine scriptEngine)
public ContentOperationContext(IAppProvider appProvider, IEnumerable<IValidatorsFactory> factories, IScriptEngine scriptEngine, ISemanticLog log)
{
this.appProvider = appProvider;
this.factories = factories;
this.scriptEngine = scriptEngine;
this.log = log;
}
public ISchemaEntity Schema
@ -85,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task ValidateInputAsync(NamedContentData data)
{
var validator = new ContentValidator(app.PartitionResolver(), validationContext, factories);
var validator = new ContentValidator(app.PartitionResolver(), validationContext, factories, log);
await validator.ValidateInputAsync(data);
@ -94,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task ValidateInputPartialAsync(NamedContentData data)
{
var validator = new ContentValidator(app.PartitionResolver(), validationContext, factories);
var validator = new ContentValidator(app.PartitionResolver(), validationContext, factories, log);
await validator.ValidateInputPartialAsync(data);
@ -103,7 +107,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task ValidateContentAsync(NamedContentData data)
{
var validator = new ContentValidator(app.PartitionResolver(), validationContext, factories);
var validator = new ContentValidator(app.PartitionResolver(), validationContext, factories, log);
await validator.ValidateContentAsync(data);

3
backend/src/Squidex.Shared/Texts.it.resx

@ -526,6 +526,9 @@
<data name="contents.validation.duplicates" xml:space="preserve">
<value>Non può avere valori duplicati.</value>
</data>
<data name="contents.validation.error" xml:space="preserve">
<value>Validation failed with internal error.</value>
</data>
<data name="contents.validation.exactValue" xml:space="preserve">
<value>Deve essere esattamente {value}.</value>
</data>

3
backend/src/Squidex.Shared/Texts.nl.resx

@ -526,6 +526,9 @@
<data name="contents.validation.duplicates" xml:space="preserve">
<value>Mag geen dubbele waarden bevatten.</value>
</data>
<data name="contents.validation.error" xml:space="preserve">
<value>Validation failed with internal error.</value>
</data>
<data name="contents.validation.exactValue" xml:space="preserve">
<value>Moet exact {waarde} zijn.</value>
</data>

3
backend/src/Squidex.Shared/Texts.resx

@ -526,6 +526,9 @@
<data name="contents.validation.duplicates" xml:space="preserve">
<value>Must not contain duplicate values.</value>
</data>
<data name="contents.validation.error" xml:space="preserve">
<value>Validation failed with internal error.</value>
</data>
<data name="contents.validation.exactValue" xml:space="preserve">
<value>Must be exactly {value}.</value>
</data>

66
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs

@ -5,13 +5,17 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation;
@ -25,6 +29,68 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private readonly List<ValidationError> errors = new List<ValidationError>();
private Schema schema = new Schema("my-schema");
[Fact]
public async Task Should_add_error_if_value_validator_throws_exception()
{
var validator = A.Fake<IValidator>();
A.CallTo(() => validator.ValidateAsync(A<object?>._, A<ValidationContext>._, A<AddError>._))
.Throws(new InvalidOperationException());
var validatorFactory = A.Fake<IValidatorsFactory>();
A.CallTo(() => validatorFactory.CreateValueValidators(A<ValidationContext>._, A<IField>._, A<FieldValidatorFactory>._))
.Returns(Enumerable.Repeat(validator, 1));
schema = schema.AddNumber(1, "my-field", Partitioning.Invariant,
new NumberFieldProperties());
var data =
new NamedContentData()
.AddField("my-field",
new ContentFieldData()
.AddValue("iv", 1000));
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema, factory: validatorFactory);
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Validation failed with internal error.", "my-field")
});
}
[Fact]
public async Task Should_add_error_if_field_validator_throws_exception()
{
var validator = A.Fake<IValidator>();
A.CallTo(() => validator.ValidateAsync(A<object?>._, A<ValidationContext>._, A<AddError>._))
.Throws(new InvalidOperationException());
var validatorFactory = A.Fake<IValidatorsFactory>();
A.CallTo(() => validatorFactory.CreateFieldValidators(A<ValidationContext>._, A<IField>._, A<FieldValidatorFactory>._))
.Returns(Enumerable.Repeat(validator, 1));
schema = schema.AddNumber(1, "my-field", Partitioning.Invariant,
new NumberFieldProperties());
var data =
new NamedContentData()
.AddField("my-field",
new ContentFieldData()
.AddValue("iv", 1000));
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema, factory: validatorFactory);
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Validation failed with internal error.", "my-field")
});
}
[Fact]
public async Task Should_add_error_if_validating_data_with_unknown_field()
{

49
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs

@ -9,23 +9,30 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Core.ValidateContent.Validators;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
public delegate ValidationContext ValidationUpdater(ValidationContext context);
public static class ValidationTestExtensions
{
private static readonly NamedId<Guid> AppId = NamedId.Of(Guid.NewGuid(), "my-app");
private static readonly NamedId<Guid> SchemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private static readonly ISemanticLog Log = A.Fake<ISemanticLog>();
private static readonly IValidatorsFactory Factory = new DefaultValidatorsFactory();
public static Task ValidateAsync(this IValidator validator, object? value, IList<string> errors,
Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func<ValidationContext, ValidationContext>? updater = null)
Schema? schema = null,
ValidationMode mode = ValidationMode.Default,
ValidationUpdater? updater = null)
{
var context = CreateContext(schema, mode, updater);
@ -33,22 +40,28 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
}
public static Task ValidateAsync(this IField field, object? value, IList<string> errors,
Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func<ValidationContext, ValidationContext>? updater = null)
Schema? schema = null,
ValidationMode mode = ValidationMode.Default,
ValidationUpdater? updater = null,
IValidatorsFactory? factory = null)
{
var context = CreateContext(schema, mode, updater);
var validators = Factory.CreateValueValidators(context, field, null!);
var validators = Factories(factory).SelectMany(x => x.CreateValueValidators(context, field, null!)).ToArray();
return new FieldValidator(validators.ToArray(), field)
return new FieldValidator(validators, field)
.ValidateAsync(value, context, CreateFormatter(errors));
}
public static async Task ValidatePartialAsync(this NamedContentData data, PartitionResolver partitionResolver, IList<ValidationError> errors,
Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func<ValidationContext, ValidationContext>? updater = null)
Schema? schema = null,
ValidationMode mode = ValidationMode.Default,
ValidationUpdater? updater = null,
IValidatorsFactory? factory = null)
{
var context = CreateContext(schema, mode, updater);
var validator = new ContentValidator(partitionResolver, context, Enumerable.Repeat(Factory, 1));
var validator = new ContentValidator(partitionResolver, context, Factories(factory), Log);
await validator.ValidateInputPartialAsync(data);
@ -59,11 +72,14 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
}
public static async Task ValidateAsync(this NamedContentData data, PartitionResolver partitionResolver, IList<ValidationError> errors,
Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func<ValidationContext, ValidationContext>? updater = null)
Schema? schema = null,
ValidationMode mode = ValidationMode.Default,
ValidationUpdater? updater = null,
IValidatorsFactory? factory = null)
{
var context = CreateContext(schema, mode, updater);
var validator = new ContentValidator(partitionResolver, context, Enumerable.Repeat(Factory, 1));
var validator = new ContentValidator(partitionResolver, context, Factories(factory), Log);
await validator.ValidateInputAsync(data);
@ -88,7 +104,22 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
};
}
public static ValidationContext CreateContext(Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func<ValidationContext, ValidationContext>? updater = null)
private static IEnumerable<IValidatorsFactory> Factories(IValidatorsFactory? factory)
{
var result = Enumerable.Repeat(Factory, 1);
if (factory != null)
{
result = result.Union(Enumerable.Repeat(factory, 1));
}
return result;
}
public static ValidationContext CreateContext(
Schema? schema = null,
ValidationMode mode = ValidationMode.Default,
ValidationUpdater? updater = null)
{
var context = new ValidationContext(
AppId,

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs

@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
patched = patch.MergeInto(data);
var context = new ContentOperationContext(appProvider, Enumerable.Repeat(new DefaultValidatorsFactory(), 1), scriptEngine);
var context = new ContentOperationContext(appProvider, Enumerable.Repeat(new DefaultValidatorsFactory(), 1), scriptEngine, A.Fake<ISemanticLog>());
sut = new ContentDomainObject(Store, contentWorkflow, context, A.Dummy<ISemanticLog>());
sut.Setup(Id);

Loading…
Cancel
Save