V15: Serverside Media Picker Validation (#18429)

* Add TypedJsonValidator to avoid duplicate serialization

* Add allowed type validator

* Validate multiple media toggle

* Add startnode validator

* Fix tests

* Add validation tests

* Apply suggestions from code review

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Add XML docs

* Remove unnecessary obsolete constructor

* Avoid multiple checks

* Use value instead of specific member names

* Remove test

* Optimize StartNodeValidator

* Clarify Validates_Allowed_Type

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
Mole
2025-02-25 13:26:16 +01:00
committed by GitHub
parent 790c451df1
commit 74eb66ef86
8 changed files with 453 additions and 49 deletions

View File

@@ -113,6 +113,9 @@ Mange hilsner fra Umbraco robotten
<key alias="entriesShort"><![CDATA[Minimum %0% element(er), tilføj <strong>%1%</strong> mere.]]></key>
<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="multipleMediaNotAllowed">Det er kun tilladt at vælge ét medie.</key>
<key alias="invalidStartNode">Valgt medie kommer fra en ugyldig mappe.</key>
</area>
<area alias="recycleBin">
<key alias="contentTrashed">Slettet indhold med Id: {0} Relateret til original "parent" med id: {1}</key>
@@ -124,4 +127,4 @@ Mange hilsner fra Umbraco robotten
<key alias="FileWriting">Filskrivning</key>
<key alias="MediaFolderCreation">Mediemappeoprettelse</key>
</area>
</language>
</language>

View File

@@ -387,6 +387,9 @@
<key alias="duplicateUserGroupName">User group name '%0%' is already taken</key>
<key alias="duplicateMemberGroupName">Member group name '%0%' is already taken</key>
<key alias="duplicateUsername">Username '%0%' is already taken</key>
<key alias="invalidMediaType">The chosen media type is invalid.</key>
<key alias="multipleMediaNotAllowed">Multiple selected media is not allowed.</key>
<key alias="invalidStartNode">The selected media is from the wrong folder.</key>
</area>
<area alias="healthcheck">
<!-- The following keys get these tokens passed in:

View File

@@ -388,6 +388,9 @@
<key alias="entriesShort"><![CDATA[Minimum %0% entries, requires <strong>%1%</strong> more.]]></key>
<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="multipleMediaNotAllowed">Multiple selected media is not allowed.</key>
<key alias="invalidStartNode">The selected media is from the wrong folder.</key>
</area>
<area alias="healthcheck">
<!-- The following keys get these tokens passed in:

View File

@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Models.Validation;
namespace Umbraco.Cms.Core.PropertyEditors.Validation;
/// <summary>
/// A specific validator used for JSON based value editors, to avoid doing multiple deserialization.
/// <remarks>Is used together with <see cref="TypedJsonValidatorRunner{TValue,TConfiguration}"/></remarks>
/// </summary>
/// <typeparam name="TValue">The type of the value consumed by the validator.</typeparam>
/// <typeparam name="TConfiguration">The type of the configuration consumed by validator.</typeparam>
public interface ITypedJsonValidator<TValue, TConfiguration>
{
public abstract IEnumerable<ValidationResult> Validate(
TValue? value,
TConfiguration? configuration,
string? valueType,
PropertyValidationContext validationContext);
}

View File

@@ -0,0 +1,51 @@
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Serialization;
namespace Umbraco.Cms.Core.PropertyEditors.Validation;
/// <summary>
/// <para>
/// An aggregate validator for JSON based value editors, to avoid doing multiple deserialization.
/// </para>
/// <para>
/// Will deserialize once, and cast the configuration once, and pass those values to each <see cref="ITypedJsonValidator{TValue,TConfiguration}"/>, aggregating the results.
/// </para>
/// </summary>
/// <typeparam name="TValue">The type of the expected value.</typeparam>
/// <typeparam name="TConfiguration">The type of the expected configuration</typeparam>
public class TypedJsonValidatorRunner<TValue, TConfiguration> : IValueValidator
where TValue : class
{
private readonly IJsonSerializer _jsonSerializer;
private readonly ITypedJsonValidator<TValue, TConfiguration>[] _validators;
public TypedJsonValidatorRunner(IJsonSerializer jsonSerializer, params ITypedJsonValidator<TValue, TConfiguration>[] validators)
{
_jsonSerializer = jsonSerializer;
_validators = validators;
}
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration,
PropertyValidationContext validationContext)
{
var validationResults = new List<ValidationResult>();
if (dataTypeConfiguration is not TConfiguration configuration)
{
return validationResults;
}
if (value is null || _jsonSerializer.TryDeserialize(value, out TValue? deserializedValue) is false)
{
return validationResults;
}
foreach (ITypedJsonValidator<TValue, TConfiguration> validator in _validators)
{
validationResults.AddRange(validator.Validate(deserializedValue, configuration, valueType, validationContext));
}
return validationResults;
}
}

View File

@@ -1,16 +1,16 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Cache;
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.TemporaryFile;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors.Validation;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Extensions;
@@ -71,7 +71,9 @@ public class MediaPicker3PropertyEditor : DataEditor
IScopeProvider scopeProvider,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IDataTypeConfigurationCache dataTypeReadCache,
ILocalizedTextService localizedTextService)
ILocalizedTextService localizedTextService,
IMediaTypeService mediaTypeService,
IMediaNavigationQueryService mediaNavigationQueryService)
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_jsonSerializer = jsonSerializer;
@@ -81,34 +83,13 @@ public class MediaPicker3PropertyEditor : DataEditor
_scopeProvider = scopeProvider;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_dataTypeReadCache = dataTypeReadCache;
Validators.Add(new MinMaxValidator(jsonSerializer, localizedTextService));
}
[Obsolete("Use non obsoleted constructor instead. Scheduled for removal in v17")]
public MediaPicker3PropertyValueEditor(
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
DataEditorAttribute attribute,
IMediaImportService mediaImportService,
IMediaService mediaService,
ITemporaryFileService temporaryFileService,
IScopeProvider scopeProvider,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IDataTypeConfigurationCache dataTypeReadCache)
: this(
shortStringHelper,
var validators = new TypedJsonValidatorRunner<List<MediaWithCropsDto>, MediaPicker3Configuration>(
jsonSerializer,
ioHelper,
attribute,
mediaImportService,
mediaService,
temporaryFileService,
scopeProvider,
backOfficeSecurityAccessor,
dataTypeReadCache,
StaticServiceProvider.Instance.GetRequiredService<ILocalizedTextService>())
{
new MinMaxValidator(localizedTextService),
new AllowedTypeValidator(localizedTextService, mediaTypeService),
new StartNodeValidator(localizedTextService, mediaNavigationQueryService));
Validators.Add(validators);
}
/// <remarks>
@@ -328,34 +309,30 @@ public class MediaPicker3PropertyEditor : DataEditor
}
}
private class MinMaxValidator : IValueValidator
internal class MinMaxValidator : ITypedJsonValidator<List<MediaWithCropsDto>, MediaPicker3Configuration>
{
private readonly IJsonSerializer _jsonSerializer;
private readonly ILocalizedTextService _localizedTextService;
public MinMaxValidator(IJsonSerializer jsonSerializer, ILocalizedTextService localizedTextService)
{
_jsonSerializer = jsonSerializer;
_localizedTextService = localizedTextService;
}
public MinMaxValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
public IEnumerable<ValidationResult> Validate(
object? value,
List<MediaWithCropsDto>? mediaWithCropsDtos,
MediaPicker3Configuration? mediaPickerConfiguration,
string? valueType,
object? dataTypeConfiguration,
PropertyValidationContext validationContext)
{
var validationResults = new List<ValidationResult>();
if (dataTypeConfiguration is not MediaPicker3Configuration mediaPickerConfiguration)
if (mediaWithCropsDtos is null || mediaPickerConfiguration is null)
{
return validationResults;
}
if (value is null ||
_jsonSerializer.TryDeserialize(value, out List<MediaWithCropsDto>? mediaWithCropsDtos) is false)
if (mediaPickerConfiguration.Multiple is false && mediaWithCropsDtos.Count > 1)
{
return validationResults;
validationResults.Add(new ValidationResult(
_localizedTextService.Localize("validation", "multipleMediaNotAllowed"),
["value"]));
}
if (mediaPickerConfiguration.ValidationLimit.Min is not null
@@ -365,8 +342,9 @@ public class MediaPicker3PropertyEditor : DataEditor
_localizedTextService.Localize(
"validation",
"entriesShort",
new[] { mediaPickerConfiguration.ValidationLimit.Min.ToString(), (mediaPickerConfiguration.ValidationLimit.Min - mediaWithCropsDtos.Count).ToString(), }),
new[] { "validationLimit" }));
[mediaPickerConfiguration.ValidationLimit.Min.ToString(), (mediaPickerConfiguration.ValidationLimit.Min - mediaWithCropsDtos.Count).ToString()
]),
["value"]));
}
if (mediaPickerConfiguration.ValidationLimit.Max is not null
@@ -376,12 +354,141 @@ public class MediaPicker3PropertyEditor : DataEditor
_localizedTextService.Localize(
"validation",
"entriesExceed",
new[] { mediaPickerConfiguration.ValidationLimit.Max.ToString(), (mediaWithCropsDtos.Count - mediaPickerConfiguration.ValidationLimit.Max).ToString(), }),
new[] { "validationLimit" }));
[mediaPickerConfiguration.ValidationLimit.Max.ToString(), (mediaWithCropsDtos.Count - mediaPickerConfiguration.ValidationLimit.Max).ToString()
]),
["value"]));
}
return validationResults;
}
}
internal class AllowedTypeValidator : ITypedJsonValidator<List<MediaWithCropsDto>, MediaPicker3Configuration>
{
private readonly ILocalizedTextService _localizedTextService;
private readonly IMediaTypeService _mediaTypeService;
public AllowedTypeValidator(ILocalizedTextService localizedTextService, IMediaTypeService mediaTypeService)
{
_localizedTextService = localizedTextService;
_mediaTypeService = mediaTypeService;
}
public IEnumerable<ValidationResult> Validate(
List<MediaWithCropsDto>? value,
MediaPicker3Configuration? configuration,
string? valueType,
PropertyValidationContext validationContext)
{
if (value is null || configuration is null)
{
return [];
}
var allowedTypes = configuration.Filter?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries);
// No allowed types = all types are allowed
if (allowedTypes is null || allowedTypes.Length == 0)
{
return [];
}
IEnumerable<string> distinctTypeAliases = value.DistinctBy(x => x.MediaTypeAlias).Select(x => x.MediaTypeAlias);
foreach (var typeAlias in distinctTypeAliases)
{
IMediaType? type = _mediaTypeService.Get(typeAlias);
if (type is null || allowedTypes.Contains(type.Key.ToString()) is false)
{
return
[
new ValidationResult(
_localizedTextService.Localize("validation", "invalidMediaType"),
["value"])
];
}
}
return [];
}
}
internal class StartNodeValidator : ITypedJsonValidator<List<MediaWithCropsDto>, MediaPicker3Configuration>
{
private readonly ILocalizedTextService _localizedTextService;
private readonly IMediaNavigationQueryService _mediaNavigationQueryService;
public StartNodeValidator(
ILocalizedTextService localizedTextService,
IMediaNavigationQueryService mediaNavigationQueryService)
{
_localizedTextService = localizedTextService;
_mediaNavigationQueryService = mediaNavigationQueryService;
}
public IEnumerable<ValidationResult> Validate(
List<MediaWithCropsDto>? value,
MediaPicker3Configuration? configuration,
string? valueType,
PropertyValidationContext validationContext)
{
if (value is null || configuration?.StartNodeId is null)
{
return [];
}
List<Guid> uniqueParentKeys = [];
foreach (Guid distinctMediaKey in value.DistinctBy(x => x.MediaKey).Select(x => x.MediaKey))
{
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 [];
}
}
}
}

View File

@@ -12,6 +12,7 @@ using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Cms.Infrastructure.Serialization;
@@ -37,7 +38,9 @@ public class DataValueReferenceFactoryCollectionTests
Mock.Of<IScopeProvider>(),
Mock.Of<IBackOfficeSecurityAccessor>(),
Mock.Of<IDataTypeConfigurationCache>(),
Mock.Of<ILocalizedTextService>()));
Mock.Of<ILocalizedTextService>(),
Mock.Of<IMediaTypeService>(),
Mock.Of<IMediaNavigationQueryService>()));
private IIOHelper IOHelper { get; } = Mock.Of<IIOHelper>();

View File

@@ -0,0 +1,215 @@
using System.ComponentModel.DataAnnotations;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Cms.Infrastructure.Serialization;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors;
[TestFixture]
internal class MediaPicker3ValueEditorValidationTests
{
[TestCase(true, true)]
[TestCase(false, false)]
public void Validates_Start_Node_Immediate_Parent(bool shouldSucceed, bool hasValidParentKey)
{
var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor();
Guid? validParentKey = Guid.NewGuid();
var mediaKey = Guid.NewGuid();
if (hasValidParentKey)
{
mediaNavigationQueryServiceMock.Setup(x => x.TryGetParentKey(mediaKey, out validParentKey)).Returns(true);
}
else
{
Guid? invalidParentKey = Guid.NewGuid();
mediaNavigationQueryServiceMock.Setup(x => x.TryGetParentKey(mediaKey, out invalidParentKey)).Returns(true);
}
valueEditor.ConfigurationObject = new MediaPicker3Configuration { StartNodeId = validParentKey };
var value = "[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"" + mediaKey + "\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]";
var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty());
ValidateResult(shouldSucceed, result);
}
[Test]
public void Validates_Start_Node_Parent_Not_Found()
{
var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor();
Guid? parentKey = null;
var mediaKey = Guid.NewGuid();
mediaNavigationQueryServiceMock.Setup(x => x.TryGetParentKey(mediaKey, out parentKey)).Returns(true);
valueEditor.ConfigurationObject = new MediaPicker3Configuration { StartNodeId = Guid.NewGuid() };
var value = "[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"" + mediaKey + "\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]";
var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty());
ValidateResult(false, result);
}
[Test]
[TestCase(true, true, true)]
[TestCase(false, false, true)]
[TestCase(false, true, false)]
public void Validates_Start_Node_Ancestor(bool shouldSucceed, bool findsAncestor, bool hasValidAncestorKey)
{
var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor();
Guid ancestorKey = Guid.NewGuid();
Guid? parentKey = Guid.NewGuid();
var mediaKey = Guid.NewGuid();
IEnumerable<Guid> ancestorKeys = findsAncestor is false ? [] : hasValidAncestorKey ? [ancestorKey] : [Guid.NewGuid()];
mediaNavigationQueryServiceMock.Setup(x => x.TryGetParentKey(mediaKey, out parentKey)).Returns(true);
mediaNavigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(parentKey.Value, out ancestorKeys)).Returns(true);
valueEditor.ConfigurationObject = new MediaPicker3Configuration { StartNodeId = ancestorKey };
var value = "[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"" + mediaKey + "\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]";
var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty());
ValidateResult(shouldSucceed, result);
}
[TestCase(true, true, true)]
[TestCase(false, true, false)]
[TestCase(false, false, true)]
public void Validates_Allowed_Type(bool shouldSucceed, bool hasAllowedType, bool findsMediaType)
{
var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor();
var mediaKey = Guid.NewGuid();
var mediaTypeKey = Guid.NewGuid();
var mediaTypeAlias = "Alias";
valueEditor.ConfigurationObject = new MediaPicker3Configuration() { Filter = $"{mediaTypeKey}" };
var mediaTypeMock = new Mock<IMediaType>();
if (hasAllowedType)
{
mediaTypeMock.Setup(x => x.Key).Returns(mediaTypeKey);
}
else
{
mediaTypeMock.Setup(x => x.Key).Returns(Guid.NewGuid());
}
if (findsMediaType)
{
mediaTypeServiceMock.Setup(x => x.Get(mediaTypeAlias)).Returns(mediaTypeMock.Object);
}
else
{
mediaTypeServiceMock.Setup(x => x.Get(It.IsAny<string>())).Returns((IMediaType)null);
}
var value = "[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"" + mediaKey + "\",\n \"mediaTypeAlias\" : \"" + mediaTypeAlias + "\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]";
var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty());
ValidateResult(shouldSucceed, result);
}
[TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", false, true)]
[TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n}, {\n \" key\" : \"1C70519E-C3AE-4D45-8E48-30B3D02E455E\",\n \"mediaKey\" : \"E243A7E2-8D2E-4DC9-88FB-822350A40142\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", false, false)]
[TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n}, {\n \" key\" : \"1C70519E-C3AE-4D45-8E48-30B3D02E455E\",\n \"mediaKey\" : \"E243A7E2-8D2E-4DC9-88FB-822350A40142\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", true, true)]
[TestCase("[]", true, true)]
[TestCase("[]", false, true)]
public void Validates_Multiple(string value, bool multiple, bool succeed)
{
var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor();
valueEditor.ConfigurationObject = new MediaPicker3Configuration() { Multiple = multiple };
var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty());
ValidateResult(succeed, result);
}
[TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 2, false)]
[TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n}, {\n \" key\" : \"1C70519E-C3AE-4D45-8E48-30B3D02E455E\",\n \"mediaKey\" : \"E243A7E2-8D2E-4DC9-88FB-822350A40142\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 1, true)]
[TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n}, {\n \" key\" : \"1C70519E-C3AE-4D45-8E48-30B3D02E455E\",\n \"mediaKey\" : \"E243A7E2-8D2E-4DC9-88FB-822350A40142\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 2, true)]
[TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n}, {\n \" key\" : \"1C70519E-C3AE-4D45-8E48-30B3D02E455E\",\n \"mediaKey\" : \"E243A7E2-8D2E-4DC9-88FB-822350A40142\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 3, false)]
[TestCase("[]", 1, false)]
[TestCase("[]", 0, true)]
public void Validates_Min_Limit(string value, int min, bool succeed)
{
var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor();
valueEditor.ConfigurationObject = new MediaPicker3Configuration() { Multiple = true, ValidationLimit = new MediaPicker3Configuration.NumberRange { Min = min } };
var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty());
ValidateResult(succeed, result);
}
[TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 1, true)]
[TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 0, false)]
[TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n}, {\n \" key\" : \"1C70519E-C3AE-4D45-8E48-30B3D02E455E\",\n \"mediaKey\" : \"E243A7E2-8D2E-4DC9-88FB-822350A40142\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 1, false)]
[TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n}, {\n \" key\" : \"1C70519E-C3AE-4D45-8E48-30B3D02E455E\",\n \"mediaKey\" : \"E243A7E2-8D2E-4DC9-88FB-822350A40142\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 2, true)]
[TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n}, {\n \" key\" : \"1C70519E-C3AE-4D45-8E48-30B3D02E455E\",\n \"mediaKey\" : \"E243A7E2-8D2E-4DC9-88FB-822350A40142\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 3, true)]
[TestCase("[]", 1, true)]
[TestCase("[]", 0, true)]
public void Validates_Max_Limit(string value, int max, bool succeed)
{
var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor();
valueEditor.ConfigurationObject = new MediaPicker3Configuration() { Multiple = true, ValidationLimit = new MediaPicker3Configuration.NumberRange { Max = max } };
var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty());
ValidateResult(succeed, result);
}
private static void ValidateResult(bool succeed, IEnumerable<ValidationResult> result)
{
if (succeed)
{
Assert.IsEmpty(result);
}
else
{
Assert.That(result.Count(), Is.EqualTo(1));
}
}
private static (MediaPicker3PropertyEditor.MediaPicker3PropertyValueEditor ValueEditor, Mock<IMediaTypeService> MediaTypeServiceMock, Mock<IMediaNavigationQueryService> MediaNavigationQueryServiceMock) CreateValueEditor()
{
var mediaTypeServiceMock = new Mock<IMediaTypeService>();
var mediaNavigationQueryServiceMock = new Mock<IMediaNavigationQueryService>();
var valueEditor = new MediaPicker3PropertyEditor.MediaPicker3PropertyValueEditor(
Mock.Of<IShortStringHelper>(),
new SystemTextJsonSerializer(),
Mock.Of<IIOHelper>(),
new DataEditorAttribute("alias"),
Mock.Of<IMediaImportService>(),
Mock.Of<IMediaService>(),
Mock.Of<ITemporaryFileService>(),
Mock.Of<IScopeProvider>(),
Mock.Of<IBackOfficeSecurityAccessor>(),
Mock.Of<IDataTypeConfigurationCache>(),
Mock.Of<ILocalizedTextService>(),
mediaTypeServiceMock.Object,
mediaNavigationQueryServiceMock.Object)
{
ConfigurationObject = new MediaPicker3Configuration()
};
return (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock);
}
}