diff --git a/src/Umbraco.Tests/Routing/NiceUrlRoutesTests.cs b/src/Umbraco.Tests/Routing/NiceUrlRoutesTests.cs new file mode 100644 index 0000000000..79f2b2b64d --- /dev/null +++ b/src/Umbraco.Tests/Routing/NiceUrlRoutesTests.cs @@ -0,0 +1,256 @@ +using System; +using NUnit.Framework; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Tests.TestHelpers; +using Umbraco.Web.PublishedCache.XmlPublishedCache; +using Umbraco.Web.Routing; + +namespace Umbraco.Tests.Routing +{ + [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerFixture)] + [TestFixture] + public class NiceUrlRoutesTests : BaseRoutingTest + { + #region Test Setup + + protected override void FreezeResolution() + { + SiteDomainHelperResolver.Current = new SiteDomainHelperResolver(new SiteDomainHelper()); + + base.FreezeResolution(); + } + + private IUmbracoSettingsSection _umbracoSettings; + + public override void Initialize() + { + base.Initialize(); + + //generate new mock settings and assign so we can configure in individual tests + _umbracoSettings = SettingsForTests.GenerateMockSettings(); + SettingsForTests.ConfigureSettings(_umbracoSettings); + } + + protected override string GetXmlContent(int templateId) + { + return @" + + +]> + + + + + + + + + + + + + + + + + + + + + + + +"; + } + + #endregion + + /* + * 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 + + 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 + + return DetermineRouteById(id) + + + +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 + * + * And the tests should verify all the quirks that are due to + * hideTopLevelFromPath + */ + + [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")] + [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 GetRouteById(int id, bool hide, string expected) + { + var umbracoContext = GetUmbracoContext("/test", 0); + var cache = umbracoContext.ContentCache.InnerCache as PublishedContentCache; + if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); + + SettingsForTests.HideTopLevelNodeFromPath = hide; + + var route = cache.GetRouteById(umbracoContext, false, id); + Assert.AreEqual(expected, 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)] + [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 GetByRoute(string route, bool hide, int expected) + { + var umbracoContext = GetUmbracoContext("/test", 0); + var cache = umbracoContext.ContentCache.InnerCache as PublishedContentCache; + if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); + + SettingsForTests.HideTopLevelNodeFromPath = hide; + + var content = cache.GetByRoute(umbracoContext, false, route); + if (expected < 0) + { + Assert.IsNull(content); + } + else + { + Assert.IsNotNull(content); + Assert.AreEqual(expected, content.Id); + } + } + } +}