Files
Umbraco-CMS/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs
Laura Neto fd0ccc529b Property Editors: Improve Missing Property Editor UI and allow save/publish (#20104)
* Initial implementation of non existing property editor

* Adjust `MissingPropertyEditor` to not require registering in PropertyEditorCollection

* Add `MissingPropertyEditor.name` back

* Remove unused dependencies from DataTypeService

* Removed reference to non existing property

* Add parameterless constructor back to MissingPropertyEditor

* Add validation error on document open to property with missing editor

* Update labels

* Removed public editor alias const

* Update src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/manifests.ts

* Add test that checks whether the new MissingPropertyEditor is returned when an editor is not found

* Also check if the editor UI alias is correct in the test

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Share property editor instances between properties

* Only store missing property editors in memory in `ContentMapDefinition.MapValueViewModels()`

* Add value converter for the missing property editor to always return a string (same as the Label did previously)

* Small improvements to code block

* Adjust property validation to accept missing property editors

* Return the current value when trying to update a property with a missing editor

Same logic as for when the property is readonly.

* Fix failing unit tests

* Small fix

* Add unit test

* Remove client validation

* UI adjustments

* Adjustments from code review

* Adjust test

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-18 08:55:58 +02:00

324 lines
12 KiB
C#

// Copyright (c) Umbraco.
// See LICENSE for more details.
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Dictionary;
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.PropertyEditors.Validators;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Serialization;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Services;
[TestFixture]
public class PropertyValidationServiceTests
{
private IShortStringHelper ShortStringHelper => new DefaultShortStringHelper(new DefaultShortStringHelperConfig());
private void MockObjects(out PropertyValidationService validationService, out IDataType dt)
{
var dataTypeService = new Mock<IDataTypeService>();
var dataType = Mock.Of<IDataType>(x => x.ConfigurationObject == string.Empty // irrelevant but needs a value
&& x.DatabaseType == ValueStorageType.Nvarchar
&& x.EditorAlias == Constants.PropertyEditors.Aliases.TextBox);
dataTypeService.Setup(x => x.GetDataType(It.IsAny<int>())).Returns(() => dataType);
dt = dataType;
// new data editor that returns a TextOnlyValueEditor which will do the validation for the properties
var dataEditor = Mock.Of<IDataEditor>(x => x.Alias == Constants.PropertyEditors.Aliases.TextBox);
Mock.Get(dataEditor).Setup(x => x.GetValueEditor(It.IsAny<object>()))
.Returns(new CustomTextOnlyValueEditor(
new DataEditorAttribute(Constants.PropertyEditors.Aliases.TextBox),
Mock.Of<IShortStringHelper>(),
new SystemTextJsonSerializer(new DefaultJsonSerializerEncoderFactory()),
Mock.Of<IIOHelper>()));
var languageService = new Mock<ILanguageService>();
languageService
.Setup(s => s.GetDefaultIsoCodeAsync())
.ReturnsAsync(() => "en-US");
var propEditors = new PropertyEditorCollection(new DataEditorCollection(() => [dataEditor]));
var contentSettings = new Mock<IOptions<ContentSettings>>();
contentSettings.Setup(x => x.Value).Returns(new ContentSettings());
validationService = new PropertyValidationService(
propEditors,
dataTypeService.Object,
Mock.Of<ILocalizedTextService>(),
new ValueEditorCache(),
Mock.Of<ICultureDictionary>(),
languageService.Object,
contentSettings.Object);
}
[Test]
public void Validate_Invariant_Properties_On_Variant_Default_Culture()
{
MockObjects(out var validationService, out var dataType);
var p1 = new Property(
new PropertyType(ShortStringHelper, dataType, "test1")
{
Mandatory = true,
Variations = ContentVariation.Culture,
});
p1.SetValue("Hello", "en-US");
var p2 = new Property(
new PropertyType(ShortStringHelper, dataType, "test2")
{
Mandatory = true,
Variations = ContentVariation.Nothing,
});
p2.SetValue("Hello");
var p3 = new Property(
new PropertyType(ShortStringHelper, dataType, "test3")
{
Mandatory = true,
Variations = ContentVariation.Culture,
});
p3.SetValue(null, "en-US"); // invalid
var p4 = new Property(
new PropertyType(ShortStringHelper, dataType, "test4")
{
Mandatory = true,
Variations = ContentVariation.Nothing,
});
p4.SetValue(null); // invalid
var content = Mock.Of<IContent>(
x => x.Published == true // set to published, the default culture will validate invariant anyways
&& x.Properties == new PropertyCollection(new[] { p1, p2, p3, p4 }));
var result =
validationService.IsPropertyDataValid(content, out var invalid, CultureImpact.Explicit("en-US", true));
Assert.IsFalse(result);
Assert.AreEqual(2, invalid.Length);
}
[Test]
public void Validate_Invariant_Properties_On_Variant_Non_Default_Culture()
{
MockObjects(out var validationService, out var dataType);
var p1 = new Property(
new PropertyType(ShortStringHelper, dataType, "test1")
{
Mandatory = true,
Variations = ContentVariation.Culture,
});
p1.SetValue("Hello", "en-US");
var p2 = new Property(
new PropertyType(ShortStringHelper, dataType, "test2")
{
Mandatory = true,
Variations = ContentVariation.Nothing,
});
p2.SetValue("Hello");
var p3 = new Property(
new PropertyType(ShortStringHelper, dataType, "test3")
{
Mandatory = true,
Variations = ContentVariation.Culture,
});
p3.SetValue(null, "en-US"); // invalid
var p4 = new Property(
new PropertyType(ShortStringHelper, dataType, "test4")
{
Mandatory = true,
Variations = ContentVariation.Nothing,
});
p4.SetValue(null); // invalid
var content = Mock.Of<IContent>(
x =>
x.Published ==
false // set to not published, the non default culture will need to validate invariant too
&& x.Properties == new PropertyCollection(new[] { p1, p2, p3, p4 }));
var result =
validationService.IsPropertyDataValid(content, out var invalid, CultureImpact.Explicit("en-US", false));
Assert.IsFalse(result);
Assert.AreEqual(2, invalid.Length);
}
[Test]
public void Validate_Variant_Properties_On_Variant()
{
MockObjects(out var validationService, out var dataType);
var p1 = new Property(
new PropertyType(ShortStringHelper, dataType, "test1")
{
Mandatory = true,
Variations = ContentVariation.Culture,
});
p1.SetValue(null, "en-US"); // invalid
var p2 = new Property(
new PropertyType(ShortStringHelper, dataType, "test2")
{
Mandatory = true,
Variations = ContentVariation.Nothing,
});
p2.SetValue(null); // invalid
var p3 = new Property(
new PropertyType(ShortStringHelper, dataType, "test3")
{
Mandatory = true,
Variations = ContentVariation.Culture,
});
p3.SetValue(null, "en-US"); // ignored because the impact isn't the default lang + the content is published
var p4 = new Property(
new PropertyType(ShortStringHelper, dataType, "test4")
{
Mandatory = true,
Variations = ContentVariation.Nothing,
});
p4.SetValue(null); // ignored because the impact isn't the default lang + the content is published
var content = Mock.Of<IContent>(
x => x.Published == true // set to published
&& x.Properties == new PropertyCollection(new[] { p1, p2, p3, p4 }));
var result =
validationService.IsPropertyDataValid(content, out var invalid, CultureImpact.Explicit("en-US", false));
Assert.IsFalse(result);
Assert.AreEqual(2, invalid.Length);
}
[Test]
public void Validate_Invariant_Properties_On_Invariant()
{
MockObjects(out var validationService, out var dataType);
var p1 = new Property(
new PropertyType(ShortStringHelper, dataType, "test1")
{
Mandatory = true,
Variations = ContentVariation.Culture,
});
p1.SetValue(null, "en-US"); // ignored since this is variant
var p2 = new Property(
new PropertyType(ShortStringHelper, dataType, "test2")
{
Mandatory = true,
Variations = ContentVariation.Nothing,
});
p2.SetValue(null); // invalid
var p3 = new Property(
new PropertyType(ShortStringHelper, dataType, "test3")
{
Mandatory = true,
Variations = ContentVariation.Culture,
});
p3.SetValue("Hello", "en-US"); // ignored since this is variant
var p4 = new Property(
new PropertyType(ShortStringHelper, dataType, "test4")
{
Mandatory = true,
Variations = ContentVariation.Nothing,
});
p4.SetValue(null); // invalid
var content = Mock.Of<IContent>(
x => x.Properties == new PropertyCollection(new[] { p1, p2, p3, p4 }));
var result = validationService.IsPropertyDataValid(content, out var invalid, CultureImpact.Invariant);
Assert.IsFalse(result);
Assert.AreEqual(2, invalid.Length);
}
[Test]
public void Validate_Properties_On_All()
{
MockObjects(out var validationService, out var dataType);
var p1 = new Property(
new PropertyType(ShortStringHelper, dataType, "test1")
{
Mandatory = true,
Variations = ContentVariation.Culture,
});
p1.SetValue(null, "en-US"); // invalid
var p2 = new Property(
new PropertyType(ShortStringHelper, dataType, "test2")
{
Mandatory = true,
Variations = ContentVariation.Nothing,
});
p2.SetValue(null); // invalid
var p3 = new Property(
new PropertyType(ShortStringHelper, dataType, "test3")
{
Mandatory = true,
Variations = ContentVariation.Culture,
});
p3.SetValue(null, "en-US"); // invalid
var p4 = new Property(
new PropertyType(ShortStringHelper, dataType, "test4")
{
Mandatory = true,
Variations = ContentVariation.Nothing,
});
p4.SetValue(null); // invalid
var content = Mock.Of<IContent>(
x => x.Properties == new PropertyCollection(new[] { p1, p2, p3, p4 }));
var result = validationService.IsPropertyDataValid(content, out var invalid, CultureImpact.All);
Assert.IsFalse(result);
Assert.AreEqual(4, invalid.Length);
}
[TestCase(null)]
[TestCase(24)]
[TestCase("test")]
[TestCase("{\"test\": true}")]
public void ValidatePropertyValue_Always_Returns_No_Validation_Errors_For_Missing_Editor(object? value)
{
MockObjects(out var validationService, out _);
var p1 = new PropertyType(ShortStringHelper, "Missing.Alias", ValueStorageType.Ntext)
{
Variations = ContentVariation.Nothing,
};
var result = validationService.ValidatePropertyValue(p1, value, PropertyValidationContext.Empty());
Assert.AreEqual(0, result.Count());
}
// used so we can inject a mock - we should fix the base class DataValueEditor to be able to have the ILocalizedTextField passed
// in to create the Requried and Regex validators so we aren't using singletons
private class CustomTextOnlyValueEditor : TextOnlyValueEditor
{
public CustomTextOnlyValueEditor(
DataEditorAttribute attribute,
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper)
: base(attribute, Mock.Of<ILocalizedTextService>(), shortStringHelper, jsonSerializer, ioHelper)
{
}
public override IValueRequiredValidator RequiredValidator => new RequiredValidator();
public override IValueFormatValidator FormatValidator => new RegexValidator();
}
}