From fde1bd4710c7ceb4dcee44ea6b145acb014823a7 Mon Sep 17 00:00:00 2001 From: nikolajlauridsen Date: Tue, 14 Jan 2025 13:58:59 +0100 Subject: [PATCH 1/4] Make URL overview align with the old routing This means including custom url providers, other URLS, etc. --- .../Factories/DocumentUrlFactory.cs | 131 +++++++++++++++++- 1 file changed, 128 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs index 6d0e40e2b100..db3c3a091c2b 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -15,17 +16,101 @@ namespace Umbraco.Cms.Api.Management.Factories; public class DocumentUrlFactory : IDocumentUrlFactory { - private readonly IDocumentUrlService _documentUrlService; + private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly ILanguageService _languageService; + private readonly IPublishedRouter _publishedRouter; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly ILocalizedTextService _localizedTextService; + private readonly ILogger _logger; + private readonly UriUtility _uriUtility; + [Obsolete("Use the constructor that takes all dependencies, scheduled for removal in v16")] public DocumentUrlFactory(IDocumentUrlService documentUrlService) + : this( + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService>(), + StaticServiceProvider.Instance.GetRequiredService() + ) { - _documentUrlService = documentUrlService; + } + + [Obsolete("Use the constructor that takes all dependencies, scheduled for removal in v16")] + public DocumentUrlFactory( + IDocumentUrlService documentUrlService, + IPublishedUrlProvider publishedUrlProvider, + ILanguageService languageService, + IPublishedRouter publishedRouter, + IUmbracoContextAccessor umbracoContextAccessor, + ILocalizedTextService localizedTextService, + ILogger logger, + UriUtility uriUtility) + : this(publishedUrlProvider, + languageService, + publishedRouter, + umbracoContextAccessor, + localizedTextService, + logger, + uriUtility) + { + _publishedUrlProvider = publishedUrlProvider; + _languageService = languageService; + _publishedRouter = publishedRouter; + _umbracoContextAccessor = umbracoContextAccessor; + _localizedTextService = localizedTextService; + _logger = logger; + _uriUtility = uriUtility; + } + + public DocumentUrlFactory( + IPublishedUrlProvider publishedUrlProvider, + ILanguageService languageService, + IPublishedRouter publishedRouter, + IUmbracoContextAccessor umbracoContextAccessor, + ILocalizedTextService localizedTextService, + ILogger logger, + UriUtility uriUtility) + { + _publishedUrlProvider = publishedUrlProvider; + _languageService = languageService; + _publishedRouter = publishedRouter; + _umbracoContextAccessor = umbracoContextAccessor; + _localizedTextService = localizedTextService; + _logger = logger; + _uriUtility = uriUtility; } public async Task> CreateUrlsAsync(IContent content) { - IEnumerable urlInfos = await _documentUrlService.ListUrlsAsync(content.Key); + HashSet urlInfos = new(); + var cultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode).ToArray(); + + // First we get the urls of all cultures, using the published router, meaning we respect any extensions. + foreach (var culture in cultures) + { + var url = _publishedUrlProvider.GetUrl(content.Key, culture: culture); + // Check for collision + Attempt hasCollision = await VerifyCollisionAsync(content, url, culture); + + if (hasCollision.Success && hasCollision.Result is not null) + { + urlInfos.Add(hasCollision.Result); + continue; + } + + urlInfos.Add(UrlInfo.Url(url, culture)); + } + + // Then get "other" urls - I.E. Not what you'd get with GetUrl(), this includes all the urls registered using domains. + // for these 'other' URLs, we don't check whether they are routable, collide, anything - we just report them. + foreach (UrlInfo otherUrl in _publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text).ThenBy(x => x.Culture)) + { + urlInfos.Add(otherUrl); + } return urlInfos .Where(urlInfo => urlInfo.IsUrl) @@ -33,6 +118,46 @@ public async Task> CreateUrlsAsync(IContent content .ToArray(); } + private async Task> VerifyCollisionAsync(IContent content, string url, string culture) + { + var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute); + if (uri.IsAbsoluteUri is false) + { + uri = uri.MakeAbsolute(_umbracoContextAccessor.GetRequiredUmbracoContext().CleanedUmbracoUrl); + } + + uri = _uriUtility.UriToUmbraco(uri); + IPublishedRequestBuilder builder = await _publishedRouter.CreateRequestAsync(uri); + IPublishedRequest publishedRequest = await _publishedRouter.RouteRequestAsync(builder, new RouteRequestOptions(RouteDirection.Outbound)); + + if (publishedRequest.HasPublishedContent() is false) + { + if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + { + const string logMsg = nameof(VerifyCollisionAsync) + + " did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}"; + _logger.LogDebug(logMsg, url, uri, culture); + } + + var urlInfo = UrlInfo.Message(_localizedTextService.Localize("content", "routeErrorCannotRoute"), culture); + return Attempt.Succeed(urlInfo); + } + + if (publishedRequest.IgnorePublishedContentCollisions) + { + return Attempt.Fail(); + } + + if (publishedRequest.PublishedContent?.Id != content.Id) + { + var urlInfo = UrlInfo.Message(_localizedTextService.Localize("content", "routeError"), culture); + return Attempt.Succeed(urlInfo); + } + + // No collision + return Attempt.Fail(); + } + public async Task> CreateUrlSetsAsync(IEnumerable contentItems) { var documentUrlInfoResourceSets = new List(); From c5c6fa99ca5722d01d5a4ccb0ea3c7a64e5b6fde Mon Sep 17 00:00:00 2001 From: nikolajlauridsen Date: Wed, 15 Jan 2025 13:33:41 +0100 Subject: [PATCH 2/4] Move implementation to its own provider --- .../Factories/DocumentUrlFactory.cs | 134 ++---------------- .../Factories/IDocumentUrlFactory.cs | 1 + .../DependencyInjection/UmbracoBuilder.cs | 1 + .../Routing/IPublishedUrlInfoProvider.cs | 13 ++ .../Routing/PublishedUrlInfoProvider.cs | 108 ++++++++++++++ .../Services/DocumentUrlService.cs | 1 + 6 files changed, 132 insertions(+), 126 deletions(-) create mode 100644 src/Umbraco.Core/Routing/IPublishedUrlInfoProvider.cs create mode 100644 src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs index db3c3a091c2b..ba6ebfaf9a0e 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs @@ -1,116 +1,38 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Umbraco.Cms.Api.Management.ViewModels.Document; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; -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; -using Umbraco.Cms.Core.Web; -using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Factories; public class DocumentUrlFactory : IDocumentUrlFactory { - private readonly IPublishedUrlProvider _publishedUrlProvider; - private readonly ILanguageService _languageService; - private readonly IPublishedRouter _publishedRouter; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly ILocalizedTextService _localizedTextService; - private readonly ILogger _logger; - private readonly UriUtility _uriUtility; + private readonly IPublishedUrlInfoProvider _publishedUrlInfoProvider; [Obsolete("Use the constructor that takes all dependencies, scheduled for removal in v16")] public DocumentUrlFactory(IDocumentUrlService documentUrlService) - : this( - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService>(), - StaticServiceProvider.Instance.GetRequiredService() - ) + : this(StaticServiceProvider.Instance.GetRequiredService()) { } [Obsolete("Use the constructor that takes all dependencies, scheduled for removal in v16")] - public DocumentUrlFactory( - IDocumentUrlService documentUrlService, - IPublishedUrlProvider publishedUrlProvider, - ILanguageService languageService, - IPublishedRouter publishedRouter, - IUmbracoContextAccessor umbracoContextAccessor, - ILocalizedTextService localizedTextService, - ILogger logger, - UriUtility uriUtility) - : this(publishedUrlProvider, - languageService, - publishedRouter, - umbracoContextAccessor, - localizedTextService, - logger, - uriUtility) + public DocumentUrlFactory(IDocumentUrlService documentUrlService, IPublishedUrlInfoProvider publishedUrlInfoProvider) + : this(publishedUrlInfoProvider) { - _publishedUrlProvider = publishedUrlProvider; - _languageService = languageService; - _publishedRouter = publishedRouter; - _umbracoContextAccessor = umbracoContextAccessor; - _localizedTextService = localizedTextService; - _logger = logger; - _uriUtility = uriUtility; + } - public DocumentUrlFactory( - IPublishedUrlProvider publishedUrlProvider, - ILanguageService languageService, - IPublishedRouter publishedRouter, - IUmbracoContextAccessor umbracoContextAccessor, - ILocalizedTextService localizedTextService, - ILogger logger, - UriUtility uriUtility) + public DocumentUrlFactory(IPublishedUrlInfoProvider publishedUrlInfoProvider) { - _publishedUrlProvider = publishedUrlProvider; - _languageService = languageService; - _publishedRouter = publishedRouter; - _umbracoContextAccessor = umbracoContextAccessor; - _localizedTextService = localizedTextService; - _logger = logger; - _uriUtility = uriUtility; + _publishedUrlInfoProvider = publishedUrlInfoProvider; } public async Task> CreateUrlsAsync(IContent content) { - HashSet urlInfos = new(); - var cultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode).ToArray(); - - // First we get the urls of all cultures, using the published router, meaning we respect any extensions. - foreach (var culture in cultures) - { - var url = _publishedUrlProvider.GetUrl(content.Key, culture: culture); - // Check for collision - Attempt hasCollision = await VerifyCollisionAsync(content, url, culture); - - if (hasCollision.Success && hasCollision.Result is not null) - { - urlInfos.Add(hasCollision.Result); - continue; - } - - urlInfos.Add(UrlInfo.Url(url, culture)); - } - - // Then get "other" urls - I.E. Not what you'd get with GetUrl(), this includes all the urls registered using domains. - // for these 'other' URLs, we don't check whether they are routable, collide, anything - we just report them. - foreach (UrlInfo otherUrl in _publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text).ThenBy(x => x.Culture)) - { - urlInfos.Add(otherUrl); - } + ISet urlInfos = await _publishedUrlInfoProvider.GetAllAsync(content); return urlInfos .Where(urlInfo => urlInfo.IsUrl) @@ -118,46 +40,6 @@ public async Task> CreateUrlsAsync(IContent content .ToArray(); } - private async Task> VerifyCollisionAsync(IContent content, string url, string culture) - { - var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute); - if (uri.IsAbsoluteUri is false) - { - uri = uri.MakeAbsolute(_umbracoContextAccessor.GetRequiredUmbracoContext().CleanedUmbracoUrl); - } - - uri = _uriUtility.UriToUmbraco(uri); - IPublishedRequestBuilder builder = await _publishedRouter.CreateRequestAsync(uri); - IPublishedRequest publishedRequest = await _publishedRouter.RouteRequestAsync(builder, new RouteRequestOptions(RouteDirection.Outbound)); - - if (publishedRequest.HasPublishedContent() is false) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - const string logMsg = nameof(VerifyCollisionAsync) + - " did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}"; - _logger.LogDebug(logMsg, url, uri, culture); - } - - var urlInfo = UrlInfo.Message(_localizedTextService.Localize("content", "routeErrorCannotRoute"), culture); - return Attempt.Succeed(urlInfo); - } - - if (publishedRequest.IgnorePublishedContentCollisions) - { - return Attempt.Fail(); - } - - if (publishedRequest.PublishedContent?.Id != content.Id) - { - var urlInfo = UrlInfo.Message(_localizedTextService.Localize("content", "routeError"), culture); - return Attempt.Succeed(urlInfo); - } - - // No collision - return Attempt.Fail(); - } - public async Task> CreateUrlSetsAsync(IEnumerable contentItems) { var documentUrlInfoResourceSets = new List(); diff --git a/src/Umbraco.Cms.Api.Management/Factories/IDocumentUrlFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IDocumentUrlFactory.cs index f87a0aaec8bf..a64d06970409 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/IDocumentUrlFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/IDocumentUrlFactory.cs @@ -6,5 +6,6 @@ namespace Umbraco.Cms.Api.Management.Factories; public interface IDocumentUrlFactory { Task> CreateUrlsAsync(IContent content); + Task> CreateUrlSetsAsync(IEnumerable contentItems); } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 78692dff98d1..11c12f1b82e4 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -238,6 +238,7 @@ private void AddCoreServices() // register published router Services.AddUnique(); + Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); diff --git a/src/Umbraco.Core/Routing/IPublishedUrlInfoProvider.cs b/src/Umbraco.Core/Routing/IPublishedUrlInfoProvider.cs new file mode 100644 index 000000000000..aa3d322381ff --- /dev/null +++ b/src/Umbraco.Core/Routing/IPublishedUrlInfoProvider.cs @@ -0,0 +1,13 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Routing; + +public interface IPublishedUrlInfoProvider +{ + /// + /// Gets all published urls for a content item. + /// + /// The content to get urls for. + /// Set of all published url infos. + Task> GetAllAsync(IContent content); +} diff --git a/src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs b/src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs new file mode 100644 index 000000000000..c7e8e6b17214 --- /dev/null +++ b/src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs @@ -0,0 +1,108 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Routing; + +public class PublishedUrlInfoProvider : IPublishedUrlInfoProvider +{ + private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly ILanguageService _languageService; + private readonly IPublishedRouter _publishedRouter; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly ILocalizedTextService _localizedTextService; + private readonly ILogger _logger; + private readonly UriUtility _uriUtility; + + public PublishedUrlInfoProvider( + IPublishedUrlProvider publishedUrlProvider, + ILanguageService languageService, + IPublishedRouter publishedRouter, + IUmbracoContextAccessor umbracoContextAccessor, + ILocalizedTextService localizedTextService, + ILogger logger, + UriUtility uriUtility) + { + _publishedUrlProvider = publishedUrlProvider; + _languageService = languageService; + _publishedRouter = publishedRouter; + _umbracoContextAccessor = umbracoContextAccessor; + _localizedTextService = localizedTextService; + _logger = logger; + _uriUtility = uriUtility; + } + + /// + public async Task> GetAllAsync(IContent content) + { + HashSet urlInfos = []; + var cultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode).ToArray(); + + // First we get the urls of all cultures, using the published router, meaning we respect any extensions. + foreach (var culture in cultures) + { + var url = _publishedUrlProvider.GetUrl(content.Key, culture: culture); + // Check for collision + Attempt hasCollision = await VerifyCollisionAsync(content, url, culture); + + if (hasCollision is { Success: true, Result: not null }) + { + urlInfos.Add(hasCollision.Result); + continue; + } + + urlInfos.Add(UrlInfo.Url(url, culture)); + } + + // Then get "other" urls - I.E. Not what you'd get with GetUrl(), this includes all the urls registered using domains. + // for these 'other' URLs, we don't check whether they are routable, collide, anything - we just report them. + foreach (UrlInfo otherUrl in _publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text).ThenBy(x => x.Culture)) + { + urlInfos.Add(otherUrl); + } + + return urlInfos; + } + + private async Task> VerifyCollisionAsync(IContent content, string url, string culture) + { + var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute); + if (uri.IsAbsoluteUri is false) + { + uri = uri.MakeAbsolute(_umbracoContextAccessor.GetRequiredUmbracoContext().CleanedUmbracoUrl); + } + + uri = _uriUtility.UriToUmbraco(uri); + IPublishedRequestBuilder builder = await _publishedRouter.CreateRequestAsync(uri); + IPublishedRequest publishedRequest = await _publishedRouter.RouteRequestAsync(builder, new RouteRequestOptions(RouteDirection.Outbound)); + + if (publishedRequest.HasPublishedContent() is false) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + const string logMsg = nameof(VerifyCollisionAsync) + + " did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}"; + _logger.LogDebug(logMsg, url, uri, culture); + } + + var urlInfo = UrlInfo.Message(_localizedTextService.Localize("content", "routeErrorCannotRoute"), culture); + return Attempt.Succeed(urlInfo); + } + + if (publishedRequest.IgnorePublishedContentCollisions) + { + return Attempt.Fail(); + } + + if (publishedRequest.PublishedContent?.Id != content.Id) + { + var urlInfo = UrlInfo.Message(_localizedTextService.Localize("content", "routeError"), culture); + return Attempt.Succeed(urlInfo); + } + + // No collision + return Attempt.Fail(); + } +} diff --git a/src/Umbraco.Core/Services/DocumentUrlService.cs b/src/Umbraco.Core/Services/DocumentUrlService.cs index baeb520df7ac..3dddd1f7257a 100644 --- a/src/Umbraco.Core/Services/DocumentUrlService.cs +++ b/src/Umbraco.Core/Services/DocumentUrlService.cs @@ -525,6 +525,7 @@ public bool HasAny() } + [Obsolete("This method is obsolete and will be removed in future versions. Use IPublishedUrlInfoProvider.GetAllAsync instead.")] public async Task> ListUrlsAsync(Guid contentKey) { var result = new List(); From 2ebe1e59eb5f8b91118ad94092c9ff4ebb73422d Mon Sep 17 00:00:00 2001 From: nikolajlauridsen Date: Wed, 15 Jan 2025 14:19:22 +0100 Subject: [PATCH 3/4] Handle could not get url --- .../Routing/PublishedUrlInfoProvider.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs b/src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs index c7e8e6b17214..d4059bcab806 100644 --- a/src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs +++ b/src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; @@ -15,6 +16,7 @@ public class PublishedUrlInfoProvider : IPublishedUrlInfoProvider private readonly ILocalizedTextService _localizedTextService; private readonly ILogger _logger; private readonly UriUtility _uriUtility; + private readonly IVariationContextAccessor _variationContextAccessor; public PublishedUrlInfoProvider( IPublishedUrlProvider publishedUrlProvider, @@ -23,7 +25,8 @@ public PublishedUrlInfoProvider( IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService localizedTextService, ILogger logger, - UriUtility uriUtility) + UriUtility uriUtility, + IVariationContextAccessor variationContextAccessor) { _publishedUrlProvider = publishedUrlProvider; _languageService = languageService; @@ -32,6 +35,7 @@ public PublishedUrlInfoProvider( _localizedTextService = localizedTextService; _logger = logger; _uriUtility = uriUtility; + _variationContextAccessor = variationContextAccessor; } /// @@ -44,6 +48,14 @@ public async Task> GetAllAsync(IContent content) foreach (var culture in cultures) { var url = _publishedUrlProvider.GetUrl(content.Key, culture: culture); + + // Handle "could not get URL" + if (url is "#" or "#ex") + { + urlInfos.Add(UrlInfo.Message(_localizedTextService.Localize("content", "getUrlException"), culture)); + continue; + } + // Check for collision Attempt hasCollision = await VerifyCollisionAsync(content, url, culture); @@ -98,7 +110,9 @@ public async Task> GetAllAsync(IContent content) if (publishedRequest.PublishedContent?.Id != content.Id) { - var urlInfo = UrlInfo.Message(_localizedTextService.Localize("content", "routeError"), culture); + var collidingContent = publishedRequest.PublishedContent?.Key.ToString(); + + var urlInfo = UrlInfo.Message(_localizedTextService.Localize("content", "routeError", [collidingContent]), culture); return Attempt.Succeed(urlInfo); } From cc86fb8d68b811374748a8f1ce2b6c488384df57 Mon Sep 17 00:00:00 2001 From: mole Date: Wed, 22 Jan 2025 11:16:15 +0100 Subject: [PATCH 4/4] Migrate intergration tests to new implementation --- .../Services/DocumentUrlServiceTest.cs | 38 ------------ ...cumentUrlServiceTest_hidetoplevel_false.cs | 29 --------- .../Services/PublishedUrlInfoProviderTests.cs | 43 +++++++++++++ .../PublishedUrlInfoProviderTestsBase.cs | 62 +++++++++++++++++++ ...ishedUrlInfoProvider_hidetoplevel_false.cs | 45 ++++++++++++++ .../Umbraco.Tests.Integration.csproj | 6 ++ 6 files changed, 156 insertions(+), 67 deletions(-) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTestsBase.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProvider_hidetoplevel_false.cs diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs index 46a983e43560..500b7bdc0d80 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs @@ -213,44 +213,6 @@ public void Unpublished_Pages_Are_not_available() return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); } - [Test] - public async Task Two_items_in_level_1_with_same_name_will_have_conflicting_routes() - { - // Create a second root - var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); - ContentService.Save(secondRoot, -1, contentSchedule); - - // Create a child of second root - var childOfSecondRoot = ContentBuilder.CreateSimpleContent(ContentType, Subpage.Name, secondRoot); - childOfSecondRoot.Key = new Guid("FF6654FB-BC68-4A65-8C6C-135567F50BD6"); - ContentService.Save(childOfSecondRoot, -1, contentSchedule); - - // Publish both the main root and the second root with descendants - ContentService.PublishBranch(Textpage, true, new[] { "*" }); - ContentService.PublishBranch(secondRoot, true, new[] { "*" }); - - var subPageUrls = await DocumentUrlService.ListUrlsAsync(Subpage.Key); - var childOfSecondRootUrls = await DocumentUrlService.ListUrlsAsync(childOfSecondRoot.Key); - - //Assert the url of subpage is correct - Assert.AreEqual(1, subPageUrls.Count()); - Assert.IsTrue(subPageUrls.First().IsUrl); - Assert.AreEqual("/text-page-1", subPageUrls.First().Text); - Assert.AreEqual(Subpage.Key, DocumentUrlService.GetDocumentKeyByRoute("/text-page-1", "en-US", null, false)); - - //Assert the url of child of second root is not exposed - Assert.AreEqual(1, childOfSecondRootUrls.Count()); - Assert.IsFalse(childOfSecondRootUrls.First().IsUrl); - - //Ensure the url without hide top level is not finding the child of second root - Assert.AreNotEqual(childOfSecondRoot.Key, DocumentUrlService.GetDocumentKeyByRoute("/second-root/text-page-1", "en-US", null, false)); - - - - } - - //TODO test cases: // - Find the root, when a domain is set diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest_hidetoplevel_false.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest_hidetoplevel_false.cs index 1944a114fc16..41e3f1897983 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest_hidetoplevel_false.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest_hidetoplevel_false.cs @@ -120,33 +120,4 @@ public override void Setup() return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); } - - [Test] - public async Task Two_items_in_level_1_with_same_name_will_not_have_conflicting_routes() - { - // Create a second root - var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); - ContentService.Save(secondRoot, -1, contentSchedule); - - // Create a child of second root - var childOfSecondRoot = ContentBuilder.CreateSimpleContent(ContentType, Subpage.Name, secondRoot); - childOfSecondRoot.Key = new Guid("FF6654FB-BC68-4A65-8C6C-135567F50BD6"); - ContentService.Save(childOfSecondRoot, -1, contentSchedule); - - // Publish both the main root and the second root with descendants - ContentService.PublishBranch(Textpage, true, new[] { "*" }); - ContentService.PublishBranch(secondRoot, true, new[] { "*" }); - - var subPageUrls = await DocumentUrlService.ListUrlsAsync(Subpage.Key); - var childOfSecondRootUrls = await DocumentUrlService.ListUrlsAsync(childOfSecondRoot.Key); - - Assert.AreEqual(1, subPageUrls.Count()); - Assert.IsTrue(subPageUrls.First().IsUrl); - Assert.AreEqual("/textpage/text-page-1", subPageUrls.First().Text); - - Assert.AreEqual(1, childOfSecondRootUrls.Count()); - Assert.IsTrue(childOfSecondRootUrls.First().IsUrl); - Assert.AreEqual("/second-root/text-page-1", childOfSecondRootUrls.First().Text); - } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTests.cs new file mode 100644 index 000000000000..0b73743b8a2f --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTests.cs @@ -0,0 +1,43 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Tests.Common.Builders; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public class PublishedUrlInfoProviderTests : PublishedUrlInfoProviderTestsBase +{ + + [Test] + public async Task Two_items_in_level_1_with_same_name_will_have_conflicting_routes() + { + // Create a second root + var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + ContentService.Save(secondRoot, -1, contentSchedule); + + // Create a child of second root + var childOfSecondRoot = ContentBuilder.CreateSimpleContent(ContentType, Subpage.Name, secondRoot); + childOfSecondRoot.Key = new Guid("FF6654FB-BC68-4A65-8C6C-135567F50BD6"); + ContentService.Save(childOfSecondRoot, -1, contentSchedule); + + // Publish both the main root and the second root with descendants + ContentService.PublishBranch(Textpage, true, new[] { "*" }); + ContentService.PublishBranch(secondRoot, true, new[] { "*" }); + + var subPageUrls = await PublishedUrlInfoProvider.GetAllAsync(Subpage); + var childOfSecondRootUrls = await PublishedUrlInfoProvider.GetAllAsync(childOfSecondRoot); + + // Assert the url of subpage is correct + Assert.AreEqual(1, subPageUrls.Count); + Assert.IsTrue(subPageUrls.First().IsUrl); + Assert.AreEqual("/text-page-1/", subPageUrls.First().Text); + Assert.AreEqual(Subpage.Key, DocumentUrlService.GetDocumentKeyByRoute("/text-page-1/", "en-US", null, false)); + + // Assert the url of child of second root is not exposed + Assert.AreEqual(1, childOfSecondRootUrls.Count); + Assert.IsFalse(childOfSecondRootUrls.First().IsUrl); + + // Ensure the url without hide top level is not finding the child of second root + Assert.AreNotEqual(childOfSecondRoot.Key, DocumentUrlService.GetDocumentKeyByRoute("/second-root/text-page-1/", "en-US", null, false)); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTestsBase.cs new file mode 100644 index 000000000000..1a27d998af35 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTestsBase.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Tests.Common; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Mock)] +public abstract class PublishedUrlInfoProviderTestsBase : UmbracoIntegrationTestWithContent +{ + protected IDocumentUrlService DocumentUrlService => GetRequiredService(); + + protected IPublishedUrlInfoProvider PublishedUrlInfoProvider => GetRequiredService(); + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.AddUnique(); + builder.AddNotificationHandler(); + builder.Services.AddNotificationAsyncHandler(); + builder.Services.AddUnique(serviceProvider => new TestUmbracoContextAccessor(GetUmbracoContext(serviceProvider))); + builder.Services.AddUnique(CreateHttpContextAccessor()); + } + + public override void Setup() + { + DocumentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult(); + base.Setup(); + } + + private IUmbracoContext GetUmbracoContext(IServiceProvider serviceProvider) + { + var mock = new Mock(); + + mock.Setup(x => x.Content).Returns(serviceProvider.GetRequiredService()); + mock.Setup(x => x.CleanedUmbracoUrl).Returns(new Uri("https://localhost:44339")); + + return mock.Object; + } + + private IHttpContextAccessor CreateHttpContextAccessor() + { + var mock = new Mock(); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "https"; + httpContext.Request.Host = new HostString("localhost"); + + mock.Setup(x => x.HttpContext).Returns(httpContext); + return mock.Object; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProvider_hidetoplevel_false.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProvider_hidetoplevel_false.cs new file mode 100644 index 000000000000..b0f1a03ec1a0 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProvider_hidetoplevel_false.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Tests.Common.Builders; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public class PublishedUrlInfoProvider_hidetoplevel_false : PublishedUrlInfoProviderTestsBase +{ + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.Configure(x => x.HideTopLevelNodeFromPath = false); + base.CustomTestSetup(builder); + } + + [Test] + public async Task Two_items_in_level_1_with_same_name_will_not_have_conflicting_routes() + { + // Create a second root + var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + ContentService.Save(secondRoot, -1, contentSchedule); + + // Create a child of second root + var childOfSecondRoot = ContentBuilder.CreateSimpleContent(ContentType, Subpage.Name, secondRoot); + childOfSecondRoot.Key = new Guid("FF6654FB-BC68-4A65-8C6C-135567F50BD6"); + ContentService.Save(childOfSecondRoot, -1, contentSchedule); + + // Publish both the main root and the second root with descendants + ContentService.PublishBranch(Textpage, true, new[] { "*" }); + ContentService.PublishBranch(secondRoot, true, new[] { "*" }); + + var subPageUrls = await PublishedUrlInfoProvider.GetAllAsync(Subpage); + var childOfSecondRootUrls = await PublishedUrlInfoProvider.GetAllAsync(childOfSecondRoot); + + Assert.AreEqual(1, subPageUrls.Count); + Assert.IsTrue(subPageUrls.First().IsUrl); + Assert.AreEqual("/textpage/text-page-1/", subPageUrls.First().Text); + + Assert.AreEqual(1, childOfSecondRootUrls.Count); + Assert.IsTrue(childOfSecondRootUrls.First().IsUrl); + Assert.AreEqual("/second-root/text-page-1/", childOfSecondRootUrls.First().Text); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 50d67af360c3..9efd93a4bc8a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -247,5 +247,11 @@ MediaNavigationServiceTests.cs + + PublishedUrlInfoProviderTestsBase.cs + + + PublishedUrlInfoProviderTestsBase.cs +