diff --git a/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs b/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs index 83f063a2b4..0ba4b6873b 100644 --- a/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs +++ b/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs @@ -96,13 +96,23 @@ internal sealed class PublishedProperty : PublishedPropertyBase _content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, _propertyTypeAlias, ref culture, ref segment); var value = GetSourceValue(culture, segment); - var hasValue = PropertyType.IsValue(value, PropertyValueLevel.Source); - if (hasValue.HasValue) + var isValue = PropertyType.IsValue(value, PropertyValueLevel.Source); + if (isValue.HasValue) { - return hasValue.Value; + return isValue.Value; } - return PropertyType.IsValue(GetInterValue(culture, segment), PropertyValueLevel.Object) ?? false; + value = GetInterValue(culture, segment); + isValue = PropertyType.IsValue(value, PropertyValueLevel.Inter); + if (isValue.HasValue) + { + return isValue.Value; + } + + value = GetValue(culture, segment); + isValue = PropertyType.IsValue(value, PropertyValueLevel.Object); + + return isValue ?? false; } public override object? GetSourceValue(string? culture = null, string? segment = null) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/PropertyValueLevelDetectionTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/PropertyValueLevelDetectionTests.cs new file mode 100644 index 0000000000..118f3758dc --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/PropertyValueLevelDetectionTests.cs @@ -0,0 +1,156 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Infrastructure.HybridCache; +using Umbraco.Cms.Infrastructure.HybridCache.Factories; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +internal sealed class PropertyValueLevelDetectionTests : UmbracoIntegrationTestWithContent +{ + private IPublishedContentFactory PublishedContentFactory => GetRequiredService(); + + private IPublishedValueFallback PublishedValueFallback => GetRequiredService(); + + private IPublishedContentTypeFactory PublishedContentTypeFactory => GetRequiredService(); + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + var requestCache = new DictionaryAppCache(); + var appCaches = new AppCaches( + NoAppCache.Instance, + requestCache, + new IsolatedCaches(type => NoAppCache.Instance)); + builder.Services.AddUnique(appCaches); + + builder.PropertyValueConverters() + .Remove() + .Append(); + } + + [TestCase("validSourceLevel", true)] + [TestCase("validInterLevel", true)] + [TestCase("validObjectLevel", true)] + [TestCase("invalidSourceLevel", false)] + [TestCase("invalidInterLevel", false)] + [TestCase("invalidObjectLevel", false)] + [TestCase("nullSourceLevel", false)] + [TestCase("nullInterLevel", false)] + [TestCase("nullObjectLevel", false)] + [TestCase("somethingElse", false)] + public void Can_Detect_Property_Value_At_All_Levels_For_Document(string titleValue, bool expectHasValue) + { + var contentCacheNode = new ContentCacheNode + { + Id = Textpage.Id, + Key = Textpage.Key, + ContentTypeId = Textpage.ContentType.Id, + CreateDate = Textpage.CreateDate, + CreatorId = Textpage.CreatorId, + SortOrder = Textpage.SortOrder, + Data = new ContentData( + Textpage.Name, + "text-page", + Textpage.VersionId, + Textpage.UpdateDate, + Textpage.WriterId, + Textpage.TemplateId, + true, + new Dictionary + { + { + "title", new[] + { + new PropertyData + { + Value = titleValue, + Culture = string.Empty, + Segment = string.Empty, + }, + } + }, + }, + null), + }; + var result = PublishedContentFactory.ToIPublishedContent(contentCacheNode, false); + Assert.IsNotNull(result); + Assert.AreEqual(expectHasValue, result.HasValue("title")); + + // NOTE: the .Value() extensions always end up returning the source value, no matter if the property value + // converter returns false from .HasValue()... this is the case both at property and at content level + var value = result.Value(PublishedValueFallback, "title"); + Assert.AreEqual(titleValue, value); + } + + [TestCase("validSourceLevel", true)] + [TestCase("validInterLevel", true)] + [TestCase("validObjectLevel", true)] + [TestCase("invalidSourceLevel", false)] + [TestCase("invalidInterLevel", false)] + [TestCase("invalidObjectLevel", false)] + [TestCase("nullSourceLevel", false)] + [TestCase("nullInterLevel", false)] + [TestCase("nullObjectLevel", false)] + [TestCase("somethingElse", false)] + public async Task Can_Detect_Property_Value_At_All_Levels_For_Element(string titleValue, bool expectHasValue) + { + var elementType = ContentTypeBuilder.CreateSimpleContentType("umbElement"); + elementType.IsElement = true; + await ContentTypeService.UpdateAsync(elementType, Constants.Security.SuperUserKey); + var publishedElementType = PublishedContentTypeFactory.CreateContentType(elementType); + + var element = new PublishedElement( + publishedElementType, + Guid.NewGuid(), + new Dictionary { { "title", titleValue } }, + false, + new VariationContext()); + + Assert.AreEqual(expectHasValue, element.HasValue("title")); + + // NOTE: the .Value() extensions always end up returning the source value, no matter if the property value + // converter returns false from .HasValue()... this is the case both at property and at content level + var value = element.Value(PublishedValueFallback, "title"); + Assert.AreEqual(titleValue, value); + } + + [HideFromTypeFinder] + public class PropertyValueLevelDetectionTestsConverter : PropertyValueConverterBase + { + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.TextBox; + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.None; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source as string; + + public override bool? IsValue(object? value, PropertyValueLevel level) + => level switch + { + PropertyValueLevel.Source when value?.ToString() is "validSourceLevel" => true, + PropertyValueLevel.Source when value?.ToString() is "invalidSourceLevel" => false, + PropertyValueLevel.Source when value?.ToString() is "nullSourceLevel" => null, + PropertyValueLevel.Inter when value?.ToString() is "validInterLevel" => true, + PropertyValueLevel.Inter when value?.ToString() is "invalidInterLevel" => false, + PropertyValueLevel.Source when value?.ToString() is "nullInternalLevel" => null, + PropertyValueLevel.Object when value?.ToString() is "validObjectLevel" => true, + PropertyValueLevel.Object when value?.ToString() is "invalidObjectLevel" => false, + PropertyValueLevel.Source when value?.ToString() is "nullObjectlLevel" => null, + _ => null, + }; + } +}