Merge branch 'main' into v17/dev

# Conflicts:
#	src/Umbraco.Core/Services/PropertyValidationService.cs
#	src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs
#	src/Umbraco.Infrastructure/PublishedContentQuery.cs
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts
#	src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts
#	templates/UmbracoProject/.template.config/template.json
#	version.json
This commit is contained in:
Andy Butland
2025-09-24 23:31:14 +02:00
265 changed files with 5961 additions and 1546 deletions

View File

@@ -26,8 +26,10 @@ public class DecimalPropertyValueEditorTests
{ 123, 123m },
{ -123, -123m },
{ 123.45d, 123.45m },
{ 123.45f, 123.45m },
{ "123.45", 123.45m },
{ "1234.56", 1234.56m },
{ "1,234.56", 1234.56m },
{ "123,45", 12345m },
{ "1.234,56", null },
{ "123 45", null },
@@ -49,6 +51,18 @@ public class DecimalPropertyValueEditorTests
}
}
[SetCulture("it-IT")]
[SetUICulture("it-IT")]
[TestCase("123,45", 123.45)]
[TestCase("1.234,56", 1234.56)]
[TestCase("123.45", 12345)]
[TestCase("1,234.56", null)]
public void Can_Parse_Values_From_Editor_Using_Culture_With_Non_EnUs_Decimal_Separator(object value, decimal? expected)
{
var fromEditor = FromEditor(value);
Assert.AreEqual(expected, fromEditor);
}
[Test]
public void Can_Parse_Values_To_Editor()
{

View File

@@ -128,6 +128,7 @@ public class MultiValuePropertyEditorTests
Assert.AreEqual("Item 3", result.Items[2]);
}
[TestCase("", true, "")]
[TestCase("Red", true, "")]
[TestCase("Yellow", false, "notOneOfOptions")]
[TestCase("Red,Green", true, "")]

View File

@@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Core.Templates;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Tests.Common;
using Umbraco.Cms.Tests.UnitTests.TestHelpers.Objects;
@@ -216,18 +217,204 @@ public class HtmlLocalLinkParserTests
var umbracoContextAccessor = new TestUmbracoContextAccessor();
var umbracoContextFactory = TestUmbracoContextFactory.Create(
umbracoContextAccessor: umbracoContextAccessor);
using (var reference = umbracoContextFactory.EnsureUmbracoContext())
{
var contentCache = Mock.Get(reference.UmbracoContext.Content);
contentCache.Setup(x => x.GetById(It.IsAny<int>())).Returns(publishedContent.Object);
contentCache.Setup(x => x.GetById(It.IsAny<Guid>())).Returns(publishedContent.Object);
var mediaCache = Mock.Get(reference.UmbracoContext.Media);
mediaCache.Setup(x => x.GetById(It.IsAny<int>())).Returns(media.Object);
mediaCache.Setup(x => x.GetById(It.IsAny<Guid>())).Returns(media.Object);
var publishedUrlProvider = CreatePublishedUrlProvider(
contentUrlProvider,
mediaUrlProvider,
umbracoContextAccessor);
var linkParser = new HtmlLocalLinkParser(publishedUrlProvider);
var output = linkParser.EnsureInternalLinks(input);
Assert.AreEqual(result, output);
}
}
[Test]
public void ParseLocalLinks_WithUrlMode_RespectsUrlMode()
{
// Arrange
var input = "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world";
// Setup content URL provider that returns different URLs based on UrlMode
var contentUrlProvider = new Mock<IUrlProvider>();
contentUrlProvider
.Setup(x => x.GetUrl(
It.IsAny<IPublishedContent>(),
UrlMode.Relative,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/relative-url"));
contentUrlProvider
.Setup(x => x.GetUrl(
It.IsAny<IPublishedContent>(),
UrlMode.Absolute,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("http://example.com/absolute-url"));
var contentType = new PublishedContentType(
Guid.NewGuid(),
666,
"alias",
PublishedItemType.Content,
Enumerable.Empty<string>(),
Enumerable.Empty<PublishedPropertyType>(),
ContentVariation.Nothing);
var publishedContent = new Mock<IPublishedContent>();
publishedContent.Setup(x => x.Id).Returns(1234);
publishedContent.Setup(x => x.ContentType).Returns(contentType);
var umbracoContextAccessor = new TestUmbracoContextAccessor();
var umbracoContextFactory = TestUmbracoContextFactory.Create(
umbracoContextAccessor: umbracoContextAccessor);
var webRoutingSettings = new WebRoutingSettings();
var navigationQueryService = new Mock<IDocumentNavigationQueryService>();
// Guid? parentKey = null;
// navigationQueryService.Setup(x => x.TryGetParentKey(It.IsAny<Guid>(), out parentKey)).Returns(true);
IEnumerable<Guid> ancestorKeys = [];
navigationQueryService.Setup(x => x.TryGetAncestorsKeys(It.IsAny<Guid>(), out ancestorKeys)).Returns(true);
var publishedUrlProvider = CreatePublishedUrlProvider(
contentUrlProvider,
new Mock<IMediaUrlProvider>(),
umbracoContextAccessor);
var publishedContentStatusFilteringService = new Mock<IPublishedContentStatusFilteringService>();
using (var reference = umbracoContextFactory.EnsureUmbracoContext())
{
var contentCache = Mock.Get(reference.UmbracoContext.Content);
contentCache.Setup(x => x.GetById(It.IsAny<Guid>())).Returns(publishedContent.Object);
var linkParser = new HtmlLocalLinkParser(publishedUrlProvider);
// Act
var relativeOutput = linkParser.EnsureInternalLinks(input, UrlMode.Relative);
var absoluteOutput = linkParser.EnsureInternalLinks(input, UrlMode.Absolute);
// Assert
Assert.AreEqual("hello href=\"/relative-url\" world", relativeOutput);
Assert.AreEqual("hello href=\"http://example.com/absolute-url\" world", absoluteOutput);
}
}
[TestCase(UrlMode.Default, "hello href=\"{localLink:1234}\" world ", "hello href=\"/relative-url\" world ")]
[TestCase(UrlMode.Relative, "hello href=\"{localLink:1234}\" world ", "hello href=\"/relative-url\" world ")]
[TestCase(UrlMode.Absolute, "hello href=\"{localLink:1234}\" world ", "hello href=\"https://example.com/absolute-url\" world ")]
[TestCase(UrlMode.Auto, "hello href=\"{localLink:1234}\" world ", "hello href=\"/relative-url\" world ")]
[TestCase(UrlMode.Default, "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/relative-url\" world ")]
[TestCase(UrlMode.Relative, "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/relative-url\" world ")]
[TestCase(UrlMode.Absolute, "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"https://example.com/absolute-url\" world ")]
[TestCase(UrlMode.Auto, "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/relative-url\" world ")]
[TestCase(UrlMode.Default, "hello href=\"{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/media/relative/image.jpg\" world ")]
[TestCase(UrlMode.Relative, "hello href=\"{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/media/relative/image.jpg\" world ")]
[TestCase(UrlMode.Absolute, "hello href=\"{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"https://example.com/media/absolute/image.jpg\" world ")]
[TestCase(UrlMode.Auto, "hello href=\"{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/media/relative/image.jpg\" world ")]
public void ParseLocalLinks_WithVariousUrlModes_ReturnsCorrectUrls(UrlMode urlMode, string input, string expectedResult)
{
// Setup content URL provider that returns different URLs based on UrlMode
var contentUrlProvider = new Mock<IUrlProvider>();
contentUrlProvider
.Setup(x => x.GetUrl(
It.IsAny<IPublishedContent>(),
UrlMode.Default,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/relative-url"));
contentUrlProvider
.Setup(x => x.GetUrl(
It.IsAny<IPublishedContent>(),
UrlMode.Relative,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/relative-url"));
contentUrlProvider
.Setup(x => x.GetUrl(
It.IsAny<IPublishedContent>(),
UrlMode.Absolute,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("https://example.com/absolute-url"));
contentUrlProvider
.Setup(x => x.GetUrl(
It.IsAny<IPublishedContent>(),
UrlMode.Auto,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/relative-url"));
var contentType = new PublishedContentType(
Guid.NewGuid(),
666,
"alias",
PublishedItemType.Content,
Enumerable.Empty<string>(),
Enumerable.Empty<PublishedPropertyType>(),
ContentVariation.Nothing);
var publishedContent = new Mock<IPublishedContent>();
publishedContent.Setup(x => x.Id).Returns(1234);
publishedContent.Setup(x => x.ContentType).Returns(contentType);
// Setup media URL provider that returns different URLs based on UrlMode
var mediaUrlProvider = new Mock<IMediaUrlProvider>();
mediaUrlProvider.Setup(x => x.GetMediaUrl(
It.IsAny<IPublishedContent>(),
It.IsAny<string>(),
UrlMode.Default,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/media/relative/image.jpg"));
mediaUrlProvider.Setup(x => x.GetMediaUrl(
It.IsAny<IPublishedContent>(),
It.IsAny<string>(),
UrlMode.Relative,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/media/relative/image.jpg"));
mediaUrlProvider.Setup(x => x.GetMediaUrl(
It.IsAny<IPublishedContent>(),
It.IsAny<string>(),
UrlMode.Absolute,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("https://example.com/media/absolute/image.jpg"));
mediaUrlProvider.Setup(x => x.GetMediaUrl(
It.IsAny<IPublishedContent>(),
It.IsAny<string>(),
UrlMode.Auto,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/media/relative/image.jpg"));
var mediaType = new PublishedContentType(
Guid.NewGuid(),
777,
"image",
PublishedItemType.Media,
Enumerable.Empty<string>(),
Enumerable.Empty<PublishedPropertyType>(),
ContentVariation.Nothing);
var media = new Mock<IPublishedContent>();
media.Setup(x => x.ContentType).Returns(mediaType);
var umbracoContextAccessor = new TestUmbracoContextAccessor();
var umbracoContextFactory = TestUmbracoContextFactory.Create(
umbracoContextAccessor: umbracoContextAccessor);
var webRoutingSettings = new WebRoutingSettings();
var publishedUrlProvider = CreatePublishedUrlProvider(
contentUrlProvider,
mediaUrlProvider,
umbracoContextAccessor);
using (var reference = umbracoContextFactory.EnsureUmbracoContext())
{
@@ -239,25 +426,35 @@ public class HtmlLocalLinkParserTests
mediaCache.Setup(x => x.GetById(It.IsAny<int>())).Returns(media.Object);
mediaCache.Setup(x => x.GetById(It.IsAny<Guid>())).Returns(media.Object);
var publishStatusQueryService = new Mock<IPublishStatusQueryService>();
publishStatusQueryService
.Setup(x => x.IsDocumentPublished(It.IsAny<Guid>(), It.IsAny<string>()))
.Returns(true);
var publishedUrlProvider = new UrlProvider(
umbracoContextAccessor,
Options.Create(webRoutingSettings),
new UrlProviderCollection(() => new[] { contentUrlProvider.Object }),
new MediaUrlProviderCollection(() => new[] { mediaUrlProvider.Object }),
Mock.Of<IVariationContextAccessor>(),
navigationQueryService.Object,
publishedContentStatusFilteringService.Object);
var linkParser = new HtmlLocalLinkParser(publishedUrlProvider);
var output = linkParser.EnsureInternalLinks(input);
var output = linkParser.EnsureInternalLinks(input, urlMode);
Assert.AreEqual(result, output);
Assert.AreEqual(expectedResult, output);
}
}
private static UrlProvider CreatePublishedUrlProvider(
Mock<IUrlProvider> contentUrlProvider,
Mock<IMediaUrlProvider> mediaUrlProvider,
TestUmbracoContextAccessor umbracoContextAccessor)
{
var navigationQueryService = new Mock<IDocumentNavigationQueryService>();
IEnumerable<Guid> ancestorKeys = [];
navigationQueryService.Setup(x => x.TryGetAncestorsKeys(It.IsAny<Guid>(), out ancestorKeys)).Returns(true);
var publishStatusQueryService = new Mock<IPublishStatusQueryService>();
publishStatusQueryService
.Setup(x => x.IsDocumentPublished(It.IsAny<Guid>(), It.IsAny<string>()))
.Returns(true);
return new UrlProvider(
umbracoContextAccessor,
Options.Create(new WebRoutingSettings()),
new UrlProviderCollection(() => new[] { contentUrlProvider.Object }),
new MediaUrlProviderCollection(() => new[] { mediaUrlProvider.Object }),
Mock.Of<IVariationContextAccessor>(),
navigationQueryService.Object,
new Mock<IPublishedContentStatusFilteringService>().Object);
}
}

View File

@@ -10,6 +10,7 @@ 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;
@@ -27,16 +28,14 @@ public class PropertyValidationServiceTests
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);
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);
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),
@@ -44,7 +43,15 @@ public class PropertyValidationServiceTests
new SystemTextJsonSerializer(new DefaultJsonSerializerEncoderFactory()),
Mock.Of<IIOHelper>()));
var propEditors = new PropertyEditorCollection(new DataEditorCollection(() => new[] { dataEditor }));
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,
@@ -52,8 +59,8 @@ public class PropertyValidationServiceTests
Mock.Of<ILocalizedTextService>(),
new ValueEditorCache(),
Mock.Of<ICultureDictionary>(),
Mock.Of<ILanguageService>(),
Mock.Of<IOptions<ContentSettings>>());
languageService.Object,
contentSettings.Object);
}
[Test]
@@ -279,6 +286,23 @@ public class PropertyValidationServiceTests
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