diff --git a/src/Umbraco.Core/PropertyEditors/TextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/TextStringValueConverter.cs index 8fe15645e1..0a290e7492 100644 --- a/src/Umbraco.Core/PropertyEditors/TextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/TextStringValueConverter.cs @@ -40,7 +40,7 @@ public class TextStringValueConverter : PropertyValueConverterBase, IDeliveryApi var sourceString = source.ToString(); // ensures string is parsed for {localLink} and URLs are resolved correctly - sourceString = _linkParser.EnsureInternalLinks(sourceString!, preview); + sourceString = _linkParser.EnsureInternalLinks(sourceString!); sourceString = _urlParser.EnsureUrls(sourceString); return sourceString; diff --git a/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs b/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs index 4714ebcd2e..73aec2e74d 100644 --- a/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs +++ b/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Text.RegularExpressions; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Routing; namespace Umbraco.Cms.Core.Templates; @@ -45,17 +46,18 @@ public sealed class HtmlLocalLinkParser /// /// Parses the string looking for the {localLink} syntax and updates them to their correct links. /// - /// - /// - /// + [Obsolete("This method overload is no longer used in Umbraco and delegates to the overload without the preview parameter. Scheduled for removal in Umbraco 18.")] public string EnsureInternalLinks(string text, bool preview) => EnsureInternalLinks(text); /// /// Parses the string looking for the {localLink} syntax and updates them to their correct links. /// - /// - /// - public string EnsureInternalLinks(string text) + public string EnsureInternalLinks(string text) => EnsureInternalLinks(text, UrlMode.Default); + + /// + /// Parses the string looking for the {localLink} syntax and updates them to their correct links. + /// + public string EnsureInternalLinks(string text, UrlMode urlMode) { foreach (LocalLinkTag tagData in FindLocalLinkIds(text)) { @@ -63,8 +65,8 @@ public sealed class HtmlLocalLinkParser { var newLink = tagData.Udi?.EntityType switch { - Constants.UdiEntityType.Document => _publishedUrlProvider.GetUrl(tagData.Udi.Guid), - Constants.UdiEntityType.Media => _publishedUrlProvider.GetMediaUrl(tagData.Udi.Guid), + Constants.UdiEntityType.Document => _publishedUrlProvider.GetUrl(tagData.Udi.Guid, urlMode), + Constants.UdiEntityType.Media => _publishedUrlProvider.GetMediaUrl(tagData.Udi.Guid, urlMode), _ => string.Empty, }; @@ -73,7 +75,7 @@ public sealed class HtmlLocalLinkParser } else if (tagData.IntId.HasValue) { - var newLink = _publishedUrlProvider.GetUrl(tagData.IntId.Value); + var newLink = _publishedUrlProvider.GetUrl(tagData.IntId.Value, urlMode); text = text.Replace(tagData.TagHref, newLink); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 88f9540a4f..882b678a2e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -671,6 +671,15 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 protected override void PersistDeletedItem(IUser entity) { + // Clear user group caches for any user groups associated with the deleted user. + // We need to do this because the count of the number of users in the user group is cached + // along with the user group, and if we've made changes to the user groups assigned to the user, + // the count for the groups need to be refreshed. + foreach (IReadOnlyUserGroup group in entity.Groups) + { + ClearRepositoryCacheForUserGroup(group.Id); + } + IEnumerable deletes = GetDeleteClauses(); foreach (var delete in deletes) { @@ -713,8 +722,8 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 if (entity.IsPropertyDirty("Groups")) { // lookup all assigned - List? assigned = entity.Groups == null || entity.Groups.Any() == false - ? new List() + List? assigned = entity.Groups.Any() is false + ? [] : Database.Fetch( "SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups.Select(x => x.Alias) }); @@ -724,6 +733,15 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 var dto = new User2UserGroupDto { UserGroupId = groupDto.Id, UserId = entity.Id }; Database.Insert(dto); } + + // Clear user group caches for the user groups associated with the new user. + // We need to do this because the count of the number of users in the user group is cached + // along with the user group, and if we've made changes to the user groups assigned to the user, + // the count for the groups need to be refreshed. + foreach (IReadOnlyUserGroup group in entity.Groups) + { + ClearRepositoryCacheForUserGroup(group.Id); + } } entity.ResetDirtyProperties(); @@ -836,27 +854,66 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 if (entity.IsPropertyDirty("Groups")) { - //lookup all assigned - List? assigned = entity.Groups == null || entity.Groups.Any() == false - ? new List() - : Database.Fetch( - "SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", - new { aliases = entity.Groups.Select(x => x.Alias) }); + // Get all user groups Ids currently assigned to the user. + var existingUserGroupIds = Database.Fetch( + "WHERE UserId = @UserId", + new { UserId = entity.Id }) + .Select(x => x.UserGroupId) + .ToList(); - //first delete all - // TODO: We could do this a nicer way instead of "Nuke and Pave" - Database.Delete("WHERE UserId = @UserId", new { UserId = entity.Id }); + // Get the user groups Ids that need to be removed and added. + var userGroupsIdsToRemove = existingUserGroupIds + .Except(entity.Groups.Select(x => x.Id)) + .ToList(); + var userGroupIdsToAdd = entity.Groups + .Select(x => x.Id) + .Except(existingUserGroupIds) + .ToList(); - foreach (UserGroupDto? groupDto in assigned) + // Remove user groups that are no longer assigned to the user. + if (userGroupsIdsToRemove.Count > 0) { - var dto = new User2UserGroupDto { UserGroupId = groupDto.Id, UserId = entity.Id }; - Database.Insert(dto); + Database.Delete( + "WHERE UserId = @UserId AND UserGroupId IN (@userGroupIds)", + new { UserId = entity.Id, userGroupIds = userGroupsIdsToRemove }); + } + + // Add user groups that are newly assigned to the user. + if (userGroupIdsToAdd.Count > 0) + { + IEnumerable user2UserGroupDtos = userGroupIdsToAdd + .Select(userGroupId => new User2UserGroupDto + { + UserGroupId = userGroupId, + UserId = entity.Id, + }); + Database.InsertBulk(user2UserGroupDtos); + } + + // Clear user group caches for any user group that have been removed or added. + // We need to do this because the count of the number of users in the user group is cached + // along with the user group, and if we've made changes to the user groups assigned to the user, + // the count for the groups need to be refreshed. + var userGroupIdsToRefresh = userGroupsIdsToRemove + .Union(userGroupIdsToAdd) + .ToList(); + foreach (int userGroupIdToRefresh in userGroupIdsToRefresh) + { + ClearRepositoryCacheForUserGroup(userGroupIdToRefresh); } } entity.ResetDirtyProperties(); } + private void ClearRepositoryCacheForUserGroup(int id) + { + IAppPolicyCache userGroupCache = AppCaches.IsolatedCaches.GetOrCreate(); + + string cacheKey = RepositoryCacheKeys.GetKey(id); + userGroupCache.Clear(cacheKey); + } + private void AddingOrUpdateStartNodes(IEntity entity, IEnumerable current, UserStartNodeDto.StartNodeTypeValue startNodeType, int[]? entityStartIds) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index 05c6a8a4f1..ff0962a827 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -41,7 +41,7 @@ public class MarkdownEditorValueConverter : PropertyValueConverterBase, IDeliver var sourceString = source.ToString()!; // ensures string is parsed for {localLink} and URLs are resolved correctly - sourceString = _localLinkParser.EnsureInternalLinks(sourceString, preview); + sourceString = _localLinkParser.EnsureInternalLinks(sourceString); sourceString = _urlParser.EnsureUrls(sourceString); return sourceString; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs index b2c47fc3cb..d39d13e243 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs @@ -135,7 +135,7 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe var sourceString = intermediateValue.Markup; // ensures string is parsed for {localLink} and URLs and media are resolved correctly - sourceString = _linkParser.EnsureInternalLinks(sourceString, preview); + sourceString = _linkParser.EnsureInternalLinks(sourceString); sourceString = _urlParser.EnsureUrls(sourceString); sourceString = _imageSourceParser.EnsureImageSources(sourceString); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts index 2e898bf7e8..0e6132cefc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts @@ -266,6 +266,15 @@ export class UmbInputTiptapElement extends UmbFormControlMixin { Constants.Security.AdminGroupKey }; + var updateResult = await userService.UpdateAsync(Constants.Security.SuperUserKey, updateModel); + Assert.IsTrue(updateResult.Success); + } + + var adminUser = await userService.GetAsync(Constants.Security.SuperUserKey); + var adminUserUpdateModel = await MapUserToUpdateModel(adminUser); + adminUserUpdateModel.Email = "admin@test.com"; + adminUserUpdateModel.UserGroupKeys = new HashSet { Constants.Security.EditorGroupKey }; + var adminUserUpdateResult = await userService.UpdateAsync(Constants.Security.SuperUserKey, adminUserUpdateModel); + + if (expectSuccess) + { + Assert.IsTrue(adminUserUpdateResult.Success); + } + else + { + Assert.IsFalse(adminUserUpdateResult.Success); + Assert.AreEqual(UserOperationStatus.AdminUserGroupMustNotBeEmpty, adminUserUpdateResult.Status); + } + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/UserServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/UserServiceTests.cs index d5ef54ece6..6c8e737493 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/UserServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/UserServiceTests.cs @@ -1019,6 +1019,42 @@ internal sealed class UserServiceTests : UmbracoIntegrationTest } } + [Test] + public async Task Can_Assign_And_Get_Groups_For_User() + { + // Arrange + var (user, userGroup1) = await CreateTestUserAndGroup(); + var userGroup2 = await CreateTestUserGroup("testGroup2", "Test Group 2"); + + // Act & Assert + user = UserService.GetByUsername(user.Username); + + Assert.IsNotNull(user); + Assert.AreEqual(1, user.Groups.Count()); + Assert.AreEqual(userGroup1.Alias, user.Groups.First().Alias); + + // - add second group + user.AddGroup(userGroup2); + UserService.Save(user); + user = UserService.GetByUsername(user.Username); + Assert.AreEqual(2, user.Groups.Count()); + + // - remove first group + user.RemoveGroup(userGroup1.Alias); + UserService.Save(user); + user = UserService.GetByUsername(user.Username); + Assert.AreEqual(1, user.Groups.Count()); + Assert.AreEqual(userGroup2.Alias, user.Groups.First().Alias); + + // - remove second group and add first + user.RemoveGroup(userGroup2.Alias); + user.AddGroup(userGroup1.ToReadOnlyGroup()); + UserService.Save(user); + user = UserService.GetByUsername(user.Username); + Assert.AreEqual(1, user.Groups.Count()); + Assert.AreEqual(userGroup1.Alias, user.Groups.First().Alias); + } + [TestCase(UserKind.Default, UserClientCredentialsOperationStatus.InvalidUser)] [TestCase(UserKind.Api, UserClientCredentialsOperationStatus.Success)] public async Task Can_Assign_ClientId_To_Api_User(UserKind userKind, UserClientCredentialsOperationStatus expectedResult) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs index d1e5e0f494..0aa00b48d6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Templates; +using Umbraco.Cms.Core.Web; using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Tests.UnitTests.TestHelpers.Objects; @@ -216,18 +217,204 @@ public class HtmlLocalLinkParserTests var umbracoContextAccessor = new TestUmbracoContextAccessor(); + var umbracoContextFactory = TestUmbracoContextFactory.Create( + umbracoContextAccessor: umbracoContextAccessor); + + using (var reference = umbracoContextFactory.EnsureUmbracoContext()) + { + var contentCache = Mock.Get(reference.UmbracoContext.Content); + contentCache.Setup(x => x.GetById(It.IsAny())).Returns(publishedContent.Object); + contentCache.Setup(x => x.GetById(It.IsAny())).Returns(publishedContent.Object); + + var mediaCache = Mock.Get(reference.UmbracoContext.Media); + mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); + mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); + + var publishedUrlProvider = CreatePublishedUrlProvider( + contentUrlProvider, + mediaUrlProvider, + umbracoContextAccessor); + + var linkParser = new HtmlLocalLinkParser(publishedUrlProvider); + + var output = linkParser.EnsureInternalLinks(input); + + Assert.AreEqual(result, output); + } + } + + [Test] + public void ParseLocalLinks_WithUrlMode_RespectsUrlMode() + { + // Arrange + var input = "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world"; + + // Setup content URL provider that returns different URLs based on UrlMode + var contentUrlProvider = new Mock(); + contentUrlProvider + .Setup(x => x.GetUrl( + It.IsAny(), + UrlMode.Relative, + It.IsAny(), + It.IsAny())) + .Returns(UrlInfo.Url("/relative-url")); + contentUrlProvider + .Setup(x => x.GetUrl( + It.IsAny(), + UrlMode.Absolute, + It.IsAny(), + It.IsAny())) + .Returns(UrlInfo.Url("http://example.com/absolute-url")); + + var contentType = new PublishedContentType( + Guid.NewGuid(), + 666, + "alias", + PublishedItemType.Content, + Enumerable.Empty(), + Enumerable.Empty(), + ContentVariation.Nothing); + var publishedContent = new Mock(); + publishedContent.Setup(x => x.Id).Returns(1234); + publishedContent.Setup(x => x.ContentType).Returns(contentType); + + var umbracoContextAccessor = new TestUmbracoContextAccessor(); var umbracoContextFactory = TestUmbracoContextFactory.Create( umbracoContextAccessor: umbracoContextAccessor); var webRoutingSettings = new WebRoutingSettings(); - var navigationQueryService = new Mock(); - // Guid? parentKey = null; - // navigationQueryService.Setup(x => x.TryGetParentKey(It.IsAny(), out parentKey)).Returns(true); - IEnumerable ancestorKeys = []; - navigationQueryService.Setup(x => x.TryGetAncestorsKeys(It.IsAny(), out ancestorKeys)).Returns(true); + var publishedUrlProvider = CreatePublishedUrlProvider( + contentUrlProvider, + new Mock(), + umbracoContextAccessor); - var publishedContentStatusFilteringService = new Mock(); + using (var reference = umbracoContextFactory.EnsureUmbracoContext()) + { + var contentCache = Mock.Get(reference.UmbracoContext.Content); + contentCache.Setup(x => x.GetById(It.IsAny())).Returns(publishedContent.Object); + + var linkParser = new HtmlLocalLinkParser(publishedUrlProvider); + + // Act + var relativeOutput = linkParser.EnsureInternalLinks(input, UrlMode.Relative); + var absoluteOutput = linkParser.EnsureInternalLinks(input, UrlMode.Absolute); + + // Assert + Assert.AreEqual("hello href=\"/relative-url\" world", relativeOutput); + Assert.AreEqual("hello href=\"http://example.com/absolute-url\" world", absoluteOutput); + } + } + + [TestCase(UrlMode.Default, "hello href=\"{localLink:1234}\" world ", "hello href=\"/relative-url\" world ")] + [TestCase(UrlMode.Relative, "hello href=\"{localLink:1234}\" world ", "hello href=\"/relative-url\" world ")] + [TestCase(UrlMode.Absolute, "hello href=\"{localLink:1234}\" world ", "hello href=\"https://example.com/absolute-url\" world ")] + [TestCase(UrlMode.Auto, "hello href=\"{localLink:1234}\" world ", "hello href=\"/relative-url\" world ")] + [TestCase(UrlMode.Default, "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/relative-url\" world ")] + [TestCase(UrlMode.Relative, "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/relative-url\" world ")] + [TestCase(UrlMode.Absolute, "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"https://example.com/absolute-url\" world ")] + [TestCase(UrlMode.Auto, "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/relative-url\" world ")] + [TestCase(UrlMode.Default, "hello href=\"{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/media/relative/image.jpg\" world ")] + [TestCase(UrlMode.Relative, "hello href=\"{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/media/relative/image.jpg\" world ")] + [TestCase(UrlMode.Absolute, "hello href=\"{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"https://example.com/media/absolute/image.jpg\" world ")] + [TestCase(UrlMode.Auto, "hello href=\"{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/media/relative/image.jpg\" world ")] + public void ParseLocalLinks_WithVariousUrlModes_ReturnsCorrectUrls(UrlMode urlMode, string input, string expectedResult) + { + // Setup content URL provider that returns different URLs based on UrlMode + var contentUrlProvider = new Mock(); + contentUrlProvider + .Setup(x => x.GetUrl( + It.IsAny(), + UrlMode.Default, + It.IsAny(), + It.IsAny())) + .Returns(UrlInfo.Url("/relative-url")); + contentUrlProvider + .Setup(x => x.GetUrl( + It.IsAny(), + UrlMode.Relative, + It.IsAny(), + It.IsAny())) + .Returns(UrlInfo.Url("/relative-url")); + contentUrlProvider + .Setup(x => x.GetUrl( + It.IsAny(), + UrlMode.Absolute, + It.IsAny(), + It.IsAny())) + .Returns(UrlInfo.Url("https://example.com/absolute-url")); + contentUrlProvider + .Setup(x => x.GetUrl( + It.IsAny(), + UrlMode.Auto, + It.IsAny(), + It.IsAny())) + .Returns(UrlInfo.Url("/relative-url")); + + var contentType = new PublishedContentType( + Guid.NewGuid(), + 666, + "alias", + PublishedItemType.Content, + Enumerable.Empty(), + Enumerable.Empty(), + ContentVariation.Nothing); + var publishedContent = new Mock(); + publishedContent.Setup(x => x.Id).Returns(1234); + publishedContent.Setup(x => x.ContentType).Returns(contentType); + + // Setup media URL provider that returns different URLs based on UrlMode + var mediaUrlProvider = new Mock(); + mediaUrlProvider.Setup(x => x.GetMediaUrl( + It.IsAny(), + It.IsAny(), + UrlMode.Default, + It.IsAny(), + It.IsAny())) + .Returns(UrlInfo.Url("/media/relative/image.jpg")); + mediaUrlProvider.Setup(x => x.GetMediaUrl( + It.IsAny(), + It.IsAny(), + UrlMode.Relative, + It.IsAny(), + It.IsAny())) + .Returns(UrlInfo.Url("/media/relative/image.jpg")); + mediaUrlProvider.Setup(x => x.GetMediaUrl( + It.IsAny(), + It.IsAny(), + UrlMode.Absolute, + It.IsAny(), + It.IsAny())) + .Returns(UrlInfo.Url("https://example.com/media/absolute/image.jpg")); + mediaUrlProvider.Setup(x => x.GetMediaUrl( + It.IsAny(), + It.IsAny(), + UrlMode.Auto, + It.IsAny(), + It.IsAny())) + .Returns(UrlInfo.Url("/media/relative/image.jpg")); + + var mediaType = new PublishedContentType( + Guid.NewGuid(), + 777, + "image", + PublishedItemType.Media, + Enumerable.Empty(), + Enumerable.Empty(), + ContentVariation.Nothing); + var media = new Mock(); + media.Setup(x => x.ContentType).Returns(mediaType); + + var umbracoContextAccessor = new TestUmbracoContextAccessor(); + var umbracoContextFactory = TestUmbracoContextFactory.Create( + umbracoContextAccessor: umbracoContextAccessor); + + var webRoutingSettings = new WebRoutingSettings(); + + var publishedUrlProvider = CreatePublishedUrlProvider( + contentUrlProvider, + mediaUrlProvider, + umbracoContextAccessor); using (var reference = umbracoContextFactory.EnsureUmbracoContext()) { @@ -239,25 +426,35 @@ public class HtmlLocalLinkParserTests mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); - var publishStatusQueryService = new Mock(); - publishStatusQueryService - .Setup(x => x.IsDocumentPublished(It.IsAny(), It.IsAny())) - .Returns(true); - - var publishedUrlProvider = new UrlProvider( - umbracoContextAccessor, - Options.Create(webRoutingSettings), - new UrlProviderCollection(() => new[] { contentUrlProvider.Object }), - new MediaUrlProviderCollection(() => new[] { mediaUrlProvider.Object }), - Mock.Of(), - navigationQueryService.Object, - publishedContentStatusFilteringService.Object); - var linkParser = new HtmlLocalLinkParser(publishedUrlProvider); - var output = linkParser.EnsureInternalLinks(input); + var output = linkParser.EnsureInternalLinks(input, urlMode); - Assert.AreEqual(result, output); + Assert.AreEqual(expectedResult, output); } } + + private static UrlProvider CreatePublishedUrlProvider( + Mock contentUrlProvider, + Mock mediaUrlProvider, + TestUmbracoContextAccessor umbracoContextAccessor) + { + var navigationQueryService = new Mock(); + IEnumerable ancestorKeys = []; + navigationQueryService.Setup(x => x.TryGetAncestorsKeys(It.IsAny(), out ancestorKeys)).Returns(true); + + var publishStatusQueryService = new Mock(); + publishStatusQueryService + .Setup(x => x.IsDocumentPublished(It.IsAny(), It.IsAny())) + .Returns(true); + + return new UrlProvider( + umbracoContextAccessor, + Options.Create(new WebRoutingSettings()), + new UrlProviderCollection(() => new[] { contentUrlProvider.Object }), + new MediaUrlProviderCollection(() => new[] { mediaUrlProvider.Object }), + Mock.Of(), + navigationQueryService.Object, + new Mock().Object); + } }