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);
}
}