diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentBuilder.cs b/src/Umbraco.Core/DeliveryApi/ApiContentBuilder.cs index 64c1029b02..f362344390 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiContentBuilder.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiContentBuilder.cs @@ -7,8 +7,6 @@ namespace Umbraco.Cms.Core.DeliveryApi; public sealed class ApiContentBuilder : ApiContentBuilderBase, IApiContentBuilder { - private readonly IVariationContextAccessor _variationContextAccessor; - [Obsolete("Please use the constructor that takes an IVariationContextAccessor instead. Scheduled for removal in V17.")] public ApiContentBuilder( IApiContentNameProvider apiContentNameProvider, @@ -27,9 +25,10 @@ public sealed class ApiContentBuilder : ApiContentBuilderBase, IApi IApiContentRouteBuilder apiContentRouteBuilder, IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor, IVariationContextAccessor variationContextAccessor) - : base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor) - => _variationContextAccessor = variationContextAccessor; + : base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor, variationContextAccessor) + { + } protected override IApiContent Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary properties) - => new ApiContent(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(_variationContextAccessor), route, properties); + => new ApiContent(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(VariationContextAccessor), route, properties); } diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentBuilderBase.cs b/src/Umbraco.Core/DeliveryApi/ApiContentBuilderBase.cs index 8ffcd6d849..209ce4e33e 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiContentBuilderBase.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiContentBuilderBase.cs @@ -1,5 +1,8 @@ -using Umbraco.Cms.Core.Models.DeliveryApi; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.DeliveryApi; @@ -7,26 +10,63 @@ public abstract class ApiContentBuilderBase where T : IApiContent { private readonly IApiContentNameProvider _apiContentNameProvider; - private readonly IApiContentRouteBuilder _apiContentRouteBuilder; private readonly IOutputExpansionStrategyAccessor _outputExpansionStrategyAccessor; - protected ApiContentBuilderBase(IApiContentNameProvider apiContentNameProvider, IApiContentRouteBuilder apiContentRouteBuilder, IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor) + [Obsolete("Please use the constructor that takes all parameters. Scheduled for removal in Umbraco 17.")] + protected ApiContentBuilderBase( + IApiContentNameProvider apiContentNameProvider, + IApiContentRouteBuilder apiContentRouteBuilder, + IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor) + : this( + apiContentNameProvider, + apiContentRouteBuilder, + outputExpansionStrategyAccessor, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + protected ApiContentBuilderBase( + IApiContentNameProvider apiContentNameProvider, + IApiContentRouteBuilder apiContentRouteBuilder, + IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor, + IVariationContextAccessor variationContextAccessor) { _apiContentNameProvider = apiContentNameProvider; - _apiContentRouteBuilder = apiContentRouteBuilder; + ApiContentRouteBuilder = apiContentRouteBuilder; _outputExpansionStrategyAccessor = outputExpansionStrategyAccessor; + VariationContextAccessor = variationContextAccessor; } + protected IApiContentRouteBuilder ApiContentRouteBuilder { get; } + + protected IVariationContextAccessor VariationContextAccessor { get; } + protected abstract T Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary properties); public virtual T? Build(IPublishedContent content) { - IApiContentRoute? route = _apiContentRouteBuilder.Build(content); + IApiContentRoute? route = ApiContentRouteBuilder.Build(content); if (route is null) { return default; } + // If a segment is requested and no segmented properties have any values, we consider the segment as not created or non-existing and return null. + // This aligns the behaviour of the API when it comes to "Accept-Segment" and "Accept-Language" requests, so 404 is returned for both when + // the segment or language is not created or does not exist. + // It also aligns with what we show in the backoffice for whether a segment is "Published" or "Not created". + // Requested languages that aren't created or don't exist will already have exited early in the route builder. + var segment = VariationContextAccessor.VariationContext?.Segment; + if (segment.IsNullOrWhiteSpace() is false + && content.ContentType.VariesBySegment() + && content + .Properties + .Where(p => p.PropertyType.VariesBySegment()) + .All(p => p.HasValue(VariationContextAccessor.VariationContext?.Culture, segment) is false)) + { + return default; + } + IDictionary properties = _outputExpansionStrategyAccessor.TryGetValue(out IOutputExpansionStrategy? outputExpansionStrategy) ? outputExpansionStrategy.MapContentProperties(content) diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentResponseBuilder.cs b/src/Umbraco.Core/DeliveryApi/ApiContentResponseBuilder.cs index 833d7f2016..17d6271cb5 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiContentResponseBuilder.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiContentResponseBuilder.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; @@ -7,9 +7,6 @@ namespace Umbraco.Cms.Core.DeliveryApi; public class ApiContentResponseBuilder : ApiContentBuilderBase, IApiContentResponseBuilder { - private readonly IApiContentRouteBuilder _apiContentRouteBuilder; - private readonly IVariationContextAccessor _variationContextAccessor; - [Obsolete("Please use the constructor that takes an IVariationContextAccessor instead. Scheduled for removal in V17.")] public ApiContentResponseBuilder( IApiContentNameProvider apiContentNameProvider, @@ -28,16 +25,14 @@ public class ApiContentResponseBuilder : ApiContentBuilderBase properties) { IDictionary cultures = GetCultures(content); - return new ApiContentResponse(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(_variationContextAccessor), route, properties, cultures); + return new ApiContentResponse(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(VariationContextAccessor), route, properties, cultures); } protected virtual IDictionary GetCultures(IPublishedContent content) @@ -52,7 +47,7 @@ public class ApiContentResponseBuilder : ApiContentBuilderBase(); + + var sharedPropertyValueConverter = new Mock(); + sharedPropertyValueConverter.Setup(p => p.IsValue(It.IsAny(), It.IsAny())).Returns(sharedValue is not null); + sharedPropertyValueConverter.Setup(p => p.ConvertIntermediateToDeliveryApiObject( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).Returns(sharedValue); + sharedPropertyValueConverter.Setup(p => p.IsConverter(It.IsAny())).Returns(true); + sharedPropertyValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); + sharedPropertyValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); + + var sharedPropertyType = SetupPublishedPropertyType(sharedPropertyValueConverter.Object, "sharedMessage", "Umbraco.Textstring"); + var sharedProperty = new PublishedElementPropertyBase(sharedPropertyType, content.Object, false, PropertyCacheLevel.None, new VariationContext(), Mock.Of()); + + var segmentedPropertyValueConverter = new Mock(); + segmentedPropertyValueConverter.Setup(p => p.IsValue(It.IsAny(), It.IsAny())).Returns(segmentedValue is not null); + segmentedPropertyValueConverter.Setup(p => p.ConvertIntermediateToDeliveryApiObject( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).Returns(segmentedValue); + segmentedPropertyValueConverter.Setup(p => p.IsConverter(It.IsAny())).Returns(true); + segmentedPropertyValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); + segmentedPropertyValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); + + var segmentedPropertyType = SetupPublishedPropertyType(segmentedPropertyValueConverter.Object, "segmentedMessage", "Umbraco.Textstring", contentVariation: ContentVariation.Segment); + var segmentedProperty = new PublishedElementPropertyBase(segmentedPropertyType, content.Object, false, PropertyCacheLevel.None, new VariationContext(), Mock.Of()); + + var contentType = new Mock(); + contentType.SetupGet(c => c.Alias).Returns("thePageType"); + contentType.SetupGet(c => c.ItemType).Returns(PublishedItemType.Content); + contentType.SetupGet(c => c.Variations).Returns(ContentVariation.Segment); + + var key = Guid.NewGuid(); + var urlSegment = "url-segment"; + var name = "The page"; + ConfigurePublishedContentMock(content, key, name, urlSegment, contentType.Object, [sharedProperty, segmentedProperty]); + + var routeBuilderMock = new Mock(); + routeBuilderMock + .Setup(r => r.Build(content.Object, It.IsAny())) + .Returns(new ApiContentRoute(content.Object.UrlSegment!, new ApiContentStartItem(Guid.NewGuid(), "/"))); + + var variationContextAccessorMock = new Mock(); + variationContextAccessorMock.Setup(v => v.VariationContext).Returns(new VariationContext(segment: "missingSegment")); + + var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilderMock.Object, CreateOutputExpansionStrategyAccessor(), variationContextAccessorMock.Object); + var result = builder.Build(content.Object); + + if (expectNull) + { + Assert.IsNull(result); + } + else + { + Assert.IsNotNull(result); + } + } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs index 3c5b6d8ca0..c165666cc3 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs @@ -73,7 +73,12 @@ public class DeliveryApiTests PublishStatusQueryService = publishStatusQueryService.Object; } - protected IPublishedPropertyType SetupPublishedPropertyType(IPropertyValueConverter valueConverter, string propertyTypeAlias, string editorAlias, object? dataTypeConfiguration = null) + protected IPublishedPropertyType SetupPublishedPropertyType( + IPropertyValueConverter valueConverter, + string propertyTypeAlias, + string editorAlias, + object? dataTypeConfiguration = null, + ContentVariation contentVariation = ContentVariation.Nothing) { var mockPublishedContentTypeFactory = new Mock(); mockPublishedContentTypeFactory.Setup(x => x.GetDataType(It.IsAny())) @@ -83,7 +88,7 @@ public class DeliveryApiTests propertyTypeAlias, 123, true, - ContentVariation.Nothing, + contentVariation, new PropertyValueConverterCollection(() => new[] { valueConverter }), Mock.Of(), mockPublishedContentTypeFactory.Object);