Skip to content

Commit ae54b92

Browse files
authored
Output culture variant update dates from the Delivery API (#19180)
1 parent 758a9cf commit ae54b92

File tree

10 files changed

+218
-29
lines changed

10 files changed

+218
-29
lines changed
Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,35 @@
1+
using Umbraco.Cms.Core.DependencyInjection;
12
using Umbraco.Cms.Core.Models.DeliveryApi;
23
using Umbraco.Cms.Core.Models.PublishedContent;
4+
using Umbraco.Extensions;
35

46
namespace Umbraco.Cms.Core.DeliveryApi;
57

68
public sealed class ApiContentBuilder : ApiContentBuilderBase<IApiContent>, IApiContentBuilder
79
{
8-
public ApiContentBuilder(IApiContentNameProvider apiContentNameProvider, IApiContentRouteBuilder apiContentRouteBuilder, IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
9-
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
10+
private readonly IVariationContextAccessor _variationContextAccessor;
11+
12+
[Obsolete("Please use the constructor that takes an IVariationContextAccessor instead. Scheduled for removal in V17.")]
13+
public ApiContentBuilder(
14+
IApiContentNameProvider apiContentNameProvider,
15+
IApiContentRouteBuilder apiContentRouteBuilder,
16+
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
17+
: this(
18+
apiContentNameProvider,
19+
apiContentRouteBuilder,
20+
outputExpansionStrategyAccessor,
21+
StaticServiceProvider.Instance.CreateInstance<IVariationContextAccessor>())
1022
{
1123
}
1224

25+
public ApiContentBuilder(
26+
IApiContentNameProvider apiContentNameProvider,
27+
IApiContentRouteBuilder apiContentRouteBuilder,
28+
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor,
29+
IVariationContextAccessor variationContextAccessor)
30+
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
31+
=> _variationContextAccessor = variationContextAccessor;
32+
1333
protected override IApiContent Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary<string, object?> properties)
14-
=> new ApiContent(content.Key, name, content.ContentType.Alias, content.CreateDate, content.UpdateDate, route, properties);
34+
=> new ApiContent(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(_variationContextAccessor), route, properties);
1535
}

src/Umbraco.Core/DeliveryApi/ApiContentResponseBuilder.cs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Umbraco.Cms.Core.Models.DeliveryApi;
1+
using Umbraco.Cms.Core.DependencyInjection;
2+
using Umbraco.Cms.Core.Models.DeliveryApi;
23
using Umbraco.Cms.Core.Models.PublishedContent;
34
using Umbraco.Extensions;
45

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

11-
public ApiContentResponseBuilder(IApiContentNameProvider apiContentNameProvider, IApiContentRouteBuilder apiContentRouteBuilder, IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
13+
[Obsolete("Please use the constructor that takes an IVariationContextAccessor instead. Scheduled for removal in V17.")]
14+
public ApiContentResponseBuilder(
15+
IApiContentNameProvider apiContentNameProvider,
16+
IApiContentRouteBuilder apiContentRouteBuilder,
17+
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
18+
: this(
19+
apiContentNameProvider,
20+
apiContentRouteBuilder,
21+
outputExpansionStrategyAccessor,
22+
StaticServiceProvider.Instance.CreateInstance<IVariationContextAccessor>())
23+
{
24+
}
25+
26+
public ApiContentResponseBuilder(
27+
IApiContentNameProvider apiContentNameProvider,
28+
IApiContentRouteBuilder apiContentRouteBuilder,
29+
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor,
30+
IVariationContextAccessor variationContextAccessor)
1231
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
13-
=> _apiContentRouteBuilder = apiContentRouteBuilder;
32+
{
33+
_apiContentRouteBuilder = apiContentRouteBuilder;
34+
_variationContextAccessor = variationContextAccessor;
35+
}
1436

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

2143
protected virtual IDictionary<string, IApiContentRoute> GetCultures(IPublishedContent content)
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using Microsoft.AspNetCore.Http;
2+
using NUnit.Framework;
3+
using Umbraco.Cms.Core;
4+
using Umbraco.Cms.Core.Cache;
5+
using Umbraco.Cms.Core.DeliveryApi;
6+
using Umbraco.Cms.Core.Models;
7+
using Umbraco.Cms.Core.Models.PublishedContent;
8+
using Umbraco.Cms.Core.Services;
9+
using Umbraco.Cms.Core.Services.Changes;
10+
using Umbraco.Cms.Core.Web;
11+
using Umbraco.Cms.Tests.Common.Builders;
12+
using Umbraco.Cms.Tests.Common.Builders.Extensions;
13+
using Umbraco.Cms.Tests.Common.Testing;
14+
using Umbraco.Cms.Tests.Integration.Testing;
15+
16+
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.DeliveryApi.Request;
17+
18+
[TestFixture]
19+
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
20+
public class ApiContentResponseBuilderTests : UmbracoIntegrationTest
21+
{
22+
private IContentService ContentService => GetRequiredService<IContentService>();
23+
24+
private IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
25+
26+
private IApiContentResponseBuilder ApiContentResponseBuilder => GetRequiredService<IApiContentResponseBuilder>();
27+
28+
protected IUmbracoContextAccessor UmbracoContextAccessor => GetRequiredService<IUmbracoContextAccessor>();
29+
30+
protected IUmbracoContextFactory UmbracoContextFactory => GetRequiredService<IUmbracoContextFactory>();
31+
32+
protected IVariationContextAccessor VariationContextAccessor => GetRequiredService<IVariationContextAccessor>();
33+
34+
protected override void CustomTestSetup(IUmbracoBuilder builder)
35+
{
36+
builder.AddUmbracoHybridCache();
37+
builder.AddDeliveryApi();
38+
}
39+
40+
[SetUp]
41+
public void SetUpTest()
42+
{
43+
var httpContextAccessor = GetRequiredService<IHttpContextAccessor>();
44+
45+
httpContextAccessor.HttpContext = new DefaultHttpContext
46+
{
47+
Request =
48+
{
49+
Scheme = "https",
50+
Host = new HostString("localhost"),
51+
Path = "/",
52+
QueryString = new QueryString(string.Empty)
53+
},
54+
RequestServices = Services
55+
};
56+
}
57+
58+
[Test]
59+
public async Task ContentBuilder_MapsContentDatesCorrectlyForCultureVariance()
60+
{
61+
await GetRequiredService<ILanguageService>().CreateAsync(new Language("da-DK", "Danish"), Constants.Security.SuperUserKey);
62+
63+
var contentType = new ContentTypeBuilder()
64+
.WithAlias("theContentType")
65+
.WithContentVariation(ContentVariation.Culture)
66+
.Build();
67+
contentType.AllowedAsRoot = true;
68+
await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
69+
70+
var content = new ContentBuilder()
71+
.WithContentType(contentType)
72+
.WithCultureName("en-US", "Content EN")
73+
.WithCultureName("da-DK", "Content DA")
74+
.Build();
75+
ContentService.Save(content);
76+
ContentService.Publish(content, ["*"]);
77+
78+
Thread.Sleep(200);
79+
content.SetCultureName("Content DA updated", "da-DK");
80+
ContentService.Save(content);
81+
ContentService.Publish(content, ["da-DK"]);
82+
83+
RefreshContentCache();
84+
85+
UmbracoContextAccessor.Clear();
86+
var umbracoContext = UmbracoContextFactory.EnsureUmbracoContext().UmbracoContext;
87+
var publishedContent = umbracoContext.Content.GetById(content.Key);
88+
Assert.IsNotNull(publishedContent);
89+
90+
VariationContextAccessor.VariationContext = new VariationContext(culture: "en-US");
91+
var enResult = ApiContentResponseBuilder.Build(publishedContent);
92+
Assert.IsNotNull(enResult);
93+
94+
VariationContextAccessor.VariationContext = new VariationContext(culture: "da-DK");
95+
var daResult = ApiContentResponseBuilder.Build(publishedContent);
96+
Assert.IsNotNull(daResult);
97+
98+
Assert.GreaterOrEqual((daResult.UpdateDate - enResult.UpdateDate).TotalMilliseconds, 200);
99+
}
100+
101+
private void RefreshContentCache()
102+
=> GetRequiredService<ContentCacheRefresher>().Refresh([new ContentCacheRefresher.JsonPayload { ChangeTypes = TreeChangeTypes.RefreshAll }]);
103+
}

tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
using Moq;
22
using NUnit.Framework;
33
using Umbraco.Cms.Core.DeliveryApi;
4+
using Umbraco.Cms.Core.Models;
45
using Umbraco.Cms.Core.Models.DeliveryApi;
56
using Umbraco.Cms.Core.Models.PublishedContent;
67
using Umbraco.Cms.Core.PropertyEditors;
78
using Umbraco.Cms.Core.PublishedCache;
89
using Umbraco.Cms.Core.Services.Navigation;
10+
using Umbraco.Cms.Tests.Common;
911

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

@@ -42,7 +44,7 @@ public void ContentBuilder_MapsContentDataAndPropertiesCorrectly()
4244

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

45-
var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder, CreateOutputExpansionStrategyAccessor());
47+
var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder, CreateOutputExpansionStrategyAccessor(), CreateVariationContextAccessor());
4648
var result = builder.Build(content.Object);
4749

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

62+
[TestCase("en-US", "2023-08-04")]
63+
[TestCase("da-DK", "2023-09-08")]
64+
public void ContentBuilder_MapsContentDatesCorrectlyForCultureVariance(string culture, string expectedUpdateDate)
65+
{
66+
var content = new Mock<IPublishedContent>();
67+
68+
var contentType = new Mock<IPublishedContentType>();
69+
contentType.SetupGet(c => c.Alias).Returns("thePageType");
70+
contentType.SetupGet(c => c.ItemType).Returns(PublishedItemType.Content);
71+
contentType.SetupGet(c => c.Variations).Returns(ContentVariation.Culture);
72+
73+
var key = Guid.NewGuid();
74+
var urlSegment = "url-segment";
75+
var name = "The page";
76+
ConfigurePublishedContentMock(content, key, name, urlSegment, contentType.Object, []);
77+
content.SetupGet(c => c.CreateDate).Returns(new DateTime(2023, 07, 02));
78+
content
79+
.SetupGet(c => c.Cultures)
80+
.Returns(new Dictionary<string, PublishedCultureInfo>
81+
{
82+
{ "en-US", new PublishedCultureInfo("en-US", "EN Name", "en-url-segment", new DateTime(2023, 08, 04)) },
83+
{ "da-DK", new PublishedCultureInfo("da-DK", "DA Name", "da-url-segment", new DateTime(2023, 09, 08)) },
84+
});
85+
86+
var routeBuilder = new Mock<IApiContentRouteBuilder>();
87+
routeBuilder
88+
.Setup(r => r.Build(content.Object, It.IsAny<string?>()))
89+
.Returns(new ApiContentRoute(content.Object.UrlSegment!, new ApiContentStartItem(Guid.NewGuid(), "/")));
90+
91+
var variationContextAccessor = new TestVariationContextAccessor { VariationContext = new VariationContext(culture) };
92+
93+
var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder.Object, CreateOutputExpansionStrategyAccessor(), variationContextAccessor);
94+
var result = builder.Build(content.Object);
95+
96+
Assert.NotNull(result);
97+
Assert.AreEqual(new DateTime(2023, 07, 02), result.CreateDate);
98+
Assert.AreEqual(DateTime.Parse(expectedUpdateDate), result.UpdateDate);
99+
}
100+
60101
[Test]
61102
public void ContentBuilder_CanCustomizeContentNameInDeliveryApiOutput()
62103
{
@@ -75,7 +116,7 @@ public void ContentBuilder_CanCustomizeContentNameInDeliveryApiOutput()
75116
.Setup(r => r.Build(content.Object, It.IsAny<string?>()))
76117
.Returns(new ApiContentRoute(content.Object.UrlSegment!, new ApiContentStartItem(Guid.NewGuid(), "/")));
77118

78-
var builder = new ApiContentBuilder(customNameProvider.Object, routeBuilder.Object, CreateOutputExpansionStrategyAccessor());
119+
var builder = new ApiContentBuilder(customNameProvider.Object, routeBuilder.Object, CreateOutputExpansionStrategyAccessor(), CreateVariationContextAccessor());
79120
var result = builder.Build(content.Object);
80121

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

100-
var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder.Object, CreateOutputExpansionStrategyAccessor());
141+
var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder.Object, CreateOutputExpansionStrategyAccessor(), CreateVariationContextAccessor());
101142
var result = builder.Build(content.Object);
102143

103144
Assert.Null(result);

tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ private ContentPickerValueConverter CreateValueConverter(IApiContentNameProvider
1919
new ApiContentBuilder(
2020
nameProvider ?? new ApiContentNameProvider(),
2121
CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()),
22-
CreateOutputExpansionStrategyAccessor()));
22+
CreateOutputExpansionStrategyAccessor(), CreateVariationContextAccessor()));
2323

2424
[Test]
2525
public void ContentPickerValueConverter_BuildsDeliveryApiOutput()

tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Umbraco.Cms.Core.PublishedCache;
1212
using Umbraco.Cms.Core.Services;
1313
using Umbraco.Cms.Core.Services.Navigation;
14+
using Umbraco.Cms.Tests.Common;
1415
using Umbraco.Extensions;
1516

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

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

96+
protected IVariationContextAccessor CreateVariationContextAccessor() => new TestVariationContextAccessor();
97+
9598
protected IOptions<GlobalSettings> CreateGlobalSettings(bool hideTopLevelNodeFromPath = true)
9699
{
97100
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = hideTopLevelNodeFromPath };

tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ private MultiNodeTreePickerValueConverter MultiNodeTreePickerValueConverter(IApi
2525
return new MultiNodeTreePickerValueConverter(
2626
Mock.Of<IUmbracoContextAccessor>(),
2727
Mock.Of<IMemberService>(),
28-
new ApiContentBuilder(contentNameProvider, routeBuilder, expansionStrategyAccessor),
28+
new ApiContentBuilder(contentNameProvider, routeBuilder, expansionStrategyAccessor, CreateVariationContextAccessor()),
2929
new ApiMediaBuilder(contentNameProvider, apiUrProvider, Mock.Of<IPublishedValueFallback>(), expansionStrategyAccessor),
3030
CacheManager.Content,
3131
CacheManager.Media,

tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTestBase.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public void SetUp()
4343
public void OutputExpansionStrategy_ExpandsNothingByDefault()
4444
{
4545
var accessor = CreateOutputExpansionStrategyAccessor(false);
46-
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
46+
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
4747

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

7474
var content = new Mock<IPublishedContent>();
7575

@@ -142,7 +142,7 @@ public void OutputExpansionStrategy_CanExpandSpecificMedia(bool mediaPicker3)
142142
public void OutputExpansionStrategy_CanExpandAllContent()
143143
{
144144
var accessor = CreateOutputExpansionStrategyAccessor(true);
145-
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
145+
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
146146

147147
var content = new Mock<IPublishedContent>();
148148

@@ -177,7 +177,7 @@ public void OutputExpansionStrategy_CanExpandAllContent()
177177
public void OutputExpansionStrategy_DoesNotExpandNestedContentPicker(string rootPropertyTypeAlias, string nestedPropertyTypeAlias)
178178
{
179179
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { rootPropertyTypeAlias, nestedPropertyTypeAlias });
180-
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
180+
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
181181

182182
var content = new Mock<IPublishedContent>();
183183

@@ -207,7 +207,7 @@ public void OutputExpansionStrategy_DoesNotExpandNestedContentPicker(string root
207207
public void OutputExpansionStrategy_DoesNotExpandElementsByDefault()
208208
{
209209
var accessor = CreateOutputExpansionStrategyAccessor(false);
210-
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
210+
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
211211
var apiElementBuilder = new ApiElementBuilder(accessor);
212212

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

288288
var content = new Mock<IPublishedContent>();
289289

0 commit comments

Comments
 (0)