Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions src/Umbraco.Core/DeliveryApi/ApiContentBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
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;

public sealed class ApiContentBuilder : ApiContentBuilderBase<IApiContent>, IApiContentBuilder
{
public ApiContentBuilder(IApiContentNameProvider apiContentNameProvider, IApiContentRouteBuilder apiContentRouteBuilder, IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
private readonly IVariationContextAccessor _variationContextAccessor;

[Obsolete("Please use the constructor that takes an IVariationContextAccessor instead. Scheduled for removal in V17.")]
public ApiContentBuilder(
IApiContentNameProvider apiContentNameProvider,
IApiContentRouteBuilder apiContentRouteBuilder,
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
: this(
apiContentNameProvider,
apiContentRouteBuilder,
outputExpansionStrategyAccessor,
StaticServiceProvider.Instance.CreateInstance<IVariationContextAccessor>())
{
}

public ApiContentBuilder(
IApiContentNameProvider apiContentNameProvider,
IApiContentRouteBuilder apiContentRouteBuilder,
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor,
IVariationContextAccessor variationContextAccessor)
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
=> _variationContextAccessor = variationContextAccessor;

protected override IApiContent Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary<string, object?> properties)
=> new ApiContent(content.Key, name, content.ContentType.Alias, content.CreateDate, content.UpdateDate, route, properties);
=> new ApiContent(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(_variationContextAccessor), route, properties);
}
30 changes: 26 additions & 4 deletions src/Umbraco.Core/DeliveryApi/ApiContentResponseBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;

Expand All @@ -7,15 +8,36 @@ namespace Umbraco.Cms.Core.DeliveryApi;
public class ApiContentResponseBuilder : ApiContentBuilderBase<IApiContentResponse>, IApiContentResponseBuilder
{
private readonly IApiContentRouteBuilder _apiContentRouteBuilder;
private readonly IVariationContextAccessor _variationContextAccessor;

public ApiContentResponseBuilder(IApiContentNameProvider apiContentNameProvider, IApiContentRouteBuilder apiContentRouteBuilder, IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
[Obsolete("Please use the constructor that takes an IVariationContextAccessor instead. Scheduled for removal in V17.")]
public ApiContentResponseBuilder(
IApiContentNameProvider apiContentNameProvider,
IApiContentRouteBuilder apiContentRouteBuilder,
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
: this(
apiContentNameProvider,
apiContentRouteBuilder,
outputExpansionStrategyAccessor,
StaticServiceProvider.Instance.CreateInstance<IVariationContextAccessor>())
{
}

public ApiContentResponseBuilder(
IApiContentNameProvider apiContentNameProvider,
IApiContentRouteBuilder apiContentRouteBuilder,
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor,
IVariationContextAccessor variationContextAccessor)
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
=> _apiContentRouteBuilder = apiContentRouteBuilder;
{
_apiContentRouteBuilder = apiContentRouteBuilder;
_variationContextAccessor = variationContextAccessor;
}

protected override IApiContentResponse Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary<string, object?> properties)
{
IDictionary<string, IApiContentRoute> cultures = GetCultures(content);
return new ApiContentResponse(content.Key, name, content.ContentType.Alias, content.CreateDate, content.UpdateDate, route, properties, cultures);
return new ApiContentResponse(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(_variationContextAccessor), route, properties, cultures);
}

protected virtual IDictionary<string, IApiContentRoute> GetCultures(IPublishedContent content)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using Microsoft.AspNetCore.Http;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;

namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.DeliveryApi.Request;

[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
public class ApiContentResponseBuilderTests : UmbracoIntegrationTest
{
private IContentService ContentService => GetRequiredService<IContentService>();

private IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();

private IApiContentResponseBuilder ApiContentResponseBuilder => GetRequiredService<IApiContentResponseBuilder>();

protected IUmbracoContextAccessor UmbracoContextAccessor => GetRequiredService<IUmbracoContextAccessor>();

protected IUmbracoContextFactory UmbracoContextFactory => GetRequiredService<IUmbracoContextFactory>();

protected IVariationContextAccessor VariationContextAccessor => GetRequiredService<IVariationContextAccessor>();

protected override void CustomTestSetup(IUmbracoBuilder builder)
{
builder.AddUmbracoHybridCache();
builder.AddDeliveryApi();
}

[SetUp]
public void SetUpTest()
{
var httpContextAccessor = GetRequiredService<IHttpContextAccessor>();

httpContextAccessor.HttpContext = new DefaultHttpContext
{
Request =
{
Scheme = "https",
Host = new HostString("localhost"),
Path = "/",
QueryString = new QueryString(string.Empty)
},
RequestServices = Services
};
}

[Test]
public async Task ContentBuilder_MapsContentDatesCorrectlyForCultureVariance()
{
await GetRequiredService<ILanguageService>().CreateAsync(new Language("da-DK", "Danish"), Constants.Security.SuperUserKey);

var contentType = new ContentTypeBuilder()
.WithAlias("theContentType")
.WithContentVariation(ContentVariation.Culture)
.Build();
contentType.AllowedAsRoot = true;
await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);

var content = new ContentBuilder()
.WithContentType(contentType)
.WithCultureName("en-US", "Content EN")
.WithCultureName("da-DK", "Content DA")
.Build();
ContentService.Save(content);
ContentService.Publish(content, ["*"]);

Thread.Sleep(200);
content.SetCultureName("Content DA updated", "da-DK");
ContentService.Save(content);
ContentService.Publish(content, ["da-DK"]);

RefreshContentCache();

UmbracoContextAccessor.Clear();
var umbracoContext = UmbracoContextFactory.EnsureUmbracoContext().UmbracoContext;
var publishedContent = umbracoContext.Content.GetById(content.Key);
Assert.IsNotNull(publishedContent);

VariationContextAccessor.VariationContext = new VariationContext(culture: "en-US");
var enResult = ApiContentResponseBuilder.Build(publishedContent);
Assert.IsNotNull(enResult);

VariationContextAccessor.VariationContext = new VariationContext(culture: "da-DK");
var daResult = ApiContentResponseBuilder.Build(publishedContent);
Assert.IsNotNull(daResult);

Assert.GreaterOrEqual((daResult.UpdateDate - enResult.UpdateDate).TotalMilliseconds, 200);
}

private void RefreshContentCache()
=> GetRequiredService<ContentCacheRefresher>().Refresh([new ContentCacheRefresher.JsonPayload { ChangeTypes = TreeChangeTypes.RefreshAll }]);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Tests.Common;

namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi;

Expand Down Expand Up @@ -42,7 +44,7 @@ public void ContentBuilder_MapsContentDataAndPropertiesCorrectly()

var routeBuilder = CreateContentRouteBuilder(apiContentRouteProvider.Object, CreateGlobalSettings(), navigationQueryService: navigationQueryServiceMock.Object);

var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder, CreateOutputExpansionStrategyAccessor());
var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder, CreateOutputExpansionStrategyAccessor(), CreateVariationContextAccessor());
var result = builder.Build(content.Object);

Assert.NotNull(result);
Expand All @@ -57,6 +59,45 @@ public void ContentBuilder_MapsContentDataAndPropertiesCorrectly()
Assert.AreEqual(new DateTime(2023, 07, 12), result.UpdateDate);
}

[TestCase("en-US", "2023-08-04")]
[TestCase("da-DK", "2023-09-08")]
public void ContentBuilder_MapsContentDatesCorrectlyForCultureVariance(string culture, string expectedUpdateDate)
{
var content = new Mock<IPublishedContent>();

var contentType = new Mock<IPublishedContentType>();
contentType.SetupGet(c => c.Alias).Returns("thePageType");
contentType.SetupGet(c => c.ItemType).Returns(PublishedItemType.Content);
contentType.SetupGet(c => c.Variations).Returns(ContentVariation.Culture);

var key = Guid.NewGuid();
var urlSegment = "url-segment";
var name = "The page";
ConfigurePublishedContentMock(content, key, name, urlSegment, contentType.Object, []);
content.SetupGet(c => c.CreateDate).Returns(new DateTime(2023, 07, 02));
content
.SetupGet(c => c.Cultures)
.Returns(new Dictionary<string, PublishedCultureInfo>
{
{ "en-US", new PublishedCultureInfo("en-US", "EN Name", "en-url-segment", new DateTime(2023, 08, 04)) },
{ "da-DK", new PublishedCultureInfo("da-DK", "DA Name", "da-url-segment", new DateTime(2023, 09, 08)) },
});

var routeBuilder = new Mock<IApiContentRouteBuilder>();
routeBuilder
.Setup(r => r.Build(content.Object, It.IsAny<string?>()))
.Returns(new ApiContentRoute(content.Object.UrlSegment!, new ApiContentStartItem(Guid.NewGuid(), "/")));

var variationContextAccessor = new TestVariationContextAccessor { VariationContext = new VariationContext(culture) };

var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder.Object, CreateOutputExpansionStrategyAccessor(), variationContextAccessor);
var result = builder.Build(content.Object);

Assert.NotNull(result);
Assert.AreEqual(new DateTime(2023, 07, 02), result.CreateDate);
Assert.AreEqual(DateTime.Parse(expectedUpdateDate), result.UpdateDate);
}

[Test]
public void ContentBuilder_CanCustomizeContentNameInDeliveryApiOutput()
{
Expand All @@ -75,7 +116,7 @@ public void ContentBuilder_CanCustomizeContentNameInDeliveryApiOutput()
.Setup(r => r.Build(content.Object, It.IsAny<string?>()))
.Returns(new ApiContentRoute(content.Object.UrlSegment!, new ApiContentStartItem(Guid.NewGuid(), "/")));

var builder = new ApiContentBuilder(customNameProvider.Object, routeBuilder.Object, CreateOutputExpansionStrategyAccessor());
var builder = new ApiContentBuilder(customNameProvider.Object, routeBuilder.Object, CreateOutputExpansionStrategyAccessor(), CreateVariationContextAccessor());
var result = builder.Build(content.Object);

Assert.NotNull(result);
Expand All @@ -97,7 +138,7 @@ public void ContentBuilder_ReturnsNullForUnRoutableContent()
.Setup(r => r.Build(content.Object, It.IsAny<string?>()))
.Returns((ApiContentRoute)null);

var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder.Object, CreateOutputExpansionStrategyAccessor());
var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder.Object, CreateOutputExpansionStrategyAccessor(), CreateVariationContextAccessor());
var result = builder.Build(content.Object);

Assert.Null(result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ private ContentPickerValueConverter CreateValueConverter(IApiContentNameProvider
new ApiContentBuilder(
nameProvider ?? new ApiContentNameProvider(),
CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()),
CreateOutputExpansionStrategyAccessor()));
CreateOutputExpansionStrategyAccessor(), CreateVariationContextAccessor()));

[Test]
public void ContentPickerValueConverter_BuildsDeliveryApiOutput()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Tests.Common;
using Umbraco.Extensions;

namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi;
Expand Down Expand Up @@ -92,6 +93,8 @@ protected IPublishedPropertyType SetupPublishedPropertyType(IPropertyValueConver

protected IOutputExpansionStrategyAccessor CreateOutputExpansionStrategyAccessor() => new NoopOutputExpansionStrategyAccessor();

protected IVariationContextAccessor CreateVariationContextAccessor() => new TestVariationContextAccessor();

protected IOptions<GlobalSettings> CreateGlobalSettings(bool hideTopLevelNodeFromPath = true)
{
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = hideTopLevelNodeFromPath };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ private MultiNodeTreePickerValueConverter MultiNodeTreePickerValueConverter(IApi
return new MultiNodeTreePickerValueConverter(
Mock.Of<IUmbracoContextAccessor>(),
Mock.Of<IMemberService>(),
new ApiContentBuilder(contentNameProvider, routeBuilder, expansionStrategyAccessor),
new ApiContentBuilder(contentNameProvider, routeBuilder, expansionStrategyAccessor, CreateVariationContextAccessor()),
new ApiMediaBuilder(contentNameProvider, apiUrProvider, Mock.Of<IPublishedValueFallback>(), expansionStrategyAccessor),
CacheManager.Content,
CacheManager.Media,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public void SetUp()
public void OutputExpansionStrategy_ExpandsNothingByDefault()
{
var accessor = CreateOutputExpansionStrategyAccessor(false);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());

var content = new Mock<IPublishedContent>();
var prop1 = new PublishedElementPropertyBase(DeliveryApiPropertyType, content.Object, false, PropertyCacheLevel.None, VariationContext, CacheManager);
Expand All @@ -69,7 +69,7 @@ public void OutputExpansionStrategy_ExpandsNothingByDefault()
public void OutputExpansionStrategy_CanExpandSpecificContent()
{
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { "contentPickerTwo" });
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());

var content = new Mock<IPublishedContent>();

Expand Down Expand Up @@ -142,7 +142,7 @@ public void OutputExpansionStrategy_CanExpandSpecificMedia(bool mediaPicker3)
public void OutputExpansionStrategy_CanExpandAllContent()
{
var accessor = CreateOutputExpansionStrategyAccessor(true);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());

var content = new Mock<IPublishedContent>();

Expand Down Expand Up @@ -177,7 +177,7 @@ public void OutputExpansionStrategy_CanExpandAllContent()
public void OutputExpansionStrategy_DoesNotExpandNestedContentPicker(string rootPropertyTypeAlias, string nestedPropertyTypeAlias)
{
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { rootPropertyTypeAlias, nestedPropertyTypeAlias });
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());

var content = new Mock<IPublishedContent>();

Expand Down Expand Up @@ -207,7 +207,7 @@ public void OutputExpansionStrategy_DoesNotExpandNestedContentPicker(string root
public void OutputExpansionStrategy_DoesNotExpandElementsByDefault()
{
var accessor = CreateOutputExpansionStrategyAccessor(false);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var apiElementBuilder = new ApiElementBuilder(accessor);

var contentPickerValue = CreateSimplePickedContent(111, 222);
Expand Down Expand Up @@ -283,7 +283,7 @@ public void OutputExpansionStrategy_MappingMedia_ThrowsOnInvalidItemType()
public void OutputExpansionStrategy_ForwardsExpansionStateToPropertyValueConverter(bool expanding)
{
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { expanding ? "theAlias" : "noSuchAlias" });
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());

var content = new Mock<IPublishedContent>();

Expand Down
Loading