using System.Collections.Generic; using System.Linq; using NUnit.Framework; using Umbraco.Cms.Infrastructure.PublishedCache; using Umbraco.Cms.Tests.Common.Published; using Umbraco.Cms.Tests.UnitTests.TestHelpers; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache; // purpose: test the values returned by PublishedContentCache.GetRouteById // and .GetByRoute (no caching at all, just routing nice URLs) including all // the quirks due to hideTopLevelFromPath and backward compatibility. public class UrlRoutesTests : PublishedSnapshotServiceTestBase { private static string GetXmlContent(int templateId) => @" ]> "; /* * Just so it's documented somewhere, as of jan. 2017, routes obey the following pseudo-code: GetByRoute(route, hide = null): route is "[id]/[path]" hide = hide ?? global.hide root = id ? node(id) : document content = cached(route) ?? DetermineIdByRoute(route, hide) # route is "1234/path/to/content", finds "content" # but if there is domain 5678 on "to", the *true* route of "content" is "5678/content" # so although the route does match, we don't cache it # there are not other reason not to cache it if content and no domain between root and content: cache route (as trusted) return content DetermineIdByRoute(route, hide): route is "[id]/[path]" try return NavigateRoute(id ?? 0, path, hide:hide) return null NavigateRoute(id, path, hide): if path: if id: start = node(id) else: start = document # 'navigate ... from ...' uses lowest sortOrder in case of collision if hide and ![id]: # if hiding, then for "/foo" we want to look for "/[any]/foo" for each child of start: try return navigate path from child # but if it fails, we also want to try "/foo" # fail now if more than one part eg "/foo/bar" if path is "/[any]/...": fail try return navigate path from start else: if id: return node(id) else: return root node with lowest sortOrder GetRouteById(id): route = cached(id) if route: return route # never cache the route, it may be colliding route = DetermineRouteById(id) if route: cache route (as not trusted) return route DetermineRouteById(id): node = node(id) walk up from node to domain or root, assemble parts = URL segments if !domain and global.hide: if id.parent: # got /top/[path]content, can remove /top remove top part else: # got /content, should remove only if it is the # node with lowest sort order root = root node with lowest sortOrder if root == node: remove top part compose path from parts route = assemble "[domain.id]/[path]" return route */ /* * The Xml structure for the following tests is: * * root * A 1000 * B 1001 * C 1002 * D 1003 * X 2000 * Y 2001 * Z 2002 * A 2003 * B 2004 * C 2005 * E 2006 * */ [TestCase(1000, false, "/a")] [TestCase(1001, false, "/a/b")] [TestCase(1002, false, "/a/b/c")] [TestCase(1003, false, "/a/b/c/d")] [TestCase(2000, false, "/x")] [TestCase(2001, false, "/x/y")] [TestCase(2002, false, "/x/y/z")] [TestCase(2003, false, "/x/a")] [TestCase(2004, false, "/x/b")] [TestCase(2005, false, "/x/b/c")] [TestCase(2006, false, "/x/b/e")] public void GetRouteByIdNoHide(int id, bool hide, string expected) { GlobalSettings.HideTopLevelNodeFromPath = hide; var xml = GetXmlContent(1234); IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( xml, TestHelper.ShortStringHelper, out var contentTypes, out var dataTypes).ToList(); InitializedCache(kits, contentTypes, dataTypes); var cache = GetPublishedSnapshot().Content; var route = cache.GetRouteById(false, id); Assert.AreEqual(expected, route); } [TestCase(1000, true, "/")] [TestCase(1001, true, "/b")] [TestCase(1002, true, "/b/c")] [TestCase(1003, true, "/b/c/d")] [TestCase(2000, true, "/x")] [TestCase(2001, true, "/y")] [TestCase(2002, true, "/y/z")] [TestCase(2003, true, "/a")] [TestCase(2004, true, "/b")] // collision! [TestCase(2005, true, "/b/c")] // collision! [TestCase(2006, true, "/b/e")] // risky! public void GetRouteByIdHide(int id, bool hide, string expected) { GlobalSettings.HideTopLevelNodeFromPath = hide; var xml = GetXmlContent(1234); IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( xml, TestHelper.ShortStringHelper, out var contentTypes, out var dataTypes).ToList(); InitializedCache(kits, contentTypes, dataTypes); var cache = GetPublishedSnapshot().Content; var route = cache.GetRouteById(false, id); Assert.AreEqual(expected, route); } [Test] public void GetRouteByIdCache() { GlobalSettings.HideTopLevelNodeFromPath = false; var xml = GetXmlContent(1234); IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( xml, TestHelper.ShortStringHelper, out var contentTypes, out var dataTypes).ToList(); InitializedCache(kits, contentTypes, dataTypes); var cache = GetPublishedSnapshot().Content; var route = cache.GetRouteById(false, 1000); Assert.AreEqual("/a", route); } [TestCase("/", false, 1000)] [TestCase("/a", false, 1000)] // yes! [TestCase("/a/b", false, 1001)] [TestCase("/a/b/c", false, 1002)] [TestCase("/a/b/c/d", false, 1003)] [TestCase("/x", false, 2000)] public void GetByRouteNoHide(string route, bool hide, int expected) { GlobalSettings.HideTopLevelNodeFromPath = hide; var xml = GetXmlContent(1234); IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( xml, TestHelper.ShortStringHelper, out var contentTypes, out var dataTypes).ToList(); InitializedCache(kits, contentTypes, dataTypes); var cache = GetPublishedSnapshot().Content; const bool preview = false; // make sure we don't cache - but HOW? should be some sort of switch?! var content = cache.GetByRoute(preview, route); if (expected < 0) { Assert.IsNull(content); } else { Assert.IsNotNull(content); Assert.AreEqual(expected, content.Id); } } [TestCase("/", true, 1000)] [TestCase("/a", true, 2003)] [TestCase("/a/b", true, -1)] [TestCase("/x", true, 2000)] // oops! [TestCase("/x/y", true, -1)] // yes! [TestCase("/y", true, 2001)] [TestCase("/y/z", true, 2002)] [TestCase("/b", true, 1001)] // (hence the 2004 collision) [TestCase("/b/c", true, 1002)] // (hence the 2005 collision) public void GetByRouteHide(string route, bool hide, int expected) { GlobalSettings.HideTopLevelNodeFromPath = hide; var xml = GetXmlContent(1234); IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( xml, TestHelper.ShortStringHelper, out var contentTypes, out var dataTypes).ToList(); InitializedCache(kits, contentTypes, dataTypes); var cache = GetPublishedSnapshot().Content; const bool preview = false; // make sure we don't cache - but HOW? should be some sort of switch?! var content = cache.GetByRoute(preview, route); if (expected < 0) { Assert.IsNull(content); } else { Assert.IsNotNull(content); Assert.AreEqual(expected, content.Id); } } [Test] public void GetByRouteCache() { GlobalSettings.HideTopLevelNodeFromPath = false; var xml = GetXmlContent(1234); IEnumerable kits = PublishedContentXmlAdapter.GetContentNodeKits( xml, TestHelper.ShortStringHelper, out var contentTypes, out var dataTypes).ToList(); InitializedCache(kits, contentTypes, dataTypes); var cache = GetPublishedSnapshot().Content; var content = cache.GetByRoute(false, "/a/b/c"); Assert.IsNotNull(content); Assert.AreEqual(1002, content.Id); } }