diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs b/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs index 1868ee0aea..9c3aad1cfc 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs @@ -5,6 +5,7 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; @@ -19,6 +20,7 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder private readonly IPublishedContentCache _contentCache; private readonly IDocumentNavigationQueryService _navigationQueryService; private readonly IPublishStatusQueryService _publishStatusQueryService; + private readonly IDocumentUrlService _documentUrlService; private RequestHandlerSettings _requestSettings; public ApiContentRouteBuilder( @@ -29,7 +31,8 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder IOptionsMonitor requestSettings, IPublishedContentCache contentCache, IDocumentNavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService) + IPublishStatusQueryService publishStatusQueryService, + IDocumentUrlService documentUrlService) { _apiContentPathProvider = apiContentPathProvider; _variationContextAccessor = variationContextAccessor; @@ -37,11 +40,35 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder _contentCache = contentCache; _navigationQueryService = navigationQueryService; _publishStatusQueryService = publishStatusQueryService; + _documentUrlService = documentUrlService; _globalSettings = globalSettings.Value; _requestSettings = requestSettings.CurrentValue; requestSettings.OnChange(settings => _requestSettings = settings); } + [Obsolete("Use the non-obsolete constructor, scheduled for removal in v17")] + public ApiContentRouteBuilder( + IApiContentPathProvider apiContentPathProvider, + IOptions globalSettings, + IVariationContextAccessor variationContextAccessor, + IRequestPreviewService requestPreviewService, + IOptionsMonitor requestSettings, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService, + IPublishStatusQueryService publishStatusQueryService) + : this( + apiContentPathProvider, + globalSettings, + variationContextAccessor, + requestPreviewService, + requestSettings, + contentCache, + navigationQueryService, + publishStatusQueryService, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + [Obsolete("Use the non-obsolete constructor, scheduled for removal in v17")] public ApiContentRouteBuilder( IApiContentPathProvider apiContentPathProvider, @@ -59,7 +86,8 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder requestSettings, contentCache, navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -113,7 +141,7 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder // we can perform fallback to the content route. if (IsInvalidContentPath(contentPath)) { - contentPath = _contentCache.GetRouteById(content.Id, culture) ?? contentPath; + contentPath = _documentUrlService.GetLegacyRouteFormat(content.Key, culture ?? _variationContextAccessor.VariationContext?.Culture, isPreview); } // if the content path has still not been resolved as a valid path, the content is un-routable in this culture @@ -125,7 +153,9 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder : null; } - return contentPath; + return _requestSettings.AddTrailingSlash + ? contentPath?.EnsureEndsWith('/') + : contentPath?.TrimEnd('/'); } private string ContentPreviewPath(IPublishedContent content) => $"{Constants.DeliveryApi.Routing.PreviewContentPathPrefix}{content.Key:D}{(_requestSettings.AddTrailingSlash ? "/" : string.Empty)}"; diff --git a/src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs b/src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs index 0d6f9ecbf5..5e48fb01c0 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs @@ -86,10 +86,35 @@ public sealed class ApiPublishedContentCache : IApiPublishedContentCache _variationContextAccessor.VariationContext?.Culture, _requestPreviewService.IsPreview()); + // in multi-root settings, we've historically resolved all but the first root by their ID + URL segment, + // e.g. "1234/second-root-url-segment". in V15+, IDocumentUrlService won't resolve this anymore; it will + // however resolve "1234/" correctly, so to remain backwards compatible, we need to perform this extra step. + var verifyUrlSegment = false; + if (documentKey is null && route.TrimEnd('/').CountOccurrences("/") is 1) + { + documentKey = _apiDocumentUrlService.GetDocumentKeyByRoute( + route[..(route.IndexOf('/') + 1)], + _variationContextAccessor.VariationContext?.Culture, + _requestPreviewService.IsPreview()); + verifyUrlSegment = true; + } + IPublishedContent? content = documentKey.HasValue ? _publishedContentCache.GetById(isPreviewMode, documentKey.Value) : null; + // the additional look-up above can result in false positives; if attempting to request a non-existing child to + // the currently contextualized request root (either by start item or by domain), the root content key might + // get resolved. to counter for this, we compare the requested URL segment with the resolved content URL segment. + if (content is not null && verifyUrlSegment) + { + var expectedUrlSegment = route[(route.IndexOf('/') + 1)..]; + if (content.UrlSegment != expectedUrlSegment) + { + content = null; + } + } + return ContentOrNullIfDisallowed(content); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentPathResolverInvariantTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentPathResolverInvariantTests.cs new file mode 100644 index 0000000000..371f0d70b2 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentPathResolverInvariantTests.cs @@ -0,0 +1,258 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Integration.Attributes; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.DeliveryApi.Request; + +public class ApiContentPathResolverInvariantTests : ApiContentPathResolverTestBase +{ + private Dictionary _contentByName = new (); + + public static void ConfigureIncludeTopLevelNodeInPath(IUmbracoBuilder builder) + => builder.Services.Configure(config => config.HideTopLevelNodeFromPath = false); + + [SetUp] + public async Task SetUpTest() + { + UmbracoContextFactory.EnsureUmbracoContext(); + SetRequestHost("localhost"); + + if (_contentByName.Any()) + { + // these tests all run on the same DB to make them run faster, so we need to get the cache in a + // predictable state with each test run. + RefreshContentCache(); + return; + } + + await DocumentUrlService.InitAsync(true, CancellationToken.None); + + var contentType = new ContentTypeBuilder() + .WithAlias("theContentType") + .Build(); + contentType.AllowedAsRoot = true; + await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); + contentType.AllowedContentTypes = [new() { Alias = contentType.Alias, Key = contentType.Key }]; + await ContentTypeService.UpdateAsync(contentType, Constants.Security.SuperUserKey); + foreach (var rootNumber in Enumerable.Range(1, 3)) + { + var root = new ContentBuilder() + .WithContentType(contentType) + .WithName($"Root {rootNumber}") + .Build(); + ContentService.Save(root); + ContentService.Publish(root, ["*"]); + _contentByName[root.Name!] = root; + + foreach (var childNumber in Enumerable.Range(1, 3)) + { + var child = new ContentBuilder() + .WithContentType(contentType) + .WithParent(root) + .WithName($"Child {childNumber}") + .Build(); + ContentService.Save(child); + ContentService.Publish(child, ["*"]); + _contentByName[$"{root.Name!}/{child.Name!}"] = child; + + foreach (var grandchildNumber in Enumerable.Range(1, 3)) + { + var grandchild = new ContentBuilder() + .WithContentType(contentType) + .WithParent(child) + .WithName($"Grandchild {grandchildNumber}") + .Build(); + ContentService.Save(grandchild); + ContentService.Publish(grandchild, ["*"]); + _contentByName[$"{root.Name!}/{child.Name!}/{grandchild.Name!}"] = grandchild; + } + } + } + } + + [Test] + public void First_Root_Without_StartItem() + { + Assert.IsEmpty(GetRequiredService().HttpContext!.Request.Headers["Start-Item"].ToString()); + + var content = ApiContentPathResolver.ResolveContentPath("/"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName["Root 1"].Key, content.Key); + } + + [Test] + [ConfigureBuilder(ActionName = nameof(ConfigureIncludeTopLevelNodeInPath))] + public void First_Root_Without_StartItem_With_Top_Level_Node_Included() + { + Assert.IsEmpty(GetRequiredService().HttpContext!.Request.Headers["Start-Item"].ToString()); + + var content = ApiContentPathResolver.ResolveContentPath("/"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName["Root 1"].Key, content.Key); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + public void First_Root_Child_Without_StartItem(int child) + { + Assert.IsEmpty(GetRequiredService().HttpContext!.Request.Headers["Start-Item"].ToString()); + + var content = ApiContentPathResolver.ResolveContentPath($"/child-{child}"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root 1/Child {child}"].Key, content.Key); + } + + [TestCase(1, 1)] + [TestCase(2, 2)] + [TestCase(3, 3)] + [TestCase(1, 2)] + [TestCase(2, 3)] + [TestCase(3, 1)] + public void First_Root_Grandchild_Without_StartItem(int child, int grandchild) + { + Assert.IsEmpty(GetRequiredService().HttpContext!.Request.Headers["Start-Item"].ToString()); + + var content = ApiContentPathResolver.ResolveContentPath($"/child-{child}/grandchild-{grandchild}"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root 1/Child {child}/Grandchild {grandchild}"].Key, content.Key); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + public void Root_With_StartItem(int root) + { + SetRequestStartItem($"root-{root}"); + + var content = ApiContentPathResolver.ResolveContentPath("/"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, content.Key); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [ConfigureBuilder(ActionName = nameof(ConfigureIncludeTopLevelNodeInPath))] + public void Root_With_StartItem_With_Top_Level_Node_Included(int root) + { + SetRequestStartItem($"root-{root}"); + + var content = ApiContentPathResolver.ResolveContentPath("/"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, content.Key); + } + + [TestCase(1, 1)] + [TestCase(2, 2)] + [TestCase(3, 3)] + [TestCase(1, 2)] + [TestCase(2, 3)] + [TestCase(3, 1)] + public void Child_With_StartItem(int root, int child) + { + SetRequestStartItem($"root-{root}"); + + var content = ApiContentPathResolver.ResolveContentPath($"/child-{child}"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root {root}/Child {child}"].Key, content.Key); + } + + [TestCase(1, 1)] + [TestCase(2, 2)] + [TestCase(3, 3)] + [TestCase(1, 2)] + [TestCase(2, 3)] + [TestCase(3, 1)] + [ConfigureBuilder(ActionName = nameof(ConfigureIncludeTopLevelNodeInPath))] + public void Child_With_StartItem_With_Top_Level_Node_Included(int root, int child) + { + SetRequestStartItem($"root-{root}"); + + var content = ApiContentPathResolver.ResolveContentPath($"/child-{child}"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root {root}/Child {child}"].Key, content.Key); + } + + [TestCase(1, 1, 1)] + [TestCase(2, 2, 2)] + [TestCase(3, 3, 3)] + [TestCase(1, 2, 3)] + [TestCase(2, 3, 1)] + [TestCase(3, 1, 2)] + public void Grandchild_With_StartItem(int root, int child, int grandchild) + { + SetRequestStartItem($"root-{root}"); + + var content = ApiContentPathResolver.ResolveContentPath($"/child-{child}/grandchild-{grandchild}"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root {root}/Child {child}/Grandchild {grandchild}"].Key, content.Key); + } + + [TestCase("/", 1)] + [TestCase("/root-2", 2)] + [TestCase("/root-3", 3)] + public void Root_By_Path_With_StartItem(string path, int root) + { + SetRequestStartItem($"root-{root}"); + + var content = ApiContentPathResolver.ResolveContentPath(path); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, content.Key); + } + + [TestCase("/", 1)] + [TestCase("/root-2", 2)] + [TestCase("/root-3", 3)] + public void Root_By_Path_Without_StartItem(string path, int root) + { + var content = ApiContentPathResolver.ResolveContentPath(path); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, content.Key); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + public async Task Root_With_Domain_Bindings(int root) + { + await SetContentHost(_contentByName[$"Root {root}"], "some.host", "en-US"); + SetRequestHost("some.host"); + + var content = ApiContentPathResolver.ResolveContentPath("/"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, content.Key); + } + + [TestCase("/a", 1)] + [TestCase("/123", 2)] + [TestCase("/no-such-child", 3)] + [TestCase("/a/b", 1)] + [TestCase("/123/456", 2)] + [TestCase("/no-such-child/no-such-grandchild", 3)] + public void Non_Existant_Descendant_By_Path_With_StartItem(string path, int root) + { + SetRequestStartItem($"root-{root}"); + + var content = ApiContentPathResolver.ResolveContentPath(path); + Assert.IsNull(content); + } + + [TestCase("/a")] + [TestCase("/123")] + [TestCase("/a/b")] + [TestCase("/123/456")] + public void Non_Existant_Descendant_By_Path_Without_StartItem(string path) + { + var content = ApiContentPathResolver.ResolveContentPath(path); + Assert.IsNull(content); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentPathResolverTestBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentPathResolverTestBase.cs new file mode 100644 index 0000000000..05b650de58 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentPathResolverTestBase.cs @@ -0,0 +1,15 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.DeliveryApi.Request; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] +public abstract class ApiContentPathResolverTestBase : ApiContentRequestTestBase +{ + protected IApiContentPathResolver ApiContentPathResolver => GetRequiredService(); + + protected IDocumentUrlService DocumentUrlService => GetRequiredService(); +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentPathResolverVariantTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentPathResolverVariantTests.cs new file mode 100644 index 0000000000..628b1839ec --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentPathResolverVariantTests.cs @@ -0,0 +1,326 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Integration.Attributes; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.DeliveryApi.Request; + +public class ApiContentPathResolverVariantTests : ApiContentPathResolverTestBase +{ + private readonly Dictionary _contentByName = new (); + + public static void ConfigureIncludeTopLevelNodeInPath(IUmbracoBuilder builder) + => builder.Services.Configure(config => config.HideTopLevelNodeFromPath = false); + + [SetUp] + public async Task SetUpTest() + { + UmbracoContextFactory.EnsureUmbracoContext(); + SetRequestHost("localhost"); + + if (_contentByName.Any()) + { + // these tests all run on the same DB to make them run faster, so we need to get the cache in a + // predictable state with each test run. + RefreshContentCache(); + return; + } + + await DocumentUrlService.InitAsync(true, CancellationToken.None); + + await GetRequiredService().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); + contentType.AllowedContentTypes = [new() { Alias = contentType.Alias, Key = contentType.Key }]; + await ContentTypeService.UpdateAsync(contentType, Constants.Security.SuperUserKey); + foreach (var rootNumber in Enumerable.Range(1, 3)) + { + var root = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName("en-US", $"Root {rootNumber} en-US") + .WithCultureName("da-DK", $"Root {rootNumber} da-DK") + .Build(); + ContentService.Save(root); + ContentService.Publish(root, ["*"]); + _contentByName[$"Root {rootNumber}"] = root; + + foreach (var childNumber in Enumerable.Range(1, 3)) + { + var child = new ContentBuilder() + .WithContentType(contentType) + .WithParent(root) + .WithCultureName("en-US", $"Child {childNumber} en-US") + .WithCultureName("da-DK", $"Child {childNumber} da-DK") + .Build(); + ContentService.Save(child); + ContentService.Publish(child, ["*"]); + _contentByName[$"Root {rootNumber}/Child {childNumber}"] = child; + + foreach (var grandchildNumber in Enumerable.Range(1, 3)) + { + var grandchild = new ContentBuilder() + .WithContentType(contentType) + .WithParent(child) + .WithCultureName("en-US", $"Grandchild {grandchildNumber} en-US") + .WithCultureName("da-DK", $"Grandchild {grandchildNumber} da-DK") + .Build(); + ContentService.Save(grandchild); + ContentService.Publish(grandchild, ["*"]); + _contentByName[$"Root {rootNumber}/Child {childNumber}/Grandchild {grandchildNumber}"] = grandchild; + } + } + } + } + + [TestCase("en-US")] + [TestCase("da-DK")] + public void First_Root_Without_StartItem(string culture) + { + Assert.IsEmpty(GetRequiredService().HttpContext!.Request.Headers["Start-Item"].ToString()); + SetVariationContext(culture); + + var content = ApiContentPathResolver.ResolveContentPath("/"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName["Root 1"].Key, content.Key); + } + + [TestCase("en-US")] + [TestCase("da-DK")] + [ConfigureBuilder(ActionName = nameof(ConfigureIncludeTopLevelNodeInPath))] + public void First_Root_Without_StartItem_With_Top_Level_Node_Included(string culture) + { + Assert.IsEmpty(GetRequiredService().HttpContext!.Request.Headers["Start-Item"].ToString()); + SetVariationContext(culture); + + var content = ApiContentPathResolver.ResolveContentPath("/"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName["Root 1"].Key, content.Key); + } + + [TestCase(1, "en-US")] + [TestCase(2, "en-US")] + [TestCase(3, "en-US")] + [TestCase(1, "da-DK")] + [TestCase(2, "da-DK")] + [TestCase(3, "da-DK")] + public void First_Root_Child_Without_StartItem(int child, string culture) + { + Assert.IsEmpty(GetRequiredService().HttpContext!.Request.Headers["Start-Item"].ToString()); + SetVariationContext(culture); + + var content = ApiContentPathResolver.ResolveContentPath($"/child-{child}-{culture.ToLowerInvariant()}"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root 1/Child {child}"].Key, content.Key); + } + + [TestCase(1, 1, "en-US")] + [TestCase(2, 2, "en-US")] + [TestCase(3, 3, "en-US")] + [TestCase(1, 2, "en-US")] + [TestCase(2, 3, "en-US")] + [TestCase(3, 1, "en-US")] + [TestCase(1, 1, "da-DK")] + [TestCase(2, 2, "da-DK")] + [TestCase(3, 3, "da-DK")] + [TestCase(1, 2, "da-DK")] + [TestCase(2, 3, "da-DK")] + [TestCase(3, 1, "da-DK")] + public void First_Root_Grandchild_Without_StartItem(int child, int grandchild, string culture) + { + Assert.IsEmpty(GetRequiredService().HttpContext!.Request.Headers["Start-Item"].ToString()); + SetVariationContext(culture); + + var content = ApiContentPathResolver.ResolveContentPath($"/child-{child}-{culture.ToLowerInvariant()}/grandchild-{grandchild}-{culture.ToLowerInvariant()}"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root 1/Child {child}/Grandchild {grandchild}"].Key, content.Key); + } + + [TestCase(1, "en-US")] + [TestCase(2, "en-US")] + [TestCase(3, "en-US")] + [TestCase(1, "da-DK")] + [TestCase(2, "da-DK")] + [TestCase(3, "da-DK")] + public void Root_With_StartItem(int root, string culture) + { + SetRequestStartItem($"root-{root}-{culture.ToLowerInvariant()}"); + SetVariationContext(culture); + + var content = ApiContentPathResolver.ResolveContentPath("/"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, content.Key); + } + + [TestCase(1, "en-US")] + [TestCase(2, "en-US")] + [TestCase(3, "en-US")] + [TestCase(1, "da-DK")] + [TestCase(2, "da-DK")] + [TestCase(3, "da-DK")] + [ConfigureBuilder(ActionName = nameof(ConfigureIncludeTopLevelNodeInPath))] + public void Root_With_StartItem_With_Top_Level_Node_Included(int root, string culture) + { + SetRequestStartItem($"root-{root}-{culture.ToLowerInvariant()}"); + SetVariationContext(culture); + + var content = ApiContentPathResolver.ResolveContentPath("/"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, content.Key); + } + + [TestCase(1, 1, "en-US")] + [TestCase(2, 2, "en-US")] + [TestCase(3, 3, "en-US")] + [TestCase(1, 2, "en-US")] + [TestCase(2, 3, "en-US")] + [TestCase(3, 1, "en-US")] + [TestCase(1, 1, "da-DK")] + [TestCase(2, 2, "da-DK")] + [TestCase(3, 3, "da-DK")] + [TestCase(1, 2, "da-DK")] + [TestCase(2, 3, "da-DK")] + [TestCase(3, 1, "da-DK")] + public void Child_With_StartItem(int root, int child, string culture) + { + SetRequestStartItem($"root-{root}-{culture.ToLowerInvariant()}"); + SetVariationContext(culture); + + var content = ApiContentPathResolver.ResolveContentPath($"/child-{child}-{culture.ToLowerInvariant()}"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root {root}/Child {child}"].Key, content.Key); + } + + [TestCase(1, 1, "en-US")] + [TestCase(2, 2, "en-US")] + [TestCase(3, 3, "en-US")] + [TestCase(1, 2, "en-US")] + [TestCase(2, 3, "en-US")] + [TestCase(3, 1, "en-US")] + [TestCase(1, 1, "da-DK")] + [TestCase(2, 2, "da-DK")] + [TestCase(3, 3, "da-DK")] + [TestCase(1, 2, "da-DK")] + [TestCase(2, 3, "da-DK")] + [TestCase(3, 1, "da-DK")] + [ConfigureBuilder(ActionName = nameof(ConfigureIncludeTopLevelNodeInPath))] + public void Child_With_StartItem_With_Top_Level_Node_Included(int root, int child, string culture) + { + SetRequestStartItem($"root-{root}-{culture.ToLowerInvariant()}"); + SetVariationContext(culture); + + var content = ApiContentPathResolver.ResolveContentPath($"/child-{child}-{culture.ToLowerInvariant()}"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root {root}/Child {child}"].Key, content.Key); + } + + [TestCase(1, 1, 1, "en-US")] + [TestCase(2, 2, 2, "en-US")] + [TestCase(3, 3, 3, "en-US")] + [TestCase(1, 2, 3, "en-US")] + [TestCase(2, 3, 1, "en-US")] + [TestCase(3, 1, 2, "en-US")] + [TestCase(1, 1, 1, "da-DK")] + [TestCase(2, 2, 2, "da-DK")] + [TestCase(3, 3, 3, "da-DK")] + [TestCase(1, 2, 3, "da-DK")] + [TestCase(2, 3, 1, "da-DK")] + [TestCase(3, 1, 2, "da-DK")] + public void Grandchild_With_StartItem(int root, int child, int grandchild, string culture) + { + SetRequestStartItem($"root-{root}-{culture.ToLowerInvariant()}"); + SetVariationContext(culture); + + var content = ApiContentPathResolver.ResolveContentPath($"/child-{child}-{culture.ToLowerInvariant()}/grandchild-{grandchild}-{culture.ToLowerInvariant()}"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root {root}/Child {child}/Grandchild {grandchild}"].Key, content.Key); + } + + [TestCase("/", 1, "en-US")] + [TestCase("/root-2-en-us", 2, "en-US")] + [TestCase("/root-3-en-us", 3, "en-US")] + [TestCase("/", 1, "da-DK")] + [TestCase("/root-2-da-dk", 2, "da-DK")] + [TestCase("/root-3-da-dk", 3, "da-DK")] + public void Root_By_Path_With_StartItem(string path, int root, string culture) + { + SetRequestStartItem($"root-{root}-{culture.ToLowerInvariant()}"); + SetVariationContext(culture); + + var content = ApiContentPathResolver.ResolveContentPath(path); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, content.Key); + } + + [TestCase("/", 1, "en-US")] + [TestCase("/root-2-en-us", 2, "en-US")] + [TestCase("/root-3-en-us", 3, "en-US")] + [TestCase("/", 1, "da-DK")] + [TestCase("/root-2-da-dk", 2, "da-DK")] + [TestCase("/root-3-da-dk", 3, "da-DK")] + public void Root_By_Path_Without_StartItem(string path, int root, string culture) + { + SetVariationContext(culture); + + var content = ApiContentPathResolver.ResolveContentPath(path); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, content.Key); + } + + [TestCase(1, "en-US")] + [TestCase(2, "en-US")] + [TestCase(3, "en-US")] + [TestCase(1, "da-DK")] + [TestCase(2, "da-DK")] + [TestCase(3, "da-DK")] + public async Task Root_With_Domain_Bindings(int root, string culture) + { + await SetContentHost(_contentByName[$"Root {root}"], "some.host", "en-US"); + SetRequestHost("some.host"); + SetVariationContext(culture); + + var content = ApiContentPathResolver.ResolveContentPath("/"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, content.Key); + } + + [TestCase("/a", 1, "en-US")] + [TestCase("/b", 1, "da-DK")] + [TestCase("/123", 2, "en-US")] + [TestCase("/456", 2, "da-DK")] + [TestCase("/no-such-child", 3, "en-US")] + [TestCase("/not-at-all", 3, "da-DK")] + [TestCase("/a/b", 1, "en-US")] + [TestCase("/c/d", 1, "da-DK")] + [TestCase("/123/456", 2, "en-US")] + [TestCase("/789/012", 2, "da-DK")] + [TestCase("/no-such-child/no-such-grandchild", 3, "en-US")] + [TestCase("/not-at-all/aint-no-way", 3, "da-DK")] + public void Non_Existant_Descendant_By_Path_With_StartItem(string path, int root, string culture) + { + SetRequestStartItem($"root-{root}-{culture.ToLowerInvariant()}"); + + var content = ApiContentPathResolver.ResolveContentPath(path); + Assert.IsNull(content); + } + + [TestCase("/a")] + [TestCase("/123")] + [TestCase("/a/b")] + [TestCase("/123/456")] + public void Non_Existant_Descendant_By_Path_Without_StartItem(string path) + { + var content = ApiContentPathResolver.ResolveContentPath(path); + Assert.IsNull(content); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentRequestTestBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentRequestTestBase.cs new file mode 100644 index 0000000000..8ec8e92bba --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentRequestTestBase.cs @@ -0,0 +1,93 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.DeliveryApi.Request; + +public abstract class ApiContentRequestTestBase : UmbracoIntegrationTest +{ + protected IContentService ContentService => GetRequiredService(); + + protected IContentTypeService ContentTypeService => GetRequiredService(); + + protected IApiContentRouteBuilder ApiContentRouteBuilder => GetRequiredService(); + + protected IVariationContextAccessor VariationContextAccessor => GetRequiredService(); + + protected IUmbracoContextAccessor UmbracoContextAccessor => GetRequiredService(); + + protected IUmbracoContextFactory UmbracoContextFactory => GetRequiredService(); + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.AddUmbracoHybridCache(); + builder.AddNotificationHandler(); + builder.Services.AddUnique(); + + builder.AddDeliveryApi(); + } + + [TearDown] + public async Task CleanUpAfterTest() + { + var domainService = GetRequiredService(); + foreach (var content in ContentService.GetRootContent()) + { + await domainService.UpdateDomainsAsync(content.Key, new DomainsUpdateModel { Domains = [] }); + } + + var httpContextAccessor = GetRequiredService(); + httpContextAccessor.HttpContext?.Request.Headers.Clear(); + } + + protected void SetVariationContext(string? culture) + => VariationContextAccessor.VariationContext = new VariationContext(culture: culture); + + protected async Task SetContentHost(IContent content, string host, string culture) + => await GetRequiredService().UpdateDomainsAsync( + content.Key, + new DomainsUpdateModel { Domains = [new DomainModel { DomainName = host, IsoCode = culture }] }); + + protected void SetRequestHost(string host) + { + var httpContextAccessor = GetRequiredService(); + + httpContextAccessor.HttpContext = new DefaultHttpContext + { + Request = + { + Scheme = "https", + Host = new HostString(host), + Path = "/", + QueryString = new QueryString(string.Empty) + }, + RequestServices = Services + }; + } + + protected void SetRequestStartItem(string startItem) + { + var httpContextAccessor = GetRequiredService(); + if (httpContextAccessor.HttpContext is null) + { + throw new InvalidOperationException("HTTP context is null"); + } + + httpContextAccessor.HttpContext.Request.Headers["Start-Item"] = startItem; + } + + protected void RefreshContentCache() + => Services.GetRequiredService().Refresh([new ContentCacheRefresher.JsonPayload { ChangeTypes = TreeChangeTypes.RefreshAll }]); +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentRouteBuilderInvariantTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentRouteBuilderInvariantTests.cs new file mode 100644 index 0000000000..dd077a7f16 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentRouteBuilderInvariantTests.cs @@ -0,0 +1,242 @@ +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Integration.Attributes; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.DeliveryApi.Request; + +public class ApiContentRouteBuilderInvariantTests : ApiContentRouteBuilderTestBase +{ + private readonly Dictionary _contentByName = new (); + + public static void ConfigureIncludeTopLevelNodeInPath(IUmbracoBuilder builder) + => builder.Services.Configure(config => config.HideTopLevelNodeFromPath = false); + + public static void ConfigureOmitTrailingSlash(IUmbracoBuilder builder) + => builder.Services.Configure(config => config.AddTrailingSlash = false); + + [SetUp] + public async Task SetUpTest() + { + SetRequestHost("localhost"); + + if (_contentByName.Any()) + { + // these tests all run on the same DB to make them run faster, so we need to get the cache in a + // predictable state with each test run. + RefreshContentCache(); + return; + } + + var contentType = new ContentTypeBuilder() + .WithAlias("theContentType") + .Build(); + contentType.AllowedAsRoot = true; + await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); + contentType.AllowedContentTypes = [new() { Alias = contentType.Alias, Key = contentType.Key }]; + await ContentTypeService.UpdateAsync(contentType, Constants.Security.SuperUserKey); + foreach (var rootNumber in Enumerable.Range(1, 3)) + { + var root = new ContentBuilder() + .WithContentType(contentType) + .WithName($"Root {rootNumber}") + .Build(); + ContentService.Save(root); + ContentService.Publish(root, ["*"]); + _contentByName[root.Name!] = root; + + foreach (var childNumber in Enumerable.Range(1, 3)) + { + var child = new ContentBuilder() + .WithContentType(contentType) + .WithParent(root) + .WithName($"Child {childNumber}") + .Build(); + ContentService.Save(child); + ContentService.Publish(child, ["*"]); + _contentByName[$"{root.Name!}/{child.Name!}"] = child; + + foreach (var grandchildNumber in Enumerable.Range(1, 3)) + { + var grandchild = new ContentBuilder() + .WithContentType(contentType) + .WithParent(child) + .WithName($"Grandchild {grandchildNumber}") + .Build(); + ContentService.Save(grandchild); + ContentService.Publish(grandchild, ["*"]); + _contentByName[$"{root.Name!}/{child.Name!}/{grandchild.Name!}"] = grandchild; + } + } + } + } + + [Test] + public void First_Root() + { + var publishedContent = GetPublishedContent(_contentByName["Root 1"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual("/", route.Path); + Assert.AreEqual("root-1", route.StartItem.Path); + Assert.AreEqual(_contentByName["Root 1"].Key, route.StartItem.Id); + }); + } + + [Test] + public void Last_Root() + { + var publishedContent = GetPublishedContent(_contentByName["Root 3"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual("/root-3/", route.Path); + Assert.AreEqual("root-3", route.StartItem.Path); + Assert.AreEqual(_contentByName["Root 3"].Key, route.StartItem.Id); + }); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + public void First_Child(int root) + { + var publishedContent = GetPublishedContent(_contentByName[$"Root {root}/Child 1"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual("/child-1/", route.Path); + Assert.AreEqual($"root-{root}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, route.StartItem.Id); + }); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + public void Last_Child(int root) + { + var publishedContent = GetPublishedContent(_contentByName[$"Root {root}/Child 3"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual("/child-3/", route.Path); + Assert.AreEqual($"root-{root}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, route.StartItem.Id); + }); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + public void First_Grandchild(int root) + { + var publishedContent = GetPublishedContent(_contentByName[$"Root {root}/Child 1/Grandchild 1"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual("/child-1/grandchild-1/", route.Path); + Assert.AreEqual($"root-{root}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, route.StartItem.Id); + }); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + public void Last_Grandchild(int root) + { + var publishedContent = GetPublishedContent(_contentByName[$"Root {root}/Child 3/Grandchild 3"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual("/child-3/grandchild-3/", route.Path); + Assert.AreEqual($"root-{root}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, route.StartItem.Id); + }); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [ConfigureBuilder(ActionName = nameof(ConfigureIncludeTopLevelNodeInPath))] + public void Root_With_Top_Level_Node_Included(int root) + { + var publishedContent = GetPublishedContent(_contentByName[$"Root {root}"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual("/", route.Path); + Assert.AreEqual($"root-{root}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, route.StartItem.Id); + }); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + public async Task Root_With_Domain_Bindings(int root) + { + await SetContentHost(_contentByName[$"Root {root}"], "some.host", "en-US"); + SetRequestHost("some.host"); + + var publishedContent = GetPublishedContent(_contentByName[$"Root {root}"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual("/", route.Path); + Assert.AreEqual($"root-{root}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, route.StartItem.Id); + }); + } + + [TestCase(1, "/")] + [TestCase(2, "/root-2")] + [TestCase(3, "/root-3")] + [ConfigureBuilder(ActionName = nameof(ConfigureOmitTrailingSlash))] + public void Root_Without_Trailing_Slash(int root, string expectedPath) + { + var publishedContent = GetPublishedContent(_contentByName[$"Root {root}"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual(expectedPath, route.Path); + Assert.AreEqual($"root-{root}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, route.StartItem.Id); + }); + } + + [TestCase(1, 1)] + [TestCase(2, 2)] + [TestCase(3, 3)] + [TestCase(1, 2)] + [TestCase(2, 3)] + [TestCase(3, 1)] + [ConfigureBuilder(ActionName = nameof(ConfigureOmitTrailingSlash))] + public void Child_Without_Trailing_Slash(int root, int child) + { + var publishedContent = GetPublishedContent(_contentByName[$"Root {root}/Child {child}"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual($"/child-{child}", route.Path); + Assert.AreEqual($"root-{root}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, route.StartItem.Id); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentRouteBuilderTestBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentRouteBuilderTestBase.cs new file mode 100644 index 0000000000..52a1c3d35d --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentRouteBuilderTestBase.cs @@ -0,0 +1,20 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Tests.Common.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.DeliveryApi.Request; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] +public abstract class ApiContentRouteBuilderTestBase : ApiContentRequestTestBase +{ + protected IPublishedContent GetPublishedContent(Guid key) + { + UmbracoContextAccessor.Clear(); + var umbracoContext = UmbracoContextFactory.EnsureUmbracoContext().UmbracoContext; + var publishedContent = umbracoContext.Content?.GetById(key); + Assert.IsNotNull(publishedContent); + + return publishedContent; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentRouteBuilderVariantTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentRouteBuilderVariantTests.cs new file mode 100644 index 0000000000..d268d3f196 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentRouteBuilderVariantTests.cs @@ -0,0 +1,287 @@ +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Integration.Attributes; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.DeliveryApi.Request; + +public class ApiContentRouteBuilderVariantTests : ApiContentRouteBuilderTestBase +{ + private readonly Dictionary _contentByName = new (); + + public static void ConfigureIncludeTopLevelNodeInPath(IUmbracoBuilder builder) + => builder.Services.Configure(config => config.HideTopLevelNodeFromPath = false); + + public static void ConfigureOmitTrailingSlash(IUmbracoBuilder builder) + => builder.Services.Configure(config => config.AddTrailingSlash = false); + + [SetUp] + public async Task SetUpTest() + { + SetRequestHost("localhost"); + + if (_contentByName.Any()) + { + // these tests all run on the same DB to make them run faster, so we need to get the cache in a + // predictable state with each test run. + RefreshContentCache(); + return; + } + + await GetRequiredService().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); + contentType.AllowedContentTypes = [new() { Alias = contentType.Alias, Key = contentType.Key }]; + await ContentTypeService.UpdateAsync(contentType, Constants.Security.SuperUserKey); + foreach (var rootNumber in Enumerable.Range(1, 3)) + { + var root = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName("en-US", $"Root {rootNumber} en-US") + .WithCultureName("da-DK", $"Root {rootNumber} da-DK") + .Build(); + ContentService.Save(root); + ContentService.Publish(root, ["*"]); + _contentByName[$"Root {rootNumber}"] = root; + + foreach (var childNumber in Enumerable.Range(1, 3)) + { + var child = new ContentBuilder() + .WithContentType(contentType) + .WithParent(root) + .WithCultureName("en-US", $"Child {childNumber} en-US") + .WithCultureName("da-DK", $"Child {childNumber} da-DK") + .Build(); + ContentService.Save(child); + ContentService.Publish(child, ["*"]); + _contentByName[$"Root {rootNumber}/Child {childNumber}"] = child; + + foreach (var grandchildNumber in Enumerable.Range(1, 3)) + { + var grandchild = new ContentBuilder() + .WithContentType(contentType) + .WithParent(child) + .WithCultureName("en-US", $"Grandchild {grandchildNumber} en-US") + .WithCultureName("da-DK", $"Grandchild {grandchildNumber} da-DK") + .Build(); + ContentService.Save(grandchild); + ContentService.Publish(grandchild, ["*"]); + _contentByName[$"Root {rootNumber}/Child {childNumber}/Grandchild {grandchildNumber}"] = grandchild; + } + } + } + } + + [TestCase("da-DK")] + [TestCase("en-US")] + public void First_Root(string culture) + { + SetVariationContext(culture); + var publishedContent = GetPublishedContent(_contentByName[$"Root 1"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual("/", route.Path); + Assert.AreEqual($"root-1-{culture.ToLowerInvariant()}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root 1"].Key, route.StartItem.Id); + }); + } + + [TestCase("da-DK")] + [TestCase("en-US")] + public void Last_Root(string culture) + { + SetVariationContext(culture); + var publishedContent = GetPublishedContent(_contentByName["Root 3"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual($"/root-3-{culture.ToLowerInvariant()}/", route.Path); + Assert.AreEqual($"root-3-{culture.ToLowerInvariant()}", route.StartItem.Path); + Assert.AreEqual(_contentByName["Root 3"].Key, route.StartItem.Id); + }); + } + + [TestCase(1, "en-US")] + [TestCase(1, "da-DK")] + [TestCase(2, "en-US")] + [TestCase(2, "da-DK")] + [TestCase(3, "en-US")] + [TestCase(3, "da-DK")] + public void First_Child(int root, string culture) + { + SetVariationContext(culture); + var publishedContent = GetPublishedContent(_contentByName[$"Root {root}/Child 1"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual($"/child-1-{culture.ToLowerInvariant()}/", route.Path); + Assert.AreEqual($"root-{root}-{culture.ToLowerInvariant()}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, route.StartItem.Id); + }); + } + + [TestCase(1, "en-US")] + [TestCase(1, "da-DK")] + [TestCase(2, "en-US")] + [TestCase(2, "da-DK")] + [TestCase(3, "en-US")] + [TestCase(3, "da-DK")] + public void Last_Child(int root, string culture) + { + SetVariationContext(culture); + var publishedContent = GetPublishedContent(_contentByName[$"Root {root}/Child 3"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual($"/child-3-{culture.ToLowerInvariant()}/", route.Path); + Assert.AreEqual($"root-{root}-{culture.ToLowerInvariant()}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, route.StartItem.Id); + }); + } + + [TestCase(1, "en-US")] + [TestCase(1, "da-DK")] + [TestCase(2, "en-US")] + [TestCase(2, "da-DK")] + [TestCase(3, "en-US")] + [TestCase(3, "da-DK")] + public void First_Grandchild(int root, string culture) + { + SetVariationContext(culture); + var publishedContent = GetPublishedContent(_contentByName[$"Root {root}/Child 1/Grandchild 1"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual($"/child-1-{culture.ToLowerInvariant()}/grandchild-1-{culture.ToLowerInvariant()}/", route.Path); + Assert.AreEqual($"root-{root}-{culture.ToLowerInvariant()}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, route.StartItem.Id); + }); + } + + [TestCase(1, "en-US")] + [TestCase(1, "da-DK")] + [TestCase(2, "en-US")] + [TestCase(2, "da-DK")] + [TestCase(3, "en-US")] + [TestCase(3, "da-DK")] + public void Last_Grandchild(int root, string culture) + { + SetVariationContext(culture); + var publishedContent = GetPublishedContent(_contentByName[$"Root {root}/Child 3/Grandchild 3"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual($"/child-3-{culture.ToLowerInvariant()}/grandchild-3-{culture.ToLowerInvariant()}/", route.Path); + Assert.AreEqual($"root-{root}-{culture.ToLowerInvariant()}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, route.StartItem.Id); + }); + } + + [TestCase(1, "en-US")] + [TestCase(1, "da-DK")] + [TestCase(2, "en-US")] + [TestCase(2, "da-DK")] + [TestCase(3, "en-US")] + [TestCase(3, "da-DK")] + [ConfigureBuilder(ActionName = nameof(ConfigureIncludeTopLevelNodeInPath))] + public void Root_With_Top_Level_Node_Included(int root, string culture) + { + SetVariationContext(culture); + var publishedContent = GetPublishedContent(_contentByName[$"Root {root}"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual("/", route.Path); + Assert.AreEqual($"root-{root}-{culture.ToLowerInvariant()}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, route.StartItem.Id); + }); + } + + [TestCase(1, "en-US")] + [TestCase(1, "da-DK")] + [TestCase(2, "en-US")] + [TestCase(2, "da-DK")] + [TestCase(3, "en-US")] + [TestCase(3, "da-DK")] + public async Task Root_With_Domain_Bindings(int root, string culture) + { + await SetContentHost(_contentByName[$"Root {root}"], "some.host", culture); + SetRequestHost("some.host"); + SetVariationContext(culture); + var publishedContent = GetPublishedContent(_contentByName[$"Root {root}"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual("/", route.Path); + Assert.AreEqual($"root-{root}-{culture.ToLowerInvariant()}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, route.StartItem.Id); + }); + } + + [TestCase(1, "en-US", "/")] + [TestCase(1, "da-DK", "/")] + [TestCase(2, "en-US", "/root-2-en-us")] + [TestCase(2, "da-DK", "/root-2-da-dk")] + [TestCase(3, "en-US", "/root-3-en-us")] + [TestCase(3, "da-DK", "/root-3-da-dk")] + [ConfigureBuilder(ActionName = nameof(ConfigureOmitTrailingSlash))] + public void Root_Without_Trailing_Slash(int root, string culture, string expectedPath) + { + SetVariationContext(culture); + var publishedContent = GetPublishedContent(_contentByName[$"Root {root}"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual(expectedPath, route.Path); + Assert.AreEqual($"root-{root}-{culture.ToLowerInvariant()}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, route.StartItem.Id); + }); + } + + [TestCase(1, 1, "en-US")] + [TestCase(1, 1, "da-DK")] + [TestCase(2, 2, "en-US")] + [TestCase(2, 2, "da-DK")] + [TestCase(3, 3, "en-US")] + [TestCase(3, 3, "da-DK")] + [TestCase(1, 2, "en-US")] + [TestCase(1, 2, "da-DK")] + [TestCase(2, 3, "en-US")] + [TestCase(2, 3, "da-DK")] + [TestCase(3, 1, "en-US")] + [TestCase(3, 1, "da-DK")] + [ConfigureBuilder(ActionName = nameof(ConfigureOmitTrailingSlash))] + public void Child_Without_Trailing_Slash(int root, int child, string culture) + { + SetVariationContext(culture); + var publishedContent = GetPublishedContent(_contentByName[$"Root {root}/Child {child}"].Key); + var route = ApiContentRouteBuilder.Build(publishedContent); + Assert.IsNotNull(route); + Assert.Multiple(() => + { + Assert.AreEqual($"/child-{child}-{culture.ToLowerInvariant()}", route.Path); + Assert.AreEqual($"root-{root}-{culture.ToLowerInvariant()}", route.StartItem.Path); + Assert.AreEqual(_contentByName[$"Root {root}"].Key, route.StartItem.Id); + }); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs index 99ccbd7a61..91b67673d9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs @@ -48,7 +48,7 @@ public class ContentBuilderTests : DeliveryApiTests Assert.NotNull(result); Assert.AreEqual("The page", result.Name); Assert.AreEqual("thePageType", result.ContentType); - Assert.AreEqual("/url:url-segment", result.Route.Path); + Assert.AreEqual("/url:url-segment/", result.Route.Path); Assert.AreEqual(key, result.Id); Assert.AreEqual(2, result.Properties.Count); Assert.AreEqual("Delivery API value", result.Properties["deliveryApi"]); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs index c876278eec..1aca987862 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs @@ -40,7 +40,7 @@ public class ContentPickerValueConverterTests : PropertyValueConverterTests Assert.NotNull(result); Assert.AreEqual("The page", result.Name); Assert.AreEqual(PublishedContent.Key, result.Id); - Assert.AreEqual("/the-page-url", result.Route.Path); + Assert.AreEqual("/the-page-url/", result.Route.Path); Assert.AreEqual("TheContentType", result.ContentType); Assert.IsEmpty(result.Properties); } @@ -103,7 +103,7 @@ public class ContentPickerValueConverterTests : PropertyValueConverterTests Assert.NotNull(result); Assert.AreEqual("The page", result.Name); Assert.AreEqual(content.Object.Key, result.Id); - Assert.AreEqual("/page-url-segment", result.Route.Path); + Assert.AreEqual("/page-url-segment/", result.Route.Path); Assert.AreEqual("TheContentType", result.ContentType); Assert.AreEqual(2, result.Properties.Count); Assert.AreEqual("Delivery API value", result.Properties[DeliveryApiPropertyType.Alias]); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs index 9da66ba10c..03da8c6989 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs @@ -50,7 +50,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests IEnumerable ancestorsKeys = [rootKey]; navigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); - var contentCache = CreatePublishedContentCache("#"); + var contentCache = CreatePublishedContentCache(); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); @@ -77,7 +77,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests var grandchildKey = Guid.NewGuid(); var grandchild = SetupInvariantPublishedContent("The Grandchild", grandchildKey, navigationQueryServiceMock, child); - var contentCache = CreatePublishedContentCache("#"); + var contentCache = CreatePublishedContentCache(); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), grandchild.Key)).Returns(grandchild); @@ -104,7 +104,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests var childKey = Guid.NewGuid(); var child = SetupVariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); - var contentCache = CreatePublishedContentCache("#"); + var contentCache = CreatePublishedContentCache(); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); @@ -136,7 +136,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests var childKey = Guid.NewGuid(); var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); - var contentCache = CreatePublishedContentCache("#"); + var contentCache = CreatePublishedContentCache(); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); @@ -168,7 +168,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests var childKey = Guid.NewGuid(); var child = SetupVariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); - var contentCache = CreatePublishedContentCache("#"); + var contentCache = CreatePublishedContentCache(); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); @@ -209,7 +209,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests { var result = GetUnRoutableRoute(resolvedUrl, "/the/content/route"); Assert.IsNotNull(result); - Assert.AreEqual("/the/content/route", result.Path); + Assert.AreEqual("/the/content/route/", result.Path); } [TestCase("")] @@ -266,7 +266,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests var childKey = Guid.NewGuid(); var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root, false); - var contentCache = CreatePublishedContentCache("#"); + var contentCache = CreatePublishedContentCache(); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); @@ -293,7 +293,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests var childKey = Guid.NewGuid(); var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root, false); - var contentCache = CreatePublishedContentCache("#"); + var contentCache = CreatePublishedContentCache(); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); @@ -321,7 +321,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests var requestPreviewServiceMock = new Mock(); requestPreviewServiceMock.Setup(m => m.IsPreview()).Returns(isPreview); - var contentCache = CreatePublishedContentCache("#"); + var contentCache = CreatePublishedContentCache(); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); @@ -355,7 +355,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests var childKey = Guid.NewGuid(); var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); - var contentCache = CreatePublishedContentCache("#"); + var contentCache = CreatePublishedContentCache(); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); @@ -460,7 +460,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests var requestPreviewServiceMock = new Mock(); requestPreviewServiceMock.Setup(m => m.IsPreview()).Returns(isPreview); - contentCache ??= CreatePublishedContentCache("#"); + contentCache ??= CreatePublishedContentCache(); apiContentPathProvider ??= SetupApiContentPathProvider(hideTopLevelNodeFromPath, contentCache, navigationQueryService); return CreateContentRouteBuilder( @@ -480,25 +480,24 @@ public class ContentRouteBuilderTests : DeliveryApiTests .Returns(publishedUrl); var contentPathProvider = new ApiContentPathProvider(publishedUrlProviderMock.Object); - var contentCache = CreatePublishedContentCache(routeById); + var contentCache = CreatePublishedContentCache(); var navigationQueryServiceMock = new Mock(); var content = SetupVariantPublishedContent("The Content", Guid.NewGuid(), navigationQueryServiceMock); + var documentUrlServiceMock = new Mock(); + documentUrlServiceMock + .Setup(m => m.GetLegacyRouteFormat(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(routeById); + var builder = CreateContentRouteBuilder( contentPathProvider, CreateGlobalSettings(), - contentCache: contentCache); + contentCache: contentCache, + documentUrlService: documentUrlServiceMock.Object); return builder.Build(content); } - private IPublishedContentCache CreatePublishedContentCache(string routeById) - { - var publishedContentCacheMock = new Mock(); - publishedContentCacheMock - .Setup(c => c.GetRouteById(It.IsAny(), It.IsAny())) - .Returns(routeById); - - return publishedContentCacheMock.Object; - } + private IPublishedContentCache CreatePublishedContentCache() + => Mock.Of(); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs index 08c09c92f2..feac0d6096 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.DeliveryApi; using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; @@ -128,7 +129,8 @@ public class DeliveryApiTests IOptionsMonitor? requestHandlerSettingsMonitor = null, IPublishedContentCache? contentCache = null, IDocumentNavigationQueryService? navigationQueryService = null, - IPublishStatusQueryService? publishStatusQueryService = null) + IPublishStatusQueryService? publishStatusQueryService = null, + IDocumentUrlService? documentUrlService = null) { if (requestHandlerSettingsMonitor == null) { @@ -145,6 +147,7 @@ public class DeliveryApiTests requestHandlerSettingsMonitor, contentCache ?? Mock.Of(), navigationQueryService ?? Mock.Of(), - publishStatusQueryService ?? PublishStatusQueryService); + publishStatusQueryService ?? PublishStatusQueryService, + documentUrlService ?? Mock.Of()); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs index 4b6c052392..9d7be346aa 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs @@ -59,7 +59,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest Assert.AreEqual(1, result.Count()); Assert.AreEqual(PublishedContent.Name, result.First().Name); Assert.AreEqual(PublishedContent.Key, result.First().Id); - Assert.AreEqual("/the-page-url", result.First().Route.Path); + Assert.AreEqual("/the-page-url/", result.First().Route.Path); Assert.AreEqual("TheContentType", result.First().ContentType); Assert.IsEmpty(result.First().Properties); } @@ -86,7 +86,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest Assert.AreEqual(PublishedContent.Name, result.First().Name); Assert.AreEqual(PublishedContent.Key, result.First().Id); - Assert.AreEqual("/the-page-url", result.First().Route.Path); + Assert.AreEqual("/the-page-url/", result.First().Route.Path); Assert.AreEqual("TheContentType", result.First().ContentType); Assert.AreEqual("The other page", result.Last().Name); @@ -130,7 +130,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest Assert.AreEqual(1, result.Count()); Assert.AreEqual("The page", result.First().Name); Assert.AreEqual(key, result.First().Id); - Assert.AreEqual("/page-url-segment", result.First().Route.Path); + Assert.AreEqual("/page-url-segment/", result.First().Route.Path); Assert.AreEqual("TheContentType", result.First().ContentType); Assert.AreEqual(2, result.First().Properties.Count); Assert.AreEqual("Delivery API value", result.First().Properties[DeliveryApiPropertyType.Alias]); @@ -204,7 +204,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest Assert.AreEqual(1, result.Count()); Assert.AreEqual(PublishedContent.Name, result.First().Name); Assert.AreEqual(PublishedContent.Key, result.First().Id); - Assert.AreEqual("/the-page-url", result.First().Route.Path); + Assert.AreEqual("/the-page-url/", result.First().Route.Path); Assert.AreEqual("TheContentType", result.First().ContentType); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiUrlPickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiUrlPickerValueConverterTests.cs index 4547078d15..d9ede7b56e 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiUrlPickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiUrlPickerValueConverterTests.cs @@ -46,7 +46,7 @@ public class MultiUrlPickerValueConverterTests : PropertyValueConverterTests Assert.Null(link.Target); var route = link.Route; Assert.NotNull(route); - Assert.AreEqual("/the-page-url", route.Path); + Assert.AreEqual("/the-page-url/", route.Path); } [Test] @@ -221,7 +221,7 @@ public class MultiUrlPickerValueConverterTests : PropertyValueConverterTests var link = result.First(); Assert.AreEqual("Custom link name", link.Title); Assert.AreEqual(PublishedContent.Key, link.DestinationId); - Assert.AreEqual("/the-page-url", link.Route!.Path); + Assert.AreEqual("/the-page-url/", link.Route!.Path); Assert.AreEqual(LinkType.Content, link.LinkType); Assert.AreEqual("_blank", link.Target); Assert.AreEqual("?something=true", link.QueryString); @@ -252,7 +252,7 @@ public class MultiUrlPickerValueConverterTests : PropertyValueConverterTests var link = result.First(); Assert.AreEqual(PublishedContent.Name, link.Title); Assert.AreEqual(PublishedContent.Key, link.DestinationId); - Assert.AreEqual("/the-page-url", link.Route!.Path); + Assert.AreEqual("/the-page-url/", link.Route!.Path); Assert.AreEqual(LinkType.Content, link.LinkType); Assert.Null(link.Target); Assert.Null(link.Url); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs index 2751cf6f13..59238902f7 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; @@ -127,7 +128,8 @@ public class PropertyValueConverterTests : DeliveryApiTests IOptionsMonitor? requestHandlerSettingsMonitor = null, IPublishedContentCache? contentCache = null, IDocumentNavigationQueryService? navigationQueryService = null, - IPublishStatusQueryService? publishStatusQueryService = null) + IPublishStatusQueryService? publishStatusQueryService = null, + IDocumentUrlService? documentUrlService = null) { contentCache ??= PublishedContentCacheMock.Object; navigationQueryService ??= DocumentNavigationQueryServiceMock.Object; @@ -140,6 +142,7 @@ public class PropertyValueConverterTests : DeliveryApiTests requestHandlerSettingsMonitor, contentCache, navigationQueryService, - publishStatusQueryService); + publishStatusQueryService, + documentUrlService); } }