From 511d3641098bde7631575a1115c3419f9af0079b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 29 May 2018 15:46:23 +0200 Subject: [PATCH] More tests and fixes to make the tests greeen. --- .../ValidateContent/ContentValidator.cs | 15 ++- .../ValidateContent/JsonValueConverter.cs | 28 ++--- .../ValidateContent/ObjectPath.cs | 52 ++++++++ .../ValidateContent/ValidationContext.cs | 19 ++- .../Validators/AllowedValuesValidator.cs | 2 +- .../Validators/AssetsValidator.cs | 31 +++-- .../Validators/CollectionItemValidator.cs | 6 +- .../Validators/CollectionValidator.cs | 6 +- .../Validators/FieldValidator.cs | 12 +- .../ValidateContent/Validators/Formatter.cs | 46 ------- .../ValidateContent/Validators/IValidator.cs | 5 +- .../Validators/ObjectValidator.cs | 16 +-- .../Validators/PatternValidator.cs | 6 +- .../Validators/RangeValidator.cs | 4 +- .../Validators/ReferencesValidator.cs | 2 +- .../Validators/RequiredStringValidator.cs | 2 +- .../Validators/RequiredValidator.cs | 2 +- .../Validators/StringLengthValidator.cs | 4 +- .../ValidateContent/ValidatorsFactory.cs | 6 +- .../Model/Schemas/ArrayFieldTests.cs | 2 +- .../Model/Schemas/SchemaTests.cs | 2 +- .../ValidateContent/ArrayFieldTests.cs | 116 ++++++++++++++++++ .../ValidateContent/ContentValidationTests.cs | 51 ++++++-- .../ValidateContent/ReferencesFieldTests.cs | 4 +- .../ValidateContent/TagsFieldTests.cs | 10 +- .../ValidationTestExtensions.cs | 8 +- 26 files changed, 307 insertions(+), 150 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/Formatter.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs index 69ee24968..c62931490 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs @@ -5,8 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; +using System.Text; using System.Threading.Tasks; using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Contents; @@ -42,9 +45,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent this.partitionResolver = partitionResolver; } - private void AddError(string field, string message) + private void AddError(IEnumerable path, string message) { - errors.Add(new ValidationError($"{field}: {message}", field)); + var pathString = path.ToPathString(); + + errors.Add(new ValidationError($"{pathString}: {message}", pathString)); } public Task ValidatePartialAsync(NamedContentData data) @@ -74,14 +79,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent fieldsValidators[field.Key] = (!field.Value.RawProperties.IsRequired, CreateFieldValidator(field.Value, isPartial)); } - return new ObjectValidator(fieldsValidators, isPartial, "field", DefaultFieldData, Formatter.CombineForLanguage); + return new ObjectValidator(fieldsValidators, isPartial, "field", DefaultFieldData); } private IValidator CreateFieldValidator(IRootField field, bool isPartial) { var partitioning = partitionResolver(field.Partitioning); - var fieldValidator = new FieldValidator(ValidatorsFactory.CreateValidators(field), field); + var fieldValidator = new FieldValidator(ValidatorsFactory.CreateValidators(field).ToArray(), field); var fieldsValidators = new Dictionary(); foreach (var partition in partitioning) @@ -93,7 +98,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent var type = isLanguage ? "language" : "invariant value"; - return new ObjectValidator(fieldsValidators, isPartial, type, DefaultValue, Formatter.Combine); + return new ObjectValidator(fieldsValidators, isPartial, type, DefaultValue); } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs index 80841803b..a43f8a095 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs @@ -16,11 +16,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { public sealed class JsonValueConverter : IFieldVisitor { - public JToken Value { get; } + private readonly JToken value; private JsonValueConverter(JToken value) { - Value = value; + this.value = value; } public static object ConvertValue(IField field, JToken json) @@ -30,24 +30,24 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public object Visit(IArrayField field) { - return Value.ToObject>(); + return value.ToObject>(); } public object Visit(IField field) { - return Value.ToObject>(); + return value.ToObject>(); } public object Visit(IField field) { - return (bool?)Value; + return (bool?)value; } public object Visit(IField field) { - if (Value.Type == JTokenType.String) + if (value.Type == JTokenType.String) { - var parseResult = InstantPattern.General.Parse(Value.ToString()); + var parseResult = InstantPattern.General.Parse(value.ToString()); if (!parseResult.Success) { @@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public object Visit(IField field) { - var geolocation = (JObject)Value; + var geolocation = (JObject)value; foreach (var property in geolocation.Properties()) { @@ -86,32 +86,32 @@ namespace Squidex.Domain.Apps.Core.ValidateContent throw new InvalidCastException("Longitude must be between -180 and 180."); } - return Value; + return value; } public object Visit(IField field) { - return Value; + return value; } public object Visit(IField field) { - return (double?)Value; + return (double?)value; } public object Visit(IField field) { - return Value.ToObject>(); + return value.ToObject>(); } public object Visit(IField field) { - return Value.ToString(); + return value.ToString(); } public object Visit(IField field) { - return Value.ToObject>(); + return value.ToObject>(); } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs new file mode 100644 index 000000000..3b1c216cb --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs @@ -0,0 +1,52 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public static class ObjectPath + { + public static string ToPathString(this IEnumerable path) + { + var sb = new StringBuilder(); + + var index = 0; + foreach (var property in path) + { + if (index == 0) + { + sb.Append(property); + } + else if (index == 1) + { + if (!property.Equals(InvariantPartitioning.Instance.Master.Key, StringComparison.OrdinalIgnoreCase)) + { + sb.Append("("); + sb.Append(property); + sb.Append(")"); + } + } + else + { + if (property[0] != '[') + { + sb.Append("."); + } + + sb.Append(property); + } + + index++; + } + + return sb.ToString(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs index 4a57b5fc8..5db995c34 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Threading.Tasks; using Squidex.Infrastructure; @@ -16,24 +17,33 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { private readonly Func, Guid, Task>> checkContent; private readonly Func, Task>> checkAsset; + private readonly ImmutableQueue propertyPath; + + public ImmutableQueue Path + { + get { return propertyPath; } + } public bool IsOptional { get; } public ValidationContext( Func, Guid, Task>> checkContent, Func, Task>> checkAsset) - : this(checkContent, checkAsset, false) + : this(checkContent, checkAsset, ImmutableQueue.Empty, false) { } private ValidationContext( Func, Guid, Task>> checkContent, Func, Task>> checkAsset, + ImmutableQueue propertyPath, bool isOptional) { Guard.NotNull(checkAsset, nameof(checkAsset)); Guard.NotNull(checkContent, nameof(checkAsset)); + this.propertyPath = propertyPath; + this.checkContent = checkContent; this.checkAsset = checkAsset; @@ -42,7 +52,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public ValidationContext Optional(bool isOptional) { - return isOptional == IsOptional ? this : new ValidationContext(checkContent, checkAsset, isOptional); + return isOptional == IsOptional ? this : new ValidationContext(checkContent, checkAsset, propertyPath, isOptional); + } + + public ValidationContext Nested(string property) + { + return new ValidationContext(checkContent, checkAsset, propertyPath.Enqueue(property), IsOptional); } public Task> GetInvalidContentIdsAsync(IEnumerable contentIds, Guid schemaId) diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs index 9cd0a56dc..9b1d6129d 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (!allowedValues.Contains(typedValue)) { - addError(null, "Not an allowed value."); + addError(context.Path, "Not an allowed value."); } return TaskHelper.Done; diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs index 21a701ed2..f1a87c283 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs @@ -28,47 +28,44 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (value is ICollection assetIds && assetIds.Count > 0) { var assets = await context.GetAssetInfosAsync(assetIds); - var i = 0; + var index = 0; foreach (var assetId in assetIds) { - i++; + index++; - var asset = assets.FirstOrDefault(x => x.AssetId == assetId); + var path = context.Path.Enqueue($"[{index}]"); - void Error(string message) - { - addError($"[{i}]", message); - } + var asset = assets.FirstOrDefault(x => x.AssetId == assetId); if (asset == null) { - Error($"Id '{assetId}' not found."); + addError(path, $"Id '{assetId}' not found."); continue; } if (properties.MinSize.HasValue && asset.FileSize < properties.MinSize) { - Error($"'{asset.FileSize.ToReadableSize()}' less than minimum of '{properties.MinSize.Value.ToReadableSize()}'."); + addError(path, $"'{asset.FileSize.ToReadableSize()}' less than minimum of '{properties.MinSize.Value.ToReadableSize()}'."); } if (properties.MaxSize.HasValue && asset.FileSize > properties.MaxSize) { - Error($"'{asset.FileSize.ToReadableSize()}' greater than maximum of '{properties.MaxSize.Value.ToReadableSize()}'."); + addError(path, $"'{asset.FileSize.ToReadableSize()}' greater than maximum of '{properties.MaxSize.Value.ToReadableSize()}'."); } if (properties.AllowedExtensions != null && properties.AllowedExtensions.Count > 0 && !properties.AllowedExtensions.Any(x => asset.FileName.EndsWith("." + x, StringComparison.OrdinalIgnoreCase))) { - Error("Invalid file extension."); + addError(path, "Invalid file extension."); } if (!asset.IsImage) { if (properties.MustBeImage) { - Error("Not an image."); + addError(path, "Not an image."); } continue; @@ -84,22 +81,22 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (properties.MinWidth.HasValue && w < properties.MinWidth) { - Error($"Width '{w}px' less than minimum of '{properties.MinWidth}px'."); + addError(path, $"Width '{w}px' less than minimum of '{properties.MinWidth}px'."); } if (properties.MaxWidth.HasValue && w > properties.MaxWidth) { - Error($"Width '{w}px' greater than maximum of '{properties.MaxWidth}px'."); + addError(path, $"Width '{w}px' greater than maximum of '{properties.MaxWidth}px'."); } if (properties.MinHeight.HasValue && h < properties.MinHeight) { - Error($"Height '{h}px' less than minimum of '{properties.MinHeight}px'."); + addError(path, $"Height '{h}px' less than minimum of '{properties.MinHeight}px'."); } if (properties.MaxHeight.HasValue && h > properties.MaxHeight) { - Error($"Height '{h}px' greater than maximum of '{properties.MaxHeight}px'."); + addError(path, $"Height '{h}px' greater than maximum of '{properties.MaxHeight}px'."); } if (properties.AspectHeight.HasValue && properties.AspectWidth.HasValue) @@ -108,7 +105,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (Math.Abs(expectedRatio - actualRatio) > double.Epsilon) { - Error($"Aspect ratio not '{properties.AspectWidth}:{properties.AspectHeight}'."); + addError(path, $"Aspect ratio not '{properties.AspectWidth}:{properties.AspectHeight}'."); } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs index e4d928cd2..8e8efdd46 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs @@ -29,17 +29,15 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (value is ICollection items && items.Count > 0) { var innerTasks = new List(); - var innerContext = context.Optional(false); - var index = 1; foreach (var item in items) { - var itemFormatter = Formatter.Combine($"[{index}]", addError); + var innerContext = context.Nested($"[{index}]"); foreach (var itemValidator in itemValidators) { - innerTasks.Add(itemValidator.ValidateAsync(item, innerContext, itemFormatter)); + innerTasks.Add(itemValidator.ValidateAsync(item, innerContext, addError)); } index++; diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs index a5cca83cd..820afe308 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { if (isRequired && !context.IsOptional) { - addError(null, "Field is required."); + addError(context.Path, "Field is required."); } return TaskHelper.Done; @@ -38,12 +38,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (minItems.HasValue && items.Count < minItems.Value) { - addError(null, $"Must have at least {minItems} item(s)."); + addError(context.Path, $"Must have at least {minItems} item(s)."); } if (maxItems.HasValue && items.Count > maxItems.Value) { - addError(null, $"Must have not more than {maxItems} item(s)."); + addError(context.Path, $"Must have not more than {maxItems} item(s)."); } return TaskHelper.Done; diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs index b49d790e4..2c8e4d680 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs @@ -15,10 +15,10 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { public sealed class FieldValidator : IValidator { - private readonly IEnumerable validators; + private readonly IValidator[] validators; private readonly IField field; - public FieldValidator(IEnumerable validators, IField field) + public FieldValidator(IValidator[] validators, IField field) { this.validators = validators; this.field = field; @@ -35,14 +35,18 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators typedValue = jToken.IsNull() ? null : JsonValueConverter.ConvertValue(field, jToken); } + var tasks = new List(); + foreach (var validator in ValidatorsFactory.CreateValidators(field)) { - await validator.ValidateAsync(typedValue, context, addError); + tasks.Add(validator.ValidateAsync(typedValue, context, addError)); } + + await Task.WhenAll(tasks); } catch { - addError(null, "Not a valid value."); + addError(context.Path, "Not a valid value."); } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/Formatter.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/Formatter.cs deleted file mode 100644 index e6debf3ae..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/Formatter.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public static class Formatter - { - private static readonly string IV = InvariantPartitioning.Instance.Master.Key; - - public static AddError Combine(string field, AddError formatter) - { - return (f, m) => - { - if (!string.IsNullOrWhiteSpace(f)) - { - formatter($"{field}.{f}", m); - } - else - { - formatter(field, m); - } - }; - } - - public static AddError CombineForLanguage(string field, AddError formatter) - { - return (f, m) => - { - if (!string.IsNullOrWhiteSpace(f) && !string.Equals(f, IV, StringComparison.OrdinalIgnoreCase)) - { - formatter($"{field}.{f}", m); - } - else - { - formatter(field, m); - } - }; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs index d9faf958c..47592700f 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; using System.Threading.Tasks; namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { - public delegate void AddError(string field, string message); - - public delegate AddError CombineFields(string field, AddError formatter); + public delegate void AddError(IEnumerable path, string message); public interface IValidator { diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs index 5426b40de..6dd8a9c28 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs @@ -16,12 +16,10 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators private readonly bool isPartial; private readonly string fieldType; private readonly TValue fieldDefault; - private readonly CombineFields combiner; - public ObjectValidator(IDictionary schema, bool isPartial, string fieldType, TValue fieldDefault, CombineFields combiner) + public ObjectValidator(IDictionary schema, bool isPartial, string fieldType, TValue fieldDefault) { this.schema = schema; - this.combiner = combiner; this.fieldDefault = fieldDefault; this.fieldType = fieldType; this.isPartial = isPartial; @@ -29,7 +27,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators public async Task ValidateAsync(object value, ValidationContext context, AddError addError) { - if (value is IReadOnlyDictionary values) + if (value is IDictionary values) { foreach (var fieldData in values) { @@ -37,9 +35,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (!schema.ContainsKey(name)) { - var fieldFormatter = combiner?.Invoke(name, addError) ?? Formatter.Combine(name, addError); - - fieldFormatter(null, $"Not a known {fieldType}."); + addError(context.Path.Enqueue(name), $"Not a known {fieldType}."); } } @@ -60,11 +56,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators } var (isOptional, validator) = field.Value; + var fieldContext = context.Nested(name).Optional(isOptional); - var fieldContext = context.Optional(isOptional); - var fieldFormatter = combiner(name, addError); - - tasks.Add(validator.ValidateAsync(fieldValue, fieldContext, fieldFormatter)); + tasks.Add(validator.ValidateAsync(fieldValue, fieldContext, addError)); } await Task.WhenAll(tasks); diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs index 981f1e3e1..c358c3bdf 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs @@ -37,17 +37,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { if (string.IsNullOrWhiteSpace(errorMessage)) { - addError(null, "Not valid."); + addError(context.Path, "Not valid."); } else { - addError(null, errorMessage); + addError(context.Path, errorMessage); } } } catch { - addError(null, "Regex is too slow."); + addError(context.Path, "Regex is too slow."); } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs index f000f3cd8..5c26227fd 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs @@ -38,12 +38,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (min.HasValue && typedValue.CompareTo(min.Value) < 0) { - addError(null, $"Must be greater or equals than '{min}'."); + addError(context.Path, $"Must be greater or equals than '{min}'."); } if (max.HasValue && typedValue.CompareTo(max.Value) > 0) { - addError(null, $"Must be less or equals than '{max}'."); + addError(context.Path, $"Must be less or equals than '{max}'."); } return TaskHelper.Done; diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs index 417ca6107..9fd97bc02 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators foreach (var invalidId in invalidIds) { - addError(null, $"Contains invalid reference '{invalidId}'."); + addError(context.Path, $"Contains invalid reference '{invalidId}'."); } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs index c21523458..eacbcdb79 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (valueAsString == null || (validateEmptyStrings && string.IsNullOrWhiteSpace(valueAsString))) { - addError(null, "Field is required."); + addError(context.Path, "Field is required."); } return TaskHelper.Done; diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs index efd862c77..6d2d308a6 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs @@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { if (value == null && !context.IsOptional) { - addError(null, "Field is required."); + addError(context.Path, "Field is required."); } return TaskHelper.Done; diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs index 569236751..f89b1ed3e 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs @@ -33,12 +33,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { if (minLength.HasValue && stringValue.Length < minLength.Value) { - addError(null, $"Must have more than '{minLength}' characters."); + addError(context.Path, $"Must have more than '{minLength}' characters."); } if (maxLength.HasValue && stringValue.Length > maxLength.Value) { - addError(null, $"Must have less than '{maxLength}' characters."); + addError(context.Path, $"Must have less than '{maxLength}' characters."); } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs index 3e415bdbf..eba15cb29 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs @@ -38,14 +38,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); } - var fieldsValidators = new Dictionary(); + var nestedSchema = new Dictionary(); foreach (var nestedField in field.Fields) { - fieldsValidators[nestedField.Name] = (false, new FieldValidator(nestedField.Accept(this), nestedField)); + nestedSchema[nestedField.Name] = (false, new FieldValidator(nestedField.Accept(this).ToArray(), nestedField)); } - yield return new CollectionItemValidator(new ObjectValidator(fieldsValidators, false, "field", JValue.CreateNull(), Formatter.Combine)); + yield return new CollectionItemValidator(new ObjectValidator(nestedSchema, false, "field", JValue.CreateNull())); } public IEnumerable Visit(IField field) diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs index de33f9949..b8c910077 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs @@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas [Fact] public void Should_return_same_parent_if_field_to_hide_does_not_exist() { - var parent_1 = parent_0.UpdateField(1, f => f.Hide()); ; + var parent_1 = parent_0.UpdateField(1, f => f.Hide()); Assert.Same(parent_0, parent_1); } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs index bcee356da..8b9dfcc3b 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs @@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas [Fact] public void Should_return_same_schema_if_field_to_hide_does_not_exist() { - var schema_1 = schema_0.UpdateField(1, f => f.Hide()); ; + var schema_1 = schema_0.UpdateField(1, f => f.Hide()); Assert.Same(schema_0, schema_1); } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs new file mode 100644 index 000000000..488cf757f --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs @@ -0,0 +1,116 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +{ + public class ArrayFieldTests + { + private readonly List errors = new List(); + + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new ArrayFieldProperties()); + + Assert.Equal("my-array", sut.Name); + } + + [Fact] + public async Task Should_not_add_error_if_tags_are_valid() + { + var sut = Field(new ArrayFieldProperties()); + + await sut.ValidateAsync(CreateValue(new JObject()), errors, ValidationTestExtensions.ValidContext); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_tags_are_null_and_valid() + { + var sut = Field(new ArrayFieldProperties()); + + await sut.ValidateAsync(CreateValue(null), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_errors_if_tags_are_required_and_null() + { + var sut = Field(new ArrayFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(null), errors); + + errors.ShouldBeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_errors_if_tags_are_required_and_empty() + { + var sut = Field(new ArrayFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(), errors); + + errors.ShouldBeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_errors_if_value_is_not_valid() + { + var sut = Field(new ArrayFieldProperties()); + + await sut.ValidateAsync("invalid", errors); + + errors.ShouldBeEquivalentTo( + new[] { "Not a valid value." }); + } + + [Fact] + public async Task Should_add_errors_if_value_has_not_enough_items() + { + var sut = Field(new ArrayFieldProperties { MinItems = 3 }); + + await sut.ValidateAsync(CreateValue(new JObject(), new JObject()), errors); + + errors.ShouldBeEquivalentTo( + new[] { "Must have at least 3 item(s)." }); + } + + [Fact] + public async Task Should_add_errors_if_value_has_too_much_items() + { + var sut = Field(new ArrayFieldProperties { MaxItems = 1 }); + + await sut.ValidateAsync(CreateValue(new JObject(), new JObject()), errors); + + errors.ShouldBeEquivalentTo( + new[] { "Must have not more than 1 item(s)." }); + } + + private static JToken CreateValue(params JObject[] ids) + { + return ids == null ? JValue.CreateNull() : (JToken)new JArray(ids.OfType().ToArray()); + } + + private static RootField Field(ArrayFieldProperties properties) + { + return Fields.Array(1, "my-array", Partitioning.Invariant, properties); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs index c9f9bf4b1..299609881 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; @@ -79,8 +80,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.ShouldBeEquivalentTo( new List { - new ValidationError("my-field.es: Not a known invariant value.", "my-field.es"), - new ValidationError("my-field.it: Not a known invariant value.", "my-field.it") + new ValidationError("my-field(es): Not a known invariant value.", "my-field(es)"), + new ValidationError("my-field(it): Not a known invariant value.", "my-field(it)") }); } @@ -98,8 +99,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.ShouldBeEquivalentTo( new List { - new ValidationError("my-field.de: Field is required.", "my-field.de"), - new ValidationError("my-field.en: Field is required.", "my-field.en") + new ValidationError("my-field(de): Field is required.", "my-field(de)"), + new ValidationError("my-field(en): Field is required.", "my-field(en)") }); } @@ -138,7 +139,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.ShouldBeEquivalentTo( new List { - new ValidationError("my-field.xx: Not a known language.", "my-field.xx") + new ValidationError("my-field(xx): Not a known language.", "my-field(xx)") }); } @@ -181,8 +182,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.ShouldBeEquivalentTo( new List { - new ValidationError("my-field.es: Not a known language.", "my-field.es"), - new ValidationError("my-field.it: Not a known language.", "my-field.it") + new ValidationError("my-field(es): Not a known language.", "my-field(es)"), + new ValidationError("my-field(it): Not a known language.", "my-field(it)") }); } @@ -241,8 +242,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.ShouldBeEquivalentTo( new List { - new ValidationError("my-field.es: Not a known invariant value.", "my-field.es"), - new ValidationError("my-field.it: Not a known invariant value.", "my-field.it") + new ValidationError("my-field(es): Not a known invariant value.", "my-field(es)"), + new ValidationError("my-field(it): Not a known invariant value.", "my-field(it)") }); } @@ -291,7 +292,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.ShouldBeEquivalentTo( new List { - new ValidationError("my-field.xx: Not a known language.", "my-field.xx") + new ValidationError("my-field(xx): Not a known language.", "my-field(xx)") }); } @@ -312,8 +313,34 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.ShouldBeEquivalentTo( new List { - new ValidationError("my-field.es: Not a known language.", "my-field.es"), - new ValidationError("my-field.it: Not a known language.", "my-field.it") + new ValidationError("my-field(es): Not a known language.", "my-field(es)"), + new ValidationError("my-field(it): Not a known language.", "my-field(it)") + }); + } + + [Fact] + public async Task Should_add_error_if_array_field_has_required_nested_field() + { + schema = + schema.AddArray(1, "my-field", Partitioning.Invariant, f => f. + AddNumber(1, "my-nested", new NumberFieldProperties { IsRequired = true })); + + var data = + new NamedContentData() + .AddField("my-field", + new ContentFieldData() + .AddValue("iv", new JArray( + new JObject(), + new JObject(new JProperty("my-nested", 1)), + new JObject()))); + + await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("my-field[1].my-nested: Field is required.", "my-field[1].my-nested"), + new ValidationError("my-field[3].my-nested: Field is required.", "my-field[3].my-nested"), }); } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs index 9f43a92ed..7d2781f5c 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs @@ -32,11 +32,9 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_not_add_error_if_references_are_valid() { - var referenceId = Guid.NewGuid(); - var sut = Field(new ReferencesFieldProperties()); - await sut.ValidateAsync(CreateValue(referenceId), errors, ValidationTestExtensions.ValidContext); + await sut.ValidateAsync(CreateValue(Guid.NewGuid()), errors, ValidationTestExtensions.ValidContext); Assert.Empty(errors); } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs index 132c41ca8..1a069900c 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs @@ -31,11 +31,9 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_not_add_error_if_tags_are_valid() { - var referenceId = Guid.NewGuid(); - var sut = Field(new TagsFieldProperties()); - await sut.ValidateAsync(CreateValue(referenceId), errors, ValidationTestExtensions.ValidContext); + await sut.ValidateAsync(CreateValue("tag"), errors, ValidationTestExtensions.ValidContext); Assert.Empty(errors); } @@ -88,7 +86,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new TagsFieldProperties { MinItems = 3 }); - await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors); + await sut.ValidateAsync(CreateValue("tag-1", "tag-2"), errors); errors.ShouldBeEquivalentTo( new[] { "Must have at least 3 item(s)." }); @@ -99,13 +97,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new TagsFieldProperties { MaxItems = 1 }); - await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors); + await sut.ValidateAsync(CreateValue("tag-1", "tag-2"), errors); errors.ShouldBeEquivalentTo( new[] { "Must have not more than 1 item(s)." }); } - private static JToken CreateValue(params Guid[] ids) + private static JToken CreateValue(params string[] ids) { return ids == null ? JValue.CreateNull() : (JToken)new JArray(ids.OfType().ToArray()); } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs index ad5fccf34..3b1e1c454 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs @@ -39,14 +39,14 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent public static Task ValidateAsync(this IField field, JToken value, IList errors, ValidationContext context = null) { - return new FieldValidator(ValidatorsFactory.CreateValidators(field), field).ValidateAsync(value, + return new FieldValidator(ValidatorsFactory.CreateValidators(field).ToArray(), field).ValidateAsync(value, CreateContext(context), CreateFormatter(errors)); } public static Task ValidateOptionalAsync(this IField field, JToken value, IList errors, ValidationContext context = null) { - return new FieldValidator(ValidatorsFactory.CreateValidators(field), field).ValidateAsync(value, + return new FieldValidator(ValidatorsFactory.CreateValidators(field).ToArray(), field).ValidateAsync(value, CreateContext(context).Optional(true), CreateFormatter(errors)); } @@ -55,13 +55,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { return (field, message) => { - if (field == null) + if (field == null || !field.Any()) { errors.Add(message); } else { - errors.Add($"{field}: {message}"); + errors.Add($"{field.ToPathString()}: {message}"); } }; }