V15: Add MNTP serverside validation (#18526)
* Add amount validator * Add ObjectTypeValidator to MNTP * Move validate startnode to helper method * Validate allowed type * Fix tests * Added some XML header comments and resolved nit-picky warnings. * Further XML comments. * Fix null validation case --------- Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
@@ -114,13 +114,16 @@ Mange hilsner fra Umbraco robotten
|
||||
<key alias="entriesExceed"><![CDATA[Maksimum %0% element(er), <strong>%1%</strong> for mange.]]></key>
|
||||
<key alias="entriesAreasMismatch">Ét eller flere områder lever ikke op til kravene for antal indholdselementer.</key>
|
||||
<key alias="invalidMediaType">Den valgte medie type er ugyldig.</key>
|
||||
<key alias="invalidContentType">Det valgte indhold er af en ugyldig type.</key>
|
||||
<key alias="missingContent">Det valgte indhold eksistere ikke.</key>
|
||||
<key alias="multipleMediaNotAllowed">Det er kun tilladt at vælge ét medie.</key>
|
||||
<key alias="invalidStartNode">Valgt medie kommer fra en ugyldig mappe.</key>
|
||||
<key alias="invalidStartNode">Valgt indhold kommer fra en ugyldig mappe.</key>
|
||||
<key alias="outOfRangeMinimum">Værdien %0% er mindre end det tilladte minimum af %1%.</key>
|
||||
<key alias="outOfRangeMaximum">Værdien %0% er større end det tilladte maksimum af %1%.</key>
|
||||
<key alias="invalidStep">Værdien %0% passer ikke med den konfigureret trin værdi af %1% og mindste værdi af %2%.</key>
|
||||
<key alias="unexpectedRange">Værdien %0% forventes ikke at indeholde et spænd.</key>
|
||||
<key alias="invalidRange">Værdien %0% forventes at have en værdi der er større end fra værdien.</key>
|
||||
<key alias="invalidObjectType">Det valgte indhold er af den forkerte type.</key>
|
||||
<key alias="notOneOfOptions">"Værdien '%0%' er ikke en af de tilgængelige valgmuligheder.</key>
|
||||
<key alias="invalidColor">"Den valgte farve '%0%' er ikke en af de tilgængelige valgmuligheder.</key>
|
||||
</area>
|
||||
|
||||
@@ -393,10 +393,14 @@
|
||||
<key alias="unexpectedRange">The value %0% is not expected to contain a range</key>
|
||||
<key alias="invalidRange">The value %0% is not expected to have a to value less than the from value</key>
|
||||
<key alias="invalidMediaType">The chosen media type is invalid.</key>
|
||||
<key alias="invalidContentType">The chosen content is of invalid type.</key>
|
||||
<key alias="missingContent">The chosen content does not exist.</key>
|
||||
<key alias="multipleMediaNotAllowed">Multiple selected media is not allowed.</key>
|
||||
<key alias="invalidStartNode">The selected media is from the wrong folder.</key>
|
||||
<key alias="notOneOfOptions">The value '%0%' is not one of the available options.</key>
|
||||
<key alias="invalidColor">"The selected colour '%0%' is not one of the available options.</key>
|
||||
<key alias="invalidStartNode">The selected item is from the wrong folder.</key>
|
||||
<key alias="invalidObjectType">The selected item is of the wrong type.</key>
|
||||
<key alias="notOneOfOptions">"The value '%0%' is not one of the available options.</key>
|
||||
</area>
|
||||
<area alias="healthcheck">
|
||||
<!-- The following keys get these tokens passed in:
|
||||
|
||||
@@ -394,9 +394,12 @@
|
||||
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, <strong>%1%</strong> too many.]]></key>
|
||||
<key alias="entriesAreasMismatch">The content amount requirements are not met for one or more areas.</key>
|
||||
<key alias="invalidMediaType">The chosen media type is invalid.</key>
|
||||
<key alias="invalidContentType">The chosen content is of invalid type.</key>
|
||||
<key alias="missingContent">The chosen content does not exist.</key>
|
||||
<key alias="multipleMediaNotAllowed">Multiple selected media is not allowed.</key>
|
||||
<key alias="invalidStartNode">The selected media is from the wrong folder.</key>
|
||||
<key alias="notOneOfOptions">The value '%0%' is not one of the available options.</key>
|
||||
<key alias="invalidStartNode">The selected item is from the wrong folder.</key>
|
||||
<key alias="invalidObjectType">The selected item is of the wrong type.</key>
|
||||
<key alias="notOneOfOptions">"The value '%0%' is not one of the available options.</key>
|
||||
<key alias="invalidColor">The selected color '%0%' is not one of the available options.</key>
|
||||
</area>
|
||||
<area alias="healthcheck">
|
||||
|
||||
@@ -17,6 +17,9 @@ public class MultiNodePickerConfiguration : IIgnoreUserStartNodesConfig
|
||||
[ConfigurationField("maxNumber")]
|
||||
public int MaxNumber { get; set; }
|
||||
|
||||
[ConfigurationField("filter")]
|
||||
public string? Filter { get; set; }
|
||||
|
||||
[ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes)]
|
||||
public bool IgnoreUserStartNodes { get; set; }
|
||||
}
|
||||
|
||||
@@ -20,13 +20,22 @@ public class TypedJsonValidatorRunner<TValue, TConfiguration> : IValueValidator
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly ITypedJsonValidator<TValue, TConfiguration>[] _validators;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TypedJsonValidatorRunner{TValue, TConfiguration}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="jsonSerializer">The JSON serializer.</param>
|
||||
/// <param name="validators">The collection of validators to run.</param>
|
||||
public TypedJsonValidatorRunner(IJsonSerializer jsonSerializer, params ITypedJsonValidator<TValue, TConfiguration>[] validators)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_validators = validators;
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration,
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(
|
||||
object? value,
|
||||
string? valueType,
|
||||
object? dataTypeConfiguration,
|
||||
PropertyValidationContext validationContext)
|
||||
{
|
||||
var validationResults = new List<ValidationResult>();
|
||||
@@ -36,7 +45,8 @@ public class TypedJsonValidatorRunner<TValue, TConfiguration> : IValueValidator
|
||||
return validationResults;
|
||||
}
|
||||
|
||||
if (value is null || _jsonSerializer.TryDeserialize(value, out TValue? deserializedValue) is false)
|
||||
TValue? deserializedValue = null;
|
||||
if (value is not null && _jsonSerializer.TryDeserialize(value, out deserializedValue) is false)
|
||||
{
|
||||
return validationResults;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors.Validation;
|
||||
|
||||
/// <summary>
|
||||
@@ -26,4 +28,49 @@ public static class ValidationHelper
|
||||
|
||||
return (value - min) % step == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if all provided entities has the start node as an ancestor.
|
||||
/// </summary>
|
||||
/// <param name="entityKeys">Keys to check.</param>
|
||||
/// <param name="startNode">The configured start node.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service to use for the checks.</param>
|
||||
/// <returns>True if the startnode key is in the ancestry tree.</returns>
|
||||
public static bool HasValidStartNode(IEnumerable<Guid> entityKeys, Guid startNode, INavigationQueryService navigationQueryService)
|
||||
{
|
||||
List<Guid> uniqueParentKeys = [];
|
||||
foreach (Guid distinctMediaKey in entityKeys.Distinct())
|
||||
{
|
||||
if (navigationQueryService.TryGetParentKey(distinctMediaKey, out Guid? parentKey) is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there is a start node, the media must have a parent.
|
||||
if (parentKey is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uniqueParentKeys.Add(parentKey.Value);
|
||||
}
|
||||
|
||||
IEnumerable<Guid> parentKeysNotInStartNode = uniqueParentKeys.Where(x => x != startNode);
|
||||
foreach (Guid parentKey in parentKeysNotInStartNode)
|
||||
{
|
||||
if (navigationQueryService.TryGetAncestorsKeys(parentKey, out IEnumerable<Guid> foundAncestorKeys) is false)
|
||||
{
|
||||
// We couldn't find the parent node, so we fail.
|
||||
return false;
|
||||
}
|
||||
|
||||
Guid[] ancestorKeys = foundAncestorKeys.ToArray();
|
||||
if (ancestorKeys.Length == 0 || ancestorKeys.Contains(startNode) is false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ using Umbraco.Extensions;
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a media picker property editor.
|
||||
/// Represents a media picker property editor.
|
||||
/// </summary>
|
||||
[DataEditor(
|
||||
Constants.PropertyEditors.Aliases.MediaPicker3,
|
||||
@@ -38,6 +38,7 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
SupportsReadOnly = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPropertyIndexValueFactory PropertyIndexValueFactory { get; } = new NoopPropertyIndexValueFactory();
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -48,8 +49,9 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
protected override IDataValueEditor CreateValueEditor() =>
|
||||
DataValueEditorFactory.Create<MediaPicker3PropertyValueEditor>(Attribute!);
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Defines the value editor for the media picker property editor.
|
||||
/// </summary>
|
||||
internal class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference
|
||||
{
|
||||
private readonly IDataTypeConfigurationCache _dataTypeReadCache;
|
||||
@@ -60,6 +62,13 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MediaPicker3PropertyValueEditor"/> class.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note on FromEditor() and ToEditor() methods.
|
||||
/// We do not want to transform the way the data is stored in the DB and would like to keep a raw JSON string.
|
||||
/// </remarks>
|
||||
public MediaPicker3PropertyValueEditor(
|
||||
IShortStringHelper shortStringHelper,
|
||||
IJsonSerializer jsonSerializer,
|
||||
@@ -92,10 +101,7 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
Validators.Add(validators);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Note: no FromEditor() and ToEditor() methods
|
||||
/// We do not want to transform the way the data is stored in the DB and would like to keep a raw JSON string
|
||||
/// </remarks>
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
|
||||
{
|
||||
foreach (MediaWithCropsDto dto in Deserialize(_jsonSerializer, value))
|
||||
@@ -104,6 +110,7 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object ToEditor(IProperty property, string? culture = null, string? segment = null)
|
||||
{
|
||||
var value = property.GetValue(culture, segment);
|
||||
@@ -111,7 +118,7 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
var dtos = Deserialize(_jsonSerializer, value).ToList();
|
||||
dtos = UpdateMediaTypeAliases(dtos);
|
||||
|
||||
var configuration = _dataTypeReadCache.GetConfigurationAs<MediaPicker3Configuration>(property.PropertyType.DataTypeKey);
|
||||
MediaPicker3Configuration? configuration = _dataTypeReadCache.GetConfigurationAs<MediaPicker3Configuration>(property.PropertyType.DataTypeKey);
|
||||
if (configuration is not null)
|
||||
{
|
||||
foreach (MediaWithCropsDto dto in dtos)
|
||||
@@ -123,6 +130,7 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
return dtos;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
|
||||
{
|
||||
if (editorValue.Value is null ||
|
||||
@@ -145,6 +153,9 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
return _jsonSerializer.Serialize(mediaWithCropsDtos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the provided JSON value into a list of <see cref="MediaWithCropsDto"/>.
|
||||
/// </summary>
|
||||
internal static IEnumerable<MediaWithCropsDto> Deserialize(IJsonSerializer jsonSerializer, object? value)
|
||||
{
|
||||
var rawJson = value is string str ? str : value?.ToString();
|
||||
@@ -248,14 +259,29 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
/// </summary>
|
||||
internal class MediaWithCropsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the key.
|
||||
/// </summary>
|
||||
public Guid Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the media key.
|
||||
/// </summary>
|
||||
public Guid MediaKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the media type alias.
|
||||
/// </summary>
|
||||
public string MediaTypeAlias { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the crops.
|
||||
/// </summary>
|
||||
public IEnumerable<ImageCropperValue.ImageCropperCrop>? Crops { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the focal point.
|
||||
/// </summary>
|
||||
public ImageCropperValue.ImageCropperFocalPoint? FocalPoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -309,12 +335,19 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the min/max configuration for the media picker property editor.
|
||||
/// </summary>
|
||||
internal class MinMaxValidator : ITypedJsonValidator<List<MediaWithCropsDto>, MediaPicker3Configuration>
|
||||
{
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MinMaxValidator"/> class.
|
||||
/// </summary>
|
||||
public MinMaxValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(
|
||||
List<MediaWithCropsDto>? mediaWithCropsDtos,
|
||||
MediaPicker3Configuration? mediaPickerConfiguration,
|
||||
@@ -363,12 +396,18 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the allowed type configuration for the media picker property editor.
|
||||
/// </summary>
|
||||
internal class AllowedTypeValidator : ITypedJsonValidator<List<MediaWithCropsDto>, MediaPicker3Configuration>
|
||||
{
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
private readonly IMediaTypeService _mediaTypeService;
|
||||
private readonly IMediaService _mediaService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AllowedTypeValidator"/> class.
|
||||
/// </summary>
|
||||
public AllowedTypeValidator(ILocalizedTextService localizedTextService, IMediaTypeService mediaTypeService, IMediaService mediaService)
|
||||
{
|
||||
_localizedTextService = localizedTextService;
|
||||
@@ -376,6 +415,7 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
_mediaService = mediaService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(
|
||||
List<MediaWithCropsDto>? value,
|
||||
MediaPicker3Configuration? configuration,
|
||||
@@ -429,11 +469,17 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the start node configuration for the media picker property editor.
|
||||
/// </summary>
|
||||
internal class StartNodeValidator : ITypedJsonValidator<List<MediaWithCropsDto>, MediaPicker3Configuration>
|
||||
{
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
private readonly IMediaNavigationQueryService _mediaNavigationQueryService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StartNodeValidator"/> class.
|
||||
/// </summary>
|
||||
public StartNodeValidator(
|
||||
ILocalizedTextService localizedTextService,
|
||||
IMediaNavigationQueryService mediaNavigationQueryService)
|
||||
@@ -442,6 +488,7 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
_mediaNavigationQueryService = mediaNavigationQueryService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(
|
||||
List<MediaWithCropsDto>? value,
|
||||
MediaPicker3Configuration? configuration,
|
||||
@@ -453,53 +500,14 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
List<Guid> uniqueParentKeys = [];
|
||||
foreach (Guid distinctMediaKey in value.DistinctBy(x => x.MediaKey).Select(x => x.MediaKey))
|
||||
if (ValidationHelper.HasValidStartNode(value.Select(x => x.MediaKey), configuration.StartNodeId.Value, _mediaNavigationQueryService) is false)
|
||||
{
|
||||
if (_mediaNavigationQueryService.TryGetParentKey(distinctMediaKey, out Guid? parentKey) is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there is a start node, the media must have a parent.
|
||||
if (parentKey is null)
|
||||
{
|
||||
return
|
||||
[
|
||||
new ValidationResult(
|
||||
_localizedTextService.Localize("validation", "invalidStartNode"),
|
||||
["value"])
|
||||
];
|
||||
}
|
||||
|
||||
uniqueParentKeys.Add(parentKey.Value);
|
||||
}
|
||||
|
||||
IEnumerable<Guid> parentKeysNotInStartNode = uniqueParentKeys.Where(x => x != configuration.StartNodeId.Value);
|
||||
foreach (Guid parentKey in parentKeysNotInStartNode)
|
||||
{
|
||||
if (_mediaNavigationQueryService.TryGetAncestorsKeys(parentKey, out IEnumerable<Guid> foundAncestorKeys) is false)
|
||||
{
|
||||
// We couldn't find the parent node, so we fail.
|
||||
return
|
||||
[
|
||||
new ValidationResult(
|
||||
_localizedTextService.Localize("validation", "invalidStartNode"),
|
||||
["value"])
|
||||
];
|
||||
}
|
||||
|
||||
Guid[] ancestorKeys = foundAncestorKeys.ToArray();
|
||||
if (ancestorKeys.Length == 0 || ancestorKeys.Contains(configuration.StartNodeId.Value) is false)
|
||||
{
|
||||
return
|
||||
[
|
||||
new ValidationResult(
|
||||
_localizedTextService.Localize("validation", "invalidStartNode"),
|
||||
["value"])
|
||||
];
|
||||
}
|
||||
return
|
||||
[
|
||||
new ValidationResult(
|
||||
_localizedTextService.Localize("validation", "invalidStartNode"),
|
||||
["value"])
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Editors;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Models.Validation;
|
||||
using Umbraco.Cms.Core.PropertyEditors.Validation;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a multi-node tree picker property editor.
|
||||
/// </summary>
|
||||
[DataEditor(
|
||||
Constants.PropertyEditors.Aliases.MultiNodeTreePicker,
|
||||
ValueType = ValueTypes.Text,
|
||||
@@ -19,6 +30,9 @@ public class MultiNodeTreePickerPropertyEditor : DataEditor
|
||||
{
|
||||
private readonly IIOHelper _ioHelper;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MultiNodeTreePickerPropertyEditor"/> class.
|
||||
/// </summary>
|
||||
public MultiNodeTreePickerPropertyEditor(IDataValueEditorFactory dataValueEditorFactory, IIOHelper ioHelper)
|
||||
: base(dataValueEditorFactory)
|
||||
{
|
||||
@@ -26,32 +40,75 @@ public class MultiNodeTreePickerPropertyEditor : DataEditor
|
||||
SupportsReadOnly = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IConfigurationEditor CreateConfigurationEditor() =>
|
||||
new MultiNodePickerConfigurationEditor(_ioHelper);
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IDataValueEditor CreateValueEditor() =>
|
||||
DataValueEditorFactory.Create<MultiNodeTreePickerPropertyValueEditor>(Attribute!);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the value editor for the media picker property editor.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// At first glance, the fromEditor and toEditor methods might seem strange.
|
||||
/// This is because we wanted to stop the leaking of UDI's to the frontend while not having to do database migrations
|
||||
/// so we opted to, for now, translate the udi string in the database into a structured format unique to the client
|
||||
/// At first glance, the FromEditor and ToEditor methods might seem strange.
|
||||
/// This is because we wanted to stop the leaking of UDIs to the frontend while not having to do database migrations
|
||||
/// so we opted to, for now, translate the UDI string in the database into a structured format unique to the client.
|
||||
/// This way, for now, no migration is needed and no changes outside of the editor logic needs to be touched to stop the leaking.
|
||||
/// </remarks>
|
||||
public class MultiNodeTreePickerPropertyValueEditor : DataValueEditor, IDataValueReference
|
||||
{
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MultiNodeTreePickerPropertyValueEditor"/> class.
|
||||
/// </summary>
|
||||
public MultiNodeTreePickerPropertyValueEditor(
|
||||
IShortStringHelper shortStringHelper,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IIOHelper ioHelper,
|
||||
DataEditorAttribute attribute,
|
||||
ILocalizedTextService localizedTextService,
|
||||
IEntityService entityService,
|
||||
ICoreScopeProvider coreScopeProvider,
|
||||
IContentService contentService,
|
||||
IMediaService mediaService,
|
||||
IMemberService memberService)
|
||||
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
Validators.Add(new TypedJsonValidatorRunner<EditorEntityReference[], MultiNodePickerConfiguration>(
|
||||
jsonSerializer,
|
||||
new MinMaxValidator(localizedTextService),
|
||||
new ObjectTypeValidator(localizedTextService, coreScopeProvider, entityService),
|
||||
new ContentTypeValidator(localizedTextService, coreScopeProvider, contentService, mediaService, memberService)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MultiNodeTreePickerPropertyValueEditor"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
|
||||
public MultiNodeTreePickerPropertyValueEditor(
|
||||
IShortStringHelper shortStringHelper,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IIOHelper ioHelper,
|
||||
DataEditorAttribute attribute)
|
||||
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
|
||||
: this(
|
||||
shortStringHelper,
|
||||
jsonSerializer,
|
||||
ioHelper,
|
||||
attribute,
|
||||
StaticServiceProvider.Instance.GetRequiredService<ILocalizedTextService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IEntityService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<ICoreScopeProvider>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IContentService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IMediaService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IMemberService>())
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
|
||||
{
|
||||
var asString = value == null ? string.Empty : value is string str ? str : value.ToString();
|
||||
@@ -66,12 +123,13 @@ public class MultiNodeTreePickerPropertyEditor : DataEditor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
|
||||
=> editorValue.Value is JsonArray jsonArray
|
||||
? EntityReferencesToUdis(_jsonSerializer.Deserialize<IEnumerable<EditorEntityReference>>(jsonArray.ToJsonString()) ?? Enumerable.Empty<EditorEntityReference>())
|
||||
: null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? ToEditor(IProperty property, string? culture = null, string? segment = null)
|
||||
{
|
||||
var value = property.GetValue(culture, segment);
|
||||
@@ -80,7 +138,7 @@ public class MultiNodeTreePickerPropertyEditor : DataEditor
|
||||
: null;
|
||||
}
|
||||
|
||||
private IEnumerable<EditorEntityReference> UdisToEntityReferences(IEnumerable<string> stringUdis)
|
||||
private static IEnumerable<EditorEntityReference> UdisToEntityReferences(IEnumerable<string> stringUdis)
|
||||
{
|
||||
foreach (var stringUdi in stringUdis)
|
||||
{
|
||||
@@ -93,14 +151,277 @@ public class MultiNodeTreePickerPropertyEditor : DataEditor
|
||||
}
|
||||
}
|
||||
|
||||
private string EntityReferencesToUdis(IEnumerable<EditorEntityReference> nodeReferences)
|
||||
private static string EntityReferencesToUdis(IEnumerable<EditorEntityReference> nodeReferences)
|
||||
=> string.Join(",", nodeReferences.Select(entityReference => Udi.Create(entityReference.Type, entityReference.Unique).ToString()));
|
||||
|
||||
/// <summary>
|
||||
/// Describes and editor entity reference.
|
||||
/// </summary>
|
||||
public class EditorEntityReference
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the entity object type.
|
||||
/// </summary>
|
||||
public required string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the entity unique identifier.
|
||||
/// </summary>
|
||||
public required Guid Unique { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the configured object type for documents.
|
||||
/// </summary>
|
||||
internal const string DocumentObjectType = "content";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the configured object type for media.
|
||||
/// </summary>
|
||||
internal const string MediaObjectType = "media";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the configured object type for members.
|
||||
/// </summary>
|
||||
internal const string MemberObjectType = "member";
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <summary>
|
||||
/// Validates the min/max configuration for the multi-node tree picker property editor.
|
||||
/// </summary>
|
||||
internal class MinMaxValidator : ITypedJsonValidator<EditorEntityReference[], MultiNodePickerConfiguration>
|
||||
{
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MinMaxValidator"/> class.
|
||||
/// </summary>
|
||||
public MinMaxValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(
|
||||
EditorEntityReference[]? entityReferences,
|
||||
MultiNodePickerConfiguration? configuration,
|
||||
string? valueType,
|
||||
PropertyValidationContext validationContext)
|
||||
{
|
||||
var validationResults = new List<ValidationResult>();
|
||||
|
||||
if (configuration is null)
|
||||
{
|
||||
return validationResults;
|
||||
}
|
||||
|
||||
if (configuration.MinNumber > 0 && (entityReferences is null || entityReferences.Length < configuration.MinNumber))
|
||||
{
|
||||
validationResults.Add(new ValidationResult(
|
||||
_localizedTextService.Localize(
|
||||
"validation",
|
||||
"entriesShort",
|
||||
[configuration.MinNumber.ToString(), (configuration.MinNumber - (entityReferences?.Length ?? 0)).ToString()
|
||||
]),
|
||||
["value"]));
|
||||
}
|
||||
|
||||
if (entityReferences is null)
|
||||
{
|
||||
return validationResults;
|
||||
}
|
||||
|
||||
if (configuration.MaxNumber > 0 && entityReferences.Length > configuration.MaxNumber)
|
||||
{
|
||||
validationResults.Add(new ValidationResult(
|
||||
_localizedTextService.Localize(
|
||||
"validation",
|
||||
"entriesExceed",
|
||||
[configuration.MaxNumber.ToString(), (entityReferences.Length - configuration.MaxNumber).ToString()
|
||||
]),
|
||||
["value"]));
|
||||
}
|
||||
|
||||
return validationResults;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <summary>
|
||||
/// Validates the selected object type for the multi-node tree picker property editor.
|
||||
/// </summary>
|
||||
internal class ObjectTypeValidator : ITypedJsonValidator<EditorEntityReference[], MultiNodePickerConfiguration>
|
||||
{
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
private readonly ICoreScopeProvider _coreScopeProvider;
|
||||
private readonly IEntityService _entityService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ObjectTypeValidator"/> class.
|
||||
/// </summary>
|
||||
public ObjectTypeValidator(
|
||||
ILocalizedTextService localizedTextService,
|
||||
ICoreScopeProvider coreScopeProvider,
|
||||
IEntityService entityService)
|
||||
{
|
||||
_localizedTextService = localizedTextService;
|
||||
_coreScopeProvider = coreScopeProvider;
|
||||
_entityService = entityService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(
|
||||
EditorEntityReference[]? entityReferences,
|
||||
MultiNodePickerConfiguration? configuration,
|
||||
string? valueType,
|
||||
PropertyValidationContext validationContext)
|
||||
{
|
||||
var validationResults = new List<ValidationResult>();
|
||||
|
||||
if (entityReferences is null || configuration?.TreeSource?.ObjectType is null)
|
||||
{
|
||||
return validationResults;
|
||||
}
|
||||
|
||||
Guid[] uniqueKeys = entityReferences.DistinctBy(x => x.Unique).Select(x => x.Unique).ToArray();
|
||||
|
||||
if (uniqueKeys.Length == 0)
|
||||
{
|
||||
return validationResults;
|
||||
}
|
||||
|
||||
Guid? allowedObjectType = GetObjectType(configuration.TreeSource.ObjectType);
|
||||
if (allowedObjectType is null)
|
||||
{
|
||||
return
|
||||
[
|
||||
|
||||
// Some invalid object type was sent.
|
||||
new ValidationResult(
|
||||
_localizedTextService.Localize(
|
||||
"validation",
|
||||
"invalidObjectType"),
|
||||
["value"])
|
||||
];
|
||||
}
|
||||
|
||||
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
|
||||
foreach (Guid key in uniqueKeys)
|
||||
{
|
||||
IEntitySlim? entity = _entityService.Get(key);
|
||||
if (entity is not null && entity.NodeObjectType != allowedObjectType)
|
||||
{
|
||||
validationResults.Add(new ValidationResult(
|
||||
_localizedTextService.Localize(
|
||||
"validation",
|
||||
"invalidObjectType"),
|
||||
["value"]));
|
||||
}
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
|
||||
return validationResults;
|
||||
}
|
||||
|
||||
private static Guid? GetObjectType(string objectType) =>
|
||||
objectType switch
|
||||
{
|
||||
DocumentObjectType => Constants.ObjectTypes.Document,
|
||||
MediaObjectType => Constants.ObjectTypes.Media,
|
||||
MemberObjectType => Constants.ObjectTypes.Member,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <summary>
|
||||
/// Validates the selected content type for the multi-node tree picker property editor.
|
||||
/// </summary>
|
||||
internal class ContentTypeValidator : ITypedJsonValidator<EditorEntityReference[], MultiNodePickerConfiguration>
|
||||
{
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
private readonly ICoreScopeProvider _coreScopeProvider;
|
||||
private readonly IContentService _contentService;
|
||||
private readonly IMediaService _mediaService;
|
||||
private readonly IMemberService _memberService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContentTypeValidator"/> class.
|
||||
/// </summary>
|
||||
public ContentTypeValidator(
|
||||
ILocalizedTextService localizedTextService,
|
||||
ICoreScopeProvider coreScopeProvider,
|
||||
IContentService contentService,
|
||||
IMediaService mediaService,
|
||||
IMemberService memberService)
|
||||
{
|
||||
_localizedTextService = localizedTextService;
|
||||
_coreScopeProvider = coreScopeProvider;
|
||||
_contentService = contentService;
|
||||
_mediaService = mediaService;
|
||||
_memberService = memberService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(
|
||||
EditorEntityReference[]? entityReferences,
|
||||
MultiNodePickerConfiguration? configuration,
|
||||
string? valueType,
|
||||
PropertyValidationContext validationContext)
|
||||
{
|
||||
var validationResults = new List<ValidationResult>();
|
||||
|
||||
Guid[] allowedTypes = configuration?.Filter?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).Select(Guid.Parse).ToArray() ?? [];
|
||||
|
||||
// We can't validate if there is no object type, and we don't need to if there's no filter.
|
||||
if (entityReferences is null || allowedTypes.Length == 0 || configuration?.TreeSource?.ObjectType is null)
|
||||
{
|
||||
return validationResults;
|
||||
}
|
||||
|
||||
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
|
||||
|
||||
Guid?[] uniqueContentTypeKeys = entityReferences
|
||||
.Select(x => x.Unique)
|
||||
.Distinct()
|
||||
.Select(x => GetContent(configuration.TreeSource.ObjectType, x))
|
||||
.Select(x => x?.ContentType.Key)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
scope.Complete();
|
||||
|
||||
foreach (Guid? key in uniqueContentTypeKeys)
|
||||
{
|
||||
if (key is null)
|
||||
{
|
||||
validationResults.Add(new ValidationResult(
|
||||
_localizedTextService.Localize(
|
||||
"validation",
|
||||
"missingContent"),
|
||||
["value"]));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (allowedTypes.Contains(key.Value) is false)
|
||||
{
|
||||
validationResults.Add(new ValidationResult(
|
||||
_localizedTextService.Localize(
|
||||
"validation",
|
||||
"invalidObjectType"),
|
||||
["value"]));
|
||||
}
|
||||
}
|
||||
|
||||
return validationResults;
|
||||
}
|
||||
|
||||
private IContentBase? GetContent(string objectType, Guid key) =>
|
||||
objectType switch
|
||||
{
|
||||
DocumentObjectType => _contentService.GetById(key),
|
||||
MediaObjectType => _mediaService.GetById(key),
|
||||
MemberObjectType => _memberService.GetById(key),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Data;
|
||||
using System.Text.Json.Nodes;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Org.BouncyCastle.Asn1.X500;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Editors;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
@@ -244,11 +247,30 @@ public class MultiNodeTreePickerTests
|
||||
private static MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor CreateValueEditor(
|
||||
IJsonSerializer? jsonSerializer = null)
|
||||
{
|
||||
var mockScope = new Mock<IScope>();
|
||||
var mockScopeProvider = new Mock<ICoreScopeProvider>();
|
||||
mockScopeProvider
|
||||
.Setup(x => x.CreateCoreScope(
|
||||
It.IsAny<IsolationLevel>(),
|
||||
It.IsAny<RepositoryCacheMode>(),
|
||||
It.IsAny<IEventDispatcher>(),
|
||||
It.IsAny<IScopedNotificationPublisher>(),
|
||||
It.IsAny<bool?>(),
|
||||
It.IsAny<bool>(),
|
||||
It.IsAny<bool>()))
|
||||
.Returns(mockScope.Object);
|
||||
|
||||
var valueEditor = new MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor(
|
||||
Mock.Of<IShortStringHelper>(),
|
||||
jsonSerializer ?? Mock.Of<IJsonSerializer>(),
|
||||
Mock.Of<IIOHelper>(),
|
||||
new DataEditorAttribute("alias"));
|
||||
new DataEditorAttribute("alias"),
|
||||
Mock.Of<ILocalizedTextService>(),
|
||||
Mock.Of<IEntityService>(),
|
||||
mockScopeProvider.Object,
|
||||
Mock.Of<IContentService>(),
|
||||
Mock.Of<IMediaService>(),
|
||||
Mock.Of<IMemberService>());
|
||||
return valueEditor;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Data;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Models.Validation;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors;
|
||||
|
||||
[TestFixture]
|
||||
public class MultiNodeTreePickerValidationTests
|
||||
{
|
||||
// Remember 0 = no limit
|
||||
[TestCase(0, true, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"}]")]
|
||||
[TestCase(1, true, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"}]")]
|
||||
[TestCase(2, true, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"},{\"type\":\"document\",\"unique\":\"25ef6fd2-db48-450a-8c48-df3ad75adf4b\"}]")]
|
||||
[TestCase(3, false, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"},{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"}]")]
|
||||
[TestCase(2, false, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"}]")]
|
||||
[TestCase(1, false, null)]
|
||||
[TestCase(0, true, null)]
|
||||
public void Validates_Minimum_Entries(int min, bool shouldSucceed, string? value)
|
||||
{
|
||||
var (valueEditor, _, _, _, _) = CreateValueEditor();
|
||||
valueEditor.ConfigurationObject = new MultiNodePickerConfiguration { MinNumber = min};
|
||||
|
||||
var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty());
|
||||
|
||||
TestShouldSucceed(shouldSucceed, result);
|
||||
}
|
||||
|
||||
private static void TestShouldSucceed(bool shouldSucceed, IEnumerable<ValidationResult> result)
|
||||
{
|
||||
if (shouldSucceed)
|
||||
{
|
||||
Assert.IsEmpty(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNotEmpty(result);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(0, true, "[]")]
|
||||
[TestCase(1, true, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"}]")]
|
||||
[TestCase(0, true, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"}]")]
|
||||
[TestCase(1, false, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"},{\"type\":\"document\",\"unique\":\"25ef6fd2-db48-450a-8c48-df3ad75adf4b\"}]")]
|
||||
[TestCase(3, true, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"},{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"}]")]
|
||||
[TestCase(2, true, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"}]")]
|
||||
public void Validates_Maximum_Entries(int max, bool shouldSucceed, string value)
|
||||
{
|
||||
var (valueEditor, _, _, _, _) = CreateValueEditor();
|
||||
valueEditor.ConfigurationObject = new MultiNodePickerConfiguration { MaxNumber = max };
|
||||
|
||||
var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty());
|
||||
|
||||
TestShouldSucceed(shouldSucceed, result);
|
||||
}
|
||||
|
||||
private readonly Dictionary<Guid, Guid> _entityTypeMap = new()
|
||||
{
|
||||
{ Constants.ObjectTypes.Document, Guid.Parse("08035A7E-AE9C-4D36-BA2E-63F639005758") },
|
||||
{ Constants.ObjectTypes.Media, Guid.Parse("AAF97C7D-A586-45CC-AC7F-CE0A80BCFEE3") },
|
||||
{ Constants.ObjectTypes.Member, Guid.Parse("E477804E-C903-470B-B7EC-67DCAF71E37C") },
|
||||
};
|
||||
|
||||
private class ObjectTypeTestSetup
|
||||
{
|
||||
public ObjectTypeTestSetup(string expectedObjectType, bool shouldSucceed, string value)
|
||||
{
|
||||
ExpectedObjectType = expectedObjectType;
|
||||
ShouldSucceed = shouldSucceed;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string ExpectedObjectType { get; }
|
||||
|
||||
public bool ShouldSucceed { get; }
|
||||
|
||||
public string Value { get; }
|
||||
}
|
||||
|
||||
private void SetupEntityServiceForObjectTypeTest(Mock<IEntityService> entityServiceMock)
|
||||
{
|
||||
foreach (var objectTypeEntity in _entityTypeMap)
|
||||
{
|
||||
var entity = new Mock<IEntitySlim>();
|
||||
entity.Setup(x => x.NodeObjectType).Returns(objectTypeEntity.Key);
|
||||
entityServiceMock.Setup(x => x.Get(objectTypeEntity.Value)).Returns(entity.Object);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<ObjectTypeTestSetup> GetObjectTypeTestSetup() =>
|
||||
[
|
||||
new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.DocumentObjectType, true, "[]"),
|
||||
new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.DocumentObjectType, true, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Document]}\"}}]"),
|
||||
new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.DocumentObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Media]}\"}}]"),
|
||||
new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.DocumentObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Member]}\"}}]"),
|
||||
new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MediaObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Document]}\"}}]"),
|
||||
new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MemberObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Document]}\"}}]"),
|
||||
new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.DocumentObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Document]}\"}}, {{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Media]}\"}}]"),
|
||||
new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MediaObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Document]}\"}}]"),
|
||||
new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MediaObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Member]}\"}}]"),
|
||||
new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MemberObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Document]}\"}}]"),
|
||||
new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MemberObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Media]}\"}}]"),
|
||||
];
|
||||
|
||||
[Test]
|
||||
public void Validates_Object_Type()
|
||||
{
|
||||
var setups = GetObjectTypeTestSetup();
|
||||
|
||||
foreach (var setup in setups)
|
||||
{
|
||||
var (valueEditor, entityServiceMock, _, _, _) = CreateValueEditor();
|
||||
SetupEntityServiceForObjectTypeTest(entityServiceMock);
|
||||
valueEditor.ConfigurationObject = new MultiNodePickerConfiguration { TreeSource = new MultiNodePickerConfigurationTreeSource() { ObjectType = setup.ExpectedObjectType } };
|
||||
var result = valueEditor.Validate(setup.Value, false, null, PropertyValidationContext.Empty());
|
||||
|
||||
TestShouldSucceed(setup.ShouldSucceed, result);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(true, true, true, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.DocumentObjectType)]
|
||||
[TestCase(true, true, true, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MediaObjectType)]
|
||||
[TestCase(true, true, true, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MemberObjectType)]
|
||||
[TestCase(false, false, true, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.DocumentObjectType)]
|
||||
[TestCase(false, false, true, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MediaObjectType)]
|
||||
[TestCase(false, false, true, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MemberObjectType)]
|
||||
[TestCase(false, true, false, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.DocumentObjectType)]
|
||||
[TestCase(false, true, false, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MediaObjectType)]
|
||||
[TestCase(false, true, false, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MemberObjectType)]
|
||||
public void Validates_Allowed_Type(bool shouldSucceed, bool hasAllowedType, bool findsContent, string objectType)
|
||||
{
|
||||
var (valueEditor, _, contentService, mediaService, memberService) = CreateValueEditor();
|
||||
|
||||
var expectedEntityKey = Guid.NewGuid();
|
||||
var allowedTypeKey = Guid.NewGuid();
|
||||
valueEditor.ConfigurationObject = new MultiNodePickerConfiguration()
|
||||
{
|
||||
Filter = $"{allowedTypeKey}",
|
||||
TreeSource = new MultiNodePickerConfigurationTreeSource { ObjectType = objectType },
|
||||
};
|
||||
|
||||
var contentTypeMock = new Mock<ISimpleContentType>();
|
||||
contentTypeMock.Setup(x => x.Key).Returns(() => hasAllowedType ? allowedTypeKey : Guid.NewGuid());
|
||||
|
||||
var contentMock = new Mock<IContent>();
|
||||
contentMock.Setup(x => x.ContentType).Returns(contentTypeMock.Object);
|
||||
contentService.Setup(x => x.GetById(expectedEntityKey)).Returns(contentMock.Object);
|
||||
|
||||
var mediaMock = new Mock<IMedia>();
|
||||
mediaMock.Setup(x => x.ContentType).Returns(contentTypeMock.Object);
|
||||
mediaService.Setup(x => x.GetById(expectedEntityKey)).Returns(mediaMock.Object);
|
||||
|
||||
var memberMock = new Mock<IMember>();
|
||||
memberMock.Setup(x => x.ContentType).Returns(contentTypeMock.Object);
|
||||
memberService.Setup(x => x.GetById(expectedEntityKey)).Returns(memberMock.Object);
|
||||
|
||||
var actualkey = findsContent ? expectedEntityKey : Guid.NewGuid();
|
||||
var value = $"[{{\"type\":\"document\",\"unique\":\"{actualkey}\"}}]";
|
||||
|
||||
var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty());
|
||||
TestShouldSucceed(shouldSucceed, result);
|
||||
|
||||
}
|
||||
|
||||
private static (MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor ValueEditor,
|
||||
Mock<IEntityService> EntityService,
|
||||
Mock<IContentService> ContentService,
|
||||
Mock<IMediaService> MediaService,
|
||||
Mock<IMemberService> MemberService) CreateValueEditor()
|
||||
{
|
||||
var entityServiceMock = new Mock<IEntityService>();
|
||||
var contentServiceMock = new Mock<IContentService>();
|
||||
var mediaServiceMock = new Mock<IMediaService>();
|
||||
var memberServiceMock = new Mock<IMemberService>();
|
||||
|
||||
var mockScope = new Mock<ICoreScope>();
|
||||
var mockScopeProvider = new Mock<ICoreScopeProvider>();
|
||||
mockScopeProvider
|
||||
.Setup(x => x.CreateCoreScope(
|
||||
It.IsAny<IsolationLevel>(),
|
||||
It.IsAny<RepositoryCacheMode>(),
|
||||
It.IsAny<IEventDispatcher>(),
|
||||
It.IsAny<IScopedNotificationPublisher>(),
|
||||
It.IsAny<bool?>(),
|
||||
It.IsAny<bool>(),
|
||||
It.IsAny<bool>()))
|
||||
.Returns(mockScope.Object);
|
||||
|
||||
var valueEditor = new MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor(
|
||||
Mock.Of<IShortStringHelper>(),
|
||||
new SystemTextJsonSerializer(),
|
||||
Mock.Of<IIOHelper>(),
|
||||
new DataEditorAttribute("alias"),
|
||||
Mock.Of<ILocalizedTextService>(),
|
||||
entityServiceMock.Object,
|
||||
mockScopeProvider.Object,
|
||||
contentServiceMock.Object,
|
||||
mediaServiceMock.Object,
|
||||
memberServiceMock.Object)
|
||||
{
|
||||
ConfigurationObject = new MultiNodePickerConfiguration(),
|
||||
};
|
||||
|
||||
return (valueEditor, entityServiceMock, contentServiceMock, mediaServiceMock, memberServiceMock);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user