Migrating tests that depend on Published Cache from the old test project (#11242)

* starts cleaning up old test project, removing ones we'll never convert, moves new test to where it should be.

* Makes ContentNodeKit immutable properties, moves first nucache tests over

* Gets the Nucache unit tests working and refactors a bit to use builder pattern for models.

* Migrates first xml based cache test to use nucache.

* Migrates a bunch more

* Migrates remaining tests for PublishedContentTests

* Moves PublishedRouterTests

* Moves PublishedContentExtensionTests

* Moves more tests.

* committing wip

* committing wip

* Gets PublishedContentLanguageVariantTests converted and working.

* Fixes DataTable ext method and moves PublishedContentDataTableTests

* Moves PublishedMediaTests

* wip - moving EntityXmlSerializerTests

* Moves more tests

* moves more tests

* moves more tests

* Move another test

* Moves more tests

* Fix test

* move another test

* Moves more tests

* Moves more tests

* Moves more tests

* wip before merge

* More tests

* More tests

* More tests

* More tests

* More tests

* More tests

* Cleanup and moving classes.

* Remove unused code

* Fixed failing tests, due to new null checks, that did not exist in v8

* Avoid breaking changes

* Unbreak more things, even that it the old solution was crazy..

* Fixed bug where ordering of stream readings was changed..

* cleanup

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
Shannon Deminick
2021-10-19 23:11:54 +11:00
committed by GitHub
parent 49e1aec71c
commit c77dc5dc00
171 changed files with 5400 additions and 17944 deletions

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -18,6 +18,8 @@ namespace Umbraco.Cms.Core.Cache
/// </summary>
private readonly ConcurrentDictionary<string, Lazy<object>> _items = new ConcurrentDictionary<string, Lazy<object>>();
public IEnumerable<string> Keys => _items.Keys;
public int Count => _items.Count;
/// <inheritdoc />

View File

@@ -1328,8 +1328,8 @@ namespace Umbraco.Extensions
{"NodeTypeAlias", "NodeTypeAlias"},
{"CreateDate", "CreateDate"},
{"UpdateDate", "UpdateDate"},
{"CreatorName", "CreatorName"},
{"WriterName", "WriterName"},
{"CreatorId", "CreatorId"},
{"WriterId", "WriterId"},
{"Url", "Url"}
};

View File

@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Serialization;
@@ -16,8 +16,6 @@ namespace Umbraco.Cms.Core.PropertyEditors
[HideFromTypeFinder]
public class VoidEditor : DataEditor
{
private readonly IJsonSerializer _jsonSerializer;
/// <summary>
/// Initializes a new instance of the <see cref="VoidEditor"/> class.
/// </summary>

View File

@@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PublishedCache.Internal
{
// TODO: Only used in unit tests, needs to be moved to test project
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class InternalPublishedContent : IPublishedContent
{
public InternalPublishedContent(IPublishedContentType contentType)

View File

@@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Xml;
namespace Umbraco.Cms.Core.PublishedCache.Internal
{
// TODO: Only used in unit tests, needs to be moved to test project
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class InternalPublishedContentCache : PublishedCacheBase, IPublishedContentCache, IPublishedMediaCache
{
private readonly Dictionary<int, IPublishedContent> _content = new Dictionary<int, IPublishedContent>();

View File

@@ -1,7 +1,10 @@
using Umbraco.Cms.Core.Models.PublishedContent;
using System.ComponentModel;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Core.PublishedCache.Internal
{
// TODO: Only used in unit tests, needs to be moved to test project
[EditorBrowsable(EditorBrowsableState.Never)]
public class InternalPublishedProperty : IPublishedProperty
{
public IPublishedPropertyType PropertyType { get; set; }

View File

@@ -1,8 +1,12 @@
using System;
using System.ComponentModel;
using Umbraco.Cms.Core.Cache;
namespace Umbraco.Cms.Core.PublishedCache.Internal
{
// TODO: Only used in unit tests, needs to be moved to test project
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class InternalPublishedSnapshot : IPublishedSnapshot
{
public InternalPublishedContentCache InnerContentCache { get; } = new InternalPublishedContentCache();

View File

@@ -1,10 +1,13 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
using Umbraco.Cms.Core.Cache;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PublishedCache.Internal
{
// TODO: Only used in unit tests, needs to be moved to test project
[EditorBrowsable(EditorBrowsableState.Never)]
public class InternalPublishedSnapshotService : IPublishedSnapshotService
{
private InternalPublishedSnapshot _snapshot;

View File

@@ -1,116 +1,109 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Routing
{
/// <summary>
/// Provides urls.
/// Provides urls.
/// </summary>
public class DefaultUrlProvider : IUrlProvider
{
private readonly RequestHandlerSettings _requestSettings;
private readonly ILocalizationService _localizationService;
private readonly ILocalizedTextService _localizedTextService;
private readonly ILogger<DefaultUrlProvider> _logger;
private readonly RequestHandlerSettings _requestSettings;
private readonly ISiteDomainMapper _siteDomainMapper;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly UriUtility _uriUtility;
public DefaultUrlProvider(IOptions<RequestHandlerSettings> requestSettings, ILogger<DefaultUrlProvider> logger, ISiteDomainMapper siteDomainMapper, IUmbracoContextAccessor umbracoContextAccessor, UriUtility uriUtility)
[Obsolete("Use ctor with all parameters")]
public DefaultUrlProvider(IOptions<RequestHandlerSettings> requestSettings, ILogger<DefaultUrlProvider> logger,
ISiteDomainMapper siteDomainMapper, IUmbracoContextAccessor umbracoContextAccessor, UriUtility uriUtility)
: this(requestSettings, logger, siteDomainMapper, umbracoContextAccessor, uriUtility,
StaticServiceProvider.Instance.GetRequiredService<ILocalizationService>())
{
}
public DefaultUrlProvider(
IOptions<RequestHandlerSettings> requestSettings,
ILogger<DefaultUrlProvider> logger,
ISiteDomainMapper siteDomainMapper,
IUmbracoContextAccessor umbracoContextAccessor,
UriUtility uriUtility,
ILocalizationService localizationService)
{
_requestSettings = requestSettings.Value;
_logger = logger;
_siteDomainMapper = siteDomainMapper;
_uriUtility = uriUtility;
_umbracoContextAccessor = umbracoContextAccessor;
_uriUtility = uriUtility;
_localizationService = localizationService;
}
#region GetUrl
/// <inheritdoc />
public virtual UrlInfo GetUrl(IPublishedContent content, UrlMode mode, string culture, Uri current)
{
if (!current.IsAbsoluteUri) throw new ArgumentException("Current URL must be absolute.", nameof(current));
var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext();
// will not use cache if previewing
var route = umbracoContext.Content.GetRouteById(content.Id, culture);
return GetUrlFromRoute(route, umbracoContext, content.Id, current, mode, culture);
}
internal UrlInfo GetUrlFromRoute(string route, IUmbracoContext umbracoContext, int id, Uri current, UrlMode mode, string culture)
{
if (string.IsNullOrWhiteSpace(route))
{
_logger.LogDebug("Couldn't find any page with nodeId={NodeId}. This is most likely caused by the page not being published.", id);
return null;
}
// extract domainUri and path
// route is /<path> or <domainRootId>/<path>
var pos = route.IndexOf('/');
var path = pos == 0 ? route : route.Substring(pos);
var domainUri = pos == 0
? null
: DomainUtilities.DomainForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, int.Parse(route.Substring(0, pos), CultureInfo.InvariantCulture), current, culture);
// assemble the URL from domainUri (maybe null) and path
var url = AssembleUrl(domainUri, path, current, mode).ToString();
return UrlInfo.Url(url, culture);
}
#endregion
#region GetOtherUrls
/// <summary>
/// Gets the other URLs of a published content.
/// Gets the other URLs of a published content.
/// </summary>
/// <param name="umbracoContextAccessor">The Umbraco context.</param>
/// <param name="id">The published content id.</param>
/// <param name="current">The current absolute URL.</param>
/// <returns>The other URLs for the published content.</returns>
/// <remarks>
/// <para>Other URLs are those that <c>GetUrl</c> would not return in the current context, but would be valid
/// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...).</para>
/// <para>
/// Other URLs are those that <c>GetUrl</c> would not return in the current context, but would be valid
/// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...).
/// </para>
/// </remarks>
public virtual IEnumerable<UrlInfo> GetOtherUrls(int id, Uri current)
{
var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext();
var node = umbracoContext.Content.GetById(id);
IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext();
IPublishedContent node = umbracoContext.Content.GetById(id);
if (node == null)
{
yield break;
}
// look for domains, walking up the tree
var n = node;
var domainUris = DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, false);
IPublishedContent n = node;
IEnumerable<DomainAndUri> domainUris =
DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id,
current, false);
while (domainUris == null && n != null) // n is null at root
{
n = n.Parent; // move to parent node
domainUris = n == null ? null : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, excludeDefault: true);
domainUris = n == null
? null
: DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id,
current);
}
// no domains = exit
if (domainUris ==null)
if (domainUris == null)
{
yield break;
}
foreach (var d in domainUris)
foreach (DomainAndUri d in domainUris)
{
var culture = d?.Culture;
// although we are passing in culture here, if any node in this path is invariant, it ignores the culture anyways so this is ok
var route = umbracoContext.Content.GetRouteById(id, culture);
if (route == null) continue;
if (route == null)
{
continue;
}
// need to strip off the leading ID for the route if it exists (occurs if the route is for a node with a domain assigned)
var pos = route.IndexOf('/');
@@ -124,9 +117,57 @@ namespace Umbraco.Cms.Core.Routing
#endregion
#region GetUrl
/// <inheritdoc />
public virtual UrlInfo GetUrl(IPublishedContent content, UrlMode mode, string culture, Uri current)
{
if (!current.IsAbsoluteUri)
{
throw new ArgumentException("Current URL must be absolute.", nameof(current));
}
IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext();
// will not use cache if previewing
var route = umbracoContext.Content.GetRouteById(content.Id, culture);
return GetUrlFromRoute(route, umbracoContext, content.Id, current, mode, culture);
}
internal UrlInfo GetUrlFromRoute(string route, IUmbracoContext umbracoContext, int id, Uri current,
UrlMode mode, string culture)
{
if (string.IsNullOrWhiteSpace(route))
{
_logger.LogDebug(
"Couldn't find any page with nodeId={NodeId}. This is most likely caused by the page not being published.",
id);
return null;
}
// extract domainUri and path
// route is /<path> or <domainRootId>/<path>
var pos = route.IndexOf('/');
var path = pos == 0 ? route : route.Substring(pos);
var domainUri = pos == 0
? null
: DomainUtilities.DomainForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, int.Parse(route.Substring(0, pos), CultureInfo.InvariantCulture), current, culture);
var defaultCulture = _localizationService.GetDefaultLanguageIsoCode();
if (domainUri is not null || culture == defaultCulture || culture is null)
{
var url = AssembleUrl(domainUri, path, current, mode).ToString();
return UrlInfo.Url(url, culture);
}
return null;
}
#endregion
#region Utilities
Uri AssembleUrl(DomainAndUri domainUri, string path, Uri current, UrlMode mode)
private Uri AssembleUrl(DomainAndUri domainUri, string path, Uri current, UrlMode mode)
{
Uri uri;
@@ -135,7 +176,9 @@ namespace Umbraco.Cms.Core.Routing
if (domainUri == null) // no domain was found
{
if (current == null)
{
mode = UrlMode.Relative; // best we can do
}
switch (mode)
{
@@ -155,10 +198,15 @@ namespace Umbraco.Cms.Core.Routing
if (mode == UrlMode.Auto)
{
//this check is a little tricky, we can't just compare domains
if (current != null && domainUri.Uri.GetLeftPart(UriPartial.Authority) == current.GetLeftPart(UriPartial.Authority))
if (current != null && domainUri.Uri.GetLeftPart(UriPartial.Authority) ==
current.GetLeftPart(UriPartial.Authority))
{
mode = UrlMode.Relative;
}
else
{
mode = UrlMode.Absolute;
}
}
switch (mode)
@@ -179,9 +227,9 @@ namespace Umbraco.Cms.Core.Routing
return _uriUtility.UriFromUmbraco(uri, _requestSettings);
}
string CombinePaths(string path1, string path2)
private string CombinePaths(string path1, string path2)
{
string path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2;
var path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2;
return path == "/" ? path : path.TrimEnd(Constants.CharArrays.ForwardSlash);
}

View File

@@ -4,18 +4,36 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using Umbraco.Extensions;
using System.ComponentModel;
namespace Umbraco.Cms.Core.Routing
{
/// <summary>
/// Provides utilities to handle site domains.
/// Provides utilities to handle site domains.
/// </summary>
public class SiteDomainMapper : ISiteDomainMapper, IDisposable
{
public void Dispose() =>
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(true);
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
// This is pretty nasty disposing a static on an instance but it's because this whole class
// is pretty fubar. I'm sure we've fixed this all up in netcore now? We need to remove all statics.
_configLock.Dispose();
}
_disposedValue = true;
}
}
#region Configure
private readonly ReaderWriterLockSlim _configLock = new ReaderWriterLockSlim();
private readonly ReaderWriterLockSlim _configLock = new();
private Dictionary<string, Dictionary<string, string[]>> _qualifiedSites;
private bool _disposedValue;
@@ -25,10 +43,12 @@ namespace Umbraco.Cms.Core.Routing
// these are for validation
//private const string DomainValidationSource = @"^(\*|((?i:http[s]?://)?([-\w]+(\.[-\w]+)*)(:\d+)?(/[-\w]*)?))$";
private const string DomainValidationSource = @"^(((?i:http[s]?://)?([-\w]+(\.[-\w]+)*)(:\d+)?(/)?))$";
private static readonly Regex s_domainValidation = new Regex(DomainValidationSource, RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex s_domainValidation =
new(DomainValidationSource, RegexOptions.IgnoreCase | RegexOptions.Compiled);
/// <summary>
/// Clears the entire configuration.
/// Clears the entire configuration.
/// </summary>
public void Clear()
{
@@ -49,20 +69,21 @@ namespace Umbraco.Cms.Core.Routing
}
}
private IEnumerable<string> ValidateDomains(IEnumerable<string> domains)
{
private IEnumerable<string> ValidateDomains(IEnumerable<string> domains) =>
// must use authority format w/optional scheme and port, but no path
// any domain should appear only once
return domains.Select(domain =>
domains.Select(domain =>
{
if (!s_domainValidation.IsMatch(domain))
{
if (!s_domainValidation.IsMatch(domain))
throw new ArgumentOutOfRangeException(nameof(domains), $"Invalid domain: \"{domain}\".");
return domain;
});
}
throw new ArgumentOutOfRangeException(nameof(domains), $"Invalid domain: \"{domain}\".");
}
return domain;
});
/// <summary>
/// Adds a site.
/// Adds a site.
/// </summary>
/// <param name="key">A key uniquely identifying the site.</param>
/// <param name="domains">The site domains.</param>
@@ -87,7 +108,7 @@ namespace Umbraco.Cms.Core.Routing
}
/// <summary>
/// Adds a site.
/// Adds a site.
/// </summary>
/// <param name="key">A key uniquely identifying the site.</param>
/// <param name="domains">The site domains.</param>
@@ -112,7 +133,7 @@ namespace Umbraco.Cms.Core.Routing
}
/// <summary>
/// Removes a site.
/// Removes a site.
/// </summary>
/// <param name="key">A key uniquely identifying the site.</param>
internal void RemoveSite(string key)
@@ -122,11 +143,15 @@ namespace Umbraco.Cms.Core.Routing
_configLock.EnterWriteLock();
if (Sites == null || !Sites.ContainsKey(key))
{
return;
}
Sites.Remove(key);
if (Sites.Count == 0)
{
Sites = null;
}
if (Bindings != null && Bindings.ContainsKey(key))
{
@@ -134,11 +159,16 @@ namespace Umbraco.Cms.Core.Routing
{
Bindings[b].Remove(key);
if (Bindings[b].Count == 0)
{
Bindings.Remove(b);
}
}
Bindings.Remove(key);
if (Bindings.Count > 0)
{
Bindings = null;
}
}
_qualifiedSites = null;
@@ -153,12 +183,12 @@ namespace Umbraco.Cms.Core.Routing
}
/// <summary>
/// Binds some sites.
/// Binds some sites.
/// </summary>
/// <param name="keys">The keys uniquely identifying the sites to bind.</param>
/// <remarks>
/// <para>At the moment there is no public way to unbind sites. Clear and reconfigure.</para>
/// <para>If site1 is bound to site2 and site2 is bound to site3 then site1 is bound to site3.</para>
/// <para>At the moment there is no public way to unbind sites. Clear and reconfigure.</para>
/// <para>If site1 is bound to site2 and site2 is bound to site3 then site1 is bound to site3.</para>
/// </remarks>
public void BindSites(params string[] keys)
{
@@ -167,7 +197,9 @@ namespace Umbraco.Cms.Core.Routing
_configLock.EnterWriteLock();
foreach (var key in keys.Where(key => !Sites.ContainsKey(key)))
{
throw new ArgumentException($"Not an existing site key: {key}.", nameof(keys));
}
Bindings = Bindings ?? new Dictionary<string, List<string>>();
@@ -180,9 +212,12 @@ namespace Umbraco.Cms.Core.Routing
foreach (var key in allkeys)
{
if (!Bindings.ContainsKey(key))
{
Bindings[key] = new List<string>();
}
var xkey = key;
var addKeys = allkeys.Where(k => k != xkey).Except(Bindings[key]);
IEnumerable<string> addKeys = allkeys.Where(k => k != xkey).Except(Bindings[key]);
Bindings[key].AddRange(addKeys);
}
}
@@ -200,16 +235,18 @@ namespace Umbraco.Cms.Core.Routing
#region Map domains
/// <inheritdoc />
public virtual DomainAndUri MapDomain(IReadOnlyCollection<DomainAndUri> domainAndUris, Uri current, string culture, string defaultCulture)
public virtual DomainAndUri MapDomain(IReadOnlyCollection<DomainAndUri> domainAndUris, Uri current,
string culture, string defaultCulture)
{
var currentAuthority = current.GetLeftPart(UriPartial.Authority);
var qualifiedSites = GetQualifiedSites(current);
Dictionary<string, string[]> qualifiedSites = GetQualifiedSites(current);
return MapDomain(domainAndUris, qualifiedSites, currentAuthority, culture, defaultCulture);
}
/// <inheritdoc />
public virtual IEnumerable<DomainAndUri> MapDomains(IReadOnlyCollection<DomainAndUri> domainAndUris, Uri current, bool excludeDefault, string culture, string defaultCulture)
public virtual IEnumerable<DomainAndUri> MapDomains(IReadOnlyCollection<DomainAndUri> domainAndUris,
Uri current, bool excludeDefault, string culture, string defaultCulture)
{
// TODO: ignoring cultures entirely?
@@ -221,15 +258,18 @@ namespace Umbraco.Cms.Core.Routing
{
_configLock.EnterReadLock();
var qualifiedSites = GetQualifiedSitesInsideLock(current);
Dictionary<string, string[]> qualifiedSites = GetQualifiedSitesInsideLock(current);
if (excludeDefault)
{
// exclude the current one (avoid producing the absolute equivalent of what GetUrl returns)
var hintWithSlash = current.EndPathWithSlash();
var hinted = domainAndUris.FirstOrDefault(d => d.Uri.EndPathWithSlash().IsBaseOf(hintWithSlash));
Uri hintWithSlash = current.EndPathWithSlash();
DomainAndUri hinted =
domainAndUris.FirstOrDefault(d => d.Uri.EndPathWithSlash().IsBaseOf(hintWithSlash));
if (hinted != null)
{
ret = ret.Where(d => d != hinted);
}
// exclude the default one (avoid producing a possible duplicate of what GetUrl returns)
// only if the default one cannot be the current one ie if hinted is not null
@@ -237,17 +277,21 @@ namespace Umbraco.Cms.Core.Routing
{
// it is illegal to call MapDomain if domainAndUris is empty
// also, domainAndUris should NOT contain current, hence the test on hinted
var mainDomain = MapDomain(domainAndUris, qualifiedSites, currentAuthority, culture, defaultCulture); // what GetUrl would get
DomainAndUri mainDomain = MapDomain(domainAndUris, qualifiedSites, currentAuthority, culture,
defaultCulture); // what GetUrl would get
ret = ret.Where(d => d != mainDomain);
}
}
// we do our best, but can't do the impossible
if (qualifiedSites == null)
{
return ret;
}
// find a site that contains the current authority
var currentSite = qualifiedSites.FirstOrDefault(site => site.Value.Contains(currentAuthority));
KeyValuePair<string, string[]> currentSite =
qualifiedSites.FirstOrDefault(site => site.Value.Contains(currentAuthority));
// if current belongs to a site, pick every element from domainAndUris that also belong
// to that site -- or to any site bound to that site
@@ -257,7 +301,8 @@ namespace Umbraco.Cms.Core.Routing
candidateSites = new[] { currentSite };
if (Bindings != null && Bindings.ContainsKey(currentSite.Key))
{
var boundSites = qualifiedSites.Where(site => Bindings[currentSite.Key].Contains(site.Key));
IEnumerable<KeyValuePair<string, string[]>> boundSites =
qualifiedSites.Where(site => Bindings[currentSite.Key].Contains(site.Key));
candidateSites = candidateSites.Union(boundSites).ToArray();
// .ToArray ensures it is evaluated before the configuration lock is exited
@@ -273,7 +318,9 @@ namespace Umbraco.Cms.Core.Routing
}
// if we are able to filter, then filter, else return the whole lot
return candidateSites == null ? ret : ret.Where(d =>
return candidateSites == null
? ret
: ret.Where(d =>
{
var authority = d.Uri.GetLeftPart(UriPartial.Authority);
return candidateSites.Any(site => site.Value.Contains(authority));
@@ -301,11 +348,15 @@ namespace Umbraco.Cms.Core.Routing
{
// we do our best, but can't do the impossible
if (Sites == null)
{
return null;
}
// cached?
if (_qualifiedSites != null && _qualifiedSites.ContainsKey(current.Scheme))
{
return _qualifiedSites[current.Scheme];
}
_qualifiedSites = _qualifiedSites ?? new Dictionary<string, Dictionary<string, string[]>>();
@@ -314,7 +365,10 @@ namespace Umbraco.Cms.Core.Routing
return _qualifiedSites[current.Scheme] = Sites
.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.Select(d => new Uri(UriUtilityCore.StartWithScheme(d, current.Scheme)).GetLeftPart(UriPartial.Authority)).ToArray()
kvp => kvp.Value.Select(d =>
new Uri(UriUtilityCore.StartWithScheme(d, current.Scheme))
.GetLeftPart(UriPartial.Authority))
.ToArray()
);
// .ToDictionary will evaluate and create the dictionary immediately
@@ -322,61 +376,54 @@ namespace Umbraco.Cms.Core.Routing
// therefore it is safe to return and exit the configuration lock
}
private DomainAndUri MapDomain(IReadOnlyCollection<DomainAndUri> domainAndUris, Dictionary<string, string[]> qualifiedSites, string currentAuthority, string culture, string defaultCulture)
private DomainAndUri MapDomain(IReadOnlyCollection<DomainAndUri> domainAndUris,
Dictionary<string, string[]> qualifiedSites, string currentAuthority, string culture, string defaultCulture)
{
if (domainAndUris == null)
{
throw new ArgumentNullException(nameof(domainAndUris));
}
if (domainAndUris.Count == 0)
{
throw new ArgumentException("Cannot be empty.", nameof(domainAndUris));
}
// TODO: how shall we deal with cultures?
// we do our best, but can't do the impossible
// get the "default" domain ie the first one for the culture, else the first one (exists, length > 0)
if (qualifiedSites == null)
{
return domainAndUris.FirstOrDefault(x => x.Culture.InvariantEquals(culture))
?? domainAndUris.FirstOrDefault(x => x.Culture.InvariantEquals(defaultCulture));
?? domainAndUris.FirstOrDefault(x => x.Culture.InvariantEquals(defaultCulture))
?? (culture is null ? domainAndUris.First() : null);
}
// find a site that contains the current authority
var currentSite = qualifiedSites.FirstOrDefault(site => site.Value.Contains(currentAuthority));
KeyValuePair<string, string[]> currentSite =
qualifiedSites.FirstOrDefault(site => site.Value.Contains(currentAuthority));
// if current belongs to a site - try to pick the first element
// from domainAndUris that also belongs to that site
var ret = currentSite.Equals(default(KeyValuePair<string, string[]>))
DomainAndUri ret = currentSite.Equals(default(KeyValuePair<string, string[]>))
? null
: domainAndUris.FirstOrDefault(d => currentSite.Value.Contains(d.Uri.GetLeftPart(UriPartial.Authority)));
: domainAndUris.FirstOrDefault(d =>
currentSite.Value.Contains(d.Uri.GetLeftPart(UriPartial.Authority)));
// no match means that either current does not belong to a site, or the site it belongs to
// does not contain any of domainAndUris.
// does not contain any of domainAndUris. Yet we have to return something. here, it becomes
// a bit arbitrary.
// look through sites in order and pick the first domainAndUri that belongs to a site
ret = ret ?? qualifiedSites
.Where(site => site.Key != currentSite.Key)
.Select(site => domainAndUris.FirstOrDefault(domainAndUri =>
site.Value.Contains(domainAndUri.Uri.GetLeftPart(UriPartial.Authority))))
.FirstOrDefault(domainAndUri => domainAndUri != null);
// random, really
ret = ret ?? domainAndUris.FirstOrDefault(x => x.Culture.InvariantEquals(culture)) ?? domainAndUris.First();
return ret;
}
#endregion
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
// This is pretty nasty disposing a static on an instance but it's because this whole class
// is pretty fubar. I'm sure we've fixed this all up in netcore now? We need to remove all statics.
_configLock.Dispose();
}
_disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
}
}
}

View File

@@ -15,11 +15,11 @@ namespace Umbraco.Extensions
public static class UrlProviderExtensions
{
/// <summary>
/// Gets the URLs of the content item.
/// Gets the URLs of the content item.
/// </summary>
/// <remarks>
/// <para>Use when displaying URLs. If errors occur when generating the URLs, they will show in the list.</para>
/// <para>Contains all the URLs that we can figure out (based upon domains, etc).</para>
/// <para>Use when displaying URLs. If errors occur when generating the URLs, they will show in the list.</para>
/// <para>Contains all the URLs that we can figure out (based upon domains, etc).</para>
/// </remarks>
public static async Task<IEnumerable<UrlInfo>> GetContentUrlsAsync(
this IContent content,
@@ -33,16 +33,55 @@ namespace Umbraco.Extensions
UriUtility uriUtility,
IPublishedUrlProvider publishedUrlProvider)
{
if (content == null) throw new ArgumentNullException(nameof(content));
if (publishedRouter == null) throw new ArgumentNullException(nameof(publishedRouter));
if (umbracoContext == null) throw new ArgumentNullException(nameof(umbracoContext));
if (localizationService == null) throw new ArgumentNullException(nameof(localizationService));
if (textService == null) throw new ArgumentNullException(nameof(textService));
if (contentService == null) throw new ArgumentNullException(nameof(contentService));
if (logger == null) throw new ArgumentNullException(nameof(logger));
if (publishedUrlProvider == null) throw new ArgumentNullException(nameof(publishedUrlProvider));
if (uriUtility == null) throw new ArgumentNullException(nameof(uriUtility));
if (variationContextAccessor == null) throw new ArgumentNullException(nameof(variationContextAccessor));
if (content == null)
{
throw new ArgumentNullException(nameof(content));
}
if (publishedRouter == null)
{
throw new ArgumentNullException(nameof(publishedRouter));
}
if (umbracoContext == null)
{
throw new ArgumentNullException(nameof(umbracoContext));
}
if (localizationService == null)
{
throw new ArgumentNullException(nameof(localizationService));
}
if (textService == null)
{
throw new ArgumentNullException(nameof(textService));
}
if (contentService == null)
{
throw new ArgumentNullException(nameof(contentService));
}
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
if (publishedUrlProvider == null)
{
throw new ArgumentNullException(nameof(publishedUrlProvider));
}
if (uriUtility == null)
{
throw new ArgumentNullException(nameof(uriUtility));
}
if (variationContextAccessor == null)
{
throw new ArgumentNullException(nameof(variationContextAccessor));
}
var result = new List<UrlInfo>();
@@ -68,7 +107,9 @@ namespace Umbraco.Extensions
// get all URLs for all cultures
// in a HashSet, so de-duplicates too
foreach (UrlInfo cultureUrl in await GetContentUrlsByCultureAsync(content, cultures, publishedRouter, umbracoContext, contentService, textService, variationContextAccessor, logger, uriUtility, publishedUrlProvider))
foreach (UrlInfo cultureUrl in await GetContentUrlsByCultureAsync(content, cultures, publishedRouter,
umbracoContext, contentService, textService, variationContextAccessor, logger, uriUtility,
publishedUrlProvider))
{
urls.Add(cultureUrl);
}
@@ -79,15 +120,25 @@ namespace Umbraco.Extensions
// in some cases there will be the same URL for multiple cultures:
// * The entire branch is invariant
// * If there are less domain/cultures assigned to the branch than the number of cultures/languages installed
foreach (UrlInfo dUrl in urlGroup.DistinctBy(x => x.Text.ToUpperInvariant()).OrderBy(x => x.Text).ThenBy(x => x.Culture))
if (urlGroup.Key)
{
result.Add(dUrl);
result.AddRange(urlGroup.DistinctBy(x => x.Text.ToUpperInvariant())
.OrderBy(x => x.Text).ThenBy(x => x.Culture));
}
else
{
result.AddRange(urlGroup);
}
}
// get the 'other' URLs - ie not what you'd get with GetUrl() but URLs that would route to the document, nevertheless.
// for these 'other' URLs, we don't check whether they are routable, collide, anything - we just report them.
foreach (var otherUrl in publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text).ThenBy(x => x.Culture))
foreach (UrlInfo otherUrl in publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text)
.ThenBy(x => x.Culture))
{
// avoid duplicates
if (urls.Add(otherUrl))
@@ -100,7 +151,7 @@ namespace Umbraco.Extensions
}
/// <summary>
/// Tries to return a <see cref="UrlInfo"/> for each culture for the content while detecting collisions/errors
/// Tries to return a <see cref="UrlInfo" /> for each culture for the content while detecting collisions/errors
/// </summary>
private static async Task<IEnumerable<UrlInfo>> GetContentUrlsByCultureAsync(
IContent content,
@@ -151,7 +202,8 @@ namespace Umbraco.Extensions
// got a URL, deal with collisions, add URL
default:
// detect collisions, etc
Attempt<UrlInfo> hasCollision = await DetectCollisionAsync(logger, content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility);
Attempt<UrlInfo> hasCollision = await DetectCollisionAsync(logger, content, url, culture,
umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility);
if (hasCollision)
{
result.Add(hasCollision.Result);
@@ -168,7 +220,8 @@ namespace Umbraco.Extensions
return result;
}
private static UrlInfo HandleCouldNotGetUrl(IContent content, string culture, IContentService contentService, ILocalizedTextService textService)
private static UrlInfo HandleCouldNotGetUrl(IContent content, string culture, IContentService contentService,
ILocalizedTextService textService)
{
// document has a published version yet its URL is "#" => a parent must be
// unpublished, walk up the tree until we find it, and report.
@@ -176,27 +229,31 @@ namespace Umbraco.Extensions
do
{
parent = parent.ParentId > 0 ? contentService.GetParent(parent) : null;
}
while (parent != null && parent.Published && (!parent.ContentType.VariesByCulture() || parent.IsCulturePublished(culture)));
} while (parent != null && parent.Published &&
(!parent.ContentType.VariesByCulture() || parent.IsCulturePublished(culture)));
if (parent == null)
{
// oops, internal error
return UrlInfo.Message(textService.Localize("content", "parentNotPublishedAnomaly"), culture);
}
else if (!parent.Published)
if (!parent.Published)
{
// totally not published
return UrlInfo.Message(textService.Localize("content", "parentNotPublished", new[] { parent.Name }), culture);
}
else
{
// culture not published
return UrlInfo.Message(textService.Localize("content", "parentCultureNotPublished", new[] {parent.Name}), culture);
return UrlInfo.Message(textService.Localize("content", "parentNotPublished", new[] { parent.Name }),
culture);
}
// culture not published
return UrlInfo.Message(textService.Localize("content", "parentCultureNotPublished", new[] { parent.Name }),
culture);
}
private static async Task<Attempt<UrlInfo>> DetectCollisionAsync(ILogger logger, IContent content, string url, string culture, IUmbracoContext umbracoContext, IPublishedRouter publishedRouter, ILocalizedTextService textService, IVariationContextAccessor variationContextAccessor, UriUtility uriUtility)
private static async Task<Attempt<UrlInfo>> DetectCollisionAsync(ILogger logger, IContent content, string url,
string culture, IUmbracoContext umbracoContext, IPublishedRouter publishedRouter,
ILocalizedTextService textService, IVariationContextAccessor variationContextAccessor,
UriUtility uriUtility)
{
// test for collisions on the 'main' URL
var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute);
@@ -207,11 +264,13 @@ namespace Umbraco.Extensions
uri = uriUtility.UriToUmbraco(uri);
IPublishedRequestBuilder builder = await publishedRouter.CreateRequestAsync(uri);
IPublishedRequest pcr = await publishedRouter.RouteRequestAsync(builder, new RouteRequestOptions(RouteDirection.Outbound));
IPublishedRequest pcr =
await publishedRouter.RouteRequestAsync(builder, new RouteRequestOptions(RouteDirection.Outbound));
if (!pcr.HasPublishedContent())
{
var logMsg = nameof(DetectCollisionAsync) + " did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}";
var logMsg = nameof(DetectCollisionAsync) +
" did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}";
if (pcr.IgnorePublishedContentCollisions)
{
logger.LogDebug(logMsg, url, uri, culture);
@@ -243,14 +302,7 @@ namespace Umbraco.Extensions
l.Reverse();
var s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent.Id + ")";
var urlInfo = UrlInfo.Message(textService.Localize("content", "routeError", new[] { s }), culture);
return Attempt.Succeed(urlInfo);
}
// collisions with a different culture of the same content can never be routed.
if (!culture.InvariantEquals(pcr.Culture))
{
var urlInfo = UrlInfo.Message(textService.Localize("content", "routeErrorCannotRoute"), culture);
var urlInfo = UrlInfo.Message(textService.Localize("content", "routeError", new[] { s }), culture);
return Attempt.Succeed(urlInfo);
}

View File

@@ -25,6 +25,7 @@ namespace Umbraco.Cms.Core.PublishedCache
private readonly IPublishedContentTypeFactory _publishedContentTypeFactory;
private readonly ILogger<PublishedContentTypeCache> _logger;
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private bool _disposedValue;
// default ctor
public PublishedContentTypeCache(IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, IPublishedContentTypeFactory publishedContentTypeFactory, ILogger<PublishedContentTypeCache> logger)
@@ -259,8 +260,6 @@ namespace Umbraco.Cms.Core.PublishedCache
private IPublishedContentType CreatePublishedContentType(PublishedItemType itemType, string alias)
{
if (GetPublishedContentTypeByAlias != null)
return GetPublishedContentTypeByAlias(alias);
IContentTypeComposition contentType = itemType switch
{
PublishedItemType.Content => _contentTypeService.Get(alias),
@@ -276,8 +275,6 @@ namespace Umbraco.Cms.Core.PublishedCache
private IPublishedContentType CreatePublishedContentType(PublishedItemType itemType, int id)
{
if (GetPublishedContentTypeById != null)
return GetPublishedContentTypeById(id);
IContentTypeComposition contentType = itemType switch
{
PublishedItemType.Content => _contentTypeService.Get(id),
@@ -291,56 +288,6 @@ namespace Umbraco.Cms.Core.PublishedCache
return _publishedContentTypeFactory.CreateContentType(contentType);
}
// for unit tests - changing the callback must reset the cache obviously
// TODO: Why does this even exist? For testing you'd pass in a mocked service to get by id
private Func<string, IPublishedContentType> _getPublishedContentTypeByAlias;
internal Func<string, IPublishedContentType> GetPublishedContentTypeByAlias
{
get => _getPublishedContentTypeByAlias;
set
{
try
{
_lock.EnterWriteLock();
_typesByAlias.Clear();
_typesById.Clear();
_getPublishedContentTypeByAlias = value;
}
finally
{
if (_lock.IsWriteLockHeld)
_lock.ExitWriteLock();
}
}
}
// for unit tests - changing the callback must reset the cache obviously
// TODO: Why does this even exist? For testing you'd pass in a mocked service to get by id
private Func<int, IPublishedContentType> _getPublishedContentTypeById;
private bool _disposedValue;
internal Func<int, IPublishedContentType> GetPublishedContentTypeById
{
get => _getPublishedContentTypeById;
set
{
try
{
_lock.EnterWriteLock();
_typesByAlias.Clear();
_typesById.Clear();
_getPublishedContentTypeById = value;
}
finally
{
if (_lock.IsWriteLockHeld)
_lock.ExitWriteLock();
}
}
}
private static string GetAliasKey(PublishedItemType itemType, string alias)
{
string k;

View File

@@ -13,19 +13,24 @@ using Umbraco.Cms.Core.Xml;
using Umbraco.Cms.Core.Xml.XPath;
using Umbraco.Cms.Infrastructure.PublishedCache.Navigable;
using Umbraco.Extensions;
using Constants = Umbraco.Cms.Core.Constants;
namespace Umbraco.Cms.Infrastructure.PublishedCache
{
public class ContentCache : PublishedCacheBase, IPublishedContentCache, INavigableData, IDisposable
{
private readonly IDomainCache _domainCache;
private readonly IAppCache _elementsCache;
private readonly GlobalSettings _globalSettings;
private readonly ContentStore.Snapshot _snapshot;
private readonly IAppCache _snapshotCache;
private readonly IAppCache _elementsCache;
private readonly IDomainCache _domainCache;
private readonly GlobalSettings _globalSettings;
private readonly IVariationContextAccessor _variationContextAccessor;
#region IDisposable
public void Dispose() => _snapshot.Dispose();
#endregion
#region Constructor
// TODO: figure this out
@@ -33,7 +38,9 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
// it's too late for UmbracoContext which has captured previewDefault and stuff into these ctor vars
// but, no, UmbracoContext returns snapshot.Content which comes from elements SO a resync should create a new cache
public ContentCache(bool previewDefault, ContentStore.Snapshot snapshot, IAppCache snapshotCache, IAppCache elementsCache, IDomainCache domainCache, IOptions<GlobalSettings> globalSettings, IVariationContextAccessor variationContextAccessor)
public ContentCache(bool previewDefault, ContentStore.Snapshot snapshot, IAppCache snapshotCache,
IAppCache elementsCache, IDomainCache domainCache, IOptions<GlobalSettings> globalSettings,
IVariationContextAccessor variationContextAccessor)
: base(previewDefault)
{
_snapshot = snapshot;
@@ -59,18 +66,23 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
// at the moment we try our best to be backward compatible, but really,
// should get rid of hideTopLevelNode and other oddities entirely, eventually
public IPublishedContent GetByRoute(string route, bool? hideTopLevelNode = null, string culture = null)
{
return GetByRoute(PreviewDefault, route, hideTopLevelNode, culture);
}
public IPublishedContent GetByRoute(string route, bool? hideTopLevelNode = null, string culture = null) =>
GetByRoute(PreviewDefault, route, hideTopLevelNode, culture);
public IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string culture = null)
public IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null,
string culture = null)
{
if (route == null) throw new ArgumentNullException(nameof(route));
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
var cache = preview == false || PublishedSnapshotService.FullCacheWhenPreviewing ? _elementsCache : _snapshotCache;
IAppCache cache = preview == false || PublishedSnapshotService.FullCacheWhenPreviewing
? _elementsCache
: _snapshotCache;
var key = CacheKeys.ContentCacheContentByRoute(route, preview, culture);
return cache.GetCacheItem<IPublishedContent>(key, () => GetByRouteInternal(preview, route, hideTopLevelNode, culture));
return cache.GetCacheItem(key, () => GetByRouteInternal(preview, route, hideTopLevelNode, culture));
}
private IPublishedContent GetByRouteInternal(bool preview, string route, bool? hideTopLevelNode, string culture)
@@ -108,8 +120,10 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
// hideTopLevelNode = support legacy stuff, look for /*/path/to/node
// else normal, look for /path/to/node
content = hideTopLevelNode.Value
? GetAtRoot(preview).SelectMany(x => x.Children(_variationContextAccessor, culture)).FirstOrDefault(x => x.UrlSegment(_variationContextAccessor, culture) == parts[0])
: GetAtRoot(preview).FirstOrDefault(x => x.UrlSegment(_variationContextAccessor, culture) == parts[0]);
? GetAtRoot(preview).SelectMany(x => x.Children(_variationContextAccessor, culture))
.FirstOrDefault(x => x.UrlSegment(_variationContextAccessor, culture) == parts[0])
: GetAtRoot(preview)
.FirstOrDefault(x => x.UrlSegment(_variationContextAccessor, culture) == parts[0]);
content = FollowRoute(content, parts, 1, culture);
}
@@ -118,59 +132,72 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
// have to look for /foo (see note in ApplyHideTopLevelNodeFromPath).
if (content == null && hideTopLevelNode.Value && parts.Length == 1)
{
content = GetAtRoot(preview).FirstOrDefault(x => x.UrlSegment(_variationContextAccessor, culture) == parts[0]);
content = GetAtRoot(preview)
.FirstOrDefault(x => x.UrlSegment(_variationContextAccessor, culture) == parts[0]);
}
return content;
}
public string GetRouteById(int contentId, string culture = null)
{
return GetRouteById(PreviewDefault, contentId, culture);
}
public string GetRouteById(int contentId, string culture = null) =>
GetRouteById(PreviewDefault, contentId, culture);
public string GetRouteById(bool preview, int contentId, string culture = null)
{
var cache = (preview == false || PublishedSnapshotService.FullCacheWhenPreviewing) ? _elementsCache : _snapshotCache;
IAppCache cache = preview == false || PublishedSnapshotService.FullCacheWhenPreviewing
? _elementsCache
: _snapshotCache;
var key = CacheKeys.ContentCacheRouteByContent(contentId, preview, culture);
return cache.GetCacheItem<string>(key, () => GetRouteByIdInternal(preview, contentId, null, culture));
return cache.GetCacheItem(key, () => GetRouteByIdInternal(preview, contentId, null, culture));
}
private string GetRouteByIdInternal(bool preview, int contentId, bool? hideTopLevelNode, string culture)
{
var node = GetById(preview, contentId);
IPublishedContent node = GetById(preview, contentId);
if (node == null)
{
return null;
}
hideTopLevelNode = hideTopLevelNode ?? HideTopLevelNodeFromPath; // default = settings
// walk up from that node until we hit a node with a domain,
// or we reach the content root, collecting URLs in the way
var pathParts = new List<string>();
var n = node;
IPublishedContent n = node;
var urlSegment = n.UrlSegment(_variationContextAccessor, culture);
var hasDomains = _domainCache.HasAssigned(n.Id);
var hasDomains = _domainCache.GetAssignedWithCulture(culture, n.Id);
while (hasDomains == false && n != null) // n is null at root
{
// no segment indicates this is not published when this is a variant
if (urlSegment.IsNullOrWhiteSpace()) return null;
if (urlSegment.IsNullOrWhiteSpace())
{
return null;
}
pathParts.Add(urlSegment);
// move to parent node
n = n.Parent;
if (n != null)
{
urlSegment = n.UrlSegment(_variationContextAccessor, culture);
}
hasDomains = n != null && _domainCache.HasAssigned(n.Id);
hasDomains = n != null && _domainCache.GetAssignedWithCulture(culture, n.Id);
}
// at this point this will be the urlSegment of the root, no segment indicates this is not published when this is a variant
if (urlSegment.IsNullOrWhiteSpace()) return null;
if (urlSegment.IsNullOrWhiteSpace())
{
return null;
}
// no domain, respect HideTopLevelNodeFromPath for legacy purposes
if (hasDomains == false && hideTopLevelNode.Value)
{
ApplyHideTopLevelNodeFromPath(node, pathParts, preview);
}
// assemble the route
pathParts.Reverse();
@@ -182,7 +209,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
return route;
}
private IPublishedContent FollowRoute(IPublishedContent content, IReadOnlyList<string> parts, int start, string culture)
private IPublishedContent FollowRoute(IPublishedContent content, IReadOnlyList<string> parts, int start,
string culture)
{
var i = start;
while (content != null && i < parts.Count)
@@ -194,6 +222,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
return urlSegment == part;
});
}
return content;
}
@@ -209,11 +238,16 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
// that's the way it works pre-4.10 and we try to be backward compat for the time being
if (content.Parent == null)
{
var rootNode = GetByRoute(preview, "/", true);
IPublishedContent rootNode = GetByRoute(preview, "/", true);
if (rootNode == null)
{
throw new Exception("Failed to get node at /.");
}
if (rootNode.Id == content.Id) // remove only if we're the default node
{
segments.RemoveAt(segments.Count - 1);
}
}
else
{
@@ -227,13 +261,13 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
public override IPublishedContent GetById(bool preview, int contentId)
{
var node = _snapshot.Get(contentId);
ContentNode node = _snapshot.Get(contentId);
return GetNodePublishedContent(node, preview);
}
public override IPublishedContent GetById(bool preview, Guid contentId)
{
var node = _snapshot.Get(contentId);
ContentNode node = _snapshot.Get(contentId);
return GetNodePublishedContent(node, preview);
}
@@ -241,18 +275,26 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
{
var guidUdi = contentId as GuidUdi;
if (guidUdi == null)
{
throw new ArgumentException($"Udi must be of type {typeof(GuidUdi).Name}.", nameof(contentId));
}
if (guidUdi.EntityType != Constants.UdiEntityType.Document)
throw new ArgumentException($"Udi entity type must be \"{Constants.UdiEntityType.Document}\".", nameof(contentId));
{
throw new ArgumentException($"Udi entity type must be \"{Constants.UdiEntityType.Document}\".",
nameof(contentId));
}
return GetById(preview, guidUdi.Guid);
}
public override bool HasById(bool preview, int contentId)
{
var n = _snapshot.Get(contentId);
if (n == null) return false;
ContentNode n = _snapshot.Get(contentId);
if (n == null)
{
return false;
}
return preview || n.PublishedModel != null;
}
@@ -263,7 +305,9 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
{
// handle context culture for variant
if (culture == null)
{
culture = _variationContextAccessor?.VariationContext?.Culture ?? "";
}
// _snapshot.GetAtRoot() returns all ContentNode at root
// both .Draft and .Published cannot be null at the same time
@@ -272,13 +316,15 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
// GetNodePublishedContent may return null if !preview and there is no
// published model, so we need to filter these nulls out
var atRoot = _snapshot.GetAtRoot()
IEnumerable<IPublishedContent> atRoot = _snapshot.GetAtRoot()
.Select(n => GetNodePublishedContent(n, preview))
.WhereNotNull();
// if a culture is specified, we must ensure that it is avail/published
if (culture != "*")
{
atRoot = atRoot.Where(x => x.IsInvariantOrHasCulture(culture));
}
return atRoot;
}
@@ -286,7 +332,9 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
private static IPublishedContent GetNodePublishedContent(ContentNode node, bool preview)
{
if (node == null)
{
return null;
}
// both .Draft and .Published cannot be null at the same time
@@ -299,7 +347,10 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
// this is for published content when previewing
private static IPublishedContent GetPublishedContentAsDraft(IPublishedContent content /*, bool preview*/)
{
if (content == null /*|| preview == false*/) return null; //content;
if (content == null /*|| preview == false*/)
{
return null; //content;
}
// an object in the cache is either an IPublishedContentOrMedia,
// or a model inheriting from PublishedContentExtended - in which
@@ -309,12 +360,10 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
return inner.AsDraft();
}
public override bool HasContent(bool preview)
{
return preview
public override bool HasContent(bool preview) =>
preview
? _snapshot.IsEmpty == false
: _snapshot.GetAtRoot().Any(x => x.PublishedModel != null);
}
#endregion
@@ -322,21 +371,24 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
public override IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars)
{
var navigator = CreateNavigator(preview);
var iterator = navigator.Select(xpath, vars);
XPathNavigator navigator = CreateNavigator(preview);
XPathNodeIterator iterator = navigator.Select(xpath, vars);
return GetSingleByXPath(iterator);
}
public override IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars)
{
var navigator = CreateNavigator(preview);
var iterator = navigator.Select(xpath, vars);
XPathNavigator navigator = CreateNavigator(preview);
XPathNodeIterator iterator = navigator.Select(xpath, vars);
return GetSingleByXPath(iterator);
}
private static IPublishedContent GetSingleByXPath(XPathNodeIterator iterator)
{
if (iterator.MoveNext() == false) return null;
if (iterator.MoveNext() == false)
{
return null;
}
var xnav = iterator.Current as NavigableNavigator;
var xcontent = xnav?.UnderlyingObject as NavigableContent;
@@ -345,15 +397,16 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, string xpath, XPathVariable[] vars)
{
var navigator = CreateNavigator(preview);
var iterator = navigator.Select(xpath, vars);
XPathNavigator navigator = CreateNavigator(preview);
XPathNodeIterator iterator = navigator.Select(xpath, vars);
return GetByXPath(iterator);
}
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars)
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, XPathExpression xpath,
XPathVariable[] vars)
{
var navigator = CreateNavigator(preview);
var iterator = navigator.Select(xpath, vars);
XPathNavigator navigator = CreateNavigator(preview);
XPathNodeIterator iterator = navigator.Select(xpath, vars);
return GetByXPath(iterator);
}
@@ -364,7 +417,10 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
{
var xnav = iterator.Current as NavigableNavigator;
var xcontent = xnav?.UnderlyingObject as NavigableContent;
if (xcontent == null) continue;
if (xcontent == null)
{
continue;
}
yield return xcontent.InnerContent;
}
@@ -395,14 +451,5 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
public override IPublishedContentType GetContentType(Guid key) => _snapshot.GetContentType(key);
#endregion
#region IDisposable
public void Dispose()
{
_snapshot.Dispose();
}
#endregion
}
}

View File

@@ -175,13 +175,6 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
public IPublishedContent PublishedModel => GetModel(ref _publishedModel, _publishedData);
public ContentNodeKit ToKit()
=> new ContentNodeKit
{
Node = this,
ContentTypeId = ContentType.Id,
DraftData = _draftData,
PublishedData = _publishedData
};
=> new ContentNodeKit(this, ContentType.Id, _draftData, _publishedData);
}
}

View File

@@ -1,23 +1,39 @@
using Umbraco.Cms.Core.Models.PublishedContent;
using System;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
namespace Umbraco.Cms.Infrastructure.PublishedCache
{
// what's needed to actually build a content node
public struct ContentNodeKit
{
[Obsolete("This will be changed to a property in future versions")]
public ContentNode Node;
[Obsolete("This will be changed to a property in future versions")]
public int ContentTypeId;
[Obsolete("This will be changed to a property in future versions")]
public ContentData DraftData;
[Obsolete("This will be changed to a property in future versions")]
public ContentData PublishedData;
public ContentNodeKit(ContentNode node, int contentTypeId, ContentData draftData, ContentData publishedData)
{
Node = node;
ContentTypeId = contentTypeId;
DraftData = draftData;
PublishedData = publishedData;
}
public bool IsEmpty => Node == null;
public bool IsNull => ContentTypeId < 0;
public static ContentNodeKit Empty { get; } = new ContentNodeKit();
public static ContentNodeKit Null { get; } = new ContentNodeKit { ContentTypeId = -1 };
public static ContentNodeKit Null { get; } = new ContentNodeKit(null, -1, null, null);
public void Build(
IPublishedContentType contentType,
@@ -41,12 +57,9 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
}
public ContentNodeKit Clone(IPublishedModelFactory publishedModelFactory)
=> new ContentNodeKit
{
ContentTypeId = ContentTypeId,
DraftData = DraftData,
PublishedData = PublishedData,
Node = new ContentNode(Node, publishedModelFactory)
};
=> new ContentNodeKit(new ContentNode(Node, publishedModelFactory), ContentTypeId, DraftData, PublishedData);
public ContentNodeKit Clone(IPublishedModelFactory publishedModelFactory, ContentData draftData, ContentData publishedData)
=> new ContentNodeKit(new ContentNode(Node, publishedModelFactory), ContentTypeId, draftData, publishedData);
}
}

View File

@@ -104,7 +104,11 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
private class WriteLockInfo
{
#pragma warning disable IDE1006 // Naming Styles
// This is a field that is used for ref operations
public bool Taken;
#pragma warning restore IDE1006 // Naming Styles
}
// a scope contextual that represents a locked writer to the dictionary

View File

@@ -13,27 +13,25 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
_dictionaryOfPropertyDataSerializer = dictionaryOfPropertyDataSerializer;
if(_dictionaryOfPropertyDataSerializer == null)
{
_dictionaryOfPropertyDataSerializer = DefaultPropertiesSerializer;
_dictionaryOfPropertyDataSerializer = s_defaultPropertiesSerializer;
}
}
private static readonly DictionaryOfPropertyDataSerializer DefaultPropertiesSerializer = new DictionaryOfPropertyDataSerializer();
private static readonly DictionaryOfCultureVariationSerializer DefaultCultureVariationsSerializer = new DictionaryOfCultureVariationSerializer();
private static readonly DictionaryOfPropertyDataSerializer s_defaultPropertiesSerializer = new DictionaryOfPropertyDataSerializer();
private static readonly DictionaryOfCultureVariationSerializer s_defaultCultureVariationsSerializer = new DictionaryOfCultureVariationSerializer();
private readonly IDictionaryOfPropertyDataSerializer _dictionaryOfPropertyDataSerializer;
public ContentData ReadFrom(Stream stream)
{
return new ContentData
{
Published = PrimitiveSerializer.Boolean.ReadFrom(stream),
Name = PrimitiveSerializer.String.ReadFrom(stream),
UrlSegment = PrimitiveSerializer.String.ReadFrom(stream),
VersionId = PrimitiveSerializer.Int32.ReadFrom(stream),
VersionDate = PrimitiveSerializer.DateTime.ReadFrom(stream),
WriterId = PrimitiveSerializer.Int32.ReadFrom(stream),
TemplateId = PrimitiveSerializer.Int32.ReadFrom(stream),
Properties = _dictionaryOfPropertyDataSerializer.ReadFrom(stream), // TODO: We don't want to allocate empty arrays
CultureInfos = DefaultCultureVariationsSerializer.ReadFrom(stream) // TODO: We don't want to allocate empty arrays
};
var published = PrimitiveSerializer.Boolean.ReadFrom(stream);
var name = PrimitiveSerializer.String.ReadFrom(stream);
var urlSegment = PrimitiveSerializer.String.ReadFrom(stream);
var versionId = PrimitiveSerializer.Int32.ReadFrom(stream);
var versionDate = PrimitiveSerializer.DateTime.ReadFrom(stream);
var writerId = PrimitiveSerializer.Int32.ReadFrom(stream);
var templateId = PrimitiveSerializer.Int32.ReadFrom(stream);
var properties = _dictionaryOfPropertyDataSerializer.ReadFrom(stream); // TODO: We don't want to allocate empty arrays
var cultureInfos = s_defaultCultureVariationsSerializer.ReadFrom(stream); // TODO: We don't want to allocate empty arrays
return new ContentData(name, urlSegment, versionId, versionDate, writerId, templateId, published, properties, cultureInfos);
}
public void WriteTo(ContentData value, Stream stream)
@@ -49,7 +47,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
PrimitiveSerializer.Int32.WriteTo(value.TemplateId.Value, stream);
}
_dictionaryOfPropertyDataSerializer.WriteTo(value.Properties, stream);
DefaultCultureVariationsSerializer.WriteTo(value.CultureInfos, stream);
s_defaultCultureVariationsSerializer.WriteTo(value.CultureInfos, stream);
}
}
}

View File

@@ -1,4 +1,4 @@
using System.IO;
using System.IO;
using CSharpTest.Net.Serialization;
namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
@@ -10,19 +10,17 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
_contentDataSerializer = contentDataSerializer;
if(_contentDataSerializer == null)
{
_contentDataSerializer = DefaultDataSerializer;
_contentDataSerializer = s_defaultDataSerializer;
}
}
static readonly ContentDataSerializer DefaultDataSerializer = new ContentDataSerializer();
static readonly ContentDataSerializer s_defaultDataSerializer = new ContentDataSerializer();
private readonly ContentDataSerializer _contentDataSerializer;
//static readonly ListOfIntSerializer ChildContentIdsSerializer = new ListOfIntSerializer();
public ContentNodeKit ReadFrom(Stream stream)
{
var kit = new ContentNodeKit
{
Node = new ContentNode(
var contentNode = new ContentNode(
PrimitiveSerializer.Int32.ReadFrom(stream), // id
PrimitiveSerializer.Guid.ReadFrom(stream), // uid
PrimitiveSerializer.Int32.ReadFrom(stream), // level
@@ -31,15 +29,27 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
PrimitiveSerializer.Int32.ReadFrom(stream), // parent id
PrimitiveSerializer.DateTime.ReadFrom(stream), // date created
PrimitiveSerializer.Int32.ReadFrom(stream) // creator id
),
ContentTypeId = PrimitiveSerializer.Int32.ReadFrom(stream)
};
);
int contentTypeId = PrimitiveSerializer.Int32.ReadFrom(stream);
var hasDraft = PrimitiveSerializer.Boolean.ReadFrom(stream);
ContentData draftData = null;
ContentData publishedData = null;
if (hasDraft)
kit.DraftData = _contentDataSerializer.ReadFrom(stream);
{
draftData = _contentDataSerializer.ReadFrom(stream);
}
var hasPublished = PrimitiveSerializer.Boolean.ReadFrom(stream);
if (hasPublished)
kit.PublishedData = _contentDataSerializer.ReadFrom(stream);
{
publishedData = _contentDataSerializer.ReadFrom(stream);
}
var kit = new ContentNodeKit(
contentNode,
contentTypeId,
draftData,
publishedData);
return kit;
}

View File

@@ -8,19 +8,38 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
/// </summary>
public class ContentData
{
public string Name { get; set; }
public string UrlSegment { get; set; }
public int VersionId { get; set; }
public DateTime VersionDate { get; set; }
public int WriterId { get; set; }
public int? TemplateId { get; set; }
public bool Published { get; set; }
[Obsolete("Use ctor with all params, as the pros should be immutable")]
public ContentData()
{
public IDictionary<string, PropertyData[]> Properties { get; set; }
}
public ContentData(string name, string urlSegment, int versionId, DateTime versionDate, int writerId, int? templateId, bool published, IDictionary<string, PropertyData[]> properties, IReadOnlyDictionary<string, CultureVariation> cultureInfos)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
UrlSegment = urlSegment;
VersionId = versionId;
VersionDate = versionDate;
WriterId = writerId;
TemplateId = templateId;
Published = published;
Properties = properties ?? throw new ArgumentNullException(nameof(properties));
CultureInfos = cultureInfos;
}
public string Name { get; [Obsolete("Do not change this, use ctor with params and have this object immutable.")] set; }
public string UrlSegment { get; [Obsolete("Do not change this, use ctor with params and have this object immutable.")] set; }
public int VersionId { get; [Obsolete("Do not change this, use ctor with params and have this object immutable.")] set; }
public DateTime VersionDate { get; [Obsolete("Do not change this, use ctor with params and have this object immutable.")] set; }
public int WriterId { get; [Obsolete("Do not change this, use ctor with params and have this object immutable.")] set; }
public int? TemplateId { get; [Obsolete("Do not change this, use ctor with params and have this object immutable.")] set; }
public bool Published { get; [Obsolete("Do not change this, use ctor with params and have this object immutable.")] set; }
public IDictionary<string, PropertyData[]> Properties { get; [Obsolete("Do not change this, use ctor with params and have this object immutable.")] set; }
/// <summary>
/// The collection of language Id to name for the content item
/// </summary>
public IReadOnlyDictionary<string, CultureVariation> CultureInfos { get; set; }
public IReadOnlyDictionary<string, CultureVariation> CultureInfos { get; [Obsolete("Do not change this, use ctor with params and have this object immutable.")] set; }
}
}

View File

@@ -0,0 +1,15 @@
using System.Linq;
using Umbraco.Cms.Core.PublishedCache;
namespace Umbraco.Cms.Infrastructure.PublishedCache
{
public static class DomainCacheExtensions
{
public static bool GetAssignedWithCulture(this IDomainCache domainCache, string culture, int documentId, bool includeWildcards = false)
{
var assigned = domainCache.GetAssigned(documentId, includeWildcards);
return culture is null ? assigned.Any() : assigned.Any(x => x.Culture == culture);
}
}
}

View File

@@ -820,18 +820,16 @@ AND cmsContentNu.nodeId IS NULL
bool published = false;
var deserializedContent = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, published);
d = new ContentData
{
Name = dto.EditName,
Published = published,
TemplateId = dto.EditTemplateId,
VersionId = dto.VersionId,
VersionDate = dto.EditVersionDate,
WriterId = dto.EditWriterId,
Properties = deserializedContent.PropertyData, // TODO: We don't want to allocate empty arrays
CultureInfos = deserializedContent.CultureData,
UrlSegment = deserializedContent.UrlSegment
};
d = new ContentData(
dto.EditName,
deserializedContent.UrlSegment,
dto.VersionId,
dto.EditVersionDate,
dto.EditWriterId,
dto.EditTemplateId,
published,
deserializedContent.PropertyData,
deserializedContent.CultureData);
}
}
@@ -851,31 +849,23 @@ AND cmsContentNu.nodeId IS NULL
bool published = true;
var deserializedContent = serializer.Deserialize(dto, dto.PubData, dto.PubDataRaw, published);
p = new ContentData
{
Name = dto.PubName,
UrlSegment = deserializedContent.UrlSegment,
Published = published,
TemplateId = dto.PubTemplateId,
VersionId = dto.VersionId,
VersionDate = dto.PubVersionDate,
WriterId = dto.PubWriterId,
Properties = deserializedContent.PropertyData, // TODO: We don't want to allocate empty arrays
CultureInfos = deserializedContent.CultureData
};
p = new ContentData(
dto.PubName,
deserializedContent.UrlSegment,
dto.VersionId,
dto.PubVersionDate,
dto.PubWriterId,
dto.PubTemplateId,
published,
deserializedContent.PropertyData,
deserializedContent.CultureData);
}
}
var n = new ContentNode(dto.Id, dto.Key,
dto.Level, dto.Path, dto.SortOrder, dto.ParentId, dto.CreateDate, dto.CreatorId);
var s = new ContentNodeKit
{
Node = n,
ContentTypeId = dto.ContentTypeId,
DraftData = d,
PublishedData = p
};
var s = new ContentNodeKit(n, dto.ContentTypeId, d, p);
return s;
}
@@ -888,27 +878,21 @@ AND cmsContentNu.nodeId IS NULL
bool published = true;
var deserializedMedia = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, published);
var p = new ContentData
{
Name = dto.EditName,
Published = published,
TemplateId = -1,
VersionId = dto.VersionId,
VersionDate = dto.EditVersionDate,
WriterId = dto.CreatorId, // what-else?
Properties = deserializedMedia.PropertyData, // TODO: We don't want to allocate empty arrays
CultureInfos = deserializedMedia.CultureData
};
var p = new ContentData(
dto.EditName,
null,
dto.VersionId,
dto.EditVersionDate,
dto.CreatorId,
-1,
published,
deserializedMedia.PropertyData,
deserializedMedia.CultureData);
var n = new ContentNode(dto.Id, dto.Key,
dto.Level, dto.Path, dto.SortOrder, dto.ParentId, dto.CreateDate, dto.CreatorId);
var s = new ContentNodeKit
{
Node = n,
ContentTypeId = dto.ContentTypeId,
PublishedData = p
};
var s = new ContentNodeKit(n, dto.ContentTypeId, null, p);
return s;
}

View File

@@ -34,15 +34,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
IVariationContextAccessor variationContextAccessor,
IPublishedModelFactory publishedModelFactory)
{
var d = new ContentData
{
Name = member.Name,
Published = previewing,
TemplateId = -1,
VersionDate = member.UpdateDate,
WriterId = member.CreatorId, // what else?
Properties = GetPropertyValues(contentType, member)
};
var d = new ContentData(member.Name, null, 0, member.UpdateDate, member.CreatorId, -1, previewing, GetPropertyValues(contentType, member), null);
var n = new ContentNode(
member.Id,
member.Key,

View File

@@ -46,7 +46,6 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
private readonly IPublishedModelFactory _publishedModelFactory;
private readonly IDefaultCultureAccessor _defaultCultureAccessor;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IContentCacheDataSerializerFactory _contentCacheDataSerializerFactory;
private readonly ContentDataSerializer _contentDataSerializer;
private readonly NuCacheSettings _config;
@@ -93,7 +92,6 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
IPublishedModelFactory publishedModelFactory,
IHostingEnvironment hostingEnvironment,
IOptions<NuCacheSettings> config,
IContentCacheDataSerializerFactory contentCacheDataSerializerFactory,
ContentDataSerializer contentDataSerializer)
{
_options = options;
@@ -111,7 +109,6 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
_defaultCultureAccessor = defaultCultureAccessor;
_globalSettings = globalSettings.Value;
_hostingEnvironment = hostingEnvironment;
_contentCacheDataSerializerFactory = contentCacheDataSerializerFactory;
_contentDataSerializer = contentDataSerializer;
_config = config.Value;
_publishedModelFactory = publishedModelFactory;

View File

@@ -36,7 +36,6 @@ namespace Umbraco.Cms.Web.BackOffice.Security
private readonly ISystemClock _systemClock;
private readonly UmbracoRequestPaths _umbracoRequestPaths;
private readonly IBasicAuthService _basicAuthService;
private readonly IOptionsMonitor<BasicAuthSettings> _optionsSnapshot;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureBackOfficeCookieOptions"/> class.

View File

@@ -72,7 +72,7 @@ namespace Umbraco.Cms.Tests.Common.Builders
public ContentBuilder WithContentType(IContentType contentType)
{
_contentTypeBuilder = null;
_contentType = contentType;
_contentType = contentType;
return this;
}
@@ -172,6 +172,11 @@ namespace Umbraco.Cms.Tests.Common.Builders
content.SortOrder = sortOrder;
content.Trashed = trashed;
if (contentType.DefaultTemplate?.Id > 0)
{
content.TemplateId = contentType.DefaultTemplate.Id;
}
foreach (KeyValuePair<string, string> cultureName in _cultureNames)
{
content.SetCultureName(cultureName.Value, cultureName.Key);

View File

@@ -0,0 +1,221 @@
using System;
using System.Collections.Generic;
using Umbraco.Extensions;
using Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.Common.Builders.Interfaces;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Core.PropertyEditors;
using Moq;
using Umbraco.Cms.Infrastructure.Serialization;
using System.Linq;
namespace Umbraco.Cms.Tests.Common.Builders
{
public class ContentDataBuilder : BuilderBase<ContentData>, IWithNameBuilder
{
private string _name;
private DateTime? _now;
private string _segment;
private int? _versionId;
private int? _writerId;
private int? _templateId;
private bool? _published;
private Dictionary<string, PropertyData[]> _properties;
private Dictionary<string, CultureVariation> _cultureInfos;
string IWithNameBuilder.Name
{
get => _name;
set => _name = value;
}
public ContentDataBuilder WithVersionDate(DateTime now)
{
_now = now;
return this;
}
public ContentDataBuilder WithUrlSegment(string segment)
{
_segment = segment;
return this;
}
public ContentDataBuilder WithVersionId(int versionId)
{
_versionId = versionId;
return this;
}
public ContentDataBuilder WithWriterId(int writerId)
{
_writerId = writerId;
return this;
}
public ContentDataBuilder WithTemplateId(int templateId)
{
_templateId = templateId;
return this;
}
public ContentDataBuilder WithPublished(bool published)
{
_published = published;
return this;
}
public ContentDataBuilder WithProperties(Dictionary<string, PropertyData[]> properties)
{
_properties = properties;
return this;
}
public ContentDataBuilder WithCultureInfos(Dictionary<string, CultureVariation> cultureInfos)
{
_cultureInfos = cultureInfos;
return this;
}
/// <summary>
/// Build and dynamically update an existing content type
/// </summary>
/// <typeparam name="TContentType"></typeparam>
/// <param name="shortStringHelper"></param>
/// <param name="propertyDataTypes"></param>
/// <param name="contentType"></param>
/// <param name="contentTypeAlias">
/// Will configure the content type with this alias/name if supplied when it's not already set on the content type.
/// </param>
/// <param name="autoCreateCultureNames"></param>
/// <returns></returns>
public ContentData Build<TContentType>(
IShortStringHelper shortStringHelper,
Dictionary<string, IDataType> propertyDataTypes,
TContentType contentType,
string contentTypeAlias = null,
bool autoCreateCultureNames = false) where TContentType : class, IContentTypeComposition
{
if (_name.IsNullOrWhiteSpace())
{
throw new InvalidOperationException("Cannot build without a name");
}
_segment ??= _name.ToLower().ReplaceNonAlphanumericChars('-');
// create or copy the current culture infos for the content
Dictionary<string, CultureVariation> contentCultureInfos = _cultureInfos == null
? new Dictionary<string, CultureVariation>()
: new Dictionary<string, CultureVariation>(_cultureInfos);
contentType.Alias ??= contentTypeAlias;
contentType.Name ??= contentTypeAlias;
contentType.Key = contentType.Key == default ? Guid.NewGuid() : contentType.Key;
contentType.Id = contentType.Id == default ? Math.Abs(contentTypeAlias.GetHashCode()) : contentType.Id;
if (_properties == null)
{
_properties = new Dictionary<string, PropertyData[]>();
}
foreach (KeyValuePair<string, PropertyData[]> prop in _properties)
{
//var dataType = new DataType(new VoidEditor("Label", Mock.Of<IDataValueEditorFactory>()), new ConfigurationEditorJsonSerializer())
//{
// Id = 4
//};
if (!propertyDataTypes.TryGetValue(prop.Key, out IDataType dataType))
{
dataType = propertyDataTypes.First().Value;
}
var propertyType = new PropertyType(shortStringHelper, dataType, prop.Key);
// check each property for culture and set variations accordingly,
// this will also ensure that we have the correct culture name on the content
// set for each culture too.
foreach (PropertyData cultureValue in prop.Value.Where(x => !x.Culture.IsNullOrWhiteSpace()))
{
// set the property type to vary based on the values
propertyType.Variations |= ContentVariation.Culture;
// if there isn't already a culture, then add one with the default name
if (autoCreateCultureNames && !contentCultureInfos.TryGetValue(cultureValue.Culture, out CultureVariation cultureVariation))
{
cultureVariation = new CultureVariation
{
Date = DateTime.Now,
IsDraft = true,
Name = _name,
UrlSegment = _segment
};
contentCultureInfos[cultureValue.Culture] = cultureVariation;
}
}
// set variations for segments if there is any
if (prop.Value.Any(x => !x.Segment.IsNullOrWhiteSpace()))
{
propertyType.Variations |= ContentVariation.Segment;
contentType.Variations |= ContentVariation.Segment;
}
if (!contentType.PropertyTypeExists(propertyType.Alias))
{
contentType.AddPropertyType(propertyType);
}
}
if (contentCultureInfos.Count > 0)
{
contentType.Variations |= ContentVariation.Culture;
WithCultureInfos(contentCultureInfos);
}
var result = Build();
return result;
}
public override ContentData Build()
{
var now = _now ?? DateTime.Now;
var versionId = _versionId ?? 1;
var writerId = _writerId ?? -1;
var templateId = _templateId ?? 0;
var published = _published ?? true;
var properties = _properties ?? new Dictionary<string, PropertyData[]>();
var cultureInfos = _cultureInfos ?? new Dictionary<string, CultureVariation>();
var segment = _segment ?? _name.ToLower().ReplaceNonAlphanumericChars('-');
var contentData = new ContentData(
_name,
segment,
versionId,
now,
writerId,
templateId,
published,
properties,
cultureInfos);
return contentData;
}
public static ContentData CreateBasic(string name, DateTime? versionDate = null)
=> new ContentDataBuilder()
.WithName(name)
.WithVersionDate(versionDate ?? DateTime.Now)
.Build();
public static ContentData CreateVariant(string name, Dictionary<string, CultureVariation> cultureInfos, DateTime? versionDate = null, bool published = true)
=> new ContentDataBuilder()
.WithName(name)
.WithVersionDate(versionDate ?? DateTime.Now)
.WithCultureInfos(cultureInfos)
.WithPublished(published)
.Build();
}
}

View File

@@ -0,0 +1,96 @@
using System;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
namespace Umbraco.Cms.Tests.Common.Builders
{
public class ContentNodeKitBuilder : BuilderBase<ContentNodeKit>
{
private int _contentTypeId;
private ContentNode _contentNode;
private ContentData _draftData;
private ContentData _publishedData;
public ContentNodeKitBuilder WithContentNode(ContentNode contentNode)
{
_contentNode = contentNode;
return this;
}
public ContentNodeKitBuilder WithContentNode(int id, Guid uid, int level, string path, int sortOrder, int parentContentId, DateTime createDate, int creatorId)
{
_contentNode = new ContentNode(id, uid, level, path, sortOrder, parentContentId, createDate, creatorId);
return this;
}
public ContentNodeKitBuilder WithContentTypeId(int contentTypeId)
{
_contentTypeId = contentTypeId;
return this;
}
public ContentNodeKitBuilder WithDraftData(ContentData draftData)
{
_draftData = draftData;
return this;
}
public ContentNodeKitBuilder WithPublishedData(ContentData publishedData)
{
_publishedData = publishedData;
return this;
}
public override ContentNodeKit Build()
{
var data = new ContentNodeKit(_contentNode, _contentTypeId, _draftData, _publishedData);
return data;
}
/// <summary>
/// Creates a ContentNodeKit
/// </summary>
/// <param name="contentTypeId"></param>
/// <param name="id"></param>
/// <param name="path"></param>
/// <param name="sortOrder"></param>
/// <param name="level">
/// Optional. Will get calculated based on the path value if not specified.
/// </param>
/// <param name="parentContentId">
/// Optional. Will get calculated based on the path value if not specified.
/// </param>
/// <param name="creatorId"></param>
/// <param name="uid"></param>
/// <param name="createDate"></param>
/// <param name="draftData"></param>
/// <param name="publishedData"></param>
/// <returns></returns>
public static ContentNodeKit CreateWithContent(
int contentTypeId,
int id,
string path,
int? sortOrder = null,
int? level = null,
int? parentContentId = null,
int creatorId = -1,
Guid? uid = null,
DateTime? createDate = null,
ContentData draftData = null,
ContentData publishedData = null)
{
var pathParts = path.Split(',');
if (pathParts.Length >= 2)
{
parentContentId ??= int.Parse(pathParts[^2]);
}
return new ContentNodeKitBuilder()
.WithContentTypeId(contentTypeId)
.WithContentNode(id, uid ?? Guid.NewGuid(), level ?? pathParts.Length - 1, path, sortOrder ?? 0, parentContentId.Value, createDate ?? DateTime.Now, creatorId)
.WithDraftData(draftData)
.WithPublishedData(publishedData)
.Build();
}
}
}

View File

@@ -0,0 +1,7 @@
namespace Umbraco.Cms.Tests.Common.Builders.Interfaces
{
public interface IWithAllowAsRootBuilder
{
bool? AllowAsRoot { get; set; }
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using Umbraco.Extensions;
using Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
using System.Linq;
namespace Umbraco.Cms.Tests.Common.Builders
{
public class PropertyDataBuilder : BuilderBase<Dictionary<string, PropertyData[]>>
{
private readonly Dictionary<string, List<PropertyData>> _properties = new();
public PropertyDataBuilder WithPropertyData(string alias, PropertyData propertyData)
{
if (!_properties.TryGetValue(alias, out List<PropertyData> propertyDataCollection))
{
propertyDataCollection = new List<PropertyData>();
_properties[alias] = propertyDataCollection;
}
propertyDataCollection.Add(propertyData);
return this;
}
public PropertyDataBuilder WithPropertyData(string alias, object value, string culture = null, string segment = null)
=> WithPropertyData(alias, new PropertyData
{
Culture = culture ?? string.Empty,
Segment = segment ?? string.Empty,
Value = value
});
public override Dictionary<string, PropertyData[]> Build()
=> _properties.ToDictionary(x => x.Key, x => x.Value.ToArray());
}
}

View File

@@ -0,0 +1,152 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
namespace Umbraco.Cms.Tests.Common.Published
{
public static class PublishedContentXml
{
// The content XML that was used for the old PublishContentCacheTests
public static string PublishContentCacheTestsXml()
=> @"<?xml version=""1.0"" encoding=""utf-8""?><!DOCTYPE root[
<!ELEMENT Home ANY>
<!ATTLIST Home id ID #REQUIRED>
]>
<root id=""-1"">
<Home id=""1046"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1045"" sortOrder=""2"" createDate=""2012-06-12T14:13:17"" updateDate=""2012-07-20T18:50:43"" nodeName=""Home"" urlName=""home"" writerName=""admin"" creatorName=""admin"" path=""-1,1046"" isDoc=""""><content><![CDATA[]]></content>
<Home id=""1173"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1045"" sortOrder=""1"" createDate=""2012-07-20T18:06:45"" updateDate=""2012-07-20T19:07:31"" nodeName=""Sub1"" urlName=""sub1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173"" isDoc=""""><content><![CDATA[]]></content>
<Home id=""1174"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1045"" sortOrder=""1"" createDate=""2012-07-20T18:07:54"" updateDate=""2012-07-20T19:10:27"" nodeName=""Sub2"" urlName=""sub2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1174"" isDoc=""""><content><![CDATA[]]></content>
</Home>
<Home id=""1176"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1045"" sortOrder=""2"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc=""""><content><![CDATA[]]></content>
</Home>
</Home>
<Home id=""1175"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1045"" sortOrder=""2"" createDate=""2012-07-20T18:08:01"" updateDate=""2012-07-20T18:49:32"" nodeName=""Sub 2"" urlName=""sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1175"" isDoc=""""><content><![CDATA[]]></content>
</Home>
<Home id=""1177"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1045"" sortOrder=""2"" createDate=""2012-07-20T18:08:01"" updateDate=""2012-07-20T18:49:32"" nodeName=""Sub'Apostrophe"" urlName=""sub'apostrophe"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1177"" isDoc=""""><content><![CDATA[]]></content>
</Home>
</Home>
<Home id=""1172"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1045"" sortOrder=""3"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""Test"" urlName=""test"" writerName=""admin"" creatorName=""admin"" path=""-1,1172"" isDoc="""" />
</root>";
// The content XML that was used in the old BaseWebTest class
public static string BaseWebTestXml(int templateId)
=> @"<?xml version=""1.0"" encoding=""utf-8""?>
<!DOCTYPE root[
<!ELEMENT Home ANY>
<!ATTLIST Home id ID #REQUIRED>
<!ELEMENT CustomDocument ANY>
<!ATTLIST CustomDocument id ID #REQUIRED>
]>
<root id=""-1"">
<Home id=""1046"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""1"" createDate=""2012-06-12T14:13:17"" updateDate=""2012-07-20T18:50:43"" nodeName=""Home"" urlName=""home"" writerName=""admin"" creatorName=""admin"" path=""-1,1046"" isDoc="""">
<content><![CDATA[]]></content>
<umbracoUrlAlias><![CDATA[this/is/my/alias, anotheralias]]></umbracoUrlAlias>
<umbracoNaviHide>1</umbracoNaviHide>
<Home id=""1173"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""2"" createDate=""2012-07-20T18:06:45"" updateDate=""2012-07-20T19:07:31"" nodeName=""Sub1"" urlName=""sub1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173"" isDoc="""">
<content><![CDATA[<div>This is some content</div>]]></content>
<umbracoUrlAlias><![CDATA[page2/alias, 2ndpagealias]]></umbracoUrlAlias>
<Home id=""1174"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""2"" createDate=""2012-07-20T18:07:54"" updateDate=""2012-07-20T19:10:27"" nodeName=""Sub2"" urlName=""sub2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1174"" isDoc="""">
<content><![CDATA[]]></content>
<umbracoUrlAlias><![CDATA[only/one/alias]]></umbracoUrlAlias>
<creatorName><![CDATA[Custom data with same property name as the member name]]></creatorName>
</Home>
<Home id=""1176"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""3"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
<content><![CDATA[]]></content>
</Home>
<CustomDocument id=""1177"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""4"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""custom sub 1"" urlName=""custom-sub-1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1177"" isDoc="""" />
<CustomDocument id=""1178"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""4"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-16T14:23:35"" nodeName=""custom sub 2"" urlName=""custom-sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1178"" isDoc="""" />
<CustomDocument id=""1179"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""4"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-16T14:23:35"" nodeName=""custom sub 3 with accént character"" urlName=""custom-sub-3-with-accént-character"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1179"" isDoc="""" />
<CustomDocument id=""1180"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""4"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-16T14:23:35"" nodeName=""custom sub 4 with æøå"" urlName=""custom-sub-4-with-æøå"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1180"" isDoc="""" />
</Home>
<Home id=""1175"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""3"" createDate=""2012-07-20T18:08:01"" updateDate=""2012-07-20T18:49:32"" nodeName=""Sub 2"" urlName=""sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1175"" isDoc=""""><content><![CDATA[]]></content>
</Home>
</Home>
<CustomDocument id=""1172"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""2"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""Test"" urlName=""test-page"" writerName=""admin"" creatorName=""admin"" path=""-1,1172"" isDoc="""" />
</root>";
// The content XML that was used in the old TestWithDatabase class
public static string TestWithDatabaseXml(int templateId)
=> @"<?xml version=""1.0"" encoding=""utf-8""?>
<!DOCTYPE root[
<!ELEMENT Home ANY>
<!ATTLIST Home id ID #REQUIRED>
<!ELEMENT CustomDocument ANY>
<!ATTLIST CustomDocument id ID #REQUIRED>
]>
<root id=""-1"">
<Home id=""1046"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""1"" createDate=""2012-06-12T14:13:17"" updateDate=""2012-07-20T18:50:43"" nodeName=""Home"" urlName=""home"" writerName=""admin"" creatorName=""admin"" path=""-1,1046"" isDoc="""">
<content><![CDATA[]]></content>
<umbracoUrlAlias><![CDATA[this/is/my/alias, anotheralias]]></umbracoUrlAlias>
<umbracoNaviHide>1</umbracoNaviHide>
<Home id=""1173"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""2"" createDate=""2012-07-20T18:06:45"" updateDate=""2012-07-20T19:07:31"" nodeName=""Sub1"" urlName=""sub1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173"" isDoc="""">
<content><![CDATA[<div>This is some content</div>]]></content>
<umbracoUrlAlias><![CDATA[page2/alias, 2ndpagealias]]></umbracoUrlAlias>
<Home id=""1174"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""2"" createDate=""2012-07-20T18:07:54"" updateDate=""2012-07-20T19:10:27"" nodeName=""Sub2"" urlName=""sub2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1174"" isDoc="""">
<content><![CDATA[]]></content>
<umbracoUrlAlias><![CDATA[only/one/alias]]></umbracoUrlAlias>
<creatorName><![CDATA[Custom data with same property name as the member name]]></creatorName>
</Home>
<Home id=""1176"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""3"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
<content><![CDATA[]]></content>
</Home>
<CustomDocument id=""1177"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""4"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""custom sub 1"" urlName=""custom-sub-1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1177"" isDoc="""" />
<CustomDocument id=""1178"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""4"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-16T14:23:35"" nodeName=""custom sub 2"" urlName=""custom-sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1178"" isDoc="""" />
</Home>
<Home id=""1175"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""3"" createDate=""2012-07-20T18:08:01"" updateDate=""2012-07-20T18:49:32"" nodeName=""Sub 2"" urlName=""sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1175"" isDoc=""""><content><![CDATA[]]></content>
</Home>
</Home>
<CustomDocument id=""1172"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""2"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""Test"" urlName=""test-page"" writerName=""admin"" creatorName=""admin"" path=""-1,1172"" isDoc="""" />
</root>";
// The content XML that was used in the old PublishedContentTest class
public static string PublishedContentTestXml(int templateId, Guid node1173Guid)
=> @"<?xml version=""1.0"" encoding=""utf-8""?>
<!DOCTYPE root[
<!ELEMENT Home ANY>
<!ATTLIST Home id ID #REQUIRED>
<!ELEMENT CustomDocument ANY>
<!ATTLIST CustomDocument id ID #REQUIRED>
]>
<root id=""-1"">
<Home id=""1046"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""1"" createDate=""2012-06-12T14:13:17"" updateDate=""2012-07-20T18:50:43"" nodeName=""Home"" urlName=""home"" writerName=""admin"" creatorName=""admin"" path=""-1,1046"" isDoc="""">
<content><![CDATA[]]></content>
<umbracoUrlAlias><![CDATA[this/is/my/alias, anotheralias]]></umbracoUrlAlias>
<umbracoNaviHide>1</umbracoNaviHide>
<testRecursive><![CDATA[This is the recursive val]]></testRecursive>
<Home id=""1173"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""1"" createDate=""2012-07-20T18:06:45"" updateDate=""2012-07-20T19:07:31"" nodeName=""Sub1"" urlName=""sub1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173"" isDoc="""" key=""" + node1173Guid + @""">
<content><![CDATA[<div>This is some content</div>]]></content>
<umbracoUrlAlias><![CDATA[page2/alias, 2ndpagealias]]></umbracoUrlAlias>
<testRecursive><![CDATA[]]></testRecursive>
<Home id=""1174"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""1"" createDate=""2012-07-20T18:07:54"" updateDate=""2012-07-20T19:10:27"" nodeName=""Sub2"" urlName=""sub2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1174"" isDoc="""">
<content><![CDATA[]]></content>
<umbracoUrlAlias><![CDATA[only/one/alias]]></umbracoUrlAlias>
<creatorName><![CDATA[Custom data with same property name as the member name]]></creatorName>
<testRecursive><![CDATA[]]></testRecursive>
</Home>
<CustomDocument id=""117"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""2"" createDate=""2018-07-18T10:06:37"" updateDate=""2018-07-18T10:06:37"" nodeName=""custom sub 1"" urlName=""custom-sub-1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,117"" isDoc="""">
<umbracoNaviHide>0</umbracoNaviHide>
</CustomDocument>
<CustomDocument id=""1177"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""3"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""custom sub 1"" urlName=""custom-sub-1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1177"" isDoc="""">
<umbracoNaviHide>0</umbracoNaviHide>
</CustomDocument>
<CustomDocument id=""1178"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""4"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-16T14:23:35"" nodeName=""custom sub 2"" urlName=""custom-sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1178"" isDoc="""">
<umbracoNaviHide>0</umbracoNaviHide>
<CustomDocument id=""1179"" parentID=""1178"" level=""4"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""1"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""custom sub sub 1"" urlName=""custom-sub-sub-1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1178,1179"" isDoc="""" />
</CustomDocument>
<Home id=""1176"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""5"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""" key=""CDB83BBC-A83B-4BA6-93B8-AADEF67D3C09"">
<content><![CDATA[]]></content>
<umbracoNaviHide>1</umbracoNaviHide>
</Home>
</Home>
<Home id=""1175"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""2"" createDate=""2012-07-20T18:08:01"" updateDate=""2012-07-20T18:49:32"" nodeName=""Sub 2"" urlName=""sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1175"" isDoc=""""><content><![CDATA[]]></content>
</Home>
<CustomDocument id=""4444"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""3"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""Test"" urlName=""test-page"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,4444"" isDoc="""">
<selectedNodes><![CDATA[1172,1176,1173]]></selectedNodes>
</CustomDocument>
</Home>
<CustomDocument id=""1172"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""2"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""Test"" urlName=""test-page"" writerName=""admin"" creatorName=""admin"" path=""-1,1172"" isDoc="""" />
</root>";
}
}

View File

@@ -0,0 +1,146 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Collections.Generic;
using System.Xml.Linq;
using System.Xml.XPath;
using System.Linq;
using Umbraco.Extensions;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Tests.Common.Builders;
using System;
using Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Core.PropertyEditors;
using Moq;
using Umbraco.Cms.Infrastructure.Serialization;
namespace Umbraco.Cms.Tests.Common.Published
{
/// <summary>
/// Converts legacy Umbraco XML structures to NuCache <see cref="ContentNodeKit"/> collections
/// to populate a test implementation of <see cref="INuCacheContentService"/>
/// </summary>
/// <remarks>
/// This does not support variant data because the XML structure doesn't support variant data.
/// </remarks>
public static class PublishedContentXmlAdapter
{
/// <summary>
/// Generate a collection of <see cref="ContentNodeKit"/> based on legacy umbraco XML
/// </summary>
/// <param name="xml">The legacy umbraco XML</param>
/// <param name="shortStringHelper"></param>
/// <param name="contentTypes">Dynamically generates a list of <see cref="ContentType"/>s based on the XML data</param>
/// <param name="dataTypes">Dynamically generates a list of <see cref="DataType"/> for tests</param>
/// <returns></returns>
public static IEnumerable<ContentNodeKit> GetContentNodeKits(
string xml,
IShortStringHelper shortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes)
{
// use the label data type for all data for these tests except in the case
// where a property is named 'content', in which case use the RTE.
var serializer = new ConfigurationEditorJsonSerializer();
var labelDataType = new DataType(new VoidEditor("Label", Mock.Of<IDataValueEditorFactory>()), serializer) { Id = 3 };
var rteDataType = new DataType(new VoidEditor("RTE", Mock.Of<IDataValueEditorFactory>()), serializer) { Id = 4 };
dataTypes = new[] { labelDataType, rteDataType };
var kitsAndXml = new List<(ContentNodeKit kit, XElement node)>();
var xDoc = XDocument.Parse(xml);
IEnumerable<XElement> nodes = xDoc.XPathSelectElements("//*[@isDoc]");
foreach (XElement node in nodes)
{
var id = node.AttributeValue<int>("id");
Guid key = node.AttributeValue<Guid?>("key") ?? id.ToGuid();
var propertyElements = node.Elements().Where(x => x.Attribute("id") == null);
var properties = new Dictionary<string, PropertyData[]>();
foreach(XElement propertyElement in propertyElements)
{
properties[propertyElement.Name.LocalName] = new[]
{
// TODO: builder?
new PropertyData
{
Culture = string.Empty,
Segment = string.Empty,
Value = propertyElement.Value
}
};
}
var contentData = new ContentDataBuilder()
.WithName(node.AttributeValue<string>("nodeName"))
.WithProperties(properties)
.WithPublished(true)
.WithTemplateId(node.AttributeValue<int>("template"))
.WithUrlSegment(node.AttributeValue<string>("urlName"))
.WithVersionDate(node.AttributeValue<DateTime>("updateDate"))
.WithWriterId(node.AttributeValue<int>("writerID"))
.Build();
ContentNodeKit kit = ContentNodeKitBuilder.CreateWithContent(
node.AttributeValue<int>("nodeType"),
id,
node.AttributeValue<string>("path"),
node.AttributeValue<int>("sortOrder"),
node.AttributeValue<int>("level"),
node.AttributeValue<int>("parentID"),
node.AttributeValue<int>("creatorID"),
key,
node.AttributeValue<DateTime>("createDate"),
contentData,
contentData);
kitsAndXml.Add((kit, node));
}
// put together the unique content types
var contentTypesIdToType = new Dictionary<int, ContentType>();
foreach((ContentNodeKit kit, XElement node) in kitsAndXml)
{
if (!contentTypesIdToType.TryGetValue(kit.ContentTypeId, out ContentType contentType))
{
contentType = new ContentType(shortStringHelper, -1)
{
Id = kit.ContentTypeId,
Alias = node.Name.LocalName
};
SetContentTypeProperties(shortStringHelper, labelDataType, rteDataType, kit, contentType);
contentTypesIdToType[kit.ContentTypeId] = contentType;
}
else
{
// we've already created it but might need to add properties
SetContentTypeProperties(shortStringHelper, labelDataType, rteDataType, kit, contentType);
}
}
contentTypes = contentTypesIdToType.Values.ToArray();
return kitsAndXml.Select(x => x.kit);
}
private static void SetContentTypeProperties(IShortStringHelper shortStringHelper, DataType labelDataType, DataType rteDataType, ContentNodeKit kit, ContentType contentType)
{
foreach (KeyValuePair<string, PropertyData[]> property in kit.DraftData.Properties)
{
var propertyType = new PropertyType(shortStringHelper, labelDataType, property.Key);
if (!contentType.PropertyTypeExists(propertyType.Alias))
{
if (propertyType.Alias == "content")
{
propertyType.DataTypeId = rteDataType.Id;
}
contentType.AddPropertyType(propertyType);
}
}
}
}
}

View File

@@ -8,6 +8,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.Common.Published
{
public class PublishedSnapshotTestObjects
{
[PublishedModel("element1")]

View File

@@ -1,68 +0,0 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Collections.Generic;
using System.Linq;
using Moq;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Serialization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.Common.TestHelpers.PublishedContent
{
public class AutoPublishedContentType : PublishedContentType
{
private static readonly IPublishedPropertyType Default;
static AutoPublishedContentType()
{
var configurationEditorJsonSerializer = new ConfigurationEditorJsonSerializer();
var jsonSerializer = new JsonNetSerializer();
var dataTypeServiceMock = new Mock<IDataTypeService>();
var dataType = new DataType(
new VoidEditor(
Mock.Of<IDataValueEditorFactory>()),
configurationEditorJsonSerializer)
{
Id = 666
};
dataTypeServiceMock.Setup(x => x.GetAll()).Returns(dataType.Yield);
var factory = new PublishedContentTypeFactory(Mock.Of<IPublishedModelFactory>(), new PropertyValueConverterCollection(() => Enumerable.Empty<IPropertyValueConverter>()), dataTypeServiceMock.Object);
Default = factory.CreatePropertyType("*", 666);
}
public AutoPublishedContentType(Guid key, int id, string alias, IEnumerable<PublishedPropertyType> propertyTypes)
: base(key, id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, ContentVariation.Nothing)
{
}
public AutoPublishedContentType(Guid key, int id, string alias, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes)
: base(key, id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, ContentVariation.Nothing)
{
}
public AutoPublishedContentType(Guid key, int id, string alias, IEnumerable<string> compositionAliases, IEnumerable<PublishedPropertyType> propertyTypes)
: base(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing)
{
}
public AutoPublishedContentType(Guid key, int id, string alias, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes)
: base(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing)
{
}
public override IPublishedPropertyType GetPropertyType(string alias)
{
IPublishedPropertyType propertyType = base.GetPropertyType(alias);
return propertyType ?? Default;
}
}
}

View File

@@ -1,20 +0,0 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Moq;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.Common.TestHelpers.PublishedContent
{
[PublishedModel("ContentType2")]
public class ContentType2 : PublishedContentModel
{
public ContentType2(IPublishedContent content, IPublishedValueFallback fallback)
: base(content, fallback)
{
}
public int Prop1 => this.Value<int>(Mock.Of<IPublishedValueFallback>(), "prop1");
}
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Tests.Common.TestHelpers.PublishedContent
{
[PublishedModel("ContentType2Sub")]
public class ContentType2Sub : ContentType2
{
public ContentType2Sub(IPublishedContent content, IPublishedValueFallback fallback)
: base(content, fallback)
{
}
}
}

View File

@@ -1,84 +0,0 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Collections.Generic;
using Umbraco.Cms.Core.PublishedCache.Internal;
namespace Umbraco.Cms.Tests.Common.TestHelpers.PublishedContent
{
public class InternalPublishedPropertyWithLanguageVariants : InternalPublishedProperty
{
private readonly IDictionary<string, object> _solidSourceValues = new Dictionary<string, object>();
private readonly IDictionary<string, object> _solidValues = new Dictionary<string, object>();
private readonly IDictionary<string, object> _solidXPathValues = new Dictionary<string, object>();
public override object GetSourceValue(string culture = null, string segment = null)
{
if (string.IsNullOrEmpty(culture))
{
return base.GetSourceValue(culture, segment);
}
return _solidSourceValues.ContainsKey(culture) ? _solidSourceValues[culture] : null;
}
public override object GetValue(string culture = null, string segment = null)
{
if (string.IsNullOrEmpty(culture))
{
return base.GetValue(culture, segment);
}
return _solidValues.ContainsKey(culture) ? _solidValues[culture] : null;
}
public override object GetXPathValue(string culture = null, string segment = null)
{
if (string.IsNullOrEmpty(culture))
{
return base.GetXPathValue(culture, segment);
}
return _solidXPathValues.ContainsKey(culture) ? _solidXPathValues[culture] : null;
}
public override bool HasValue(string culture = null, string segment = null)
{
if (string.IsNullOrEmpty(culture))
{
return base.HasValue(culture, segment);
}
return _solidSourceValues.ContainsKey(culture);
}
public void SetSourceValue(string culture, object value, bool defaultValue = false)
{
_solidSourceValues.Add(culture, value);
if (defaultValue)
{
SolidSourceValue = value;
SolidHasValue = true;
}
}
public void SetValue(string culture, object value, bool defaultValue = false)
{
_solidValues.Add(culture, value);
if (defaultValue)
{
SolidValue = value;
SolidHasValue = true;
}
}
public void SetXPathValue(string culture, object value, bool defaultValue = false)
{
_solidXPathValues.Add(culture, value);
if (defaultValue)
{
SolidXPathValue = value;
}
}
}
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Moq;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.Common.TestHelpers.PublishedContent
{
public class PublishedContentStrong1 : PublishedContentModel
{
public PublishedContentStrong1(IPublishedContent content, IPublishedValueFallback fallback)
: base(content, fallback)
{
}
public int StrongValue => this.Value<int>(Mock.Of<IPublishedValueFallback>(), "strongValue");
}
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Moq;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.Common.TestHelpers.PublishedContent
{
public class PublishedContentStrong1Sub : PublishedContentStrong1
{
public PublishedContentStrong1Sub(IPublishedContent content, IPublishedValueFallback fallback)
: base(content, fallback)
{
}
public int AnotherValue => this.Value<int>(Mock.Of<IPublishedValueFallback>(), "anotherValue");
}
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Moq;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.Common.TestHelpers.PublishedContent
{
public class PublishedContentStrong2 : PublishedContentModel
{
public PublishedContentStrong2(IPublishedContent content, IPublishedValueFallback fallback)
: base(content, fallback)
{
}
public int StrongValue => this.Value<int>(Mock.Of<IPublishedValueFallback>(), "strongValue");
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Routing;
namespace Umbraco.Cms.Tests.Common
{
public class TestLastChanceFinder : IContentLastChanceFinder
{
public bool TryFindContent(IPublishedRequestBuilder frequest) => false;
}
}

View File

@@ -7,10 +7,14 @@ namespace Umbraco.Cms.Tests.Common
{
public class TestPublishedSnapshotAccessor : IPublishedSnapshotAccessor
{
private IPublishedSnapshot _snapshot = null;
public bool TryGetPublishedSnapshot(out IPublishedSnapshot publishedSnapshot)
{
publishedSnapshot = null;
return false;
publishedSnapshot = _snapshot;
return _snapshot != null;
}
public void SetCurrent(IPublishedSnapshot snapshot) => _snapshot = snapshot;
}
}

View File

@@ -52,9 +52,14 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
protected override void CustomTestSetup(IUmbracoBuilder builder)
{
InMemoryConfiguration[Constants.Configuration.ConfigNuCache + ":" + nameof(NuCacheSettings.NuCacheSerializerType)] = NuCacheSerializerType.JSON.ToString();
builder.AddNuCache();
builder.Services.AddUnique<IServerMessenger, ScopedRepositoryTests.LocalServerMessenger>();
builder.Services.PostConfigure<NuCacheSettings>(options =>
{
options.NuCacheSerializerType = NuCacheSerializerType.JSON;
});
}
private void AssertJsonStartsWith(int id, string expected)

View File

@@ -6,7 +6,13 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using NUnit.Framework;
using Umbraco.Extensions;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Tests.Common.Builders;
@@ -14,6 +20,12 @@ using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.Importing;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.PropertyEditors;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Strings;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
{
@@ -22,6 +34,14 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
public class EntityXmlSerializerTests : UmbracoIntegrationTest
{
private IEntityXmlSerializer Serializer => GetRequiredService<IEntityXmlSerializer>();
private IContentService ContentService => GetRequiredService<IContentService>();
private IMediaService MediaService => GetRequiredService<IMediaService>();
private IUserService UserService => GetRequiredService<IUserService>();
private IMediaTypeService MediaTypeService => GetRequiredService<IMediaTypeService>();
private IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
private IDataValueEditorFactory DataValueEditorFactory => GetRequiredService<IDataValueEditorFactory>();
private ILocalizedTextService TextService => GetRequiredService<ILocalizedTextService>();
private IFileService FileService => GetRequiredService<IFileService>();
[Test]
public void Can_Export_Macro()
@@ -89,6 +109,123 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
Assert.That(xml.ToString(), Is.EqualTo(languageItemsElement.ToString()));
}
[Test]
public void Can_Generate_Xml_Representation_Of_Content()
{
// Arrange
var template = TemplateBuilder.CreateTextPageTemplate();
FileService.SaveTemplate(template); // else, FK violation on contentType!
var contentType = ContentTypeBuilder.CreateTextPageContentType(
defaultTemplateId: template.Id);
ContentTypeService.Save(contentType);
var content = ContentBuilder.CreateTextpageContent(contentType, "Root Home", -1);
ContentService.Save(content, Constants.Security.SuperUserId);
var nodeName = content.ContentType.Alias.ToSafeAlias(ShortStringHelper);
var urlName = content.GetUrlSegment(ShortStringHelper, new[] { new DefaultUrlSegmentProvider(ShortStringHelper) });
// Act
XElement element = content.ToXml(Serializer);
// Assert
Assert.That(element, Is.Not.Null);
Assert.That(element.Name.LocalName, Is.EqualTo(nodeName));
Assert.AreEqual(content.Id.ToString(), (string)element.Attribute("id"));
Assert.AreEqual(content.ParentId.ToString(), (string)element.Attribute("parentID"));
Assert.AreEqual(content.Level.ToString(), (string)element.Attribute("level"));
Assert.AreEqual(content.CreatorId.ToString(), (string)element.Attribute("creatorID"));
Assert.AreEqual(content.SortOrder.ToString(), (string)element.Attribute("sortOrder"));
Assert.AreEqual(content.CreateDate.ToString("s"), (string)element.Attribute("createDate"));
Assert.AreEqual(content.UpdateDate.ToString("s"), (string)element.Attribute("updateDate"));
Assert.AreEqual(content.Name, (string)element.Attribute("nodeName"));
Assert.AreEqual(urlName, (string)element.Attribute("urlName"));
Assert.AreEqual(content.Path, (string)element.Attribute("path"));
Assert.AreEqual("", (string)element.Attribute("isDoc"));
Assert.AreEqual(content.ContentType.Id.ToString(), (string)element.Attribute("nodeType"));
Assert.AreEqual(content.GetCreatorProfile(UserService).Name, (string)element.Attribute("creatorName"));
Assert.AreEqual(content.GetWriterProfile(UserService).Name, (string)element.Attribute("writerName"));
Assert.AreEqual(content.WriterId.ToString(), (string)element.Attribute("writerID"));
Assert.AreEqual(content.TemplateId.ToString(), (string)element.Attribute("template"));
Assert.AreEqual(content.Properties["title"].GetValue().ToString(), element.Elements("title").Single().Value);
Assert.AreEqual(content.Properties["bodyText"].GetValue().ToString(), element.Elements("bodyText").Single().Value);
Assert.AreEqual(content.Properties["keywords"].GetValue().ToString(), element.Elements("keywords").Single().Value);
Assert.AreEqual(content.Properties["description"].GetValue().ToString(), element.Elements("description").Single().Value);
}
[Test]
public void Can_Generate_Xml_Representation_Of_Media()
{
// Arrange
var mediaType = MediaTypeBuilder.CreateImageMediaType("image2");
MediaTypeService.Save(mediaType);
// reference, so static ctor runs, so event handlers register
// and then, this will reset the width, height... because the file does not exist, of course ;-(
var loggerFactory = NullLoggerFactory.Instance;
var scheme = Mock.Of<IMediaPathScheme>();
var contentSettings = new ContentSettings();
var mediaFileManager = new MediaFileManager(
Mock.Of<IFileSystem>(),
scheme,
loggerFactory.CreateLogger<MediaFileManager>(),
ShortStringHelper,
Services,
Options.Create(new ContentSettings()));
var ignored = new FileUploadPropertyEditor(
DataValueEditorFactory,
mediaFileManager,
Options.Create(contentSettings),
TextService,
Services.GetRequiredService<UploadAutoFillProperties>(),
ContentService,
IOHelper);
var media = MediaBuilder.CreateMediaImage(mediaType, -1);
media.WriterId = -1; // else it's zero and that's not a user and it breaks the tests
MediaService.Save(media, Constants.Security.SuperUserId);
// so we have to force-reset these values because the property editor has cleared them
media.SetValue(Constants.Conventions.Media.Width, "200");
media.SetValue(Constants.Conventions.Media.Height, "200");
media.SetValue(Constants.Conventions.Media.Bytes, "100");
media.SetValue(Constants.Conventions.Media.Extension, "png");
var nodeName = media.ContentType.Alias.ToSafeAlias(ShortStringHelper);
var urlName = media.GetUrlSegment(ShortStringHelper, new[] { new DefaultUrlSegmentProvider(ShortStringHelper) });
// Act
XElement element = media.ToXml(Serializer);
// Assert
Assert.That(element, Is.Not.Null);
Assert.That(element.Name.LocalName, Is.EqualTo(nodeName));
Assert.AreEqual(media.Id.ToString(), (string)element.Attribute("id"));
Assert.AreEqual(media.ParentId.ToString(), (string)element.Attribute("parentID"));
Assert.AreEqual(media.Level.ToString(), (string)element.Attribute("level"));
Assert.AreEqual(media.SortOrder.ToString(), (string)element.Attribute("sortOrder"));
Assert.AreEqual(media.CreateDate.ToString("s"), (string)element.Attribute("createDate"));
Assert.AreEqual(media.UpdateDate.ToString("s"), (string)element.Attribute("updateDate"));
Assert.AreEqual(media.Name, (string)element.Attribute("nodeName"));
Assert.AreEqual(urlName, (string)element.Attribute("urlName"));
Assert.AreEqual(media.Path, (string)element.Attribute("path"));
Assert.AreEqual("", (string)element.Attribute("isDoc"));
Assert.AreEqual(media.ContentType.Id.ToString(), (string)element.Attribute("nodeType"));
Assert.AreEqual(media.GetCreatorProfile(UserService).Name, (string)element.Attribute("writerName"));
Assert.AreEqual(media.CreatorId.ToString(), (string)element.Attribute("writerID"));
Assert.IsNull(element.Attribute("template"));
Assert.AreEqual(media.Properties[Constants.Conventions.Media.File].GetValue().ToString(), element.Elements(Constants.Conventions.Media.File).Single().Value);
Assert.AreEqual(media.Properties[Constants.Conventions.Media.Width].GetValue().ToString(), element.Elements(Constants.Conventions.Media.Width).Single().Value);
Assert.AreEqual(media.Properties[Constants.Conventions.Media.Height].GetValue().ToString(), element.Elements(Constants.Conventions.Media.Height).Single().Value);
Assert.AreEqual(media.Properties[Constants.Conventions.Media.Bytes].GetValue().ToString(), element.Elements(Constants.Conventions.Media.Bytes).Single().Value);
Assert.AreEqual(media.Properties[Constants.Conventions.Media.Extension].GetValue().ToString(), element.Elements(Constants.Conventions.Media.Extension).Single().Value);
}
private void CreateDictionaryData()
{
ILocalizationService localizationService = GetRequiredService<ILocalizationService>();
@@ -104,7 +241,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
.Build();
localizationService.Save(languageEnGb);
var parentItem = new DictionaryItem("Parent") {Key = Guid.Parse("28f2e02a-8c66-4fcd-85e3-8524d551c0d3")};
var parentItem = new DictionaryItem("Parent") { Key = Guid.Parse("28f2e02a-8c66-4fcd-85e3-8524d551c0d3") };
var parentTranslations = new List<IDictionaryTranslation>
{
new DictionaryTranslation(languageNbNo, "ForelderVerdi"),
@@ -113,7 +250,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
parentItem.Translations = parentTranslations;
localizationService.Save(parentItem);
var childItem = new DictionaryItem(parentItem.Key, "Child"){Key = Guid.Parse("e7dba0a9-d517-4ba4-8e18-2764d392c611")};
var childItem = new DictionaryItem(parentItem.Key, "Child") { Key = Guid.Parse("e7dba0a9-d517-4ba4-8e18-2764d392c611") };
var childTranslations = new List<IDictionaryTranslation>
{
new DictionaryTranslation(languageNbNo, "BarnVerdi"),

View File

@@ -52,6 +52,8 @@
<DesignTime>True</DesignTime>
<DependentUpon>ImportResources.resx</DependentUpon>
</Compile>
<None Remove="Umbraco.Web.BackOffice\UrlAndDomains\package.xml" />
<EmbeddedResource Include="Umbraco.Web.BackOffice\UrlAndDomains\package.xml" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,153 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Packaging;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Tests.Common;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.UrlAndDomains
{
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Mapper = true, WithApplication = true,
Logger = UmbracoTestOptions.Logger.Console)]
public class DomainAndUrlsTests : UmbracoIntegrationTest
{
[SetUp]
public void Setup()
{
XDocument xml = PackageMigrationResource.GetEmbeddedPackageDataManifest(GetType());
IPackagingService packagingService = GetRequiredService<IPackagingService>();
InstallationSummary = packagingService.InstallCompiledPackageData(xml);
Root = InstallationSummary.ContentInstalled.First();
ContentService.SaveAndPublish(Root);
var cultures = new List<string>();
cultures.Add(GetRequiredService<ILocalizationService>().GetDefaultLanguageIsoCode());
foreach (ILanguage language in InstallationSummary.LanguagesInstalled)
{
cultures.Add(language.IsoCode);
}
Cultures = cultures.ToArray();
IHttpContextAccessor httpContextAccessor = GetRequiredService<IHttpContextAccessor>();
httpContextAccessor.HttpContext = new DefaultHttpContext
{
Request =
{
Scheme = "https",
Host = new HostString("localhost"),
Path = "/",
QueryString = new QueryString(string.Empty)
}
};
//Like the request middleware we specify the VariationContext to the default language.
_variationContextAccessor.VariationContext = new VariationContext(Cultures[0]);
GetRequiredService<IUmbracoContextFactory>().EnsureUmbracoContext();
}
private IContentService ContentService => GetRequiredService<IContentService>();
public InstallationSummary InstallationSummary { get; set; }
protected override void CustomTestSetup(IUmbracoBuilder builder)
{
builder.Services.AddUnique<IVariationContextAccessor>(_variationContextAccessor);
builder.AddNuCache();
}
private readonly TestVariationContextAccessor _variationContextAccessor = new TestVariationContextAccessor();
public IContent Root { get; set; }
public string[] Cultures { get; set; }
[Test]
public void Having_three_cultures_and_set_domain_on_all_of_them()
{
foreach (var culture in Cultures)
{
SetDomainOnContent(Root, culture, GetDomainUrlFromCultureCode(culture));
}
IEnumerable<UrlInfo> rootUrls = GetContentUrlsAsync(Root);
Assert.Multiple(() =>
{
Assert.AreEqual(6, rootUrls.Count());
foreach (var culture in Cultures)
{
var domain = GetDomainUrlFromCultureCode(culture);
Assert.IsTrue(rootUrls.Any(x => x.Text == domain));
Assert.IsTrue(rootUrls.Any(x => x.Text == "https://localhost" + domain));
}
});
}
[Test]
public void Having_three_cultures_but_set_domain_on_a_non_default_language()
{
var culture = Cultures[1];
var domain = GetDomainUrlFromCultureCode(culture);
SetDomainOnContent(Root, culture, domain);
IEnumerable<UrlInfo> rootUrls = GetContentUrlsAsync(Root);
Assert.Multiple(() =>
{
Assert.AreEqual(4, rootUrls.Count());
//We expect two for the domain that is setup
Assert.IsTrue(rootUrls.Any(x => x.IsUrl && x.Text == domain && x.Culture == culture));
Assert.IsTrue(rootUrls.Any(x => x.IsUrl && x.Text == "https://localhost" + domain && x.Culture == culture));
//We expect the default language to be routable on the default path "/"
Assert.IsTrue(rootUrls.Any(x=>x.IsUrl && x.Text == "/" && x.Culture == Cultures[0]));
//We dont expect non-default languages without a domain to be routable
Assert.IsTrue(rootUrls.Any(x=>x.IsUrl == false && x.Culture == Cultures[2]));
});
}
private static string GetDomainUrlFromCultureCode(string culture) =>
"/" + culture.Replace("-", string.Empty).ToLower() + "/";
private void SetDomainOnContent(IContent content, string cultureIsoCode, string domain)
{
IDomainService domainService = GetRequiredService<IDomainService>();
var langId = GetRequiredService<ILocalizationService>().GetLanguageIdByIsoCode(cultureIsoCode);
domainService.Save(
new UmbracoDomain(domain) { RootContentId = content.Id, LanguageId = langId });
}
private IEnumerable<UrlInfo> GetContentUrlsAsync(IContent root) =>
root.GetContentUrlsAsync(
GetRequiredService<IPublishedRouter>(),
GetRequiredService<IUmbracoContextAccessor>().GetRequiredUmbracoContext(),
GetRequiredService<ILocalizationService>(),
GetRequiredService<ILocalizedTextService>(),
ContentService,
GetRequiredService<IVariationContextAccessor>(),
GetRequiredService<ILogger<IContent>>(),
GetRequiredService<UriUtility>(),
GetRequiredService<IPublishedUrlProvider>()
).GetAwaiter().GetResult();
}
}

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<umbPackage>
<info>
<package>
<name>Test</name>
</package>
</info>
<Documents>
<DocumentSet importMode="root">
<textPageLang id="5818" key="092b28ba-f1ce-4469-9573-ed716cfe32bc" parentID="-1" level="1" creatorID="-1" sortOrder="0" createDate="2021-10-13T15:39:32" updateDate="2021-10-13T15:39:32" nodeName="svSE" urlName="svse" path="-1,5818" isDoc="" nodeName-sv-se="svSE" nodeName-da="daDK" nodeName-en-us="enUS" nodeType="4130" nodeTypeAlias="textPageLang" creatorName="Bjarke Berg" writerName="Bjarke Berg" writerID="1" template="4129" isPublished="true" />
</DocumentSet>
</Documents>
<DocumentTypes>
<DocumentType>
<Info>
<Name>TextPageLang</Name>
<Alias>textPageLang</Alias>
<Key>d4a44303-bf68-4f34-9f87-6668ff9e9cdc</Key>
<Icon>icon-document</Icon>
<Thumbnail>folder.png</Thumbnail>
<Description />
<AllowAtRoot>True</AllowAtRoot>
<IsListView>False</IsListView>
<IsElement>False</IsElement>
<Variations>Culture</Variations>
<Compositions />
<AllowedTemplates>
<Template>TextPageLang</Template>
</AllowedTemplates>
<DefaultTemplate>TextPageLang</DefaultTemplate>
</Info>
<Structure>
<DocumentType>textPage</DocumentType>
<DocumentType>textPageLang</DocumentType>
</Structure>
<GenericProperties>
<GenericProperty>
<Name>Grid</Name>
<Alias>grid</Alias>
<Key>74dcad47-03e6-4190-a7d5-480bd1d96783</Key>
<Type>Umbraco.Grid</Type>
<Definition>9ccf22c3-34c5-4e58-b365-a2d806c00540</Definition>
<Tab Alias="content">Content</Tab>
<SortOrder>0</SortOrder>
<Mandatory>False</Mandatory>
<LabelOnTop>False</LabelOnTop>
<Variations>Culture</Variations>
</GenericProperty>
</GenericProperties>
<Tabs>
<Tab>
<Id>2074</Id>
<Key>1cd3a297-a821-49e9-98d8-5c21ddd2aeaf</Key>
<Type>Group</Type>
<Caption>Content</Caption>
<Alias>content</Alias>
<SortOrder>0</SortOrder>
</Tab>
</Tabs>
</DocumentType>
</DocumentTypes>
<MediaTypes />
<Templates />
<Stylesheets />
<Scripts />
<PartialViews />
<Macros />
<MacroPartialViews />
<DictionaryItems />
<Languages>
<Language Id="1003" CultureAlias="da" FriendlyName="dansk" />
<Language Id="3" CultureAlias="sv-SE" FriendlyName="svensk (Sverige)" />
<Language Id="1" CultureAlias="en-US" FriendlyName="English (United States)" />
</Languages>
<DataTypes />
<MediaItems />
</umbPackage>

View File

@@ -0,0 +1,278 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
using Umbraco.Cms.Infrastructure.Serialization;
using Umbraco.Cms.Tests.Common;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.TestHelpers
{
[TestFixture]
public class PublishedSnapshotServiceTestBase
{
[SetUp]
public virtual void Setup()
{
VariationContextAccessor = new TestVariationContextAccessor();
PublishedSnapshotAccessor = new TestPublishedSnapshotAccessor();
}
[TearDown]
public void Teardown() => SnapshotService?.Dispose();
protected IShortStringHelper ShortStringHelper { get; } = TestHelper.ShortStringHelper;
protected virtual IPublishedModelFactory PublishedModelFactory { get; } = new NoopPublishedModelFactory();
protected IContentTypeService ContentTypeService { get; private set; }
protected IMediaTypeService MediaTypeService { get; private set; }
protected IDataTypeService DataTypeService { get; private set; }
protected IDomainService DomainService { get; private set; }
protected IPublishedValueFallback PublishedValueFallback { get; private set; }
protected IPublishedSnapshotService SnapshotService { get; private set; }
protected IVariationContextAccessor VariationContextAccessor { get; private set; }
protected TestPublishedSnapshotAccessor PublishedSnapshotAccessor { get; private set; }
protected TestNuCacheContentService NuCacheContentService { get; private set; }
protected PublishedContentTypeFactory PublishedContentTypeFactory { get; private set; }
protected GlobalSettings GlobalSettings { get; } = new();
protected virtual PropertyValueConverterCollection PropertyValueConverterCollection =>
new(() => new[] { new TestSimpleTinyMceValueConverter() });
protected IPublishedContent GetContent(int id)
{
IPublishedSnapshot snapshot = GetPublishedSnapshot();
IPublishedContent doc = snapshot.Content.GetById(id);
Assert.IsNotNull(doc);
return doc;
}
protected IPublishedContent GetMedia(int id)
{
IPublishedSnapshot snapshot = GetPublishedSnapshot();
IPublishedContent doc = snapshot.Media.GetById(id);
Assert.IsNotNull(doc);
return doc;
}
protected UrlProvider GetUrlProvider(
IUmbracoContextAccessor umbracoContextAccessor,
RequestHandlerSettings requestHandlerSettings,
WebRoutingSettings webRoutingSettings,
out UriUtility uriUtility)
{
uriUtility = new UriUtility(Mock.Of<IHostingEnvironment>());
var urlProvider = new DefaultUrlProvider(
Options.Create(requestHandlerSettings),
Mock.Of<ILogger<DefaultUrlProvider>>(),
new SiteDomainMapper(),
umbracoContextAccessor,
uriUtility,
Mock.Of<ILocalizationService>(x=>x.GetDefaultLanguageIsoCode() == GlobalSettings.DefaultUILanguage)
);
var publishedUrlProvider = new UrlProvider(
umbracoContextAccessor,
Options.Create(webRoutingSettings),
new UrlProviderCollection(() => new[] { urlProvider }),
new MediaUrlProviderCollection(() => Enumerable.Empty<IMediaUrlProvider>()),
Mock.Of<IVariationContextAccessor>());
return publishedUrlProvider;
}
protected static PublishedRouter CreatePublishedRouter(
IUmbracoContextAccessor umbracoContextAccessor,
IEnumerable<IContentFinder> contentFinders = null,
IPublishedUrlProvider publishedUrlProvider = null) => new(Options.Create(new WebRoutingSettings()),
new ContentFinderCollection(() => contentFinders ?? Enumerable.Empty<IContentFinder>()),
new TestLastChanceFinder(), new TestVariationContextAccessor(), Mock.Of<IProfilingLogger>(),
Mock.Of<ILogger<PublishedRouter>>(), publishedUrlProvider ?? Mock.Of<IPublishedUrlProvider>(),
Mock.Of<IRequestAccessor>(), Mock.Of<IPublishedValueFallback>(), Mock.Of<IFileService>(),
Mock.Of<IContentTypeService>(), umbracoContextAccessor, Mock.Of<IEventAggregator>());
protected IUmbracoContextAccessor GetUmbracoContextAccessor(string urlAsString)
{
IPublishedSnapshot snapshot = GetPublishedSnapshot();
var uri = new Uri(urlAsString.Contains(Uri.SchemeDelimiter)
? urlAsString
: $"http://example.com{urlAsString}");
IUmbracoContext umbracoContext = Mock.Of<IUmbracoContext>(
x => x.CleanedUmbracoUrl == uri
&& x.Content == snapshot.Content
&& x.PublishedSnapshot == snapshot);
var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext);
return umbracoContextAccessor;
}
/// <summary>
/// Used as a property editor for any test property that has an editor alias called "Umbraco.Void.RTE"
/// </summary>
private class TestSimpleTinyMceValueConverter : SimpleTinyMceValueConverter
{
public override bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.EditorAlias == "Umbraco.Void.RTE";
}
protected static DataType[] GetDefaultDataTypes()
{
var serializer = new ConfigurationEditorJsonSerializer();
// create data types, property types and content types
var dataType =
new DataType(new VoidEditor("Editor", Mock.Of<IDataValueEditorFactory>()), serializer) { Id = 3 };
return new[] { dataType };
}
protected virtual ServiceContext CreateServiceContext(IContentType[] contentTypes, IMediaType[] mediaTypes,
IDataType[] dataTypes)
{
var contentTypeService = new Mock<IContentTypeService>();
contentTypeService.Setup(x => x.GetAll()).Returns(contentTypes);
contentTypeService.Setup(x => x.GetAll(It.IsAny<int[]>())).Returns(contentTypes);
contentTypeService.Setup(x => x.Get(It.IsAny<string>()))
.Returns((string alias) => contentTypes.FirstOrDefault(x => x.Alias.InvariantEquals(alias)));
var mediaTypeService = new Mock<IMediaTypeService>();
mediaTypeService.Setup(x => x.GetAll()).Returns(mediaTypes);
mediaTypeService.Setup(x => x.GetAll(It.IsAny<int[]>())).Returns(mediaTypes);
mediaTypeService.Setup(x => x.Get(It.IsAny<string>()))
.Returns((string alias) => mediaTypes.FirstOrDefault(x => x.Alias.InvariantEquals(alias)));
var contentTypeServiceBaseFactory = new Mock<IContentTypeBaseServiceProvider>();
contentTypeServiceBaseFactory.Setup(x => x.For(It.IsAny<IContentBase>()))
.Returns(contentTypeService.Object);
var dataTypeServiceMock = new Mock<IDataTypeService>();
dataTypeServiceMock.Setup(x => x.GetAll()).Returns(dataTypes);
return ServiceContext.CreatePartial(
dataTypeService: dataTypeServiceMock.Object,
memberTypeService: Mock.Of<IMemberTypeService>(),
memberService: Mock.Of<IMemberService>(),
contentTypeService: contentTypeService.Object,
mediaTypeService: mediaTypeService.Object,
localizationService: Mock.Of<ILocalizationService>(),
domainService: Mock.Of<IDomainService>(),
fileService: Mock.Of<IFileService>()
);
}
/// <summary>
/// Creates a published snapshot and set the accessor to resolve the created one
/// </summary>
/// <returns></returns>
protected IPublishedSnapshot GetPublishedSnapshot()
{
IPublishedSnapshot snapshot = SnapshotService.CreatePublishedSnapshot(null);
PublishedSnapshotAccessor.SetCurrent(snapshot);
return snapshot;
}
/// <summary>
/// Initializes the <see cref="IPublishedSnapshotService'" /> with a source of data
/// </summary>
/// <param name="contentNodeKits"></param>
/// <param name="contentTypes"></param>
protected void InitializedCache(
IEnumerable<ContentNodeKit> contentNodeKits,
IContentType[] contentTypes,
IDataType[] dataTypes = null,
IEnumerable<ContentNodeKit> mediaNodeKits = null,
IMediaType[] mediaTypes = null)
{
// create a data source for NuCache
NuCacheContentService = new TestNuCacheContentService(contentNodeKits, mediaNodeKits);
IRuntimeState runtime = Mock.Of<IRuntimeState>();
Mock.Get(runtime).Setup(x => x.Level).Returns(RuntimeLevel.Run);
// create a service context
ServiceContext serviceContext = CreateServiceContext(
contentTypes ?? Array.Empty<IContentType>(),
mediaTypes ?? Array.Empty<IMediaType>(),
dataTypes ?? GetDefaultDataTypes());
DataTypeService = serviceContext.DataTypeService;
ContentTypeService = serviceContext.ContentTypeService;
MediaTypeService = serviceContext.MediaTypeService;
DomainService = serviceContext.DomainService;
// create a scope provider
IScopeProvider scopeProvider = Mock.Of<IScopeProvider>();
Mock.Get(scopeProvider)
.Setup(x => x.CreateScope(
It.IsAny<IsolationLevel>(),
It.IsAny<RepositoryCacheMode>(),
It.IsAny<IEventDispatcher>(),
It.IsAny<IScopedNotificationPublisher>(),
It.IsAny<bool?>(),
It.IsAny<bool>(),
It.IsAny<bool>()))
.Returns(Mock.Of<IScope>);
// create a published content type factory
PublishedContentTypeFactory = new PublishedContentTypeFactory(
PublishedModelFactory,
PropertyValueConverterCollection,
DataTypeService);
ITypeFinder typeFinder = TestHelper.GetTypeFinder();
var nuCacheSettings = new NuCacheSettings();
// at last, create the complete NuCache snapshot service!
var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true };
SnapshotService = new PublishedSnapshotService(
options,
Mock.Of<ISyncBootStateAccessor>(x => x.GetSyncBootState() == SyncBootState.WarmBoot),
new SimpleMainDom(),
serviceContext,
PublishedContentTypeFactory,
PublishedSnapshotAccessor,
VariationContextAccessor,
Mock.Of<IProfilingLogger>(),
NullLoggerFactory.Instance,
scopeProvider,
NuCacheContentService,
new TestDefaultCultureAccessor(),
Options.Create(GlobalSettings),
PublishedModelFactory,
TestHelper.GetHostingEnvironment(),
Options.Create(nuCacheSettings),
//ContentNestedDataSerializerFactory,
new ContentDataSerializer(new DictionaryOfPropertyDataSerializer()));
// invariant is the current default
VariationContextAccessor.VariationContext = new VariationContext();
PublishedValueFallback = new PublishedValueFallback(serviceContext, VariationContextAccessor);
}
}
}

View File

@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Infrastructure.PublishedCache.Persistence;
namespace Umbraco.Cms.Tests.UnitTests.TestHelpers
{
public class TestNuCacheContentService : INuCacheContentService
{
private IPublishedModelFactory PublishedModelFactory { get; } = new NoopPublishedModelFactory();
public TestNuCacheContentService(params ContentNodeKit[] kits)
: this((IEnumerable<ContentNodeKit>)kits)
{ }
public TestNuCacheContentService(IEnumerable<ContentNodeKit> contentKits, IEnumerable<ContentNodeKit> mediaKits = null)
{
ContentKits = contentKits?.ToDictionary(x => x.Node.Id, x => x) ?? new Dictionary<int, ContentNodeKit>();
MediaKits = mediaKits?.ToDictionary(x => x.Node.Id, x => x) ?? new Dictionary<int, ContentNodeKit>();
}
public Dictionary<int, ContentNodeKit> ContentKits { get; }
public Dictionary<int, ContentNodeKit> MediaKits { get; }
// note: it is important to clone the returned kits, as the inner
// ContentNode is directly reused and modified by the snapshot service
public ContentNodeKit GetContentSource(int id)
=> ContentKits.TryGetValue(id, out ContentNodeKit kit) ? kit.Clone(PublishedModelFactory) : default;
public IEnumerable<ContentNodeKit> GetAllContentSources()
=> ContentKits.Values
.OrderBy(x => x.Node.Level)
.ThenBy(x => x.Node.ParentContentId)
.ThenBy(x => x.Node.SortOrder)
.Select(x => x.Clone(PublishedModelFactory));
public IEnumerable<ContentNodeKit> GetBranchContentSources(int id)
=> ContentKits.Values
.Where(x => x.Node.Path.EndsWith("," + id) || x.Node.Path.Contains("," + id + ","))
.OrderBy(x => x.Node.Level)
.ThenBy(x => x.Node.ParentContentId)
.ThenBy(x => x.Node.SortOrder)
.Select(x => x.Clone(PublishedModelFactory));
public IEnumerable<ContentNodeKit> GetTypeContentSources(IEnumerable<int> ids)
=> ContentKits.Values
.Where(x => ids.Contains(x.ContentTypeId))
.OrderBy(x => x.Node.Level)
.ThenBy(x => x.Node.ParentContentId)
.ThenBy(x => x.Node.SortOrder)
.Select(x => x.Clone(PublishedModelFactory));
public ContentNodeKit GetMediaSource(int id)
=> MediaKits.TryGetValue(id, out ContentNodeKit kit) ? kit.Clone(PublishedModelFactory) : default;
public IEnumerable<ContentNodeKit> GetAllMediaSources()
=> MediaKits.Values
.OrderBy(x => x.Node.Level)
.ThenBy(x => x.Node.ParentContentId)
.ThenBy(x => x.Node.SortOrder)
.Select(x => x.Clone(PublishedModelFactory));
public IEnumerable<ContentNodeKit> GetBranchMediaSources(int id)
=> MediaKits.Values
.Where(x => x.Node.Path.EndsWith("," + id) || x.Node.Path.Contains("," + id + ","))
.OrderBy(x => x.Node.Level)
.ThenBy(x => x.Node.ParentContentId)
.ThenBy(x => x.Node.SortOrder)
.Select(x => x.Clone(PublishedModelFactory));
public IEnumerable<ContentNodeKit> GetTypeMediaSources(IEnumerable<int> ids)
=> MediaKits.Values
.Where(x => ids.Contains(x.ContentTypeId))
.OrderBy(x => x.Node.Level)
.ThenBy(x => x.Node.ParentContentId)
.ThenBy(x => x.Node.SortOrder)
.Select(x => x.Clone(PublishedModelFactory));
public void DeleteContentItem(IContentBase item) => throw new NotImplementedException();
public void DeleteContentItems(IEnumerable<IContentBase> items) => throw new NotImplementedException();
public void RefreshContent(IContent content) => throw new NotImplementedException();
public void RebuildDatabaseCacheIfSerializerChanged() => throw new NotImplementedException();
public void RefreshMedia(IMedia media) => throw new NotImplementedException();
public void RefreshMember(IMember member) => throw new NotImplementedException();
public void Rebuild(IReadOnlyCollection<int> contentTypeIds = null, IReadOnlyCollection<int> mediaTypeIds = null, IReadOnlyCollection<int> memberTypeIds = null) => throw new NotImplementedException();
public bool VerifyContentDbCache() => throw new NotImplementedException();
public bool VerifyMediaDbCache() => throw new NotImplementedException();
public bool VerifyMemberDbCache() => throw new NotImplementedException();
}
}

View File

@@ -1,7 +1,7 @@
using NUnit.Framework;
using NUnit.Framework;
using Umbraco.Core.Collections;
namespace Umbraco.Tests.Collections
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Collections
{
[TestFixture]
public class StackQueueTests
@@ -16,7 +16,7 @@ namespace Umbraco.Tests.Collections
}
var expected = 0;
while(sq.Count > 0)
while (sq.Count > 0)
{
var next = sq.Dequeue();
Assert.AreEqual(expected, next);

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Tests.Common;
using Umbraco.Cms.Tests.Common.Published;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
using Umbraco.Extensions;
using Constants = Umbraco.Cms.Core.Constants;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing
{
// TODO: We should be able to decouple this from the base db tests since we're just mocking the services now
[TestFixture]
public class ContentFinderByAliasTests : UrlRoutingTestBase
{
[TestCase("/this/is/my/alias", 1001)]
[TestCase("/anotheralias", 1001)]
[TestCase("/page2/alias", 10011)]
[TestCase("/2ndpagealias", 10011)]
[TestCase("/only/one/alias", 100111)]
[TestCase("/ONLY/one/Alias", 100111)]
[TestCase("/alias43", 100121)]
public async Task Lookup_By_Url_Alias(string urlAsString, int nodeMatch)
{
var umbracoContextAccessor = GetUmbracoContextAccessor(urlAsString);
var publishedRouter = CreatePublishedRouter(umbracoContextAccessor);
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
var lookup =
new ContentFinderByUrlAlias(Mock.Of<ILogger<ContentFinderByUrlAlias>>(), Mock.Of<IPublishedValueFallback>(), VariationContextAccessor, umbracoContextAccessor);
var result = lookup.TryFindContent(frequest);
Assert.IsTrue(result);
Assert.AreEqual(frequest.PublishedContent.Id, nodeMatch);
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -8,40 +9,18 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Tests.Common.Published;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
using Umbraco.Extensions;
using Constants = Umbraco.Cms.Core.Constants;
namespace Umbraco.Tests.Routing
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing
{
[TestFixture]
public class ContentFinderByAliasWithDomainsTests : ContentFinderByAliasTests
public class ContentFinderByAliasWithDomainsTests : UrlRoutingTestBase
{
private PublishedContentType _publishedContentType;
protected override void Initialize()
{
base.Initialize();
var properties = new[]
{
new PublishedPropertyType(
propertyTypeAlias:"umbracoUrlAlias",
dataTypeId: Constants.DataTypes.Textbox,
isUserProperty:false,
variations: ContentVariation.Nothing,
propertyValueConverters:new PropertyValueConverterCollection(Enumerable.Empty<IPropertyValueConverter>()),
contentType:Mock.Of<IPublishedContentType>(),
publishedModelFactory:Mock.Of<IPublishedModelFactory>(),
factory:Mock.Of<IPublishedContentTypeFactory>()
)
};
_publishedContentType = new PublishedContentType(Guid.NewGuid(), 0, "Doc", PublishedItemType.Content, Enumerable.Empty<string>(), properties, ContentVariation.Nothing);
}
protected override PublishedContentType GetPublishedContentTypeByAlias(string alias)
{
if (alias == "Doc") return _publishedContentType;
return null;
}
[TestCase("http://domain1.com/this/is/my/alias", "de-DE", -1001)] // alias to domain's page fails - no alias on domain's home
[TestCase("http://domain1.com/page2/alias", "de-DE", 10011)] // alias to sub-page works
@@ -56,10 +35,11 @@ namespace Umbraco.Tests.Routing
public async Task Lookup_By_Url_Alias_And_Domain(string inputUrl, string expectedCulture, int expectedNode)
{
//SetDomains1();
var umbracoContextAccessor = GetUmbracoContextAccessor(inputUrl);
var publishedRouter = CreatePublishedRouter(umbracoContextAccessor);
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
var umbracoContext = GetUmbracoContext(inputUrl);
var publishedRouter = CreatePublishedRouter(GetUmbracoContextAccessor(umbracoContext));
var request = await publishedRouter .CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
var request = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
// must lookup domain
publishedRouter.FindDomain(request);
@@ -68,7 +48,7 @@ namespace Umbraco.Tests.Routing
Assert.AreEqual(expectedCulture, request.Culture);
}
var finder = new ContentFinderByUrlAlias(LoggerFactory.CreateLogger<ContentFinderByUrlAlias>(), Mock.Of<IPublishedValueFallback>(), VariationContextAccessor, GetUmbracoContextAccessor(umbracoContext));
var finder = new ContentFinderByUrlAlias(Mock.Of<ILogger<ContentFinderByUrlAlias>>(), Mock.Of<IPublishedValueFallback>(), VariationContextAccessor, umbracoContextAccessor);
var result = finder.TryFindContent(request);
if (expectedNode > 0)

View File

@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Tests.Common.Published;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing
{
[TestFixture]
public class ContentFinderByIdTests : PublishedSnapshotServiceTestBase
{
[SetUp]
public override void Setup()
{
base.Setup();
string xml = PublishedContentXml.BaseWebTestXml(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
}
[TestCase("/1046", 1046)]
public async Task Lookup_By_Id(string urlAsString, int nodeMatch)
{
var umbracoContextAccessor = GetUmbracoContextAccessor(urlAsString);
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
var publishedRouter = CreatePublishedRouter(umbracoContextAccessor);
var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
var webRoutingSettings = new WebRoutingSettings();
var lookup = new ContentFinderByIdPath(Options.Create(webRoutingSettings), Mock.Of<ILogger<ContentFinderByIdPath>>(), Mock.Of<IRequestAccessor>(), umbracoContextAccessor);
var result = lookup.TryFindContent(frequest);
Assert.IsTrue(result);
Assert.AreEqual(frequest.PublishedContent.Id, nodeMatch);
}
}
}

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Tests.Common.Published;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing
{
[TestFixture]
public class ContentFinderByPageIdQueryTests : PublishedSnapshotServiceTestBase
{
[SetUp]
public override void Setup()
{
base.Setup();
string xml = PublishedContentXml.BaseWebTestXml(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
}
[TestCase("/?umbPageId=1046", 1046)]
[TestCase("/?UMBPAGEID=1046", 1046)]
[TestCase("/default.aspx?umbPageId=1046", 1046)] // TODO: Should this match??
[TestCase("/some/other/page?umbPageId=1046", 1046)] // TODO: Should this match??
[TestCase("/some/other/page.aspx?umbPageId=1046", 1046)] // TODO: Should this match??
public async Task Lookup_By_Page_Id(string urlAsString, int nodeMatch)
{
var umbracoContextAccessor = GetUmbracoContextAccessor(urlAsString);
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
var publishedRouter = CreatePublishedRouter(umbracoContextAccessor);
var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
var queryStrings = HttpUtility.ParseQueryString(umbracoContext.CleanedUmbracoUrl.Query);
var mockRequestAccessor = new Mock<IRequestAccessor>();
mockRequestAccessor.Setup(x => x.GetRequestValue("umbPageID"))
.Returns(queryStrings["umbPageID"]);
var lookup = new ContentFinderByPageIdQuery(mockRequestAccessor.Object, umbracoContextAccessor);
var result = lookup.TryFindContent(frequest);
Assert.IsTrue(result);
Assert.AreEqual(frequest.PublishedContent.Id, nodeMatch);
}
}
}

View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Tests.Common.Published;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing
{
[TestFixture]
public class ContentFinderByUrlAndTemplateTests : PublishedSnapshotServiceTestBase
{
private IFileService _fileService;
[SetUp]
public override void Setup()
{
base.Setup();
string xml = PublishedContentXml.BaseWebTestXml(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
}
protected override ServiceContext CreateServiceContext(IContentType[] contentTypes, IMediaType[] mediaTypes, IDataType[] dataTypes)
{
var serviceContext = base.CreateServiceContext(contentTypes, mediaTypes, dataTypes);
var fileService = Mock.Get(serviceContext.FileService);
fileService.Setup(x => x.GetTemplate(It.IsAny<string>()))
.Returns((string alias) => new Template(ShortStringHelper, alias, alias));
_fileService = fileService.Object;
return serviceContext;
}
[TestCase("/blah")]
[TestCase("/home/Sub1/blah")]
[TestCase("/Home/Sub1/Blah")] //different cases
public async Task Match_Document_By_Url_With_Template(string urlAsString)
{
GlobalSettings.HideTopLevelNodeFromPath = false;
var umbracoContextAccessor = GetUmbracoContextAccessor(urlAsString);
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
var publishedRouter = CreatePublishedRouter(umbracoContextAccessor);
var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
var webRoutingSettings = new WebRoutingSettings();
var lookup = new ContentFinderByUrlAndTemplate(
Mock.Of<ILogger<ContentFinderByUrlAndTemplate>>(),
_fileService,
ContentTypeService,
umbracoContextAccessor,
Microsoft.Extensions.Options.Options.Create(webRoutingSettings));
var result = lookup.TryFindContent(frequest);
IPublishedRequest request = frequest.Build();
Assert.IsTrue(result);
Assert.IsNotNull(frequest.PublishedContent);
var templateAlias = request.GetTemplateAlias();
Assert.IsNotNull(templateAlias);
Assert.AreEqual("blah".ToUpperInvariant(), templateAlias.ToUpperInvariant());
}
}
}

View File

@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Tests.Common.Published;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing
{
[TestFixture]
public class ContentFinderByUrlTests : PublishedSnapshotServiceTestBase
{
private async Task<(ContentFinderByUrl finder, IPublishedRequestBuilder frequest)> GetContentFinder(string urlString)
{
string xml = PublishedContentXml.BaseWebTestXml(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
var umbracoContextAccessor = GetUmbracoContextAccessor(urlString);
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
var publishedRouter = CreatePublishedRouter(umbracoContextAccessor);
var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
var lookup = new ContentFinderByUrl(Mock.Of<ILogger<ContentFinderByUrl>>(), umbracoContextAccessor);
return (lookup, frequest);
}
[TestCase("/", 1046)]
[TestCase("/Sub1", 1173)]
[TestCase("/sub1", 1173)]
[TestCase("/home/sub1", -1)] // should fail
// these two are special. getNiceUrl(1046) returns "/" but getNiceUrl(1172) cannot also return "/" so
// we've made it return "/test-page" => we have to support that URL back in the lookup...
[TestCase("/home", 1046)]
[TestCase("/test-page", 1172)]
public async Task Match_Document_By_Url_Hide_Top_Level(string urlString, int expectedId)
{
GlobalSettings.HideTopLevelNodeFromPath = true;
var lookup = await GetContentFinder(urlString);
Assert.IsTrue(GlobalSettings.HideTopLevelNodeFromPath);
// FIXME: debugging - going further down, the routes cache is NOT empty?!
if (urlString == "/home/sub1")
System.Diagnostics.Debugger.Break();
var result = lookup.finder.TryFindContent(lookup.frequest);
if (expectedId > 0)
{
Assert.IsTrue(result);
Assert.AreEqual(expectedId, lookup.frequest.PublishedContent.Id);
}
else
{
Assert.IsFalse(result);
}
}
[TestCase("/", 1046)]
[TestCase("/home", 1046)]
[TestCase("/home/Sub1", 1173)]
[TestCase("/Home/Sub1", 1173)] //different cases
public async Task Match_Document_By_Url(string urlString, int expectedId)
{
GlobalSettings.HideTopLevelNodeFromPath = false;
var lookup = await GetContentFinder(urlString);
Assert.IsFalse(GlobalSettings.HideTopLevelNodeFromPath);
var result = lookup.finder.TryFindContent(lookup.frequest);
Assert.IsTrue(result);
Assert.AreEqual(expectedId, lookup.frequest.PublishedContent.Id);
}
/// <summary>
/// This test handles requests with special characters in the URL.
/// </summary>
/// <param name="urlString"></param>
/// <param name="expectedId"></param>
[TestCase("/", 1046)]
[TestCase("/home/sub1/custom-sub-3-with-accént-character", 1179)]
[TestCase("/home/sub1/custom-sub-4-with-æøå", 1180)]
public async Task Match_Document_By_Url_With_Special_Characters(string urlString, int expectedId)
{
GlobalSettings.HideTopLevelNodeFromPath = false;
var lookup = await GetContentFinder(urlString);
var result = lookup.finder.TryFindContent(lookup.frequest);
Assert.IsTrue(result);
Assert.AreEqual(expectedId, lookup.frequest.PublishedContent.Id);
}
/// <summary>
/// This test handles requests with a hostname associated.
/// The logic for handling this goes through the DomainHelper and is a bit different
/// from what happens in a normal request - so it has a separate test with a mocked
/// hostname added.
/// </summary>
/// <param name="urlString"></param>
/// <param name="expectedId"></param>
[TestCase("/", 1046)]
[TestCase("/home/sub1/custom-sub-3-with-accént-character", 1179)]
[TestCase("/home/sub1/custom-sub-4-with-æøå", 1180)]
public async Task Match_Document_By_Url_With_Special_Characters_Using_Hostname(string urlString, int expectedId)
{
GlobalSettings.HideTopLevelNodeFromPath = false;
var lookup = await GetContentFinder(urlString);
lookup.frequest.SetDomain(new DomainAndUri(new Domain(1, "mysite", -1, "en-US", false), new Uri("http://mysite/")));
var result = lookup.finder.TryFindContent(lookup.frequest);
Assert.IsTrue(result);
Assert.AreEqual(expectedId, lookup.frequest.PublishedContent.Id);
}
/// <summary>
/// This test handles requests with a hostname with special characters associated.
/// The logic for handling this goes through the DomainHelper and is a bit different
/// from what happens in a normal request - so it has a separate test with a mocked
/// hostname added.
/// </summary>
/// <param name="urlString"></param>
/// <param name="expectedId"></param>
[TestCase("/æøå/", 1046)]
[TestCase("/æøå/home/sub1", 1173)]
[TestCase("/æøå/home/sub1/custom-sub-3-with-accént-character", 1179)]
[TestCase("/æøå/home/sub1/custom-sub-4-with-æøå", 1180)]
public async Task Match_Document_By_Url_With_Special_Characters_In_Hostname(string urlString, int expectedId)
{
GlobalSettings.HideTopLevelNodeFromPath = false;
var lookup = await GetContentFinder(urlString);
lookup.frequest.SetDomain(new DomainAndUri(new Domain(1, "mysite/æøå", -1, "en-US", false), new Uri("http://mysite/æøå")));
var result = lookup.finder.TryFindContent(lookup.frequest);
Assert.IsTrue(result);
Assert.AreEqual(expectedId, lookup.frequest.PublishedContent.Id);
}
}
}

View File

@@ -1,42 +1,47 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Routing;
using Umbraco.Extensions;
namespace Umbraco.Tests.Routing
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing
{
[TestFixture]
public class ContentFinderByUrlWithDomainsTests : UrlRoutingTestBase
{
void SetDomains3()
private void SetDomains3()
{
SetupDomainServiceMock(new[]
{
new UmbracoDomain("domain1.com/") {Id = 1, LanguageId = LangDeId, RootContentId = 1001, LanguageIsoCode = "de-DE"}
});
var domainService = Mock.Get(DomainService);
domainService.Setup(service => service.GetAll(It.IsAny<bool>()))
.Returns((bool incWildcards) => new[]
{
new UmbracoDomain("domain1.com/") {Id = 1, LanguageId = LangDeId, RootContentId = 1001, LanguageIsoCode = "de-DE"}
});
}
void SetDomains4()
private void SetDomains4()
{
SetupDomainServiceMock(new[]
{
new UmbracoDomain("domain1.com/") {Id = 1, LanguageId = LangEngId, RootContentId = 1001, LanguageIsoCode = "en-US"},
new UmbracoDomain("domain1.com/en") {Id = 1, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US"},
new UmbracoDomain("domain1.com/fr") {Id = 1, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR"},
new UmbracoDomain("http://domain3.com/") {Id = 1, LanguageId = LangEngId, RootContentId = 1003, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain3.com/en") {Id = 1, LanguageId = LangEngId, RootContentId = 10031, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain3.com/fr") {Id = 1, LanguageId = LangFrId, RootContentId = 10032, LanguageIsoCode = "fr-FR"}
});
var domainService = Mock.Get(DomainService);
domainService.Setup(service => service.GetAll(It.IsAny<bool>()))
.Returns((bool incWildcards) => new[]
{
new UmbracoDomain("domain1.com/") {Id = 1, LanguageId = LangEngId, RootContentId = 1001, LanguageIsoCode = "en-US"},
new UmbracoDomain("domain1.com/en") {Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US"},
new UmbracoDomain("domain1.com/fr") {Id = 3, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR"},
new UmbracoDomain("http://domain3.com/") {Id = 4, LanguageId = LangEngId, RootContentId = 1003, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain3.com/en") {Id = 5, LanguageId = LangEngId, RootContentId = 10031, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain3.com/fr") {Id = 6, LanguageId = LangFrId, RootContentId = 10032, LanguageIsoCode = "fr-FR"}
});
}
protected override string GetXmlContent(int templateId)
{
return @"<?xml version=""1.0"" encoding=""utf-8""?>
=> @"<?xml version=""1.0"" encoding=""utf-8""?>
<!DOCTYPE root[
<!ELEMENT Doc ANY>
<!ATTLIST Doc id ID #REQUIRED>
@@ -115,7 +120,6 @@ namespace Umbraco.Tests.Routing
</Doc>
</Doc>
</root>";
}
[TestCase("http://domain1.com/", 1001)]
[TestCase("http://domain1.com/1001-1", 10011)]
@@ -125,16 +129,17 @@ namespace Umbraco.Tests.Routing
{
SetDomains3();
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = true };
GlobalSettings.HideTopLevelNodeFromPath = true;
var umbracoContext = GetUmbracoContext(url, globalSettings:globalSettings);
var publishedRouter = CreatePublishedRouter(GetUmbracoContextAccessor(umbracoContext), Factory);
var umbracoContextAccessor = GetUmbracoContextAccessor(url);
var publishedRouter = CreatePublishedRouter(umbracoContextAccessor);
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
// must lookup domain else lookup by URL fails
publishedRouter.FindDomain(frequest);
var lookup = new ContentFinderByUrl(LoggerFactory.CreateLogger<ContentFinderByUrl>(), GetUmbracoContextAccessor(umbracoContext));
var lookup = new ContentFinderByUrl(Mock.Of<ILogger<ContentFinderByUrl>>(), umbracoContextAccessor);
var result = lookup.TryFindContent(frequest);
Assert.IsTrue(result);
Assert.AreEqual(expectedId, frequest.PublishedContent.Id);
@@ -164,19 +169,20 @@ namespace Umbraco.Tests.Routing
SetDomains4();
// defaults depend on test environment
expectedCulture = expectedCulture ?? System.Threading.Thread.CurrentThread.CurrentUICulture.Name;
expectedCulture ??= System.Threading.Thread.CurrentThread.CurrentUICulture.Name;
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = true };
GlobalSettings.HideTopLevelNodeFromPath = true;
var umbracoContext = GetUmbracoContext(url, globalSettings:globalSettings);
var publishedRouter = CreatePublishedRouter(GetUmbracoContextAccessor(umbracoContext), Factory);
var frequest = await publishedRouter .CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
var umbracoContextAccessor = GetUmbracoContextAccessor(url);
var publishedRouter = CreatePublishedRouter(umbracoContextAccessor);
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
// must lookup domain else lookup by URL fails
publishedRouter.FindDomain(frequest);
Assert.AreEqual(expectedCulture, frequest.Culture);
var lookup = new ContentFinderByUrl(LoggerFactory.CreateLogger<ContentFinderByUrl>(), GetUmbracoContextAccessor(umbracoContext));
var lookup = new ContentFinderByUrl(Mock.Of<ILogger<ContentFinderByUrl>>(), umbracoContextAccessor);
var result = lookup.TryFindContent(frequest);
Assert.IsTrue(result);
Assert.AreEqual(expectedId, frequest.PublishedContent.Id);

View File

@@ -1,124 +1,151 @@
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Routing;
using Umbraco.Extensions;
namespace Umbraco.Tests.Routing
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing
{
[TestFixture]
internal class DomainsAndCulturesTests : UrlRoutingTestBase
public class DomainsAndCulturesTests : UrlRoutingTestBase
{
protected override void Compose()
{
base.Compose();
Builder.Services.AddTransient<ISiteDomainMapper, SiteDomainMapper>();
}
private void SetDomains1()
{
SetupDomainServiceMock(new[]
{
new UmbracoDomain("domain1.com/")
var domainService = Mock.Get(DomainService);
domainService.Setup(service => service.GetAll(It.IsAny<bool>()))
.Returns((bool incWildcards) => new[]
{
Id = 1,
LanguageId = LangDeId,
RootContentId = 1001,
LanguageIsoCode = "de-DE"
},
new UmbracoDomain("domain1.com/en")
{
Id = 1,
LanguageId = LangEngId,
RootContentId = 10011,
LanguageIsoCode = "en-US"
},
new UmbracoDomain("domain1.com/fr")
{
Id = 1,
LanguageId = LangFrId,
RootContentId = 10012,
LanguageIsoCode = "fr-FR"
}
});
new UmbracoDomain("domain1.com/")
{
Id = 1,
LanguageId = LangDeId,
RootContentId = 1001,
LanguageIsoCode = "de-DE"
},
new UmbracoDomain("domain1.com/en")
{
Id = 2,
LanguageId = LangEngId,
RootContentId = 10011,
LanguageIsoCode = "en-US"
},
new UmbracoDomain("domain1.com/fr")
{
Id = 3,
LanguageId = LangFrId,
RootContentId = 10012,
LanguageIsoCode = "fr-FR"
}
});
}
private void SetDomains2()
{
SetupDomainServiceMock(new[]
{
new UmbracoDomain("domain1.com/")
var domainService = Mock.Get(DomainService);
domainService.Setup(service => service.GetAll(It.IsAny<bool>()))
.Returns((bool incWildcards) => new[]
{
Id = 1,
LanguageId = LangDeId,
RootContentId = 1001,
LanguageIsoCode = "de-DE"
},
new UmbracoDomain("domain1.com/en")
new UmbracoDomain("domain1.com/")
{
Id = 1,
LanguageId = LangDeId,
RootContentId = 1001,
LanguageIsoCode = "de-DE"
},
new UmbracoDomain("domain1.com/en")
{
Id = 2,
LanguageId = LangEngId,
RootContentId = 10011,
LanguageIsoCode = "en-US"
},
new UmbracoDomain("domain1.com/fr")
{
Id = 3,
LanguageId = LangFrId,
RootContentId = 10012,
LanguageIsoCode = "fr-FR"
},
new UmbracoDomain("*1001")
{
Id = 4,
LanguageId = LangDeId,
RootContentId = 1001,
LanguageIsoCode = "de-DE"
},
new UmbracoDomain("*10011")
{
Id = 5,
LanguageId = LangCzId,
RootContentId = 10011,
LanguageIsoCode = "cs-CZ"
},
new UmbracoDomain("*100112")
{
Id = 6,
LanguageId = LangNlId,
RootContentId = 100112,
LanguageIsoCode = "nl-NL"
},
new UmbracoDomain("*1001122")
{
Id = 7,
LanguageId = LangDkId,
RootContentId = 1001122,
LanguageIsoCode = "da-DK"
},
new UmbracoDomain("*10012")
{
Id = 8,
LanguageId = LangNlId,
RootContentId = 10012,
LanguageIsoCode = "nl-NL"
},
new UmbracoDomain("*10031")
{
Id = 9,
LanguageId = LangNlId,
RootContentId =10031,
LanguageIsoCode = "nl-NL"
}
});
}
// domains such as "/en" are natively supported, and when instanciating
// DomainAndUri for them, the host will come from the current request
//
private void SetDomains3()
{
var domainService = Mock.Get(DomainService);
domainService.Setup(service => service.GetAll(It.IsAny<bool>()))
.Returns((bool incWildcards) => new[]
{
Id = 1,
LanguageId = LangEngId,
RootContentId = 10011,
LanguageIsoCode = "en-US"
},
new UmbracoDomain("domain1.com/fr")
{
Id = 1,
LanguageId = LangFrId,
RootContentId = 10012,
LanguageIsoCode = "fr-FR"
},
new UmbracoDomain("*1001")
{
Id = 1,
LanguageId = LangDeId,
RootContentId = 1001,
LanguageIsoCode = "de-DE"
},
new UmbracoDomain("*10011")
{
Id = 1,
LanguageId = LangCzId,
RootContentId = 10011,
LanguageIsoCode = "cs-CZ"
},
new UmbracoDomain("*100112")
{
Id = 1,
LanguageId = LangNlId,
RootContentId = 100112,
LanguageIsoCode = "nl-NL"
},
new UmbracoDomain("*1001122")
{
Id = 1,
LanguageId = LangDkId,
RootContentId = 1001122,
LanguageIsoCode = "da-DK"
},
new UmbracoDomain("*10012")
{
Id = 1,
LanguageId = LangNlId,
RootContentId = 10012,
LanguageIsoCode = "nl-NL"
},
new UmbracoDomain("*10031")
{
Id = 1,
LanguageId = LangNlId,
RootContentId =10031,
LanguageIsoCode = "nl-NL"
}
});
new UmbracoDomain("/en")
{
Id = 1,
LanguageId = LangEngId,
RootContentId = 10011,
LanguageIsoCode = "en-US"
},
new UmbracoDomain("/fr")
{
Id = 2,
LanguageId = LangFrId,
RootContentId = 10012,
LanguageIsoCode = "fr-FR"
}
});
}
protected override string GetXmlContent(int templateId)
{
return @"<?xml version=""1.0"" encoding=""utf-8""?>
=> @"<?xml version=""1.0"" encoding=""utf-8""?>
<!DOCTYPE root[
<!ELEMENT Doc ANY>
<!ATTLIST Doc id ID #REQUIRED>
@@ -250,7 +277,6 @@ namespace Umbraco.Tests.Routing
</Doc>
</Doc>
</root>";
}
#region Cases
[TestCase("http://domain1.com/", "de-DE", 1001)]
@@ -265,18 +291,19 @@ namespace Umbraco.Tests.Routing
{
SetDomains1();
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = false };
GlobalSettings.HideTopLevelNodeFromPath = false;
var umbracoContext = GetUmbracoContext(inputUrl, globalSettings:globalSettings);
var publishedRouter = CreatePublishedRouter(GetUmbracoContextAccessor(umbracoContext), Factory);
var frequest = await publishedRouter .CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
var umbracoContextAccessor = GetUmbracoContextAccessor(inputUrl);
var publishedRouter = CreatePublishedRouter(umbracoContextAccessor);
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
// lookup domain
publishedRouter.FindDomain(frequest);
Assert.AreEqual(expectedCulture, frequest.Culture);
var finder = new ContentFinderByUrl(LoggerFactory.CreateLogger<ContentFinderByUrl>(), GetUmbracoContextAccessor(umbracoContext));
var finder = new ContentFinderByUrl(Mock.Of<ILogger<ContentFinderByUrl>>(), umbracoContextAccessor);
var result = finder.TryFindContent(frequest);
Assert.IsTrue(result);
@@ -311,19 +338,20 @@ namespace Umbraco.Tests.Routing
SetDomains2();
// defaults depend on test environment
expectedCulture = expectedCulture ?? System.Threading.Thread.CurrentThread.CurrentUICulture.Name;
expectedCulture ??= System.Threading.Thread.CurrentThread.CurrentUICulture.Name;
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = false };
GlobalSettings.HideTopLevelNodeFromPath = false;
var umbracoContext = GetUmbracoContext(inputUrl, globalSettings:globalSettings);
var publishedRouter = CreatePublishedRouter(GetUmbracoContextAccessor(umbracoContext), Factory);
var frequest = await publishedRouter .CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
var umbracoContextAccessor = GetUmbracoContextAccessor(inputUrl);
var publishedRouter = CreatePublishedRouter(umbracoContextAccessor);
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
// lookup domain
publishedRouter.FindDomain(frequest);
// find document
var finder = new ContentFinderByUrl(LoggerFactory.CreateLogger<ContentFinderByUrl>(), GetUmbracoContextAccessor(umbracoContext));
var finder = new ContentFinderByUrl(Mock.Of<ILogger<ContentFinderByUrl>>(), umbracoContextAccessor);
var result = finder.TryFindContent(frequest);
// apply wildcard domain
@@ -333,29 +361,7 @@ namespace Umbraco.Tests.Routing
Assert.AreEqual(expectedCulture, frequest.Culture);
Assert.AreEqual(frequest.PublishedContent.Id, expectedNode);
}
// domains such as "/en" are natively supported, and when instanciating
// DomainAndUri for them, the host will come from the current request
//
private void SetDomains3()
{
SetupDomainServiceMock(new[]
{
new UmbracoDomain("/en")
{
Id = 1,
LanguageId = LangEngId,
RootContentId = 10011,
LanguageIsoCode = "en-US"
},
new UmbracoDomain("/fr")
{
Id = 1,
LanguageId = LangFrId,
RootContentId = 10012,
LanguageIsoCode = "fr-FR"
}
});
}
#region Cases
[TestCase("http://domain1.com/en", "en-US", 10011)]
@@ -367,10 +373,13 @@ namespace Umbraco.Tests.Routing
{
SetDomains3();
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = false };
var umbracoContext = GetUmbracoContext(inputUrl, globalSettings:globalSettings);
var publishedRouter = CreatePublishedRouter(GetUmbracoContextAccessor(umbracoContext), Factory);
var frequest = await publishedRouter .CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
GlobalSettings.HideTopLevelNodeFromPath = false;
var umbracoContextAccessor = GetUmbracoContextAccessor(inputUrl);
var publishedRouter = CreatePublishedRouter(umbracoContextAccessor);
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
// lookup domain
publishedRouter.FindDomain(frequest);
@@ -378,7 +387,7 @@ namespace Umbraco.Tests.Routing
Assert.AreEqual(expectedCulture, frequest.Culture);
var finder = new ContentFinderByUrl(LoggerFactory.CreateLogger<ContentFinderByUrl>(), GetUmbracoContextAccessor(umbracoContext));
var finder = new ContentFinderByUrl(Mock.Of<ILogger<ContentFinderByUrl>>(), umbracoContextAccessor);
var result = finder.TryFindContent(frequest);
Assert.IsTrue(result);

View File

@@ -0,0 +1,216 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Published;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing
{
[TestFixture]
public class GetContentUrlsTests : PublishedSnapshotServiceTestBase
{
private WebRoutingSettings _webRoutingSettings;
private RequestHandlerSettings _requestHandlerSettings;
[SetUp]
public override void Setup()
{
base.Setup();
_webRoutingSettings = new WebRoutingSettings();
_requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
GlobalSettings.HideTopLevelNodeFromPath = false;
string xml = PublishedContentXml.BaseWebTestXml(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
}
private ILocalizedTextService GetTextService()
{
var textService = new Mock<ILocalizedTextService>();
textService.Setup(x => x.Localize(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<CultureInfo>(),
It.IsAny<IDictionary<string, string>>()
))
.Returns((string key, string alias, CultureInfo culture, IDictionary<string, string> args)
=> $"{key}/{alias}");
return textService.Object;
}
private ILocalizationService GetLangService(params string[] isoCodes)
{
var allLangs = isoCodes
.Select(CultureInfo.GetCultureInfo)
.Select(culture => new Language(GlobalSettings, culture.Name)
{
CultureName = culture.DisplayName,
IsDefault = true,
IsMandatory = true
}).ToArray();
var langServiceMock = new Mock<ILocalizationService>();
langServiceMock.Setup(x => x.GetAllLanguages()).Returns(allLangs);
langServiceMock.Setup(x => x.GetDefaultLanguageIsoCode()).Returns(allLangs.First(x=>x.IsDefault).IsoCode);
return langServiceMock.Object;
}
[Test]
public async Task Content_Not_Published()
{
var contentType = ContentTypeBuilder.CreateBasicContentType();
var content = ContentBuilder.CreateBasicContent(contentType);
content.Id = 1046; // FIXME: we are using this ID only because it's built into the test XML published cache
content.Path = "-1,1046";
var umbracoContextAccessor = GetUmbracoContextAccessor("http://localhost:8000");
var publishedRouter = CreatePublishedRouter(
umbracoContextAccessor,
new[] { new ContentFinderByUrl(Mock.Of<ILogger<ContentFinderByUrl>>(), umbracoContextAccessor) });
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, _requestHandlerSettings, _webRoutingSettings, out UriUtility uriUtility);
var urls = (await content.GetContentUrlsAsync(
publishedRouter,
umbracoContext,
GetLangService("en-US", "fr-FR"),
GetTextService(),
Mock.Of<IContentService>(),
VariationContextAccessor,
Mock.Of<ILogger<IContent>>(),
uriUtility,
urlProvider)).ToList();
Assert.AreEqual(1, urls.Count);
Assert.AreEqual("content/itemNotPublished", urls[0].Text);
Assert.IsFalse(urls[0].IsUrl);
}
[Test]
public async Task Invariant_Root_Content_Published_No_Domains()
{
var contentType = ContentTypeBuilder.CreateBasicContentType();
var content = ContentBuilder.CreateBasicContent(contentType);
content.Id = 1046; // FIXME: we are using this ID only because it's built into the test XML published cache
content.Path = "-1,1046";
content.Published = true;
var umbracoContextAccessor = GetUmbracoContextAccessor("http://localhost:8000");
var publishedRouter = CreatePublishedRouter(
umbracoContextAccessor,
new[] { new ContentFinderByUrl(Mock.Of<ILogger<ContentFinderByUrl>>(), umbracoContextAccessor) });
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, _requestHandlerSettings, _webRoutingSettings, out UriUtility uriUtility);
var urls = (await content.GetContentUrlsAsync(
publishedRouter,
umbracoContext,
GetLangService("en-US", "fr-FR"),
GetTextService(),
Mock.Of<IContentService>(),
VariationContextAccessor,
Mock.Of<ILogger<IContent>>(),
uriUtility,
urlProvider)).ToList();
Assert.AreEqual(2, urls.Count);
var enUrl = urls.First(x => x.Culture == "en-US");
Assert.AreEqual("/home/", enUrl.Text);
Assert.AreEqual("en-US", enUrl.Culture);
Assert.IsTrue(enUrl.IsUrl);
var frUrl = urls.First(x => x.Culture == "fr-FR");
Assert.IsFalse(frUrl.IsUrl);
}
[Test]
public async Task Invariant_Child_Content_Published_No_Domains()
{
var contentType = ContentTypeBuilder.CreateBasicContentType();
var parent = ContentBuilder.CreateBasicContent(contentType);
parent.Id = 1046; // FIXME: we are using this ID only because it's built into the test XML published cache
parent.Name = "home";
parent.Path = "-1,1046";
parent.Published = true;
var child = ContentBuilder.CreateBasicContent(contentType);
child.Name = "sub1";
child.Id = 1173; // FIXME: we are using this ID only because it's built into the test XML published cache
child.Path = "-1,1046,1173";
child.Published = true;
var umbracoContextAccessor = GetUmbracoContextAccessor("http://localhost:8000");
var publishedRouter = CreatePublishedRouter(
umbracoContextAccessor,
new[] { new ContentFinderByUrl(Mock.Of<ILogger<ContentFinderByUrl>>(), umbracoContextAccessor) });
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
var localizationService = GetLangService("en-US", "fr-FR");
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, _requestHandlerSettings, _webRoutingSettings, out UriUtility uriUtility);
var urls = (await child.GetContentUrlsAsync(
publishedRouter,
umbracoContext,
localizationService,
GetTextService(),
Mock.Of<IContentService>(),
VariationContextAccessor,
Mock.Of<ILogger<IContent>>(),
uriUtility,
urlProvider)).ToList();
Assert.AreEqual(2, urls.Count);
var enUrl = urls.First(x => x.Culture == "en-US");
Assert.AreEqual("/home/sub1/", enUrl.Text);
Assert.AreEqual("en-US", enUrl.Culture);
Assert.IsTrue(enUrl.IsUrl);
var frUrl = urls.First(x => x.Culture == "fr-FR");
Assert.IsFalse(frUrl.IsUrl);
}
// TODO: We need a lot of tests here, the above was just to get started with being able to unit test this method
// * variant URLs without domains assigned, what happens?
// * variant URLs with domains assigned, but also having more languages installed than there are domains/cultures assigned
// * variant URLs with an ancestor culture unpublished
// * invariant URLs with ancestors as variants
// * ... probably a lot more
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Tests.Common;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing
{
[TestFixture]
public class PublishedRouterTests
{
private PublishedRouter CreatePublishedRouter(IUmbracoContextAccessor umbracoContextAccessor)
=> new PublishedRouter(
Microsoft.Extensions.Options.Options.Create(new WebRoutingSettings()),
new ContentFinderCollection(() => Enumerable.Empty<IContentFinder>()),
new TestLastChanceFinder(),
new TestVariationContextAccessor(),
Mock.Of<IProfilingLogger>(),
Mock.Of<ILogger<PublishedRouter>>(),
Mock.Of<IPublishedUrlProvider>(),
Mock.Of<IRequestAccessor>(),
Mock.Of<IPublishedValueFallback>(),
Mock.Of<IFileService>(),
Mock.Of<IContentTypeService>(),
umbracoContextAccessor,
Mock.Of<IEventAggregator>());
private IUmbracoContextAccessor GetUmbracoContextAccessor()
{
var uri = new Uri("http://example.com");
var umbracoContext = Mock.Of<IUmbracoContext>(x => x.CleanedUmbracoUrl == uri);
var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext);
return umbracoContextAccessor;
}
[Test]
public async Task ConfigureRequest_Returns_False_Without_HasPublishedContent()
{
var umbracoContextAccessor = GetUmbracoContextAccessor();
var publishedRouter = CreatePublishedRouter(umbracoContextAccessor);
var request = await publishedRouter.CreateRequestAsync(umbracoContextAccessor.GetRequiredUmbracoContext().CleanedUmbracoUrl);
var result = publishedRouter.BuildRequest(request);
Assert.IsFalse(result.Success());
}
[Test]
public async Task ConfigureRequest_Returns_False_When_IsRedirect()
{
var umbracoContextAccessor = GetUmbracoContextAccessor();
var publishedRouter = CreatePublishedRouter(umbracoContextAccessor);
var request = await publishedRouter.CreateRequestAsync(umbracoContextAccessor.GetRequiredUmbracoContext().CleanedUmbracoUrl);
var content = GetPublishedContentMock();
request.SetPublishedContent(content.Object);
request.SetCulture("en-AU");
request.SetRedirect("/hello");
var result = publishedRouter.BuildRequest(request);
Assert.IsFalse(result.Success());
}
private Mock<IPublishedContent> GetPublishedContentMock()
{
var pc = new Mock<IPublishedContent>();
pc.Setup(content => content.Id).Returns(1);
pc.Setup(content => content.Name).Returns("test");
pc.Setup(content => content.CreateDate).Returns(DateTime.Now);
pc.Setup(content => content.UpdateDate).Returns(DateTime.Now);
pc.Setup(content => content.Path).Returns("-1,1");
pc.Setup(content => content.Parent).Returns(() => null);
pc.Setup(content => content.Properties).Returns(new Collection<IPublishedProperty>());
pc.Setup(content => content.ContentType).Returns(new PublishedContentType(Guid.NewGuid(), 22, "anything", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing));
return pc;
}
}
}

View File

@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Tests.Common;
using Umbraco.Cms.Tests.Common.Published;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing
{
[TestFixture]
public class UrlProviderWithHideTopLevelNodeFromPathTests : PublishedSnapshotServiceTestBase
{
[SetUp]
public override void Setup()
{
base.Setup();
string xml = PublishedContentXml.BaseWebTestXml(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
GlobalSettings.HideTopLevelNodeFromPath = true;
}
[TestCase(1046, "/")]
[TestCase(1173, "/sub1/")]
[TestCase(1174, "/sub1/sub2/")]
[TestCase(1176, "/sub1/sub-3/")]
[TestCase(1177, "/sub1/custom-sub-1/")]
[TestCase(1178, "/sub1/custom-sub-2/")]
[TestCase(1175, "/sub-2/")]
[TestCase(1172, "/test-page/")] // not hidden because not first root
public void Get_Url_Hiding_Top_Level(int nodeId, string niceUrlMatch)
{
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
var umbracoContextAccessor = GetUmbracoContextAccessor("/test");
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out UriUtility uriUtility);
var result = urlProvider.GetUrl(nodeId);
Assert.AreEqual(niceUrlMatch, result);
}
}
}

View File

@@ -0,0 +1,336 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
using Umbraco.Cms.Tests.Common;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.Common.Published;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing
{
[TestFixture]
public class UrlProviderWithoutHideTopLevelNodeFromPathTests : PublishedSnapshotServiceTestBase
{
private const string CacheKeyPrefix = "NuCache.ContentCache.RouteByContent";
[SetUp]
public override void Setup()
{
base.Setup();
GlobalSettings.HideTopLevelNodeFromPath = false;
}
private void PopulateCache(string culture = "fr-FR")
{
var dataTypes = GetDefaultDataTypes();
var propertyDataTypes = new Dictionary<string, IDataType>
{
// we only have one data type for this test which will be resolved with string empty.
[string.Empty] = dataTypes[0]
};
var contentType1 = new ContentType(ShortStringHelper, -1);
ContentData rootData = new ContentDataBuilder()
.WithName("Page" + Guid.NewGuid())
.WithCultureInfos(new Dictionary<string, CultureVariation>
{
[culture] = new CultureVariation
{
Name = "root",
IsDraft = true,
Date = DateTime.Now,
UrlSegment = "root"
},
})
.Build(ShortStringHelper, propertyDataTypes, contentType1, "alias");
ContentNodeKit root = ContentNodeKitBuilder.CreateWithContent(
contentType1.Id,
9876, $"-1,9876",
draftData: rootData,
publishedData: rootData);
ContentData parentData = new ContentDataBuilder()
.WithName("Page" + Guid.NewGuid())
.WithCultureInfos(new Dictionary<string, CultureVariation>
{
[culture] = new CultureVariation
{
Name = "home",
IsDraft = true,
Date = DateTime.Now,
UrlSegment = "home"
},
})
.Build();
ContentNodeKit parent = ContentNodeKitBuilder.CreateWithContent(
contentType1.Id,
5432, $"-1,9876,5432",
parentContentId: 9876,
draftData: parentData,
publishedData: parentData);
ContentData contentData = new ContentDataBuilder()
.WithName("Page" + Guid.NewGuid())
.WithCultureInfos(new Dictionary<string, CultureVariation>
{
[culture] = new CultureVariation
{
Name = "name-fr2",
IsDraft = true,
Date = DateTime.Now,
UrlSegment = "test-fr"
},
})
.Build();
ContentNodeKit content = ContentNodeKitBuilder.CreateWithContent(
contentType1.Id,
1234, $"-1,9876,5432,1234",
parentContentId: 5432,
draftData: contentData,
publishedData: contentData);
InitializedCache(new[] { root, parent, content }, new[] { contentType1 }, dataTypes: dataTypes);
}
private void SetDomains1()
{
var domainService = Mock.Get(DomainService);
domainService.Setup(service => service.GetAll(It.IsAny<bool>()))
.Returns((bool incWildcards) => new[]
{
new UmbracoDomain("http://example.us/") {Id = 1, RootContentId = 9876, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://example.fr/") {Id = 2, RootContentId = 9876, LanguageIsoCode = "fr-FR"}
});
}
/// <summary>
/// This checks that when we retrieve a NiceUrl for multiple items that there are no issues with cache overlap
/// and that they are all cached correctly.
/// </summary>
[Test]
public void Ensure_Cache_Is_Correct()
{
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = false };
string xml = PublishedContentXml.BaseWebTestXml(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
var umbracoContextAccessor = GetUmbracoContextAccessor("/test");
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out UriUtility uriUtility);
var samples = new Dictionary<int, string> {
{ 1046, "/home" },
{ 1173, "/home/sub1" },
{ 1174, "/home/sub1/sub2" },
{ 1176, "/home/sub1/sub-3" },
{ 1177, "/home/sub1/custom-sub-1" },
{ 1178, "/home/sub1/custom-sub-2" },
{ 1175, "/home/sub-2" },
{ 1172, "/test-page" }
};
foreach (var sample in samples)
{
var result = urlProvider.GetUrl(sample.Key);
Assert.AreEqual(sample.Value, result);
}
var randomSample = new KeyValuePair<int, string>(1177, "/home/sub1/custom-sub-1");
for (int i = 0; i < 5; i++)
{
var result = urlProvider.GetUrl(randomSample.Key);
Assert.AreEqual(randomSample.Value, result);
}
var cache = (FastDictionaryAppCache)umbracoContext.PublishedSnapshot.ElementsCache;
var cachedRoutes = cache.Keys.Where(x => x.StartsWith(CacheKeyPrefix)).ToList();
Assert.AreEqual(8, cachedRoutes.Count);
foreach (var sample in samples)
{
var cacheKey = $"{CacheKeyPrefix}[P:{sample.Key}]";
var found = (string)cache.Get(cacheKey);
Assert.IsNotNull(found);
Assert.AreEqual(sample.Value, found);
}
}
[TestCase(1046, "/home/")]
[TestCase(1173, "/home/sub1/")]
[TestCase(1174, "/home/sub1/sub2/")]
[TestCase(1176, "/home/sub1/sub-3/")]
[TestCase(1177, "/home/sub1/custom-sub-1/")]
[TestCase(1178, "/home/sub1/custom-sub-2/")]
[TestCase(1175, "/home/sub-2/")]
[TestCase(1172, "/test-page/")]
public void Get_Url_Not_Hiding_Top_Level(int nodeId, string niceUrlMatch)
{
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
string xml = PublishedContentXml.BaseWebTestXml(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
var umbracoContextAccessor = GetUmbracoContextAccessor("/test");
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out UriUtility uriUtility);
var result = urlProvider.GetUrl(nodeId);
Assert.AreEqual(niceUrlMatch, result);
}
[Test]
[TestCase("fr-FR", ExpectedResult = "#")] // Non default cultures cannot return urls
[TestCase("en-US", ExpectedResult = "/root/home/test-fr/")] // Default culture can return urls
public string Get_Url_For_Culture_Variant_Without_Domains_Non_Current_Url(string culture)
{
const string currentUri = "http://example.us/test";
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
PopulateCache(culture);
var umbracoContextAccessor = GetUmbracoContextAccessor(currentUri);
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out UriUtility uriUtility);
//even though we are asking for a specific culture URL, there are no domains assigned so all that can be returned is a normal relative URL.
var url = urlProvider.GetUrl(1234, culture: culture);
return url;
}
/// <summary>
/// This tests DefaultUrlProvider.GetUrl with a specific culture when the current URL is the culture specific domain
/// </summary>
[Test]
public void Get_Url_For_Culture_Variant_With_Current_Url()
{
const string currentUri = "http://example.fr/test";
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
PopulateCache();
SetDomains1();
var umbracoContextAccessor = GetUmbracoContextAccessor(currentUri);
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out UriUtility uriUtility);
var url = urlProvider.GetUrl(1234, culture: "fr-FR");
Assert.AreEqual("/home/test-fr/", url);
}
/// <summary>
/// This tests DefaultUrlProvider.GetUrl with a specific culture when the current URL is not the culture specific domain
/// </summary>
[Test]
public void Get_Url_For_Culture_Variant_Non_Current_Url()
{
const string currentUri = "http://example.us/test";
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
PopulateCache();
SetDomains1();
var umbracoContextAccessor = GetUmbracoContextAccessor(currentUri);
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out UriUtility uriUtility);
var url = urlProvider.GetUrl(1234, culture: "fr-FR");
//the current uri is not the culture specific domain we want, so the result is an absolute path to the culture specific domain
Assert.AreEqual("http://example.fr/home/test-fr/", url);
}
[Test]
public void Get_Url_Relative_Or_Absolute()
{
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
string xml = PublishedContentXml.BaseWebTestXml(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
var umbracoContextAccessor = GetUmbracoContextAccessor("http://example.com/test");
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out UriUtility uriUtility);
Assert.AreEqual("/home/sub1/custom-sub-1/", urlProvider.GetUrl(1177));
urlProvider.Mode = UrlMode.Absolute;
Assert.AreEqual("http://example.com/home/sub1/custom-sub-1/", urlProvider.GetUrl(1177));
}
[Test]
public void Get_Url_Unpublished()
{
var requestHandlerSettings = new RequestHandlerSettings();
string xml = PublishedContentXml.BaseWebTestXml(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
var umbracoContextAccessor = GetUmbracoContextAccessor("http://example.com/test");
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out UriUtility uriUtility);
//mock the Umbraco settings that we need
Assert.AreEqual("#", urlProvider.GetUrl(999999));
urlProvider.Mode = UrlMode.Absolute;
Assert.AreEqual("#", urlProvider.GetUrl(999999));
}
}
}

View File

@@ -1,68 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Routing;
using Constants = Umbraco.Cms.Core.Constants;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Tests.Common.Published;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
namespace Umbraco.Tests.Routing
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing
{
// TODO: We should be able to decouple this from the base db tests since we're just mocking the services now
[TestFixture]
public class ContentFinderByAliasTests : UrlRoutingTestBase
public abstract class UrlRoutingTestBase : PublishedSnapshotServiceTestBase
{
private PublishedContentType _publishedContentType;
protected override void Initialize()
[SetUp]
public override void Setup()
{
base.Initialize();
base.Setup();
string xml = GetXmlContent(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
var properties = new[]
{
new PublishedPropertyType("umbracoUrlAlias", Constants.DataTypes.Textbox, false, ContentVariation.Nothing,
new PropertyValueConverterCollection(Enumerable.Empty<IPropertyValueConverter>()),
Mock.Of<IPublishedModelFactory>(),
Mock.Of<IPublishedContentTypeFactory>()),
};
_publishedContentType = new PublishedContentType(Guid.NewGuid(), 0, "Doc", PublishedItemType.Content, Enumerable.Empty<string>(), properties, ContentVariation.Nothing);
}
protected override PublishedContentType GetPublishedContentTypeByAlias(string alias)
// Sets up the mock domain service
protected override ServiceContext CreateServiceContext(IContentType[] contentTypes, IMediaType[] mediaTypes, IDataType[] dataTypes)
{
if (alias == "Doc") return _publishedContentType;
return null;
var serviceContext = base.CreateServiceContext(contentTypes, mediaTypes, dataTypes);
//setup mock domain service
var domainService = Mock.Get(serviceContext.DomainService);
domainService.Setup(service => service.GetAll(It.IsAny<bool>()))
.Returns((bool incWildcards) => new[]
{
new UmbracoDomain("domain1.com/"){Id = 1, LanguageId = LangDeId, RootContentId = 1001, LanguageIsoCode = "de-DE"},
new UmbracoDomain("domain1.com/en"){Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US"},
new UmbracoDomain("domain1.com/fr"){Id = 3, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR"}
});
return serviceContext;
}
[TestCase("/this/is/my/alias", 1001)]
[TestCase("/anotheralias", 1001)]
[TestCase("/page2/alias", 10011)]
[TestCase("/2ndpagealias", 10011)]
[TestCase("/only/one/alias", 100111)]
[TestCase("/ONLY/one/Alias", 100111)]
[TestCase("/alias43", 100121)]
public async Task Lookup_By_Url_Alias(string urlAsString, int nodeMatch)
{
var umbracoContext = GetUmbracoContext(urlAsString);
var publishedRouter = CreatePublishedRouter(GetUmbracoContextAccessor(umbracoContext));
var frequest = await publishedRouter .CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
var lookup =
new ContentFinderByUrlAlias(LoggerFactory.CreateLogger<ContentFinderByUrlAlias>(), Mock.Of<IPublishedValueFallback>(), VariationContextAccessor, GetUmbracoContextAccessor(umbracoContext));
var result = lookup.TryFindContent(frequest);
Assert.IsTrue(result);
Assert.AreEqual(frequest.PublishedContent.Id, nodeMatch);
}
protected override string GetXmlContent(int templateId)
{
return @"<?xml version=""1.0"" encoding=""utf-8""?>
protected virtual string GetXmlContent(int templateId)
=> @"<?xml version=""1.0"" encoding=""utf-8""?>
<!DOCTYPE root[
<!ELEMENT Doc ANY>
<!ATTLIST Doc id ID #REQUIRED>
@@ -147,7 +134,12 @@ namespace Umbraco.Tests.Routing
</Doc>
</Doc>
</root>";
}
public const int LangDeId = 333;
public const int LangEngId = 334;
public const int LangFrId = 335;
public const int LangCzId = 336;
public const int LangNlId = 337;
public const int LangDkId = 338;
}
}

View File

@@ -1,10 +1,11 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
@@ -13,77 +14,83 @@ using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Tests.Common;
using Umbraco.Extensions;
using Umbraco.Tests.LegacyXmlPublishedCache;
namespace Umbraco.Tests.Routing
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing
{
[TestFixture]
public class UrlsProviderWithDomainsTests : UrlRoutingTestBase
{
private IUmbracoContextAccessor UmbracoContextAccessor { get; } = new TestUmbracoContextAccessor();
protected override void Compose()
{
base.Compose();
private const string CacheKeyPrefix = "NuCache.ContentCache.RouteByContent";
Builder.Services.AddUnique(Mock.Of<IDomainService>());
Builder.Services.AddTransient<ISiteDomainMapper, SiteDomainMapper>();
private void SetDomains1()
{
var domainService = Mock.Get(DomainService);
domainService.Setup(service => service.GetAll(It.IsAny<bool>()))
.Returns((bool incWildcards) => new[]
{
new UmbracoDomain("domain1.com") {Id = 1, LanguageId = LangFrId, RootContentId = 1001, LanguageIsoCode = "fr-FR"}
});
}
void SetDomains1()
private void SetDomains2()
{
SetupDomainServiceMock(new[]
{
new UmbracoDomain("domain1.com") {Id = 1, LanguageId = LangFrId, RootContentId = 1001, LanguageIsoCode = "fr-FR"}
});
var domainService = Mock.Get(DomainService);
domainService.Setup(service => service.GetAll(It.IsAny<bool>()))
.Returns((bool incWildcards) => new[]
{
new UmbracoDomain("http://domain1.com/foo") {Id = 1, LanguageId = LangFrId, RootContentId = 1001, LanguageIsoCode = "fr-FR"}
});
}
void SetDomains2()
private void SetDomains3()
{
SetupDomainServiceMock(new[]
{
new UmbracoDomain("http://domain1.com/foo") {Id = 1, LanguageId = LangFrId, RootContentId = 1001, LanguageIsoCode = "fr-FR"}
});
var domainService = Mock.Get(DomainService);
domainService.Setup(service => service.GetAll(It.IsAny<bool>()))
.Returns((bool incWildcards) => new[]
{
new UmbracoDomain("http://domain1.com/") {Id = 1, LanguageId = LangFrId, RootContentId = 10011, LanguageIsoCode = "fr-FR"}
});
}
void SetDomains3()
private void SetDomains4()
{
SetupDomainServiceMock(new[]
{
new UmbracoDomain("http://domain1.com/") {Id = 1, LanguageId = LangFrId, RootContentId = 10011, LanguageIsoCode = "fr-FR"}
});
var domainService = Mock.Get(DomainService);
domainService.Setup(service => service.GetAll(It.IsAny<bool>()))
.Returns((bool incWildcards) => new[]
{
new UmbracoDomain("http://domain1.com/") {Id = 1, LanguageId = LangEngId, RootContentId = 1001, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain1.com/en") {Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain1.com/fr") {Id = 3, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR"},
new UmbracoDomain("http://domain3.com/") {Id = 4, LanguageId = LangEngId, RootContentId = 1003, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain3.com/en") {Id = 5, LanguageId = LangEngId, RootContentId = 10031, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain3.com/fr") {Id = 6, LanguageId = LangFrId, RootContentId = 10032, LanguageIsoCode = "fr-FR"}
});
}
void SetDomains4()
private void SetDomains5()
{
SetupDomainServiceMock(new[]
{
new UmbracoDomain("http://domain1.com/") {Id = 1, LanguageId = LangEngId, RootContentId = 1001, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain1.com/en") {Id = 1, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain1.com/fr") {Id = 1, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR"},
new UmbracoDomain("http://domain3.com/") {Id = 1, LanguageId = LangEngId, RootContentId = 1003, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain3.com/en") {Id = 1, LanguageId = LangEngId, RootContentId = 10031, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain3.com/fr") {Id = 1, LanguageId = LangFrId, RootContentId = 10032, LanguageIsoCode = "fr-FR"}
});
}
var domainService = Mock.Get(DomainService);
void SetDomains5()
{
SetupDomainServiceMock(new[]
{
new UmbracoDomain("http://domain1.com/en") {Id = 1, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain1a.com/en") {Id = 1, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain1b.com/en") {Id = 1, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain1.com/fr") {Id = 1, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR"},
new UmbracoDomain("http://domain1a.com/fr") {Id = 1, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR"},
new UmbracoDomain("http://domain1b.com/fr") {Id = 1, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR"},
new UmbracoDomain("http://domain3.com/en") {Id = 1, LanguageId = LangEngId, RootContentId = 10031, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain3.com/fr") {Id = 1, LanguageId = LangFrId, RootContentId = 10032, LanguageIsoCode = "fr-FR"}
});
domainService.Setup(service => service.GetAll(It.IsAny<bool>()))
.Returns((bool incWildcards) => new[]
{
new UmbracoDomain("http://domain1.com/en") {Id = 1, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain1a.com/en") {Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain1b.com/en") {Id = 3, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain1.com/fr") {Id = 4, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR"},
new UmbracoDomain("http://domain1a.com/fr") {Id = 5, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR"},
new UmbracoDomain("http://domain1b.com/fr") {Id = 6, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR"},
new UmbracoDomain("http://domain3.com/en") {Id = 7, LanguageId = LangEngId, RootContentId = 10031, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain3.com/fr") {Id = 8, LanguageId = LangFrId, RootContentId = 10032, LanguageIsoCode = "fr-FR"}
});
}
protected override string GetXmlContent(int templateId)
{
return @"<?xml version=""1.0"" encoding=""utf-8""?>
=> @"<?xml version=""1.0"" encoding=""utf-8""?>
<!DOCTYPE root[
<!ELEMENT Doc ANY>
<!ATTLIST Doc id ID #REQUIRED>
@@ -162,7 +169,6 @@ namespace Umbraco.Tests.Routing
</Doc>
</Doc>
</root>";
}
// with one simple domain "domain1.com"
// basic tests
@@ -179,21 +185,18 @@ namespace Umbraco.Tests.Routing
[TestCase(10011, "https://domain1.com", false, "/1001-1/")]
public void Get_Url_SimpleDomain(int nodeId, string currentUrl, bool absolute, string expected)
{
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = false };
var umbracoContext = GetUmbracoContext("/test", 1111, globalSettings: globalSettings);
var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext);
var urlProvider = new DefaultUrlProvider(Microsoft.Extensions.Options.Options.Create(requestHandlerSettings),
LoggerFactory.CreateLogger<DefaultUrlProvider>(),
new SiteDomainMapper(), umbracoContextAccessor, UriUtility);
var publishedUrlProvider = GetPublishedUrlProvider(umbracoContext, urlProvider);
SetDomains1();
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
GlobalSettings.HideTopLevelNodeFromPath = false;
var umbracoContextAccessor = GetUmbracoContextAccessor("/test");
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out UriUtility uriUtility);
var currentUri = new Uri(currentUrl);
var mode = absolute ? UrlMode.Absolute : UrlMode.Auto;
var result = publishedUrlProvider.GetUrl(nodeId, mode, current: currentUri);
var result = urlProvider.GetUrl(nodeId, mode, current: currentUri);
Assert.AreEqual(expected, result);
}
@@ -212,21 +215,18 @@ namespace Umbraco.Tests.Routing
[TestCase(10011, "https://domain1.com", false, "http://domain1.com/foo/1001-1/")]
public void Get_Url_SimpleWithSchemeAndPath(int nodeId, string currentUrl, bool absolute, string expected)
{
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = false };
var umbracoContext = GetUmbracoContext("/test", 1111, globalSettings: globalSettings);
var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext);
var urlProvider = new DefaultUrlProvider(Microsoft.Extensions.Options.Options.Create(requestHandlerSettings),
LoggerFactory.CreateLogger<DefaultUrlProvider>(),
new SiteDomainMapper(), umbracoContextAccessor, UriUtility);
var publishedUrlProvider = GetPublishedUrlProvider(umbracoContext, urlProvider);
SetDomains2();
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
GlobalSettings.HideTopLevelNodeFromPath = false;
var umbracoContextAccessor = GetUmbracoContextAccessor("/test");
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out UriUtility uriUtility);
var currentUri = new Uri(currentUrl);
var mode = absolute ? UrlMode.Absolute : UrlMode.Auto;
var result = publishedUrlProvider.GetUrl(nodeId, mode, current : currentUri);
var result = urlProvider.GetUrl(nodeId, mode, current: currentUri);
Assert.AreEqual(expected, result);
}
@@ -237,21 +237,18 @@ namespace Umbraco.Tests.Routing
[TestCase(1002, "http://domain1.com", false, "/1002/")]
public void Get_Url_DeepDomain(int nodeId, string currentUrl, bool absolute, string expected)
{
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = false };
var umbracoContext = GetUmbracoContext("/test", 1111, globalSettings: globalSettings);
var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext);
var urlProvider = new DefaultUrlProvider(Microsoft.Extensions.Options.Options.Create(requestHandlerSettings),
LoggerFactory.CreateLogger<DefaultUrlProvider>(),
new SiteDomainMapper(), umbracoContextAccessor, UriUtility);
var publishedUrlProvider = GetPublishedUrlProvider(umbracoContext, urlProvider);
SetDomains3();
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
GlobalSettings.HideTopLevelNodeFromPath = false;
var umbracoContextAccessor = GetUmbracoContextAccessor("/test");
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out UriUtility uriUtility);
var currentUri = new Uri(currentUrl);
var mode = absolute ? UrlMode.Absolute : UrlMode.Auto;
var result = publishedUrlProvider.GetUrl(nodeId, mode, current : currentUri);
var result = urlProvider.GetUrl(nodeId, mode, current: currentUri);
Assert.AreEqual(expected, result);
}
@@ -268,152 +265,130 @@ namespace Umbraco.Tests.Routing
[TestCase(100321, "http://domain3.com", false, "/fr/1003-2-1/")]
public void Get_Url_NestedDomains(int nodeId, string currentUrl, bool absolute, string expected)
{
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = false };
var umbracoContext = GetUmbracoContext("/test", 1111, globalSettings: globalSettings);
var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext);
var urlProvider = new DefaultUrlProvider(
Microsoft.Extensions.Options.Options.Create(requestHandlerSettings),
LoggerFactory.CreateLogger<DefaultUrlProvider>(),
new SiteDomainMapper(), umbracoContextAccessor, UriUtility);
var publishedUrlProvider = GetPublishedUrlProvider(umbracoContext, urlProvider);
SetDomains4();
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
GlobalSettings.HideTopLevelNodeFromPath = false;
var umbracoContextAccessor = GetUmbracoContextAccessor("/test");
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out UriUtility uriUtility);
var currentUri = new Uri(currentUrl);
var mode = absolute ? UrlMode.Absolute : UrlMode.Auto;
var result = publishedUrlProvider.GetUrl(nodeId, mode, current : currentUri);
var result = urlProvider.GetUrl(nodeId, mode, current: currentUri);
Assert.AreEqual(expected, result);
}
[Test]
public void Get_Url_DomainsAndCache()
{
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = false };
var umbracoContext = GetUmbracoContext("/test", 1111, globalSettings: globalSettings);
var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext);
var urlProvider = new DefaultUrlProvider(
Microsoft.Extensions.Options.Options.Create(requestHandlerSettings),
LoggerFactory.CreateLogger<DefaultUrlProvider>(),
new SiteDomainMapper(), umbracoContextAccessor, UriUtility);
var publishedUrlProvider = GetPublishedUrlProvider(umbracoContext, urlProvider);
SetDomains4();
string ignore;
ignore = publishedUrlProvider.GetUrl(1001, UrlMode.Auto, current: new Uri("http://domain1.com"));
ignore = publishedUrlProvider.GetUrl(10011, UrlMode.Auto, current: new Uri("http://domain1.com"));
ignore = publishedUrlProvider.GetUrl(100111, UrlMode.Auto, current: new Uri("http://domain1.com"));
ignore = publishedUrlProvider.GetUrl(10012, UrlMode.Auto, current: new Uri("http://domain1.com"));
ignore = publishedUrlProvider.GetUrl(100121, UrlMode.Auto, current: new Uri("http://domain1.com"));
ignore = publishedUrlProvider.GetUrl(10013, UrlMode.Auto, current: new Uri("http://domain1.com"));
ignore = publishedUrlProvider.GetUrl(1002, UrlMode.Auto, current: new Uri("http://domain1.com"));
ignore = publishedUrlProvider.GetUrl(1001, UrlMode.Auto, current: new Uri("http://domain2.com"));
ignore = publishedUrlProvider.GetUrl(10011, UrlMode.Auto, current: new Uri("http://domain2.com"));
ignore = publishedUrlProvider.GetUrl(100111, UrlMode.Auto, current: new Uri("http://domain2.com"));
ignore = publishedUrlProvider.GetUrl(1002, UrlMode.Auto, current: new Uri("http://domain2.com"));
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
GlobalSettings.HideTopLevelNodeFromPath = false;
var cache = umbracoContext.Content as PublishedContentCache;
if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported.");
var cachedRoutes = cache.RoutesCache.GetCachedRoutes();
var umbracoContextAccessor = GetUmbracoContextAccessor("/test");
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out UriUtility uriUtility);
string ignore;
ignore = urlProvider.GetUrl(1001, UrlMode.Auto, current: new Uri("http://domain1.com"));
ignore = urlProvider.GetUrl(10011, UrlMode.Auto, current: new Uri("http://domain1.com"));
ignore = urlProvider.GetUrl(100111, UrlMode.Auto, current: new Uri("http://domain1.com"));
ignore = urlProvider.GetUrl(10012, UrlMode.Auto, current: new Uri("http://domain1.com"));
ignore = urlProvider.GetUrl(100121, UrlMode.Auto, current: new Uri("http://domain1.com"));
ignore = urlProvider.GetUrl(10013, UrlMode.Auto, current: new Uri("http://domain1.com"));
ignore = urlProvider.GetUrl(1002, UrlMode.Auto, current: new Uri("http://domain1.com"));
ignore = urlProvider.GetUrl(1001, UrlMode.Auto, current: new Uri("http://domain2.com"));
ignore = urlProvider.GetUrl(10011, UrlMode.Auto, current: new Uri("http://domain2.com"));
ignore = urlProvider.GetUrl(100111, UrlMode.Auto, current: new Uri("http://domain2.com"));
ignore = urlProvider.GetUrl(1002, UrlMode.Auto, current: new Uri("http://domain2.com"));
var cache = (FastDictionaryAppCache)umbracoContext.PublishedSnapshot.ElementsCache;
var cachedRoutes = cache.Keys.Where(x => x.StartsWith(CacheKeyPrefix)).ToList();
Assert.AreEqual(7, cachedRoutes.Count);
var cachedIds = cache.RoutesCache.GetCachedIds();
Assert.AreEqual(0, cachedIds.Count);
//var cachedIds = cache.RoutesCache.GetCachedIds();
//Assert.AreEqual(0, cachedIds.Count);
CheckRoute(cachedRoutes, cachedIds, 1001, "1001/");
CheckRoute(cachedRoutes, cachedIds, 10011, "10011/");
CheckRoute(cachedRoutes, cachedIds, 100111, "10011/1001-1-1");
CheckRoute(cachedRoutes, cachedIds, 10012, "10012/");
CheckRoute(cachedRoutes, cachedIds, 100121, "10012/1001-2-1");
CheckRoute(cachedRoutes, cachedIds, 10013, "1001/1001-3");
CheckRoute(cachedRoutes, cachedIds, 1002, "/1002");
CheckRoute(cache, 1001, "1001/");
CheckRoute(cache, 10011, "10011/");
CheckRoute(cache, 100111, "10011/1001-1-1");
CheckRoute(cache, 10012, "10012/");
CheckRoute(cache, 100121, "10012/1001-2-1");
CheckRoute(cache, 10013, "1001/1001-3");
CheckRoute(cache, 1002, "/1002");
// use the cache
Assert.AreEqual("/", publishedUrlProvider.GetUrl(1001, UrlMode.Auto, current: new Uri("http://domain1.com")));
Assert.AreEqual("/en/", publishedUrlProvider.GetUrl(10011, UrlMode.Auto, current: new Uri("http://domain1.com")));
Assert.AreEqual("/en/1001-1-1/", publishedUrlProvider.GetUrl(100111, UrlMode.Auto, current: new Uri("http://domain1.com")));
Assert.AreEqual("/fr/", publishedUrlProvider.GetUrl(10012, UrlMode.Auto, current: new Uri("http://domain1.com")));
Assert.AreEqual("/fr/1001-2-1/", publishedUrlProvider.GetUrl(100121, UrlMode.Auto, current: new Uri("http://domain1.com")));
Assert.AreEqual("/1001-3/", publishedUrlProvider.GetUrl(10013, UrlMode.Auto, current: new Uri("http://domain1.com")));
Assert.AreEqual("/1002/", publishedUrlProvider.GetUrl(1002, UrlMode.Auto, current: new Uri("http://domain1.com")));
Assert.AreEqual("/", urlProvider.GetUrl(1001, UrlMode.Auto, current: new Uri("http://domain1.com")));
Assert.AreEqual("/en/", urlProvider.GetUrl(10011, UrlMode.Auto, current: new Uri("http://domain1.com")));
Assert.AreEqual("/en/1001-1-1/", urlProvider.GetUrl(100111, UrlMode.Auto, current: new Uri("http://domain1.com")));
Assert.AreEqual("/fr/", urlProvider.GetUrl(10012, UrlMode.Auto, current: new Uri("http://domain1.com")));
Assert.AreEqual("/fr/1001-2-1/", urlProvider.GetUrl(100121, UrlMode.Auto, current: new Uri("http://domain1.com")));
Assert.AreEqual("/1001-3/", urlProvider.GetUrl(10013, UrlMode.Auto, current: new Uri("http://domain1.com")));
Assert.AreEqual("/1002/", urlProvider.GetUrl(1002, UrlMode.Auto, current: new Uri("http://domain1.com")));
Assert.AreEqual("http://domain1.com/fr/1001-2-1/", publishedUrlProvider.GetUrl(100121, UrlMode.Auto, current: new Uri("http://domain2.com")));
Assert.AreEqual("http://domain1.com/fr/1001-2-1/", urlProvider.GetUrl(100121, UrlMode.Auto, current: new Uri("http://domain2.com")));
}
private static void CheckRoute(IDictionary<int, string> routes, IDictionary<string, int> ids, int id, string route)
private static void CheckRoute(FastDictionaryAppCache routes, int id, string route)
{
Assert.IsTrue(routes.ContainsKey(id));
Assert.AreEqual(route, routes[id]);
Assert.IsFalse(ids.ContainsKey(route));
var cacheKey = $"{CacheKeyPrefix}[P:{id}]";
var found = (string)routes.Get(cacheKey);
Assert.IsNotNull(found);
Assert.AreEqual(route, found);
}
[Test]
public void Get_Url_Relative_Or_Absolute()
{
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = false };
var umbracoContext = GetUmbracoContext("http://domain1.com/test", 1111, globalSettings: globalSettings);
var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext);
var urlProvider = new DefaultUrlProvider(
Microsoft.Extensions.Options.Options.Create(requestHandlerSettings),
LoggerFactory.CreateLogger<DefaultUrlProvider>(),
new SiteDomainMapper(), umbracoContextAccessor, UriUtility);
var publishedUrlProvider = GetPublishedUrlProvider(umbracoContext, urlProvider);
SetDomains4();
Assert.AreEqual("/en/1001-1-1/", publishedUrlProvider.GetUrl(100111));
Assert.AreEqual("http://domain3.com/en/1003-1-1/", publishedUrlProvider.GetUrl(100311));
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
GlobalSettings.HideTopLevelNodeFromPath = false;
publishedUrlProvider.Mode = UrlMode.Absolute;
var umbracoContextAccessor = GetUmbracoContextAccessor("http://domain1.com/test");
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out UriUtility uriUtility);
Assert.AreEqual("http://domain1.com/en/1001-1-1/", publishedUrlProvider.GetUrl(100111));
Assert.AreEqual("http://domain3.com/en/1003-1-1/", publishedUrlProvider.GetUrl(100311));
Assert.AreEqual("/en/1001-1-1/", urlProvider.GetUrl(100111));
Assert.AreEqual("http://domain3.com/en/1003-1-1/", urlProvider.GetUrl(100311));
urlProvider.Mode = UrlMode.Absolute;
Assert.AreEqual("http://domain1.com/en/1001-1-1/", urlProvider.GetUrl(100111));
Assert.AreEqual("http://domain3.com/en/1003-1-1/", urlProvider.GetUrl(100311));
}
[Test]
public void Get_Url_Alternate()
{
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = false };
var umbracoContext = GetUmbracoContext("http://domain1.com/en/test", 1111, globalSettings: globalSettings);
var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext);
var urlProvider = new DefaultUrlProvider(
Microsoft.Extensions.Options.Options.Create(requestHandlerSettings),
LoggerFactory.CreateLogger<DefaultUrlProvider>(),
new SiteDomainMapper(), umbracoContextAccessor, UriUtility);
var publishedUrlProvider = GetPublishedUrlProvider(umbracoContext, urlProvider);
SetDomains5();
var url = publishedUrlProvider.GetUrl(100111, UrlMode.Absolute);
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
GlobalSettings.HideTopLevelNodeFromPath = false;
var umbracoContextAccessor = GetUmbracoContextAccessor("http://domain1.com/en/test");
UrlProvider urlProvider = GetUrlProvider(umbracoContextAccessor, requestHandlerSettings, new WebRoutingSettings(), out UriUtility uriUtility);
var url = urlProvider.GetUrl(100111, UrlMode.Absolute);
Assert.AreEqual("http://domain1.com/en/1001-1-1/", url);
var result = publishedUrlProvider.GetOtherUrls(100111).ToArray();
var result = urlProvider.GetOtherUrls(100111).ToArray();
foreach (var x in result) Console.WriteLine(x);
foreach (var x in result)
Console.WriteLine(x);
Assert.AreEqual(2, result.Length);
Assert.AreEqual(result[0].Text, "http://domain1b.com/en/1001-1-1/");
Assert.AreEqual(result[1].Text, "http://domain1a.com/en/1001-1-1/");
}
private IPublishedUrlProvider GetPublishedUrlProvider(IUmbracoContext umbracoContext, DefaultUrlProvider urlProvider)
{
var webRoutingSettings = new WebRoutingSettings();
return new UrlProvider(
new TestUmbracoContextAccessor(umbracoContext),
Microsoft.Extensions.Options.Options.Create(webRoutingSettings),
new UrlProviderCollection(new []{urlProvider}),
new MediaUrlProviderCollection(Enumerable.Empty<IMediaUrlProvider>()),
Mock.Of<IVariationContextAccessor>()
);
}
}
}

View File

@@ -5,17 +5,19 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Tests.Common;
using Umbraco.Extensions;
using Umbraco.Tests.LegacyXmlPublishedCache;
namespace Umbraco.Tests.Routing
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing
{
[TestFixture]
public class UrlsWithNestedDomains : UrlRoutingTestBase
@@ -25,61 +27,60 @@ namespace Umbraco.Tests.Routing
// using the closest domain to the node - here we test that if we request
// a non-canonical route, it is not cached / the cache is not polluted
protected override void Compose()
{
base.Compose();
Builder.Services.AddUnique(Mock.Of<IDomainService>());
Builder.Services.AddTransient<ISiteDomainMapper, SiteDomainMapper>();
}
[Test]
public async Task DoNotPolluteCache()
{
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = true };
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = false };
GlobalSettings.HideTopLevelNodeFromPath = false;
SetDomains1();
const string url = "http://domain1.com/1001-1/1001-1-1";
// get the nice URL for 100111
var umbracoContext = GetUmbracoContext(url, 9999, globalSettings: globalSettings);
var umbracoContextAccessor = GetUmbracoContextAccessor(umbracoContext);
var umbracoContextAccessor = GetUmbracoContextAccessor(url);
var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
var urlProvider = new DefaultUrlProvider(
Microsoft.Extensions.Options.Options.Create(requestHandlerSettings),
LoggerFactory.CreateLogger<DefaultUrlProvider>(),
new SiteDomainMapper(), umbracoContextAccessor, UriUtility);
Mock.Of<ILogger<DefaultUrlProvider>>(),
new SiteDomainMapper(),
umbracoContextAccessor,
new UriUtility(Mock.Of<IHostingEnvironment>()),
Mock.Of<ILocalizationService>());
var publishedUrlProvider = GetPublishedUrlProvider(umbracoContext, urlProvider);
Assert.AreEqual("http://domain2.com/1001-1-1/", publishedUrlProvider.GetUrl(100111, UrlMode.Absolute));
string absUrl = publishedUrlProvider.GetUrl(100111, UrlMode.Absolute);
Assert.AreEqual("http://domain2.com/1001-1-1/", absUrl);
const string cacheKeyPrefix = "NuCache.ContentCache.RouteByContent";
// check that the proper route has been cached
var cache = umbracoContext.Content as PublishedContentCache;
if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported.");
var cachedRoutes = cache.RoutesCache.GetCachedRoutes();
Assert.AreEqual("10011/1001-1-1", cachedRoutes[100111]);
var cache = (FastDictionaryAppCache)umbracoContext.PublishedSnapshot.ElementsCache;
var cachedRoutes = cache.Keys.Where(x => x.StartsWith(cacheKeyPrefix)).ToList();
var cacheKey = $"{cacheKeyPrefix}[P:100111]";
Assert.AreEqual("10011/1001-1-1", cache.Get(cacheKey));
// route a rogue URL
var publishedRouter = CreatePublishedRouter(umbracoContextAccessor);
var frequest = await publishedRouter .CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
publishedRouter.FindDomain(frequest);
Assert.IsTrue(frequest.HasDomain());
// check that it's been routed
var lookup = new ContentFinderByUrl(LoggerFactory.CreateLogger<ContentFinderByUrl>(), GetUmbracoContextAccessor(umbracoContext));
var lookup = new ContentFinderByUrl(Mock.Of<ILogger<ContentFinderByUrl>>(), umbracoContextAccessor);
var result = lookup.TryFindContent(frequest);
Assert.IsTrue(result);
Assert.AreEqual(100111, frequest.PublishedContent.Id);
// has the cache been polluted?
cachedRoutes = cache.RoutesCache.GetCachedRoutes();
Assert.AreEqual("10011/1001-1-1", cachedRoutes[100111]); // no
//Assert.AreEqual("1001/1001-1/1001-1-1", cachedRoutes[100111]); // yes
cachedRoutes = cache.Keys.Where(x => x.StartsWith(cacheKeyPrefix)).ToList();
Assert.AreEqual("10011/1001-1-1", cache.Get(cacheKey)); // no
// what's the nice URL now?
Assert.AreEqual("http://domain2.com/1001-1-1/", publishedUrlProvider.GetUrl(100111)); // good
//Assert.AreEqual("http://domain1.com/1001-1/1001-1-1", routingContext.NiceUrlProvider.GetNiceUrl(100111, true)); // bad
}
private IPublishedUrlProvider GetPublishedUrlProvider(IUmbracoContext umbracoContext, object urlProvider)
@@ -87,14 +88,16 @@ namespace Umbraco.Tests.Routing
throw new NotImplementedException();
}
void SetDomains1()
private void SetDomains1()
{
SetupDomainServiceMock(new[]
{
new UmbracoDomain("http://domain1.com/") {Id = 1, LanguageId = LangEngId, RootContentId = 1001, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain2.com/") {Id = 1, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US"}
});
var domainService = Mock.Get(DomainService);
domainService.Setup(service => service.GetAll(It.IsAny<bool>()))
.Returns((bool incWildcards) => new[]
{
new UmbracoDomain("http://domain1.com/") {Id = 1, LanguageId = LangEngId, RootContentId = 1001, LanguageIsoCode = "en-US"},
new UmbracoDomain("http://domain2.com/") {Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US"}
});
}
private IPublishedUrlProvider GetPublishedUrlProvider(IUmbracoContext umbracoContext, DefaultUrlProvider urlProvider)
@@ -103,15 +106,14 @@ namespace Umbraco.Tests.Routing
return new UrlProvider(
new TestUmbracoContextAccessor(umbracoContext),
Microsoft.Extensions.Options.Options.Create(webRoutingSettings),
new UrlProviderCollection(new []{urlProvider}),
new MediaUrlProviderCollection(Enumerable.Empty<IMediaUrlProvider>()),
new UrlProviderCollection(() => new[] { urlProvider }),
new MediaUrlProviderCollection(() => Enumerable.Empty<IMediaUrlProvider>()),
Mock.Of<IVariationContextAccessor>()
);
}
protected override string GetXmlContent(int templateId)
{
return @"<?xml version=""1.0"" encoding=""utf-8""?>
=> @"<?xml version=""1.0"" encoding=""utf-8""?>
<!DOCTYPE root[
<!ELEMENT Doc ANY>
<!ATTLIST Doc id ID #REQUIRED>
@@ -190,6 +192,5 @@ namespace Umbraco.Tests.Routing
</Doc>
</Doc>
</root>";
}
}
}

View File

@@ -1,12 +1,12 @@
using Moq;
using Moq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Web.PublishedCache.NuCache.DataSource;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
namespace Umbraco.Tests.PublishedContent
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache
{
[TestFixture]
public class ContentSerializationTests
@@ -77,9 +77,12 @@ namespace Umbraco.Tests.PublishedContent
{
public override int Compare(CultureVariation x, CultureVariation y)
{
if (x == null && y == null) return 0;
if (x == null && y != null) return -1;
if (x != null && y == null) return 1;
if (x == null && y == null)
return 0;
if (x == null && y != null)
return -1;
if (x != null && y == null)
return 1;
return x.Date.CompareTo(y.Date) | x.IsDraft.CompareTo(y.IsDraft) | x.Name.CompareTo(y.Name) | x.UrlSegment.CompareTo(y.UrlSegment);
}
@@ -89,9 +92,12 @@ namespace Umbraco.Tests.PublishedContent
{
public override int Compare(PropertyData x, PropertyData y)
{
if (x == null && y == null) return 0;
if (x == null && y != null) return -1;
if (x != null && y == null) return 1;
if (x == null && y == null)
return 0;
if (x == null && y != null)
return -1;
if (x != null && y == null)
return 1;
var xVal = x.Value?.ToString() ?? string.Empty;
var yVal = y.Value?.ToString() ?? string.Empty;

View File

@@ -0,0 +1,85 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PublishedCache;
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
{
[TestFixture]
public class PublishContentCacheTests : PublishedSnapshotServiceTestBase
{
private IPublishedContentCache _cache;
[SetUp]
public override void Setup()
{
base.Setup();
string xml = PublishedContentXml.PublishContentCacheTestsXml();
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
// configure the Home content type to be composed of another for tests.
var compositionType = new ContentType(TestHelper.ShortStringHelper, -1)
{
Alias = "MyCompositionAlias"
};
contentTypes.First(x => x.Alias == "Home").AddContentType(compositionType);
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
_cache = GetPublishedSnapshot().Content;
}
[Test]
public void Has_Content()
{
Assert.IsTrue(_cache.HasContent());
}
[Test]
public void Get_Root_Docs()
{
var result = _cache.GetAtRoot();
Assert.AreEqual(2, result.Count());
Assert.AreEqual(1046, result.ElementAt(0).Id);
Assert.AreEqual(1172, result.ElementAt(1).Id);
}
[TestCase("/", 1046)]
[TestCase("/home", 1046)]
[TestCase("/Home", 1046)] //test different cases
[TestCase("/home/sub1", 1173)]
[TestCase("/Home/sub1", 1173)]
[TestCase("/home/Sub1", 1173)] //test different cases
[TestCase("/home/Sub'Apostrophe", 1177)]
public void Get_Node_By_Route(string route, int nodeId)
{
var result = _cache.GetByRoute(route, false);
Assert.IsNotNull(result);
Assert.AreEqual(nodeId, result.Id);
}
[TestCase("/", 1046)]
[TestCase("/sub1", 1173)]
[TestCase("/Sub1", 1173)]
public void Get_Node_By_Route_Hiding_Top_Level_Nodes(string route, int nodeId)
{
var result = _cache.GetByRoute(route, true);
Assert.IsNotNull(result);
Assert.AreEqual(nodeId, result.Id);
}
}
}

View File

@@ -0,0 +1,200 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache
{
/// <summary>
/// Unit tests for IPublishedContent and extensions
/// </summary>
[TestFixture]
public class PublishedContentDataTableTests : PublishedSnapshotServiceTestBase
{
private readonly DataType[] _dataTypes = GetDefaultDataTypes();
private static ContentType CreateContentType(string name, IDataType dataType, IReadOnlyDictionary<string, string> propertyAliasesAndNames)
{
var contentType = new ContentType(TestHelper.ShortStringHelper, -1)
{
Alias = name,
Name = name,
Key = Guid.NewGuid(),
Id = name.GetHashCode()
};
foreach(var prop in propertyAliasesAndNames)
{
contentType.AddPropertyType(new PropertyType(TestHelper.ShortStringHelper, dataType, prop.Key)
{
Name = prop.Value
});
}
return contentType;
}
private IEnumerable<ContentNodeKit> CreateCache(
bool createChildren,
IDataType dataType,
out ContentType[] contentTypes)
{
var result = new List<ContentNodeKit>();
var valueCounter = 1;
var parentId = 3;
var properties = new Dictionary<string, string>
{
["property1"] = "Property 1",
["property2"] = "Property 2",
};
ContentType parentContentType = CreateContentType("Parent", dataType, new Dictionary<string, string>(properties)
{
["property3"] = "Property 3"
});
ContentType childContentType = CreateContentType("Child", dataType, new Dictionary<string, string>(properties)
{
["property4"] = "Property 4"
});
ContentType child2ContentType = CreateContentType("Child2", dataType, new Dictionary<string, string>(properties)
{
["property4"] = "Property 4"
});
contentTypes = new[] { parentContentType, childContentType, child2ContentType };
ContentData parentData = new ContentDataBuilder()
.WithName("Page" + Guid.NewGuid())
.WithProperties(new PropertyDataBuilder()
.WithPropertyData("property1", "value" + valueCounter)
.WithPropertyData("property2", "value" + (valueCounter + 1))
.WithPropertyData("property3", "value" + (valueCounter + 2))
.Build())
.Build();
ContentNodeKit parent = ContentNodeKitBuilder.CreateWithContent(
parentContentType.Id,
parentId, $"-1,{parentId}",
draftData: parentData,
publishedData: parentData);
result.Add(parent);
if (createChildren)
{
for (int i = 0; i < 3; i++)
{
valueCounter += 3;
var childId = parentId + i + 1;
ContentData childData = new ContentDataBuilder()
.WithName("Page" + Guid.NewGuid())
.WithProperties(new PropertyDataBuilder()
.WithPropertyData("property1", "value" + valueCounter)
.WithPropertyData("property2", "value" + (valueCounter + 1))
.WithPropertyData("property4", "value" + (valueCounter + 2))
.Build())
.Build();
ContentNodeKit child = ContentNodeKitBuilder.CreateWithContent(
i > 0 ? childContentType.Id : child2ContentType.Id,
childId, $"-1,{parentId},{childId}", i,
draftData: childData,
publishedData: childData);
result.Add(child);
}
}
return result;
}
[Test]
public void To_DataTable()
{
var cache = CreateCache(true, _dataTypes[0], out ContentType[] contentTypes);
InitializedCache(cache, contentTypes, dataTypes: _dataTypes);
var snapshot = GetPublishedSnapshot();
var root = snapshot.Content.GetAtRoot().First();
var dt = root.ChildrenAsTable(
VariationContextAccessor,
ContentTypeService,
MediaTypeService,
Mock.Of<IMemberTypeService>(),
Mock.Of<IPublishedUrlProvider>());
Assert.AreEqual(11, dt.Columns.Count);
Assert.AreEqual(3, dt.Rows.Count);
Assert.AreEqual("value4", dt.Rows[0]["Property 1"]);
Assert.AreEqual("value5", dt.Rows[0]["Property 2"]);
Assert.AreEqual("value6", dt.Rows[0]["Property 4"]);
Assert.AreEqual("value7", dt.Rows[1]["Property 1"]);
Assert.AreEqual("value8", dt.Rows[1]["Property 2"]);
Assert.AreEqual("value9", dt.Rows[1]["Property 4"]);
Assert.AreEqual("value10", dt.Rows[2]["Property 1"]);
Assert.AreEqual("value11", dt.Rows[2]["Property 2"]);
Assert.AreEqual("value12", dt.Rows[2]["Property 4"]);
}
[Test]
public void To_DataTable_With_Filter()
{
var cache = CreateCache(true, _dataTypes[0], out ContentType[] contentTypes);
InitializedCache(cache, contentTypes, dataTypes: _dataTypes);
var snapshot = GetPublishedSnapshot();
var root = snapshot.Content.GetAtRoot().First();
var dt = root.ChildrenAsTable(
VariationContextAccessor,
ContentTypeService,
MediaTypeService,
Mock.Of<IMemberTypeService>(),
Mock.Of<IPublishedUrlProvider>(),
"Child");
Assert.AreEqual(11, dt.Columns.Count);
Assert.AreEqual(2, dt.Rows.Count);
Assert.AreEqual("value7", dt.Rows[0]["Property 1"]);
Assert.AreEqual("value8", dt.Rows[0]["Property 2"]);
Assert.AreEqual("value9", dt.Rows[0]["Property 4"]);
Assert.AreEqual("value10", dt.Rows[1]["Property 1"]);
Assert.AreEqual("value11", dt.Rows[1]["Property 2"]);
Assert.AreEqual("value12", dt.Rows[1]["Property 4"]);
}
[Test]
public void To_DataTable_No_Rows()
{
var cache = CreateCache(false, _dataTypes[0], out ContentType[] contentTypes);
InitializedCache(cache, contentTypes, dataTypes: _dataTypes);
var snapshot = GetPublishedSnapshot();
var root = snapshot.Content.GetAtRoot().First();
var dt = root.ChildrenAsTable(
VariationContextAccessor,
ContentTypeService,
MediaTypeService,
Mock.Of<IMemberTypeService>(),
Mock.Of<IPublishedUrlProvider>());
//will return an empty data table
Assert.AreEqual(0, dt.Columns.Count);
Assert.AreEqual(0, dt.Rows.Count);
}
}
}

View File

@@ -0,0 +1,77 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Tests.Common.Published;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache
{
[TestFixture]
public class PublishedContentExtensionTests : PublishedSnapshotServiceTestBase
{
private const string XmlContent = @"<?xml version=""1.0"" encoding=""utf-8""?>
<!DOCTYPE root[
<!ELEMENT inherited ANY>
<!ATTLIST inherited id ID #REQUIRED>
]>
<root id=""-1"">
<inherited id=""1100"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1"" sortOrder=""1"" createDate=""2012-06-12T14:13:17"" updateDate=""2012-07-20T18:50:43"" nodeName=""Home"" urlName=""home"" writerName=""admin"" creatorName=""admin"" path=""-1,1046"" isDoc=""""/>
</root>";
[SetUp]
public override void Setup()
{
base.Setup();
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
XmlContent,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
// configure inheritance for content types
var baseType = new ContentType(TestHelper.ShortStringHelper, -1) { Alias = "Base" };
contentTypes[0].AddContentType(baseType);
InitializedCache(kits, contentTypes, dataTypes);
}
[Test]
public void IsDocumentType_NonRecursive_ActualType_ReturnsTrue()
{
var publishedContent = GetContent(1100);
Assert.That(publishedContent.IsDocumentType("Inherited", false));
}
[Test]
public void IsDocumentType_NonRecursive_BaseType_ReturnsFalse()
{
var publishedContent = GetContent(1100);
Assert.That(publishedContent.IsDocumentType("Base", false), Is.False);
}
[Test]
public void IsDocumentType_Recursive_ActualType_ReturnsTrue()
{
var publishedContent = GetContent(1100);
Assert.That(publishedContent.IsDocumentType("Inherited", true));
}
[Test]
public void IsDocumentType_Recursive_BaseType_ReturnsTrue()
{
var publishedContent = GetContent(1100);
Assert.That(publishedContent.IsDocumentType("Base", true));
}
[Test]
public void IsDocumentType_Recursive_InvalidBaseType_ReturnsFalse()
{
var publishedContent = GetContent(1100);
Assert.That(publishedContent.IsDocumentType("invalidbase", true), Is.False);
}
}
}

View File

@@ -0,0 +1,346 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache
{
[TestFixture]
public class PublishedContentLanguageVariantTests : PublishedSnapshotServiceTestBase
{
[SetUp]
public override void Setup()
{
base.Setup();
var dataTypes = GetDefaultDataTypes();
var cache = CreateCache(dataTypes, out ContentType[] contentTypes);
InitializedCache(cache, contentTypes, dataTypes: dataTypes);
}
protected override PropertyValueConverterCollection PropertyValueConverterCollection
{
get
{
PropertyValueConverterCollection collection = base.PropertyValueConverterCollection;
return new PropertyValueConverterCollection(() => collection.Append(new TestNoValueValueConverter()));
}
}
private class TestNoValueValueConverter : SimpleTinyMceValueConverter
{
public override bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.Alias == "noprop";
// for this test, we return false for IsValue for this property
public override bool? IsValue(object value, PropertyValueLevel level) => false;
}
/// <summary>
/// Override to mock localization service
/// </summary>
/// <param name="contentTypes"></param>
/// <param name="dataTypes"></param>
/// <returns></returns>
protected override ServiceContext CreateServiceContext(IContentType[] contentTypes, IMediaType[] mediaTypes, IDataType[] dataTypes)
{
var serviceContext = base.CreateServiceContext(contentTypes, mediaTypes, dataTypes);
var localizationService = Mock.Get(serviceContext.LocalizationService);
var languages = new List<Language>
{
new Language(GlobalSettings, "en-US") { Id = 1, CultureName = "English", IsDefault = true },
new Language(GlobalSettings, "fr") { Id = 2, CultureName = "French" },
new Language(GlobalSettings, "es") { Id = 3, CultureName = "Spanish", FallbackLanguageId = 1 },
new Language(GlobalSettings, "it") { Id = 4, CultureName = "Italian", FallbackLanguageId = 3 },
new Language(GlobalSettings, "de") { Id = 5, CultureName = "German" },
new Language(GlobalSettings, "da") { Id = 6, CultureName = "Danish", FallbackLanguageId = 8 },
new Language(GlobalSettings, "sv") { Id = 7, CultureName = "Swedish", FallbackLanguageId = 6 },
new Language(GlobalSettings, "no") { Id = 8, CultureName = "Norweigan", FallbackLanguageId = 7 },
new Language(GlobalSettings, "nl") { Id = 9, CultureName = "Dutch", FallbackLanguageId = 1 }
};
localizationService.Setup(x => x.GetAllLanguages()).Returns(languages);
localizationService.Setup(x => x.GetLanguageById(It.IsAny<int>()))
.Returns((int id) => languages.SingleOrDefault(y => y.Id == id));
localizationService.Setup(x => x.GetLanguageByIsoCode(It.IsAny<string>()))
.Returns((string c) => languages.SingleOrDefault(y => y.IsoCode == c));
return serviceContext;
}
/// <summary>
/// Creates a content cache
/// </summary>
/// <param name="dataTypes"></param>
/// <param name="contentTypes"></param>
/// <returns></returns>
/// <remarks>
/// Builds a content hierarchy of 3 nodes, each has a different set of cultural properties.
/// The first 2 share the same content type, the last one is a different content type.
/// NOTE: The content items themselves are 'Invariant' but their properties are 'Variant' by culture.
/// Normally in Umbraco this is prohibited but our APIs and database do actually support that behavior.
/// It is simpler to have these tests run this way, else we would need to use WithCultureInfos
/// for each item and pass in name values for all cultures we are supporting and then specify the
/// default VariationContextAccessor.VariationContext value to be a default culture instead of "".
/// </remarks>
private IEnumerable<ContentNodeKit> CreateCache(IDataType[] dataTypes, out ContentType[] contentTypes)
{
var result = new List<ContentNodeKit>();
var propertyDataTypes = new Dictionary<string, IDataType>
{
// we only have one data type for this test which will be resolved with string empty.
[string.Empty] = dataTypes[0]
};
var contentType1 = new ContentType(ShortStringHelper, -1);
ContentData item1Data = new ContentDataBuilder()
.WithName("Content 1")
.WithProperties(new PropertyDataBuilder()
.WithPropertyData("welcomeText", "Welcome")
.WithPropertyData("welcomeText", "Welcome", "en-US")
.WithPropertyData("welcomeText", "Willkommen", "de")
.WithPropertyData("welcomeText", "Welkom", "nl")
.WithPropertyData("welcomeText2", "Welcome")
.WithPropertyData("welcomeText2", "Welcome", "en-US")
.WithPropertyData("noprop", "xxx")
.Build())
// build with a dynamically created content type
.Build(ShortStringHelper, propertyDataTypes, contentType1, "ContentType1");
ContentNodeKit item1 = ContentNodeKitBuilder.CreateWithContent(
contentType1.Id,
1, "-1,1",
draftData: item1Data,
publishedData: item1Data);
result.Add(item1);
ContentData item2Data = new ContentDataBuilder()
.WithName("Content 2")
.WithProperties(new PropertyDataBuilder()
.WithPropertyData("welcomeText", "Welcome")
.WithPropertyData("welcomeText", "Welcome", "en-US")
.WithPropertyData("noprop", "xxx")
.Build())
// build while dynamically updating the same content type
.Build(ShortStringHelper, propertyDataTypes, contentType1);
ContentNodeKit item2 = ContentNodeKitBuilder.CreateWithContent(
contentType1.Id,
2, "-1,1,2",
parentContentId: 1,
draftData: item2Data,
publishedData: item2Data);
result.Add(item2);
var contentType2 = new ContentType(ShortStringHelper, -1);
ContentData item3Data = new ContentDataBuilder()
.WithName("Content 3")
.WithProperties(new PropertyDataBuilder()
.WithPropertyData("prop3", "Oxxo")
.WithPropertyData("prop3", "Oxxo", "en-US")
.Build())
// build with a dynamically created content type
.Build(ShortStringHelper, propertyDataTypes, contentType2, "ContentType2");
ContentNodeKit item3 = ContentNodeKitBuilder.CreateWithContent(
contentType2.Id,
3, "-1,1,2,3",
parentContentId: 2,
draftData: item3Data,
publishedData: item3Data);
result.Add(item3);
contentTypes = new[] { contentType1, contentType2 };
return result;
}
[Test]
public void Can_Get_Content_For_Populated_Requested_Language()
{
var snapshot = GetPublishedSnapshot();
var content = snapshot.Content.GetAtRoot().First();
var value = content.Value(Mock.Of<IPublishedValueFallback>(), "welcomeText", "en-US");
Assert.AreEqual("Welcome", value);
}
[Test]
public void Can_Get_Content_For_Populated_Requested_Non_Default_Language()
{
var snapshot = GetPublishedSnapshot();
var content = snapshot.Content.GetAtRoot().First();
var value = content.Value(Mock.Of<IPublishedValueFallback>(), "welcomeText", "de");
Assert.AreEqual("Willkommen", value);
}
[Test]
public void Do_Not_Get_Content_For_Unpopulated_Requested_Language_Without_Fallback()
{
var snapshot = GetPublishedSnapshot();
var content = snapshot.Content.GetAtRoot().First();
var value = content.Value(Mock.Of<IPublishedValueFallback>(), "welcomeText", "fr");
Assert.IsNull(value);
}
[Test]
public void Do_Not_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Unless_Requested()
{
var snapshot = GetPublishedSnapshot();
var content = snapshot.Content.GetAtRoot().First();
var value = content.Value(Mock.Of<IPublishedValueFallback>(), "welcomeText", "es");
Assert.IsNull(value);
}
[Test]
public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback()
{
var snapshot = GetPublishedSnapshot();
var content = snapshot.Content.GetAtRoot().First();
var value = content.Value(PublishedValueFallback, "welcomeText", "es", fallback: Fallback.ToLanguage);
Assert.AreEqual("Welcome", value);
}
[Test]
public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Over_Two_Levels()
{
var snapshot = GetPublishedSnapshot();
var content = snapshot.Content.GetAtRoot().First();
var value = content.Value(PublishedValueFallback, "welcomeText", "it", fallback: Fallback.To(Fallback.Language, Fallback.Ancestors));
Assert.AreEqual("Welcome", value);
}
[Test]
public void Do_Not_GetContent_For_Unpopulated_Requested_Language_With_Fallback_Over_That_Loops()
{
var snapshot = GetPublishedSnapshot();
var content = snapshot.Content.GetAtRoot().First();
var value = content.Value(Mock.Of<IPublishedValueFallback>(), "welcomeText", "no", fallback: Fallback.ToLanguage);
Assert.IsNull(value);
}
[Test]
public void Do_Not_Get_Content_Recursively_Unless_Requested()
{
var snapshot = GetPublishedSnapshot();
var content = snapshot.Content.GetAtRoot().First().Children.First();
var value = content.Value(Mock.Of<IPublishedValueFallback>(), "welcomeText2");
Assert.IsNull(value);
}
[Test]
public void Can_Get_Content_Recursively()
{
var snapshot = GetPublishedSnapshot();
var content = snapshot.Content.GetAtRoot().First().Children.First();
var value = content.Value(PublishedValueFallback, "welcomeText2", fallback: Fallback.ToAncestors);
Assert.AreEqual("Welcome", value);
}
[Test]
public void Do_Not_Get_Content_Recursively_Unless_Requested2()
{
var snapshot = GetPublishedSnapshot();
var content = snapshot.Content.GetAtRoot().First().Children.First().Children.First();
Assert.IsNull(content.GetProperty("welcomeText2"));
var value = content.Value(Mock.Of<IPublishedValueFallback>(), "welcomeText2");
Assert.IsNull(value);
}
[Test]
public void Can_Get_Content_Recursively2()
{
var snapshot = GetPublishedSnapshot();
var content = snapshot.Content.GetAtRoot().First().Children.First().Children.First();
Assert.IsNull(content.GetProperty("welcomeText2"));
var value = content.Value(PublishedValueFallback, "welcomeText2", fallback: Fallback.ToAncestors);
Assert.AreEqual("Welcome", value);
}
[Test]
public void Can_Get_Content_Recursively3()
{
var snapshot = GetPublishedSnapshot();
var content = snapshot.Content.GetAtRoot().First().Children.First().Children.First();
Assert.IsNull(content.GetProperty("noprop"));
var value = content.Value(PublishedValueFallback, "noprop", fallback: Fallback.ToAncestors);
// property has no value - based on the converter
// but we still get the value (ie, the converter would do something)
Assert.AreEqual("xxx", value.ToString());
}
[Test]
public void Can_Get_Content_With_Recursive_Priority()
{
VariationContextAccessor.VariationContext = new VariationContext("nl");
var snapshot = GetPublishedSnapshot();
var content = snapshot.Content.GetAtRoot().First().Children.First();
var value = content.Value(PublishedValueFallback, "welcomeText", "nl", fallback: Fallback.To(Fallback.Ancestors, Fallback.Language));
// No Dutch value is directly assigned. Check has fallen back to Dutch value from parent.
Assert.AreEqual("Welkom", value);
}
[Test]
public void Can_Get_Content_With_Fallback_Language_Priority()
{
var snapshot = GetPublishedSnapshot();
var content = snapshot.Content.GetAtRoot().First().Children.First();
var value = content.Value(PublishedValueFallback, "welcomeText", "nl", fallback: Fallback.ToLanguage);
// No Dutch value is directly assigned. Check has fallen back to English value from language variant.
Assert.AreEqual("Welcome", value);
}
[Test]
public void Throws_For_Non_Supported_Fallback()
{
var snapshot = GetPublishedSnapshot();
var content = snapshot.Content.GetAtRoot().First().Children.First();
Assert.Throws<NotSupportedException>(() => content.Value(PublishedValueFallback, "welcomeText", "nl", fallback: Fallback.To(999)));
}
[Test]
public void Can_Fallback_To_Default_Value()
{
var snapshot = GetPublishedSnapshot();
var content = snapshot.Content.GetAtRoot().First().Children.First();
// no Dutch value is assigned, so getting null
var value = content.Value(PublishedValueFallback, "welcomeText", "nl");
Assert.IsNull(value);
// even if we 'just' provide a default value
value = content.Value(PublishedValueFallback, "welcomeText", "nl", defaultValue: "woop");
Assert.IsNull(value);
// but it works with proper fallback settings
value = content.Value(PublishedValueFallback, "welcomeText", "nl", fallback: Fallback.ToDefaultValue, defaultValue: "woop");
Assert.AreEqual("woop", value);
}
}
}

View File

@@ -1,193 +1,108 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Core.Templates;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.Serialization;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Tests.Common.Published;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
using Umbraco.Extensions;
using Umbraco.Tests.TestHelpers;
using Umbraco.Web.Composing;
namespace Umbraco.Tests.PublishedContent
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache
{
/// <summary>
/// Tests the methods on IPublishedContent using the DefaultPublishedContentStore
/// </summary>
[TestFixture]
[UmbracoTest(TypeLoader = UmbracoTestOptions.TypeLoader.PerFixture)]
public class PublishedContentTests : PublishedContentTestBase
public class PublishedContentTests : PublishedSnapshotServiceTestBase
{
protected override void Compose()
{
base.Compose();
_publishedSnapshotAccessorMock = new Mock<IPublishedSnapshotAccessor>();
Builder.Services.AddUnique<IPublishedSnapshotAccessor>(_publishedSnapshotAccessorMock.Object);
Builder.Services.AddUnique<IPublishedModelFactory>(f => new PublishedModelFactory(f.GetRequiredService<TypeLoader>().GetTypes<PublishedContentModel>(), f.GetRequiredService<IPublishedValueFallback>()));
Builder.Services.AddUnique<IPublishedContentTypeFactory, PublishedContentTypeFactory>();
Builder.Services.AddUnique<IPublishedValueFallback, PublishedValueFallback>();
var loggerFactory = NullLoggerFactory.Instance;
var mediaService = Mock.Of<IMediaService>();
var contentTypeBaseServiceProvider = Mock.Of<IContentTypeBaseServiceProvider>();
var umbracoContextAccessor = Mock.Of<IUmbracoContextAccessor>();
var backOfficeSecurityAccessor = Mock.Of<IBackOfficeSecurityAccessor>();
var publishedUrlProvider = Mock.Of<IPublishedUrlProvider>();
var imageSourceParser = new HtmlImageSourceParser(publishedUrlProvider);
var serializer = new ConfigurationEditorJsonSerializer();
var mediaFileService = new MediaFileManager(Mock.Of<IFileSystem>(), Mock.Of<IMediaPathScheme>(),
loggerFactory.CreateLogger<MediaFileManager>(), Mock.Of<IShortStringHelper>());
var pastedImages = new RichTextEditorPastedImages(umbracoContextAccessor, loggerFactory.CreateLogger<RichTextEditorPastedImages>(), HostingEnvironment, mediaService, contentTypeBaseServiceProvider, mediaFileService, ShortStringHelper, publishedUrlProvider, serializer);
var linkParser = new HtmlLocalLinkParser(umbracoContextAccessor, publishedUrlProvider);
var dataTypeService = new TestObjects.TestDataTypeService(
new DataType(new VoidEditor(DataValueEditorFactory), serializer) { Id = 1 },
new DataType(new TrueFalsePropertyEditor(DataValueEditorFactory, IOHelper), serializer) { Id = 1001 },
new DataType(new RichTextPropertyEditor(DataValueEditorFactory, backOfficeSecurityAccessor, imageSourceParser, linkParser, pastedImages, IOHelper, Mock.Of<IImageUrlGenerator>()), serializer) { Id = 1002 },
new DataType(new IntegerPropertyEditor(DataValueEditorFactory), serializer) { Id = 1003 },
new DataType(new TextboxPropertyEditor(DataValueEditorFactory, IOHelper), serializer) { Id = 1004 },
new DataType(new MediaPickerPropertyEditor(DataValueEditorFactory, IOHelper), serializer) { Id = 1005 });
Builder.Services.AddUnique<IDataTypeService>(f => dataTypeService);
}
protected override void Initialize()
{
base.Initialize();
var factory = Factory.GetRequiredService<IPublishedContentTypeFactory>() as PublishedContentTypeFactory;
// need to specify a custom callback for unit tests
// AutoPublishedContentTypes generates properties automatically
// when they are requested, but we must declare those that we
// explicitely want to be here...
IEnumerable<IPublishedPropertyType> CreatePropertyTypes(IPublishedContentType contentType)
{
// AutoPublishedContentType will auto-generate other properties
yield return factory.CreatePropertyType(contentType, "umbracoNaviHide", 1001);
yield return factory.CreatePropertyType(contentType, "selectedNodes", 1);
yield return factory.CreatePropertyType(contentType, "umbracoUrlAlias", 1);
yield return factory.CreatePropertyType(contentType, "content", 1002);
yield return factory.CreatePropertyType(contentType, "testRecursive", 1);
}
var compositionAliases = new[] { "MyCompositionAlias" };
var anythingType = new AutoPublishedContentType(Guid.NewGuid(), 0, "anything", compositionAliases, CreatePropertyTypes);
var homeType = new AutoPublishedContentType(Guid.NewGuid(), 0, "home", compositionAliases, CreatePropertyTypes);
ContentTypesCache.GetPublishedContentTypeByAlias = alias => alias.InvariantEquals("home") ? homeType : anythingType;
}
protected override TypeLoader CreateTypeLoader(IIOHelper ioHelper, ITypeFinder typeFinder, IAppPolicyCache runtimeCache, ILogger<TypeLoader> logger, IProfilingLogger profilingLogger , IHostingEnvironment hostingEnvironment)
{
var baseLoader = base.CreateTypeLoader(ioHelper, typeFinder, runtimeCache, logger, profilingLogger , hostingEnvironment);
return new TypeLoader(typeFinder, runtimeCache, new DirectoryInfo(hostingEnvironment.LocalTempPath), logger, profilingLogger , false,
// this is so the model factory looks into the test assembly
baseLoader.AssembliesToScan
.Union(new[] { typeof(PublishedContentTests).Assembly })
.ToList());
}
private readonly Guid _node1173Guid = Guid.NewGuid();
private Mock<IPublishedSnapshotAccessor> _publishedSnapshotAccessorMock;
private PublishedModelFactory _publishedModelFactory;
private DataType[] _dataTypes;
protected override string GetXmlContent(int templateId)
[SetUp]
public override void Setup()
{
return @"<?xml version=""1.0"" encoding=""utf-8""?>
<!DOCTYPE root[
<!ELEMENT Home ANY>
<!ATTLIST Home id ID #REQUIRED>
<!ELEMENT CustomDocument ANY>
<!ATTLIST CustomDocument id ID #REQUIRED>
]>
<root id=""-1"">
<Home id=""1046"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""1"" createDate=""2012-06-12T14:13:17"" updateDate=""2012-07-20T18:50:43"" nodeName=""Home"" urlName=""home"" writerName=""admin"" creatorName=""admin"" path=""-1,1046"" isDoc="""">
<content><![CDATA[]]></content>
<umbracoUrlAlias><![CDATA[this/is/my/alias, anotheralias]]></umbracoUrlAlias>
<umbracoNaviHide>1</umbracoNaviHide>
<testRecursive><![CDATA[This is the recursive val]]></testRecursive>
<Home id=""1173"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""1"" createDate=""2012-07-20T18:06:45"" updateDate=""2012-07-20T19:07:31"" nodeName=""Sub1"" urlName=""sub1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173"" isDoc="""" key=""" + _node1173Guid + @""">
<content><![CDATA[<div>This is some content</div>]]></content>
<umbracoUrlAlias><![CDATA[page2/alias, 2ndpagealias]]></umbracoUrlAlias>
<testRecursive><![CDATA[]]></testRecursive>
<Home id=""1174"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""1"" createDate=""2012-07-20T18:07:54"" updateDate=""2012-07-20T19:10:27"" nodeName=""Sub2"" urlName=""sub2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1174"" isDoc="""">
<content><![CDATA[]]></content>
<umbracoUrlAlias><![CDATA[only/one/alias]]></umbracoUrlAlias>
<creatorName><![CDATA[Custom data with same property name as the member name]]></creatorName>
<testRecursive><![CDATA[]]></testRecursive>
</Home>
<CustomDocument id=""117"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""2"" createDate=""2018-07-18T10:06:37"" updateDate=""2018-07-18T10:06:37"" nodeName=""custom sub 1"" urlName=""custom-sub-1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,117"" isDoc="""" />
<CustomDocument id=""1177"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""3"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""custom sub 1"" urlName=""custom-sub-1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1177"" isDoc="""" />
<CustomDocument id=""1178"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""4"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-16T14:23:35"" nodeName=""custom sub 2"" urlName=""custom-sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1178"" isDoc="""">
<CustomDocument id=""1179"" parentID=""1178"" level=""4"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""1"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""custom sub sub 1"" urlName=""custom-sub-sub-1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1178,1179"" isDoc="""" />
</CustomDocument>
<Home id=""1176"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""5"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""" key=""CDB83BBC-A83B-4BA6-93B8-AADEF67D3C09"">
<content><![CDATA[]]></content>
<umbracoNaviHide>1</umbracoNaviHide>
</Home>
</Home>
<Home id=""1175"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""2"" createDate=""2012-07-20T18:08:01"" updateDate=""2012-07-20T18:49:32"" nodeName=""Sub 2"" urlName=""sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1175"" isDoc=""""><content><![CDATA[]]></content>
</Home>
<CustomDocument id=""4444"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""3"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""Test"" urlName=""test-page"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,4444"" isDoc="""">
<selectedNodes><![CDATA[1172,1176,1173]]></selectedNodes>
</CustomDocument>
</Home>
<CustomDocument id=""1172"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""2"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""Test"" urlName=""test-page"" writerName=""admin"" creatorName=""admin"" path=""-1,1172"" isDoc="""" />
</root>";
base.Setup();
string xml = PublishedContentXml.PublishedContentTestXml(1234, _node1173Guid);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
_dataTypes = dataTypes;
// configure the Home content type to be composed of another for tests.
var compositionType = new ContentType(TestHelper.ShortStringHelper, -1)
{
Alias = "MyCompositionAlias"
};
contentTypes.First(x => x.Alias == "Home").AddContentType(compositionType);
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
}
internal IPublishedContent GetNode(int id)
// override to specify our own factory with custom types
protected override IPublishedModelFactory PublishedModelFactory
=> _publishedModelFactory ??= new PublishedModelFactory(
new[] { typeof(Home), typeof(Anything), typeof(CustomDocument) },
PublishedValueFallback);
[PublishedModel("Home")]
internal class Home : PublishedContentModel
{
var ctx = GetUmbracoContext("/test");
var doc = ctx.Content.GetById(id);
Assert.IsNotNull(doc);
return doc;
public Home(IPublishedContent content, IPublishedValueFallback fallback)
: base(content, fallback)
{ }
public bool UmbracoNaviHide => this.Value<bool>(Mock.Of<IPublishedValueFallback>(), "umbracoNaviHide");
}
[PublishedModel("anything")]
internal class Anything : PublishedContentModel
{
public Anything(IPublishedContent content, IPublishedValueFallback fallback)
: base(content, fallback)
{ }
}
[PublishedModel("CustomDocument")]
internal class CustomDocument : PublishedContentModel
{
public CustomDocument(IPublishedContent content, IPublishedValueFallback fallback)
: base(content, fallback)
{ }
}
[Test]
public void GetNodeByIds()
{
var ctx = GetUmbracoContext("/test");
var contentById = ctx.Content.GetById(1173);
var snapshot = GetPublishedSnapshot();
var contentById = snapshot.Content.GetById(1173);
Assert.IsNotNull(contentById);
var contentByGuid = ctx.Content.GetById(_node1173Guid);
var contentByGuid = snapshot.Content.GetById(_node1173Guid);
Assert.IsNotNull(contentByGuid);
Assert.AreEqual(contentById.Id, contentByGuid.Id);
Assert.AreEqual(contentById.Key, contentByGuid.Key);
contentById = ctx.Content.GetById(666);
contentById = snapshot.Content.GetById(666);
Assert.IsNull(contentById);
contentByGuid = ctx.Content.GetById(Guid.NewGuid());
contentByGuid = snapshot.Content.GetById(Guid.NewGuid());
Assert.IsNull(contentByGuid);
}
[Test]
public void Is_Last_From_Where_Filter_Dynamic_Linq()
{
var doc = GetNode(1173);
var doc = GetContent(1173);
var items = doc.Children(VariationContextAccessor).Where(x => x.IsVisible(Mock.Of<IPublishedValueFallback>())).ToIndexedArray();
@@ -195,11 +110,11 @@ namespace Umbraco.Tests.PublishedContent
{
if (item.Content.Id != 1178)
{
Assert.IsFalse(item.IsLast());
Assert.IsFalse(item.IsLast(), $"The item {item.Content.Id} is last");
}
else
{
Assert.IsTrue(item.IsLast());
Assert.IsTrue(item.IsLast(), $"The item {item.Content.Id} is not last");
}
}
}
@@ -207,7 +122,7 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Is_Last_From_Where_Filter()
{
var doc = GetNode(1173);
var doc = GetContent(1173);
var items = doc
.Children(VariationContextAccessor)
@@ -243,30 +158,14 @@ namespace Umbraco.Tests.PublishedContent
}
}
[PublishedModel("Home")]
internal class Home : PublishedContentModel
{
public Home(IPublishedContent content, IPublishedValueFallback fallback)
: base(content, fallback)
{}
}
[PublishedModel("anything")]
internal class Anything : PublishedContentModel
{
public Anything(IPublishedContent content, IPublishedValueFallback fallback)
: base(content, fallback)
{ }
}
[Test]
public void Is_Last_From_Where_Filter2()
{
var doc = GetNode(1173);
var doc = GetContent(1173);
var ct = doc.ContentType;
var items = doc.Children(VariationContextAccessor)
.Select(x => x.CreateModel(Current.PublishedModelFactory)) // linq, returns IEnumerable<IPublishedContent>
.Select(x => x.CreateModel(PublishedModelFactory)) // linq, returns IEnumerable<IPublishedContent>
// only way around this is to make sure every IEnumerable<T> extension
// explicitely returns a PublishedContentSet, not an IEnumerable<T>
@@ -295,7 +194,7 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Is_Last_From_Take()
{
var doc = GetNode(1173);
var doc = GetContent(1173);
var items = doc.Children(VariationContextAccessor).Take(4).ToIndexedArray();
@@ -315,7 +214,7 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Is_Last_From_Skip()
{
var doc = GetNode(1173);
var doc = GetContent(1173);
foreach (var d in doc.Children(VariationContextAccessor).Skip(1).ToIndexedArray())
{
@@ -333,10 +232,10 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Is_Last_From_Concat()
{
var doc = GetNode(1173);
var doc = GetContent(1173);
var items = doc.Children(VariationContextAccessor)
.Concat(new[] { GetNode(1175), GetNode(4444) })
.Concat(new[] { GetContent(1175), GetContent(4444) })
.ToIndexedArray();
foreach (var item in items)
@@ -355,7 +254,7 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Descendants_Ordered_Properly()
{
var doc = GetNode(1046);
var doc = GetContent(1046);
var expected = new[] { 1046, 1173, 1174, 117, 1177, 1178, 1179, 1176, 1175, 4444, 1172 };
var exindex = 0;
@@ -370,9 +269,11 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Get_Property_Value_Recursive()
{
var doc = GetNode(1174);
var rVal = doc.Value(Factory.GetRequiredService<IPublishedValueFallback>(), "testRecursive", fallback: Fallback.ToAncestors);
var nullVal = doc.Value(Factory.GetRequiredService<IPublishedValueFallback>(), "DoNotFindThis", fallback: Fallback.ToAncestors);
// TODO: We need to use a different fallback?
var doc = GetContent(1174);
var rVal = doc.Value(PublishedValueFallback, "testRecursive", fallback: Fallback.ToAncestors);
var nullVal = doc.Value(PublishedValueFallback, "DoNotFindThis", fallback: Fallback.ToAncestors);
Assert.AreEqual("This is the recursive val", rVal);
Assert.AreEqual(null, nullVal);
}
@@ -380,17 +281,17 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Get_Property_Value_Uses_Converter()
{
var doc = GetNode(1173);
var doc = GetContent(1173);
var propVal = doc.Value(Mock.Of<IPublishedValueFallback>(), "content");
var propVal = doc.Value(PublishedValueFallback, "content");
Assert.IsInstanceOf(typeof(IHtmlEncodedString), propVal);
Assert.AreEqual("<div>This is some content</div>", propVal.ToString());
var propVal2 = doc.Value<IHtmlEncodedString>(Mock.Of<IPublishedValueFallback>(), "content");
var propVal2 = doc.Value<IHtmlEncodedString>(PublishedValueFallback, "content");
Assert.IsInstanceOf(typeof(IHtmlEncodedString), propVal2);
Assert.AreEqual("<div>This is some content</div>", propVal2.ToString());
var propVal3 = doc.Value(Mock.Of<IPublishedValueFallback>(), "Content");
var propVal3 = doc.Value(PublishedValueFallback, "Content");
Assert.IsInstanceOf(typeof(IHtmlEncodedString), propVal3);
Assert.AreEqual("<div>This is some content</div>", propVal3.ToString());
}
@@ -398,12 +299,12 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Complex_Linq()
{
var doc = GetNode(1173);
var doc = GetContent(1173);
var result = doc.Ancestors().OrderBy(x => x.Level)
.Single()
.Descendants(Mock.Of<IVariationContextAccessor>())
.FirstOrDefault(x => x.Value<string>(Mock.Of<IPublishedValueFallback>(), "selectedNodes", defaultValue: "").Split(',').Contains("1173"));
.FirstOrDefault(x => x.Value<string>(PublishedValueFallback, "selectedNodes", fallback: Fallback.ToDefaultValue, defaultValue: "").Split(',').Contains("1173"));
Assert.IsNotNull(result);
}
@@ -411,16 +312,16 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Children_GroupBy_DocumentTypeAlias()
{
var home = new AutoPublishedContentType(Guid.NewGuid(), 22, "Home", new PublishedPropertyType[] { });
var custom = new AutoPublishedContentType(Guid.NewGuid(), 23, "CustomDocument", new PublishedPropertyType[] { });
var contentTypes = new Dictionary<string, PublishedContentType>
{
{ home.Alias, home },
{ custom.Alias, custom }
};
ContentTypesCache.GetPublishedContentTypeByAlias = alias => contentTypes[alias];
//var home = new AutoPublishedContentType(Guid.NewGuid(), 22, "Home", new PublishedPropertyType[] { });
//var custom = new AutoPublishedContentType(Guid.NewGuid(), 23, "CustomDocument", new PublishedPropertyType[] { });
//var contentTypes = new Dictionary<string, PublishedContentType>
//{
// { home.Alias, home },
// { custom.Alias, custom }
//};
//ContentTypesCache.GetPublishedContentTypeByAlias = alias => contentTypes[alias];
var doc = GetNode(1046);
var doc = GetContent(1046);
var found1 = doc.Children(VariationContextAccessor).GroupBy(x => x.ContentType.Alias).ToArray();
@@ -432,16 +333,16 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Children_Where_DocumentTypeAlias()
{
var home = new AutoPublishedContentType(Guid.NewGuid(), 22, "Home", new PublishedPropertyType[] { });
var custom = new AutoPublishedContentType(Guid.NewGuid(), 23, "CustomDocument", new PublishedPropertyType[] { });
var contentTypes = new Dictionary<string, PublishedContentType>
{
{ home.Alias, home },
{ custom.Alias, custom }
};
ContentTypesCache.GetPublishedContentTypeByAlias = alias => contentTypes[alias];
//var home = new AutoPublishedContentType(Guid.NewGuid(), 22, "Home", new PublishedPropertyType[] { });
//var custom = new AutoPublishedContentType(Guid.NewGuid(), 23, "CustomDocument", new PublishedPropertyType[] { });
//var contentTypes = new Dictionary<string, PublishedContentType>
//{
// { home.Alias, home },
// { custom.Alias, custom }
//};
//ContentTypesCache.GetPublishedContentTypeByAlias = alias => contentTypes[alias];
var doc = GetNode(1046);
var doc = GetContent(1046);
var found1 = doc.Children(VariationContextAccessor).Where(x => x.ContentType.Alias == "CustomDocument");
var found2 = doc.Children(VariationContextAccessor).Where(x => x.ContentType.Alias == "Home");
@@ -453,7 +354,7 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Children_Order_By_Update_Date()
{
var doc = GetNode(1173);
var doc = GetContent(1173);
var ordered = doc.Children(VariationContextAccessor).OrderBy(x => x.UpdateDate);
@@ -468,12 +369,12 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void FirstChild()
{
var doc = GetNode(1173); // has child nodes
var doc = GetContent(1173); // has child nodes
Assert.IsNotNull(doc.FirstChild(Mock.Of<IVariationContextAccessor>()));
Assert.IsNotNull(doc.FirstChild(Mock.Of<IVariationContextAccessor>(), x => true));
Assert.IsNotNull(doc.FirstChild<IPublishedContent>(Mock.Of<IVariationContextAccessor>()));
doc = GetNode(1175); // does not have child nodes
doc = GetContent(1175); // does not have child nodes
Assert.IsNull(doc.FirstChild(Mock.Of<IVariationContextAccessor>()));
Assert.IsNull(doc.FirstChild(Mock.Of<IVariationContextAccessor>(), x => true));
Assert.IsNull(doc.FirstChild<IPublishedContent>(Mock.Of<IVariationContextAccessor>()));
@@ -482,7 +383,7 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void FirstChildAsT()
{
var doc = GetNode(1046); // has child nodes
var doc = GetContent(1046); // has child nodes
var model = doc.FirstChild<Home>(Mock.Of<IVariationContextAccessor>(), x => true); // predicate
@@ -491,7 +392,7 @@ namespace Umbraco.Tests.PublishedContent
Assert.IsInstanceOf<Home>(model);
Assert.IsInstanceOf<IPublishedContent>(model);
doc = GetNode(1175); // does not have child nodes
doc = GetContent(1175); // does not have child nodes
Assert.IsNull(doc.FirstChild<Anything>(Mock.Of<IVariationContextAccessor>()));
Assert.IsNull(doc.FirstChild<Anything>(Mock.Of<IVariationContextAccessor>(), x => true));
}
@@ -499,7 +400,7 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void IsComposedOf()
{
var doc = GetNode(1173);
var doc = GetContent(1173);
var isComposedOf = doc.IsComposedOf("MyCompositionAlias");
@@ -509,7 +410,7 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void HasProperty()
{
var doc = GetNode(1173);
var doc = GetContent(1173);
var hasProp = doc.HasProperty(Constants.Conventions.Content.UrlAlias);
@@ -519,7 +420,7 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void HasValue()
{
var doc = GetNode(1173);
var doc = GetContent(1173);
var hasValue = doc.HasValue(Mock.Of<IPublishedValueFallback>(), Constants.Conventions.Content.UrlAlias);
var noValue = doc.HasValue(Mock.Of<IPublishedValueFallback>(), "blahblahblah");
@@ -531,7 +432,7 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Ancestors_Where_Visible()
{
var doc = GetNode(1174);
var doc = GetContent(1174);
var whereVisible = doc.Ancestors().Where(x => x.IsVisible(Mock.Of<IPublishedValueFallback>()));
Assert.AreEqual(1, whereVisible.Count());
@@ -541,8 +442,8 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Visible()
{
var hidden = GetNode(1046);
var visible = GetNode(1173);
var hidden = GetContent(1046);
var visible = GetContent(1173);
Assert.IsFalse(hidden.IsVisible(Mock.Of<IPublishedValueFallback>()));
Assert.IsTrue(visible.IsVisible(Mock.Of<IPublishedValueFallback>()));
@@ -551,7 +452,7 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Ancestor_Or_Self()
{
var doc = GetNode(1173);
var doc = GetContent(1173);
var result = doc.AncestorOrSelf();
@@ -564,7 +465,7 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void U4_4559()
{
var doc = GetNode(1174);
var doc = GetContent(1174);
var result = doc.AncestorOrSelf(1);
Assert.IsNotNull(result);
Assert.AreEqual(1046, result.Id);
@@ -573,27 +474,27 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Ancestors_Or_Self()
{
var doc = GetNode(1174);
var doc = GetContent(1174);
var result = doc.AncestorsOrSelf().ToArray();
Assert.IsNotNull(result);
Assert.AreEqual(3, result.Length);
Assert.IsTrue(result.Select(x => ((dynamic)x).GetId()).ContainsAll(new dynamic[] { 1174, 1173, 1046 }));
Assert.IsTrue(result.Select(x => x.Id).ContainsAll(new [] { 1174, 1173, 1046 }));
}
[Test]
public void Ancestors()
{
var doc = GetNode(1174);
var doc = GetContent(1174);
var result = doc.Ancestors().ToArray();
Assert.IsNotNull(result);
Assert.AreEqual(2, result.Length);
Assert.IsTrue(result.Select(x => ((dynamic)x).GetId()).ContainsAll(new dynamic[] { 1173, 1046 }));
Assert.IsTrue(result.Select(x => x.Id).ContainsAll(new [] { 1173, 1046 }));
}
[Test]
@@ -607,12 +508,12 @@ namespace Umbraco.Tests.PublishedContent
// -- Custom Doc4: 117 (parent 1173)
// - Custom Doc3: 1172 (no parent)
var home = GetNode(1173);
var root = GetNode(1046);
var customDoc = GetNode(1178);
var customDoc2 = GetNode(1179);
var customDoc3 = GetNode(1172);
var customDoc4 = GetNode(117);
var home = GetContent(1173);
var root = GetContent(1046);
var customDoc = GetContent(1178);
var customDoc2 = GetContent(1179);
var customDoc3 = GetContent(1172);
var customDoc4 = GetContent(117);
Assert.IsTrue(root.IsAncestor(customDoc4));
Assert.IsFalse(root.IsAncestor(customDoc3));
@@ -656,12 +557,12 @@ namespace Umbraco.Tests.PublishedContent
// -- Custom Doc4: 117 (parent 1173)
// - Custom Doc3: 1172 (no parent)
var home = GetNode(1173);
var root = GetNode(1046);
var customDoc = GetNode(1178);
var customDoc2 = GetNode(1179);
var customDoc3 = GetNode(1172);
var customDoc4 = GetNode(117);
var home = GetContent(1173);
var root = GetContent(1046);
var customDoc = GetContent(1178);
var customDoc2 = GetContent(1179);
var customDoc3 = GetContent(1172);
var customDoc4 = GetContent(117);
Assert.IsTrue(root.IsAncestorOrSelf(customDoc4));
Assert.IsFalse(root.IsAncestorOrSelf(customDoc3));
@@ -699,27 +600,27 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Descendants_Or_Self()
{
var doc = GetNode(1046);
var doc = GetContent(1046);
var result = doc.DescendantsOrSelf(Mock.Of<IVariationContextAccessor>()).ToArray();
Assert.IsNotNull(result);
Assert.AreEqual(10, result.Count());
Assert.IsTrue(result.Select(x => ((dynamic)x).GetId()).ContainsAll(new dynamic[] { 1046, 1173, 1174, 1176, 1175 }));
Assert.IsTrue(result.Select(x => x.Id).ContainsAll(new [] { 1046, 1173, 1174, 1176, 1175 }));
}
[Test]
public void Descendants()
{
var doc = GetNode(1046);
var doc = GetContent(1046);
var result = doc.Descendants(Mock.Of<IVariationContextAccessor>()).ToArray();
Assert.IsNotNull(result);
Assert.AreEqual(9, result.Count());
Assert.IsTrue(result.Select(x => ((dynamic)x).GetId()).ContainsAll(new dynamic[] { 1173, 1174, 1176, 1175, 4444 }));
Assert.IsTrue(result.Select(x => x.Id).ContainsAll(new [] { 1173, 1174, 1176, 1175, 4444 }));
}
[Test]
@@ -733,12 +634,12 @@ namespace Umbraco.Tests.PublishedContent
// -- Custom Doc4: 117 (parent 1173)
// - Custom Doc3: 1172 (no parent)
var home = GetNode(1173);
var root = GetNode(1046);
var customDoc = GetNode(1178);
var customDoc2 = GetNode(1179);
var customDoc3 = GetNode(1172);
var customDoc4 = GetNode(117);
var home = GetContent(1173);
var root = GetContent(1046);
var customDoc = GetContent(1178);
var customDoc2 = GetContent(1179);
var customDoc3 = GetContent(1172);
var customDoc4 = GetContent(117);
Assert.IsFalse(root.IsDescendant(root));
Assert.IsFalse(root.IsDescendant(home));
@@ -782,12 +683,12 @@ namespace Umbraco.Tests.PublishedContent
// -- Custom Doc4: 117 (parent 1173)
// - Custom Doc3: 1172 (no parent)
var home = GetNode(1173);
var root = GetNode(1046);
var customDoc = GetNode(1178);
var customDoc2 = GetNode(1179);
var customDoc3 = GetNode(1172);
var customDoc4 = GetNode(117);
var home = GetContent(1173);
var root = GetContent(1046);
var customDoc = GetContent(1178);
var customDoc2 = GetContent(1179);
var customDoc3 = GetContent(1172);
var customDoc4 = GetContent(117);
Assert.IsTrue(root.IsDescendantOrSelf(root));
Assert.IsFalse(root.IsDescendantOrSelf(home));
@@ -830,39 +731,40 @@ namespace Umbraco.Tests.PublishedContent
// --- Level1.1.2: 117 (parent 1173)
// --- Level1.1.3: 1177 (parent 1173)
// --- Level1.1.4: 1178 (parent 1173)
// ---- Level1.1.4.1: 1179 (parent 1178)
// --- Level1.1.5: 1176 (parent 1173)
// -- Level1.2: 1175 (parent 1046)
// -- Level1.3: 4444 (parent 1046)
var root = GetNode(1046);
var level1_1 = GetNode(1173);
var level1_1_1 = GetNode(1174);
var level1_1_2 = GetNode(117);
var level1_1_3 = GetNode(1177);
var level1_1_4 = GetNode(1178);
var level1_1_5 = GetNode(1176);
var level1_2 = GetNode(1175);
var level1_3 = GetNode(4444);
// - Root : 1172 (no parent)
_publishedSnapshotAccessorMock.Setup(x => x.PublishedSnapshot.Content.GetAtRoot(It.IsAny<string>())).Returns(new []{root});
var root = GetContent(1046);
var level1_1 = GetContent(1173);
var level1_1_1 = GetContent(1174);
var level1_1_2 = GetContent(117);
var level1_1_3 = GetContent(1177);
var level1_1_4 = GetContent(1178);
var level1_1_5 = GetContent(1176);
var level1_2 = GetContent(1175);
var level1_3 = GetContent(4444);
var root2 = GetContent(1172);
var variationContextAccessor = Factory.GetRequiredService<IVariationContextAccessor>();
var publishedSnapshot = _publishedSnapshotAccessorMock.Object.PublishedSnapshot;
var publishedSnapshot = GetPublishedSnapshot();
CollectionAssertAreEqual(new []{root}, root.SiblingsAndSelf(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual(new[] { root, root2 }, root.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor));
CollectionAssertAreEqual( new []{level1_1, level1_2, level1_3}, level1_1.SiblingsAndSelf(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual( new []{level1_1, level1_2, level1_3}, level1_2.SiblingsAndSelf(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual( new []{level1_1, level1_2, level1_3}, level1_3.SiblingsAndSelf(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual(new[] { level1_1, level1_2, level1_3 }, level1_1.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor));
CollectionAssertAreEqual(new[] { level1_1, level1_2, level1_3 }, level1_2.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor));
CollectionAssertAreEqual(new[] { level1_1, level1_2, level1_3 }, level1_3.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor));
CollectionAssertAreEqual( new []{level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5}, level1_1_1.SiblingsAndSelf(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual( new []{level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5}, level1_1_2.SiblingsAndSelf(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual( new []{level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5}, level1_1_3.SiblingsAndSelf(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual( new []{level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5}, level1_1_4.SiblingsAndSelf(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual( new []{level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5}, level1_1_5.SiblingsAndSelf(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_1.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor));
CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_2.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor));
CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_3.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor));
CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_4.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor));
CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_5.SiblingsAndSelf(publishedSnapshot, VariationContextAccessor));
}
[Test]
[Test]
public void Siblings()
{
// Structure:
@@ -872,40 +774,41 @@ namespace Umbraco.Tests.PublishedContent
// --- Level1.1.2: 117 (parent 1173)
// --- Level1.1.3: 1177 (parent 1173)
// --- Level1.1.4: 1178 (parent 1173)
// ---- Level1.1.4.1: 1179 (parent 1178)
// --- Level1.1.5: 1176 (parent 1173)
// -- Level1.2: 1175 (parent 1046)
// -- Level1.3: 4444 (parent 1046)
var root = GetNode(1046);
var level1_1 = GetNode(1173);
var level1_1_1 = GetNode(1174);
var level1_1_2 = GetNode(117);
var level1_1_3 = GetNode(1177);
var level1_1_4 = GetNode(1178);
var level1_1_5 = GetNode(1176);
var level1_2 = GetNode(1175);
var level1_3 = GetNode(4444);
// - Root : 1172 (no parent)
_publishedSnapshotAccessorMock.Setup(x => x.PublishedSnapshot.Content.GetAtRoot(It.IsAny<string>())).Returns(new []{root});
var root = GetContent(1046);
var level1_1 = GetContent(1173);
var level1_1_1 = GetContent(1174);
var level1_1_2 = GetContent(117);
var level1_1_3 = GetContent(1177);
var level1_1_4 = GetContent(1178);
var level1_1_5 = GetContent(1176);
var level1_2 = GetContent(1175);
var level1_3 = GetContent(4444);
var root2 = GetContent(1172);
var variationContextAccessor = Factory.GetRequiredService<IVariationContextAccessor>();
var publishedSnapshot = _publishedSnapshotAccessorMock.Object.PublishedSnapshot;
var publishedSnapshot = GetPublishedSnapshot();
CollectionAssertAreEqual(new IPublishedContent[0], root.Siblings(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual(new[] { root2 }, root.Siblings(publishedSnapshot, VariationContextAccessor));
CollectionAssertAreEqual( new []{level1_2, level1_3}, level1_1.Siblings(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual( new []{level1_1, level1_3}, level1_2.Siblings(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual( new []{level1_1, level1_2}, level1_3.Siblings(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual(new[] { level1_2, level1_3 }, level1_1.Siblings(publishedSnapshot, VariationContextAccessor));
CollectionAssertAreEqual(new[] { level1_1, level1_3 }, level1_2.Siblings(publishedSnapshot, VariationContextAccessor));
CollectionAssertAreEqual(new[] { level1_1, level1_2 }, level1_3.Siblings(publishedSnapshot, VariationContextAccessor));
CollectionAssertAreEqual( new []{ level1_1_2, level1_1_3, level1_1_4, level1_1_5}, level1_1_1.Siblings(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual( new []{level1_1_1, level1_1_3, level1_1_4, level1_1_5}, level1_1_2.Siblings(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual( new []{level1_1_1, level1_1_2, level1_1_4, level1_1_5}, level1_1_3.Siblings(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual( new []{level1_1_1, level1_1_2, level1_1_3, level1_1_5}, level1_1_4.Siblings(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual( new []{level1_1_1, level1_1_2, level1_1_3, level1_1_4}, level1_1_5.Siblings(publishedSnapshot, variationContextAccessor));
CollectionAssertAreEqual(new[] { level1_1_2, level1_1_3, level1_1_4, level1_1_5 }, level1_1_1.Siblings(publishedSnapshot, VariationContextAccessor));
CollectionAssertAreEqual(new[] { level1_1_1, level1_1_3, level1_1_4, level1_1_5 }, level1_1_2.Siblings(publishedSnapshot, VariationContextAccessor));
CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_4, level1_1_5 }, level1_1_3.Siblings(publishedSnapshot, VariationContextAccessor));
CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_5 }, level1_1_4.Siblings(publishedSnapshot, VariationContextAccessor));
CollectionAssertAreEqual(new[] { level1_1_1, level1_1_2, level1_1_3, level1_1_4 }, level1_1_5.Siblings(publishedSnapshot, VariationContextAccessor));
}
private void CollectionAssertAreEqual<T>(IEnumerable<T> expected, IEnumerable<T> actual)
where T: IPublishedContent
where T : IPublishedContent
{
var e = expected.Select(x => x.Id);
var a = actual.Select(x => x.Id);
@@ -915,37 +818,26 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void FragmentProperty()
{
var factory = Factory.GetRequiredService<IPublishedContentTypeFactory>() as PublishedContentTypeFactory;
IEnumerable<IPublishedPropertyType> CreatePropertyTypes(IPublishedContentType contentType)
{
yield return factory.CreatePropertyType(contentType, "detached", 1003);
yield return PublishedContentTypeFactory.CreatePropertyType(contentType, "detached", _dataTypes[0].Id);
}
var ct = factory.CreateContentType(Guid.NewGuid(), 0, "alias", CreatePropertyTypes);
var ct = PublishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 0, "alias", CreatePropertyTypes);
var pt = ct.GetPropertyType("detached");
var prop = new PublishedElementPropertyBase(pt, null, false, PropertyCacheLevel.None, 5548);
Assert.IsInstanceOf<int>(prop.GetValue());
Assert.AreEqual(5548, prop.GetValue());
}
public void Fragment1()
{
var type = ContentTypesCache.Get(PublishedItemType.Content, "detachedSomething");
var values = new Dictionary<string, object>();
var f = new PublishedElement(type, Guid.NewGuid(), values, false);
}
[Test]
public void Fragment2()
{
var factory = Factory.GetRequiredService<IPublishedContentTypeFactory>() as PublishedContentTypeFactory;
IEnumerable<IPublishedPropertyType> CreatePropertyTypes(IPublishedContentType contentType)
{
yield return factory.CreatePropertyType(contentType, "legend", 1004);
yield return factory.CreatePropertyType(contentType, "image", 1005);
yield return factory.CreatePropertyType(contentType, "size", 1003);
yield return PublishedContentTypeFactory.CreatePropertyType(contentType, "legend", _dataTypes[0].Id);
yield return PublishedContentTypeFactory.CreatePropertyType(contentType, "image", _dataTypes[0].Id);
yield return PublishedContentTypeFactory.CreatePropertyType(contentType, "size", _dataTypes[0].Id);
}
const string val1 = "boom bam";
@@ -954,7 +846,7 @@ namespace Umbraco.Tests.PublishedContent
var guid = Guid.NewGuid();
var ct = factory.CreateContentType(Guid.NewGuid(), 0, "alias", CreatePropertyTypes);
var ct = PublishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 0, "alias", CreatePropertyTypes);
var c = new ImageWithLegendModel(ct, guid, new Dictionary<string, object>
{
@@ -967,6 +859,87 @@ namespace Umbraco.Tests.PublishedContent
Assert.AreEqual(val3, c.Size);
}
[Test]
public void First()
{
var publishedSnapshot = GetPublishedSnapshot();
var content = publishedSnapshot.Content.GetAtRoot().First();
Assert.AreEqual("Home", content.Name(VariationContextAccessor));
}
[Test]
public void Distinct()
{
var items = GetContent(1173)
.Children(VariationContextAccessor)
.Distinct()
.Distinct()
.ToIndexedArray();
Assert.AreEqual(5, items.Length);
IndexedArrayItem<IPublishedContent> item = items[0];
Assert.AreEqual(1174, item.Content.Id);
Assert.IsTrue(item.IsFirst());
Assert.IsFalse(item.IsLast());
item = items[^1];
Assert.AreEqual(1176, item.Content.Id);
Assert.IsFalse(item.IsFirst());
Assert.IsTrue(item.IsLast());
}
[Test]
public void OfType1()
{
var publishedSnapshot = GetPublishedSnapshot();
var items = publishedSnapshot.Content.GetAtRoot()
.OfType<Home>()
.Distinct()
.ToIndexedArray();
Assert.AreEqual(1, items.Length);
Assert.IsInstanceOf<Home>(items.First().Content);
}
[Test]
public void OfType2()
{
var publishedSnapshot = GetPublishedSnapshot();
var content = publishedSnapshot.Content.GetAtRoot()
.OfType<CustomDocument>()
.Distinct()
.ToIndexedArray();
Assert.AreEqual(1, content.Length);
Assert.IsInstanceOf<CustomDocument>(content.First().Content);
}
[Test]
public void OfType()
{
var content = GetContent(1173)
.Children(VariationContextAccessor)
.OfType<Home>()
.First(x => x.UmbracoNaviHide == true);
Assert.AreEqual(1176, content.Id);
}
[Test]
public void Position()
{
var items = GetContent(1173).Children(VariationContextAccessor)
.Where(x => x.Value<int?>(Mock.Of<IPublishedValueFallback>(), "umbracoNaviHide") == 0)
.ToIndexedArray();
Assert.AreEqual(3, items.Length);
Assert.IsTrue(items.First().IsFirst());
Assert.IsFalse(items.First().IsLast());
Assert.IsFalse(items.Skip(1).First().IsFirst());
Assert.IsFalse(items.Skip(1).First().IsLast());
Assert.IsFalse(items.Skip(2).First().IsFirst());
Assert.IsTrue(items.Skip(2).First().IsLast());
}
class ImageWithLegendModel : PublishedElement
{
public ImageWithLegendModel(IPublishedContentType contentType, Guid fragmentKey, Dictionary<string, object> values, bool previewing)
@@ -980,5 +953,31 @@ namespace Umbraco.Tests.PublishedContent
public int Size => this.Value<int>(Mock.Of<IPublishedValueFallback>(), "size");
}
//[PublishedModel("ContentType2")]
//public class ContentType2 : PublishedContentModel
//{
// #region Plumbing
// public ContentType2(IPublishedContent content, IPublishedValueFallback fallback)
// : base(content, fallback)
// { }
// #endregion
// public int Prop1 => this.Value<int>(Mock.Of<IPublishedValueFallback>(), "prop1");
//}
//[PublishedModel("ContentType2Sub")]
//public class ContentType2Sub : ContentType2
//{
// #region Plumbing
// public ContentType2Sub(IPublishedContent content, IPublishedValueFallback fallback)
// : base(content, fallback)
// { }
// #endregion
//}
}
}

View File

@@ -0,0 +1,242 @@
using System.Collections.Generic;
using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
using Umbraco.Cms.Infrastructure.Serialization;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache
{
/// <summary>
/// Tests the typed extension methods on IPublishedContent using the DefaultPublishedMediaStore
/// </summary>
[TestFixture]
public class PublishedMediaTests : PublishedSnapshotServiceTestBase
{
[SetUp]
public override void Setup()
{
base.Setup();
var dataTypes = GetDefaultDataTypes().ToList();
var serializer = new ConfigurationEditorJsonSerializer();
var rteDataType = new DataType(new VoidEditor("RTE", Mock.Of<IDataValueEditorFactory>()), serializer) { Id = 4 };
dataTypes.Add(rteDataType);
_dataTypes = dataTypes.ToArray();
_propertyDataTypes = new()
{
// defaults will just use the first one
[string.Empty] = _dataTypes[0],
// content uses the RTE
["content"] = _dataTypes[1]
};
}
private Dictionary<string, IDataType> _propertyDataTypes;
private DataType[] _dataTypes;
private ContentNodeKit CreateRoot(out MediaType mediaType)
{
mediaType = new MediaType(ShortStringHelper, -1);
ContentData item1Data = new ContentDataBuilder()
.WithName("Content 1")
.WithProperties(new PropertyDataBuilder()
.WithPropertyData("content", "<div>This is some content</div>")
.Build())
// build with a dynamically created media type
.Build(ShortStringHelper, _propertyDataTypes, mediaType, "image2");
ContentNodeKit item1 = ContentNodeKitBuilder.CreateWithContent(
mediaType.Id,
1, "-1,1",
draftData: item1Data,
publishedData: item1Data);
return item1;
}
private IEnumerable<ContentNodeKit> CreateChildren(
int startId,
ContentNodeKit parent,
IMediaType mediaType,
int count)
{
for (int i = 0; i < count; i++)
{
var id = startId + i + 1;
ContentData item1Data = new ContentDataBuilder()
.WithName("Child " + id)
.WithProperties(new PropertyDataBuilder()
.WithPropertyData("content", "<div>This is some content</div>")
.Build())
.Build();
var parentPath = parent.Node.Path;
ContentNodeKit item1 = ContentNodeKitBuilder.CreateWithContent(
mediaType.Id,
id, $"{parentPath},{id}",
draftData: item1Data,
publishedData: item1Data);
yield return item1;
}
}
private void InitializeWithHierarchy(
out int rootId,
out IReadOnlyList<ContentNodeKit> firstLevelChildren,
out IReadOnlyList<ContentNodeKit> secondLevelChildren)
{
var cache = new List<ContentNodeKit>();
var root = CreateRoot(out MediaType mediaType);
firstLevelChildren = CreateChildren(10, root, mediaType, 3).ToList();
secondLevelChildren = CreateChildren(20, firstLevelChildren[0], mediaType, 3).ToList();
cache.Add(root);
cache.AddRange(firstLevelChildren);
cache.AddRange(secondLevelChildren);
InitializedCache(null, null, _dataTypes, cache, new[] { mediaType });
rootId = root.Node.Id;
}
[Test]
public void Get_Property_Value_Uses_Converter()
{
var cache = CreateRoot(out MediaType mediaType);
InitializedCache(null, null, _dataTypes.ToArray(), new[] { cache }, new[] { mediaType });
var publishedMedia = GetMedia(1);
var propVal = publishedMedia.Value(PublishedValueFallback, "content");
Assert.IsInstanceOf<IHtmlEncodedString>(propVal);
Assert.AreEqual("<div>This is some content</div>", propVal.ToString());
var propVal2 = publishedMedia.Value<IHtmlEncodedString>(PublishedValueFallback, "content");
Assert.IsInstanceOf<IHtmlEncodedString>(propVal2);
Assert.AreEqual("<div>This is some content</div>", propVal2.ToString());
var propVal3 = publishedMedia.Value(PublishedValueFallback, "Content");
Assert.IsInstanceOf<IHtmlEncodedString>(propVal3);
Assert.AreEqual("<div>This is some content</div>", propVal3.ToString());
}
[Test]
public void Children()
{
InitializeWithHierarchy(
out var rootId,
out IReadOnlyList<ContentNodeKit> firstLevelChildren,
out IReadOnlyList<ContentNodeKit> secondLevelChildren);
var publishedMedia = GetMedia(rootId);
var rootChildren = publishedMedia.Children(VariationContextAccessor);
Assert.IsTrue(rootChildren.Select(x => x.Id).ContainsAll(firstLevelChildren.Select(x => x.Node.Id)));
var publishedChild1 = GetMedia(firstLevelChildren[0].Node.Id);
var subChildren = publishedChild1.Children(VariationContextAccessor);
Assert.IsTrue(subChildren.Select(x => x.Id).ContainsAll(secondLevelChildren.Select(x => x.Node.Id)));
}
[Test]
public void Descendants()
{
InitializeWithHierarchy(
out var rootId,
out IReadOnlyList<ContentNodeKit> firstLevelChildren,
out IReadOnlyList<ContentNodeKit> secondLevelChildren);
var publishedMedia = GetMedia(rootId);
var rootDescendants = publishedMedia.Descendants(VariationContextAccessor);
var descendentIds = firstLevelChildren.Select(x => x.Node.Id).Concat(secondLevelChildren.Select(x => x.Node.Id));
Assert.IsTrue(rootDescendants.Select(x => x.Id).ContainsAll(descendentIds));
var publishedChild1 = GetMedia(firstLevelChildren[0].Node.Id);
var subDescendants = publishedChild1.Descendants(VariationContextAccessor);
Assert.IsTrue(subDescendants.Select(x => x.Id).ContainsAll(secondLevelChildren.Select(x => x.Node.Id)));
}
[Test]
public void DescendantsOrSelf()
{
InitializeWithHierarchy(
out var rootId,
out IReadOnlyList<ContentNodeKit> firstLevelChildren,
out IReadOnlyList<ContentNodeKit> secondLevelChildren);
var publishedMedia = GetMedia(rootId);
var rootDescendantsOrSelf = publishedMedia.DescendantsOrSelf(VariationContextAccessor);
var descendentAndSelfIds = firstLevelChildren.Select(x => x.Node.Id)
.Concat(secondLevelChildren.Select(x => x.Node.Id))
.Append(rootId);
Assert.IsTrue(rootDescendantsOrSelf.Select(x => x.Id).ContainsAll(descendentAndSelfIds));
var publishedChild1 = GetMedia(firstLevelChildren[0].Node.Id);
var subDescendantsOrSelf = publishedChild1.DescendantsOrSelf(VariationContextAccessor);
Assert.IsTrue(subDescendantsOrSelf.Select(x => x.Id).ContainsAll(
secondLevelChildren.Select(x => x.Node.Id).Append(firstLevelChildren[0].Node.Id)));
}
[Test]
public void Parent()
{
InitializeWithHierarchy(
out var rootId,
out IReadOnlyList<ContentNodeKit> firstLevelChildren,
out IReadOnlyList<ContentNodeKit> secondLevelChildren);
var publishedMedia = GetMedia(rootId);
Assert.AreEqual(null, publishedMedia.Parent);
var publishedChild1 = GetMedia(firstLevelChildren[0].Node.Id);
Assert.AreEqual(publishedMedia.Id, publishedChild1.Parent.Id);
var publishedSubChild1 = GetMedia(secondLevelChildren[0].Node.Id);
Assert.AreEqual(firstLevelChildren[0].Node.Id, publishedSubChild1.Parent.Id);
}
[Test]
public void Ancestors()
{
InitializeWithHierarchy(
out var rootId,
out IReadOnlyList<ContentNodeKit> firstLevelChildren,
out IReadOnlyList<ContentNodeKit> secondLevelChildren);
var publishedSubChild1 = GetMedia(secondLevelChildren[0].Node.Id);
Assert.IsTrue(publishedSubChild1.Ancestors().Select(x => x.Id)
.ContainsAll(new[] { firstLevelChildren[0].Node.Id, rootId }));
}
[Test]
public void AncestorsOrSelf()
{
InitializeWithHierarchy(
out var rootId,
out IReadOnlyList<ContentNodeKit> firstLevelChildren,
out IReadOnlyList<ContentNodeKit> secondLevelChildren);
var publishedSubChild1 = GetMedia(secondLevelChildren[0].Node.Id);
Assert.IsTrue(publishedSubChild1.AncestorsOrSelf().Select(x => x.Id)
.ContainsAll(new[] { secondLevelChildren[0].Node.Id, firstLevelChildren[0].Node.Id, rootId }));
}
}
}

View File

@@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PublishedCache
{
[TestFixture]
public class PublishedSnapshotServiceContentTests : PublishedSnapshotServiceTestBase
{
private ContentType _contentType;
private PropertyType _propertyType;
[SetUp]
public override void Setup()
{
base.Setup();
_propertyType = new PropertyType(TestHelper.ShortStringHelper, "Umbraco.Void.Editor", ValueStorageType.Nvarchar) { Alias = "prop", DataTypeId = 3, Variations = ContentVariation.Culture };
_contentType = new ContentType(TestHelper.ShortStringHelper, -1) { Id = 2, Alias = "alias-ct", Variations = ContentVariation.Culture };
_contentType.AddPropertyType(_propertyType);
var contentTypes = new[]
{
_contentType
};
InitializedCache(new[] { CreateKit() }, contentTypes);
}
private ContentNodeKit CreateKit()
{
var draftData = new ContentDataBuilder()
.WithName("It Works2!")
.WithPublished(false)
.WithProperties(new Dictionary<string, PropertyData[]>
{
["prop"] = new[]
{
new PropertyData { Culture = "", Segment = "", Value = "val2" },
new PropertyData { Culture = "fr-FR", Segment = "", Value = "val-fr2" },
new PropertyData { Culture = "en-UK", Segment = "", Value = "val-uk2" },
new PropertyData { Culture = "dk-DA", Segment = "", Value = "val-da2" },
new PropertyData { Culture = "de-DE", Segment = "", Value = "val-de2" }
}
})
.WithCultureInfos(new Dictionary<string, CultureVariation>
{
// draft data = everything, and IsDraft indicates what's edited
["fr-FR"] = new CultureVariation { Name = "name-fr2", IsDraft = true, Date = new DateTime(2018, 01, 03, 01, 00, 00) },
["en-UK"] = new CultureVariation { Name = "name-uk2", IsDraft = true, Date = new DateTime(2018, 01, 04, 01, 00, 00) },
["dk-DA"] = new CultureVariation { Name = "name-da2", IsDraft = true, Date = new DateTime(2018, 01, 05, 01, 00, 00) },
["de-DE"] = new CultureVariation { Name = "name-de1", IsDraft = false, Date = new DateTime(2018, 01, 02, 01, 00, 00) }
})
.Build();
var publishedData = new ContentDataBuilder()
.WithName("It Works1!")
.WithPublished(true)
.WithProperties(new Dictionary<string, PropertyData[]>
{
["prop"] = new[]
{
new PropertyData { Culture = "", Segment = "", Value = "val1" },
new PropertyData { Culture = "fr-FR", Segment = "", Value = "val-fr1" },
new PropertyData { Culture = "en-UK", Segment = "", Value = "val-uk1" }
}
})
.WithCultureInfos(new Dictionary<string, CultureVariation>
{
// published data = only what's actually published, and IsDraft has to be false
["fr-FR"] = new CultureVariation { Name = "name-fr1", IsDraft = false, Date = new DateTime(2018, 01, 01, 01, 00, 00) },
["en-UK"] = new CultureVariation { Name = "name-uk1", IsDraft = false, Date = new DateTime(2018, 01, 02, 01, 00, 00) },
["de-DE"] = new CultureVariation { Name = "name-de1", IsDraft = false, Date = new DateTime(2018, 01, 02, 01, 00, 00) }
})
.Build();
var kit = ContentNodeKitBuilder.CreateWithContent(
2,
1, "-1,1", 0,
draftData: draftData,
publishedData: publishedData);
return kit;
}
[Test]
public void Verifies_Variant_Data()
{
// this test implements a full standalone NuCache (based upon a test IDataSource, does not
// use any local db files, does not rely on any database) - and tests variations
// get a snapshot, get a published content
IPublishedSnapshot snapshot = GetPublishedSnapshot();
IPublishedContent publishedContent = snapshot.Content.GetById(1);
Assert.IsNotNull(publishedContent);
Assert.AreEqual("val1", publishedContent.Value<string>(Mock.Of<IPublishedValueFallback>(), "prop"));
Assert.AreEqual("val-fr1", publishedContent.Value<string>(Mock.Of<IPublishedValueFallback>(), "prop", "fr-FR"));
Assert.AreEqual("val-uk1", publishedContent.Value<string>(Mock.Of<IPublishedValueFallback>(), "prop", "en-UK"));
Assert.IsNull(publishedContent.Name(VariationContextAccessor)); // no invariant name for varying content
Assert.AreEqual("name-fr1", publishedContent.Name(VariationContextAccessor, "fr-FR"));
Assert.AreEqual("name-uk1", publishedContent.Name(VariationContextAccessor, "en-UK"));
var draftContent = snapshot.Content.GetById(true, 1);
Assert.AreEqual("val2", draftContent.Value<string>(Mock.Of<IPublishedValueFallback>(), "prop"));
Assert.AreEqual("val-fr2", draftContent.Value<string>(Mock.Of<IPublishedValueFallback>(), "prop", "fr-FR"));
Assert.AreEqual("val-uk2", draftContent.Value<string>(Mock.Of<IPublishedValueFallback>(), "prop", "en-UK"));
Assert.IsNull(draftContent.Name(VariationContextAccessor)); // no invariant name for varying content
Assert.AreEqual("name-fr2", draftContent.Name(VariationContextAccessor, "fr-FR"));
Assert.AreEqual("name-uk2", draftContent.Name(VariationContextAccessor, "en-UK"));
// now french is default
VariationContextAccessor.VariationContext = new VariationContext("fr-FR");
Assert.AreEqual("val-fr1", publishedContent.Value<string>(Mock.Of<IPublishedValueFallback>(), "prop"));
Assert.AreEqual("name-fr1", publishedContent.Name(VariationContextAccessor));
Assert.AreEqual(new DateTime(2018, 01, 01, 01, 00, 00), publishedContent.CultureDate(VariationContextAccessor));
// now uk is default
VariationContextAccessor.VariationContext = new VariationContext("en-UK");
Assert.AreEqual("val-uk1", publishedContent.Value<string>(Mock.Of<IPublishedValueFallback>(), "prop"));
Assert.AreEqual("name-uk1", publishedContent.Name(VariationContextAccessor));
Assert.AreEqual(new DateTime(2018, 01, 02, 01, 00, 00), publishedContent.CultureDate(VariationContextAccessor));
// invariant needs to be retrieved explicitly, when it's not default
Assert.AreEqual("val1", publishedContent.Value<string>(Mock.Of<IPublishedValueFallback>(), "prop", culture: ""));
// but,
// if the content type / property type does not vary, then it's all invariant again
// modify the content type and property type, notify the snapshot service
_contentType.Variations = ContentVariation.Nothing;
_propertyType.Variations = ContentVariation.Nothing;
SnapshotService.Notify(new[] { new ContentTypeCacheRefresher.JsonPayload("IContentType", publishedContent.ContentType.Id, ContentTypeChangeTypes.RefreshMain) });
// get a new snapshot (nothing changed in the old one), get the published content again
var anotherSnapshot = SnapshotService.CreatePublishedSnapshot(previewToken: null);
var againContent = anotherSnapshot.Content.GetById(1);
Assert.AreEqual(ContentVariation.Nothing, againContent.ContentType.Variations);
Assert.AreEqual(ContentVariation.Nothing, againContent.ContentType.GetPropertyType("prop").Variations);
// now, "no culture" means "invariant"
Assert.AreEqual("It Works1!", againContent.Name(VariationContextAccessor));
Assert.AreEqual("val1", againContent.Value<string>(Mock.Of<IPublishedValueFallback>(), "prop"));
}
[Test]
public void Verifies_Published_And_Draft_Content()
{
// get the published published content
var snapshot = GetPublishedSnapshot();
var c1 = snapshot.Content.GetById(1);
// published content = nothing is draft here
Assert.IsFalse(c1.IsDraft("fr-FR"));
Assert.IsFalse(c1.IsDraft("en-UK"));
Assert.IsFalse(c1.IsDraft("dk-DA"));
Assert.IsFalse(c1.IsDraft("de-DE"));
// and only those with published name, are published
Assert.IsTrue(c1.IsPublished("fr-FR"));
Assert.IsTrue(c1.IsPublished("en-UK"));
Assert.IsFalse(c1.IsDraft("dk-DA"));
Assert.IsTrue(c1.IsPublished("de-DE"));
// get the draft published content
var c2 = snapshot.Content.GetById(true, 1);
// draft content = we have drafts
Assert.IsTrue(c2.IsDraft("fr-FR"));
Assert.IsTrue(c2.IsDraft("en-UK"));
Assert.IsTrue(c2.IsDraft("dk-DA"));
Assert.IsFalse(c2.IsDraft("de-DE")); // except for the one that does not
// and only those with published name, are published
Assert.IsTrue(c2.IsPublished("fr-FR"));
Assert.IsTrue(c2.IsPublished("en-UK"));
Assert.IsFalse(c2.IsPublished("dk-DA"));
Assert.IsTrue(c2.IsPublished("de-DE"));
}
}
}

View File

@@ -0,0 +1,52 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
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
{
[TestFixture]
public class RootNodeTests : PublishedSnapshotServiceTestBase
{
[SetUp]
public override void Setup()
{
base.Setup();
string xml = PublishedContentXml.TestWithDatabaseXml(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes);
}
[Test]
public void PublishedContentHasNoRootNode()
{
var snapshot = GetPublishedSnapshot();
// there is no content node with ID -1
var content = snapshot.Content.GetById(-1);
Assert.IsNull(content);
// content at root has null parent
content = snapshot.Content.GetById(1046);
Assert.IsNotNull(content);
Assert.AreEqual(1, content.Level);
Assert.IsNull(content.Parent);
// non-existing content is null
content = snapshot.Content.GetById(666);
Assert.IsNull(content);
}
}
}

View File

@@ -1,25 +1,23 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Tests.LegacyXmlPublishedCache;
using Umbraco.Tests.TestHelpers;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Tests.Common.Published;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
namespace Umbraco.Tests.Routing
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.
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)]
public class UrlRoutesTests : TestWithDatabaseBase
public class UrlRoutesTests : PublishedSnapshotServiceTestBase
{
#region Test Setup
protected override string GetXmlContent(int templateId)
{
return @"<?xml version=""1.0"" encoding=""utf-8""?>
private static string GetXmlContent(int templateId)
=> @"<?xml version=""1.0"" encoding=""utf-8""?>
<!DOCTYPE root[
<!ELEMENT Doc ANY>
<!ATTLIST Doc id ID #REQUIRED>
@@ -48,17 +46,6 @@ namespace Umbraco.Tests.Routing
</Doc>
</Doc>
</root>";
}
protected override void Initialize()
{
base.Initialize();
if (FirstTestInFixture)
ServiceContext.ContentTypeService.Save(new ContentType(ShortStringHelper, -1) { Alias = "Doc", Name = "name" });
}
#endregion
/*
* Just so it's documented somewhere, as of jan. 2017, routes obey the following pseudo-code:
@@ -193,11 +180,19 @@ DetermineRouteById(id):
[TestCase(2006, false, "/x/b/e")]
public void GetRouteByIdNoHide(int id, bool hide, string expected)
{
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = hide };
GlobalSettings.HideTopLevelNodeFromPath = hide;
var umbracoContext = GetUmbracoContext("/test", 0, globalSettings: globalSettings);
var cache = umbracoContext.Content as PublishedContentCache;
if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported.");
string xml = GetXmlContent(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
var cache = GetPublishedSnapshot().Content;
var route = cache.GetRouteById(false, id);
Assert.AreEqual(expected, route);
@@ -216,12 +211,19 @@ DetermineRouteById(id):
[TestCase(2006, true, "/b/e")] // risky!
public void GetRouteByIdHide(int id, bool hide, string expected)
{
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = hide };
GlobalSettings.HideTopLevelNodeFromPath = hide;
var snapshotService = CreatePublishedSnapshotService(globalSettings);
var umbracoContext = GetUmbracoContext("/test", 0, globalSettings: globalSettings, snapshotService: snapshotService);
var cache = umbracoContext.Content as PublishedContentCache;
if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported.");
string xml = GetXmlContent(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
var cache = GetPublishedSnapshot().Content;
var route = cache.GetRouteById(false, id);
Assert.AreEqual(expected, route);
@@ -230,27 +232,23 @@ DetermineRouteById(id):
[Test]
public void GetRouteByIdCache()
{
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = false };
GlobalSettings.HideTopLevelNodeFromPath = false;
string xml = GetXmlContent(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
var cache = GetPublishedSnapshot().Content;
var snapshotService = CreatePublishedSnapshotService(globalSettings);
var umbracoContext = GetUmbracoContext("/test", 0, globalSettings:globalSettings, snapshotService: snapshotService);
var cache = umbracoContext.Content as PublishedContentCache;
if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported.");
var route = cache.GetRouteById(false, 1000);
Assert.AreEqual("/a", route);
// GetRouteById registers a non-trusted route, which is cached for
// id -> route queries (fast GetUrl) but *not* for route -> id
// queries (safe inbound routing)
var cachedRoutes = cache.RoutesCache.GetCachedRoutes();
Assert.AreEqual(1, cachedRoutes.Count);
Assert.IsTrue(cachedRoutes.ContainsKey(1000));
Assert.AreEqual("/a", cachedRoutes[1000]);
var cachedIds = cache.RoutesCache.GetCachedIds();
Assert.AreEqual(0, cachedIds.Count);
}
[TestCase("/", false, 1000)]
@@ -261,12 +259,19 @@ DetermineRouteById(id):
[TestCase("/x", false, 2000)]
public void GetByRouteNoHide(string route, bool hide, int expected)
{
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = hide };
GlobalSettings.HideTopLevelNodeFromPath = hide;
var snapshotService = CreatePublishedSnapshotService(globalSettings);
var umbracoContext = GetUmbracoContext("/test", 0, globalSettings:globalSettings, snapshotService: snapshotService);
var cache = umbracoContext.Content as PublishedContentCache;
if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported.");
string xml = GetXmlContent(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: 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);
@@ -292,12 +297,19 @@ DetermineRouteById(id):
[TestCase("/b/c", true, 1002)] // (hence the 2005 collision)
public void GetByRouteHide(string route, bool hide, int expected)
{
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = hide };
GlobalSettings.HideTopLevelNodeFromPath = hide;
var snapshotService = CreatePublishedSnapshotService(globalSettings);
var umbracoContext = GetUmbracoContext("/test", 0, globalSettings:globalSettings, snapshotService: snapshotService);
var cache = umbracoContext.Content as PublishedContentCache;
if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported.");
string xml = GetXmlContent(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: 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);
@@ -315,30 +327,23 @@ DetermineRouteById(id):
[Test]
public void GetByRouteCache()
{
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = false };
GlobalSettings.HideTopLevelNodeFromPath = false;
var snapshotService = CreatePublishedSnapshotService(globalSettings);
var umbracoContext = GetUmbracoContext("/test", 0, globalSettings:globalSettings, snapshotService:snapshotService);
var cache = umbracoContext.Content as PublishedContentCache;
if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported.");
string xml = GetXmlContent(1234);
IEnumerable<ContentNodeKit> kits = PublishedContentXmlAdapter.GetContentNodeKits(
xml,
TestHelper.ShortStringHelper,
out ContentType[] contentTypes,
out DataType[] dataTypes).ToList();
InitializedCache(kits, contentTypes, dataTypes: dataTypes);
var cache = GetPublishedSnapshot().Content;
var content = cache.GetByRoute(false, "/a/b/c");
Assert.IsNotNull(content);
Assert.AreEqual(1002, content.Id);
// GetByRoute registers a trusted route, which is cached both for
// id -> route queries (fast GetUrl) and for route -> id queries
// (fast inbound routing)
var cachedRoutes = cache.RoutesCache.GetCachedRoutes();
Assert.AreEqual(1, cachedRoutes.Count);
Assert.IsTrue(cachedRoutes.ContainsKey(1002));
Assert.AreEqual("/a/b/c", cachedRoutes[1002]);
var cachedIds = cache.RoutesCache.GetCachedIds();
Assert.AreEqual(1, cachedIds.Count);
Assert.IsTrue(cachedIds.ContainsKey("/a/b/c"));
Assert.AreEqual(1002, cachedIds["/a/b/c"]);
}
}
}

View File

@@ -1,14 +1,11 @@
using Newtonsoft.Json;
using Newtonsoft.Json;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
using Umbraco.Core.Serialization;
using Umbraco.Cms.Infrastructure.Serialization;
namespace Umbraco.Tests.Serialization
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Serialization
{
[TestFixture]
public class AutoInterningStringConverterTests
@@ -61,7 +58,7 @@ namespace Umbraco.Tests.Serialization
public string Name { get; set; }
[JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter<int>))]
public Dictionary<string, int> Values = new Dictionary<string, int>();
public Dictionary<string, int> Values { get; set; } = new Dictionary<string, int>();
}
}
}

View File

@@ -1,133 +0,0 @@
using System.Linq;
using System.Xml;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Tests.Common;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Tests.LegacyXmlPublishedCache;
using Umbraco.Tests.TestHelpers;
using Umbraco.Web;
namespace Umbraco.Tests.Cache.PublishedCache
{
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)]
public class PublishContentCacheTests : BaseWebTest
{
private FakeHttpContextFactory _httpContextFactory;
private IUmbracoContext _umbracoContext;
private IPublishedContentCache _cache;
private XmlDocument _xml;
private string GetXml()
{
return @"<?xml version=""1.0"" encoding=""utf-8""?><!DOCTYPE root[
<!ELEMENT Home ANY>
<!ATTLIST Home id ID #REQUIRED>
]>
<root id=""-1"">
<Home id=""1046"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1045"" sortOrder=""2"" createDate=""2012-06-12T14:13:17"" updateDate=""2012-07-20T18:50:43"" nodeName=""Home"" urlName=""home"" writerName=""admin"" creatorName=""admin"" path=""-1,1046"" isDoc=""""><content><![CDATA[]]></content>
<Home id=""1173"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1045"" sortOrder=""1"" createDate=""2012-07-20T18:06:45"" updateDate=""2012-07-20T19:07:31"" nodeName=""Sub1"" urlName=""sub1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173"" isDoc=""""><content><![CDATA[]]></content>
<Home id=""1174"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1045"" sortOrder=""1"" createDate=""2012-07-20T18:07:54"" updateDate=""2012-07-20T19:10:27"" nodeName=""Sub2"" urlName=""sub2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1174"" isDoc=""""><content><![CDATA[]]></content>
</Home>
<Home id=""1176"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1045"" sortOrder=""2"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc=""""><content><![CDATA[]]></content>
</Home>
</Home>
<Home id=""1175"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1045"" sortOrder=""2"" createDate=""2012-07-20T18:08:01"" updateDate=""2012-07-20T18:49:32"" nodeName=""Sub 2"" urlName=""sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1175"" isDoc=""""><content><![CDATA[]]></content>
</Home>
<Home id=""1177"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1045"" sortOrder=""2"" createDate=""2012-07-20T18:08:01"" updateDate=""2012-07-20T18:49:32"" nodeName=""Sub'Apostrophe"" urlName=""sub'apostrophe"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1177"" isDoc=""""><content><![CDATA[]]></content>
</Home>
</Home>
<Home id=""1172"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""1045"" sortOrder=""3"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""Test"" urlName=""test"" writerName=""admin"" creatorName=""admin"" path=""-1,1172"" isDoc="""" />
</root>";
}
protected override void Initialize()
{
base.Initialize();
_httpContextFactory = new FakeHttpContextFactory("~/Home");
var globalSettings = new GlobalSettings();
var umbracoContextAccessor = Factory.GetRequiredService<IUmbracoContextAccessor>();
_xml = new XmlDocument();
_xml.LoadXml(GetXml());
var xmlStore = new XmlStore(() => _xml, null, null, null, HostingEnvironment);
var appCache = new DictionaryAppCache();
var domainCache = new DomainCache(Mock.Of<IDomainService>(), DefaultCultureAccessor);
var publishedShapshot = new PublishedSnapshot(
new PublishedContentCache(xmlStore, domainCache, appCache, globalSettings, ContentTypesCache, null, VariationContextAccessor, null),
new PublishedMediaCache(xmlStore, Mock.Of<IMediaService>(), Mock.Of<IUserService>(), appCache, ContentTypesCache, Factory.GetRequiredService<IEntityXmlSerializer>(), umbracoContextAccessor, VariationContextAccessor),
new PublishedMemberCache(ContentTypesCache, VariationContextAccessor),
domainCache);
var publishedSnapshotService = new Mock<IPublishedSnapshotService>();
publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny<string>())).Returns(publishedShapshot);
var httpContext = _httpContextFactory.HttpContext;
var httpContextAccessor = TestHelper.GetHttpContextAccessor(httpContext);
_umbracoContext = new UmbracoContext(
httpContextAccessor,
publishedSnapshotService.Object,
Mock.Of<IBackOfficeSecurity>(),
globalSettings,
HostingEnvironment,
new TestVariationContextAccessor(),
UriUtility,
new AspNetCookieManager(httpContextAccessor));
_cache = _umbracoContext.Content;
}
[Test]
public void Has_Content()
{
Assert.IsTrue(_cache.HasContent());
}
[Test]
public void Get_Root_Docs()
{
var result = _cache.GetAtRoot();
Assert.AreEqual(2, result.Count());
Assert.AreEqual(1046, result.ElementAt(0).Id);
Assert.AreEqual(1172, result.ElementAt(1).Id);
}
[TestCase("/", 1046)]
[TestCase("/home", 1046)]
[TestCase("/Home", 1046)] //test different cases
[TestCase("/home/sub1", 1173)]
[TestCase("/Home/sub1", 1173)]
[TestCase("/home/Sub1", 1173)] //test different cases
[TestCase("/home/Sub'Apostrophe", 1177)]
public void Get_Node_By_Route(string route, int nodeId)
{
var result = _cache.GetByRoute(route, false);
Assert.IsNotNull(result);
Assert.AreEqual(nodeId, result.Id);
}
[TestCase("/", 1046)]
[TestCase("/sub1", 1173)]
[TestCase("/Sub1", 1173)]
public void Get_Node_By_Route_Hiding_Top_Level_Nodes(string route, int nodeId)
{
var result = _cache.GetByRoute(route, true);
Assert.IsNotNull(result);
Assert.AreEqual(nodeId, result.Id);
}
}
}

View File

@@ -1,413 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Examine;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Extensions;
using Umbraco.Tests.LegacyXmlPublishedCache;
using Umbraco.Tests.PublishedContent;
using Umbraco.Tests.TestHelpers;
using Constants = Umbraco.Cms.Core.Constants;
using Current = Umbraco.Web.Composing.Current;
namespace Umbraco.Tests.Cache.PublishedCache
{
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
public class PublishMediaCacheTests : BaseWebTest
{
private Dictionary<string, PublishedContentType> _mediaTypes;
private int _testWriterAndCreatorId;
private IUmbracoContextAccessor _umbracoContextAccessor;
protected override void Compose()
{
base.Compose();
Builder.WithCollectionBuilder<UrlSegmentProviderCollectionBuilder>()
.Clear()
.Append<DefaultUrlSegmentProvider>();
_umbracoContextAccessor = Current.UmbracoContextAccessor;
}
protected override void Initialize()
{
base.Initialize();
var type = new AutoPublishedContentType(Guid.NewGuid(), 22, "myType", new PublishedPropertyType[] { });
var image = new AutoPublishedContentType(Guid.NewGuid(), 23, "Image", new PublishedPropertyType[] { });
var testMediaType = new AutoPublishedContentType(Guid.NewGuid(), 24, "TestMediaType", new PublishedPropertyType[] { });
_mediaTypes = new Dictionary<string, PublishedContentType>
{
{ type.Alias, type },
{ image.Alias, image },
{ testMediaType.Alias, testMediaType }
};
ContentTypesCache.GetPublishedContentTypeByAlias = alias => _mediaTypes[alias];
_testWriterAndCreatorId = ServiceContext.UserService.CreateUserWithIdentity("Shannon", "test").Id;
}
private IMediaType MakeNewMediaType(IUser user, string text, int parentId = -1)
{
var mt = new MediaType(ShortStringHelper, parentId) { Name = text, Alias = text, Thumbnail = "icon-folder", Icon = "icon-folder" };
ServiceContext.MediaTypeService.Save(mt);
return mt;
}
private IMedia MakeNewMedia(string name, IMediaType mediaType, IUser user, int parentId)
{
var m = ServiceContext.MediaService.CreateMediaWithIdentity(name, parentId, mediaType.Alias);
return m;
}
//NOTE: This is "Without_Examine" too
[Test]
public void Get_Root_Docs()
{
var user = ServiceContext.UserService.GetUserById(0);
var mType = MakeNewMediaType(user, "TestMediaType");
var mRoot1 = MakeNewMedia("MediaRoot1", mType, user, -1);
var mRoot2 = MakeNewMedia("MediaRoot2", mType, user, -1);
var mChild1 = MakeNewMedia("Child1", mType, user, mRoot1.Id);
var mChild2 = MakeNewMedia("Child2", mType, user, mRoot2.Id);
var ctx = GetUmbracoContext("/test");
var cache = new PublishedMediaCache(new XmlStore((XmlDocument) null, null, null, null, HostingEnvironment), ServiceContext.MediaService, ServiceContext.UserService, new DictionaryAppCache(), ContentTypesCache, Factory.GetRequiredService<IEntityXmlSerializer>(), Factory.GetRequiredService<IUmbracoContextAccessor>(), VariationContextAccessor);
var roots = cache.GetAtRoot();
Assert.AreEqual(2, roots.Count());
Assert.IsTrue(roots.Select(x => x.Id).ContainsAll(new[] {mRoot1.Id, mRoot2.Id}));
}
[Test]
public void Get_Item_Without_Examine()
{
var user = ServiceContext.UserService.GetUserById(0);
var mType = MakeNewMediaType(user, "TestMediaType");
_mediaTypes[mType.Alias] = new PublishedContentType(mType, null);
var mRoot = MakeNewMedia("MediaRoot", mType, user, -1);
var mChild1 = MakeNewMedia("Child1", mType, user, mRoot.Id);
//var publishedMedia = PublishedMediaTests.GetNode(mRoot.Id, GetUmbracoContext("/test", 1234));
var umbracoContext = GetUmbracoContext("/test");
var cache = new PublishedMediaCache(new XmlStore((XmlDocument)null, null, null, null, HostingEnvironment), ServiceContext.MediaService, ServiceContext.UserService, new DictionaryAppCache(), ContentTypesCache, Factory.GetRequiredService<IEntityXmlSerializer>(), Factory.GetRequiredService<IUmbracoContextAccessor>(), VariationContextAccessor);
var publishedMedia = cache.GetById(mRoot.Id);
Assert.IsNotNull(publishedMedia);
Assert.AreEqual(mRoot.Id, publishedMedia.Id);
Assert.AreEqual(mRoot.CreateDate.ToString("dd/MM/yyyy HH:mm:ss"), publishedMedia.CreateDate.ToString("dd/MM/yyyy HH:mm:ss"));
Assert.AreEqual(mRoot.CreatorId, publishedMedia.CreatorId);
//Assert.AreEqual(mRoot.User.Name, publishedMedia.CreatorName);
Assert.AreEqual(mRoot.ContentType.Alias, publishedMedia.ContentType.Alias);
Assert.AreEqual(mRoot.ContentType.Id, publishedMedia.ContentType.Id);
Assert.AreEqual(mRoot.Level, publishedMedia.Level);
Assert.AreEqual(mRoot.Name, publishedMedia.Name);
Assert.AreEqual(mRoot.Path, publishedMedia.Path);
Assert.AreEqual(mRoot.SortOrder, publishedMedia.SortOrder);
Assert.IsNull(publishedMedia.Parent);
}
[TestCase("id")]
[TestCase("__NodeId")]
public void DictionaryDocument_Id_Keys(string key)
{
var dicDoc = GetDictionaryDocument(idKey: key);
DoAssert(dicDoc);
}
[TestCase("template")]
[TestCase("templateId")]
public void DictionaryDocument_Template_Keys(string key)
{
var dicDoc = GetDictionaryDocument(templateKey: key);
DoAssert(dicDoc);
}
[TestCase("nodeName")]
public void DictionaryDocument_NodeName_Keys(string key)
{
var dicDoc = GetDictionaryDocument(nodeNameKey: key);
DoAssert(dicDoc);
}
[TestCase("nodeTypeAlias")]
[TestCase("__NodeTypeAlias")]
public void DictionaryDocument_NodeTypeAlias_Keys(string key)
{
var dicDoc = GetDictionaryDocument(nodeTypeAliasKey: key);
DoAssert(dicDoc);
}
[TestCase("path")]
[TestCase("__Path")]
public void DictionaryDocument_Path_Keys(string key)
{
var dicDoc = GetDictionaryDocument(pathKey: key);
DoAssert(dicDoc);
}
[Test]
public void DictionaryDocument_Key()
{
var key = Guid.NewGuid();
var dicDoc = GetDictionaryDocument(keyVal: key);
DoAssert(dicDoc, keyVal: key);
}
[Test]
public void DictionaryDocument_Get_Children()
{
var child1 = GetDictionaryDocument(idVal: 222333);
var child2 = GetDictionaryDocument(idVal: 444555);
var dicDoc = GetDictionaryDocument(children: new List<IPublishedContent>()
{
child1, child2
});
Assert.AreEqual(2, dicDoc.Children.Count());
Assert.AreEqual(222333, dicDoc.Children.ElementAt(0).Id);
Assert.AreEqual(444555, dicDoc.Children.ElementAt(1).Id);
}
[Test]
public void Convert_From_Search_Result()
{
var ctx = GetUmbracoContext("/test");
var key = Guid.NewGuid();
var fields = new Dictionary<string, string>
{
{"__IndexType", "media"},
{"__NodeId", "1234"},
{"__NodeTypeAlias", Constants.Conventions.MediaTypes.Image},
{"__Path", "-1,1234"},
{"__nodeName", "Test"},
{"id", "1234"},
{"key", key.ToString()},
{"urlName", "/media/test.jpg"},
{"nodeType", "0"},
{"sortOrder", "0"},
{"level", "2"},
{"nodeName", "Test"},
{"nodeTypeAlias", Constants.Conventions.MediaTypes.Image},
{"parentID", "-1"},
{"path", "-1,1234"},
{"updateDate", DateTime.Parse("2012-07-16T10:34:09").Ticks.ToString()},
{"createDate", DateTime.Parse("2012-07-17T10:34:09").Ticks.ToString()},
{"creatorID", _testWriterAndCreatorId.ToString()},
{"creatorName", "Shannon"}
};
var result = new SearchResult("1234", 1, () => fields.ToDictionary(x => x.Key, x => new List<string> { x.Value }));
var store = new PublishedMediaCache(new XmlStore((XmlDocument)null, null, null, null, HostingEnvironment), ServiceContext.MediaService, ServiceContext.UserService, new DictionaryAppCache(), ContentTypesCache, Factory.GetRequiredService<IEntityXmlSerializer>(), Factory.GetRequiredService<IUmbracoContextAccessor>(), VariationContextAccessor);
var doc = store.CreateFromCacheValues(store.ConvertFromSearchResult(result));
DoAssert(doc, 1234, key, null, 0, "/media/test.jpg", "Image", 23, "Shannon", "Shannon", "-1,1234", DateTime.Parse("2012-07-17T10:34:09"), DateTime.Parse("2012-07-16T10:34:09"), 2);
Assert.AreEqual(null, doc.Parent);
}
[Test]
public void Convert_From_XPath_Navigator()
{
var ctx = GetUmbracoContext("/test");
var key = Guid.NewGuid();
var xmlDoc = GetMediaXml();
((XmlElement)xmlDoc.DocumentElement.FirstChild).SetAttribute("key", key.ToString());
var navigator = xmlDoc.SelectSingleNode("/root/Image").CreateNavigator();
var cache = new PublishedMediaCache(new XmlStore((XmlDocument)null, null, null, null, HostingEnvironment), ServiceContext.MediaService, ServiceContext.UserService, new DictionaryAppCache(), ContentTypesCache, Factory.GetRequiredService<IEntityXmlSerializer>(), Factory.GetRequiredService<IUmbracoContextAccessor>(),VariationContextAccessor);
var doc = cache.CreateFromCacheValues(cache.ConvertFromXPathNavigator(navigator, true));
DoAssert(doc, 2000, key, null, 2, "image1", "Image", 23, "Shannon", "Shannon", "-1,2000", DateTime.Parse("2012-06-12T14:13:17"), DateTime.Parse("2012-07-20T18:50:43"), 1);
Assert.AreEqual(null, doc.Parent);
Assert.AreEqual(2, doc.Children.Count());
Assert.AreEqual(2001, doc.Children.ElementAt(0).Id);
Assert.AreEqual(2002, doc.Children.ElementAt(1).Id);
}
private XmlDocument GetMediaXml()
{
var xml = @"<?xml version=""1.0"" encoding=""utf-8""?>
<!DOCTYPE root[
<!ELEMENT Home ANY>
<!ATTLIST Home id ID #REQUIRED>
<!ELEMENT CustomDocument ANY>
<!ATTLIST CustomDocument id ID #REQUIRED>
]>
<root id=""-1"">
<Image id=""2000"" parentID=""-1"" level=""1"" writerID=""[WriterId]"" creatorID=""[CreatorId]"" nodeType=""2044"" template=""0"" sortOrder=""2"" createDate=""2012-06-12T14:13:17"" updateDate=""2012-07-20T18:50:43"" nodeName=""Image1"" urlName=""image1"" path=""-1,2000"" isDoc="""">
<file><![CDATA[/media/1234/image1.png]]></file>
<Image id=""2001"" parentID=""2000"" level=""2"" writerID=""[WriterId]"" creatorID=""[CreatorId]"" nodeType=""2044"" template=""0"" sortOrder=""2"" createDate=""2012-06-12T14:13:17"" updateDate=""2012-07-20T18:50:43"" nodeName=""Image1"" urlName=""image1"" path=""-1,2000,2001"" isDoc="""">
<file><![CDATA[/media/1234/image1.png]]></file>
</Image>
<Image id=""2002"" parentID=""2000"" level=""2"" writerID=""[WriterId]"" creatorID=""[CreatorId]"" nodeType=""2044"" template=""0"" sortOrder=""2"" createDate=""2012-06-12T14:13:17"" updateDate=""2012-07-20T18:50:43"" nodeName=""Image1"" urlName=""image1"" path=""-1,2000,2002"" isDoc="""">
<file><![CDATA[/media/1234/image1.png]]></file>
</Image>
</Image>
</root>";
xml = xml.Replace("[WriterId]", _testWriterAndCreatorId.ToString());
xml = xml.Replace("[CreatorId]", _testWriterAndCreatorId.ToString());
var xmlDoc = new XmlDocument();
xmlDoc.LoadXml(xml);
return xmlDoc;
}
private Dictionary<string, string> GetDictionary(
int id,
Guid key,
int parentId,
string idKey,
string templateKey,
string nodeNameKey,
string nodeTypeAliasKey,
string pathKey)
{
return new Dictionary<string, string>()
{
{idKey, id.ToString()},
{"key", key.ToString()},
{templateKey, "0"},
{"sortOrder", "44"},
{nodeNameKey, "Testing"},
{"urlName", "testing"},
{nodeTypeAliasKey, "myType"},
{"nodeType", "22"},
{"writerID", _testWriterAndCreatorId.ToString()},
{"creatorID", _testWriterAndCreatorId.ToString()},
{pathKey, "1,2,3,4,5"},
{"createDate", "2012-01-02"},
{"updateDate", "2012-01-02"},
{"level", "3"},
{"parentID", parentId.ToString()}
};
}
private DictionaryPublishedContent GetDictionaryDocument(
string idKey = "id",
string templateKey = "template",
string nodeNameKey = "nodeName",
string nodeTypeAliasKey = "nodeTypeAlias",
string pathKey = "path",
int idVal = 1234,
Guid keyVal = default(Guid),
int parentIdVal = 321,
IEnumerable<IPublishedContent> children = null)
{
if (children == null)
children = new List<IPublishedContent>();
var dicDoc = new DictionaryPublishedContent(
//the dictionary
GetDictionary(idVal, keyVal, parentIdVal, idKey, templateKey, nodeNameKey, nodeTypeAliasKey, pathKey),
//callback to get the parent
d => new DictionaryPublishedContent(
// the dictionary
GetDictionary(parentIdVal, default(Guid), -1, idKey, templateKey, nodeNameKey, nodeTypeAliasKey, pathKey),
// callback to get the parent: there is no parent
a => null,
// callback to get the children: we're not going to test this so ignore
(dd, n) => new List<IPublishedContent>(),
// callback to get a property
(dd, a) => dd.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(a)),
null, // cache provider
VariationContextAccessor,
ContentTypesCache,
// no xpath
null,
// not from examine
false),
//callback to get the children
(dd, n) => children,
// callback to get a property
(dd, a) => dd.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(a)),
null, // cache provider
VariationContextAccessor,
ContentTypesCache,
// no xpath
null,
// not from examine
false);
return dicDoc;
}
private void DoAssert(
DictionaryPublishedContent dicDoc,
int idVal = 1234,
Guid keyVal = default(Guid),
int? templateIdVal = null,
int sortOrderVal = 44,
string urlNameVal = "testing",
string nodeTypeAliasVal = "myType",
int nodeTypeIdVal = 22,
string writerNameVal = "Shannon",
string creatorNameVal = "Shannon",
string pathVal = "1,2,3,4,5",
DateTime? createDateVal = null,
DateTime? updateDateVal = null,
int levelVal = 3,
int parentIdVal = 321)
{
if (!createDateVal.HasValue)
createDateVal = DateTime.Parse("2012-01-02");
if (!updateDateVal.HasValue)
updateDateVal = DateTime.Parse("2012-01-02");
DoAssert((IPublishedContent)dicDoc, idVal, keyVal, templateIdVal, sortOrderVal, urlNameVal, nodeTypeAliasVal, nodeTypeIdVal, writerNameVal,
creatorNameVal, pathVal, createDateVal, updateDateVal, levelVal);
//now validate the parentId that has been parsed, this doesn't exist on the IPublishedContent
Assert.AreEqual(parentIdVal, dicDoc.ParentId);
}
private void DoAssert(
IPublishedContent doc,
int idVal = 1234,
Guid keyVal = default(Guid),
int? templateIdVal = null,
int sortOrderVal = 44,
string urlNameVal = "testing",
string nodeTypeAliasVal = "myType",
int nodeTypeIdVal = 22,
string writerNameVal = "Shannon",
string creatorNameVal = "Shannon",
string pathVal = "1,2,3,4,5",
DateTime? createDateVal = null,
DateTime? updateDateVal = null,
int levelVal = 3)
{
if (!createDateVal.HasValue)
createDateVal = DateTime.Parse("2012-01-02");
if (!updateDateVal.HasValue)
updateDateVal = DateTime.Parse("2012-01-02");
Assert.AreEqual(idVal, doc.Id);
Assert.AreEqual(keyVal, doc.Key);
Assert.AreEqual(templateIdVal, doc.TemplateId);
Assert.AreEqual(sortOrderVal, doc.SortOrder);
Assert.AreEqual(urlNameVal, doc.UrlSegment);
Assert.AreEqual(nodeTypeAliasVal, doc.ContentType.Alias);
Assert.AreEqual(nodeTypeIdVal, doc.ContentType.Id);
Assert.AreEqual(writerNameVal, doc.GetWriterName(ServiceContext.UserService));
Assert.AreEqual(creatorNameVal, doc.GetCreatorName(ServiceContext.UserService));
Assert.AreEqual(_testWriterAndCreatorId, doc.WriterId);
Assert.AreEqual(_testWriterAndCreatorId, doc.CreatorId);
Assert.AreEqual(pathVal, doc.Path);
Assert.AreEqual(createDateVal.Value, doc.CreateDate);
Assert.AreEqual(updateDateVal.Value, doc.UpdateDate);
Assert.AreEqual(levelVal, doc.Level);
}
}
}

View File

@@ -1,100 +0,0 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Tests.TestHelpers;
namespace Umbraco.Tests.Issues
{
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, WithApplication = true)]
public class U9560 : TestWithDatabaseBase
{
[Test]
public void Test()
{
// create a content type and some properties
var contentType = new ContentType(ShortStringHelper, -1);
contentType.Alias = "test";
contentType.Name = "test";
var propertyType = new PropertyType(ShortStringHelper, "test", ValueStorageType.Ntext, "prop") { Name = "Prop", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = -88 };
contentType.PropertyTypeCollection.Add(propertyType);
ServiceContext.ContentTypeService.Save(contentType);
var aliasName = string.Empty;
// read fields, same as what we do with PetaPoco Fetch<dynamic>
using (var db = Factory.GetRequiredService<IUmbracoDatabaseFactory>().CreateDatabase())
{
db.OpenSharedConnection();
try
{
var conn = db.Connection;
var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT mandatory, dataTypeId, propertyTypeGroupId, contentTypeId, sortOrder, alias, name, validationRegExp, description from cmsPropertyType where id=" + propertyType.Id;
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
for (var i = 0; i < reader.FieldCount; i++)
Console.WriteLine(reader.GetName(i));
aliasName = reader.GetName(5);
}
}
}
finally
{
db.CloseSharedConnection();
}
}
// note that although the query is for 'alias' the field is named 'Alias'
Assert.AreEqual("Alias", aliasName);
// try differently
using (var db = Factory.GetRequiredService<IUmbracoDatabaseFactory>().CreateDatabase())
{
db.OpenSharedConnection();
try
{
var conn = db.Connection;
var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT mandatory, dataTypeId, propertyTypeGroupId, contentTypeId, sortOrder, alias as alias, name, validationRegExp, description from cmsPropertyType where id=" + propertyType.Id;
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
for (var i = 0; i < reader.FieldCount; i++)
Console.WriteLine(reader.GetName(i));
aliasName = reader.GetName(5);
}
}
}
finally
{
db.CloseSharedConnection();
}
}
// and now it is OK
Assert.AreEqual("alias", aliasName);
//// get the legacy content type
//var legacyContentType = new umbraco.cms.businesslogic.ContentType(contentType.Id);
//Assert.AreEqual("test", legacyContentType.Alias);
//// get the legacy properties
//var legacyProperties = legacyContentType.PropertyTypes;
//// without the fix, due to some (swallowed) inner exception, we have no properties
////Assert.IsNull(legacyProperties);
//// thanks to the fix, it works
//Assert.IsNotNull(legacyProperties);
//Assert.AreEqual(1, legacyProperties.Count);
//Assert.AreEqual("prop", legacyProperties[0].Alias);
}
}
}

View File

@@ -1,24 +0,0 @@
using NPoco;
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
namespace Umbraco.Tests.LegacyXmlPublishedCache
{
[TableName("cmsContentXml")]
[PrimaryKey("nodeId", AutoIncrement = false)]
[ExplicitColumns]
internal class ContentXmlDto
{
[Column("nodeId")]
[PrimaryKeyColumn(AutoIncrement = false)]
[ForeignKey(typeof(ContentDto), Column = "nodeId")]
public int NodeId { get; set; }
[Column("xml")]
[SpecialDbType(SpecialDbTypes.NTEXT)]
public string Xml { get; set; }
[Column("rv")]
public long Rv { get; set; }
}
}

View File

@@ -1,221 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.XPath;
using Examine;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
using Umbraco.Web.Composing;
namespace Umbraco.Tests.LegacyXmlPublishedCache
{
/// <summary>
/// An IPublishedContent that is represented all by a dictionary.
/// </summary>
/// <remarks>
/// This is a helper class and definitely not intended for public use, it expects that all of the values required
/// to create an IPublishedContent exist in the dictionary by specific aliases.
/// </remarks>
internal class DictionaryPublishedContent : PublishedContentBase
{
// note: I'm not sure this class fully complies with IPublishedContent rules especially
// I'm not sure that _properties contains all properties including those without a value,
// neither that GetProperty will return a property without a value vs. null... @zpqrtbnk
// List of properties that will appear in the XML and do not match
// anything in the ContentType, so they must be ignored.
private static readonly string[] IgnoredKeys = { "version", "isDoc" };
public DictionaryPublishedContent(
IReadOnlyDictionary<string, string> valueDictionary,
Func<int, IPublishedContent> getParent,
Func<int, XPathNavigator, IEnumerable<IPublishedContent>> getChildren,
Func<DictionaryPublishedContent, string, IPublishedProperty> getProperty,
IAppCache appCache,
IVariationContextAccessor variationContextAccessor,
PublishedContentTypeCache contentTypeCache,
XPathNavigator nav,
bool fromExamine):base(variationContextAccessor)
{
if (valueDictionary == null) throw new ArgumentNullException(nameof(valueDictionary));
if (getParent == null) throw new ArgumentNullException(nameof(getParent));
if (getProperty == null) throw new ArgumentNullException(nameof(getProperty));
_getParent = new Lazy<IPublishedContent>(() => getParent(ParentId));
_getChildren = new Lazy<IEnumerable<IPublishedContent>>(() => getChildren(Id, nav));
_getProperty = getProperty;
_appCache = appCache;
LoadedFromExamine = fromExamine;
ValidateAndSetProperty(valueDictionary, val => _id = Int32.Parse(val), "id", "nodeId", "__NodeId"); //should validate the int!
ValidateAndSetProperty(valueDictionary, val => _key = Guid.Parse(val), "key", "__key", "__Key");
//ValidateAndSetProperty(valueDictionary, val => _templateId = int.Parse(val), "template", "templateId");
ValidateAndSetProperty(valueDictionary, val => _sortOrder = Int32.Parse(val), "sortOrder");
ValidateAndSetProperty(valueDictionary, val => _name = val, "nodeName");
ValidateAndSetProperty(valueDictionary, val => _urlName = val, "urlName");
ValidateAndSetProperty(valueDictionary, val => _documentTypeAlias = val, "nodeTypeAlias", ExamineFieldNames.ItemTypeFieldName);
ValidateAndSetProperty(valueDictionary, val => _documentTypeId = Int32.Parse(val), "nodeType");
//ValidateAndSetProperty(valueDictionary, val => _writerId = int.Parse(val), "writerID");
ValidateAndSetProperty(valueDictionary, val => _creatorId = Int32.Parse(val), "creatorID", "writerID"); //this is a bit of a hack fix for: U4-1132
ValidateAndSetProperty(valueDictionary, val => _path = val, "path", "__Path");
ValidateAndSetProperty(valueDictionary, val => _createDate = ParseDateTimeValue(val), "createDate");
ValidateAndSetProperty(valueDictionary, val => _updateDate = ParseDateTimeValue(val), "updateDate");
ValidateAndSetProperty(valueDictionary, val => _level = Int32.Parse(val), "level");
ValidateAndSetProperty(valueDictionary, val =>
{
int pId;
ParentId = -1;
if (Int32.TryParse(val, out pId))
{
ParentId = pId;
}
}, "parentID");
_contentType = contentTypeCache.Get(PublishedItemType.Media, _documentTypeAlias);
_properties = new Collection<IPublishedProperty>();
//handle content type properties
//make sure we create them even if there's no value
foreach (var propertyType in _contentType.PropertyTypes)
{
var alias = propertyType.Alias;
_keysAdded.Add(alias);
string value;
const bool isPreviewing = false; // false :: never preview a media
var property = valueDictionary.TryGetValue(alias, out value) == false || value == null
? new XmlPublishedProperty(propertyType, this, isPreviewing)
: new XmlPublishedProperty(propertyType, this, isPreviewing, value);
_properties.Add(property);
}
//loop through remaining values that haven't been applied
foreach (var i in valueDictionary.Where(x =>
_keysAdded.Contains(x.Key) == false // not already processed
&& IgnoredKeys.Contains(x.Key) == false)) // not ignorable
{
if (i.Key.InvariantStartsWith("__"))
{
// no type for that one, dunno how to convert, drop it
//IPublishedProperty property = new PropertyResult(i.Key, i.Value, PropertyResultType.CustomProperty);
//_properties.Add(property);
}
else
{
// this is a property that does not correspond to anything, ignore and log
Current.Logger.LogWarning("Dropping property '{PropertyKey}' because it does not belong to the content type.", i.Key);
}
}
}
private DateTime ParseDateTimeValue(string val)
{
if (LoadedFromExamine == false)
return DateTime.Parse(val);
//we need to parse the date time using Lucene converters
var ticks = Int64.Parse(val);
return new DateTime(ticks);
}
/// <summary>
/// Flag to get/set if this was loaded from examine cache
/// </summary>
internal bool LoadedFromExamine { get; }
//private readonly Func<DictionaryPublishedContent, IPublishedContent> _getParent;
private readonly Lazy<IPublishedContent> _getParent;
//private readonly Func<DictionaryPublishedContent, IEnumerable<IPublishedContent>> _getChildren;
private readonly Lazy<IEnumerable<IPublishedContent>> _getChildren;
private readonly Func<DictionaryPublishedContent, string, IPublishedProperty> _getProperty;
private readonly IAppCache _appCache;
/// <summary>
/// Returns 'Media' as the item type
/// </summary>
public override PublishedItemType ItemType => PublishedItemType.Media;
public override IPublishedContent Parent => _getParent.Value;
public int ParentId { get; private set; }
public override int Id => _id;
public override Guid Key => _key;
public override int? TemplateId => null;
public override int SortOrder => _sortOrder;
public override string Name => _name;
private static readonly Lazy<Dictionary<string, PublishedCultureInfo>> NoCultures = new Lazy<Dictionary<string, PublishedCultureInfo>>(() => new Dictionary<string, PublishedCultureInfo>());
public override IReadOnlyDictionary<string, PublishedCultureInfo> Cultures => NoCultures.Value;
public override string UrlSegment => _urlName;
public override int WriterId => _creatorId;
public override int CreatorId => _creatorId;
public override string Path => _path;
public override DateTime CreateDate => _createDate;
public override DateTime UpdateDate => _updateDate;
public override int Level => _level;
public override bool IsDraft(string culture = null) => false;
public override bool IsPublished(string culture = null) => true;
public override IEnumerable<IPublishedProperty> Properties => _properties;
public override IEnumerable<IPublishedContent> Children => _getChildren.Value;
public override IEnumerable<IPublishedContent> ChildrenForAllCultures => Children;
public override IPublishedProperty GetProperty(string alias)
{
return _getProperty(this, alias);
}
public override IPublishedContentType ContentType => _contentType;
private readonly List<string> _keysAdded = new List<string>();
private int _id;
private Guid _key;
//private int _templateId;
private int _sortOrder;
private string _name;
private string _urlName;
private string _documentTypeAlias;
private int _documentTypeId;
//private int _writerId;
private int _creatorId;
private string _path;
private DateTime _createDate;
private DateTime _updateDate;
//private Guid _version;
private int _level;
private readonly ICollection<IPublishedProperty> _properties;
private readonly IPublishedContentType _contentType;
private void ValidateAndSetProperty(IReadOnlyDictionary<string, string> valueDictionary, Action<string> setProperty, params string[] potentialKeys)
{
var key = potentialKeys.FirstOrDefault(x => valueDictionary.ContainsKey(x) && valueDictionary[x] != null);
if (key == null)
{
throw new FormatException("The valueDictionary is not formatted correctly and is missing any of the '" + String.Join(",", potentialKeys) + "' elements");
}
setProperty(valueDictionary[key]);
_keysAdded.Add(key);
}
}
}

View File

@@ -1,37 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Tests.LegacyXmlPublishedCache
{
internal class DomainCache : IDomainCache
{
private readonly IDomainService _domainService;
public DomainCache(IDomainService domainService, IDefaultCultureAccessor defaultCultureAccessor)
{
_domainService = domainService;
DefaultCulture = defaultCultureAccessor.DefaultCulture;
}
/// <inheritdoc />
public IEnumerable<Domain> GetAll(bool includeWildcards) => _domainService.GetAll(includeWildcards)
.Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false)
.Select(x => new Domain(x.Id, x.DomainName, x.RootContentId.Value, x.LanguageIsoCode, x.IsWildcard));
/// <inheritdoc />
public IEnumerable<Domain> GetAssigned(int documentId, bool includeWildcards = false) => _domainService.GetAssignedDomains(documentId, includeWildcards)
.Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false)
.Select(x => new Domain(x.Id, x.DomainName, x.RootContentId.Value, x.LanguageIsoCode, x.IsWildcard));
/// <inheritdoc />
public bool HasAssigned(int documentId, bool includeWildcards = false)
=> documentId > 0 && GetAssigned(documentId, includeWildcards).Any();
/// <inheritdoc />
public string DefaultCulture { get; }
}
}

View File

@@ -1,863 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Runtime;
namespace Umbraco.Web.Scheduling
{
/// <summary>
/// Manages a queue of tasks and runs them in the background.
/// </summary>
/// <remarks>This class exists for logging purposes - the one you want to use is BackgroundTaskRunner{T}.</remarks>
public abstract class BackgroundTaskRunner
{
/// <summary>
/// Represents a MainDom hook.
/// </summary>
public class MainDomHook
{
/// <summary>
/// Initializes a new instance of the <see cref="MainDomHook"/> class.
/// </summary>
/// <param name="mainDom">The <see cref="IMainDom"/> object.</param>
/// <param name="install">A method to execute when hooking into the main domain.</param>
/// <param name="release">A method to execute when the main domain releases.</param>
public MainDomHook(IMainDom mainDom, Action install, Action release)
{
MainDom = mainDom;
Install = install;
Release = release;
}
/// <summary>
/// Gets the <see cref="IMainDom"/> object.
/// </summary>
public IMainDom MainDom { get; }
/// <summary>
/// Gets the method to execute when hooking into the main domain.
/// </summary>
public Action Install { get; }
/// <summary>
/// Gets the method to execute when the main domain releases.
/// </summary>
public Action Release { get; }
internal bool Register()
{
if (MainDom != null)
{
return MainDom.Register(Install, Release);
}
// tests
Install?.Invoke();
return true;
}
}
}
/// <summary>
/// Manages a queue of tasks of type <typeparamref name="T"/> and runs them in the background.
/// </summary>
/// <typeparam name="T">The type of the managed tasks.</typeparam>
/// <remarks>The task runner is web-aware and will ensure that it shuts down correctly when the AppDomain
/// shuts down (ie is unloaded).</remarks>
public class BackgroundTaskRunner<T> : BackgroundTaskRunner, IBackgroundTaskRunner<T>
where T : class, IBackgroundTask
{
// do not remove this comment!
//
// if you plan to do anything on this class, first go and read
// http://blog.stephencleary.com/2012/12/dont-block-in-asynchronous-code.html
// http://stackoverflow.com/questions/19481964/calling-taskcompletionsource-setresult-in-a-non-blocking-manner
// http://stackoverflow.com/questions/21225361/is-there-anything-like-asynchronous-blockingcollectiont
// and more, and more, and more
// and remember: async is hard
private readonly string _logPrefix;
private readonly BackgroundTaskRunnerOptions _options;
private readonly ILogger<BackgroundTaskRunner<T>> _logger;
private readonly IApplicationShutdownRegistry _applicationShutdownRegistry;
private readonly object _locker = new object();
private readonly BufferBlock<T> _tasks = new BufferBlock<T>(new DataflowBlockOptions());
// in various places we are testing these vars outside a lock, so make them volatile
private volatile bool _isRunning; // is running
private volatile bool _completed; // does not accept tasks anymore, may still be running
private Task _runningTask; // the threading task that is currently executing background tasks
private CancellationTokenSource _shutdownTokenSource; // used to cancel everything and shutdown
private CancellationTokenSource _cancelTokenSource; // used to cancel the current task
private CancellationToken _shutdownToken;
private bool _terminating; // ensures we raise that event only once
private bool _terminated; // remember we've terminated
private readonly TaskCompletionSource<int> _terminatedSource = new TaskCompletionSource<int>(); // enable awaiting termination
/// <summary>
/// Initializes a new instance of the <see cref="BackgroundTaskRunner{T}"/> class.
/// </summary>
/// <param name="logger">A logger.</param>
/// <param name="applicationShutdownRegistry">The application shutdown registry</param>
/// <param name="hook">An optional main domain hook.</param>
public BackgroundTaskRunner(ILogger<BackgroundTaskRunner<T>> logger, IApplicationShutdownRegistry applicationShutdownRegistry, MainDomHook hook = null)
: this(typeof(T).FullName, new BackgroundTaskRunnerOptions(), logger, applicationShutdownRegistry, hook)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="BackgroundTaskRunner{T}"/> class.
/// </summary>
/// <param name="name">The name of the runner.</param>
/// <param name="logger">A logger.</param>
/// <param name="applicationShutdownRegistry">The application shutdown registry</param>
/// <param name="hook">An optional main domain hook.</param>
public BackgroundTaskRunner(string name, ILogger<BackgroundTaskRunner<T>> logger, IApplicationShutdownRegistry applicationShutdownRegistry, MainDomHook hook = null)
: this(name, new BackgroundTaskRunnerOptions(), logger, applicationShutdownRegistry, hook)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="BackgroundTaskRunner{T}"/> class with a set of options.
/// </summary>
/// <param name="options">The set of options.</param>
/// <param name="logger">A logger.</param>
/// <param name="applicationShutdownRegistry">The application shutdown registry</param>
/// <param name="hook">An optional main domain hook.</param>
public BackgroundTaskRunner(BackgroundTaskRunnerOptions options, ILogger<BackgroundTaskRunner<T>> logger, IApplicationShutdownRegistry applicationShutdownRegistry, MainDomHook hook = null)
: this(typeof(T).FullName, options, logger, applicationShutdownRegistry, hook)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="BackgroundTaskRunner{T}"/> class with a set of options.
/// </summary>
/// <param name="name">The name of the runner.</param>
/// <param name="options">The set of options.</param>
/// <param name="logger">A logger.</param>
/// <param name="applicationShutdownRegistry">The application shutdown registry</param>
/// <param name="hook">An optional main domain hook.</param>
public BackgroundTaskRunner(string name, BackgroundTaskRunnerOptions options, ILogger<BackgroundTaskRunner<T>> logger, IApplicationShutdownRegistry applicationShutdownRegistry, MainDomHook hook = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_applicationShutdownRegistry = applicationShutdownRegistry;
_logPrefix = "[" + name + "] ";
if (options.Hosted)
_applicationShutdownRegistry.RegisterObject(this);
if (hook != null)
_completed = _terminated = hook.Register() == false;
if (options.AutoStart && _terminated == false)
StartUp();
}
/// <summary>
/// Gets the number of tasks in the queue.
/// </summary>
public int TaskCount => _tasks.Count;
/// <summary>
/// Gets a value indicating whether a threading task is currently running.
/// </summary>
public bool IsRunning => _isRunning;
/// <summary>
/// Gets a value indicating whether the runner has completed and cannot accept tasks anymore.
/// </summary>
public bool IsCompleted => _completed;
/// <summary>
/// Gets the running threading task as an immutable awaitable.
/// </summary>
/// <exception cref="InvalidOperationException">There is no running task.</exception>
/// <remarks>
/// <para>Unless the AutoStart option is true, there will be no current threading task until
/// a background task is added to the queue, and there will be no current threading task
/// when the queue is empty. In which case this method returns null.</para>
/// <para>The returned value can be awaited and that is all (eg no continuation).</para>
/// </remarks>
internal ThreadingTaskImmutable CurrentThreadingTask
{
get
{
lock (_locker)
{
return _runningTask == null ? null : new ThreadingTaskImmutable(_runningTask);
}
}
}
/// <summary>
/// Gets an awaitable used to await the runner running operation.
/// </summary>
/// <returns>An awaitable instance.</returns>
/// <remarks>Used to wait until the runner is no longer running (IsRunning == false),
/// though the runner could be started again afterwards by adding tasks to it. If
/// the runner is not running, returns a completed awaitable.</remarks>
public ThreadingTaskImmutable StoppedAwaitable
{
get
{
lock (_locker)
{
var task = _runningTask ?? Task.CompletedTask;
return new ThreadingTaskImmutable(task);
}
}
}
/// <summary>
/// Gets an awaitable object that can be used to await for the runner to terminate.
/// </summary>
/// <returns>An awaitable object.</returns>
/// <remarks>
/// <para>Used to wait until the runner has terminated.</para>
/// <para>
/// The only time the runner will be terminated is by the Hosting Environment when the application is being shutdown.
/// </para>
/// </remarks>
internal ThreadingTaskImmutable TerminatedAwaitable
{
get
{
lock (_locker)
{
return new ThreadingTaskImmutable(_terminatedSource.Task);
}
}
}
/// <summary>
/// Adds a task to the queue.
/// </summary>
/// <param name="task">The task to add.</param>
/// <exception cref="InvalidOperationException">The task runner has completed.</exception>
public void Add(T task)
{
lock (_locker)
{
if (_completed)
throw new InvalidOperationException("The task runner has completed.");
// add task
_logger.LogDebug("{LogPrefix} Task Added {TaskType}", _logPrefix , task.GetType().FullName);
_tasks.Post(task);
// start
StartUpLocked();
}
}
/// <summary>
/// Tries to add a task to the queue.
/// </summary>
/// <param name="task">The task to add.</param>
/// <returns>true if the task could be added to the queue; otherwise false.</returns>
/// <remarks>Returns false if the runner is completed.</remarks>
public bool TryAdd(T task)
{
lock (_locker)
{
if (_completed)
{
_logger.LogDebug("{LogPrefix} Task cannot be added {TaskType}, the task runner has already shutdown", _logPrefix, task.GetType().FullName);
return false;
}
// add task
_logger.LogDebug("{LogPrefix} Task added {TaskType}", _logPrefix, task.GetType().FullName);
_tasks.Post(task);
// start
StartUpLocked();
return true;
}
}
/// <summary>
/// Cancels to current task, if any.
/// </summary>
/// <remarks>Has no effect if the task runs synchronously, or does not want to cancel.</remarks>
public void CancelCurrentBackgroundTask()
{
lock (_locker)
{
if (_completed)
throw new InvalidOperationException("The task runner has completed.");
_cancelTokenSource?.Cancel();
}
}
/// <summary>
/// Starts the tasks runner, if not already running.
/// </summary>
/// <remarks>Is invoked each time a task is added, to ensure it is going to be processed.</remarks>
/// <exception cref="InvalidOperationException">The task runner has completed.</exception>
internal void StartUp()
{
if (_isRunning) return;
lock (_locker)
{
if (_completed)
throw new InvalidOperationException("The task runner has completed.");
StartUpLocked();
}
}
/// <summary>
/// Starts the tasks runner, if not already running.
/// </summary>
/// <remarks>Must be invoked within lock(_locker) and with _isCompleted being false.</remarks>
private void StartUpLocked()
{
// double check
if (_isRunning) return;
_isRunning = true;
// create a new token source since this is a new process
_shutdownTokenSource = new CancellationTokenSource();
_shutdownToken = _shutdownTokenSource.Token;
using (ExecutionContext.SuppressFlow()) // Do not flow AsyncLocal to the child thread
{
_runningTask = Task.Run(async () => await Pump().ConfigureAwait(false), _shutdownToken);
}
_logger.LogDebug("{LogPrefix} Starting", _logPrefix);
}
/// <summary>
/// Shuts the tasks runner down.
/// </summary>
/// <param name="force">True for force the runner to stop.</param>
/// <param name="wait">True to wait until the runner has stopped.</param>
/// <remarks>If <paramref name="force"/> is false, no more tasks can be queued but all queued tasks
/// will run. If it is true, then only the current one (if any) will end and no other task will run.</remarks>
public void Shutdown(bool force, bool wait)
{
lock (_locker)
{
_completed = true; // do not accept new tasks
if (_isRunning == false) return; // done already
}
var hasTasks = TaskCount > 0;
if (!force && hasTasks)
{
_logger.LogInformation("{LogPrefix} Waiting for tasks to complete", _logPrefix);
}
// complete the queue
// will stop waiting on the queue or on a latch
_tasks.Complete();
if (force)
{
// we must bring everything down, now
lock (_locker)
{
// was Complete() enough?
// if _tasks.Complete() ended up triggering code to stop the runner and reset
// the _isRunning flag, then there's no need to initiate a cancel on the cancelation token.
if (_isRunning == false)
return;
}
// try to cancel running async tasks (cannot do much about sync tasks)
// break latched tasks
// stop processing the queue
_shutdownTokenSource?.Cancel(false); // false is the default
_shutdownTokenSource?.Dispose();
_shutdownTokenSource = null;
}
// tasks in the queue will be executed...
if (!wait) return;
_runningTask?.Wait(CancellationToken.None); // wait for whatever is running to end...
}
private async Task Pump()
{
while (true)
{
// get the next task
// if it returns null the runner is going down, stop
var bgTask = await GetNextBackgroundTask(_shutdownToken);
if (bgTask == null) return;
// set a cancellation source so that the current task can be cancelled
// link from _shutdownToken so that we can use _cancelTokenSource for both
lock (_locker)
{
_cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_shutdownToken);
}
try
{
// wait for latch should return the task
// if it returns null it's either that the task has been cancelled
// or the whole runner is going down - in both cases, continue,
// and GetNextBackgroundTask will take care of shutdowns
bgTask = await WaitForLatch(bgTask, _cancelTokenSource.Token);
if (bgTask != null)
{
// executes & be safe - RunAsync should NOT throw but only raise an event,
// but... just make sure we never ever take everything down
try
{
await RunAsync(bgTask, _cancelTokenSource.Token).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "{LogPrefix} Task runner exception", _logPrefix);
}
}
}
finally
{
// done
lock (_locker)
{
// always dispose CancellationTokenSource when you are done using them
// https://lowleveldesign.org/2015/11/30/catch-in-cancellationtokensource/
_cancelTokenSource.Dispose();
_cancelTokenSource = null;
}
}
}
}
// gets the next background task from the buffer
private async Task<T> GetNextBackgroundTask(CancellationToken token)
{
while (true)
{
var task = await GetNextBackgroundTask2(token);
if (task != null) return task;
lock (_locker)
{
// deal with race condition
if (_shutdownToken.IsCancellationRequested == false && TaskCount > 0) continue;
// if we really have nothing to do, stop
_logger.LogDebug("{LogPrefix} Stopping", _logPrefix);
if (_options.PreserveRunningTask == false)
_runningTask = null;
_isRunning = false;
_shutdownToken = CancellationToken.None;
}
OnEvent(Stopped, "Stopped");
return null;
}
}
private async Task<T> GetNextBackgroundTask2(CancellationToken shutdownToken)
{
// exit if canceling
if (shutdownToken.IsCancellationRequested)
return null;
// if KeepAlive is false then don't block, exit if there is
// no task in the buffer - yes, there is a race condition, which
// we'll take care of
if (_options.KeepAlive == false && TaskCount == 0)
return null;
try
{
// A Task<TResult> that informs of whether and when more output is available. If, when the
// task completes, its Result is true, more output is available in the source (though another
// consumer of the source may retrieve the data). If it returns false, more output is not
// and will never be available, due to the source completing prior to output being available.
var output = await _tasks.OutputAvailableAsync(shutdownToken); // block until output or cancelled
if (output == false) return null;
}
catch (TaskCanceledException)
{
return null;
}
try
{
// A task that represents the asynchronous receive operation. When an item value is successfully
// received from the source, the returned task is completed and its Result returns the received
// value. If an item value cannot be retrieved because the source is empty and completed, an
// InvalidOperationException exception is thrown in the returned task.
// the source cannot be empty *and* completed here - we know we have output
return await _tasks.ReceiveAsync(shutdownToken);
}
catch (TaskCanceledException)
{
return null;
}
}
// if bgTask is not a latched background task, or if it is not latched, returns immediately
// else waits for the latch, taking care of completion and shutdown and whatnot
private async Task<T> WaitForLatch(T bgTask, CancellationToken token)
{
var latched = bgTask as ILatchedBackgroundTask;
if (latched == null || latched.IsLatched == false) return bgTask;
// support canceling awaiting
// read https://github.com/dotnet/corefx/issues/2704
// read http://stackoverflow.com/questions/27238232/how-can-i-cancel-task-whenall
var tokenTaskSource = new TaskCompletionSource<bool>();
token.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tokenTaskSource);
// returns the task that completed
// - latched.Latch completes when the latch releases
// - _tasks.Completion completes when the runner completes
// - tokenTaskSource.Task completes when this task, or the whole runner is cancelled
var task = await Task.WhenAny(latched.Latch, _tasks.Completion, tokenTaskSource.Task);
// ok to run now
if (task == latched.Latch)
return bgTask;
// we are shutting down if the _tasks.Complete(); was called or the shutdown token was cancelled
var isShuttingDown = _shutdownToken.IsCancellationRequested || task == _tasks.Completion;
// if shutting down, return the task only if it runs on shutdown
if (isShuttingDown && latched.RunsOnShutdown)
return bgTask;
// else, either it does not run on shutdown or it's been cancelled, dispose
latched.Dispose();
return null;
}
// runs the background task, taking care of shutdown (as far as possible - cannot abort
// a non-async Run for example, so we'll do our best)
private async Task RunAsync(T bgTask, CancellationToken token)
{
try
{
OnTaskStarting(new TaskEventArgs<T>(bgTask));
try
{
try
{
if (bgTask.IsAsync)
{
// configure await = false since we don't care about the context, we're on a background thread.
await bgTask.RunAsync(token).ConfigureAwait(false);
}
else
{
bgTask.Run();
}
}
finally // ensure we disposed - unless latched again ie wants to re-run
{
if (!(bgTask is ILatchedBackgroundTask lbgTask) || lbgTask.IsLatched == false)
{
bgTask.Dispose();
}
}
}
catch (Exception e)
{
OnTaskError(new TaskEventArgs<T>(bgTask, e));
throw;
}
OnTaskCompleted(new TaskEventArgs<T>(bgTask));
}
catch (Exception ex)
{
_logger.LogError(ex, "{LogPrefix} Task has failed", _logPrefix);
}
}
#region Events
// triggers when a background task starts
public event TypedEventHandler<BackgroundTaskRunner<T>, TaskEventArgs<T>> TaskStarting;
// triggers when a background task has completed
public event TypedEventHandler<BackgroundTaskRunner<T>, TaskEventArgs<T>> TaskCompleted;
// triggers when a background task throws
public event TypedEventHandler<BackgroundTaskRunner<T>, TaskEventArgs<T>> TaskError;
// triggers when a background task is cancelled
public event TypedEventHandler<BackgroundTaskRunner<T>, TaskEventArgs<T>> TaskCancelled;
// triggers when the runner stops (but could start again if a task is added to it)
internal event TypedEventHandler<BackgroundTaskRunner<T>, EventArgs> Stopped;
// triggers when the hosting environment requests that the runner terminates
internal event TypedEventHandler<BackgroundTaskRunner<T>, EventArgs> Terminating;
// triggers when the hosting environment has terminated (no task can be added, no task is running)
internal event TypedEventHandler<BackgroundTaskRunner<T>, EventArgs> Terminated;
private void OnEvent(TypedEventHandler<BackgroundTaskRunner<T>, EventArgs> handler, string name)
{
OnEvent(handler, name, EventArgs.Empty);
}
private void OnEvent<TArgs>(TypedEventHandler<BackgroundTaskRunner<T>, TArgs> handler, string name, TArgs e)
{
_logger.LogDebug("{LogPrefix} OnEvent {EventName}", _logPrefix, name);
if (handler == null) return;
try
{
handler(this, e);
}
catch (Exception ex)
{
_logger.LogError(ex, "{LogPrefix} {Name} exception occurred", _logPrefix, name);
}
}
protected virtual void OnTaskError(TaskEventArgs<T> e)
{
OnEvent(TaskError, "TaskError", e);
}
protected virtual void OnTaskStarting(TaskEventArgs<T> e)
{
OnEvent(TaskStarting, "TaskStarting", e);
}
protected virtual void OnTaskCompleted(TaskEventArgs<T> e)
{
OnEvent(TaskCompleted, "TaskCompleted", e);
}
protected virtual void OnTaskCancelled(TaskEventArgs<T> e)
{
OnEvent(TaskCancelled, "TaskCancelled", e);
// dispose it
e.Task.Dispose();
}
#endregion
#region IDisposable
private readonly object _disposalLocker = new object();
public bool IsDisposed { get; private set; }
~BackgroundTaskRunner()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (IsDisposed || disposing == false)
return;
lock (_disposalLocker)
{
if (IsDisposed)
return;
DisposeResources();
IsDisposed = true;
}
}
protected virtual void DisposeResources()
{
// just make sure we eventually go down
Shutdown(true, false);
}
#endregion
#region IRegisteredObject.Stop
/// <summary>
/// Used by IRegisteredObject.Stop and shutdown on threadpool threads to not block shutdown times.
/// </summary>
/// <param name="immediate"></param>
/// <returns>
/// An awaitable Task that is used to handle the shutdown.
/// </returns>
internal Task StopInternal(bool immediate)
{
// the first time the hosting environment requests that the runner terminates,
// raise the Terminating event - that could be used to prevent any process that
// would expect the runner to be available from starting.
var onTerminating = false;
lock (_locker)
{
if (_terminating == false)
{
_terminating = true;
_logger.LogInformation("{LogPrefix} Terminating {Immediate}", _logPrefix, immediate ? immediate.ToString() : string.Empty);
onTerminating = true;
}
}
if (onTerminating)
OnEvent(Terminating, "Terminating");
// Run the Stop commands on another thread since IRegisteredObject.Stop calls are called sequentially
// with a single aspnet thread during shutdown and we don't want to delay other calls to IRegisteredObject.Stop.
if (!immediate)
{
using (ExecutionContext.SuppressFlow())
{
return Task.Run(StopInitial, CancellationToken.None);
}
}
else
{
lock (_locker)
{
if (_terminated) return Task.CompletedTask;
using (ExecutionContext.SuppressFlow())
{
return Task.Run(StopImmediate, CancellationToken.None);
}
}
}
}
/// <summary>
/// Requests a registered object to un-register.
/// </summary>
/// <param name="immediate">true to indicate the registered object should un-register from the hosting
/// environment before returning; otherwise, false.</param>
/// <remarks>
/// <para>"When the application manager needs to stop a registered object, it will call the Stop method."</para>
/// <para>The application manager will call the Stop method to ask a registered object to un-register. During
/// processing of the Stop method, the registered object must call the applicationShutdownRegistry.UnregisterObject method.</para>
/// </remarks>
public void Stop(bool immediate) => StopInternal(immediate);
/// <summary>
/// Called when immediate == false for IRegisteredObject.Stop(bool immediate)
/// </summary>
/// <remarks>
/// Called on a threadpool thread
/// </remarks>
private void StopInitial()
{
// immediate == false when the app is trying to wind down, immediate == true will be called either:
// after a call with immediate == false or if the app is not trying to wind down and needs to immediately stop.
// So Stop may be called twice or sometimes only once.
try
{
Shutdown(false, false); // do not accept any more tasks, flush the queue, do not wait
}
finally
{
// raise the completed event only after the running threading task has completed
lock (_locker)
{
if (_runningTask != null)
{
_runningTask.ContinueWith(
_ => StopImmediate(),
// Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html
TaskScheduler.Default);
}
else
{
StopImmediate();
}
}
}
// If the shutdown token was not canceled in the Shutdown call above, it means there was still tasks
// being processed, in which case we'll give it a couple seconds
if (!_shutdownToken.IsCancellationRequested)
{
// If we are called with immediate == false, wind down above and then shutdown within 2 seconds,
// we want to shut down the app as quick as possible, if we wait until immediate == true, this can
// take a very long time since immediate will only be true when a new request is received on the new
// appdomain (or another iis timeout occurs ... which can take some time).
Thread.Sleep(2000); //we are already on a threadpool thread
StopImmediate();
}
}
/// <summary>
/// Called when immediate == true for IRegisteredObject.Stop(bool immediate)
/// </summary>
/// <remarks>
/// Called on a threadpool thread
/// </remarks>
private void StopImmediate()
{
_logger.LogInformation("{LogPrefix} Canceling tasks", _logPrefix);
try
{
Shutdown(true, true); // cancel all tasks, wait for the current one to end
}
finally
{
Terminate(true);
}
}
// called by Stop either immediately or eventually
private void Terminate(bool immediate)
{
// signal the environment we have terminated
// log
// raise the Terminated event
// complete the awaitable completion source, if any
if (immediate)
{
//only unregister when it's the final call, else we won't be notified of the final call
_applicationShutdownRegistry.UnregisterObject(this);
}
if (_terminated) return; // already taken care of
TaskCompletionSource<int> terminatedSource;
lock (_locker)
{
_terminated = true;
terminatedSource = _terminatedSource;
}
_logger.LogInformation("{LogPrefix} Tasks {TaskStatus}, terminated",
_logPrefix,
immediate ? "cancelled" : "completed");
OnEvent(Terminated, "Terminated");
terminatedSource.TrySetResult(0);
}
#endregion
}
}

View File

@@ -1,53 +0,0 @@
namespace Umbraco.Web.Scheduling
{
/// <summary>
/// Provides options to the <see cref="BackgroundTaskRunner{T}"/> class.
/// </summary>
public class BackgroundTaskRunnerOptions
{
// TODO: Could add options for using a stack vs queue if required
/// <summary>
/// Initializes a new instance of the <see cref="BackgroundTaskRunnerOptions"/> class.
/// </summary>
public BackgroundTaskRunnerOptions()
{
LongRunning = false;
KeepAlive = false;
AutoStart = false;
PreserveRunningTask = false;
Hosted = true;
}
/// <summary>
/// Gets or sets a value indicating whether the running task should be a long-running,
/// coarse grained operation.
/// </summary>
public bool LongRunning { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the running task should block and wait
/// on the queue, or end, when the queue is empty.
/// </summary>
public bool KeepAlive { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the running task should start immediately
/// or only once a task has been added to the queue.
/// </summary>
public bool AutoStart { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the running task should be preserved
/// once completed, or reset to null. For unit tests.
/// </summary>
public bool PreserveRunningTask { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the runner should register with (and be
/// stopped by) the hosting. Otherwise, something else should take care of stopping
/// the runner. True by default.
/// </summary>
public bool Hosted { get; set; }
}
}

View File

@@ -1,30 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Umbraco.Web.Scheduling
{
/// <summary>
/// Represents a background task.
/// </summary>
public interface IBackgroundTask : IDisposable
{
/// <summary>
/// Runs the background task.
/// </summary>
void Run();
/// <summary>
/// Runs the task asynchronously.
/// </summary>
/// <param name="token">A cancellation token.</param>
/// <returns>A <see cref="Task"/> instance representing the execution of the background task.</returns>
/// <exception cref="NotImplementedException">The background task cannot run asynchronously.</exception>
Task RunAsync(CancellationToken token);
/// <summary>
/// Indicates whether the background task can run asynchronously.
/// </summary>
bool IsAsync { get; }
}
}

View File

@@ -1,20 +0,0 @@
using System;
using Umbraco.Cms.Core;
namespace Umbraco.Web.Scheduling
{
/// <summary>
/// Defines a service managing a queue of tasks of type <typeparamref name="T"/> and running them in the background.
/// </summary>
/// <typeparam name="T">The type of the managed tasks.</typeparam>
/// <remarks>The interface is not complete and exists only to have the contravariance on T.</remarks>
public interface IBackgroundTaskRunner<in T> : IDisposable, IRegisteredObject
where T : class, IBackgroundTask
{
bool IsCompleted { get; }
void Add(T task);
bool TryAdd(T task);
// TODO: complete the interface?
}
}

View File

@@ -1,32 +0,0 @@
using System;
using System.Threading.Tasks;
namespace Umbraco.Web.Scheduling
{
/// <summary>
/// Represents a latched background task.
/// </summary>
/// <remarks>Latched background tasks can suspend their execution until
/// a condition is met. However if the tasks runner has to terminate,
/// latched background tasks can be executed immediately, depending on
/// the value returned by RunsOnShutdown.</remarks>
public interface ILatchedBackgroundTask : IBackgroundTask
{
/// <summary>
/// Gets a task on latch.
/// </summary>
/// <exception cref="InvalidOperationException">The task is not latched.</exception>
Task Latch { get; }
/// <summary>
/// Gets a value indicating whether the task is latched.
/// </summary>
/// <remarks>Should return false as soon as the condition is met.</remarks>
bool IsLatched { get; }
/// <summary>
/// Gets a value indicating whether the task can be executed immediately if the task runner has to terminate.
/// </summary>
bool RunsOnShutdown { get; }
}
}

View File

@@ -1,61 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Umbraco.Cms.Core;
namespace Umbraco.Web.Scheduling
{
public abstract class LatchedBackgroundTaskBase : DisposableObjectSlim, ILatchedBackgroundTask
{
private TaskCompletionSource<bool> _latch;
protected LatchedBackgroundTaskBase()
{
_latch = new TaskCompletionSource<bool>();
}
/// <summary>
/// Implements IBackgroundTask.Run().
/// </summary>
public virtual void Run()
{
throw new NotSupportedException("This task cannot run synchronously.");
}
/// <summary>
/// Implements IBackgroundTask.RunAsync().
/// </summary>
public virtual Task RunAsync(CancellationToken token)
{
throw new NotSupportedException("This task cannot run asynchronously.");
}
/// <summary>
/// Indicates whether the background task can run asynchronously.
/// </summary>
public abstract bool IsAsync { get; }
public Task Latch => _latch.Task;
public bool IsLatched => _latch.Task.IsCompleted == false;
protected void Release()
{
_latch.SetResult(true);
}
protected void Reset()
{
_latch = new TaskCompletionSource<bool>();
}
public virtual bool RunsOnShutdown => false;
// the task is going to be disposed after execution,
// unless it is latched again, thus indicating it wants to
// remain active
protected override void DisposeResources()
{ }
}
}

View File

@@ -1,42 +0,0 @@
using System;
namespace Umbraco.Web.Scheduling
{
/// <summary>
/// Provides arguments for task runner events.
/// </summary>
/// <typeparam name="T">The type of the task.</typeparam>
public class TaskEventArgs<T> : EventArgs
where T : IBackgroundTask
{
/// <summary>
/// Initializes a new instance of the <see cref="TaskEventArgs{T}"/> class with a task.
/// </summary>
/// <param name="task">The task.</param>
public TaskEventArgs(T task)
{
Task = task;
}
/// <summary>
/// Initializes a new instance of the <see cref="TaskEventArgs{T}"/> class with a task and an exception.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="exception">An exception.</param>
public TaskEventArgs(T task, Exception exception)
{
Task = task;
Exception = exception;
}
/// <summary>
/// Gets or sets the task.
/// </summary>
public T Task { get; private set; }
/// <summary>
/// Gets or sets the exception.
/// </summary>
public Exception Exception { get; private set; }
}
}

View File

@@ -1,44 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace Umbraco.Web.Scheduling
{
/// <summary>
/// Wraps a <see cref="Task"/> within an object that gives access to its GetAwaiter method and Status
/// property while ensuring that it cannot be modified in any way.
/// </summary>
public class ThreadingTaskImmutable
{
private readonly Task _task;
/// <summary>
/// Initializes a new instance of the <see cref="ThreadingTaskImmutable"/> class with a Task.
/// </summary>
/// <param name="task">The task.</param>
public ThreadingTaskImmutable(Task task)
{
if (task == null)
throw new ArgumentNullException("task");
_task = task;
}
/// <summary>
/// Gets an awaiter used to await the task.
/// </summary>
/// <returns>An awaiter instance.</returns>
public TaskAwaiter GetAwaiter()
{
return _task.GetAwaiter();
}
/// <summary>
/// Gets the TaskStatus of the task.
/// </summary>
/// <returns>The current TaskStatus of the task.</returns>
public TaskStatus Status
{
get { return _task.Status; }
}
}
}

View File

@@ -1,164 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Xml;
using Microsoft.Extensions.Logging;
using Umbraco.Extensions;
using Umbraco.Tests.TestHelpers;
using Umbraco.Web.Composing;
using Constants = Umbraco.Cms.Core.Constants;
namespace Umbraco.Tests.LegacyXmlPublishedCache
{
class PreviewContent
{
private readonly int _userId;
private readonly Guid _previewSet;
private string _previewSetPath;
private XmlDocument _previewXml;
private readonly XmlStore _xmlStore;
/// <summary>
/// Gets the XML document.
/// </summary>
/// <remarks>May return <c>null</c> if the preview content set is invalid.</remarks>
public XmlDocument XmlContent
{
get
{
// null if invalid preview content
if (_previewSetPath == null) return null;
// load if not loaded yet
if (_previewXml != null)
return _previewXml;
_previewXml = new XmlDocument();
try
{
_previewXml.Load(_previewSetPath);
}
catch (Exception ex)
{
Current.Logger.LogError(ex, "Could not load preview set {PreviewSet} for user {UserId}.", _previewSet, _userId);
ClearPreviewSet();
_previewXml = null;
_previewSetPath = null; // do not try again
}
return _previewXml;
}
}
/// <summary>
/// Gets the preview token.
/// </summary>
/// <remarks>To be stored in a cookie or wherever appropriate.</remarks>
public string Token => _userId + ":" + _previewSet;
/// <summary>
/// Initializes a new instance of the <see cref="PreviewContent"/> class for a user.
/// </summary>
/// <param name="xmlStore">The underlying Xml store.</param>
/// <param name="userId">The user identifier.</param>
public PreviewContent(XmlStore xmlStore, int userId)
{
if (xmlStore == null)
throw new ArgumentNullException(nameof(xmlStore));
_xmlStore = xmlStore;
_userId = userId;
_previewSet = Guid.NewGuid();
_previewSetPath = GetPreviewSetPath(_userId, _previewSet);
}
/// <summary>
/// Initializes a new instance of the <see cref="PreviewContent"/> with a preview token.
/// </summary>
/// <param name="xmlStore">The underlying Xml store.</param>
/// <param name="token">The preview token.</param>
public PreviewContent(XmlStore xmlStore, string token)
{
if (xmlStore == null)
throw new ArgumentNullException(nameof(xmlStore));
_xmlStore = xmlStore;
if (token.IsNullOrWhiteSpace())
throw new ArgumentException("Null or empty token.", nameof(token));
var parts = token.Split(':');
if (parts.Length != 2)
throw new ArgumentException("Invalid token.", nameof(token));
if (int.TryParse(parts[0], out _userId) == false)
throw new ArgumentException("Invalid token.", nameof(token));
if (Guid.TryParse(parts[1], out _previewSet) == false)
throw new ArgumentException("Invalid token.", nameof(token));
_previewSetPath = GetPreviewSetPath(_userId, _previewSet);
}
// creates and saves a new preview set
// used in 2 places and each time includeSubs is true
// have to use the Document class at the moment because IContent does not do ToXml...
public void CreatePreviewSet(int contentId, bool includeSubs)
{
// note: always include subs
_previewXml = _xmlStore.GetPreviewXml(contentId, includeSubs);
// make sure the preview folder exists
var dir = new DirectoryInfo(TestHelper.IOHelper.MapPath(Constants.SystemDirectories.Preview));
if (dir.Exists == false)
dir.Create();
// clean old preview sets
ClearPreviewDirectory(_userId, dir);
// save
_previewXml.Save(_previewSetPath);
}
// get the full path to the preview set
private static string GetPreviewSetPath(int userId, Guid previewSet)
{
return TestHelper.IOHelper.MapPath(Path.Combine(Constants.SystemDirectories.Preview, userId + "_" + previewSet + ".config"));
}
// deletes files for the user, and files accessed more than one hour ago
private static void ClearPreviewDirectory(int userId, DirectoryInfo dir)
{
var now = DateTime.Now;
var prefix = userId + "_";
foreach (var file in dir.GetFiles("*.config")
.Where(x => x.Name.StartsWith(prefix) || (now - x.LastAccessTime).TotalMinutes > 1))
{
DeletePreviewSetFile(userId, file);
}
}
// delete one preview set file in a safe way
private static void DeletePreviewSetFile(int userId, FileSystemInfo file)
{
try
{
file.Delete();
}
catch (Exception ex)
{
Current.Logger.LogError(ex, "Couldn't delete preview set {FileName} for user {UserId}", file.Name, userId);
}
}
/// <summary>
/// Deletes the preview set in a safe way.
/// </summary>
public void ClearPreviewSet()
{
if (_previewSetPath == null) return;
var previewSetFile = new FileInfo(_previewSetPath);
DeletePreviewSetFile(_userId, previewSetFile);
}
}
}

View File

@@ -1,24 +0,0 @@
using NPoco;
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
namespace Umbraco.Tests.LegacyXmlPublishedCache
{
[TableName("cmsPreviewXml")]
[PrimaryKey("nodeId", AutoIncrement = false)]
[ExplicitColumns]
internal class PreviewXmlDto
{
[Column("nodeId")]
[PrimaryKeyColumn(AutoIncrement = false)]
[ForeignKey(typeof(ContentDto), Column = "nodeId")]
public int NodeId { get; set; }
[Column("xml")]
[SpecialDbType(SpecialDbTypes.NTEXT)]
public string Xml { get; set; }
[Column("rv")]
public long Rv { get; set; }
}
}

View File

@@ -1,548 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml;
using System.Xml.XPath;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Xml;
using Umbraco.Extensions;
using Umbraco.Tests.TestHelpers;
namespace Umbraco.Tests.LegacyXmlPublishedCache
{
internal class PublishedContentCache : PublishedCacheBase, IPublishedContentCache
{
private readonly IAppCache _appCache;
private readonly GlobalSettings _globalSettings;
private readonly RoutesCache _routesCache;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly IDomainCache _domainCache;
private readonly PublishedContentTypeCache _contentTypeCache;
// initialize a PublishedContentCache instance with
// an XmlStore containing the master xml
// an IAppCache that should be at request-level
// a RoutesCache - need to cleanup that one
// a preview token string (or null if not previewing)
public PublishedContentCache(
XmlStore xmlStore, // an XmlStore containing the master xml
IDomainCache domainCache, // an IDomainCache implementation
IAppCache appCache, // an IAppCache that should be at request-level
GlobalSettings globalSettings,
PublishedContentTypeCache contentTypeCache, // a PublishedContentType cache
RoutesCache routesCache, // a RoutesCache
IVariationContextAccessor variationContextAccessor,
string previewToken) // a preview token string (or null if not previewing)
: base(previewToken.IsNullOrWhiteSpace() == false)
{
_appCache = appCache;
_globalSettings = globalSettings;
_routesCache = routesCache; // may be null for unit-testing
_variationContextAccessor = variationContextAccessor;
_contentTypeCache = contentTypeCache;
_domainCache = domainCache;
_xmlStore = xmlStore;
_xml = _xmlStore.Xml; // capture - because the cache has to remain consistent
if (previewToken.IsNullOrWhiteSpace() == false)
_previewContent = new PreviewContent(_xmlStore, previewToken);
}
#region Unit Tests
// for INTERNAL, UNIT TESTS use ONLY
internal RoutesCache RoutesCache => _routesCache;
// for INTERNAL, UNIT TESTS use ONLY
internal XmlStore XmlStore => _xmlStore;
#endregion
#region Routes
public virtual IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string culture = null)
{
if (route == null) throw new ArgumentNullException(nameof(route));
// try to get from cache if not previewing
var contentId = preview || _routesCache == null ? 0 : _routesCache.GetNodeId(route);
// if found id in cache then get corresponding content
// and clear cache if not found - for whatever reason
IPublishedContent content = null;
if (contentId > 0)
{
content = GetById(preview, contentId);
if (content == null)
_routesCache?.ClearNode(contentId);
}
// still have nothing? actually determine the id
hideTopLevelNode = hideTopLevelNode ?? _globalSettings.HideTopLevelNodeFromPath; // default = settings
content = content ?? DetermineIdByRoute(preview, route, hideTopLevelNode.Value);
// cache if we have a content and not previewing
if (content != null && preview == false && _routesCache != null)
AddToCacheIfDeepestRoute(content, route);
return content;
}
private void AddToCacheIfDeepestRoute(IPublishedContent content, string route)
{
var domainRootNodeId = route.StartsWith("/") ? -1 : int.Parse(route.Substring(0, route.IndexOf('/')));
// so we have a route that maps to a content... say "1234/path/to/content" - however, there could be a
// domain set on "to" and route "4567/content" would also map to the same content - and due to how
// URLs computing work (by walking the tree up to the first domain we find) it is that second route
// that would be returned - the "deepest" route - and that is the route we want to cache, *not* the
// longer one - so make sure we don't cache the wrong route
var deepest = DomainUtilities.ExistsDomainInPath(_domainCache.GetAll(false), content.Path, domainRootNodeId) == false;
if (deepest)
_routesCache.Store(content.Id, route, true); // trusted route
}
public IPublishedContent GetByRoute(string route, bool? hideTopLevelNode = null, string culture = null)
{
return GetByRoute(PreviewDefault, route, hideTopLevelNode);
}
public virtual string GetRouteById(bool preview, int contentId, string culture = null)
{
// try to get from cache if not previewing
var route = preview || _routesCache == null ? null : _routesCache.GetRoute(contentId);
// if found in cache then return
if (route != null)
return route;
// else actually determine the route
route = DetermineRouteById(preview, contentId);
// node not found
if (route == null)
return null;
// cache the route BUT do NOT trust it as it can be a colliding route
// meaning if we GetRouteById again, we'll get it from cache, but it
// won't be used for inbound routing
if (preview == false)
_routesCache.Store(contentId, route, false);
return route;
}
public string GetRouteById(int contentId, string culture = null)
{
return GetRouteById(PreviewDefault, contentId, culture);
}
IPublishedContent DetermineIdByRoute(bool preview, string route, bool hideTopLevelNode)
{
//the route always needs to be lower case because we only store the urlName attribute in lower case
route = route?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(route));
var pos = route.IndexOf('/');
var path = pos == 0 ? route : route.Substring(pos);
var startNodeId = pos == 0 ? 0 : int.Parse(route.Substring(0, pos));
//check if we can find the node in our xml cache
var id = NavigateRoute(preview, startNodeId, path, hideTopLevelNode);
return id > 0 ? GetById(preview, id) : null;
}
private static XmlElement GetXmlElementChildWithLowestSortOrder(XmlNode element)
{
XmlElement elt = null;
var min = int.MaxValue;
foreach (var n in element.ChildNodes)
{
var e = n as XmlElement;
if (e == null) continue;
var sortOrder = int.Parse(e.GetAttribute("sortOrder"));
if (sortOrder >= min) continue;
min = sortOrder;
elt = e;
}
return elt;
}
private int NavigateRoute(bool preview, int startNodeId, string path, bool hideTopLevelNode)
{
var xml = GetXml(preview);
XmlElement elt;
// empty path
if (path == string.Empty || path == "/")
{
if (startNodeId > 0)
{
elt = xml.GetElementById(startNodeId.ToString(CultureInfo.InvariantCulture));
return elt == null ? -1 : startNodeId;
}
elt = GetXmlElementChildWithLowestSortOrder(xml.DocumentElement);
return elt == null ? -1 : int.Parse(elt.GetAttribute("id"));
}
// non-empty path
elt = startNodeId <= 0
? xml.DocumentElement
: xml.GetElementById(startNodeId.ToString(CultureInfo.InvariantCulture));
if (elt == null) return -1;
var urlParts = path.Split(SlashChar, StringSplitOptions.RemoveEmptyEntries);
if (hideTopLevelNode && startNodeId <= 0)
{
//Don't use OfType<T> or Cast<T>, this is critical code, all ChildNodes are XmlElement so explicitly cast
// https://gist.github.com/Shazwazza/04e2e5642a316f4a87e52dada2901198
foreach (var n in elt.ChildNodes)
{
var e = n as XmlElement;
if (e == null) continue;
var id = NavigateElementRoute(e, urlParts);
if (id > 0) return id;
}
if (urlParts.Length > 1)
return -1;
}
return NavigateElementRoute(elt, urlParts);
}
private static int NavigateElementRoute(XmlElement elt, string[] urlParts)
{
var found = true;
var i = 0;
while (found && i < urlParts.Length)
{
found = false;
//Don't use OfType<T> or Cast<T>, this is critical code, all ChildNodes are XmlElement so explicitly cast
// https://gist.github.com/Shazwazza/04e2e5642a316f4a87e52dada2901198
var sortOrder = -1;
foreach (var o in elt.ChildNodes)
{
var child = o as XmlElement;
if (child == null) continue;
var noNode = child.GetAttributeNode("isDoc") == null;
if (noNode) continue;
if (child.GetAttribute("urlName") != urlParts[i]) continue;
found = true;
var so = int.Parse(child.GetAttribute("sortOrder"));
if (sortOrder >= 0 && so >= sortOrder) continue;
sortOrder = so;
elt = child;
}
i++;
}
return found ? int.Parse(elt.GetAttribute("id")) : -1;
}
string DetermineRouteById(bool preview, int contentId)
{
var node = GetById(preview, contentId);
if (node == null) return null;
// walk up from that node until we hit a node with a domain,
// or we reach the content root, collecting URLs in the way
var pathParts = new List<string>();
var n = node;
var hasDomains = _domainCache.HasAssigned(n.Id);
while (hasDomains == false && n != null) // n is null at root
{
// get the url
var urlName = n.UrlSegment(TestHelper.VariationContextAccessor);
pathParts.Add(urlName);
// move to parent node
n = n.Parent;
hasDomains = n != null && _domainCache.HasAssigned(n.Id);
}
// no domain, respect HideTopLevelNodeFromPath for legacy purposes
if (hasDomains == false && _globalSettings.HideTopLevelNodeFromPath)
{
if (node.Parent == null)
{
var rootNode = GetByRoute(preview, "/", true);
if (rootNode == null)
throw new Exception("Failed to get node at /.");
if (rootNode.Id == node.Id) // remove only if we're the default node
pathParts.RemoveAt(pathParts.Count - 1);
}
else
{
pathParts.RemoveAt(pathParts.Count - 1);
}
}
// assemble the route
pathParts.Reverse();
var path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc
var route = (n?.Id.ToString(CultureInfo.InvariantCulture) ?? "") + path;
return route;
}
#endregion
#region XPath Strings
static class XPathStrings
{
public const string Root = "/root";
public const string RootDocuments = "/root/* [@isDoc]";
}
#endregion
#region Converters
private IPublishedContent ConvertToDocument(XmlNode xmlNode, bool isPreviewing)
{
return xmlNode == null ? null : XmlPublishedContent.Get(xmlNode, isPreviewing, _appCache, _contentTypeCache, _variationContextAccessor);
}
private IEnumerable<IPublishedContent> ConvertToDocuments(XmlNodeList xmlNodes, bool isPreviewing)
{
return xmlNodes.Cast<XmlNode>()
.Select(xmlNode => XmlPublishedContent.Get(xmlNode, isPreviewing, _appCache, _contentTypeCache, _variationContextAccessor));
}
#endregion
#region Getters
public override IPublishedContent GetById(bool preview, int nodeId)
{
return ConvertToDocument(GetXml(preview).GetElementById(nodeId.ToString(CultureInfo.InvariantCulture)), preview);
}
public override IPublishedContent GetById(bool preview, Guid nodeId)
{
// implement this, but in a more efficient way
//const string xpath = "//* [@isDoc and @key=$guid]";
//return GetSingleByXPath(preview, xpath, new[] { new XPathVariable("guid", nodeId.ToString()) });
var keyMatch = nodeId.ToString();
var nav = GetXml(preview).CreateNavigator();
if (nav.MoveToFirstChild() == false) return null; // from / to /root
if (nav.MoveToFirstChild() == false) return null; // from /root to /root/*
while (true)
{
var isDoc = false;
string key = null;
if (nav.HasAttributes)
{
nav.MoveToFirstAttribute();
do
{
if (nav.Name == "isDoc") isDoc = true;
if (nav.Name == "key") key = nav.Value;
if (isDoc && key != null) break;
} while (nav.MoveToNextAttribute());
nav.MoveToParent();
}
if (isDoc == false || key != keyMatch)
{
if (isDoc && nav.MoveToFirstChild())
continue;
while (nav.MoveToNext(XPathNodeType.Element) == false)
if (nav.MoveToParent() == false || nav.NodeType == XPathNodeType.Root) return null;
continue;
}
var elt = nav.UnderlyingObject as XmlNode;
return ConvertToDocument(elt, preview);
}
}
public override IPublishedContent GetById(bool preview, Udi nodeId)
=> throw new NotSupportedException();
public override bool HasById(bool preview, int contentId)
{
return GetXml(preview).CreateNavigator().MoveToId(contentId.ToString(CultureInfo.InvariantCulture));
}
public override IEnumerable<IPublishedContent> GetAtRoot(bool preview, string culture = null)
{
return ConvertToDocuments(GetXml(preview).SelectNodes(XPathStrings.RootDocuments), preview);
}
public override IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars)
{
if (xpath == null) throw new ArgumentNullException(nameof(xpath));
if (string.IsNullOrWhiteSpace(xpath)) return null;
var xml = GetXml(preview);
var node = vars == null
? xml.SelectSingleNode(xpath)
: xml.SelectSingleNode(xpath, vars);
return ConvertToDocument(node, preview);
}
public override IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars)
{
if (xpath == null) throw new ArgumentNullException(nameof(xpath));
var xml = GetXml(preview);
var node = vars == null
? xml.SelectSingleNode(xpath)
: xml.SelectSingleNode(xpath, vars);
return ConvertToDocument(node, preview);
}
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, string xpath, XPathVariable[] vars)
{
if (xpath == null) throw new ArgumentNullException(nameof(xpath));
if (string.IsNullOrWhiteSpace(xpath)) return Enumerable.Empty<IPublishedContent>();
var xml = GetXml(preview);
var nodes = vars == null
? xml.SelectNodes(xpath)
: xml.SelectNodes(xpath, vars);
return ConvertToDocuments(nodes, preview);
}
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars)
{
if (xpath == null) throw new ArgumentNullException(nameof(xpath));
var xml = GetXml(preview);
var nodes = vars == null
? xml.SelectNodes(xpath)
: xml.SelectNodes(xpath, vars);
return ConvertToDocuments(nodes, preview);
}
public override bool HasContent(bool preview)
{
var xml = GetXml(preview);
var node = xml?.SelectSingleNode(XPathStrings.RootDocuments);
return node != null;
}
public override XPathNavigator CreateNavigator(bool preview)
{
var xml = GetXml(preview);
return xml.CreateNavigator();
}
public override XPathNavigator CreateNodeNavigator(int id, bool preview)
{
// hackish - backward compatibility ;-(
XPathNavigator navigator = null;
if (preview)
{
var node = _xmlStore.GetPreviewXmlNode(id);
if (node != null)
{
navigator = node.CreateNavigator();
}
}
else
{
var node = GetXml(false).GetElementById(id.ToInvariantString());
if (node != null)
{
var doc = new XmlDocument();
var clone = doc.ImportNode(node, false);
var props = node.SelectNodes("./* [not(@id)]");
if (props == null) throw new Exception("oops");
foreach (var n in props.Cast<XmlNode>())
clone.AppendChild(doc.ImportNode(n, true));
navigator = node.CreateNavigator();
}
}
return navigator;
}
#endregion
#region Legacy Xml
private readonly XmlStore _xmlStore;
private XmlDocument _xml;
private readonly PreviewContent _previewContent;
internal XmlDocument GetXml(bool preview)
{
// not trying to be thread-safe here, that's not the point
if (preview == false)
{
// if there's a current enlisted reader/writer, use its xml
var tempXml = _xmlStore.TempXml;
if (tempXml != null) return tempXml;
return _xml;
}
// Xml cache does not support retrieving preview content when not previewing
if (_previewContent == null)
throw new InvalidOperationException("Cannot retrieve preview content when not previewing.");
// PreviewContent tries to load the Xml once and if it fails,
// it invalidates itself and always return null for XmlContent.
var previewXml = _previewContent.XmlContent;
return previewXml ?? _xml;
}
internal void Resync(XmlDocument xml)
{
_xml = xml; // re-capture
// note: we're not resyncing "preview" because that would mean re-building the whole
// preview set which is costly, so basically when previewing, there will be no resync.
// clear recursive properties cached by XmlPublishedContent.GetProperty
// assume that nothing else is going to cache IPublishedProperty items (else would need to do ByKeySearch)
// NOTE also clears all the media cache properties, which is OK (see media cache)
_appCache.ClearOfType<IPublishedProperty>();
//_appCache.ClearCacheByKeySearch("XmlPublishedCache.PublishedContentCache:RecursiveProperty-");
}
#endregion
#region XPathQuery
static readonly char[] SlashChar = { '/' };
#endregion
#region Content types
public override IPublishedContentType GetContentType(int id) => _contentTypeCache.Get(PublishedItemType.Content, id);
public override IPublishedContentType GetContentType(string alias) => _contentTypeCache.Get(PublishedItemType.Content, alias);
public override IPublishedContentType GetContentType(Guid key) => _contentTypeCache.Get(PublishedItemType.Content, key);
#endregion
}
}

View File

@@ -1,698 +0,0 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Threading;
using System.Xml.XPath;
using Examine;
using Examine.Search;
using Lucene.Net.Store;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Core.Xml;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Extensions;
using Umbraco.Web.Composing;
using Constants = Umbraco.Cms.Core.Constants;
namespace Umbraco.Tests.LegacyXmlPublishedCache
{
/// <summary>
/// An IPublishedMediaStore that first checks for the media in Examine, and then reverts to the database
/// </summary>
/// <remarks>
/// NOTE: In the future if we want to properly cache all media this class can be extended or replaced when these classes/interfaces are exposed publicly.
/// </remarks>
internal class PublishedMediaCache : PublishedCacheBase, IPublishedMediaCache
{
private readonly IMediaService _mediaService;
private readonly IUserService _userService;
// by default these are null unless specified by the ctor dedicated to tests
// when they are null the cache derives them from the ExamineManager, see
// method GetExamineManagerSafe().
//
private readonly ISearcher _searchProvider;
private readonly XmlStore _xmlStore;
private readonly PublishedContentTypeCache _contentTypeCache;
private readonly IEntityXmlSerializer _entitySerializer;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly IExamineManager _examineManager = new ExamineManager();
// must be specified by the ctor
private readonly IAppCache _appCache;
public PublishedMediaCache(XmlStore xmlStore, IMediaService mediaService, IUserService userService,
IAppCache appCache, PublishedContentTypeCache contentTypeCache, IEntityXmlSerializer entitySerializer,
IUmbracoContextAccessor umbracoContextAccessor, IVariationContextAccessor variationContextAccessor)
: base(false)
{
_mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService));
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
_appCache = appCache;
_xmlStore = xmlStore;
_contentTypeCache = contentTypeCache;
_entitySerializer = entitySerializer;
_umbracoContextAccessor = umbracoContextAccessor;
_variationContextAccessor = variationContextAccessor;
}
/// <summary>
/// Generally used for unit testing to use an explicit examine searcher
/// </summary>
/// <param name="mediaService"></param>
/// <param name="userService"></param>
/// <param name="searchProvider"></param>
/// <param name="appCache"></param>
/// <param name="contentTypeCache"></param>
/// <param name="entitySerializer"></param>
internal PublishedMediaCache(IMediaService mediaService, IUserService userService, ISearcher searchProvider, IAppCache appCache, PublishedContentTypeCache contentTypeCache, IEntityXmlSerializer entitySerializer, IUmbracoContextAccessor umbracoContextAccessor)
: base(false)
{
_mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService));
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
_searchProvider = searchProvider ?? throw new ArgumentNullException(nameof(searchProvider));
_appCache = appCache;
_contentTypeCache = contentTypeCache;
_entitySerializer = entitySerializer;
_umbracoContextAccessor = umbracoContextAccessor;
}
static PublishedMediaCache()
{
InitializeCacheConfig();
}
public override IPublishedContent GetById(bool preview, int nodeId)
{
return GetUmbracoMedia(nodeId);
}
public override IPublishedContent GetById(bool preview, Guid nodeId)
{
throw new NotImplementedException();
}
public override IPublishedContent GetById(bool preview, Udi nodeId)
=> throw new NotSupportedException();
public override bool HasById(bool preview, int contentId)
{
return GetUmbracoMedia(contentId) != null;
}
public override IEnumerable<IPublishedContent> GetAtRoot(bool preview, string culture = null)
{
var searchProvider = GetSearchProviderSafe();
if (searchProvider != null)
{
try
{
// first check in Examine for the cache values
// +(+parentID:-1) +__IndexType:media
var criteria = searchProvider.CreateQuery("media");
var filter = criteria.ParentId(-1).Not().Field(UmbracoExamineFieldNames.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard());
var result = filter.Execute();
if (result != null)
return result.Select(x => CreateFromCacheValues(ConvertFromSearchResult(x)));
}
catch (Exception ex)
{
if (ex is FileNotFoundException)
{
//Currently examine is throwing FileNotFound exceptions when we have a load balanced filestore and a node is published in umbraco
//See this thread: http://examine.cdodeplex.com/discussions/264341
//Catch the exception here for the time being, and just fallback to GetMedia
// TODO: Need to fix examine in LB scenarios!
Current.Logger.LogError(ex, "Could not load data from Examine index for media");
}
else if (ex is ObjectDisposedException)
{
//If the app domain is shutting down and the site is under heavy load the index reader will be closed and it really cannot
//be re-opened since the app domain is shutting down. In this case we have no option but to try to load the data from the db.
Current.Logger.LogError(ex, "Could not load data from Examine index for media, the app domain is most likely in a shutdown state");
}
else throw;
}
}
//something went wrong, fetch from the db
var rootMedia = _mediaService.GetRootMedia();
return rootMedia.Select(m => GetUmbracoMedia(m.Id));
}
public override IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars)
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
//var navigator = CreateNavigator(preview);
//var iterator = navigator.Select(xpath, vars);
//return GetSingleByXPath(iterator);
}
public override IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars)
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
//var navigator = CreateNavigator(preview);
//var iterator = navigator.Select(xpath, vars);
//return GetSingleByXPath(iterator);
}
private IPublishedContent GetSingleByXPath(XPathNodeIterator iterator)
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
//if (iterator.MoveNext() == false) return null;
//var idAttr = iterator.Current.GetAttribute("id", "");
//int id;
//return int.TryParse(idAttr, out id) ? GetUmbracoMedia(id) : null;
}
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, string xpath, XPathVariable[] vars)
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
//var navigator = CreateNavigator(preview);
//var iterator = navigator.Select(xpath, vars);
//return GetByXPath(iterator);
}
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars)
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
//var navigator = CreateNavigator(preview);
//var iterator = navigator.Select(xpath, vars);
//return GetByXPath(iterator);
}
private IEnumerable<IPublishedContent> GetByXPath(XPathNodeIterator iterator)
{
while (iterator.MoveNext())
{
var idAttr = iterator.Current.GetAttribute("id", "");
int id;
if (int.TryParse(idAttr, out id))
yield return GetUmbracoMedia(id);
}
}
public override XPathNavigator CreateNavigator(bool preview)
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
//var doc = _xmlStore.GetMediaXml();
//return doc.CreateNavigator();
}
public override XPathNavigator CreateNodeNavigator(int id, bool preview)
{
// preview is ignored for media cache
// this code is mostly used when replacing old media.ToXml() code, and that code
// stored the XML attached to the media itself - so for some time in memory - so
// unless we implement some sort of cache here, we're probably degrading perfs.
XPathNavigator navigator = null;
var node = _xmlStore.GetMediaXmlNode(id);
if (node != null)
{
navigator = node.CreateNavigator();
}
return navigator;
}
public override bool HasContent(bool preview) { throw new NotImplementedException(); }
private ISearcher GetSearchProviderSafe()
{
if (_searchProvider != null)
return _searchProvider;
try
{
return _examineManager.TryGetIndex(Constants.UmbracoIndexes.InternalIndexName, out var index) ? index.GetSearcher() : null;
}
catch (FileNotFoundException)
{
//Currently examine is throwing FileNotFound exceptions when we have a load balanced filestore and a node is published in umbraco
//See this thread: http://examine.cdodeplex.com/discussions/264341
//Catch the exception here for the time being, and just fallback to GetMedia
// TODO: Need to fix examine in LB scenarios!
}
catch (NullReferenceException)
{
//This will occur when the search provider cannot be initialized. In newer examine versions the initialization is lazy and therefore
// the manager will return the singleton without throwing initialization errors, however if examine isn't configured correctly a null
// reference error will occur because the examine settings are null.
}
catch (ObjectDisposedException)
{
//If the app domain is shutting down and the site is under heavy load the index reader will be closed and it really cannot
//be re-opened since the app domain is shutting down. In this case we have no option but to try to load the data from the db.
}
return null;
}
private IPublishedContent GetUmbracoMedia(int id)
{
// this recreates an IPublishedContent and model each time
// it is called, but at least it should NOT hit the database
// nor Lucene each time, relying on the memory cache instead
if (id <= 0) return null; // fail fast
var cacheValues = GetCacheValues(id, GetUmbracoMediaCacheValues);
return cacheValues == null ? null : CreateFromCacheValues(cacheValues);
}
private CacheValues GetUmbracoMediaCacheValues(int id)
{
var searchProvider = GetSearchProviderSafe();
if (searchProvider != null)
{
try
{
// first check in Examine as this is WAY faster
//
// the filter will create a query like this:
// +(+__NodeId:3113 -__Path:-1,-21,*) +__IndexType:media
//
// note that since the use of the wildcard, it automatically escapes it in Lucene.
var criteria = searchProvider.CreateQuery("media");
var filter = criteria.Id(id.ToInvariantString()).Not().Field(UmbracoExamineFieldNames.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard());
var result = filter.Execute().FirstOrDefault();
if (result != null) return ConvertFromSearchResult(result);
}
catch (Exception ex)
{
if (ex is FileNotFoundException)
{
//Currently examine is throwing FileNotFound exceptions when we have a load balanced filestore and a node is published in umbraco
//See this thread: http://examine.cdodeplex.com/discussions/264341
//Catch the exception here for the time being, and just fallback to GetMedia
// TODO: Need to fix examine in LB scenarios!
Current.Logger.LogError(ex, "Could not load data from Examine index for media");
}
else if (ex is ObjectDisposedException)
{
//If the app domain is shutting down and the site is under heavy load the index reader will be closed and it really cannot
//be re-opened since the app domain is shutting down. In this case we have no option but to try to load the data from the db.
Current.Logger.LogError(ex, "Could not load data from Examine index for media, the app domain is most likely in a shutdown state");
}
else throw;
}
}
// don't log a warning here, as it can flood the log in case of eg a media picker referencing a media
// that has been deleted, hence is not in the Examine index anymore (for a good reason). try to get
// the media from the service, first
var media = _mediaService.GetById(id);
if (media == null || media.Trashed) return null; // not found, ok
// so, the media was not found in Examine's index *yet* it exists, which probably indicates that
// the index is corrupted. Or not up-to-date. Log a warning, but only once, and only if seeing the
// error more that a number of times.
var miss = Interlocked.CompareExchange(ref _examineIndexMiss, 0, 0); // volatile read
if (miss < ExamineIndexMissMax && Interlocked.Increment(ref _examineIndexMiss) == ExamineIndexMissMax)
Current.Logger.LogWarning("Failed ({ExamineIndexMissMax} times) to retrieve medias from Examine index and had to load"
+ " them from DB. This may indicate that the Examine index is corrupted.", ExamineIndexMissMax);
return ConvertFromIMedia(media);
}
private const int ExamineIndexMissMax = 10;
private int _examineIndexMiss;
internal CacheValues ConvertFromXPathNodeIterator(XPathNodeIterator media, int id)
{
if (media?.Current != null)
{
return media.Current.Name.InvariantEquals("error")
? null
: ConvertFromXPathNavigator(media.Current);
}
Current.Logger.LogWarning("Could not retrieve media {MediaId} from Examine index or from legacy library.GetMedia method", id);
return null;
}
internal CacheValues ConvertFromSearchResult(ISearchResult searchResult)
{
// note: fixing fields in 7.x, removed by Shan for 8.0
return new CacheValues
{
Values = searchResult.Values,
FromExamine = true
};
}
internal CacheValues ConvertFromXPathNavigator(XPathNavigator xpath, bool forceNav = false)
{
if (xpath == null) throw new ArgumentNullException(nameof(xpath));
var values = new Dictionary<string, string> { { "nodeName", xpath.GetAttribute("nodeName", "") } };
values["nodeTypeAlias"] = xpath.Name;
var result = xpath.SelectChildren(XPathNodeType.Element);
//add the attributes e.g. id, parentId etc
if (result.Current != null && result.Current.HasAttributes)
{
if (result.Current.MoveToFirstAttribute())
{
//checking for duplicate keys because of the 'nodeTypeAlias' might already be added above.
if (values.ContainsKey(result.Current.Name) == false)
{
values[result.Current.Name] = result.Current.Value;
}
while (result.Current.MoveToNextAttribute())
{
if (values.ContainsKey(result.Current.Name) == false)
{
values[result.Current.Name] = result.Current.Value;
}
}
result.Current.MoveToParent();
}
}
// because, migration
if (values.ContainsKey("key") == false)
values["key"] = Guid.Empty.ToString();
//add the user props
while (result.MoveNext())
{
if (result.Current != null && result.Current.HasAttributes == false)
{
var value = result.Current.Value;
if (string.IsNullOrEmpty(value))
{
if (result.Current.HasAttributes || result.Current.SelectChildren(XPathNodeType.Element).Count > 0)
{
value = result.Current.OuterXml;
}
}
values[result.Current.Name] = value;
}
}
return new CacheValues
{
Values = values,
XPath = forceNav ? xpath : null // outside of tests we do NOT want to cache the navigator!
};
}
internal CacheValues ConvertFromIMedia(IMedia media)
{
var values = new Dictionary<string, string>();
var creator = _userService.GetProfileById(media.CreatorId);
var creatorName = creator == null ? "" : creator.Name;
values["id"] = media.Id.ToString();
values["key"] = media.Key.ToString();
values["parentID"] = media.ParentId.ToString();
values["level"] = media.Level.ToString();
values["creatorID"] = media.CreatorId.ToString();
values["creatorName"] = creatorName;
values["writerID"] = media.CreatorId.ToString();
values["writerName"] = creatorName;
values["template"] = "0";
values["urlName"] = "";
values["sortOrder"] = media.SortOrder.ToString();
values["createDate"] = media.CreateDate.ToString("yyyy-MM-dd HH:mm:ss");
values["updateDate"] = media.UpdateDate.ToString("yyyy-MM-dd HH:mm:ss");
values["nodeName"] = media.Name;
values["path"] = media.Path;
values["nodeType"] = media.ContentType.Id.ToString();
values["nodeTypeAlias"] = media.ContentType.Alias;
// add the user props
foreach (var prop in media.Properties)
values[prop.Alias] = prop.GetValue()?.ToString();
return new CacheValues
{
Values = values
};
}
/// <summary>
/// We will need to first check if the document was loaded by Examine, if so we'll need to check if this property exists
/// in the results, if it does not, then we'll have to revert to looking up in the db.
/// </summary>
/// <param name="dd"> </param>
/// <param name="alias"></param>
/// <returns></returns>
private IPublishedProperty GetProperty(DictionaryPublishedContent dd, string alias)
{
//lets check if the alias does not exist on the document.
//NOTE: Examine will not index empty values and we do not output empty XML Elements to the cache - either of these situations
// would mean that the property is missing from the collection whether we are getting the value from Examine or from the library media cache.
if (dd.Properties.All(x => x.Alias.InvariantEquals(alias) == false))
{
return null;
}
if (dd.LoadedFromExamine)
{
//We are going to check for a special field however, that is because in some cases we store a 'Raw'
//value in the index such as for xml/html.
var rawValue = dd.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(UmbracoExamineFieldNames.RawFieldPrefix + alias));
return rawValue
?? dd.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(alias));
}
//if its not loaded from examine, then just return the property
return dd.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(alias));
}
/// <summary>
/// A Helper methods to return the children for media whether it is based on examine or xml
/// </summary>
/// <param name="parentId"></param>
/// <param name="xpath"></param>
/// <returns></returns>
private IEnumerable<IPublishedContent> GetChildrenMedia(int parentId, XPathNavigator xpath = null)
{
// if there *is* a navigator, directly look it up
if (xpath != null)
{
return ToIPublishedContent(parentId, xpath);
}
// otherwise, try examine first, then re-look it up
var searchProvider = GetSearchProviderSafe();
if (searchProvider != null)
{
try
{
//first check in Examine as this is WAY faster
var criteria = searchProvider.CreateQuery("media");
var filter = criteria.ParentId(parentId).Not().Field(UmbracoExamineFieldNames.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard())
.OrderBy(new SortableField("sortOrder", SortType.Int));
//the above filter will create a query like this, NOTE: That since the use of the wildcard, it automatically escapes it in Lucene.
//+(+parentId:3113 -__Path:-1,-21,*) +__IndexType:media
// sort with the Sort field (updated for 8.0)
var results = filter.Execute();
if (results.Any())
{
// var medias = results.Select(ConvertFromSearchResult);
var medias = results.Select(x =>
{
int nid;
if (int.TryParse(x["__NodeId"], out nid) == false && int.TryParse(x["NodeId"], out nid) == false)
throw new Exception("Failed to extract NodeId from search result.");
var cacheValues = GetCacheValues(nid, id => ConvertFromSearchResult(x));
return CreateFromCacheValues(cacheValues);
});
return medias;
}
//if there's no result then return null. Previously we defaulted back to library.GetMedia below
//but this will always get called for when we are getting descendants since many items won't have
//children and then we are hitting the database again!
//So instead we're going to rely on Examine to have the correct results like it should.
return Enumerable.Empty<IPublishedContent>();
}
catch (FileNotFoundException)
{
//Currently examine is throwing FileNotFound exceptions when we have a load balanced filestore and a node is published in umbraco
//See this thread: http://examine.cdodeplex.com/discussions/264341
//Catch the exception here for the time being, and just fallback to GetMedia
}
}
// falling back to get media
// was library.GetMedia which had its own cache, but MediaService *also* caches
// so, library.GetMedia is gone and now we directly work with MediaService
// (code below copied from what library was doing)
var media = _mediaService.GetById(parentId);
if (media == null)
{
return Enumerable.Empty<IPublishedContent>();
}
var serialized = _entitySerializer.Serialize(media, true);
var mediaIterator = serialized.CreateNavigator().Select("/");
return mediaIterator.Current == null
? Enumerable.Empty<IPublishedContent>()
: ToIPublishedContent(parentId, mediaIterator.Current);
}
internal IEnumerable<IPublishedContent> ToIPublishedContent(int parentId, XPathNavigator xpath)
{
var mediaList = new List<IPublishedContent>();
// this is so bad, really
var item = xpath.Select("//*[@id='" + parentId + "']");
if (item.Current == null)
return Enumerable.Empty<IPublishedContent>();
var items = item.Current.SelectChildren(XPathNodeType.Element);
// and this does not work, because... meh
//var q = "//* [@id='" + parentId + "']/* [@id]";
//var items = xpath.Select(q);
foreach (XPathNavigator itemm in items)
{
int id;
if (int.TryParse(itemm.GetAttribute("id", ""), out id) == false)
continue; // uh?
var captured = itemm;
var cacheValues = GetCacheValues(id, idd => ConvertFromXPathNavigator(captured));
mediaList.Add(CreateFromCacheValues(cacheValues));
}
return mediaList;
}
internal void Resync()
{
// clear recursive properties cached by XmlPublishedContent.GetProperty
// assume that nothing else is going to cache IPublishedProperty items (else would need to do ByKeySearch)
// NOTE all properties cleared when clearing the content cache (see content cache)
//_appCache.ClearCacheObjectTypes<IPublishedProperty>();
//_appCache.ClearCacheByKeySearch("XmlPublishedCache.PublishedMediaCache:RecursiveProperty-");
}
#region Content types
public override IPublishedContentType GetContentType(int id) => _contentTypeCache.Get(PublishedItemType.Media, id);
public override IPublishedContentType GetContentType(string alias) => _contentTypeCache.Get(PublishedItemType.Media, alias);
public override IPublishedContentType GetContentType(Guid key) => _contentTypeCache.Get(PublishedItemType.Media, key);
public override IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType)
{
throw new NotSupportedException();
}
#endregion
// REFACTORING
// caching the basic atomic values - and the parent id
// but NOT caching actual parent nor children and NOT even
// the list of children ids - BUT caching the path
internal class CacheValues
{
public IReadOnlyDictionary<string, string> Values { get; set; }
public XPathNavigator XPath { get; set; }
public bool FromExamine { get; set; }
}
public const string PublishedMediaCacheKey = "MediaCacheMeh.";
private const int PublishedMediaCacheTimespanSeconds = 4 * 60; // 4 mins
private static TimeSpan _publishedMediaCacheTimespan;
private static bool _publishedMediaCacheEnabled;
private static void InitializeCacheConfig()
{
_publishedMediaCacheEnabled = true;
_publishedMediaCacheTimespan = TimeSpan.FromSeconds(PublishedMediaCacheTimespanSeconds);
}
internal IPublishedContent CreateFromCacheValues(CacheValues cacheValues)
{
var content = new DictionaryPublishedContent(
cacheValues.Values,
parentId => parentId < 0 ? null : GetUmbracoMedia(parentId),
GetChildrenMedia,
GetProperty,
_appCache,
_variationContextAccessor,
_contentTypeCache,
cacheValues.XPath, // though, outside of tests, that should be null
cacheValues.FromExamine
);
return content.CreateModel(Current.PublishedModelFactory);
}
private static CacheValues GetCacheValues(int id, Func<int, CacheValues> func)
{
if (_publishedMediaCacheEnabled == false)
return func(id);
var cache = Current.AppCaches.RuntimeCache;
var key = PublishedMediaCacheKey + id;
return (CacheValues)cache.Get(key, () => func(id), _publishedMediaCacheTimespan);
}
internal static void ClearCache(int id)
{
var cache = Current.AppCaches.RuntimeCache;
var sid = id.ToString();
var key = PublishedMediaCacheKey + sid;
// we do clear a lot of things... but the cache refresher is somewhat
// convoluted and it's hard to tell what to clear exactly ;-(
// clear the parent - NOT (why?)
//var exist = (CacheValues) cache.GetCacheItem(key);
//if (exist != null)
// cache.ClearCacheItem(PublishedMediaCacheKey + GetValuesValue(exist.Values, "parentID"));
// clear the item
cache.Clear(key);
// clear all children - in case we moved and their path has changed
var fid = "/" + sid + "/";
cache.ClearOfType<CacheValues>((k, v) =>
GetValuesValue(v.Values, "path", "__Path").Contains(fid));
}
private static string GetValuesValue(IReadOnlyDictionary<string, string> d, params string[] keys)
{
string value = null;
var ignored = keys.Any(x => d.TryGetValue(x, out value));
return value ?? "";
}
}
}

View File

@@ -1,154 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Tests.LegacyXmlPublishedCache
{
/// <summary>
/// Exposes a member object as IPublishedContent
/// </summary>
public sealed class PublishedMember : PublishedContentBase
{
private readonly IMember _member;
private readonly IMembershipUser _membershipUser;
private readonly IPublishedProperty[] _properties;
private readonly IPublishedContentType _publishedMemberType;
public PublishedMember(
IMember member,
IPublishedContentType publishedMemberType,
IVariationContextAccessor variationContextAccessor) : base(variationContextAccessor)
{
_member = member ?? throw new ArgumentNullException(nameof(member));
_membershipUser = member;
_publishedMemberType = publishedMemberType ?? throw new ArgumentNullException(nameof(publishedMemberType));
// RawValueProperty is used for two things here
// - for the 'map properties' thing that we should really get rid of
// - for populating properties that every member should always have, and that we force-create
// if they are not part of the member type properties - in which case they are created as
// simple raw properties - which are completely invariant
var properties = new List<IPublishedProperty>();
foreach (var propertyType in _publishedMemberType.PropertyTypes)
{
var property = _member.Properties[propertyType.Alias];
if (property == null) continue;
properties.Add(new RawValueProperty(propertyType, this, property.GetValue()));
}
EnsureMemberProperties(properties);
_properties = properties.ToArray();
}
#region Membership provider member properties
public string Email => _membershipUser.Email;
public string UserName => _membershipUser.Username;
public string Comments => _membershipUser.Comments;
public bool IsApproved => _membershipUser.IsApproved;
public bool IsLockedOut => _membershipUser.IsLockedOut;
public DateTime LastLockoutDate => _membershipUser.LastLockoutDate;
public DateTime CreationDate => _membershipUser.CreateDate;
public DateTime LastLoginDate => _membershipUser.LastLoginDate;
public DateTime LastPasswordChangeDate => _membershipUser.LastPasswordChangeDate;
#endregion
#region IPublishedContent
public override PublishedItemType ItemType => PublishedItemType.Member;
public override bool IsDraft(string culture = null) => false;
public override bool IsPublished(string culture = null) => true;
public override IPublishedContent Parent => null;
public override IEnumerable<IPublishedContent> Children => Enumerable.Empty<IPublishedContent>();
public override IEnumerable<IPublishedContent> ChildrenForAllCultures => Enumerable.Empty<IPublishedContent>();
public override IEnumerable<IPublishedProperty> Properties => _properties;
public override IPublishedProperty GetProperty(string alias)
{
return _properties.FirstOrDefault(x => x.Alias.InvariantEquals(alias));
}
private void EnsureMemberProperties(List<IPublishedProperty> properties)
{
var aliases = properties.Select(x => x.Alias).ToList();
EnsureMemberProperty(properties, aliases, nameof(IMember.Email), Email);
EnsureMemberProperty(properties, aliases, nameof(IMember.Username), UserName);
EnsureMemberProperty(properties, aliases, nameof(IMember.Comments), Comments);
EnsureMemberProperty(properties, aliases, nameof(IMember.IsApproved), IsApproved);
EnsureMemberProperty(properties, aliases, nameof(IMember.IsLockedOut), IsLockedOut);
EnsureMemberProperty(properties, aliases, nameof(IMember.LastLockoutDate), LastLockoutDate);
EnsureMemberProperty(properties, aliases, nameof(IMember.CreateDate), CreateDate);
EnsureMemberProperty(properties, aliases, nameof(IMember.LastLoginDate), LastLoginDate);
EnsureMemberProperty(properties, aliases, nameof(IMember.LastPasswordChangeDate), LastPasswordChangeDate);
}
private void EnsureMemberProperty(List<IPublishedProperty> properties, List<string> aliases, string alias, object value)
{
// if the property already has a value, nothing to do
if (aliases.Contains(alias)) return;
// if not a property type, ignore
var propertyType = ContentType.GetPropertyType(alias);
if (propertyType == null) return;
// create a raw-value property
// note: throws if propertyType variations is not InvariantNeutral
var property = new RawValueProperty(propertyType, this, value);
properties.Add(property);
}
public override IPublishedContentType ContentType => _publishedMemberType;
public override int Id => _member.Id;
public override Guid Key => _member.Key;
public override int? TemplateId => throw new NotSupportedException();
public override int SortOrder => 0;
public override string Name => _member.Name;
public override IReadOnlyDictionary<string, PublishedCultureInfo> Cultures => throw new NotSupportedException();
public override string UrlSegment => throw new NotSupportedException();
public override int WriterId => _member.CreatorId;
public override int CreatorId => _member.CreatorId;
public override string Path => _member.Path;
public override DateTime CreateDate => _member.CreateDate;
public override DateTime UpdateDate => _member.UpdateDate;
public override int Level => _member.Level;
public DateTime LastPasswordChangedDate => throw new NotImplementedException();
#endregion
}
}

View File

@@ -1,35 +0,0 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
using Umbraco.Web.Composing;
namespace Umbraco.Tests.LegacyXmlPublishedCache
{
class PublishedMemberCache : IPublishedMemberCache
{
private readonly PublishedContentTypeCache _contentTypeCache;
private readonly IVariationContextAccessor _variationContextAccessor;
public PublishedMemberCache(PublishedContentTypeCache contentTypeCache, IVariationContextAccessor variationContextAccessor)
{
_contentTypeCache = contentTypeCache;
_variationContextAccessor = variationContextAccessor;
}
public IPublishedContent Get(IMember member)
{
var type = _contentTypeCache.Get(PublishedItemType.Member, member.ContentTypeId);
return new PublishedMember(member, type, _variationContextAccessor)
.CreateModel(Current.PublishedModelFactory);
}
#region Content types
public IPublishedContentType GetContentType(int id) => _contentTypeCache.Get(PublishedItemType.Member, id);
public IPublishedContentType GetContentType(string alias) => _contentTypeCache.Get(PublishedItemType.Member, alias);
#endregion
}
}

View File

@@ -1,63 +0,0 @@
using System;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.PublishedCache;
namespace Umbraco.Tests.LegacyXmlPublishedCache
{
/// <summary>
/// Implements a published snapshot.
/// </summary>
class PublishedSnapshot : IPublishedSnapshot
{
/// <summary>
/// Initializes a new instance of the <see cref="PublishedSnapshot"/> class with a content cache
/// and a media cache.
/// </summary>
public PublishedSnapshot(
PublishedContentCache contentCache,
PublishedMediaCache mediaCache,
PublishedMemberCache memberCache,
DomainCache domainCache)
{
Content = contentCache;
Media = mediaCache;
Members = memberCache;
Domains = domainCache;
}
/// <inheritdoc />
public IPublishedContentCache Content { get; }
/// <inheritdoc />
public IPublishedMediaCache Media { get; }
/// <inheritdoc />
public IPublishedMemberCache Members { get; }
/// <inheritdoc />
public IDomainCache Domains { get; }
/// <inheritdoc />
public IAppCache SnapshotCache => null;
/// <inheritdoc />
public IAppCache ElementsCache => null;
/// <inheritdoc />
public IDisposable ForcedPreview(bool preview, Action<bool> callback = null)
{
// the XML cache does not support forcing preview, really, so, just pretend...
return new ForcedPreviewObject();
}
private class ForcedPreviewObject : DisposableObjectSlim
{
protected override void DisposeResources()
{ }
}
public void Dispose()
{ }
}
}

View File

@@ -1,112 +0,0 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace Umbraco.Tests.LegacyXmlPublishedCache
{
// Note: RoutesCache closely follows the caching strategy dating from v4, which
// is obviously broken in many ways (eg it's a global cache but relying to some
// extend to the content cache, which itself is local to each request...).
// Not going to fix it anyway.
class RoutesCache
{
private ConcurrentDictionary<int, string> _routes;
private ConcurrentDictionary<string, int> _nodeIds;
// NOTE
// RoutesCache is cleared by
// - ContentTypeCacheRefresher, whenever anything happens to any content type
// - DomainCacheRefresher, whenever anything happens to any domain
// - XmlStore, whenever anything happens to the XML cache
/// <summary>
/// Initializes a new instance of the <see cref="RoutesCache"/> class.
/// </summary>
public RoutesCache()
{
Clear();
}
/// <summary>
/// Used ONLY for unit tests
/// </summary>
/// <returns></returns>
internal IDictionary<int, string> GetCachedRoutes()
{
return _routes;
}
/// <summary>
/// Used ONLY for unit tests
/// </summary>
/// <returns></returns>
internal IDictionary<string, int> GetCachedIds()
{
return _nodeIds;
}
#region Public
/// <summary>
/// Stores a route for a node.
/// </summary>
/// <param name="nodeId">The node identified.</param>
/// <param name="route">The route.</param>
/// <param name="trust">A value indicating whether the value can be trusted for inbound routing.</param>
public void Store(int nodeId, string route, bool trust)
{
_routes.AddOrUpdate(nodeId, i => route, (i, s) => route);
if (trust)
_nodeIds.AddOrUpdate(route, i => nodeId, (i, s) => nodeId);
}
/// <summary>
/// Gets a route for a node.
/// </summary>
/// <param name="nodeId">The node identifier.</param>
/// <returns>The route for the node, else null.</returns>
public string GetRoute(int nodeId)
{
string val;
_routes.TryGetValue(nodeId, out val);
return val;
}
/// <summary>
/// Gets a node for a route.
/// </summary>
/// <param name="route">The route.</param>
/// <returns>The node identified for the route, else zero.</returns>
public int GetNodeId(string route)
{
int val;
_nodeIds.TryGetValue(route, out val);
return val;
}
/// <summary>
/// Clears the route for a node.
/// </summary>
/// <param name="nodeId">The node identifier.</param>
public void ClearNode(int nodeId)
{
string route;
if (_routes.TryRemove(nodeId, out route))
{
int id;
_nodeIds.TryRemove(route, out id);
}
}
/// <summary>
/// Clears all routes.
/// </summary>
public void Clear()
{
_routes = new ConcurrentDictionary<int, string>();
_nodeIds = new ConcurrentDictionary<string, int>();
}
#endregion
}
}

View File

@@ -1,154 +0,0 @@
using System;
using System.Xml;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Scoping;
namespace Umbraco.Tests.LegacyXmlPublishedCache
{
// TODO: should be a ScopeContextualBase
internal class SafeXmlReaderWriter : IDisposable
{
private readonly bool _scoped;
private readonly Action<XmlDocument> _refresh;
private readonly Action<XmlDocument, bool> _apply;
private IDisposable _releaser;
private bool _applyChanges;
private XmlDocument _xml, _origXml;
private bool _using;
private bool _registerXmlChange;
// the default enlist priority is 100
// enlist with a lower priority to ensure that anything "default" has a clean xml
private const int EnlistPriority = 60;
private const string EnlistKey = "safeXmlReaderWriter";
private SafeXmlReaderWriter(IDisposable releaser, XmlDocument xml, Action<XmlDocument> refresh, Action<XmlDocument, bool> apply, bool isWriter, bool scoped)
{
_releaser = releaser;
_refresh = refresh;
_apply = apply;
_scoped = scoped;
IsWriter = isWriter;
_xml = IsWriter ? Clone(xml) : xml;
}
public static SafeXmlReaderWriter Get(IScopeProvider scopeProvider)
{
return scopeProvider?.Context?.GetEnlisted<SafeXmlReaderWriter>(EnlistKey);
}
public static SafeXmlReaderWriter Get(IScopeProvider scopeProvider, SystemLock xmlLock, XmlDocument xml, Action<XmlDocument> refresh, Action<XmlDocument, bool> apply, bool writer)
{
var scopeContext = scopeProvider.Context;
// no real scope = just create a reader/writer instance
if (scopeContext == null)
{
// obtain exclusive access to xml and create reader/writer
var releaser = xmlLock.Lock();
return new SafeXmlReaderWriter(releaser, xml, refresh, apply, writer, false);
}
// get or create an enlisted reader/writer
var rw = scopeContext.Enlist(EnlistKey,
() => // creator
{
// obtain exclusive access to xml and create reader/writer
var releaser = xmlLock.Lock();
return new SafeXmlReaderWriter(releaser, xml, refresh, apply, writer, true);
},
(completed, item) => // action
{
item.DisposeForReal(completed);
}, EnlistPriority);
// ensure it's not already in-use - should never happen, just being super safe
if (rw._using)
throw new InvalidOperationException("panic: used.");
rw._using = true;
return rw;
}
public bool IsWriter { get; private set; }
public void UpgradeToWriter(bool auto)
{
if (IsWriter)
throw new InvalidOperationException("Already a writer.");
IsWriter = true;
_xml = Clone(_xml);
}
// for tests
internal static Action Cloning { get; set; }
private XmlDocument Clone(XmlDocument xml)
{
Cloning?.Invoke();
if (_origXml != null)
throw new Exception("panic.");
_origXml = xml;
return (XmlDocument) xml?.CloneNode(true);
}
public XmlDocument Xml
{
get => _xml;
set
{
if (IsWriter == false)
throw new InvalidOperationException("Not a writer.");
_xml = value;
}
}
// registerXmlChange indicates whether to do what should be done when Xml changes,
// that is, to request that the file be written to disk - something we don't want
// to do if we're committing Xml precisely after we've read from disk!
public void AcceptChanges(bool registerXmlChange = true)
{
if (IsWriter == false)
throw new InvalidOperationException("Not a writer.");
_applyChanges = true;
_registerXmlChange |= registerXmlChange;
}
private void DisposeForReal(bool completed)
{
if (IsWriter)
{
// apply changes, or restore the original xml for the current request
if (_applyChanges && completed)
_apply(_xml, _registerXmlChange);
else
_refresh(_origXml);
}
// release the lock
_releaser.Dispose();
_releaser = null;
}
public void Dispose()
{
_using = false;
if (_scoped == false)
{
// really dispose
DisposeForReal(true);
}
else
{
// don't really dispose,
// just apply the changes for the current request
_refresh(_xml);
}
}
}
}

View File

@@ -1,26 +0,0 @@
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using Umbraco.Cms.Core.Web;
using Umbraco.Web;
namespace Umbraco.Tests.LegacyXmlPublishedCache
{
static class UmbracoContextCache
{
static readonly ConditionalWeakTable<IUmbracoContext, ConcurrentDictionary<string, object>> Caches
= new ConditionalWeakTable<IUmbracoContext, ConcurrentDictionary<string, object>>();
public static ConcurrentDictionary<string, object> Current
{
get
{
var umbracoContext = Umbraco.Web.Composing.Current.UmbracoContext;
// will get or create a value
// a ConditionalWeakTable is thread-safe
// does not prevent the context from being disposed, and then the dictionary will be disposed too
return umbracoContext == null ? null : Caches.GetOrCreateValue(umbracoContext);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More