diff --git a/src/Umbraco.Core/Services/RedirectUrlService.cs b/src/Umbraco.Core/Services/RedirectUrlService.cs
index c2050f7ff0d6..e5c286caaf76 100644
--- a/src/Umbraco.Core/Services/RedirectUrlService.cs
+++ b/src/Umbraco.Core/Services/RedirectUrlService.cs
@@ -1,4 +1,3 @@
-using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
@@ -7,10 +6,16 @@
namespace Umbraco.Cms.Core.Services;
+///
+/// Provides services for managing redirect URLs.
+///
internal sealed class RedirectUrlService : RepositoryService, IRedirectUrlService
{
private readonly IRedirectUrlRepository _redirectUrlRepository;
+ ///
+ /// Initializes a new instance of the class.
+ ///
public RedirectUrlService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
@@ -19,109 +24,99 @@ public RedirectUrlService(
: base(provider, loggerFactory, eventMessagesFactory) =>
_redirectUrlRepository = redirectUrlRepository;
+ ///
public void Register(string url, Guid contentKey, string? culture = null)
{
- using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+ using ICoreScope scope = ScopeProvider.CreateCoreScope();
+ IRedirectUrl? redir = _redirectUrlRepository.Get(url, contentKey, culture);
+ if (redir != null)
{
- IRedirectUrl? redir = _redirectUrlRepository.Get(url, contentKey, culture);
- if (redir != null)
- {
- redir.CreateDateUtc = DateTime.UtcNow;
- }
- else
- {
- redir = new RedirectUrl { Key = Guid.NewGuid(), Url = url, ContentKey = contentKey, Culture = culture };
- }
-
- _redirectUrlRepository.Save(redir);
- scope.Complete();
+ redir.CreateDateUtc = DateTime.UtcNow;
}
+ else
+ {
+ redir = new RedirectUrl { Key = Guid.NewGuid(), Url = url, ContentKey = contentKey, Culture = culture };
+ }
+
+ _redirectUrlRepository.Save(redir);
+ scope.Complete();
}
+ ///
public void Delete(IRedirectUrl redirectUrl)
{
- using (ICoreScope scope = ScopeProvider.CreateCoreScope())
- {
- _redirectUrlRepository.Delete(redirectUrl);
- scope.Complete();
- }
+ using ICoreScope scope = ScopeProvider.CreateCoreScope();
+ _redirectUrlRepository.Delete(redirectUrl);
+ scope.Complete();
}
+ ///
public void Delete(Guid id)
{
- using (ICoreScope scope = ScopeProvider.CreateCoreScope())
- {
- _redirectUrlRepository.Delete(id);
- scope.Complete();
- }
+ using ICoreScope scope = ScopeProvider.CreateCoreScope();
+ _redirectUrlRepository.Delete(id);
+ scope.Complete();
}
+ ///
public void DeleteContentRedirectUrls(Guid contentKey)
{
- using (ICoreScope scope = ScopeProvider.CreateCoreScope())
- {
- _redirectUrlRepository.DeleteContentUrls(contentKey);
- scope.Complete();
- }
+ using ICoreScope scope = ScopeProvider.CreateCoreScope();
+ _redirectUrlRepository.DeleteContentUrls(contentKey);
+ scope.Complete();
}
+ ///
public void DeleteAll()
{
- using (ICoreScope scope = ScopeProvider.CreateCoreScope())
- {
- _redirectUrlRepository.DeleteAll();
- scope.Complete();
- }
+ using ICoreScope scope = ScopeProvider.CreateCoreScope();
+ _redirectUrlRepository.DeleteAll();
+ scope.Complete();
}
+ ///
public IRedirectUrl? GetMostRecentRedirectUrl(string url)
{
- using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
- {
- return _redirectUrlRepository.GetMostRecentUrl(url);
- }
+ using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+ return _redirectUrlRepository.GetMostRecentUrl(url);
}
+ ///
public async Task GetMostRecentRedirectUrlAsync(string url)
{
- using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
- {
- return await _redirectUrlRepository.GetMostRecentUrlAsync(url);
- }
+ using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+ return await _redirectUrlRepository.GetMostRecentUrlAsync(url);
}
+ ///
public IEnumerable GetContentRedirectUrls(Guid contentKey)
{
- using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
- {
- return _redirectUrlRepository.GetContentUrls(contentKey);
- }
+ using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+ return _redirectUrlRepository.GetContentUrls(contentKey);
}
+ ///
public IEnumerable GetAllRedirectUrls(long pageIndex, int pageSize, out long total)
{
- using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
- {
- return _redirectUrlRepository.GetAllUrls(pageIndex, pageSize, out total);
- }
+ using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+ return _redirectUrlRepository.GetAllUrls(pageIndex, pageSize, out total);
}
+ ///
public IEnumerable GetAllRedirectUrls(int rootContentId, long pageIndex, int pageSize, out long total)
{
- using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
- {
- return _redirectUrlRepository.GetAllUrls(rootContentId, pageIndex, pageSize, out total);
- }
+ using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+ return _redirectUrlRepository.GetAllUrls(rootContentId, pageIndex, pageSize, out total);
}
+ ///
public IEnumerable SearchRedirectUrls(string searchTerm, long pageIndex, int pageSize, out long total)
{
- using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
- {
- return _redirectUrlRepository.SearchUrls(searchTerm, pageIndex, pageSize, out total);
- }
+ using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+ return _redirectUrlRepository.SearchUrls(searchTerm, pageIndex, pageSize, out total);
}
+ ///
public IRedirectUrl? GetMostRecentRedirectUrl(string url, string? culture)
{
if (string.IsNullOrWhiteSpace(culture))
@@ -129,12 +124,11 @@ public IEnumerable SearchRedirectUrls(string searchTerm, long page
return GetMostRecentRedirectUrl(url);
}
- using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
- {
- return _redirectUrlRepository.GetMostRecentUrl(url, culture);
- }
+ using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+ return _redirectUrlRepository.GetMostRecentUrl(url, culture);
}
+ ///
public async Task GetMostRecentRedirectUrlAsync(string url, string? culture)
{
if (string.IsNullOrWhiteSpace(culture))
@@ -142,9 +136,7 @@ public IEnumerable SearchRedirectUrls(string searchTerm, long page
return await GetMostRecentRedirectUrlAsync(url);
}
- using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
- {
- return await _redirectUrlRepository.GetMostRecentUrlAsync(url, culture);
- }
+ using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+ return await _redirectUrlRepository.GetMostRecentUrlAsync(url, culture);
}
}
diff --git a/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs b/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs
index 18a4d45e67d9..121014104a77 100644
--- a/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs
+++ b/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
@@ -9,114 +10,137 @@
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Extensions;
-namespace Umbraco.Cms.Infrastructure.Routing
+namespace Umbraco.Cms.Infrastructure.Routing;
+
+///
+/// Tracks and manages URL redirects for content items, ensuring that old routes are stored and appropriate redirects
+/// are created when content URLs change.
+///
+internal sealed class RedirectTracker : IRedirectTracker
{
- internal sealed class RedirectTracker : IRedirectTracker
+ private readonly ILanguageService _languageService;
+ private readonly IRedirectUrlService _redirectUrlService;
+ private readonly IPublishedContentCache _contentCache;
+ private readonly IDocumentNavigationQueryService _navigationQueryService;
+ private readonly ILogger _logger;
+ private readonly IPublishedUrlProvider _publishedUrlProvider;
+ private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RedirectTracker(
+ ILanguageService languageService,
+ IRedirectUrlService redirectUrlService,
+ IPublishedContentCache contentCache,
+ IDocumentNavigationQueryService navigationQueryService,
+ ILogger logger,
+ IPublishedUrlProvider publishedUrlProvider,
+ IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
+ {
+ _languageService = languageService;
+ _redirectUrlService = redirectUrlService;
+ _contentCache = contentCache;
+ _navigationQueryService = navigationQueryService;
+ _logger = logger;
+ _publishedUrlProvider = publishedUrlProvider;
+ _publishedContentStatusFilteringService = publishedContentStatusFilteringService;
+ }
+
+ ///
+ public void StoreOldRoute(IContent entity, Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes)
{
- private readonly ILocalizationService _localizationService;
- private readonly IRedirectUrlService _redirectUrlService;
- private readonly IPublishedContentCache _contentCache;
- private readonly IDocumentNavigationQueryService _navigationQueryService;
- private readonly ILogger _logger;
- private readonly IPublishedUrlProvider _publishedUrlProvider;
- private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService;
-
- public RedirectTracker(
- ILocalizationService localizationService,
- IRedirectUrlService redirectUrlService,
- IPublishedContentCache contentCache,
- IDocumentNavigationQueryService navigationQueryService,
- ILogger logger,
- IPublishedUrlProvider publishedUrlProvider,
- IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
+ IPublishedContent? entityContent = _contentCache.GetById(entity.Id);
+ if (entityContent is null)
{
- _localizationService = localizationService;
- _redirectUrlService = redirectUrlService;
- _contentCache = contentCache;
- _navigationQueryService = navigationQueryService;
- _logger = logger;
- _publishedUrlProvider = publishedUrlProvider;
- _publishedContentStatusFilteringService = publishedContentStatusFilteringService;
+ return;
}
- ///
- public void StoreOldRoute(IContent entity, Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes)
- {
- IPublishedContent? entityContent = _contentCache.GetById(entity.Id);
- if (entityContent is null)
- {
- return;
- }
+ // Get the default affected cultures by going up the tree until we find the first culture variant entity (default to no cultures)
+ var defaultCultures = new Lazy(() => entityContent.AncestorsOrSelf(_navigationQueryService, _publishedContentStatusFilteringService).FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys.ToArray() ?? Array.Empty());
- // Get the default affected cultures by going up the tree until we find the first culture variant entity (default to no cultures)
- var defaultCultures = new Lazy(() => entityContent.AncestorsOrSelf(_navigationQueryService, _publishedContentStatusFilteringService).FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys.ToArray() ?? Array.Empty());
+ // Get all language ISO codes (in case we're dealing with invariant content with variant ancestors)
+ var languageIsoCodes = new Lazy(() => _languageService.GetAllIsoCodesAsync().GetAwaiter().GetResult().ToArray());
- // Get all language ISO codes (in case we're dealing with invariant content with variant ancestors)
- var languageIsoCodes = new Lazy(() => _localizationService.GetAllLanguages().Select(x => x.IsoCode).ToArray());
+ foreach (IPublishedContent publishedContent in entityContent.DescendantsOrSelf(_navigationQueryService, _publishedContentStatusFilteringService))
+ {
+ // If this entity defines specific cultures, use those instead of the default ones
+ IEnumerable cultures = publishedContent.Cultures.Any() ? publishedContent.Cultures.Keys : defaultCultures.Value;
- foreach (IPublishedContent publishedContent in entityContent.DescendantsOrSelf(_navigationQueryService, _publishedContentStatusFilteringService))
+ foreach (var culture in cultures)
{
- // If this entity defines specific cultures, use those instead of the default ones
- IEnumerable cultures = publishedContent.Cultures.Any() ? publishedContent.Cultures.Keys : defaultCultures.Value;
-
- foreach (var culture in cultures)
+ try
{
- try
- {
- var route = _publishedUrlProvider.GetUrl(publishedContent.Id, UrlMode.Relative, culture).TrimEnd(Constants.CharArrays.ForwardSlash);
+ var route = _publishedUrlProvider.GetUrl(publishedContent.Id, UrlMode.Relative, culture).TrimEnd(Constants.CharArrays.ForwardSlash);
- if (IsValidRoute(route))
- {
- oldRoutes[(publishedContent.Id, culture)] = (publishedContent.Key, route);
- }
- else if (string.IsNullOrEmpty(culture))
+ if (IsValidRoute(route))
+ {
+ oldRoutes[(publishedContent.Id, culture)] = (publishedContent.Key, route);
+ }
+ else if (string.IsNullOrEmpty(culture))
+ {
+ // Retry using all languages, if this is invariant but has a variant ancestor.
+ foreach (string languageIsoCode in languageIsoCodes.Value)
{
- // Retry using all languages, if this is invariant but has a variant ancestor.
- foreach (string languageIsoCode in languageIsoCodes.Value)
+ route = _publishedUrlProvider.GetUrl(publishedContent.Id, UrlMode.Relative, languageIsoCode).TrimEnd(Constants.CharArrays.ForwardSlash);
+ if (IsValidRoute(route))
{
- route = _publishedUrlProvider.GetUrl(publishedContent.Id, UrlMode.Relative, languageIsoCode).TrimEnd(Constants.CharArrays.ForwardSlash);
- if (IsValidRoute(route))
- {
- oldRoutes[(publishedContent.Id, languageIsoCode)] = (publishedContent.Key, route);
- }
+ oldRoutes[(publishedContent.Id, languageIsoCode)] = (publishedContent.Key, route);
}
}
}
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "Could not register redirects because the old route couldn't be retrieved for content ID {ContentId} and culture '{Culture}'.", publishedContent.Id, culture);
- }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Could not register redirects because the old route couldn't be retrieved for content ID {ContentId} and culture '{Culture}'.", publishedContent.Id, culture);
}
}
}
+ }
- ///
- public void CreateRedirects(IDictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes)
+ ///
+ public void CreateRedirects(IDictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes)
+ {
+ if (!oldRoutes.Any())
{
- if (!oldRoutes.Any())
- {
- return;
- }
+ return;
+ }
- foreach (((int contentId, string culture), (Guid contentKey, string oldRoute)) in oldRoutes)
+ foreach (((int contentId, string culture), (Guid contentKey, string oldRoute)) in oldRoutes)
+ {
+ try
{
- try
- {
- var newRoute = _publishedUrlProvider.GetUrl(contentKey, UrlMode.Relative, culture).TrimEnd(Constants.CharArrays.ForwardSlash);
- if (!IsValidRoute(newRoute) || oldRoute == newRoute)
- {
- continue;
- }
+ var newRoute = _publishedUrlProvider.GetUrl(contentKey, UrlMode.Relative, culture).TrimEnd(Constants.CharArrays.ForwardSlash);
- _redirectUrlService.Register(oldRoute, contentKey, culture);
- }
- catch (Exception ex)
+ if (!IsValidRoute(newRoute) || oldRoute == newRoute)
{
- _logger.LogWarning(ex, "Could not track redirects because the new route couldn't be retrieved for content ID {ContentId} and culture '{Culture}'.", contentId, culture);
+ continue;
}
+
+ // Ensure we don't create a self-referencing redirect. This can occur if a document is renamed and then the name is reverted back
+ // to the original. We resolve this by removing any existing redirect that points to the new route.
+ RemoveSelfReferencingRedirect(contentKey, newRoute);
+
+ _redirectUrlService.Register(oldRoute, contentKey, culture);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Could not track redirects because the new route couldn't be retrieved for content ID {ContentId} and culture '{Culture}'.", contentId, culture);
}
}
+ }
+
+ private static bool IsValidRoute([NotNullWhen(true)] string? route) => route is not null && !route.StartsWith("err/");
- private static bool IsValidRoute([NotNullWhen(true)] string? route) => route is not null && !route.StartsWith("err/");
+ private void RemoveSelfReferencingRedirect(Guid contentKey, string route)
+ {
+ IEnumerable allRedirectUrls = _redirectUrlService.GetContentRedirectUrls(contentKey);
+ foreach (IRedirectUrl redirectUrl in allRedirectUrls)
+ {
+ if (redirectUrl.Url == route)
+ {
+ _redirectUrlService.Delete(redirectUrl.Key);
+ }
+ }
}
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Routing/RedirectTrackerTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Routing/RedirectTrackerTests.cs
new file mode 100644
index 000000000000..2716cee8993d
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Routing/RedirectTrackerTests.cs
@@ -0,0 +1,149 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using NUnit.Framework;
+using Umbraco.Cms.Core.Cache;
+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.Infrastructure.Persistence.Repositories.Implement;
+using Umbraco.Cms.Infrastructure.Routing;
+using Umbraco.Cms.Infrastructure.Scoping;
+using Umbraco.Cms.Tests.Common.Testing;
+using Umbraco.Cms.Tests.Integration.Testing;
+
+namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Routing;
+
+[TestFixture]
+[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
+public class RedirectTrackerTests : UmbracoIntegrationTestWithContent
+{
+ private IRedirectUrlService RedirectUrlService => GetRequiredService();
+
+ private IContent _testPage;
+
+ private const string Url = "RedirectUrl";
+
+ public override void CreateTestData()
+ {
+ base.CreateTestData();
+
+ using var scope = ScopeProvider.CreateScope();
+ var repository = CreateRedirectUrlRepository();
+ var rootContent = ContentService.GetRootContent().First();
+ var subPages = ContentService.GetPagedChildren(rootContent.Id, 0, 3, out _).ToList();
+ _testPage = subPages[0];
+
+ repository.Save(new RedirectUrl { ContentKey = _testPage.Key, Url = Url, Culture = "en" });
+
+ scope.Complete();
+ }
+
+ [Test]
+ public void Can_Store_Old_Route()
+ {
+ Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> dict =
+ new Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)>
+ {
+ [(_testPage.Id, "en")] = (_testPage.Key, "/old-route"),
+ };
+
+ var redirectTracker = CreateRedirectTracker();
+
+ redirectTracker.StoreOldRoute(_testPage, dict);
+
+ Assert.That(dict.Count, Is.EqualTo(1));
+ Assert.AreEqual(dict.Values.First().OldRoute, Url);
+ }
+
+ [Test]
+ public void Can_Create_Redirects()
+ {
+ IDictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> dict =
+ new Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)>
+ {
+ [(_testPage.Id, "en")] = (_testPage.Key, "/old-route"),
+ };
+ var redirectTracker = CreateRedirectTracker();
+
+ redirectTracker.CreateRedirects(dict);
+
+ var redirects = RedirectUrlService.GetContentRedirectUrls(_testPage.Key);
+
+ Assert.IsTrue(redirects.Any(x => x.Url == "/old-route"));
+ }
+
+ [Test]
+ public void Removes_Self_Referncing_Redirects()
+ {
+ const string newUrl = "newUrl";
+
+ var redirects = RedirectUrlService.GetContentRedirectUrls(_testPage.Key);
+ Assert.IsTrue(redirects.Any(x => x.Url == Url)); // Ensure self referencing redirect exists.
+
+ IDictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> dict =
+ new Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)>
+ {
+ [(_testPage.Id, "en")] = (_testPage.Key, newUrl),
+ };
+
+ var redirectTracker = CreateRedirectTracker();
+ redirectTracker.CreateRedirects(dict);
+ redirects = RedirectUrlService.GetContentRedirectUrls(_testPage.Key);
+
+ Assert.IsFalse(redirects.Any(x => x.Url == Url));
+ Assert.IsTrue(redirects.Any(x => x.Url == newUrl));
+ }
+
+ private RedirectUrlRepository CreateRedirectUrlRepository() =>
+ new(
+ (IScopeAccessor)ScopeProvider,
+ AppCaches.Disabled,
+ new NullLogger(),
+ Mock.Of(),
+ Mock.Of());
+
+ private IRedirectTracker CreateRedirectTracker()
+ {
+ var contentType = new Mock();
+ contentType.SetupGet(c => c.Variations).Returns(ContentVariation.Nothing);
+
+ var cultures = new Dictionary
+ {
+ { "en", new PublishedCultureInfo("en", "english", "/en/", DateTime.UtcNow) },
+ };
+
+ var content = new Mock();
+
+ content.SetupGet(c => c.Key).Returns(_testPage.Key);
+ content.SetupGet(c => c.ContentType).Returns(contentType.Object);
+ content.SetupGet(c => c.Cultures).Returns(cultures);
+ content.SetupGet(c => c.Id).Returns(_testPage.Id);
+
+ IPublishedContentCache contentCache = Mock.Of();
+ Mock.Get(contentCache)
+ .Setup(x => x.GetById(_testPage.Id))
+ .Returns(content.Object);
+
+ IPublishedUrlProvider publishedUrlProvider = Mock.Of();
+ Mock.Get(publishedUrlProvider)
+ .Setup(x => x.GetUrl(_testPage.Key, UrlMode.Relative, "en", null))
+ .Returns(Url);
+
+ Mock.Get(publishedUrlProvider)
+ .Setup(x => x.GetUrl(_testPage.Id, UrlMode.Relative, "en", null))
+ .Returns(Url);
+
+ return new RedirectTracker(
+ GetRequiredService(),
+ RedirectUrlService,
+ contentCache,
+ GetRequiredService(),
+ GetRequiredService>(),
+ publishedUrlProvider,
+ GetRequiredService());
+ }
+}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs
index 2e6bb099f2d5..962aa43bd9dd 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs
@@ -1,8 +1,6 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
-using System.Linq;
-using System.Threading;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
@@ -85,4 +83,16 @@ public void Can_Get_Most_Recent_RedirectUrl_With_Culture_When_No_CultureVariant_
var redirect = RedirectUrlService.GetMostRecentRedirectUrl(UrlAlt, UnusedCulture);
Assert.AreEqual(redirect.ContentId, _thirdSubPage.Id);
}
+
+ [Test]
+ public void Can_Register_Redirect()
+ {
+ const string TestUrl = "testUrl";
+
+ RedirectUrlService.Register(TestUrl, _firstSubPage.Key);
+
+ var redirect = RedirectUrlService.GetMostRecentRedirectUrl(TestUrl, CultureEnglish);
+
+ Assert.AreEqual(redirect.ContentId, _firstSubPage.Id);
+ }
}