// Copyright (c) Umbraco. // See LICENSE for more details. using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; 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; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Templates; [TestFixture] public class HtmlLocalLinkParserTests { [Test] public void Returns_Udis_From_LocalLinks() { var input = @"

other page

media

"; var parser = new HtmlLocalLinkParser(Mock.Of()); var result = parser.FindUdisFromLocalLinks(input).ToList(); Assert.Multiple(() => { Assert.AreEqual(2, result.Count); Assert.Contains(UdiParser.Parse("umb://document/eed5fc6b-96fd-45a5-a0f1-b1adfb483c2f"), result); Assert.Contains(UdiParser.Parse("umb://media/7e21a725-b905-4c5f-86dc-8c41ec116e39"), result); }); } // todo remove at some point and the implementation. [Test] public void Returns_Udis_From_Legacy_LocalLinks() { var input = @"

hello

hello

"; var parser = new HtmlLocalLinkParser(Mock.Of()); var result = parser.FindUdisFromLocalLinks(input).ToList(); Assert.Multiple(() => { Assert.AreEqual(2, result.Count); Assert.Contains(UdiParser.Parse("umb://document/C093961595094900AAF9170DDE6AD442"), result); Assert.Contains(UdiParser.Parse("umb://document-type/2D692FCB070B4CDA92FB6883FDBFD6E2"), result); }); } // todo remove at some point and the implementation. [Test] public void Returns_Udis_From_Legacy_And_Current_LocalLinks() { var input = @"

hello

hello

other page

media

"; var parser = new HtmlLocalLinkParser(Mock.Of()); var result = parser.FindUdisFromLocalLinks(input).ToList(); Assert.Multiple(() => { Assert.AreEqual(4, result.Count); Assert.Contains(UdiParser.Parse("umb://document/eed5fc6b-96fd-45a5-a0f1-b1adfb483c2f"), result); Assert.Contains(UdiParser.Parse("umb://media/7e21a725-b905-4c5f-86dc-8c41ec116e39"), result); Assert.Contains(UdiParser.Parse("umb://document/C093961595094900AAF9170DDE6AD442"), result); Assert.Contains(UdiParser.Parse("umb://document-type/2D692FCB070B4CDA92FB6883FDBFD6E2"), result); }); } [TestCase("", "")] // current [TestCase( "world", "world")] [TestCase( "world", "world")] [TestCase( "world", "world")] [TestCase( "world", "world")] [TestCase( "

world

world

", "

world

world

")] // attributes order should not matter [TestCase( "world", "world")] [TestCase( "world", "world")] [TestCase( "world", "world")] // anchors and query strings [TestCase( "world", "world")] [TestCase( "world", "world")] // custom type ignored [TestCase( "world", "world")] // legacy [TestCase( "hello href=\"{localLink:1234}\" world ", "hello href=\"/my-test-url\" world ")] [TestCase( "hello href=\"{localLink:umb://document/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"/my-test-url\" world ")] [TestCase( "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/my-test-url\" world ")] [TestCase( "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}#anchor\" world ", "hello href=\"/my-test-url#anchor\" world ")] [TestCase( "hello href=\"{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/media/1001/my-image.jpg\" world ")] [TestCase( "hello href='{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}' world ", "hello href='/media/1001/my-image.jpg' world ")] // This one has an invalid char so won't match. [TestCase( "hello href=\"{localLink:umb^://document/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"{localLink:umb^://document/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ")] [TestCase( "hello href=\"{localLink:umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"\" world ")] public void ParseLocalLinks(string input, string result) { // setup a mock URL provider which we'll use for testing var contentUrlProvider = new Mock(); contentUrlProvider .Setup(x => x.GetUrl( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(UrlInfo.Url("/my-test-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 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 mediaUrlProvider = new Mock(); mediaUrlProvider.Setup(x => x.GetMediaUrl( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(UrlInfo.Url("/media/1001/my-image.jpg")); 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 publishedUrlProvider = CreatePublishedUrlProvider( contentUrlProvider, new Mock(), umbracoContextAccessor); 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()) { 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 linkParser = new HtmlLocalLinkParser(publishedUrlProvider); var output = linkParser.EnsureInternalLinks(input, urlMode); 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); } }