From 295f6f8720b8850d433365af2b6b6bf0eb158826 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Thu, 16 May 2024 15:53:42 +0200 Subject: [PATCH] V14: Backend changes to facilitate Preview mode in Bellissimma (#16279) * Sends GUID instead of the numeric ID for SignalR Preview Hub * Add possibility to set cookies as HttpOnly * Set UMB_PREVIEW cookie as HttpOnly * fixup! Add possibility to set cookies as HttpOnly * Refactor ContentFinderByIdPath to more readable * Create ContentFinderByKeyPath reusing logic from ContentFinderByIdPath * Add a comment to DisableFindContentByIdPath setting * Append new content finder * Change ordering of content finders registrations * Refactor with a base class * Update/refactor and add tests regarding ContentFindersByIdentifier * Fix comment * Avoiding breaking change * Make usages use non-obsolete implementation * Fixed todo in config instead of use the one old legacy name even more. Also obsoleted the ContentFinderByIdPath * add `preview` as an allowed backoffice client route --------- Co-authored-by: Sven Geusens Co-authored-by: Bjarke Berg Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> --- .../Preview/IPreviewHub.cs | 2 +- .../Preview/PreviewHubUpdater.cs | 9 +- .../Routing/BackOfficeAreaRoutes.cs | 2 +- .../Models/WebRoutingSettings.cs | 7 +- .../UmbracoBuilder.Collections.cs | 1 + .../Routing/ContentFinderByIdPath.cs | 91 +++++++--------- .../ContentFinderByIdentifierPathBase.cs | 44 ++++++++ .../Routing/ContentFinderByKeyPath.cs | 102 ++++++++++++++++++ src/Umbraco.Core/Services/PreviewService.cs | 2 +- src/Umbraco.Core/Web/ICookieManager.cs | 5 +- .../Install/InstallHelper.cs | 2 +- .../AspNetCore/AspNetCoreCookieManager.cs | 4 +- .../Profiler/WebProfiler.cs | 2 +- .../Routing/ContentFinderByIdTests.cs | 36 +++---- .../ContentFinderByIdentifierTestsBase.cs | 58 ++++++++++ .../Routing/ContentFinderByKeyTests.cs | 52 +++++++++ 16 files changed, 333 insertions(+), 86 deletions(-) create mode 100644 src/Umbraco.Core/Routing/ContentFinderByIdentifierPathBase.cs create mode 100644 src/Umbraco.Core/Routing/ContentFinderByKeyPath.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByIdentifierTestsBase.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByKeyTests.cs diff --git a/src/Umbraco.Cms.Api.Management/Preview/IPreviewHub.cs b/src/Umbraco.Cms.Api.Management/Preview/IPreviewHub.cs index fe04c028ee..6ea1f808b4 100644 --- a/src/Umbraco.Cms.Api.Management/Preview/IPreviewHub.cs +++ b/src/Umbraco.Cms.Api.Management/Preview/IPreviewHub.cs @@ -5,7 +5,7 @@ public interface IPreviewHub // define methods implemented by client // ReSharper disable InconsistentNaming - Task refreshed(int id); + Task refreshed(Guid key); // ReSharper restore InconsistentNaming } diff --git a/src/Umbraco.Cms.Api.Management/Preview/PreviewHubUpdater.cs b/src/Umbraco.Cms.Api.Management/Preview/PreviewHubUpdater.cs index f88c61ac4a..ac033cdbac 100644 --- a/src/Umbraco.Cms.Api.Management/Preview/PreviewHubUpdater.cs +++ b/src/Umbraco.Cms.Api.Management/Preview/PreviewHubUpdater.cs @@ -26,8 +26,13 @@ public class PreviewHubUpdater : INotificationAsyncHandler hubContextInstance = _hubContext.Value; foreach (ContentCacheRefresher.JsonPayload payload in payloads) { - var id = payload.Id; // keep it simple for now, ignore ChangeTypes - await hubContextInstance.Clients.All.refreshed(id); + var key = payload.Key; // keep it simple for now, ignore ChangeTypes + if (key.HasValue is false) + { + throw new InvalidOperationException($"No key is set for payload with id {payload.Id}"); + } + + await hubContextInstance.Clients.All.refreshed(key.Value); } } } diff --git a/src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs b/src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs index a7fd5bd009..397bba7078 100644 --- a/src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs +++ b/src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs @@ -88,6 +88,6 @@ public sealed class BackOfficeAreaRoutes : IAreaRoutes Controller = ControllerExtensions.GetControllerName(), Action = nameof(BackOfficeDefaultController.Index), }, - constraints: new { slug = @"^(section.*|upgrade|install|oauth_complete|logout|error)$" }); + constraints: new { slug = @"^(section.*|preview|upgrade|install|oauth_complete|logout|error)$" }); } } diff --git a/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs b/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs index 12f71c7b44..37a39a6c92 100644 --- a/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs @@ -18,6 +18,7 @@ public class WebRoutingSettings internal const bool StaticDisableAlternativeTemplates = false; internal const bool StaticValidateAlternativeTemplates = false; internal const bool StaticDisableFindContentByIdPath = false; + internal const bool StaticDisableFindContentByIdentifierPath = false; internal const bool StaticDisableRedirectUrlTracking = false; internal const string StaticUrlProviderMode = "Auto"; @@ -59,12 +60,12 @@ public class WebRoutingSettings [DefaultValue(StaticValidateAlternativeTemplates)] public bool ValidateAlternativeTemplates { get; set; } = StaticValidateAlternativeTemplates; - /// - /// Gets or sets a value indicating whether find content ID by path is disabled. - /// + [Obsolete("Use DisableFindContentByIdentifierPath instead. This will be removed in Umbraco 15." )] [DefaultValue(StaticDisableFindContentByIdPath)] public bool DisableFindContentByIdPath { get; set; } = StaticDisableFindContentByIdPath; + [DefaultValue(StaticDisableFindContentByIdentifierPath)] + public bool DisableFindContentByIdentifierPath { get; set; } = StaticDisableFindContentByIdentifierPath; /// /// Gets or sets a value indicating whether redirect URL tracking is disabled. /// diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index a54e2acdc2..18375410c9 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -37,6 +37,7 @@ public static partial class UmbracoBuilderExtensions builder.ContentFinders() .Append() .Append() + .Append() .Append() /*.Append() // disabled, this is an odd finder */ .Append() diff --git a/src/Umbraco.Core/Routing/ContentFinderByIdPath.cs b/src/Umbraco.Core/Routing/ContentFinderByIdPath.cs index c7089f0824..86a559b5db 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByIdPath.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByIdPath.cs @@ -13,13 +13,15 @@ namespace Umbraco.Cms.Core.Routing; /// /// Handles /1234 where 1234 is the identified of a document. /// -public class ContentFinderByIdPath : IContentFinder +[Obsolete("Use ContentFinderByKeyPath instead. This will be removed in Umbraco 15.")] +public class ContentFinderByIdPath : ContentFinderByIdentifierPathBase, IContentFinder { private readonly ILogger _logger; - private readonly IRequestAccessor _requestAccessor; private readonly IUmbracoContextAccessor _umbracoContextAccessor; private WebRoutingSettings _webRoutingSettings; + protected override string FailureLogMessageTemplate => "Not a node id"; + /// /// Initializes a new instance of the class. /// @@ -28,11 +30,11 @@ public class ContentFinderByIdPath : IContentFinder ILogger logger, IRequestAccessor requestAccessor, IUmbracoContextAccessor umbracoContextAccessor) + : base(requestAccessor, logger) { _webRoutingSettings = webRoutingSettings.CurrentValue ?? throw new ArgumentNullException(nameof(webRoutingSettings)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _requestAccessor = requestAccessor ?? throw new ArgumentNullException(nameof(requestAccessor)); _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); @@ -51,67 +53,52 @@ public class ContentFinderByIdPath : IContentFinder return Task.FromResult(false); } - if (umbracoContext.InPreviewMode == false && _webRoutingSettings.DisableFindContentByIdPath) + if (umbracoContext.InPreviewMode == false && (_webRoutingSettings.DisableFindContentByIdPath || _webRoutingSettings.DisableFindContentByIdentifierPath)) { return Task.FromResult(false); } - IPublishedContent? node = null; var path = frequest.AbsolutePathDecoded; - var nodeId = -1; - // no id if "/" - if (path != "/") + if (path == "/") { - var noSlashPath = path.Substring(1); - - if (int.TryParse(noSlashPath, NumberStyles.Integer, CultureInfo.InvariantCulture, out nodeId) == false) - { - nodeId = -1; - } - - if (nodeId > 0) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Id={NodeId}", nodeId); - } - - node = umbracoContext.Content?.GetById(nodeId); - - if (node != null) - { - var cultureFromQuerystring = _requestAccessor.GetQueryStringValue("culture"); - - // if we have a node, check if we have a culture in the query string - if (!string.IsNullOrEmpty(cultureFromQuerystring)) - { - // we're assuming it will match a culture, if an invalid one is passed in, an exception will throw (there is no TryGetCultureInfo method), i think this is ok though - frequest.SetCulture(cultureFromQuerystring); - } - - frequest.SetPublishedContent(node); - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Found node with id={PublishedContentId}", node.Id); - } - } - else - { - nodeId = -1; // trigger message below - } - } + return LogAndReturnFailure(); } - if (nodeId == -1) + var noSlashPath = path.Substring(1); + + if (int.TryParse(noSlashPath, NumberStyles.Integer, CultureInfo.InvariantCulture, out var nodeId) == false) { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Not a node id"); - } + return LogAndReturnFailure(); } - return Task.FromResult(node != null); + // NodeId cannot be negative or 0 + if (nodeId < 1) + { + return LogAndReturnFailure(); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Id={NodeId}", nodeId); + } + + IPublishedContent? node = umbracoContext.Content?.GetById(nodeId); + + if (node is null) + { + return LogAndReturnFailure(); + } + + ResolveAndSetCultureOnRequest(frequest); + + frequest.SetPublishedContent(node); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Found node with id={PublishedContentId}", node.Id); + } + + return Task.FromResult(true); } } diff --git a/src/Umbraco.Core/Routing/ContentFinderByIdentifierPathBase.cs b/src/Umbraco.Core/Routing/ContentFinderByIdentifierPathBase.cs new file mode 100644 index 0000000000..6bd585a339 --- /dev/null +++ b/src/Umbraco.Core/Routing/ContentFinderByIdentifierPathBase.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Web; + +namespace Umbraco.Cms.Core.Routing; + +public abstract class ContentFinderByIdentifierPathBase +{ + private readonly IRequestAccessor _requestAccessor; + private readonly ILogger _logger; + + /// + /// Used as the log message inside >. + /// + protected abstract string FailureLogMessageTemplate { get; } + + protected ContentFinderByIdentifierPathBase(IRequestAccessor requestAccessor, ILogger logger) + { + _requestAccessor = requestAccessor; + _logger = logger; + } + + protected void ResolveAndSetCultureOnRequest(IPublishedRequestBuilder frequest) + { + var cultureFromQuerystring = _requestAccessor.GetQueryStringValue("culture"); + + // Check if we have a culture in the query string + if (!string.IsNullOrEmpty(cultureFromQuerystring)) + { + // We're assuming it will match a culture, if an invalid one is passed in, + // an exception will throw (there is no TryGetCultureInfo method) + frequest.SetCulture(cultureFromQuerystring); + } + } + + protected Task LogAndReturnFailure() + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug(FailureLogMessageTemplate); + } + + return Task.FromResult(false); + } +} diff --git a/src/Umbraco.Core/Routing/ContentFinderByKeyPath.cs b/src/Umbraco.Core/Routing/ContentFinderByKeyPath.cs new file mode 100644 index 0000000000..8f0f13d0e9 --- /dev/null +++ b/src/Umbraco.Core/Routing/ContentFinderByKeyPath.cs @@ -0,0 +1,102 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Web; + +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides an implementation of that handles page key identifiers. +/// +/// +/// Handles /e7b65017-c6b3-4c11-b7c7-7ea1d0404c9a where e7b65017-c6b3-4c11-b7c7-7ea1d0404c9a is the key of a document. +/// +public class ContentFinderByKeyPath : ContentFinderByIdentifierPathBase, IContentFinder +{ + private readonly ILogger _logger; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private WebRoutingSettings _webRoutingSettings; + + protected override string FailureLogMessageTemplate => "Not a node key"; + + /// + /// Initializes a new instance of the class. + /// + public ContentFinderByKeyPath( + IOptionsMonitor webRoutingSettings, + ILogger logger, + IRequestAccessor requestAccessor, + IUmbracoContextAccessor umbracoContextAccessor) + : base(requestAccessor, logger) + { + _webRoutingSettings = webRoutingSettings.CurrentValue ?? + throw new ArgumentNullException(nameof(webRoutingSettings)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _umbracoContextAccessor = + umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + + webRoutingSettings.OnChange(x => _webRoutingSettings = x); + } + + /// + /// Tries to find and assign an Umbraco document to a PublishedRequest. + /// + /// The PublishedRequest. + /// A value indicating whether an Umbraco document was found and assigned. + public Task TryFindContent(IPublishedRequestBuilder frequest) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) + { + return Task.FromResult(false); + } + + if (umbracoContext.InPreviewMode == false && _webRoutingSettings.DisableFindContentByIdentifierPath) + { + return Task.FromResult(false); + } + + var path = frequest.AbsolutePathDecoded; + + // no id if "/" + if (path == "/") + { + return LogAndReturnFailure(); + } + + var noSlashPath = path.Substring(1); + + if (Guid.TryParse(noSlashPath, out var nodeKey) == false) + { + return LogAndReturnFailure(); + } + + // We shouldn't be persisting empty Guids + if (nodeKey.Equals(Guid.Empty)) + { + return LogAndReturnFailure(); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Key={NodeKey}", nodeKey); + } + + IPublishedContent? node = umbracoContext.Content?.GetById(nodeKey); + + if (node is null) + { + return LogAndReturnFailure(); + } + + ResolveAndSetCultureOnRequest(frequest); + + frequest.SetPublishedContent(node); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Found node with key={PublishedContentKey}", node.Key); + } + + return Task.FromResult(true); + } +} diff --git a/src/Umbraco.Core/Services/PreviewService.cs b/src/Umbraco.Core/Services/PreviewService.cs index 30316faff7..1103a1314b 100644 --- a/src/Umbraco.Core/Services/PreviewService.cs +++ b/src/Umbraco.Core/Services/PreviewService.cs @@ -8,7 +8,7 @@ public class PreviewService : IPreviewService public PreviewService(ICookieManager cookieManager) => _cookieManager = cookieManager; - public void EnterPreview() => _cookieManager.SetCookieValue(Constants.Web.PreviewCookieName, "preview"); + public void EnterPreview() => _cookieManager.SetCookieValue(Constants.Web.PreviewCookieName, "preview", true); public void EndPreview() => _cookieManager.ExpireCookie(Constants.Web.PreviewCookieName); diff --git a/src/Umbraco.Core/Web/ICookieManager.cs b/src/Umbraco.Core/Web/ICookieManager.cs index 2815675f5d..992c8fc827 100644 --- a/src/Umbraco.Core/Web/ICookieManager.cs +++ b/src/Umbraco.Core/Web/ICookieManager.cs @@ -6,7 +6,10 @@ public interface ICookieManager string? GetCookieValue(string cookieName); - void SetCookieValue(string cookieName, string value); + [Obsolete("Use overload with the httpOnly parameter instead. Scheduled for removal in V16.")] + void SetCookieValue(string cookieName, string value) => SetCookieValue(cookieName, value, false); + + void SetCookieValue(string cookieName, string value, bool httpOnly); bool HasCookie(string cookieName); } diff --git a/src/Umbraco.Infrastructure/Install/InstallHelper.cs b/src/Umbraco.Infrastructure/Install/InstallHelper.cs index 25102ffc26..0b5e04a60f 100644 --- a/src/Umbraco.Infrastructure/Install/InstallHelper.cs +++ b/src/Umbraco.Infrastructure/Install/InstallHelper.cs @@ -61,7 +61,7 @@ namespace Umbraco.Cms.Infrastructure.Install { installId = Guid.NewGuid(); - _cookieManager.SetCookieValue(Constants.Web.InstallerCookieName, installId.ToString()); + _cookieManager.SetCookieValue(Constants.Web.InstallerCookieName, installId.ToString(), false); } var dbProvider = string.Empty; diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs index 2f5ff585ed..29dcf87216 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs @@ -27,8 +27,8 @@ public class AspNetCoreCookieManager : ICookieManager public string? GetCookieValue(string cookieName) => _httpContextAccessor.HttpContext?.Request.Cookies[cookieName]; - public void SetCookieValue(string cookieName, string value) => - _httpContextAccessor.HttpContext?.Response.Cookies.Append(cookieName, value, new CookieOptions()); + public void SetCookieValue(string cookieName, string value, bool httpOnly) => + _httpContextAccessor.HttpContext?.Response.Cookies.Append(cookieName, value, new CookieOptions { HttpOnly = httpOnly }); public bool HasCookie(string cookieName) => !(GetCookieValue(cookieName) is null); } diff --git a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs index 270cf5e54a..bb4066acab 100644 --- a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs @@ -109,7 +109,7 @@ public class WebProfiler : IProfiler && !location.Contains("://")) { MiniProfilerContext.Value.Root.Name = "Before Redirect"; - cookieManager.SetCookieValue(WebProfileCookieKey, MiniProfilerContext.Value.ToJson()); + cookieManager.SetCookieValue(WebProfileCookieKey, MiniProfilerContext.Value.ToJson(), false); } } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByIdTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByIdTests.cs index d4f3074a27..d752462853 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByIdTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByIdTests.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; @@ -8,35 +5,25 @@ using NUnit.Framework; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Tests.Common.Published; -using Umbraco.Cms.Tests.UnitTests.TestHelpers; using Umbraco.Extensions; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; [TestFixture] -public class ContentFinderByIdTests : PublishedSnapshotServiceTestBase +public class ContentFinderByIdTests : ContentFinderByIdentifierTestsBase { [SetUp] public override void Setup() { base.Setup(); - - var xml = PublishedContentXml.BaseWebTestXml(1234); - - IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( - xml, - TestHelper.ShortStringHelper, - out var contentTypes, - out var dataTypes).ToList(); - - InitializedCache(kits, contentTypes, dataTypes); } - [TestCase("/1046", 1046)] - public async Task Lookup_By_Id(string urlAsString, int nodeMatch) + [TestCase("/1046", 1046, true)] + [TestCase("/1046", 1047, false)] + public async Task Lookup_By_Id(string urlAsString, int nodeId, bool shouldSucceed) { + PopulateCache(nodeId, Guid.NewGuid()); + var umbracoContextAccessor = GetUmbracoContextAccessor(urlAsString); var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); @@ -50,7 +37,14 @@ public class ContentFinderByIdTests : PublishedSnapshotServiceTestBase var result = await lookup.TryFindContent(frequest); - Assert.IsTrue(result); - Assert.AreEqual(frequest.PublishedContent.Id, nodeMatch); + Assert.AreEqual(shouldSucceed, result); + if (shouldSucceed) + { + Assert.AreEqual(frequest.PublishedContent!.Id, nodeId); + } + else + { + Assert.IsNull(frequest.PublishedContent); + } } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByIdentifierTestsBase.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByIdentifierTestsBase.cs new file mode 100644 index 0000000000..0da5aeb993 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByIdentifierTestsBase.cs @@ -0,0 +1,58 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.UnitTests.TestHelpers; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; + +public abstract class ContentFinderByIdentifierTestsBase : PublishedSnapshotServiceTestBase +{ + protected void PopulateCache(int nodeId, Guid nodeKey) + { + var dataTypes = GetDefaultDataTypes().Select(dt => dt as IDataType).ToArray(); + var propertyDataTypes = new Dictionary + { + // we only have one data type for this test which will be resolved with string empty. + [string.Empty] = dataTypes[0], + }; + IContentType contentType1 = new ContentType(ShortStringHelper, -1); + + var rootData = new ContentDataBuilder() + .WithName("Page" + Guid.NewGuid()) + .Build(ShortStringHelper, propertyDataTypes, contentType1, "alias"); + + var root = ContentNodeKitBuilder.CreateWithContent( + contentType1.Id, + 9876, + "-1,9876", + draftData: rootData, + publishedData: rootData); + + var parentData = new ContentDataBuilder() + .WithName("Page" + Guid.NewGuid()) + .Build(); + + var parent = ContentNodeKitBuilder.CreateWithContent( + contentType1.Id, + 5432, + "-1,9876,5432", + parentContentId: 9876, + draftData: parentData, + publishedData: parentData); + + var contentData = new ContentDataBuilder() + .WithName("Page" + Guid.NewGuid()) + .Build(); + + var content = ContentNodeKitBuilder.CreateWithContent( + contentType1.Id, + nodeId, + "-1,9876,5432," + nodeId, + parentContentId: 5432, + draftData: contentData, + publishedData: contentData, + uid: nodeKey); + + InitializedCache(new[] { root, parent, content }, [contentType1], dataTypes); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByKeyTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByKeyTests.cs new file mode 100644 index 0000000000..3d5b2990b2 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByKeyTests.cs @@ -0,0 +1,52 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Web; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing; + +[TestFixture] +public class ContentFinderByKeyTests : ContentFinderByIdentifierTestsBase +{ + [SetUp] + public override void Setup() + { + base.Setup(); + } + + [TestCase("/1598901d-ebbe-4996-b7fb-6a6cbac13a62", "1598901d-ebbe-4996-b7fb-6a6cbac13a62", true)] + [TestCase("/1598901d-ebbe-4996-b7fb-6a6cbac13a62", "a383f6ed-cc54-46f1-a577-33f42e7214de", false)] + public async Task Lookup_By_Key(string urlAsString, string nodeKeyString, bool shouldSucceed) + { + var nodeKey = Guid.Parse(nodeKeyString); + + PopulateCache(9999, nodeKey); + + var umbracoContextAccessor = GetUmbracoContextAccessor(urlAsString); + var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); + var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); + var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); + var webRoutingSettings = new WebRoutingSettings(); + var lookup = new ContentFinderByKeyPath( + Mock.Of>(x => x.CurrentValue == webRoutingSettings), + Mock.Of>(), + Mock.Of(), + umbracoContextAccessor); + + var result = await lookup.TryFindContent(frequest); + + Assert.AreEqual(shouldSucceed, result); + if (shouldSucceed) + { + Assert.AreEqual(frequest.PublishedContent!.Key, nodeKey); + } + else + { + Assert.IsNull(frequest.PublishedContent); + } + } +}