From c26016b0b0c197c708a4508484c38fcab4be75e5 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:00:20 +0100 Subject: [PATCH] V15: Implement content type filtering for in-memory navigation structure (#17456) * Adding contentType to navigation node * Loading contentType from DB * Considering contentTypeKey when adding a navigation node & fixing references * Using IContentTypeBaseService to load content types * Adding generics to ContentNavigationServiceBase and fixing references * Adding TryGetChildrenKeysOfType and implementation * Refactoring test data * Adding unit tests for TryGetChildrenKeysOfType * Update CreateContentCreateModel in tests to receive content type * Fix references * Cleanup * Adding integration tests for TryGetChildrenKeysOfType * Cleanup * Cleanup * Descendants of type implementation * Descendants of type tests * Interface updates * Ancestors of type implementation and tests * Siblings of type implementation and tests * Cleanup * Integration tests * Adding root of type implementation and tests * Fix Ancestors extension methods * Fix descendants extension methods * Fix children extension methods * Fix siblings extension methods * Add helper methods * Fix a bug * Fixed unit tests by setting up mocks * Adding missing extension method --------- Co-authored-by: Bjarke Berg --- .../Implement/ContentCacheRefresher.cs | 2 +- .../Implement/MediaCacheRefresher.cs | 2 +- .../Extensions/PublishedContentExtensions.cs | 347 ++++-- src/Umbraco.Core/Models/INavigationModel.cs | 5 + .../Models/Navigation/NavigationNode.cs | 5 +- .../ContentNavigationServiceBase.cs | 197 +++- .../Navigation/DocumentNavigationService.cs | 7 +- .../INavigationManagementService.cs | 3 +- .../Navigation/INavigationQueryService.cs | 16 +- .../IRecycleBinNavigationQueryService.cs | 2 +- .../Navigation/MediaNavigationService.cs | 7 +- .../Persistence/Dtos/NavigationDto.cs | 6 + .../Implement/ContentNavigationRepository.cs | 16 +- .../FriendlyPublishedContentExtensions.cs | 8 + .../DocumentNavigationServiceTests.Create.cs | 6 +- .../DocumentNavigationServiceTests.Delete.cs | 2 +- .../DocumentNavigationServiceTests.Move.cs | 2 +- ...NavigationServiceTests.MoveToRecycleBin.cs | 4 +- .../DocumentNavigationServiceTests.Rebuild.cs | 11 +- .../DocumentNavigationServiceTests.Sort.cs | 6 +- .../DocumentNavigationServiceTests.cs | 212 +++- .../DocumentNavigationServiceTestsBase.cs | 5 +- .../MediaNavigationServiceTests.Rebuild.cs | 11 +- .../Services/MediaNavigationServiceTests.cs | 23 + .../DeliveryApi/ContentRouteBuilderTests.cs | 27 + .../ContentNavigationServiceBaseTests.cs | 1013 ++++++++++++++++- .../Services/ContentNavigationServiceTest.cs | 7 +- 27 files changed, 1725 insertions(+), 227 deletions(-) diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs index 1f81c35912..1ed8e6eb33 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs @@ -244,7 +244,7 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase - content.AncestorsOrSelf(publishedCache, navigationQueryService, false, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); + string contentTypeAlias) + { + ArgumentNullException.ThrowIfNull(content); + + return content.EnumerateAncestorsOrSelfInternal(publishedCache, navigationQueryService, false, contentTypeAlias); + } /// /// Gets the ancestors of the content, of a specified content type. @@ -652,8 +656,12 @@ public static class PublishedContentExtensions this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - string contentTypeAlias) => - content.AncestorsOrSelf(publishedCache, navigationQueryService, true, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); + string contentTypeAlias) + { + ArgumentNullException.ThrowIfNull(content); + + return content.EnumerateAncestorsOrSelfInternal(publishedCache, navigationQueryService, true, contentTypeAlias); + } /// /// Gets the content and its ancestors, of a specified content type. @@ -736,8 +744,14 @@ public static class PublishedContentExtensions this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - string contentTypeAlias) => - content.EnumerateAncestors(publishedCache, navigationQueryService, false).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + string contentTypeAlias) + { + ArgumentNullException.ThrowIfNull(content); + + return content + .EnumerateAncestorsOrSelfInternal(publishedCache, navigationQueryService, false, contentTypeAlias) + .FirstOrDefault(); + } /// /// Gets the nearest ancestor of the content, of a specified content type. @@ -813,8 +827,14 @@ public static class PublishedContentExtensions this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - string contentTypeAlias) => content - .EnumerateAncestors(publishedCache, navigationQueryService, true).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)) ?? content; + string contentTypeAlias) + { + ArgumentNullException.ThrowIfNull(content); + + return content + .EnumerateAncestorsOrSelfInternal(publishedCache, navigationQueryService, true, contentTypeAlias) + .FirstOrDefault() ?? content; + } /// /// Gets the content or its nearest ancestor, of a specified content type. @@ -875,20 +895,9 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, bool orSelf) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } + ArgumentNullException.ThrowIfNull(content); - if (orSelf) - { - yield return content; - } - - while ((content = content.GetParent(publishedCache, navigationQueryService)) != null) - { - yield return content; - } + return content.EnumerateAncestorsOrSelfInternal(publishedCache, navigationQueryService, orSelf); } #endregion @@ -1059,8 +1068,15 @@ public static class PublishedContentExtensions IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - string contentTypeAlias, string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, false, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + string contentTypeAlias, + string? culture = null) => + content.EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + publishedCache, + navigationQueryService, + culture, + false, + contentTypeAlias); public static IEnumerable Descendants( this IPublishedContent content, @@ -1105,7 +1121,13 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, string contentTypeAlias, string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, true, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + content.EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + publishedCache, + navigationQueryService, + culture, + true, + contentTypeAlias); public static IEnumerable DescendantsOrSelf( this IPublishedContent content, @@ -1150,8 +1172,14 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, string contentTypeAlias, string? culture = null) => content - .EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, false, culture) - .FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + .EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + publishedCache, + navigationQueryService, + culture, + false, + contentTypeAlias) + .FirstOrDefault(); public static T? Descendant( this IPublishedContent content, @@ -1190,8 +1218,14 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, string contentTypeAlias, string? culture = null) => content - .EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, true, culture) - .FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + .EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + publishedCache, + navigationQueryService, + culture, + true, + contentTypeAlias) + .FirstOrDefault(); public static T? DescendantOrSelf( this IPublishedContent content, @@ -1231,24 +1265,16 @@ public static class PublishedContentExtensions bool orSelf, string? culture = null) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } + ArgumentNullException.ThrowIfNull(content); - if (orSelf) + foreach (IPublishedContent desc in content.EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + publishedCache, + navigationQueryService, + culture, + orSelf)) { - yield return content; - } - - IEnumerable? children = content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture); - if (children is not null) - { - foreach (IPublishedContent desc in children.SelectMany(x => - x.EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, culture))) - { - yield return desc; - } + yield return desc; } } @@ -1260,14 +1286,15 @@ public static class PublishedContentExtensions string? culture = null) { yield return content; - IEnumerable? children = content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture); - if (children is not null) + + foreach (IPublishedContent desc in content.EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + publishedCache, + navigationQueryService, + culture, + false)) { - foreach (IPublishedContent desc in children.SelectMany(x => - x.EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, culture))) - { - yield return desc; - } + yield return desc; } } @@ -1310,25 +1337,9 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, string? culture = null) { - // handle context culture for variant - if (culture is null) - { - culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; - } + IEnumerable children = GetChildren(navigationQueryService, publishedCache, content.Key); - if (navigationQueryService.TryGetChildrenKeys(content.Key, out IEnumerable childrenKeys) is false) - { - return []; - } - - IEnumerable children = childrenKeys.Select(publishedCache.GetById).WhereNotNull(); - - if (culture == "*") - { - return children; - } - - return children.Where(x => x.IsInvariantOrHasCulture(culture)) ?? []; + return children.FilterByCulture(culture, variationContextAccessor); } /// @@ -1375,9 +1386,14 @@ public static class PublishedContentExtensions IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string? contentTypeAlias, - string? culture = null) => - content.Children(variationContextAccessor, publishedCache, navigationQueryService, x => x.ContentType.Alias.InvariantEquals(contentTypeAlias), - culture); + string? culture = null) + { + IEnumerable children = contentTypeAlias is not null + ? GetChildren(navigationQueryService, publishedCache, content.Key, contentTypeAlias) + : []; + + return children.FilterByCulture(culture, variationContextAccessor); + } /// /// Gets the children of the content, of a given content type. @@ -1419,8 +1435,9 @@ public static class PublishedContentExtensions IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string contentTypeAlias, - string? culture = null) => - content.ChildrenOfType(variationContextAccessor, publishedCache, navigationQueryService, contentTypeAlias, culture)?.FirstOrDefault(); + string? culture = null) => content + .ChildrenOfType(variationContextAccessor, publishedCache, navigationQueryService, contentTypeAlias, culture) + .FirstOrDefault(); public static IPublishedContent? FirstChild( this IPublishedContent content, @@ -1510,7 +1527,7 @@ public static class PublishedContentExtensions string contentTypeAlias, string? culture = null) => SiblingsAndSelfOfType(content, variationContextAccessor, publishedCache, navigationQueryService, contentTypeAlias, culture) - ?.Where(x => x.Id != content.Id) ?? Enumerable.Empty(); + .Where(x => x.Id != content.Id); /// /// Gets the siblings of the content, of a given content type. @@ -1596,26 +1613,26 @@ public static class PublishedContentExtensions string contentTypeAlias, string? culture = null) { + var parentExists = navigationQueryService.TryGetParentKey(content.Key, out Guid? parentKey); - var parentSuccess = navigationQueryService.TryGetParentKey(content.Key, out Guid? parentKey); + IPublishedContent? parent = parentKey is null + ? null + : publishedCache.GetById(parentKey.Value); - IPublishedContent? parent = parentKey is null ? null : publishedCache.GetById(parentKey.Value); - - if (parentSuccess is false || parent is null) + if (parentExists && parent is not null) { - if (navigationQueryService.TryGetRootKeys(out IEnumerable childrenKeys) is false) - { - return Enumerable.Empty(); - } - - return childrenKeys - .Select(publishedCache.GetById) - .WhereNotNull() - .OfTypes(contentTypeAlias) - .WhereIsInvariantOrHasCulture(variationContextAccessor, culture); + return parent.ChildrenOfType(variationContextAccessor, publishedCache, navigationQueryService, contentTypeAlias, culture); } - return parent.ChildrenOfType(variationContextAccessor, publishedCache, navigationQueryService, contentTypeAlias, culture); + if (navigationQueryService.TryGetRootKeysOfType(contentTypeAlias, out IEnumerable rootKeysOfType) is false) + { + return []; + } + + return rootKeysOfType + .Select(publishedCache.GetById) + .WhereNotNull() + .WhereIsInvariantOrHasCulture(variationContextAccessor, culture); } /// @@ -2036,10 +2053,14 @@ public static class PublishedContentExtensions this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? contentTypeAlias, - string? culture = null) => - content.Children(variationContextAccessor, GetPublishedCache(content), - GetNavigationQueryService(content), x => x.ContentType.Alias.InvariantEquals(contentTypeAlias), - culture); + string? culture = null) + { + IEnumerable children = contentTypeAlias is not null + ? GetChildren(GetNavigationQueryService(content), GetPublishedCache(content), content.Key, contentTypeAlias) + : []; + + return children.FilterByCulture(culture, variationContextAccessor); + } [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable Children( @@ -2104,8 +2125,13 @@ public static class PublishedContentExtensions this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, GetPublishedCache(content), - GetNavigationQueryService(content), false, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + content.EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + GetPublishedCache(content), + GetNavigationQueryService(content), + culture, + false, + contentTypeAlias); [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable Descendants( @@ -2149,8 +2175,13 @@ public static class PublishedContentExtensions IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, GetPublishedCache(content), - GetNavigationQueryService(content), true, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + content.EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + GetPublishedCache(content), + GetNavigationQueryService(content), + culture, + true, + contentTypeAlias); [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable DescendantsOrSelf( @@ -2194,9 +2225,14 @@ public static class PublishedContentExtensions IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => content - .EnumerateDescendants(variationContextAccessor, GetPublishedCache(content), - GetNavigationQueryService(content), false, culture) - .FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + .EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + GetPublishedCache(content), + GetNavigationQueryService(content), + culture, + false, + contentTypeAlias) + .FirstOrDefault(); [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static T? Descendant( @@ -2232,9 +2268,14 @@ public static class PublishedContentExtensions IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => content - .EnumerateDescendants(variationContextAccessor, GetPublishedCache(content), - GetNavigationQueryService(content), true, culture) - .FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + .EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + GetPublishedCache(content), + GetNavigationQueryService(content), + culture, + true, + contentTypeAlias) + .FirstOrDefault(); [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static T? DescendantOrSelf( @@ -2388,4 +2429,102 @@ public static class PublishedContentExtensions throw new NotSupportedException("Unsupported content type."); } } + + private static IEnumerable GetChildren( + INavigationQueryService navigationQueryService, + IPublishedCache publishedCache, + Guid parentKey, + string? contentTypeAlias = null) + { + var nodeExists = contentTypeAlias is null + ? navigationQueryService.TryGetChildrenKeys(parentKey, out IEnumerable childrenKeys) + : navigationQueryService.TryGetChildrenKeysOfType(parentKey, contentTypeAlias, out childrenKeys); + + if (nodeExists is false) + { + return []; + } + + return childrenKeys + .Select(publishedCache.GetById) + .WhereNotNull(); + } + + private static IEnumerable FilterByCulture( + this IEnumerable contentNodes, + string? culture, + IVariationContextAccessor? variationContextAccessor) + { + // Determine the culture if not provided + culture ??= variationContextAccessor?.VariationContext?.Culture ?? string.Empty; + + return culture == "*" + ? contentNodes + : contentNodes.Where(x => x.IsInvariantOrHasCulture(culture)); + } + + private static IEnumerable EnumerateDescendantsOrSelfInternal( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture, + bool orSelf, + string? contentTypeAlias = null) + { + if (orSelf) + { + yield return content; + } + + var nodeExists = contentTypeAlias is null + ? navigationQueryService.TryGetDescendantsKeys(content.Key, out IEnumerable descendantsKeys) + : navigationQueryService.TryGetDescendantsKeysOfType(content.Key, contentTypeAlias, out descendantsKeys); + + if (nodeExists is false) + { + yield break; + } + + IEnumerable descendants = descendantsKeys + .Select(publishedCache.GetById) + .WhereNotNull() + .FilterByCulture(culture, variationContextAccessor); + + foreach (IPublishedContent descendant in descendants) + { + yield return descendant; + } + } + + private static IEnumerable EnumerateAncestorsOrSelfInternal( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + bool orSelf, + string? contentTypeAlias = null) + { + if (orSelf) + { + yield return content; + } + + var nodeExists = contentTypeAlias is null + ? navigationQueryService.TryGetAncestorsKeys(content.Key, out IEnumerable ancestorsKeys) + : navigationQueryService.TryGetAncestorsKeysOfType(content.Key, contentTypeAlias, out ancestorsKeys); + + if (nodeExists is false) + { + yield break; + } + + foreach (Guid ancestorKey in ancestorsKeys) + { + IPublishedContent? ancestor = publishedCache.GetById(ancestorKey); + if (ancestor is not null) + { + yield return ancestor; + } + } + } } diff --git a/src/Umbraco.Core/Models/INavigationModel.cs b/src/Umbraco.Core/Models/INavigationModel.cs index 9663419627..574d1b7c94 100644 --- a/src/Umbraco.Core/Models/INavigationModel.cs +++ b/src/Umbraco.Core/Models/INavigationModel.cs @@ -12,6 +12,11 @@ public interface INavigationModel /// Guid Key { get; set; } + /// + /// Gets or sets the Guid unique identifier of the entity's content type. + /// + public Guid ContentTypeKey { get; set; } + /// /// Gets or sets the integer identifier of the parent entity. /// diff --git a/src/Umbraco.Core/Models/Navigation/NavigationNode.cs b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs index 5e8e412116..d2db0c6294 100644 --- a/src/Umbraco.Core/Models/Navigation/NavigationNode.cs +++ b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs @@ -8,15 +8,18 @@ public sealed class NavigationNode public Guid Key { get; private set; } + public Guid ContentTypeKey { get; private set; } + public int SortOrder { get; private set; } public Guid? Parent { get; private set; } public ISet Children => _children; - public NavigationNode(Guid key, int sortOrder = 0) + public NavigationNode(Guid key, Guid contentTypeKey, int sortOrder = 0) { Key = key; + ContentTypeKey = contentTypeKey; SortOrder = sortOrder; _children = new HashSet(); } diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index 9a19e31dd1..531b3952a7 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -7,19 +7,25 @@ using Umbraco.Cms.Core.Scoping; namespace Umbraco.Cms.Core.Services.Navigation; -internal abstract class ContentNavigationServiceBase +internal abstract class ContentNavigationServiceBase + where TContentType : class, IContentTypeComposition + where TContentTypeService : IContentTypeBaseService { private readonly ICoreScopeProvider _coreScopeProvider; private readonly INavigationRepository _navigationRepository; + private readonly TContentTypeService _typeService; + private Lazy> _contentTypeAliasToKeyMap; private ConcurrentDictionary _navigationStructure = new(); private ConcurrentDictionary _recycleBinNavigationStructure = new(); private IList _roots = new List(); private IList _recycleBinRoots = new List(); - protected ContentNavigationServiceBase(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) + protected ContentNavigationServiceBase(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository, TContentTypeService typeService) { _coreScopeProvider = coreScopeProvider; _navigationRepository = navigationRepository; + _typeService = typeService; + _contentTypeAliasToKeyMap = new Lazy>(LoadContentTypes); } /// @@ -38,18 +44,78 @@ internal abstract class ContentNavigationServiceBase public bool TryGetRootKeys(out IEnumerable rootKeys) => TryGetRootKeysFromStructure(_roots, out rootKeys); + public bool TryGetRootKeysOfType(string contentTypeAlias, out IEnumerable rootKeys) + { + if (TryGetContentTypeKey(contentTypeAlias, out Guid? contentTypeKey)) + { + return TryGetRootKeysFromStructure(_roots, out rootKeys, contentTypeKey); + } + + // Content type alias doesn't exist + rootKeys = []; + return false; + } + public bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys) => TryGetChildrenKeysFromStructure(_navigationStructure, parentKey, out childrenKeys); + public bool TryGetChildrenKeysOfType(Guid parentKey, string contentTypeAlias, out IEnumerable childrenKeys) + { + if (TryGetContentTypeKey(contentTypeAlias, out Guid? contentTypeKey)) + { + return TryGetChildrenKeysFromStructure(_navigationStructure, parentKey, out childrenKeys, contentTypeKey); + } + + // Content type alias doesn't exist + childrenKeys = []; + return false; + } + public bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys) => TryGetDescendantsKeysFromStructure(_navigationStructure, parentKey, out descendantsKeys); + public bool TryGetDescendantsKeysOfType(Guid parentKey, string contentTypeAlias, out IEnumerable descendantsKeys) + { + if (TryGetContentTypeKey(contentTypeAlias, out Guid? contentTypeKey)) + { + return TryGetDescendantsKeysFromStructure(_navigationStructure, parentKey, out descendantsKeys, contentTypeKey); + } + + // Content type alias doesn't exist + descendantsKeys = []; + return false; + } + public bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKeys) => TryGetAncestorsKeysFromStructure(_navigationStructure, childKey, out ancestorsKeys); + public bool TryGetAncestorsKeysOfType(Guid parentKey, string contentTypeAlias, out IEnumerable ancestorsKeys) + { + if (TryGetContentTypeKey(contentTypeAlias, out Guid? contentTypeKey)) + { + return TryGetAncestorsKeysFromStructure(_navigationStructure, parentKey, out ancestorsKeys, contentTypeKey); + } + + // Content type alias doesn't exist + ancestorsKeys = []; + return false; + } + public bool TryGetSiblingsKeys(Guid key, out IEnumerable siblingsKeys) => TryGetSiblingsKeysFromStructure(_navigationStructure, key, out siblingsKeys); + public bool TryGetSiblingsKeysOfType(Guid key, string contentTypeAlias, out IEnumerable siblingsKeys) + { + if (TryGetContentTypeKey(contentTypeAlias, out Guid? contentTypeKey)) + { + return TryGetSiblingsKeysFromStructure(_navigationStructure, key, out siblingsKeys, contentTypeKey); + } + + // Content type alias doesn't exist + siblingsKeys = []; + return false; + } + public bool TryGetParentKeyInBin(Guid childKey, out Guid? parentKey) => TryGetParentKeyFromStructure(_recycleBinNavigationStructure, childKey, out parentKey); @@ -104,7 +170,7 @@ internal abstract class ContentNavigationServiceBase _navigationStructure.TryRemove(key, out _); } - public bool Add(Guid key, Guid? parentKey = null, int? sortOrder = null) + public bool Add(Guid key, Guid contentTypeKey, Guid? parentKey = null, int? sortOrder = null) { NavigationNode? parentNode = null; if (parentKey.HasValue) @@ -120,7 +186,7 @@ internal abstract class ContentNavigationServiceBase } // Note: sortOrder can't be automatically determined for items at root level, so it needs to be passed in - var newNode = new NavigationNode(key, sortOrder ?? 0); + var newNode = new NavigationNode(key, contentTypeKey, sortOrder ?? 0); if (_navigationStructure.TryAdd(key, newNode) is false) { return false; // Node with this key already exists @@ -264,18 +330,30 @@ internal abstract class ContentNavigationServiceBase return false; } - private bool TryGetRootKeysFromStructure(IList input, out IEnumerable rootKeys) + private bool TryGetRootKeysFromStructure( + IList input, + out IEnumerable rootKeys, + Guid? contentTypeKey = null) { + // Apply contentTypeKey filter + IEnumerable filteredKeys = contentTypeKey.HasValue + ? input.Where(key => _navigationStructure[key].ContentTypeKey == contentTypeKey.Value) + : input; + // TODO can we make this more efficient? // Sort by SortOrder - rootKeys = input + rootKeys = filteredKeys .OrderBy(key => _navigationStructure[key].SortOrder) .ToList(); return true; } - private bool TryGetChildrenKeysFromStructure(ConcurrentDictionary structure, Guid parentKey, out IEnumerable childrenKeys) + private bool TryGetChildrenKeysFromStructure( + ConcurrentDictionary structure, + Guid parentKey, + out IEnumerable childrenKeys, + Guid? contentTypeKey = null) { if (structure.TryGetValue(parentKey, out NavigationNode? parentNode) is false) { @@ -285,12 +363,16 @@ internal abstract class ContentNavigationServiceBase } // Keep children keys ordered based on their SortOrder - childrenKeys = GetOrderedChildren(parentNode, structure).ToList(); + childrenKeys = GetOrderedChildren(parentNode, structure, contentTypeKey).ToList(); return true; } - private bool TryGetDescendantsKeysFromStructure(ConcurrentDictionary structure, Guid parentKey, out IEnumerable descendantsKeys) + private bool TryGetDescendantsKeysFromStructure( + ConcurrentDictionary structure, + Guid parentKey, + out IEnumerable descendantsKeys, + Guid? contentTypeKey = null) { var descendants = new List(); @@ -301,13 +383,17 @@ internal abstract class ContentNavigationServiceBase return false; } - GetDescendantsRecursively(structure, parentNode, descendants); + GetDescendantsRecursively(structure, parentNode, descendants, contentTypeKey); descendantsKeys = descendants; return true; } - private bool TryGetAncestorsKeysFromStructure(ConcurrentDictionary structure, Guid childKey, out IEnumerable ancestorsKeys) + private bool TryGetAncestorsKeysFromStructure( + ConcurrentDictionary structure, + Guid childKey, + out IEnumerable ancestorsKeys, + Guid? contentTypeKey = null) { var ancestors = new List(); @@ -320,14 +406,22 @@ internal abstract class ContentNavigationServiceBase while (node.Parent is not null && structure.TryGetValue(node.Parent.Value, out node)) { - ancestors.Add(node.Key); + // Apply contentTypeKey filter + if (contentTypeKey.HasValue is false || node.ContentTypeKey == contentTypeKey.Value) + { + ancestors.Add(node.Key); + } } ancestorsKeys = ancestors; return true; } - private bool TryGetSiblingsKeysFromStructure(ConcurrentDictionary structure, Guid key, out IEnumerable siblingsKeys) + private bool TryGetSiblingsKeysFromStructure( + ConcurrentDictionary structure, + Guid key, + out IEnumerable siblingsKeys, + Guid? contentTypeKey = null) { siblingsKeys = []; @@ -339,15 +433,23 @@ internal abstract class ContentNavigationServiceBase if (node.Parent is null) { // To find siblings of a node at root level, we need to iterate over all items and add those with null Parent - siblingsKeys = structure - .Where(kv => kv.Value.Parent is null && kv.Key != key) + IEnumerable> filteredSiblings = structure + .Where(kv => kv.Value.Parent is null && kv.Key != key); + + // Apply contentTypeKey filter + if (contentTypeKey.HasValue) + { + filteredSiblings = filteredSiblings.Where(kv => kv.Value.ContentTypeKey == contentTypeKey.Value); + } + + siblingsKeys = filteredSiblings .OrderBy(kv => kv.Value.SortOrder) .Select(kv => kv.Key) .ToList(); return true; } - if (TryGetChildrenKeys(node.Parent.Value, out IEnumerable childrenKeys) is false) + if (TryGetChildrenKeysFromStructure(structure, node.Parent.Value, out IEnumerable childrenKeys, contentTypeKey) is false) { return false; // Couldn't retrieve children keys } @@ -357,17 +459,26 @@ internal abstract class ContentNavigationServiceBase return true; } - private void GetDescendantsRecursively(ConcurrentDictionary structure, NavigationNode node, List descendants) + private void GetDescendantsRecursively( + ConcurrentDictionary structure, + NavigationNode node, + List descendants, + Guid? contentTypeKey = null) { + // Get all children regardless of contentType var childrenKeys = GetOrderedChildren(node, structure).ToList(); foreach (Guid childKey in childrenKeys) { - descendants.Add(childKey); + // Apply contentTypeKey filter + if (contentTypeKey.HasValue is false || structure[childKey].ContentTypeKey == contentTypeKey.Value) + { + descendants.Add(childKey); + } // Retrieve the child node and its descendants if (structure.TryGetValue(childKey, out NavigationNode? childNode)) { - GetDescendantsRecursively(structure, childNode, descendants); + GetDescendantsRecursively(structure, childNode, descendants, contentTypeKey); } } } @@ -455,11 +566,48 @@ internal abstract class ContentNavigationServiceBase } } - private IEnumerable GetOrderedChildren(NavigationNode node, ConcurrentDictionary structure) - => node.Children - .Where(structure.ContainsKey) + private IEnumerable GetOrderedChildren( + NavigationNode node, + ConcurrentDictionary structure, + Guid? contentTypeKey = null) + { + IEnumerable children = node + .Children + .Where(structure.ContainsKey); + + // Apply contentTypeKey filter + if (contentTypeKey.HasValue) + { + children = children.Where(childKey => structure[childKey].ContentTypeKey == contentTypeKey.Value); + } + + return children .OrderBy(childKey => structure[childKey].SortOrder) .ToList(); + } + + private bool TryGetContentTypeKey(string contentTypeAlias, out Guid? contentTypeKey) + { + Dictionary aliasToKeyMap = _contentTypeAliasToKeyMap.Value; + + if (aliasToKeyMap.TryGetValue(contentTypeAlias, out Guid key)) + { + contentTypeKey = key; + return true; + } + + TContentType? contentType = _typeService.Get(contentTypeAlias); + if (contentType is null) + { + // Content type alias doesn't exist + contentTypeKey = null; + return false; + } + + aliasToKeyMap.TryAdd(contentTypeAlias, contentType.Key); + contentTypeKey = contentType.Key; + return true; + } private static void BuildNavigationDictionary(ConcurrentDictionary nodesStructure, IList roots, IEnumerable entities) { @@ -468,7 +616,7 @@ internal abstract class ContentNavigationServiceBase foreach (INavigationModel entity in entityList) { - var node = new NavigationNode(entity.Key, entity.SortOrder); + var node = new NavigationNode(entity.Key, entity.ContentTypeKey, entity.SortOrder); nodesStructure[entity.Key] = node; // We don't set the parent for items under root, it will stay null @@ -490,4 +638,7 @@ internal abstract class ContentNavigationServiceBase } } } + + private Dictionary LoadContentTypes() + => _typeService.GetAll().ToDictionary(ct => ct.Alias, ct => ct.Key); } diff --git a/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs index 44804d07c6..0f550be206 100644 --- a/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs @@ -1,12 +1,13 @@ +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; namespace Umbraco.Cms.Core.Services.Navigation; -internal sealed class DocumentNavigationService : ContentNavigationServiceBase, IDocumentNavigationQueryService, IDocumentNavigationManagementService +internal sealed class DocumentNavigationService : ContentNavigationServiceBase, IDocumentNavigationQueryService, IDocumentNavigationManagementService { - public DocumentNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) - : base(coreScopeProvider, navigationRepository) + public DocumentNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository, IContentTypeService contentTypeService) + : base(coreScopeProvider, navigationRepository, contentTypeService) { } diff --git a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs index 80dce527e1..cf3d172278 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs @@ -29,6 +29,7 @@ public interface INavigationManagementService /// provided, the new node is added at the root level. /// /// The unique identifier of the new node to add. + /// The unique identifier of the node's content type. /// /// The unique identifier of the parent node. If null, the new node will be added to /// the root level. @@ -46,7 +47,7 @@ public interface INavigationManagementService /// when adding nodes directly to the root (where parentKey is null), a sort order must be provided /// to ensure the item appears in the correct position among other root-level items. /// - bool Add(Guid key, Guid? parentKey = null, int? sortOrder = null); + bool Add(Guid key, Guid contentTypeKey, Guid? parentKey = null, int? sortOrder = null); /// /// Moves an existing node to a new parent in the main navigation structure. If a diff --git a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs index 984bf35efd..6ea28c8511 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs @@ -13,15 +13,19 @@ public interface INavigationQueryService bool TryGetRootKeys(out IEnumerable rootKeys); + bool TryGetRootKeysOfType(string contentTypeAlias, out IEnumerable rootKeys); + bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys); + bool TryGetChildrenKeysOfType(Guid parentKey, string contentTypeAlias, out IEnumerable childrenKeys); + bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys); - bool TryGetDescendantsKeysOrSelfKeys(Guid childKey, out IEnumerable descendantsOrSelfKeys) + bool TryGetDescendantsKeysOrSelfKeys(Guid parentKey, out IEnumerable descendantsOrSelfKeys) { - if (TryGetDescendantsKeys(childKey, out IEnumerable? descendantsKeys)) + if (TryGetDescendantsKeys(parentKey, out IEnumerable? descendantsKeys)) { - descendantsOrSelfKeys = childKey.Yield().Concat(descendantsKeys); + descendantsOrSelfKeys = parentKey.Yield().Concat(descendantsKeys); return true; } @@ -29,6 +33,8 @@ public interface INavigationQueryService return false; } + bool TryGetDescendantsKeysOfType(Guid parentKey, string contentTypeAlias, out IEnumerable descendantsKeys); + bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKeys); bool TryGetAncestorsOrSelfKeys(Guid childKey, out IEnumerable ancestorsOrSelfKeys) @@ -43,7 +49,11 @@ public interface INavigationQueryService return false; } + bool TryGetAncestorsKeysOfType(Guid parentKey, string contentTypeAlias, out IEnumerable ancestorsKeys); + bool TryGetSiblingsKeys(Guid key, out IEnumerable siblingsKeys); + bool TryGetSiblingsKeysOfType(Guid key, string contentTypeAlias, out IEnumerable siblingsKeys); + bool TryGetLevel(Guid contentKey, [NotNullWhen(true)] out int? level); } diff --git a/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs index 9741e86e1f..12578d5875 100644 --- a/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs +++ b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs @@ -16,7 +16,7 @@ public interface IRecycleBinNavigationQueryService bool TryGetDescendantsKeysOrSelfKeysInBin(Guid childKey, out IEnumerable descendantsOrSelfKeys) { - if(TryGetDescendantsKeysInBin(childKey, out var descendantsKeys)) + if (TryGetDescendantsKeysInBin(childKey, out IEnumerable? descendantsKeys)) { descendantsOrSelfKeys = childKey.Yield().Concat(descendantsKeys); return true; diff --git a/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs b/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs index 62ab5a1617..5a0c3c47d6 100644 --- a/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs @@ -1,12 +1,13 @@ +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; namespace Umbraco.Cms.Core.Services.Navigation; -internal sealed class MediaNavigationService : ContentNavigationServiceBase, IMediaNavigationQueryService, IMediaNavigationManagementService +internal sealed class MediaNavigationService : ContentNavigationServiceBase, IMediaNavigationQueryService, IMediaNavigationManagementService { - public MediaNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) - : base(coreScopeProvider, navigationRepository) + public MediaNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository, IMediaTypeService mediaTypeService) + : base(coreScopeProvider, navigationRepository, mediaTypeService) { } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs index 6b452b0c8a..eada4a97a4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs @@ -7,6 +7,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; [TableName(NodeDto.TableName)] internal class NavigationDto : INavigationModel { + // Public constants to bind properties between DTOs + public const string ContentTypeKeyColumnName = "contentTypeKey"; + /// [Column(NodeDto.IdColumnName)] public int Id { get; set; } @@ -15,6 +18,9 @@ internal class NavigationDto : INavigationModel [Column(NodeDto.KeyColumnName)] public Guid Key { get; set; } + [Column(ContentTypeKeyColumnName)] + public Guid ContentTypeKey { get; set; } + /// [Column(NodeDto.ParentIdColumnName)] public int ParentId { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs index 2f86d00143..b2553987d4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs @@ -27,10 +27,18 @@ public class ContentNavigationRepository : INavigationRepository private IEnumerable FetchNavigationDtos(Guid objectTypeKey, bool trashed) { Sql? sql = AmbientScope?.SqlContext.Sql() - .Select() - .From() - .Where(x => x.NodeObjectType == objectTypeKey && x.Trashed == trashed) - .OrderBy(x => x.Path); // make sure that we get the parent items first + .Select( + $"n.{NodeDto.IdColumnName} as {NodeDto.IdColumnName}", + $"n.{NodeDto.KeyColumnName} as {NodeDto.KeyColumnName}", + $"ctn.{NodeDto.KeyColumnName} as {NavigationDto.ContentTypeKeyColumnName}", + $"n.{NodeDto.ParentIdColumnName} as {NodeDto.ParentIdColumnName}", + $"n.{NodeDto.SortOrderColumnName} as {NodeDto.SortOrderColumnName}", + $"n.{NodeDto.TrashedColumnName} as {NodeDto.TrashedColumnName}") + .From("n") + .InnerJoin("c").On((n, c) => n.NodeId == c.NodeId, "n", "c") + .InnerJoin("ctn").On((c, ctn) => c.ContentTypeId == ctn.NodeId, "c", "ctn") + .Where(n => n.NodeObjectType == objectTypeKey && n.Trashed == trashed, "n") + .OrderBy(n => n.Path, "n"); // make sure that we get the parent items first return AmbientScope?.Database.Fetch(sql) ?? Enumerable.Empty(); } diff --git a/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs index 60c7a27a57..97bf4c8839 100644 --- a/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs @@ -271,6 +271,14 @@ public static class FriendlyPublishedContentExtensions where T : class, IPublishedContent => content.Parent(GetPublishedCache(content), GetNavigationQueryService(content)); + /// + /// Gets the parent of the content item. + /// + /// The content. + /// The parent of content or null. + public static IPublishedContent? Parent(this IPublishedContent content) + => content.Parent(GetPublishedCache(content), GetNavigationQueryService(content)); + /// /// Gets the ancestors of the content. /// diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs index 3e0d3e85f5..79f1e7a42a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs @@ -11,7 +11,7 @@ public partial class DocumentNavigationServiceTests // Arrange DocumentNavigationQueryService.TryGetSiblingsKeys(Root.Key, out IEnumerable initialSiblingsKeys); var initialRootNodeSiblingsCount = initialSiblingsKeys.Count(); - var createModel = CreateContentCreateModel("Root 2", Guid.NewGuid(), Constants.System.RootKey); + var createModel = CreateContentCreateModel("Root 2", Guid.NewGuid(), parentKey: Constants.System.RootKey); // Act var createAttempt = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); @@ -36,7 +36,7 @@ public partial class DocumentNavigationServiceTests // Arrange DocumentNavigationQueryService.TryGetChildrenKeys(Child1.Key, out IEnumerable initialChildrenKeys); var initialChild1ChildrenCount = initialChildrenKeys.Count(); - var createModel = CreateContentCreateModel("Grandchild 3", Guid.NewGuid(), Child1.Key); + var createModel = CreateContentCreateModel("Grandchild 3", Guid.NewGuid(), parentKey: Child1.Key); // Act var createAttempt = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); @@ -70,7 +70,7 @@ public partial class DocumentNavigationServiceTests { // Arrange Guid newNodeKey = Guid.NewGuid(); - var createModel = CreateContentCreateModel("Child", newNodeKey, parentKey); + var createModel = CreateContentCreateModel("Child", newNodeKey, parentKey: parentKey); // Act await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs index 32ba7934b1..e7fb137e19 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs @@ -63,7 +63,7 @@ public partial class DocumentNavigationServiceTests // Create a new sibling under the same parent var key = Guid.NewGuid(); - var createModel = CreateContentCreateModel("Child 4", key, Root.Key); + var createModel = CreateContentCreateModel("Child 4", key, parentKey: Root.Key); await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs index 0c8dc8c502..54abc92ce5 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs @@ -148,7 +148,7 @@ public partial class DocumentNavigationServiceTests // Create a new sibling under the same parent var key = Guid.NewGuid(); - var createModel = CreateContentCreateModel("Child 4", key, Root.Key); + var createModel = CreateContentCreateModel("Child 4", key, parentKey: Root.Key); await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs index f14c83d8fc..eeeaf39368 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs @@ -70,7 +70,7 @@ public partial class DocumentNavigationServiceTests // Create a new sibling under the same parent var key = Guid.NewGuid(); - var createModel = CreateContentCreateModel("Child 4", key, Root.Key); + var createModel = CreateContentCreateModel("Child 4", key, parentKey: Root.Key); await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation); @@ -93,7 +93,7 @@ public partial class DocumentNavigationServiceTests // Create a new grandchild under Child1 var key = Guid.NewGuid(); - var createModel = CreateContentCreateModel("Grandchild 3", key, nodeToMoveToRecycleBin); + var createModel = CreateContentCreateModel("Grandchild 3", key, parentKey: nodeToMoveToRecycleBin); await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); DocumentNavigationQueryService.TryGetChildrenKeys(nodeToMoveToRecycleBin, out IEnumerable childrenKeysBeforeDeletion); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs index addf668cb3..52f9bb77bd 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs @@ -2,6 +2,7 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; @@ -22,7 +23,10 @@ public partial class DocumentNavigationServiceTests DocumentNavigationQueryService.TryGetSiblingsKeys(nodeKey, out IEnumerable originalSiblingsKeys); // In-memory navigation structure is empty here - var newDocumentNavigationService = new DocumentNavigationService(GetRequiredService(), GetRequiredService()); + var newDocumentNavigationService = new DocumentNavigationService( + GetRequiredService(), + GetRequiredService(), + GetRequiredService()); var initialNodeExists = newDocumentNavigationService.TryGetParentKey(nodeKey, out _); // Act @@ -67,7 +71,10 @@ public partial class DocumentNavigationServiceTests DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeKey, out IEnumerable originalSiblingsKeys); // In-memory navigation structure is empty here - var newDocumentNavigationService = new DocumentNavigationService(GetRequiredService(), GetRequiredService()); + var newDocumentNavigationService = new DocumentNavigationService( + GetRequiredService(), + GetRequiredService(), + GetRequiredService()); var initialNodeExists = newDocumentNavigationService.TryGetParentKeyInBin(nodeKey, out _); // Act diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Sort.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Sort.cs index 7d1e113974..bf1768d0c0 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Sort.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Sort.cs @@ -87,7 +87,7 @@ public partial class DocumentNavigationServiceTests public async Task Structure_Updates_When_Sorting_Items_At_Root() { // Arrange - var anotherRootCreateModel = CreateContentCreateModel("Root 2", Guid.NewGuid(), Constants.System.RootKey); + var anotherRootCreateModel = CreateContentCreateModel("Root 2", Guid.NewGuid(), parentKey: Constants.System.RootKey); await ContentEditingService.CreateAsync(anotherRootCreateModel, Constants.Security.SuperUserKey); DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable initialRootKeys); @@ -175,9 +175,9 @@ public partial class DocumentNavigationServiceTests { // Arrange Guid node = Root.Key; - var anotherRootCreateModel1 = CreateContentCreateModel("Root 2", Guid.NewGuid(), Constants.System.RootKey); + var anotherRootCreateModel1 = CreateContentCreateModel("Root 2", Guid.NewGuid(), parentKey: Constants.System.RootKey); await ContentEditingService.CreateAsync(anotherRootCreateModel1, Constants.Security.SuperUserKey); - var anotherRootCreateModel2 = CreateContentCreateModel("Root 3", Guid.NewGuid(), Constants.System.RootKey); + var anotherRootCreateModel2 = CreateContentCreateModel("Root 3", Guid.NewGuid(), parentKey: Constants.System.RootKey); await ContentEditingService.CreateAsync(anotherRootCreateModel2, Constants.Security.SuperUserKey); DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable initialRootKeys); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs index 9fdedc5257..6d1db8132f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs @@ -25,7 +25,6 @@ public partial class DocumentNavigationServiceTests : DocumentNavigationServiceT ContentType = ContentTypeBuilder.CreateSimpleContentType("page", "Page"); ContentType.Key = new Guid("DD72B8A6-2CE3-47F0-887E-B695A1A5D086"); ContentType.AllowedAsRoot = true; - ContentType.AllowedTemplates = null; ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias) }; await ContentTypeService.CreateAsync(ContentType, Constants.Security.SuperUserKey); @@ -34,35 +33,35 @@ public partial class DocumentNavigationServiceTests : DocumentNavigationServiceT var rootCreateAttempt = await ContentEditingService.CreateAsync(rootModel, Constants.Security.SuperUserKey); Root = rootCreateAttempt.Result.Content!; - var child1Model = CreateContentCreateModel("Child 1", new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"), Root.Key); + var child1Model = CreateContentCreateModel("Child 1", new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"), parentKey: Root.Key); var child1CreateAttempt = await ContentEditingService.CreateAsync(child1Model, Constants.Security.SuperUserKey); Child1 = child1CreateAttempt.Result.Content!; - var grandchild1Model = CreateContentCreateModel("Grandchild 1", new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"), Child1.Key); + var grandchild1Model = CreateContentCreateModel("Grandchild 1", new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"), parentKey: Child1.Key); var grandchild1CreateAttempt = await ContentEditingService.CreateAsync(grandchild1Model, Constants.Security.SuperUserKey); Grandchild1 = grandchild1CreateAttempt.Result.Content!; - var grandchild2Model = CreateContentCreateModel("Grandchild 2", new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"), Child1.Key); + var grandchild2Model = CreateContentCreateModel("Grandchild 2", new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"), parentKey: Child1.Key); var grandchild2CreateAttempt = await ContentEditingService.CreateAsync(grandchild2Model, Constants.Security.SuperUserKey); Grandchild2 = grandchild2CreateAttempt.Result.Content!; - var child2Model = CreateContentCreateModel("Child 2", new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"), Root.Key); + var child2Model = CreateContentCreateModel("Child 2", new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"), parentKey: Root.Key); var child2CreateAttempt = await ContentEditingService.CreateAsync(child2Model, Constants.Security.SuperUserKey); Child2 = child2CreateAttempt.Result.Content!; - var grandchild3Model = CreateContentCreateModel("Grandchild 3", new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"), Child2.Key); + var grandchild3Model = CreateContentCreateModel("Grandchild 3", new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"), parentKey: Child2.Key); var grandchild3CreateAttempt = await ContentEditingService.CreateAsync(grandchild3Model, Constants.Security.SuperUserKey); Grandchild3 = grandchild3CreateAttempt.Result.Content!; - var greatGrandchild1Model = CreateContentCreateModel("Great-grandchild 1", new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"), Grandchild3.Key); + var greatGrandchild1Model = CreateContentCreateModel("Great-grandchild 1", new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"), parentKey: Grandchild3.Key); var greatGrandchild1CreateAttempt = await ContentEditingService.CreateAsync(greatGrandchild1Model, Constants.Security.SuperUserKey); GreatGrandchild1 = greatGrandchild1CreateAttempt.Result.Content!; - var child3Model = CreateContentCreateModel("Child 3", new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"), Root.Key); + var child3Model = CreateContentCreateModel("Child 3", new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"), parentKey: Root.Key); var child3CreateAttempt = await ContentEditingService.CreateAsync(child3Model, Constants.Security.SuperUserKey); Child3 = child3CreateAttempt.Result.Content!; - var grandchild4Model = CreateContentCreateModel("Grandchild 4", new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"), Child3.Key); + var grandchild4Model = CreateContentCreateModel("Grandchild 4", new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"), parentKey: Child3.Key); var grandchild4CreateAttempt = await ContentEditingService.CreateAsync(grandchild4Model, Constants.Security.SuperUserKey); Grandchild4 = grandchild4CreateAttempt.Result.Content!; } @@ -87,4 +86,199 @@ public partial class DocumentNavigationServiceTests : DocumentNavigationServiceT // Assert Assert.IsFalse(nodeExists); } + + [Test] + public async Task Can_Filter_Root_Items_By_Type() + { + // Arrange + DocumentNavigationQueryService.TryGetRootKeysOfType(ContentType.Alias, out IEnumerable initialRootKeysOfType); + List initialRootOfTypeList = initialRootKeysOfType.ToList(); + + // Doc Type + var anotherContentType = ContentTypeBuilder.CreateSimpleContentType("anotherPage", "Another page"); + anotherContentType.Key = new Guid("58A2958E-B34F-4289-A225-E99EEC2456AB"); + anotherContentType.AllowedContentTypes = new[] { new ContentTypeSort(anotherContentType.Key, 0, anotherContentType.Alias) }; + anotherContentType.AllowedAsRoot = true; + await ContentTypeService.CreateAsync(anotherContentType, Constants.Security.SuperUserKey); + + // Update old doc type + ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias), new ContentTypeSort(anotherContentType.Key, 1, anotherContentType.Alias) }; + await ContentTypeService.UpdateAsync(ContentType, Constants.Security.SuperUserKey); + + // Content + var root2Model = CreateContentCreateModel("Root 2", new Guid("11233548-2E87-4D3E-8FC4-4400F9DBEF56"), anotherContentType.Key); // Using new doc type + await ContentEditingService.CreateAsync(root2Model, Constants.Security.SuperUserKey); + var root3Model = CreateContentCreateModel("Root 3", new Guid("6E10F212-CE7F-47B5-A796-345861AEE613"), anotherContentType.Key); // Using new doc type + await ContentEditingService.CreateAsync(root3Model, Constants.Security.SuperUserKey); + + // Act + DocumentNavigationQueryService.TryGetRootKeysOfType(anotherContentType.Alias, out IEnumerable filteredRootKeysOfType); + List filteredRootList = filteredRootKeysOfType.ToList(); + + DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable allRootKeys); + List allRootList = allRootKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(1, initialRootOfTypeList.Count); // Verify that loaded doc types can be used to filter + Assert.AreEqual(2, filteredRootList.Count); // Verify that new doc type can be used to filter + Assert.AreEqual(3, allRootList.Count); + }); + } + + [Test] + public async Task Can_Filter_Children_By_Type() + { + // Arrange + Guid parentKey = Root.Key; + DocumentNavigationQueryService.TryGetChildrenKeysOfType(parentKey, ContentType.Alias, out IEnumerable initialChildrenKeysOfType); + List initialChildrenOfTypeList = initialChildrenKeysOfType.ToList(); + + // Doc Type + var anotherContentType = ContentTypeBuilder.CreateSimpleContentType("anotherPage", "Another page"); + anotherContentType.Key = new Guid("58A2958E-B34F-4289-A225-E99EEC2456AB"); + anotherContentType.AllowedContentTypes = new[] { new ContentTypeSort(anotherContentType.Key, 0, anotherContentType.Alias) }; + await ContentTypeService.CreateAsync(anotherContentType, Constants.Security.SuperUserKey); + + // Update old doc type + ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias), new ContentTypeSort(anotherContentType.Key, 1, anotherContentType.Alias) }; + await ContentTypeService.UpdateAsync(ContentType, Constants.Security.SuperUserKey); + + // Content + var child4Model = CreateContentCreateModel("Child 4", new Guid("11233548-2E87-4D3E-8FC4-4400F9DBEF56"), anotherContentType.Key, parentKey); // Using new doc type + await ContentEditingService.CreateAsync(child4Model, Constants.Security.SuperUserKey); + + // Act + DocumentNavigationQueryService.TryGetChildrenKeysOfType(parentKey, anotherContentType.Alias, out IEnumerable filteredChildrenKeysOfType); + List filteredChildrenList = filteredChildrenKeysOfType.ToList(); + + DocumentNavigationQueryService.TryGetChildrenKeys(parentKey, out IEnumerable allChildrenKeys); + List allChildrenList = allChildrenKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(3, initialChildrenOfTypeList.Count); // Verify that loaded doc types can be used to filter + Assert.AreEqual(1, filteredChildrenList.Count); // Verify that new doc type can be used to filter + Assert.AreEqual(4, allChildrenList.Count); + }); + } + + [Test] + public async Task Can_Filter_Descendants_By_Type() + { + // Arrange + Guid parentKey = Child2.Key; + DocumentNavigationQueryService.TryGetDescendantsKeysOfType(parentKey, ContentType.Alias, out IEnumerable initialDescendantsKeysOfType); + List initialDescendantsOfTypeList = initialDescendantsKeysOfType.ToList(); + + // Doc Type + var anotherContentType = ContentTypeBuilder.CreateSimpleContentType("anotherPage", "Another page"); + anotherContentType.Key = new Guid("58A2958E-B34F-4289-A225-E99EEC2456AB"); + anotherContentType.AllowedContentTypes = new[] { new ContentTypeSort(anotherContentType.Key, 0, anotherContentType.Alias) }; + await ContentTypeService.CreateAsync(anotherContentType, Constants.Security.SuperUserKey); + + // Update old doc type + ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias), new ContentTypeSort(anotherContentType.Key, 1, anotherContentType.Alias) }; + await ContentTypeService.UpdateAsync(ContentType, Constants.Security.SuperUserKey); + + // Content + var greatGreatGrandchild1Model = CreateContentCreateModel("Great-great-grandchild 1", new Guid("11233548-2E87-4D3E-8FC4-4400F9DBEF56"), anotherContentType.Key, GreatGrandchild1.Key); // Using new doc type + await ContentEditingService.CreateAsync(greatGreatGrandchild1Model, Constants.Security.SuperUserKey); + + // Act + DocumentNavigationQueryService.TryGetDescendantsKeysOfType(parentKey, anotherContentType.Alias, out IEnumerable filteredDescendantsKeysOfType); + List filteredDescendantsList = filteredDescendantsKeysOfType.ToList(); + + DocumentNavigationQueryService.TryGetDescendantsKeys(parentKey, out IEnumerable allDescendantsKeys); + List allDescendantsList = allDescendantsKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(2, initialDescendantsOfTypeList.Count); // Verify that loaded doc types can be used to filter + Assert.AreEqual(1, filteredDescendantsList.Count); // Verify that new doc type can be used to filter + Assert.AreEqual(3, allDescendantsList.Count); + }); + } + + [Test] + public async Task Can_Filter_Ancestors_By_Type() + { + // Arrange + Guid childKey = new Guid("1802D6B4-4A3C-4EBA-AFA3-1AF82C2D6483"); + + // Doc Type + var anotherContentType = ContentTypeBuilder.CreateSimpleContentType("anotherPage", "Another page"); + anotherContentType.Key = new Guid("58A2958E-B34F-4289-A225-E99EEC2456AB"); + anotherContentType.AllowedContentTypes = new[] { new ContentTypeSort(anotherContentType.Key, 0, anotherContentType.Alias) }; + await ContentTypeService.CreateAsync(anotherContentType, Constants.Security.SuperUserKey); + + // Update old doc type + ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias), new ContentTypeSort(anotherContentType.Key, 1, anotherContentType.Alias) }; + await ContentTypeService.UpdateAsync(ContentType, Constants.Security.SuperUserKey); + + // Content + var greatGrandchild2Model = CreateContentCreateModel("Great-grandchild 2", new Guid("11233548-2E87-4D3E-8FC4-4400F9DBEF56"), anotherContentType.Key, Grandchild4.Key); // Using new doc type + await ContentEditingService.CreateAsync(greatGrandchild2Model, Constants.Security.SuperUserKey); + var greatGreatGrandchild1Model = CreateContentCreateModel("Great-great-grandchild 1", childKey, anotherContentType.Key, greatGrandchild2Model.Key); // Using new doc type + await ContentEditingService.CreateAsync(greatGreatGrandchild1Model, Constants.Security.SuperUserKey); + + // Act + DocumentNavigationQueryService.TryGetAncestorsKeysOfType(childKey, ContentType.Alias, out IEnumerable ancestorsKeysOfOriginalType); + List ancestorsKeysOfOriginalTypeList = ancestorsKeysOfOriginalType.ToList(); + + DocumentNavigationQueryService.TryGetAncestorsKeysOfType(childKey, anotherContentType.Alias, out IEnumerable ancestorsKeysOfNewType); + List ancestorsKeysOfNewTypeList = ancestorsKeysOfNewType.ToList(); + + DocumentNavigationQueryService.TryGetAncestorsKeys(childKey, out IEnumerable allAncestorsKeys); + List allAncestorsList = allAncestorsKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(3, ancestorsKeysOfOriginalTypeList.Count); // Verify that loaded doc types can be used to filter + Assert.AreEqual(1, ancestorsKeysOfNewTypeList.Count); // Verify that new doc type can be used to filter + Assert.AreEqual(4, allAncestorsList.Count); + }); + } + + [Test] + public async Task Can_Filter_Siblings_By_Type() + { + // Arrange + Guid parentKey = Child3.Key; + DocumentNavigationQueryService.TryGetSiblingsKeysOfType(parentKey, ContentType.Alias, out IEnumerable initialSiblingsKeysOfType); + List initialSiblingsOfTypeList = initialSiblingsKeysOfType.ToList(); + + // Doc Type + var anotherContentType = ContentTypeBuilder.CreateSimpleContentType("anotherPage", "Another page"); + anotherContentType.Key = new Guid("58A2958E-B34F-4289-A225-E99EEC2456AB"); + anotherContentType.AllowedContentTypes = new[] { new ContentTypeSort(anotherContentType.Key, 0, anotherContentType.Alias) }; + await ContentTypeService.CreateAsync(anotherContentType, Constants.Security.SuperUserKey); + + // Update old doc type + ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias), new ContentTypeSort(anotherContentType.Key, 1, anotherContentType.Alias) }; + await ContentTypeService.UpdateAsync(ContentType, Constants.Security.SuperUserKey); + + // Content + var child4Model = CreateContentCreateModel("Child 4", new Guid("11233548-2E87-4D3E-8FC4-4400F9DBEF56"), anotherContentType.Key, Root.Key); // Using new doc type + await ContentEditingService.CreateAsync(child4Model, Constants.Security.SuperUserKey); + + // Act + DocumentNavigationQueryService.TryGetSiblingsKeysOfType(parentKey, anotherContentType.Alias, out IEnumerable filteredSiblingsKeysOfType); + List filteredSiblingsList = filteredSiblingsKeysOfType.ToList(); + + DocumentNavigationQueryService.TryGetSiblingsKeys(parentKey, out IEnumerable allSiblingsKeys); + List allSiblingsList = allSiblingsKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(2, initialSiblingsOfTypeList.Count); // Verify that loaded doc types can be used to filter + Assert.AreEqual(1, filteredSiblingsList.Count); // Verify that new doc type can be used to filter + Assert.AreEqual(3, allSiblingsList.Count); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs index 375ddb8391..4e7ea473de 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; @@ -45,10 +44,10 @@ public abstract class DocumentNavigationServiceTestsBase : UmbracoIntegrationTes protected IContent Grandchild4 { get; set; } - protected ContentCreateModel CreateContentCreateModel(string name, Guid key, Guid? parentKey = null) + protected ContentCreateModel CreateContentCreateModel(string name, Guid key, Guid? contentTypeKey = null, Guid? parentKey = null) => new() { - ContentTypeKey = ContentType.Key, + ContentTypeKey = contentTypeKey ?? ContentType.Key, ParentKey = parentKey ?? Constants.System.RootKey, InvariantName = name, Key = key, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs index 65244743e3..c5e1487a7e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs @@ -2,6 +2,7 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; @@ -22,7 +23,10 @@ public partial class MediaNavigationServiceTests MediaNavigationQueryService.TryGetSiblingsKeys(nodeKey, out IEnumerable originalSiblingsKeys); // In-memory navigation structure is empty here - var newMediaNavigationService = new MediaNavigationService(GetRequiredService(), GetRequiredService()); + var newMediaNavigationService = new MediaNavigationService( + GetRequiredService(), + GetRequiredService(), + GetRequiredService()); var initialNodeExists = newMediaNavigationService.TryGetParentKey(nodeKey, out _); // Act @@ -67,7 +71,10 @@ public partial class MediaNavigationServiceTests MediaNavigationQueryService.TryGetSiblingsKeysInBin(nodeKey, out IEnumerable originalSiblingsKeys); // In-memory navigation structure is empty here - var newMediaNavigationService = new MediaNavigationService(GetRequiredService(), GetRequiredService()); + var newMediaNavigationService = new MediaNavigationService( + GetRequiredService(), + GetRequiredService(), + GetRequiredService()); var initialNodeExists = newMediaNavigationService.TryGetParentKeyInBin(nodeKey, out _); // Act diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs index ebef7fe046..6a4761fa81 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs @@ -76,4 +76,27 @@ public partial class MediaNavigationServiceTests : MediaNavigationServiceTestsBa // Assert Assert.IsFalse(nodeExists); } + + [Test] + public void Can_Filter_Children_By_Type() + { + // Arrange + MediaNavigationQueryService.TryGetChildrenKeys(Album.Key, out IEnumerable allChildrenKeys); + List allChildrenList = allChildrenKeys.ToList(); + + // Act + MediaNavigationQueryService.TryGetChildrenKeysOfType(Album.Key, ImageMediaType.Alias, out IEnumerable childrenKeysOfTypeImage); + List imageChildrenList = childrenKeysOfTypeImage.ToList(); + + MediaNavigationQueryService.TryGetChildrenKeysOfType(Album.Key, FolderMediaType.Alias, out IEnumerable childrenKeysOfTypeFolder); + List folderChildrenList = childrenKeysOfTypeFolder.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(1, imageChildrenList.Count); + Assert.AreEqual(2, folderChildrenList.Count); + Assert.AreEqual(3, allChildrenList.Count); + }); + } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs index eca04e10f2..06950c4ba9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs @@ -46,6 +46,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests var childKey = Guid.NewGuid(); var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + var contentCache = CreatePublishedContentCache("#"); Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); @@ -78,6 +81,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); Mock.Get(contentCache).Setup(x => x.GetById(grandchild.Key)).Returns(grandchild); + IEnumerable ancestorsKeys = [childKey, rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(grandchildKey, out ancestorsKeys)).Returns(true); + var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(grandchild); Assert.IsNotNull(result); @@ -101,6 +107,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(child, "en-us"); Assert.IsNotNull(result); @@ -130,6 +139,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(child, "en-us"); Assert.IsNotNull(result); @@ -159,6 +171,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(child, "en-us"); Assert.IsNotNull(result); @@ -225,6 +240,12 @@ public class ContentRouteBuilderTests : DeliveryApiTests Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); Mock.Get(contentCache).Setup(x => x.GetById(grandchild.Key)).Returns(grandchild); + IEnumerable grandchildAncestorsKeys = [childKey, rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(grandchildKey, out grandchildAncestorsKeys)).Returns(true); + + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + // yes... actually testing the mock setup here. but it's important for the rest of the tests that this behave correct, so we better test it. var publishedUrlProvider = SetupPublishedUrlProvider(hideTopLevelNodeFromPath, contentCache, navigationQueryServiceMock.Object); Assert.AreEqual(hideTopLevelNodeFromPath ? "/" : "/the-root", publishedUrlProvider.GetUrl(root)); @@ -306,6 +327,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + var builder = CreateApiContentRouteBuilder(true, contentCache: contentCache, isPreview: isPreview, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(child); @@ -342,6 +366,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests .Setup(p => p.GetContentPath(It.IsAny(), It.IsAny())) .Returns((IPublishedContent content, string? culture) => $"my-custom-path-for-{content.UrlSegment}"); + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + var builder = CreateApiContentRouteBuilder(true, contentCache: contentCache, apiContentPathProvider: apiContentPathProvider.Object, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(root); Assert.NotNull(result); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs index 8c221dc54b..1dd81b151c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs @@ -1,7 +1,9 @@ using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services; @@ -11,6 +13,8 @@ public class ContentNavigationServiceBaseTests { private TestContentNavigationService _navigationService; + private Guid ContentType { get; set; } + private Guid Root { get; set; } private Guid Child1 { get; set; } @@ -32,46 +36,21 @@ public class ContentNavigationServiceBaseTests [SetUp] public void Setup() { - // Root - // - Child 1 - // - Grandchild 1 - // - Grandchild 2 - // - Child 2 - // - Grandchild 3 - // - Great-grandchild 1 - // - Child 3 - // - Grandchild 4 - _navigationService = new TestContentNavigationService( Mock.Of(), - Mock.Of()); + Mock.Of(), + Mock.Of()); - Root = new Guid("E48DD82A-7059-418E-9B82-CDD5205796CF"); - _navigationService.Add(Root); - - Child1 = new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"); - _navigationService.Add(Child1, Root); - - Grandchild1 = new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"); - _navigationService.Add(Grandchild1, Child1); - - Grandchild2 = new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"); - _navigationService.Add(Grandchild2, Child1); - - Child2 = new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"); - _navigationService.Add(Child2, Root); - - Grandchild3 = new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"); - _navigationService.Add(Grandchild3, Child2); - - GreatGrandchild1 = new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"); - _navigationService.Add(GreatGrandchild1, Grandchild3); - - Child3 = new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"); - _navigationService.Add(Child3, Root); - - Grandchild4 = new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"); - _navigationService.Add(Grandchild4, Child3); + // Root - E48DD82A-7059-418E-9B82-CDD5205796CF + // - Child 1 - C6173927-0C59-4778-825D-D7B9F45D8DDE + // - Grandchild 1 - E856AC03-C23E-4F63-9AA9-681B42A58573 + // - Grandchild 2 - A1B1B217-B02F-4307-862C-A5E22DB729EB + // - Child 2 - 60E0E5C4-084E-4144-A560-7393BEAD2E96 + // - Grandchild 3 - D63C1621-C74A-4106-8587-817DEE5FB732 + // - Great-grandchild 1 - 56E29EA9-E224-4210-A59F-7C2C5C0C5CC7 + // - Child 3 - B606E3FF-E070-4D46-8CB9-D31352029FDF + // - Grandchild 4 - F381906C-223C-4466-80F7-B63B4EE073F8 + CreateTestData(); } [Test] @@ -129,7 +108,8 @@ public class ContentNavigationServiceBaseTests // Arrange var emptyNavigationService = new TestContentNavigationService( Mock.Of(), - Mock.Of()); + Mock.Of(), + Mock.Of()); // Act emptyNavigationService.TryGetRootKeys(out IEnumerable rootKeys); @@ -161,7 +141,7 @@ public class ContentNavigationServiceBaseTests { // Arrange Guid anotherRoot = Guid.NewGuid(); - _navigationService.Add(anotherRoot); + _navigationService.Add(anotherRoot, ContentType); // Act var result = _navigationService.TryGetRootKeys(out IEnumerable rootKeys); @@ -176,6 +156,179 @@ public class ContentNavigationServiceBaseTests }); } + [Test] + public void Cannot_Get_Root_Items_Of_Type_From_Non_Existing_Content_Type_Alias() + { + // Arrange + var nonExistingContentTypeAlias = string.Empty; + + // Act + var result = _navigationService.TryGetRootKeysOfType(nonExistingContentTypeAlias, out IEnumerable rootKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(rootKeys); + }); + } + + [Test] + public void Can_Get_Root_Items_Of_Type() + { + // Arrange + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + Guid anotherRoot = Guid.NewGuid(); + _navigationService.Add(anotherRoot, ContentType); + + // Act + var result = _navigationService.TryGetRootKeysOfType(contentTypeAlias, out IEnumerable rootKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(2, rootKeys.Count()); + }); + } + + [Test] + public void Can_Get_Root_Items_Of_Type_Filters_Result() + { + // Arrange + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new root items with different content type + _navigationService.Add(Guid.NewGuid(), anotherContentTypeKey); + _navigationService.Add(Guid.NewGuid(), anotherContentTypeKey); + + // Act + _navigationService.TryGetRootKeysOfType(anotherContentTypeAlias, out IEnumerable rootKeysOfType); + var rootsOfTypeCount = rootKeysOfType.Count(); + + // Assert + // Retrieve all root items without filtering to compare + _navigationService.TryGetRootKeys(out IEnumerable allRootKeys); + var allRootsCount = allRootKeys.Count(); + + Assert.Multiple(() => + { + Assert.IsTrue(allRootsCount > rootsOfTypeCount); + Assert.AreEqual(3, allRootsCount); + Assert.AreEqual(2, rootsOfTypeCount); + }); + } + + [Test] + public void Can_Get_Root_Items_Of_Type_Filters_Result_And_Maintains_Their_Order_Of_Creation() + { + // Arrange + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new root items with different content type + Guid root2 = Guid.NewGuid(); + Guid root3 = Guid.NewGuid(); + _navigationService.Add(root2, anotherContentTypeKey); + _navigationService.Add(root3, anotherContentTypeKey); + + var expectedRootsOrder = new List { root2, root3 }; + + // Act + _navigationService.TryGetRootKeysOfType(anotherContentTypeAlias, out IEnumerable rootKeysOfType); + + // Assert + // Check that the order matches what is expected + Assert.IsTrue(expectedRootsOrder.SequenceEqual(rootKeysOfType)); + } + + [Test] + public void Can_Get_Root_Items_Of_Type_Even_When_Content_Type_Was_Not_Initially_Loaded() + { + // Arrange + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x + .Get(It.Is(alias => alias == contentTypeAlias))) + .Returns(contentTypeMock.Object); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Act + _navigationService.TryGetRootKeysOfType(contentTypeAlias, out IEnumerable rootKeys); + + // Assert + Assert.AreEqual(1, rootKeys.Count()); + } + [Test] public void Cannot_Get_Children_From_Non_Existing_Content_Key() { @@ -220,8 +373,9 @@ public class ContentNavigationServiceBaseTests [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new[] { - "C6173927-0C59-4778-825D-D7B9F45D8DDE", "60E0E5C4-084E-4144-A560-7393BEAD2E96", - "B606E3FF-E070-4D46-8CB9-D31352029FDF" + "C6173927-0C59-4778-825D-D7B9F45D8DDE", + "60E0E5C4-084E-4144-A560-7393BEAD2E96", + "B606E3FF-E070-4D46-8CB9-D31352029FDF", })] // Root [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", new[] { "E856AC03-C23E-4F63-9AA9-681B42A58573", "A1B1B217-B02F-4307-862C-A5E22DB729EB" })] // Child 1 @@ -246,6 +400,219 @@ public class ContentNavigationServiceBaseTests } } + [Test] + public void Cannot_Get_Children_Of_Type_From_Non_Existing_Content_Type_Alias() + { + // Arrange + Guid parentKey = Root; + var nonExistingContentTypeAlias = string.Empty; + + // Act + var result = _navigationService.TryGetChildrenKeysOfType(parentKey, nonExistingContentTypeAlias, out IEnumerable childrenKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(childrenKeys); + }); + } + + [Test] + public void Cannot_Get_Children_Of_Type_From_Non_Existing_Content_Key() + { + // Arrange + var nonExistingKey = Guid.NewGuid(); + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // Act + var result = _navigationService.TryGetChildrenKeysOfType(nonExistingKey, contentTypeAlias, out IEnumerable childrenKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(childrenKeys); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", 3)] // Root - Child 1, Child 2, Child 3 + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", 2)] // Child 1 - Grandchild 1, Grandchild 2 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", 0)] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", 0)] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", 1)] // Child 2 - Grandchild 3 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", 1)] // Grandchild 3 - Great-grandchild 1 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", 0)] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Child 3 - Grandchild 4 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", 0)] // Grandchild 4 + public void Can_Get_Children_Of_Type(Guid parentKey, int childrenCount) + { + // Arrange + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Act + var result = _navigationService.TryGetChildrenKeysOfType(parentKey, contentTypeAlias, out IEnumerable childrenKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(childrenCount, childrenKeys.Count()); + }); + } + + [Test] + public void Can_Get_Children_Of_Type_Filters_Result() + { + // Arrange + Guid parentKey = Root; + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new children with different content type under Root + _navigationService.Add(Guid.NewGuid(), anotherContentTypeKey, Root); + _navigationService.Add(Guid.NewGuid(), anotherContentTypeKey, Root); + + // Act + _navigationService.TryGetChildrenKeysOfType(parentKey, anotherContentTypeAlias, out IEnumerable childrenKeysOfType); + var childrenOfTypeCount = childrenKeysOfType.Count(); + + // Assert + // Retrieve all children without filtering to compare + _navigationService.TryGetChildrenKeys(parentKey, out IEnumerable allChildrenKeys); + var allChildrenCount = allChildrenKeys.Count(); + + Assert.Multiple(() => + { + Assert.IsTrue(allChildrenCount > childrenOfTypeCount); + Assert.AreEqual(5, allChildrenCount); + Assert.AreEqual(2, childrenOfTypeCount); + }); + } + + [Test] + public void Can_Get_Children_Of_Type_Filters_Result_And_Maintains_Their_Order_Of_Creation() + { + // Arrange + Guid parentKey = Root; + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new children with different content type under Root + Guid child4 = Guid.NewGuid(); + Guid child5 = Guid.NewGuid(); + _navigationService.Add(child4, anotherContentTypeKey, Root); + _navigationService.Add(child5, anotherContentTypeKey, Root); + + var expectedChildrenOrder = new List { child4, child5 }; + + // Act + _navigationService.TryGetChildrenKeysOfType(parentKey, anotherContentTypeAlias, out IEnumerable childrenKeysOfType); + + // Assert + // Check that the order matches what is expected + Assert.IsTrue(expectedChildrenOrder.SequenceEqual(childrenKeysOfType)); + } + + [Test] + public void Can_Get_Children_Of_Type_Even_When_Content_Type_Was_Not_Initially_Loaded() + { + // Arrange + Guid parentKey = Child1; + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x + .Get(It.Is(alias => alias == contentTypeAlias))) + .Returns(contentTypeMock.Object); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Act + _navigationService.TryGetChildrenKeysOfType(parentKey, contentTypeAlias, out IEnumerable childrenKeys); + + // Assert + Assert.AreEqual(2, childrenKeys.Count()); + } + [Test] public void Cannot_Get_Descendants_From_Non_Existing_Content_Key() { @@ -294,7 +661,7 @@ public class ContentNavigationServiceBaseTests "C6173927-0C59-4778-825D-D7B9F45D8DDE", "E856AC03-C23E-4F63-9AA9-681B42A58573", "A1B1B217-B02F-4307-862C-A5E22DB729EB", "60E0E5C4-084E-4144-A560-7393BEAD2E96", "D63C1621-C74A-4106-8587-817DEE5FB732", "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", - "B606E3FF-E070-4D46-8CB9-D31352029FDF", "F381906C-223C-4466-80F7-B63B4EE073F8" + "B606E3FF-E070-4D46-8CB9-D31352029FDF", "F381906C-223C-4466-80F7-B63B4EE073F8", })] // Root [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", new[] { "E856AC03-C23E-4F63-9AA9-681B42A58573", "A1B1B217-B02F-4307-862C-A5E22DB729EB" })] // Child 1 @@ -320,6 +687,189 @@ public class ContentNavigationServiceBaseTests } } + [Test] + public void Cannot_Get_Descendants_Of_Type_From_Non_Existing_Content_Type_Alias() + { + // Arrange + Guid parentKey = Root; + var nonExistingContentTypeAlias = string.Empty; + + // Act + var result = _navigationService.TryGetDescendantsKeysOfType(parentKey, nonExistingContentTypeAlias, out IEnumerable descendantsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(descendantsKeys); + }); + } + + [Test] + public void Cannot_Get_Descendants_Of_Type_From_Non_Existing_Content_Key() + { + // Arrange + var nonExistingKey = Guid.NewGuid(); + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // Act + var result = _navigationService.TryGetDescendantsKeysOfType(nonExistingKey, contentTypeAlias, out IEnumerable descendantsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(descendantsKeys); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", + 8)] // Root - Child 1, Grandchild 1, Grandchild 2, Child 2, Grandchild 3, Great-grandchild 1, Child 3, Grandchild 4 + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", 2)] // Child 1 - Grandchild 1, Grandchild 2 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", 0)] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", 0)] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", 2)] // Child 2 - Grandchild 3, Great-grandchild 1 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", 1)] // Grandchild 3 - Great-grandchild 1 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", 0)] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Child 3 - Grandchild 4 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", 0)] // Grandchild 4 + public void Can_Get_Descendants_Of_Type(Guid parentKey, int descendantsCount) + { + // Arrange + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Act + var result = _navigationService.TryGetDescendantsKeysOfType(parentKey, contentTypeAlias, out IEnumerable descendantsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(descendantsCount, descendantsKeys.Count()); + }); + } + + [Test] + public void Can_Get_Descendants_Of_Type_Filters_Result() + { + // Arrange + Guid parentKey = Child2; + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new descendants with different content type under Child2 + _navigationService.Add(Guid.NewGuid(), anotherContentTypeKey, Grandchild3); + _navigationService.Add(Guid.NewGuid(), anotherContentTypeKey, GreatGrandchild1); + + // Act + _navigationService.TryGetDescendantsKeysOfType(parentKey, anotherContentTypeAlias, out IEnumerable descendantsKeysOfType); + var descendantsOfTypeCount = descendantsKeysOfType.Count(); + + // Assert + // Retrieve descendants without filtering to compare + _navigationService.TryGetDescendantsKeys(parentKey, out IEnumerable allDescendantsKeys); + var allDescendantsCount = allDescendantsKeys.Count(); + + Assert.Multiple(() => + { + Assert.IsTrue(allDescendantsCount > descendantsOfTypeCount); + Assert.AreEqual(4, allDescendantsCount); + Assert.AreEqual(2, descendantsOfTypeCount); + }); + } + + [Test] + public void Can_Get_Descendants_Of_Type_Filters_Result_And_Maintains_Their_Order_Of_Creation() + { + // Arrange + Guid parentKey = Child2; + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new descendants with different content type under Child2 + Guid greatGreatGrandchild2 = Guid.NewGuid(); + Guid greatGreatGrandchild3 = Guid.NewGuid(); + _navigationService.Add(greatGreatGrandchild2, anotherContentTypeKey, Grandchild3); + _navigationService.Add(greatGreatGrandchild3, anotherContentTypeKey, Grandchild3); + + var expectedDescendantsOrder = new List { greatGreatGrandchild2, greatGreatGrandchild3 }; + + // Act + _navigationService.TryGetDescendantsKeysOfType(parentKey, anotherContentTypeAlias, out IEnumerable descendantsOfType); + + // Assert + // Check that the order matches what is expected + Assert.IsTrue(expectedDescendantsOrder.SequenceEqual(descendantsOfType)); + } + [Test] public void Cannot_Get_Ancestors_From_Non_Existing_Content_Key() { @@ -368,8 +918,9 @@ public class ContentNavigationServiceBaseTests [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new[] { - "D63C1621-C74A-4106-8587-817DEE5FB732", "60E0E5C4-084E-4144-A560-7393BEAD2E96", - "E48DD82A-7059-418E-9B82-CDD5205796CF" + "D63C1621-C74A-4106-8587-817DEE5FB732", + "60E0E5C4-084E-4144-A560-7393BEAD2E96", + "E48DD82A-7059-418E-9B82-CDD5205796CF", })] // Great-grandchild 1 public void Can_Get_Ancestors_From_Existing_Content_Key_In_Their_Order_Of_Creation(Guid childKey, string[] ancestors) { @@ -387,6 +938,145 @@ public class ContentNavigationServiceBaseTests } } + [Test] + public void Cannot_Get_Ancestors_Of_Type_From_Non_Existing_Content_Type_Alias() + { + // Arrange + Guid childKey = GreatGrandchild1; + var nonExistingContentTypeAlias = string.Empty; + + // Act + var result = _navigationService.TryGetAncestorsKeysOfType(childKey, nonExistingContentTypeAlias, out IEnumerable ancestorsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(ancestorsKeys); + }); + } + + [Test] + public void Cannot_Get_Ancestors_Of_Type_From_Non_Existing_Content_Key() + { + // Arrange + var nonExistingKey = Guid.NewGuid(); + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // Act + var result = _navigationService.TryGetAncestorsKeysOfType(nonExistingKey, contentTypeAlias, out IEnumerable ancestorsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(ancestorsKeys); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", 0)] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", 1)] // Child 1 - Root + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", 2)] // Grandchild 1 - Child 1, Root + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", 2)] // Grandchild 2 - Child 1, Root + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", 1)] // Child 2 - Root + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", 2)] // Grandchild 3 - Child 2, Root + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", 3)] // Great-grandchild 1 - Grandchild 3, Child 2, Root + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Child 3 - Root + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", 2)] // Grandchild 4 - Child 3, Root + public void Can_Get_Ancestors_Of_Type(Guid childKey, int ancestorsCount) + { + // Arrange + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Act + var result = _navigationService.TryGetAncestorsKeysOfType(childKey, contentTypeAlias, out IEnumerable ancestorsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(ancestorsCount, ancestorsKeys.Count()); + }); + } + + [Test] + public void Can_Get_Ancestors_Of_Type_Filters_Result() + { + // Arrange + Guid childKey = Guid.NewGuid(); + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new items with different content type under Grandchild 1 + var greatGrandchild2 = Guid.NewGuid(); + _navigationService.Add(greatGrandchild2, anotherContentTypeKey, Grandchild1); + _navigationService.Add(childKey, anotherContentTypeKey, greatGrandchild2); + + // Act + _navigationService.TryGetAncestorsKeysOfType(childKey, anotherContentTypeAlias, out IEnumerable ancestorsKeysOfType); + var ancestorsOfTypeCount = ancestorsKeysOfType.Count(); + + // Assert + // Retrieve all ancestors without filtering to compare + _navigationService.TryGetAncestorsKeys(childKey, out IEnumerable allAncestorsKeys); + var allAncestorsCount = allAncestorsKeys.Count(); + + Assert.Multiple(() => + { + Assert.IsTrue(allAncestorsCount > ancestorsOfTypeCount); + Assert.AreEqual(4, allAncestorsCount); + Assert.AreEqual(1, ancestorsOfTypeCount); + }); + } + [Test] public void Cannot_Get_Siblings_Of_Non_Existing_Content_Key() { @@ -428,7 +1118,7 @@ public class ContentNavigationServiceBaseTests { // Arrange Guid anotherRoot = Guid.NewGuid(); - _navigationService.Add(anotherRoot); + _navigationService.Add(anotherRoot, ContentType); // Act _navigationService.TryGetSiblingsKeys(anotherRoot, out IEnumerable siblingsKeys); @@ -486,6 +1176,188 @@ public class ContentNavigationServiceBaseTests } } + [Test] + public void Cannot_Get_Siblings_Of_Type_From_Non_Existing_Content_Type_Alias() + { + // Arrange + Guid nodeKey = Child1; + var nonExistingContentTypeAlias = string.Empty; + + // Act + var result = _navigationService.TryGetSiblingsKeysOfType(nodeKey, nonExistingContentTypeAlias, out IEnumerable siblingsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(siblingsKeys); + }); + } + + [Test] + public void Cannot_Get_Siblings_Of_Type_From_Non_Existing_Content_Key() + { + // Arrange + var nonExistingKey = Guid.NewGuid(); + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // Act + var result = _navigationService.TryGetSiblingsKeysOfType(nonExistingKey, contentTypeAlias, out IEnumerable siblingsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(siblingsKeys); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", 0)] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", 2)] // Child 1 - Child 2, Child 3 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", 1)] // Grandchild 1 - Grandchild 2 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", 1)] // Grandchild 2 - Grandchild 1 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", 2)] // Child 2 - Child 1, Child 3 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", 0)] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", 0)] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", 2)] // Child 3 - Child 1, Child 2 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", 0)] // Grandchild 4 + public void Can_Get_Siblings_Of_Type(Guid key, int siblingsCount) + { + // Arrange + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Act + var result = _navigationService.TryGetSiblingsKeysOfType(key, contentTypeAlias, out IEnumerable siblingsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(siblingsCount, siblingsKeys.Count()); + }); + } + + [Test] + public void Can_Get_Siblings_Of_Type_Filters_Result() + { + // Arrange + Guid nodeKey = Child1; + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new children with different content type under Root + _navigationService.Add(Guid.NewGuid(), anotherContentTypeKey, Root); + _navigationService.Add(Guid.NewGuid(), anotherContentTypeKey, Root); + + // Act + _navigationService.TryGetSiblingsKeysOfType(nodeKey, anotherContentTypeAlias, out IEnumerable siblingsKeysOfType); + var siblingsOfTypeCount = siblingsKeysOfType.Count(); + + // Assert + // Retrieve all siblings without filtering to compare + _navigationService.TryGetSiblingsKeys(nodeKey, out IEnumerable allSiblingsKeys); + var allSiblingsCount = allSiblingsKeys.Count(); + + Assert.Multiple(() => + { + Assert.IsTrue(allSiblingsCount > siblingsOfTypeCount); + Assert.AreEqual(4, allSiblingsCount); + Assert.AreEqual(2, siblingsOfTypeCount); + }); + } + + [Test] + public void Can_Get_Siblings_Of_Type_Filters_Result_And_Maintains_Their_Order_Of_Creation() + { + // Arrange + Guid nodeKey = Child1; + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new children with different content type under Root + Guid child4 = Guid.NewGuid(); + Guid child5 = Guid.NewGuid(); + _navigationService.Add(child4, anotherContentTypeKey, Root); + _navigationService.Add(child5, anotherContentTypeKey, Root); + + var expectedSiblingsOrder = new List { child4, child5 }; + + // Act + _navigationService.TryGetSiblingsKeysOfType(nodeKey, anotherContentTypeAlias, out IEnumerable siblingsKeysOfType); + + // Assert + // Check that the order matches what is expected + Assert.IsTrue(expectedSiblingsOrder.SequenceEqual(siblingsKeysOfType)); + } + [Test] public void Cannot_Get_Level_From_Non_Existing_Content_Key() { @@ -683,7 +1555,7 @@ public class ContentNavigationServiceBaseTests var nonExistentParentKey = Guid.NewGuid(); // Act - var result = _navigationService.Add(newNodeKey, nonExistentParentKey); + var result = _navigationService.Add(newNodeKey, ContentType, nonExistentParentKey); // Assert Assert.IsFalse(result); @@ -693,7 +1565,7 @@ public class ContentNavigationServiceBaseTests public void Cannot_Add_When_Node_With_The_Same_Key_Already_Exists() { // Act - var result = _navigationService.Add(Child1); + var result = _navigationService.Add(Child1, ContentType); // Assert Assert.IsFalse(result); @@ -706,7 +1578,7 @@ public class ContentNavigationServiceBaseTests var newNodeKey = Guid.NewGuid(); // Act - var result = _navigationService.Add(newNodeKey); // parentKey is null + var result = _navigationService.Add(newNodeKey, ContentType); // parentKey is null // Assert Assert.IsTrue(result); @@ -732,7 +1604,7 @@ public class ContentNavigationServiceBaseTests var currentChildrenCount = currentChildrenKeys.Count(); // Act - var result = _navigationService.Add(newNodeKey, parentKey); + var result = _navigationService.Add(newNodeKey, ContentType, parentKey); // Assert Assert.IsTrue(result); @@ -763,7 +1635,7 @@ public class ContentNavigationServiceBaseTests var newNodeKey = Guid.NewGuid(); // Act - _navigationService.Add(newNodeKey, parentKey); + _navigationService.Add(newNodeKey, ContentType, parentKey); // Assert _navigationService.TryGetChildrenKeys(parentKey, out IEnumerable childrenKeys); @@ -1143,12 +2015,47 @@ public class ContentNavigationServiceBaseTests Assert.AreEqual(initialDescendantsCount, descendantsCountAfterRestore); } + + private void CreateTestData() + { + ContentType = new Guid("217C492D-0067-478C-BEA8-D0CE2DECBEB9"); + + Root = new Guid("E48DD82A-7059-418E-9B82-CDD5205796CF"); + _navigationService.Add(Root, ContentType); + + Child1 = new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"); + _navigationService.Add(Child1, ContentType, Root); + + Grandchild1 = new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"); + _navigationService.Add(Grandchild1, ContentType, Child1); + + Grandchild2 = new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"); + _navigationService.Add(Grandchild2, ContentType, Child1); + + Child2 = new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"); + _navigationService.Add(Child2, ContentType, Root); + + Grandchild3 = new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"); + _navigationService.Add(Grandchild3, ContentType, Child2); + + GreatGrandchild1 = new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"); + _navigationService.Add(GreatGrandchild1, ContentType, Grandchild3); + + Child3 = new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"); + _navigationService.Add(Child3, ContentType, Root); + + Grandchild4 = new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"); + _navigationService.Add(Grandchild4, ContentType, Child3); + } } -internal class TestContentNavigationService : ContentNavigationServiceBase +internal class TestContentNavigationService : ContentNavigationServiceBase { - public TestContentNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) - : base(coreScopeProvider, navigationRepository) + public TestContentNavigationService( + ICoreScopeProvider coreScopeProvider, + INavigationRepository navigationRepository, + IContentTypeService contentTypeService) + : base(coreScopeProvider, navigationRepository, contentTypeService) { } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceTest.cs index 52863f056f..916dac279c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceTest.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceTest.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -23,7 +24,7 @@ public class ContentNavigationServiceTest navigationRepoMock.Setup(x => x.GetContentNodesByObjectType(Constants.ObjectTypes.Document)) .Returns(navigationNodes); - var contentNavigationService = new DocumentNavigationService(GetScopeProvider(), navigationRepoMock.Object); + var contentNavigationService = new DocumentNavigationService(GetScopeProvider(), navigationRepoMock.Object, Mock.Of()); await contentNavigationService.RebuildAsync(); var success = contentNavigationService.TryGetLevel(rootKey, out var level); @@ -47,7 +48,7 @@ public class ContentNavigationServiceTest navigationRepoMock.Setup(x => x.GetContentNodesByObjectType(Constants.ObjectTypes.Document)) .Returns(navigationNodes); - var contentNavigationService = new DocumentNavigationService(GetScopeProvider(), navigationRepoMock.Object); + var contentNavigationService = new DocumentNavigationService(GetScopeProvider(), navigationRepoMock.Object, Mock.Of()); await contentNavigationService.RebuildAsync(); var success = contentNavigationService.TryGetLevel(childKey, out var level); @@ -71,7 +72,7 @@ public class ContentNavigationServiceTest navigationRepoMock.Setup(x => x.GetContentNodesByObjectType(Constants.ObjectTypes.Document)) .Returns(navigationNodes); - var contentNavigationService = new DocumentNavigationService(GetScopeProvider(), navigationRepoMock.Object); + var contentNavigationService = new DocumentNavigationService(GetScopeProvider(), navigationRepoMock.Object, Mock.Of()); await contentNavigationService.RebuildAsync(); var success = contentNavigationService.TryGetLevel(grandChildKey, out var level);