From 840764f40b5bceeb04d4208fe5a90246ab3c8720 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 1 Apr 2020 12:52:40 +0200 Subject: [PATCH] Feature/validator extension (#508) * Validator extensions. * Minor fixes. * Example validator. --- .../Validation/CompositeUniqueValidator.cs | 107 ++++++++ .../CompositeUniqueValidatorFactory.cs | 47 ++++ .../CompositeUniqueValidatorPlugin.cs | 23 ++ .../ContentValidationExtensions.cs | 68 ----- .../ValidateContent/ContentValidator.cs | 49 +++- ... => DefaultFieldValueValidatorsFactory.cs} | 29 +-- ...ensions.cs => DefaultValidatorsFactory.cs} | 19 +- .../{Validators => }/IValidator.cs | 2 +- .../ValidateContent/IValidatorsFactory.cs | 33 +++ .../ValidateContent/ValidationContext.cs | 103 ++------ .../Validators/AssetsValidator.cs | 12 +- .../Validators/PatternValidator.cs | 3 + .../Validators/ReferencesValidator.cs | 12 +- .../Validators/UniqueValidator.cs | 12 +- .../Contents/ContentDomainObject.cs | 85 +++---- .../Contents/ContentOperationContext.cs | 129 +++++----- .../Validation/DependencyValidatorsFactory.cs | 76 ++++++ .../Queries/ClrFilter.cs | 12 + .../Squidex/Config/Domain/ContentsServices.cs | 11 + .../ValidateContent/ArrayFieldTests.cs | 2 +- .../ValidateContent/AssetsFieldTests.cs | 218 +--------------- .../ValidateContent/ContentValidationTests.cs | 40 ++- .../ValidateContent/NumberFieldTests.cs | 12 - .../ValidateContent/ReferencesFieldTests.cs | 72 +----- .../ValidateContent/StringFieldTests.cs | 12 - .../ValidateContent/TagsFieldTests.cs | 2 +- .../ValidateContent/UIFieldTests.cs | 29 +-- .../ValidationTestExtensions.cs | 94 ++++--- .../Validators/AssetsValidatorTests.cs | 233 ++++++++++++++++++ .../Validators/CollectionValidatorTests.cs | 2 +- .../Validators/ReferencesValidatorTests.cs | 78 ++++++ .../RequiredStringValidatorTests.cs | 2 +- .../Validators/RequiredValidatorTests.cs | 2 +- .../Validators/UniqueValidatorTests.cs | 64 ++--- .../Contents/ContentDomainObjectTests.cs | 9 +- 35 files changed, 975 insertions(+), 728 deletions(-) create mode 100644 backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs create mode 100644 backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs create mode 100644 backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorPlugin.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs rename backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/{FieldValueValidatorsFactory.cs => DefaultFieldValueValidatorsFactory.cs} (88%) rename backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/{Extensions.cs => DefaultValidatorsFactory.cs} (50%) rename backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/{Validators => }/IValidator.cs (91%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs diff --git a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs new file mode 100644 index 000000000..ddcf12fe2 --- /dev/null +++ b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs @@ -0,0 +1,107 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Extensions.Validation +{ + internal sealed class CompositeUniqueValidator : IValidator + { + private readonly string tag; + private readonly IContentRepository contentRepository; + + public CompositeUniqueValidator(string tag, IContentRepository contentRepository) + { + this.tag = tag; + + this.contentRepository = contentRepository; + } + + public async Task ValidateAsync(object value, ValidationContext context, AddError addError) + { + if (value is NamedContentData data) + { + var validateableFields = context.Schema.Fields.Where(IsValidateableField); + + var filters = new List>(); + + foreach (var field in validateableFields) + { + var fieldValue = TryGetValue(field, data); + + if (fieldValue != null) + { + filters.Add(ClrFilter.Eq($"data.{field.Name}.iv", fieldValue)); + } + } + + if (filters.Count > 0) + { + var filter = ClrFilter.And(filters); + + var found = await contentRepository.QueryIdsAsync(context.AppId.Id, context.SchemaId.Id, filter); + + if (found.Any(x => x.Id != context.ContentId)) + { + addError(Enumerable.Empty(), "A content with the same values already exist."); + } + } + } + } + + private static ClrValue? TryGetValue(IRootField field, NamedContentData data) + { + var value = JsonValue.Null; + + if (data.TryGetValue(field.Name, out var fieldValue)) + { + if (fieldValue.TryGetValue(InvariantPartitioning.Key, out var temp) && temp != null) + { + value = temp; + } + } + + switch (field.RawProperties) + { + case BooleanFieldProperties _ when value is JsonBoolean boolean: + return boolean.Value; + case BooleanFieldProperties _ when value is JsonNull _: + return ClrValue.Null; + case NumberFieldProperties _ when value is JsonNumber number: + return number.Value; + case NumberFieldProperties _ when value is JsonNull _: + return ClrValue.Null; + case StringFieldProperties _ when value is JsonString @string: + return @string.Value; + case StringFieldProperties _ when value is JsonNull _: + return ClrValue.Null; + case ReferencesFieldProperties _ when value is JsonArray array && array.FirstOrDefault() is JsonString @string: + return @string.Value; + } + + return null; + } + + private bool IsValidateableField(IRootField field) + { + return field.Partitioning == Partitioning.Invariant && field.RawProperties.Tags.Contains(tag) && + (field.RawProperties is BooleanFieldProperties || + field.RawProperties is NumberFieldProperties || + field.RawProperties is ReferencesFieldProperties || + field.RawProperties is StringFieldProperties); + } + } +} diff --git a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs new file mode 100644 index 000000000..b7a47d05d --- /dev/null +++ b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs @@ -0,0 +1,47 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Infrastructure; + +namespace Squidex.Extensions.Validation +{ + public sealed class CompositeUniqueValidatorFactory : IValidatorsFactory + { + private const string Prefix = "unique:"; + private readonly IContentRepository contentRepository; + + public CompositeUniqueValidatorFactory(IContentRepository contentRepository) + { + Guard.NotNull(contentRepository); + + this.contentRepository = contentRepository; + } + + public IEnumerable CreateContentValidators(ValidationContext context, FieldValidatorFactory createFieldValidator) + { + foreach (var validatorTag in ValidatorTags(context.Schema.Properties.Tags)) + { + yield return new CompositeUniqueValidator(validatorTag, contentRepository); + } + } + + private static IEnumerable ValidatorTags(IEnumerable tags) + { + foreach (var tag in tags) + { + if (tag.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase) && tag.Length > Prefix.Length) + { + yield return tag; + } + } + } + } +} diff --git a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorPlugin.cs b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorPlugin.cs new file mode 100644 index 000000000..86af7cd17 --- /dev/null +++ b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorPlugin.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Infrastructure.Plugins; + +namespace Squidex.Extensions.Validation +{ + public sealed class CompositeUniqueValidatorPlugin : IPlugin + { + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + services.AddSingletonAs() + .As(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs deleted file mode 100644 index b3a86522f..000000000 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// 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 Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Core.ValidateContent -{ - public static class ContentValidationExtensions - { - public static async Task ValidateAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, IList errors) - { - var validator = new ContentValidator(schema, partitionResolver, context); - - await validator.ValidateAsync(data); - - foreach (var error in validator.Errors) - { - errors.Add(error); - } - } - - public static async Task ValidateAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, Func message) - { - var validator = new ContentValidator(schema, partitionResolver, context); - - await validator.ValidateAsync(data); - - if (validator.Errors.Count > 0) - { - throw new ValidationException(message(), validator.Errors.ToList()); - } - } - - public static async Task ValidatePartialAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, IList errors) - { - var validator = new ContentValidator(schema, partitionResolver, context); - - await validator.ValidatePartialAsync(data); - - foreach (var error in validator.Errors) - { - errors.Add(error); - } - } - - public static async Task ValidatePartialAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, Func message) - { - var validator = new ContentValidator(schema, partitionResolver, context); - - await validator.ValidatePartialAsync(data); - - if (validator.Errors.Count > 0) - { - throw new ValidationException(message(), validator.Errors.ToList()); - } - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs index da12c1324..a3c801460 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs @@ -22,9 +22,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { public sealed class ContentValidator { - private readonly Schema schema; private readonly PartitionResolver partitionResolver; private readonly ValidationContext context; + private readonly IEnumerable factories; private readonly ConcurrentBag errors = new ConcurrentBag(); public IReadOnlyCollection Errors @@ -32,14 +32,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent get { return errors; } } - public ContentValidator(Schema schema, PartitionResolver partitionResolver, ValidationContext context) + public ContentValidator(PartitionResolver partitionResolver, ValidationContext context, IEnumerable factories) { - Guard.NotNull(schema); Guard.NotNull(context); + Guard.NotNull(factories); Guard.NotNull(partitionResolver); - this.schema = schema; this.context = context; + this.factories = factories; this.partitionResolver = partitionResolver; } @@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent errors.Add(new ValidationError(message, pathString)); } - public Task ValidatePartialAsync(NamedContentData data) + public Task ValidateInputPartialAsync(NamedContentData data) { Guard.NotNull(data); @@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent return validator.ValidateAsync(data, context, AddError); } - public Task ValidateAsync(NamedContentData data) + public Task ValidateInputAsync(NamedContentData data) { Guard.NotNull(data); @@ -68,11 +68,20 @@ namespace Squidex.Domain.Apps.Core.ValidateContent return validator.ValidateAsync(data, context, AddError); } + public Task ValidateContentAsync(NamedContentData data) + { + Guard.NotNull(data); + + var validator = new AggregateValidator(CreateContentValidators()); + + return validator.ValidateAsync(data, context, AddError); + } + private IValidator CreateSchemaValidator(bool isPartial) { - var fieldsValidators = new Dictionary(schema.Fields.Count); + var fieldsValidators = new Dictionary(context.Schema.Fields.Count); - foreach (var field in schema.Fields) + foreach (var field in context.Schema.Fields) { fieldsValidators[field.Name] = (!field.RawProperties.IsRequired, CreateFieldValidator(field, isPartial)); } @@ -84,7 +93,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { var partitioning = partitionResolver(field.Partitioning); - var fieldValidator = field.CreateValidator(); + var fieldValidator = CreateFieldValidator(field); var fieldsValidators = new Dictionary(); foreach (var partitionKey in partitioning.AllKeys) @@ -97,9 +106,29 @@ namespace Squidex.Domain.Apps.Core.ValidateContent var typeName = partitioning.ToString()!; return new AggregateValidator( - field.CreateBagValidator() + CreateFieldValidators(field) .Union(Enumerable.Repeat( new ObjectValidator(fieldsValidators, isPartial, typeName), 1))); } + + private IValidator CreateFieldValidator(IField field) + { + return new FieldValidator(CreateValueValidators(field), field); + } + + private IEnumerable CreateContentValidators() + { + return factories.SelectMany(x => x.CreateContentValidators(context, CreateFieldValidator)); + } + + private IEnumerable CreateValueValidators(IField field) + { + return factories.SelectMany(x => x.CreateValueValidators(context, field, CreateFieldValidator)); + } + + private IEnumerable CreateFieldValidators(IField field) + { + return factories.SelectMany(x => x.CreateFieldValidators(context, field, CreateFieldValidator)); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs similarity index 88% rename from backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs index a5681d300..7e7c7cdc0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs @@ -15,19 +15,22 @@ using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.ValidateContent { - public sealed class FieldValueValidatorsFactory : IFieldVisitor> + internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor> { - private static readonly FieldValueValidatorsFactory Instance = new FieldValueValidatorsFactory(); + private readonly FieldValidatorFactory createFieldValidator; - private FieldValueValidatorsFactory() + private DefaultFieldValueValidatorsFactory(FieldValidatorFactory createFieldValidator) { + this.createFieldValidator = createFieldValidator; } - public static IEnumerable CreateValidators(IField field) + public static IEnumerable CreateValidators(IField field, FieldValidatorFactory createFieldValidator) { Guard.NotNull(field); - return field.Accept(Instance); + var visitor = new DefaultFieldValueValidatorsFactory(createFieldValidator); + + return field.Accept(visitor); } public IEnumerable Visit(IArrayField field) @@ -41,7 +44,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent foreach (var nestedField in field.Fields) { - nestedSchema[nestedField.Name] = (false, nestedField.CreateValidator()); + nestedSchema[nestedField.Name] = (false, createFieldValidator(nestedField)); } yield return new CollectionItemValidator(new ObjectValidator(nestedSchema, false, "field")); @@ -58,8 +61,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { yield return new UniqueValuesValidator(); } - - yield return new AssetsValidator(field.Properties); } public IEnumerable Visit(IField field) @@ -115,11 +116,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { yield return new AllowedValuesValidator(field.Properties.AllowedValues); } - - if (field.Properties.IsUnique) - { - yield return new UniqueValidator(); - } } public IEnumerable Visit(IField field) @@ -133,8 +129,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { yield return new UniqueValuesValidator(); } - - yield return new ReferencesValidator(field.Properties.SchemaIds); } public IEnumerable Visit(IField field) @@ -158,11 +152,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { yield return new AllowedValuesValidator(field.Properties.AllowedValues); } - - if (field.Properties.IsUnique) - { - yield return new UniqueValidator(); - } } public IEnumerable Visit(IField field) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs similarity index 50% rename from backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs index 9da804a22..9542301e5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs @@ -5,27 +5,26 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.ValidateContent.Validators; namespace Squidex.Domain.Apps.Core.ValidateContent { - public static class Extensions + public sealed class DefaultValidatorsFactory : IValidatorsFactory { - public static FieldValidator CreateValidator(this IField field) + public IEnumerable CreateFieldValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator) { - return new FieldValidator(CreateValueValidators(field), field); + if (field is IField) + { + yield return NoValueValidator.Instance; + } } - private static IEnumerable CreateValueValidators(IField field) + public IEnumerable CreateValueValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator) { - return FieldValueValidatorsFactory.CreateValidators(field); - } - - public static IEnumerable CreateBagValidator(this IField field) - { - return FieldBagValidatorsFactory.CreateValidators(field); + return DefaultFieldValueValidatorsFactory.CreateValidators(field, createFieldValidator); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidator.cs similarity index 91% rename from backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidator.cs index fbe2a92f4..8b260fe55 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidator.cs @@ -8,7 +8,7 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent { public delegate void AddError(IEnumerable path, string message); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs new file mode 100644 index 000000000..87d5920ee --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public delegate IValidator FieldValidatorFactory(IField field); + + public interface IValidatorsFactory + { + IEnumerable CreateFieldValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator) + { + yield break; + } + + IEnumerable CreateValueValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator) + { + yield break; + } + + IEnumerable CreateContentValidators(ValidationContext context, FieldValidatorFactory createFieldValidator) + { + yield break; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs index 8d2a2a6b0..1a8c660e7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs @@ -6,85 +6,55 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; namespace Squidex.Domain.Apps.Core.ValidateContent { - public delegate Task> CheckContents(Guid schemaId, FilterNode filter); - - public delegate Task> CheckContentsByIds(HashSet ids); - - public delegate Task> CheckAssets(IEnumerable ids); - public sealed class ValidationContext { - private readonly Guid contentId; - private readonly Guid schemaId; - private readonly CheckContents checkContent; - private readonly CheckContentsByIds checkContentByIds; - private readonly CheckAssets checkAsset; - private readonly ImmutableQueue propertyPath; - - public ImmutableQueue Path - { - get { return propertyPath; } - } + public ImmutableQueue Path { get; } - public Guid ContentId - { - get { return contentId; } - } + public NamedId AppId { get; } - public Guid SchemaId - { - get { return schemaId; } - } + public NamedId SchemaId { get; } + + public Schema Schema { get; } + + public Guid ContentId { get; } public bool IsOptional { get; } public ValidationMode Mode { get; } public ValidationContext( + NamedId appId, + NamedId schemaId, + Schema schema, Guid contentId, - Guid schemaId, - CheckContents checkContent, - CheckContentsByIds checkContentsByIds, - CheckAssets checkAsset, ValidationMode mode = ValidationMode.Default) - : this(contentId, schemaId, checkContent, checkContentsByIds, checkAsset, ImmutableQueue.Empty, false, mode) + : this(appId, schemaId, schema, contentId, ImmutableQueue.Empty, false, mode) { } private ValidationContext( + NamedId appId, + NamedId schemaId, + Schema schema, Guid contentId, - Guid schemaId, - CheckContents checkContent, - CheckContentsByIds checkContentByIds, - CheckAssets checkAsset, - ImmutableQueue propertyPath, + ImmutableQueue path, bool isOptional, ValidationMode mode = ValidationMode.Default) { - Guard.NotNull(checkAsset); - Guard.NotNull(checkContent); - Guard.NotNull(checkContentByIds); - - this.propertyPath = propertyPath; - - this.checkContent = checkContent; - this.checkContentByIds = checkContentByIds; - this.checkAsset = checkAsset; - this.contentId = contentId; - - this.schemaId = schemaId; - + AppId = appId; + ContentId = contentId; + IsOptional = isOptional; Mode = mode; + Path = path; - IsOptional = isOptional; + Schema = schema; + SchemaId = schemaId; } public ValidationContext Optimized(bool isOptimized = true) @@ -96,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent return this; } - return Clone(propertyPath, IsOptional, mode); + return Clone(Path, IsOptional, mode); } public ValidationContext Optional(bool isOptional) @@ -106,38 +76,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent return this; } - return Clone(propertyPath, isOptional, Mode); + return Clone(Path, isOptional, Mode); } public ValidationContext Nested(string property) { - return Clone(propertyPath.Enqueue(property), IsOptional, Mode); + return Clone(Path.Enqueue(property), IsOptional, Mode); } private ValidationContext Clone(ImmutableQueue path, bool isOptional, ValidationMode mode) { - return new ValidationContext( - contentId, - schemaId, - checkContent, - checkContentByIds, - checkAsset, - path, isOptional, mode); - } - - public Task> GetContentIdsAsync(HashSet ids) - { - return checkContentByIds(ids); - } - - public Task> GetContentIdsAsync(Guid schemaId, FilterNode filter) - { - return checkContent(schemaId, filter); - } - - public Task> GetAssetInfosAsync(IEnumerable assetId) - { - return checkAsset(assetId); + return new ValidationContext(AppId, SchemaId, Schema, ContentId, path, isOptional, mode); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs index 46dacdba3..638b7e691 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs @@ -15,13 +15,21 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { + public delegate Task> CheckAssets(IEnumerable ids); + public sealed class AssetsValidator : IValidator { private readonly AssetsFieldProperties properties; + private readonly CheckAssets checkAssets; - public AssetsValidator(AssetsFieldProperties properties) + public AssetsValidator(AssetsFieldProperties properties, CheckAssets checkAssets) { + Guard.NotNull(properties); + Guard.NotNull(checkAssets); + this.properties = properties; + + this.checkAssets = checkAssets; } public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) @@ -33,7 +41,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (value is ICollection assetIds && assetIds.Count > 0) { - var assets = await context.GetAssetInfosAsync(assetIds); + var assets = await checkAssets(assetIds); var index = 0; foreach (var assetId in assetIds) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs index a027d70d4..33f794f88 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs @@ -8,6 +8,7 @@ using System; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { @@ -19,6 +20,8 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators public PatternValidator(string pattern, string? errorMessage = null) { + Guard.NotNullOrEmpty(pattern); + this.errorMessage = errorMessage; regex = new Regex($"^{pattern}$", RegexOptions.None, Timeout); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs index 55ab72669..8606d5586 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs @@ -9,16 +9,24 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { + public delegate Task> CheckContentsByIds(HashSet ids); + public sealed class ReferencesValidator : IValidator { private readonly IEnumerable? schemaIds; + private readonly CheckContentsByIds checkReferences; - public ReferencesValidator(IEnumerable? schemaIds) + public ReferencesValidator(IEnumerable? schemaIds, CheckContentsByIds checkReferences) { + Guard.NotNull(checkReferences); + this.schemaIds = schemaIds; + + this.checkReferences = checkReferences; } public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) @@ -30,7 +38,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (value is ICollection contentIds) { - var foundIds = await context.GetContentIdsAsync(contentIds.ToHashSet()); + var foundIds = await checkReferences(contentIds.ToHashSet()); foreach (var id in contentIds) { diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs index 1d7f1d9ac..e16194f6e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -12,8 +13,17 @@ using Squidex.Infrastructure.Queries; namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { + public delegate Task> CheckUniqueness(FilterNode filter); + public sealed class UniqueValidator : IValidator { + private readonly CheckUniqueness checkUniqueness; + + public UniqueValidator(CheckUniqueness checkUniqueness) + { + this.checkUniqueness = checkUniqueness; + } + public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) { if (context.Mode == ValidationMode.Optimized) @@ -38,7 +48,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (filter != null) { - var found = await context.GetContentIdsAsync(context.SchemaId, filter); + var found = await checkUniqueness(filter); if (found.Any(x => x.Id != context.ContentId)) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs index d28e06bb5..34442d68b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs @@ -10,10 +10,8 @@ using System.Threading.Tasks; using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Guards; -using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Contents; @@ -28,33 +26,17 @@ namespace Squidex.Domain.Apps.Entities.Contents { public class ContentDomainObject : LogSnapshotDomainObject { - private readonly IAppProvider appProvider; - private readonly IAssetRepository assetRepository; - private readonly IContentRepository contentRepository; - private readonly IScriptEngine scriptEngine; private readonly IContentWorkflow contentWorkflow; + private readonly ContentOperationContext context; - public ContentDomainObject( - IStore store, - ISemanticLog log, - IAppProvider appProvider, - IAssetRepository assetRepository, - IScriptEngine scriptEngine, - IContentWorkflow contentWorkflow, - IContentRepository contentRepository) + public ContentDomainObject(IStore store, IContentWorkflow contentWorkflow, ContentOperationContext context, ISemanticLog log) : base(store, log) { - Guard.NotNull(appProvider); - Guard.NotNull(scriptEngine); - Guard.NotNull(assetRepository); + Guard.NotNull(context); Guard.NotNull(contentWorkflow); - Guard.NotNull(contentRepository); - this.appProvider = appProvider; - this.scriptEngine = scriptEngine; - this.assetRepository = assetRepository; this.contentWorkflow = contentWorkflow; - this.contentRepository = contentRepository; + this.context = context; } public override Task ExecuteAsync(IAggregateCommand command) @@ -66,15 +48,20 @@ namespace Squidex.Domain.Apps.Entities.Contents case CreateContent createContent: return CreateReturnAsync(createContent, async c => { - var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, c, () => "Failed to create content."); + await LoadContext(c.AppId, c.SchemaId, c, () => "Failed to create content.", c.OptimizeValidation); - await GuardContent.CanCreate(ctx.Schema, contentWorkflow, c); + await GuardContent.CanCreate(context.Schema, contentWorkflow, c); - var status = await contentWorkflow.GetInitialStatusAsync(ctx.Schema); + var status = await contentWorkflow.GetInitialStatusAsync(context.Schema); + + if (!c.DoNotValidate) + { + await context.ValidateInputAsync(c.Data); + } if (!c.DoNotScript) { - c.Data = await ctx.ExecuteScriptAndTransformAsync(s => s.Create, + c.Data = await context.ExecuteScriptAndTransformAsync(s => s.Create, new ScriptContext { Operation = "Create", @@ -84,16 +71,16 @@ namespace Squidex.Domain.Apps.Entities.Contents }); } - await ctx.GenerateDefaultValuesAsync(c.Data); + await context.GenerateDefaultValuesAsync(c.Data); if (!c.DoNotValidate) { - await ctx.ValidateAsync(c.Data, c.OptimizeValidation); + await context.ValidateContentAsync(c.Data); } if (c.Publish) { - await ctx.ExecuteScriptAsync(s => s.Change, + await context.ExecuteScriptAsync(s => s.Change, new ScriptContext { Operation = "Published", @@ -111,11 +98,11 @@ namespace Squidex.Domain.Apps.Entities.Contents case CreateContentDraft createContentDraft: return UpdateReturnAsync(createContentDraft, async c => { - var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, c, () => "Failed to create draft."); + await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c, () => "Failed to create draft."); - GuardContent.CanCreateDraft(c, ctx.Schema, Snapshot); + GuardContent.CanCreateDraft(c, context.Schema, Snapshot); - var status = await contentWorkflow.GetInitialStatusAsync(ctx.Schema); + var status = await contentWorkflow.GetInitialStatusAsync(context.Schema); CreateDraft(c, status); @@ -125,9 +112,9 @@ namespace Squidex.Domain.Apps.Entities.Contents case DeleteContentDraft deleteContentDraft: return UpdateReturnAsync(deleteContentDraft, async c => { - var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, c, () => "Failed to delete draft."); + await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c, () => "Failed to delete draft."); - GuardContent.CanDeleteDraft(c, ctx.Schema, Snapshot); + GuardContent.CanDeleteDraft(c, context.Schema, Snapshot); DeleteDraft(c); @@ -155,9 +142,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { try { - var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, c, () => "Failed to change content."); + await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c, () => "Failed to change content."); - await GuardContent.CanChangeStatus(ctx.Schema, Snapshot, contentWorkflow, c); + await GuardContent.CanChangeStatus(context.Schema, Snapshot, contentWorkflow, c); if (c.DueTime.HasValue) { @@ -167,7 +154,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var change = GetChange(c); - await ctx.ExecuteScriptAsync(s => s.Change, + await context.ExecuteScriptAsync(s => s.Change, new ScriptContext { Operation = change.ToString(), @@ -197,11 +184,11 @@ namespace Squidex.Domain.Apps.Entities.Contents case DeleteContent deleteContent: return UpdateAsync(deleteContent, async c => { - var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, c, () => "Failed to delete content."); + await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c, () => "Failed to delete content."); - GuardContent.CanDelete(ctx.Schema, c); + GuardContent.CanDelete(context.Schema, c); - await ctx.ExecuteScriptAsync(s => s.Delete, + await context.ExecuteScriptAsync(s => s.Delete, new ScriptContext { Operation = "Delete", @@ -226,18 +213,18 @@ namespace Squidex.Domain.Apps.Entities.Contents if (!currentData!.Equals(newData)) { - var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, command, () => "Failed to update content."); + await LoadContext(Snapshot.AppId, Snapshot.SchemaId, command, () => "Failed to update content."); if (partial) { - await ctx.ValidatePartialAsync(command.Data, false); + await context.ValidateInputPartialAsync(command.Data); } else { - await ctx.ValidateAsync(command.Data, false); + await context.ValidateInputAsync(command.Data); } - newData = await ctx.ExecuteScriptAndTransformAsync(s => s.Update, + newData = await context.ExecuteScriptAndTransformAsync(s => s.Update, new ScriptContext { Operation = "Create", @@ -247,6 +234,8 @@ namespace Squidex.Domain.Apps.Entities.Contents StatusOld = default }); + await context.ValidateContentAsync(newData); + Update(command, newData); } @@ -337,13 +326,9 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - private async Task CreateContext(Guid appId, Guid schemaId, ContentCommand command, Func message) + private Task LoadContext(NamedId appId, NamedId schemaId, ContentCommand command, Func message, bool optimized = false) { - var operationContext = - await ContentOperationContext.CreateAsync(appId, schemaId, command, - appProvider, assetRepository, contentRepository, scriptEngine, message); - - return operationContext; + return context.LoadAsync(appId, schemaId, command, message, optimized); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs index 937bf9e8e..8742515a2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.DefaultValues; @@ -14,86 +15,100 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +#pragma warning disable IDE0016 // Use 'throw' expression namespace Squidex.Domain.Apps.Entities.Contents { public sealed class ContentOperationContext { - private IContentRepository contentRepository; - private IAssetRepository assetRepository; - private IScriptEngine scriptEngine; - private ISchemaEntity schemaEntity; - private IAppEntity appEntity; + private readonly IScriptEngine scriptEngine; + private readonly IAppProvider appProvider; + private readonly IEnumerable factories; + private ISchemaEntity schema; + private IAppEntity app; private ContentCommand command; - private Guid schemaId; + private ValidationContext validationContext; private Func message; + public ContentOperationContext(IAppProvider appProvider, IEnumerable factories, IScriptEngine scriptEngine) + { + this.appProvider = appProvider; + this.factories = factories; + this.scriptEngine = scriptEngine; + } + public ISchemaEntity Schema { - get { return schemaEntity; } + get { return schema; } } - public static async Task CreateAsync( - Guid appId, - Guid schemaId, - ContentCommand command, - IAppProvider appProvider, - IAssetRepository assetRepository, - IContentRepository contentRepository, - IScriptEngine scriptEngine, - Func message) + public async Task LoadAsync(NamedId appId, NamedId schemaId, ContentCommand command, Func message, bool optimized) { - var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(appId, schemaId); + var (app, schema) = await appProvider.GetAppWithSchemaAsync(appId.Id, schemaId.Id); - if (appEntity == null) + if (app == null) { throw new InvalidOperationException("Cannot resolve app."); } - if (schemaEntity == null) + if (schema == null) { throw new InvalidOperationException("Cannot resolve schema."); } - var context = new ContentOperationContext - { - appEntity = appEntity, - assetRepository = assetRepository, - command = command, - contentRepository = contentRepository, - message = message, - schemaId = schemaId, - schemaEntity = schemaEntity, - scriptEngine = scriptEngine - }; - - return context; + this.app = app; + this.schema = schema; + this.command = command; + this.message = message; + + validationContext = new ValidationContext(appId, schemaId, schema.SchemaDef, command.ContentId).Optimized(optimized); } public Task GenerateDefaultValuesAsync(NamedContentData data) { - data.GenerateDefaultValues(schemaEntity.SchemaDef, appEntity.PartitionResolver()); + data.GenerateDefaultValues(schema.SchemaDef, app.PartitionResolver()); return Task.CompletedTask; } - public Task ValidateAsync(NamedContentData data, bool optimized) + public async Task ValidateInputAsync(NamedContentData data) { - var ctx = CreateValidationContext(optimized); + var validator = new ContentValidator(app.PartitionResolver(), validationContext, factories); - return data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message); + await validator.ValidateInputAsync(data); + + CheckErrors(validator); } - public Task ValidatePartialAsync(NamedContentData data, bool optimized) + public async Task ValidateInputPartialAsync(NamedContentData data) { - var ctx = CreateValidationContext(optimized); + var validator = new ContentValidator(app.PartitionResolver(), validationContext, factories); + + await validator.ValidateInputPartialAsync(data); + + CheckErrors(validator); + } + + public async Task ValidateContentAsync(NamedContentData data) + { + var validator = new ContentValidator(app.PartitionResolver(), validationContext, factories); + + await validator.ValidateContentAsync(data); + + CheckErrors(validator); + } - return data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message); + private void CheckErrors(ContentValidator validator) + { + if (validator.Errors.Count > 0) + { + throw new ValidationException(message(), validator.Errors.ToList()); + } } public async Task ExecuteScriptAndTransformAsync(Func script, ScriptContext context) @@ -127,38 +142,14 @@ namespace Squidex.Domain.Apps.Entities.Contents private void Enrich(ScriptContext context) { context.ContentId = command.ContentId; - context.AppId = appEntity.Id; - context.AppName = appEntity.Name; + context.AppId = app.Id; + context.AppName = app.Name; context.User = command.User; } - private ValidationContext CreateValidationContext(bool optimized) - { - return new ValidationContext(command.ContentId, schemaId, - QueryContentsAsync, - QueryContentsAsync, - QueryAssetsAsync) - .Optimized(optimized); - } - - private async Task> QueryAssetsAsync(IEnumerable assetIds) - { - return await assetRepository.QueryAsync(appEntity.Id, new HashSet(assetIds)); - } - - private async Task> QueryContentsAsync(Guid filterSchemaId, FilterNode filterNode) - { - return await contentRepository.QueryIdsAsync(appEntity.Id, filterSchemaId, filterNode); - } - - private async Task> QueryContentsAsync(HashSet ids) - { - return await contentRepository.QueryIdsAsync(appEntity.Id, ids, SearchScope.All); - } - private string GetScript(Func script) { - return script(schemaEntity.SchemaDef.Scripts); + return script(schema.SchemaDef.Scripts); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs new file mode 100644 index 000000000..c0e6a93cc --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs @@ -0,0 +1,76 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.Validation +{ + public sealed class DependencyValidatorsFactory : IValidatorsFactory + { + private readonly IAssetRepository assetRepository; + private readonly IContentRepository contentRepository; + + public DependencyValidatorsFactory(IAssetRepository assetRepository, IContentRepository contentRepository) + { + Guard.NotNull(assetRepository); + Guard.NotNull(contentRepository); + + this.assetRepository = assetRepository; + this.contentRepository = contentRepository; + } + + public IEnumerable CreateValueValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator) + { + if (field is IField assetsField) + { + var checkAssets = new CheckAssets(async ids => + { + return await assetRepository.QueryAsync(context.AppId.Id, new HashSet(ids)); + }); + + yield return new AssetsValidator(assetsField.Properties, checkAssets); + } + + if (field is IField referencesField) + { + var checkReferences = new CheckContentsByIds(async ids => + { + return await contentRepository.QueryIdsAsync(context.AppId.Id, ids, SearchScope.All); + }); + + yield return new ReferencesValidator(referencesField.Properties.SchemaIds, checkReferences); + } + + if (field is IField numberField && numberField.Properties.IsUnique) + { + var checkUniqueness = new CheckUniqueness(async filter => + { + return await contentRepository.QueryIdsAsync(context.AppId.Id, context.SchemaId.Id, filter); + }); + + yield return new UniqueValidator(checkUniqueness); + } + + if (field is IField stringField && stringField.Properties.IsUnique) + { + var checkUniqueness = new CheckUniqueness(async filter => + { + return await contentRepository.QueryIdsAsync(context.AppId.Id, context.SchemaId.Id, filter); + }); + + yield return new UniqueValidator(checkUniqueness); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs b/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs index c784969bd..0a6a13b7d 100644 --- a/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs +++ b/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; + namespace Squidex.Infrastructure.Queries { public static class ClrFilter @@ -14,11 +16,21 @@ namespace Squidex.Infrastructure.Queries return new LogicalFilter(LogicalFilterType.And, filters); } + public static LogicalFilter And(IReadOnlyList> filters) + { + return new LogicalFilter(LogicalFilterType.And, filters); + } + public static LogicalFilter Or(params FilterNode[] filters) { return new LogicalFilter(LogicalFilterType.Or, filters); } + public static LogicalFilter Or(IReadOnlyList> filters) + { + return new LogicalFilter(LogicalFilterType.Or, filters); + } + public static NegateFilter Not(FilterNode filter) { return new NegateFilter(filter); diff --git a/backend/src/Squidex/Config/Domain/ContentsServices.cs b/backend/src/Squidex/Config/Domain/ContentsServices.cs index 0bcc713a1..f3498fb66 100644 --- a/backend/src/Squidex/Config/Domain/ContentsServices.cs +++ b/backend/src/Squidex/Config/Domain/ContentsServices.cs @@ -8,11 +8,13 @@ using System; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Queries; using Squidex.Domain.Apps.Entities.Contents.Queries.Steps; using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.Contents.Text.Lucene; +using Squidex.Domain.Apps.Entities.Contents.Validation; using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.Search; using Squidex.Infrastructure.EventSourcing; @@ -36,6 +38,15 @@ namespace Squidex.Config.Domain services.AddTransientAs() .AsSelf(); + services.AddTransientAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs index 475e4604e..68b2237c9 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs @@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ArrayFieldProperties()); - await sut.ValidateAsync(CreateValue(JsonValue.Object()), errors, ValidationTestExtensions.ValidContext); + await sut.ValidateAsync(CreateValue(JsonValue.Object()), errors); Assert.Empty(errors); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs index 4d90706de..f289a8b7e 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs @@ -23,68 +23,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { private readonly List errors = new List(); - public sealed class AssetInfo : IAssetInfo - { - public Guid AssetId { get; set; } - - public string FileName { get; set; } - - public string FileHash { get; set; } - - public string Slug { get; set; } - - public long FileSize { get; set; } - - public bool IsImage { get; set; } - - public int? PixelWidth { get; set; } - - public int? PixelHeight { get; set; } - - public AssetMetadata Metadata { get; set; } - - public AssetType Type { get; set; } - } - - private readonly AssetInfo document = new AssetInfo - { - AssetId = Guid.NewGuid(), - FileName = "MyDocument.pdf", - FileSize = 1024 * 4, - Type = AssetType.Unknown - }; - - private readonly AssetInfo image1 = new AssetInfo - { - AssetId = Guid.NewGuid(), - FileName = "MyImage.png", - FileSize = 1024 * 8, - Type = AssetType.Image, - Metadata = - new AssetMetadata() - .SetPixelWidth(800) - .SetPixelHeight(600) - }; - - private readonly AssetInfo image2 = new AssetInfo - { - AssetId = Guid.NewGuid(), - FileName = "MyImage.png", - FileSize = 1024 * 8, - Type = AssetType.Image, - Metadata = - new AssetMetadata() - .SetPixelWidth(800) - .SetPixelHeight(600) - }; - - private readonly ValidationContext ctx; - - public AssetsFieldTests() - { - ctx = ValidationTestExtensions.Assets(image1, image2, document); - } - [Fact] public void Should_instantiate_field() { @@ -93,22 +31,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent Assert.Equal("my-assets", sut.Name); } - [Fact] - public async Task Should_not_add_error_if_assets_are_valid() - { - var sut = Field(new AssetsFieldProperties()); - - await sut.ValidateAsync(CreateValue(document.AssetId), errors, ctx); - - Assert.Empty(errors); - } - [Fact] public async Task Should_not_add_error_if_assets_are_null_and_valid() { var sut = Field(new AssetsFieldProperties()); - await sut.ValidateAsync(CreateValue(null), errors, ctx); + await sut.ValidateAsync(CreateValue(null), errors); Assert.Empty(errors); } @@ -118,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new AssetsFieldProperties { MinItems = 2, MaxItems = 2 }); - await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx); + await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors); Assert.Empty(errors); } @@ -128,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new AssetsFieldProperties { AllowDuplicates = true }); - await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors, ctx); + await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors); Assert.Empty(errors); } @@ -138,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new AssetsFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(null), errors, ctx); + await sut.ValidateAsync(CreateValue(null), errors); errors.Should().BeEquivalentTo( new[] { "Field is required." }); @@ -149,7 +77,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new AssetsFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(), errors, ctx); + await sut.ValidateAsync(CreateValue(), errors); errors.Should().BeEquivalentTo( new[] { "Field is required." }); @@ -171,7 +99,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new AssetsFieldProperties { MinItems = 3 }); - await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx); + await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors); errors.Should().BeEquivalentTo( new[] { "Must have at least 3 item(s)." }); @@ -182,151 +110,25 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new AssetsFieldProperties { MaxItems = 1 }); - await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx); + await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors); errors.Should().BeEquivalentTo( new[] { "Must not have more than 1 item(s)." }); } [Fact] - public async Task Should_add_error_if_asset_are_not_valid() - { - var assetId = Guid.NewGuid(); - - var sut = Field(new AssetsFieldProperties()); - - await sut.ValidateAsync(CreateValue(assetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { $"[1]: Id '{assetId}' not found." }); - } - - [Fact] - public async Task Should_not_add_error_if_asset_are_not_valid_but_in_optimized_mode() + public async Task Should_add_error_if_values_contains_duplicate() { - var assetId = Guid.NewGuid(); - var sut = Field(new AssetsFieldProperties()); - await sut.ValidateAsync(CreateValue(assetId), errors, ctx.Optimized()); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_document_is_too_small() - { - var sut = Field(new AssetsFieldProperties { MinSize = 5 * 1024 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[1]: \'4 kB\' less than minimum of \'5 kB\'." }); - } - - [Fact] - public async Task Should_add_error_if_document_is_too_big() - { - var sut = Field(new AssetsFieldProperties { MaxSize = 5 * 1024 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: \'8 kB\' greater than maximum of \'5 kB\'." }); - } - - [Fact] - public async Task Should_add_error_if_document_is_not_an_image() - { - var sut = Field(new AssetsFieldProperties { MustBeImage = true }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[1]: Not an image." }); - } - - [Fact] - public async Task Should_add_error_if_values_contains_duplicate() - { - var sut = Field(new AssetsFieldProperties { MustBeImage = true }); + var id = Guid.NewGuid(); - await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors, ctx); + await sut.ValidateAsync(CreateValue(id, id), errors); errors.Should().BeEquivalentTo( new[] { "Must not contain duplicate values." }); } - [Fact] - public async Task Should_add_error_if_image_width_is_too_small() - { - var sut = Field(new AssetsFieldProperties { MinWidth = 1000 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: Width \'800px\' less than minimum of \'1000px\'." }); - } - - [Fact] - public async Task Should_add_error_if_image_width_is_too_big() - { - var sut = Field(new AssetsFieldProperties { MaxWidth = 700 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: Width \'800px\' greater than maximum of \'700px\'." }); - } - - [Fact] - public async Task Should_add_error_if_image_height_is_too_small() - { - var sut = Field(new AssetsFieldProperties { MinHeight = 800 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: Height \'600px\' less than minimum of \'800px\'." }); - } - - [Fact] - public async Task Should_add_error_if_image_height_is_too_big() - { - var sut = Field(new AssetsFieldProperties { MaxHeight = 500 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: Height \'600px\' greater than maximum of \'500px\'." }); - } - - [Fact] - public async Task Should_add_error_if_image_has_invalid_aspect_ratio() - { - var sut = Field(new AssetsFieldProperties { AspectWidth = 1, AspectHeight = 1 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: Aspect ratio not '1:1'." }); - } - - [Fact] - public async Task Should_add_error_if_image_has_invalid_extension() - { - var sut = Field(new AssetsFieldProperties { AllowedExtensions = ReadOnlyCollection.Create("mp4") }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] - { - "[1]: Invalid file extension.", - "[2]: Invalid file extension." - }); - } - private static IJsonValue CreateValue(params Guid[]? ids) { return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs index 7e7620a57..08bcd7961 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs @@ -11,7 +11,6 @@ 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.ValidateContent; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Validation; @@ -23,7 +22,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE); private readonly List errors = new List(); - private readonly ValidationContext context = ValidationTestExtensions.ValidContext; private Schema schema = new Schema("my-schema"); [Fact] @@ -34,7 +32,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddField("unknown", new ContentFieldData()); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema); errors.Should().BeEquivalentTo( new List @@ -55,7 +53,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new ContentFieldData() .AddValue("iv", 1000)); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema); errors.Should().BeEquivalentTo( new List @@ -76,7 +74,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddValue("es", 1) .AddValue("it", 1)); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema); errors.Should().BeEquivalentTo( new List @@ -95,7 +93,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent var data = new NamedContentData(); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema); errors.Should().BeEquivalentTo( new List @@ -114,7 +112,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent var data = new NamedContentData(); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema); errors.Should().BeEquivalentTo( new List @@ -132,7 +130,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent var data = new NamedContentData(); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema); errors.Should().BeEquivalentTo( new List @@ -153,7 +151,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddValue("de", 1) .AddValue("xx", 1)); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema); errors.Should().BeEquivalentTo( new List @@ -180,7 +178,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new ContentFieldData() .AddValue("es", "value")); - await data.ValidateAsync(context, schema, optionalConfig.ToResolver(), errors); + await data.ValidateAsync(optionalConfig.ToResolver(), errors, schema); Assert.Empty(errors); } @@ -197,7 +195,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddValue("es", 1) .AddValue("it", 1)); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema); errors.Should().BeEquivalentTo( new List @@ -215,7 +213,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddField("unknown", new ContentFieldData()); - await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema); errors.Should().BeEquivalentTo( new List @@ -236,7 +234,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new ContentFieldData() .AddValue("iv", 1000)); - await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema); errors.Should().BeEquivalentTo( new List @@ -257,7 +255,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddValue("es", 1) .AddValue("it", 1)); - await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema); errors.Should().BeEquivalentTo( new List @@ -276,7 +274,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent var data = new NamedContentData(); - await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema); Assert.Empty(errors); } @@ -290,7 +288,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent var data = new NamedContentData(); - await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema); Assert.Empty(errors); } @@ -307,7 +305,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddValue("de", 1) .AddValue("xx", 1)); - await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema); errors.Should().BeEquivalentTo( new List @@ -328,7 +326,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddValue("es", 1) .AddValue("it", 1)); - await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema); errors.Should().BeEquivalentTo( new List @@ -354,7 +352,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent JsonValue.Object().Add("my-nested", 1), JsonValue.Object()))); - await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema); errors.Should().BeEquivalentTo( new List @@ -372,7 +370,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent var data = new NamedContentData(); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema); Assert.Empty(errors); } @@ -391,7 +389,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent JsonValue.Array( JsonValue.Object()))); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema); Assert.Empty(errors); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs index 26a069b4c..10f7c7e28 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; @@ -93,17 +92,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new[] { "Invalid json type, expected number." }); } - [Fact] - public async Task Should_add_error_if_unique_constraint_failed() - { - var sut = Field(new NumberFieldProperties { IsUnique = true }); - - await sut.ValidateAsync(CreateValue(12.5), errors, ValidationTestExtensions.References((Guid.NewGuid(), Guid.NewGuid()))); - - errors.Should().BeEquivalentTo( - new[] { "Another content with the same value exists." }); - } - private static IJsonValue CreateValue(double v) { return JsonValue.Create(v); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs index 3110a8bbc..7bafbcead 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs @@ -11,7 +11,6 @@ using System.Linq; using System.Threading.Tasks; using FluentAssertions; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Infrastructure.Json.Objects; using Xunit; @@ -37,7 +36,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties()); - await sut.ValidateAsync(CreateValue(ref1), errors, Context()); + await sut.ValidateAsync(CreateValue(ref1), errors); Assert.Empty(errors); } @@ -47,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties()); - await sut.ValidateAsync(CreateValue(null), errors, Context()); + await sut.ValidateAsync(CreateValue(null), errors); Assert.Empty(errors); } @@ -57,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2 }); - await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); + await sut.ValidateAsync(CreateValue(ref1, ref2), errors); Assert.Empty(errors); } @@ -67,17 +66,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2, AllowDuplicates = true }); - await sut.ValidateAsync(CreateValue(ref1, ref1), errors, Context()); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_schemas_not_defined() - { - var sut = Field(new ReferencesFieldProperties()); - - await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References((Guid.NewGuid(), ref1))); + await sut.ValidateAsync(CreateValue(ref1, ref1), errors); Assert.Empty(errors); } @@ -87,7 +76,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); - await sut.ValidateAsync(CreateValue(null), errors, Context()); + await sut.ValidateAsync(CreateValue(null), errors); errors.Should().BeEquivalentTo( new[] { "Field is required." }); @@ -98,7 +87,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); - await sut.ValidateAsync(CreateValue(), errors, Context()); + await sut.ValidateAsync(CreateValue(), errors); errors.Should().BeEquivalentTo( new[] { "Field is required." }); @@ -109,7 +98,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties()); - await sut.ValidateAsync(JsonValue.Create("invalid"), errors, Context()); + await sut.ValidateAsync(JsonValue.Create("invalid"), errors); errors.Should().BeEquivalentTo( new[] { "Invalid json type, expected array of guid strings." }); @@ -120,7 +109,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MinItems = 3 }); - await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); + await sut.ValidateAsync(CreateValue(ref1, ref2), errors); errors.Should().BeEquivalentTo( new[] { "Must have at least 3 item(s)." }); @@ -131,52 +120,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MaxItems = 1 }); - await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); + await sut.ValidateAsync(CreateValue(ref1, ref2), errors); errors.Should().BeEquivalentTo( new[] { "Must not have more than 1 item(s)." }); } - [Fact] - public async Task Should_add_error_if_reference_are_not_valid() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); - - await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References()); - - errors.Should().BeEquivalentTo( - new[] { $"Contains invalid reference '{ref1}'." }); - } - - [Fact] - public async Task Should_not_add_error_if_reference_are_not_valid_but_in_optimized_mode() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); - - await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References().Optimized()); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_reference_schema_is_not_valid() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); - - await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References((Guid.NewGuid(), ref1))); - - errors.Should().BeEquivalentTo( - new[] { $"Contains reference '{ref1}' to invalid schema." }); - } - [Fact] public async Task Should_add_error_if_reference_contains_duplicate_values() { var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); - await sut.ValidateAsync(CreateValue(ref1, ref1), errors, - ValidationTestExtensions.References( - (schemaId, ref1))); + await sut.ValidateAsync(CreateValue(ref1, ref1), errors); errors.Should().BeEquivalentTo( new[] { "Must not contain duplicate values." }); @@ -187,13 +142,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); } - private ValidationContext Context() - { - return ValidationTestExtensions.References( - (schemaId, ref1), - (schemaId, ref2)); - } - private static RootField Field(ReferencesFieldProperties properties) { return Fields.References(1, "my-refs", Partitioning.Invariant, properties); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs index d05115cb8..987473646 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; @@ -115,17 +114,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new[] { "Custom Error Message." }); } - [Fact] - public async Task Should_add_error_if_unique_constraint_failed() - { - var sut = Field(new StringFieldProperties { IsUnique = true }); - - await sut.ValidateAsync(CreateValue("abc"), errors, ValidationTestExtensions.References((Guid.NewGuid(), Guid.NewGuid()))); - - errors.Should().BeEquivalentTo( - new[] { "Another content with the same value exists." }); - } - private static IJsonValue CreateValue(string? v) { return JsonValue.Create(v); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs index d41bd69df..d05a2d120 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs @@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new TagsFieldProperties()); - await sut.ValidateAsync(CreateValue("tag"), errors, ValidationTestExtensions.ValidContext); + await sut.ValidateAsync(CreateValue("tag"), errors); Assert.Empty(errors); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs index 375d9813c..557fc0ae4 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; @@ -35,7 +34,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new UIFieldProperties()); - await sut.ValidateAsync(Undefined.Value, errors, ValidationTestExtensions.ValidContext); + await sut.ValidateAsync(Undefined.Value, errors); Assert.Empty(errors); } @@ -76,12 +75,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddField("my-ui2", new ContentFieldData() .AddValue("iv", null)); - var validationContext = ValidationTestExtensions.ValidContext; - var validator = new ContentValidator(schema, x => InvariantPartitioning.Instance, validationContext); + var dataErrors = new List(); - await validator.ValidateAsync(data); + await data.ValidateAsync(x => InvariantPartitioning.Instance, dataErrors, schema); - validator.Errors.Should().BeEquivalentTo( + dataErrors.Should().BeEquivalentTo( new[] { new ValidationError("Value must not be defined.", "my-ui1"), @@ -105,20 +103,15 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent JsonValue.Object() .Add("my-ui", null)))); - var validationContext = - new ValidationContext( - Guid.NewGuid(), - Guid.NewGuid(), - (c, s) => null!, - (s) => null!, - (c) => null!); + var dataErrors = new List(); - var validator = new ContentValidator(schema, x => InvariantPartitioning.Instance, validationContext); + await data.ValidateAsync(x => InvariantPartitioning.Instance, dataErrors, schema); - await validator.ValidateAsync(data); - - validator.Errors.Should().BeEquivalentTo( - new[] { new ValidationError("Value must not be defined.", "my-array[1].my-ui") }); + dataErrors.Should().BeEquivalentTo( + new[] + { + new ValidationError("Value must not be defined.", "my-array[1].my-ui") + }); } private static NestedField Field(UIFieldProperties properties) diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs index ecd4a74dd..88a79d9ca 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs @@ -9,47 +9,71 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +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.Validation; namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public static class ValidationTestExtensions { - private static readonly Task> EmptyReferences = Task.FromResult>(new List<(Guid SchemaId, Guid Id)>()); - private static readonly Task> EmptyAssets = Task.FromResult>(new List()); + private static readonly NamedId AppId = NamedId.Of(Guid.NewGuid(), "my-app"); + private static readonly NamedId SchemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private static readonly IValidatorsFactory Factory = new DefaultValidatorsFactory(); - public static readonly ValidationContext ValidContext = new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), - (x, y) => EmptyReferences, - (x) => EmptyReferences, - (x) => EmptyAssets); + public static Task ValidateAsync(this IValidator validator, object? value, IList errors, + Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func? updater = null) + { + var context = CreateContext(schema, mode, updater); + + return validator.ValidateAsync(value, context, CreateFormatter(errors)); + } - public static Task ValidateAsync(this IValidator validator, object? value, IList errors, ValidationContext? context = null) + public static Task ValidateAsync(this IField field, object? value, IList errors, + Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func? updater = null) { - return validator.ValidateAsync(value, - CreateContext(context), - CreateFormatter(errors)); + var context = CreateContext(schema, mode, updater); + + var validators = Factory.CreateValueValidators(context, field, null!); + + return new FieldValidator(validators.ToArray(), field) + .ValidateAsync(value, context, CreateFormatter(errors)); } - public static Task ValidateOptionalAsync(this IValidator validator, object? value, IList errors, ValidationContext? context = null) + public static async Task ValidatePartialAsync(this NamedContentData data, PartitionResolver partitionResolver, IList errors, + Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func? updater = null) { - return validator.ValidateAsync( - value, - CreateContext(context).Optional(true), - CreateFormatter(errors)); + var context = CreateContext(schema, mode, updater); + + var validator = new ContentValidator(partitionResolver, context, Enumerable.Repeat(Factory, 1)); + + await validator.ValidateInputPartialAsync(data); + + foreach (var error in validator.Errors) + { + errors.Add(error); + } } - public static Task ValidateAsync(this IField field, object? value, IList errors, ValidationContext? context = null) + public static async Task ValidateAsync(this NamedContentData data, PartitionResolver partitionResolver, IList errors, + Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func? updater = null) { - return new FieldValidator(FieldValueValidatorsFactory.CreateValidators(field).ToArray(), field) - .ValidateAsync( - value, - CreateContext(context), - CreateFormatter(errors)); + var context = CreateContext(schema, mode, updater); + + var validator = new ContentValidator(partitionResolver, context, Enumerable.Repeat(Factory, 1)); + + await validator.ValidateInputAsync(data); + + foreach (var error in validator.Errors) + { + errors.Add(error); + } } - private static AddError CreateFormatter(IList errors) + public static AddError CreateFormatter(IList errors) { return (field, message) => { @@ -64,23 +88,21 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent }; } - private static ValidationContext CreateContext(ValidationContext? context) - { - return context ?? ValidContext; - } - - public static ValidationContext Assets(params IAssetInfo[] assets) + public static ValidationContext CreateContext(Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func? updater = null) { - var actual = Task.FromResult>(assets.ToList()); - - return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => EmptyReferences, x => EmptyReferences, x => actual); - } + var context = new ValidationContext( + AppId, + SchemaId, + schema ?? new Schema(SchemaId.Name), + Guid.NewGuid(), + mode); - public static ValidationContext References(params (Guid Id, Guid SchemaId)[] referencesIds) - { - var actual = Task.FromResult>(referencesIds.ToList()); + if (updater != null) + { + context = updater(context); + } - return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => actual, x => actual, x => EmptyAssets); + return context; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs new file mode 100644 index 000000000..3a52c3a38 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs @@ -0,0 +1,233 @@ +// ========================================================================== +// 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 Squidex.Domain.Apps.Core.Assets; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; +using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +{ + public class AssetsValidatorTests + { + private readonly List errors = new List(); + + public sealed class AssetInfo : IAssetInfo + { + public Guid AssetId { get; set; } + + public string FileName { get; set; } + + public string FileHash { get; set; } + + public string Slug { get; set; } + + public long FileSize { get; set; } + + public bool IsImage { get; set; } + + public int? PixelWidth { get; set; } + + public int? PixelHeight { get; set; } + + public AssetMetadata Metadata { get; set; } + + public AssetType Type { get; set; } + } + + private readonly AssetInfo document = new AssetInfo + { + AssetId = Guid.NewGuid(), + FileName = "MyDocument.pdf", + FileSize = 1024 * 4, + Type = AssetType.Unknown + }; + + private readonly AssetInfo image1 = new AssetInfo + { + AssetId = Guid.NewGuid(), + FileName = "MyImage.png", + FileSize = 1024 * 8, + Type = AssetType.Image, + Metadata = + new AssetMetadata() + .SetPixelWidth(800) + .SetPixelHeight(600) + }; + + private readonly AssetInfo image2 = new AssetInfo + { + AssetId = Guid.NewGuid(), + FileName = "MyImage.png", + FileSize = 1024 * 8, + Type = AssetType.Image, + Metadata = + new AssetMetadata() + .SetPixelWidth(800) + .SetPixelHeight(600) + }; + + [Fact] + public async Task Should_not_add_error_if_assets_are_valid() + { + var sut = Validator(new AssetsFieldProperties()); + + await sut.ValidateAsync(CreateValue(document.AssetId), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_asset_are_not_valid() + { + var assetId = Guid.NewGuid(); + + var sut = Validator(new AssetsFieldProperties()); + + await sut.ValidateAsync(CreateValue(assetId), errors); + + errors.Should().BeEquivalentTo( + new[] { $"[1]: Id '{assetId}' not found." }); + } + + [Fact] + public async Task Should_not_add_error_if_asset_are_not_valid_but_in_optimized_mode() + { + var assetId = Guid.NewGuid(); + + var sut = Validator(new AssetsFieldProperties()); + + await sut.ValidateAsync(CreateValue(assetId), errors, updater: c => c.Optimized()); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_document_is_too_small() + { + var sut = Validator(new AssetsFieldProperties { MinSize = 5 * 1024 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + + errors.Should().BeEquivalentTo( + new[] { "[1]: \'4 kB\' less than minimum of \'5 kB\'." }); + } + + [Fact] + public async Task Should_add_error_if_document_is_too_big() + { + var sut = Validator(new AssetsFieldProperties { MaxSize = 5 * 1024 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + + errors.Should().BeEquivalentTo( + new[] { "[2]: \'8 kB\' greater than maximum of \'5 kB\'." }); + } + + [Fact] + public async Task Should_add_error_if_document_is_not_an_image() + { + var sut = Validator(new AssetsFieldProperties { MustBeImage = true }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + + errors.Should().BeEquivalentTo( + new[] { "[1]: Not an image." }); + } + + [Fact] + public async Task Should_add_error_if_image_width_is_too_small() + { + var sut = Validator(new AssetsFieldProperties { MinWidth = 1000 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + + errors.Should().BeEquivalentTo( + new[] { "[2]: Width \'800px\' less than minimum of \'1000px\'." }); + } + + [Fact] + public async Task Should_add_error_if_image_width_is_too_big() + { + var sut = Validator(new AssetsFieldProperties { MaxWidth = 700 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + + errors.Should().BeEquivalentTo( + new[] { "[2]: Width \'800px\' greater than maximum of \'700px\'." }); + } + + [Fact] + public async Task Should_add_error_if_image_height_is_too_small() + { + var sut = Validator(new AssetsFieldProperties { MinHeight = 800 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + + errors.Should().BeEquivalentTo( + new[] { "[2]: Height \'600px\' less than minimum of \'800px\'." }); + } + + [Fact] + public async Task Should_add_error_if_image_height_is_too_big() + { + var sut = Validator(new AssetsFieldProperties { MaxHeight = 500 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + + errors.Should().BeEquivalentTo( + new[] { "[2]: Height \'600px\' greater than maximum of \'500px\'." }); + } + + [Fact] + public async Task Should_add_error_if_image_has_invalid_aspect_ratio() + { + var sut = Validator(new AssetsFieldProperties { AspectWidth = 1, AspectHeight = 1 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + + errors.Should().BeEquivalentTo( + new[] { "[2]: Aspect ratio not '1:1'." }); + } + + [Fact] + public async Task Should_add_error_if_image_has_invalid_extension() + { + var sut = Validator(new AssetsFieldProperties { AllowedExtensions = ReadOnlyCollection.Create("mp4") }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + + errors.Should().BeEquivalentTo( + new[] + { + "[1]: Invalid file extension.", + "[2]: Invalid file extension." + }); + } + + private static object CreateValue(params Guid[] ids) + { + return ids.ToList(); + } + + private IValidator Validator(AssetsFieldProperties properties) + { + return new AssetsValidator(properties, new CheckAssets(ids => + { + return Task.FromResult>(new List { document, image1, image2 }); + })); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs index 394075fb1..094f01fcb 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs @@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { var sut = new CollectionValidator(true, 1, 3); - await sut.ValidateOptionalAsync(null, errors); + await sut.ValidateAsync(null, errors, updater: c => c.Optional(true)); Assert.Empty(errors); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs new file mode 100644 index 000000000..9434d8057 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// 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 Squidex.Domain.Apps.Core.ValidateContent.Validators; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +{ + public class ReferencesValidatorTests + { + private readonly List errors = new List(); + private readonly Guid schemaId = Guid.NewGuid(); + private readonly Guid ref1 = Guid.NewGuid(); + private readonly Guid ref2 = Guid.NewGuid(); + + [Fact] + public async Task Should_add_error_if_references_are_not_valid() + { + var sut = new ReferencesValidator(Enumerable.Repeat(schemaId, 1), FoundReferences()); + + await sut.ValidateAsync(CreateValue(ref1), errors); + + errors.Should().BeEquivalentTo( + new[] { $"Contains invalid reference '{ref1}'." }); + } + + [Fact] + public async Task Should_not_add_error_if_reference_are_not_valid_but_in_optimized_mode() + { + var sut = new ReferencesValidator(Enumerable.Repeat(schemaId, 1), FoundReferences()); + + await sut.ValidateAsync(CreateValue(ref1), errors, updater: c => c.Optimized()); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_not_defined() + { + var sut = new ReferencesValidator(null, FoundReferences((Guid.NewGuid(), ref2))); + + await sut.ValidateAsync(CreateValue(ref2), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_reference_schema_is_not_valid() + { + var sut = new ReferencesValidator(Enumerable.Repeat(schemaId, 1), FoundReferences((Guid.NewGuid(), ref2))); + + await sut.ValidateAsync(CreateValue(ref2), errors); + + errors.Should().BeEquivalentTo( + new[] { $"Contains reference '{ref2}' to invalid schema." }); + } + + private static List CreateValue(params Guid[] ids) + { + return ids.ToList(); + } + + private static CheckContentsByIds FoundReferences(params (Guid SchemaId, Guid Id)[] references) + { + return new CheckContentsByIds(x => Task.FromResult>(references.ToList())); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs index 9a94dfa87..8e1ed7b42 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs @@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { var sut = new RequiredStringValidator(); - await sut.ValidateOptionalAsync(string.Empty, errors); + await sut.ValidateAsync(string.Empty, errors, updater: c => c.Optional(true)); Assert.Empty(errors); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs index 852485cec..d13cdeb1c 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs @@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { var sut = new RequiredValidator(); - await sut.ValidateOptionalAsync(null, errors); + await sut.ValidateAsync(null, errors, updater: c => c.Optional(true)); Assert.Empty(errors); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs index eedf90485..8372dea9f 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs @@ -9,7 +9,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; -using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; @@ -17,18 +16,17 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public class UniqueValidatorTests { - private readonly List errors = new List(); - private readonly Guid contentId = Guid.NewGuid(); private readonly Guid schemaId = Guid.NewGuid(); + private readonly List errors = new List(); [Fact] public async Task Should_add_error_if_string_value_not_found() { - var sut = new UniqueValidator(); - var filter = string.Empty; - await sut.ValidateAsync("hi", errors, Context(Guid.NewGuid(), f => filter = f, ValidationMode.Default)); + var sut = new UniqueValidator(Check(Guid.NewGuid(), f => filter = f)); + + await sut.ValidateAsync("hi", errors, updater: c => c.Nested("property").Nested("iv")); errors.Should().BeEquivalentTo( new[] { "property: Another content with the same value exists." }); @@ -39,11 +37,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators [Fact] public async Task Should_add_error_if_double_value_not_found() { - var sut = new UniqueValidator(); - var filter = string.Empty; - await sut.ValidateAsync(12.5, errors, Context(Guid.NewGuid(), f => filter = f, ValidationMode.Default)); + var sut = new UniqueValidator(Check(Guid.NewGuid(), f => filter = f)); + + await sut.ValidateAsync(12.5, errors, updater: c => c.Nested("property").Nested("iv")); errors.Should().BeEquivalentTo( new[] { "property: Another content with the same value exists." }); @@ -54,59 +52,45 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators [Fact] public async Task Should_not_add_error_if_string_value_not_found_but_in_optimized_mode() { - var sut = new UniqueValidator(); + var sut = new UniqueValidator(Check(Guid.NewGuid())); - var filter = string.Empty; - - await sut.ValidateAsync("hi", errors, Context(Guid.NewGuid(), f => filter = f, ValidationMode.Optimized)); + await sut.ValidateAsync(null, errors); Assert.Empty(errors); } [Fact] - public async Task Should_not_add_error_if_string_value_found() + public async Task Should_not_add_error_if_string_value_found_with_same_content_id() { - var sut = new UniqueValidator(); + var ctx = ValidationTestExtensions.CreateContext(); - var filter = string.Empty; + var sut = new UniqueValidator(Check(ctx.ContentId)); - await sut.ValidateAsync("hi", errors, Context(contentId, f => filter = f, ValidationMode.Default)); + await sut.ValidateAsync("hi", ctx, ValidationTestExtensions.CreateFormatter(errors)); Assert.Empty(errors); } [Fact] - public async Task Should_not_add_error_if_double_value_found() + public async Task Should_not_add_error_if_double_value_found_with_same_content_id() { - var sut = new UniqueValidator(); + var ctx = ValidationTestExtensions.CreateContext(); - var filter = string.Empty; + var sut = new UniqueValidator(Check(ctx.ContentId)); - await sut.ValidateAsync(12.5, errors, Context(contentId, f => filter = f, ValidationMode.Default)); + await sut.ValidateAsync(12.5, ctx, ValidationTestExtensions.CreateFormatter(errors)); Assert.Empty(errors); } - private ValidationContext Context(Guid id, Action filter, ValidationMode mode) + private CheckUniqueness Check(Guid id, Action? filter = null) { - return new ValidationContext(contentId, schemaId, - (schema, filterNode) => - { - filter(filterNode.ToString()); - - return Task.FromResult>(new List<(Guid, Guid)> { (schemaId, id) }); - }, - (ids) => - { - return Task.FromResult>(new List<(Guid, Guid)> { (schemaId, id) }); - }, - ids => - { - return Task.FromResult>(new List()); - }, - mode) - .Nested("property") - .Nested("iv"); + return new CheckUniqueness(filterNode => + { + filter?.Invoke(filterNode.ToString()); + + return Task.FromResult>(new List<(Guid, Guid)> { (schemaId, id) }); + }); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs index 0af098ed6..fcf025c35 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Linq; using System.Threading.Tasks; using FakeItEasy; using NodaTime; @@ -13,10 +14,9 @@ using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.TestHelpers; @@ -34,7 +34,6 @@ namespace Squidex.Domain.Apps.Entities.Contents private readonly Guid contentId = Guid.NewGuid(); private readonly IAppEntity app; private readonly IAppProvider appProvider = A.Fake(); - private readonly IContentRepository contentRepository = A.Dummy(); private readonly IContentWorkflow contentWorkflow = A.Fake(x => x.Wrapping(new DefaultContentWorkflow())); private readonly ISchemaEntity schema; private readonly IScriptEngine scriptEngine = A.Fake(); @@ -106,7 +105,9 @@ namespace Squidex.Domain.Apps.Entities.Contents patched = patch.MergeInto(data); - sut = new ContentDomainObject(Store, A.Dummy(), appProvider, A.Dummy(), scriptEngine, contentWorkflow, contentRepository); + var context = new ContentOperationContext(appProvider, Enumerable.Repeat(new DefaultValidatorsFactory(), 1), scriptEngine); + + sut = new ContentDomainObject(Store, contentWorkflow, context, A.Dummy()); sut.Setup(Id); }