diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs index d7a803b584..8c15708f1f 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs @@ -42,11 +42,21 @@ public abstract class UserStartNodeTreeControllerBase : EntityTreeControl protected override IEntitySlim[] GetPagedChildEntities(Guid parentKey, int skip, int take, out long totalItems) { - IEntitySlim[] children = base.GetPagedChildEntities(parentKey, skip, take, out totalItems); - return UserHasRootAccess() || IgnoreUserStartNodes() - ? children - // Keeping the correct totalItems amount from GetPagedChildEntities - : CalculateAccessMap(() => _userStartNodeEntitiesService.ChildUserAccessEntities(children, UserStartNodePaths), out _); + if (UserHasRootAccess() || IgnoreUserStartNodes()) + { + return base.GetPagedChildEntities(parentKey, skip, take, out totalItems); + } + + IEnumerable userAccessEntities = _userStartNodeEntitiesService.ChildUserAccessEntities( + ItemObjectType, + UserStartNodePaths, + parentKey, + skip, + take, + ItemOrdering, + out totalItems); + + return CalculateAccessMap(() => userAccessEntities, out _); } protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) diff --git a/src/Umbraco.Cms.Api.Management/Services/Entities/IUserStartNodeEntitiesService.cs b/src/Umbraco.Cms.Api.Management/Services/Entities/IUserStartNodeEntitiesService.cs index 0a00d2e963..2753bd29b8 100644 --- a/src/Umbraco.Cms.Api.Management/Services/Entities/IUserStartNodeEntitiesService.cs +++ b/src/Umbraco.Cms.Api.Management/Services/Entities/IUserStartNodeEntitiesService.cs @@ -16,9 +16,41 @@ public interface IUserStartNodeEntitiesService /// /// The returned entities may include entities that outside of the user start node scope, but are needed to /// for browsing to the actual user start nodes. These entities will be marked as "no access" entities. + /// + /// This method does not support pagination, because it must load all entities explicitly in order to calculate + /// the correct result, given that user start nodes can be descendants of root nodes. Consumers need to apply + /// pagination to the result if applicable. /// IEnumerable RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds); + /// + /// Calculates the applicable child entities for a given object type for users without root access. + /// + /// The object type. + /// The calculated start node paths for the user. + /// The key of the parent. + /// The number of applicable children to skip. + /// The number of applicable children to take. + /// The ordering to apply when fetching and paginating the children. + /// The total number of applicable children available. + /// A list of child entities applicable for the user. + /// + /// The returned entities may include entities that outside of the user start node scope, but are needed to + /// for browsing to the actual user start nodes. These entities will be marked as "no access" entities. + /// + IEnumerable ChildUserAccessEntities( + UmbracoObjectTypes umbracoObjectType, + string[] userStartNodePaths, + Guid parentKey, + int skip, + int take, + Ordering ordering, + out long totalItems) + { + totalItems = 0; + return []; + } + /// /// Calculates the applicable child entities from a list of candidate child entities for users without root access. /// diff --git a/src/Umbraco.Cms.Api.Management/Services/Entities/UserStartNodeEntitiesService.cs b/src/Umbraco.Cms.Api.Management/Services/Entities/UserStartNodeEntitiesService.cs index 1a5572303b..c811d2c9c7 100644 --- a/src/Umbraco.Cms.Api.Management/Services/Entities/UserStartNodeEntitiesService.cs +++ b/src/Umbraco.Cms.Api.Management/Services/Entities/UserStartNodeEntitiesService.cs @@ -1,8 +1,12 @@ -using Umbraco.Cms.Core; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Api.Management.Models.Entities; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Persistence.Querying; +using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Services.Entities; @@ -10,8 +14,24 @@ namespace Umbraco.Cms.Api.Management.Services.Entities; public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService { private readonly IEntityService _entityService; + private readonly ICoreScopeProvider _scopeProvider; + private readonly IIdKeyMap _idKeyMap; - public UserStartNodeEntitiesService(IEntityService entityService) => _entityService = entityService; + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V17.")] + public UserStartNodeEntitiesService(IEntityService entityService) + : this( + entityService, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public UserStartNodeEntitiesService(IEntityService entityService, ICoreScopeProvider scopeProvider, IIdKeyMap idKeyMap) + { + _entityService = entityService; + _scopeProvider = scopeProvider; + _idKeyMap = idKeyMap; + } /// public IEnumerable RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds) @@ -43,6 +63,54 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService .ToArray(); } + public IEnumerable ChildUserAccessEntities(UmbracoObjectTypes umbracoObjectType, string[] userStartNodePaths, Guid parentKey, int skip, int take, Ordering ordering, out long totalItems) + { + Attempt parentIdAttempt = _idKeyMap.GetIdForKey(parentKey, umbracoObjectType); + if (parentIdAttempt.Success is false) + { + totalItems = 0; + return []; + } + + var parentId = parentIdAttempt.Result; + IEntitySlim? parent = _entityService.Get(parentId); + if (parent is null) + { + totalItems = 0; + return []; + } + + IEntitySlim[] children; + if (userStartNodePaths.Any(path => $"{parent.Path},".StartsWith($"{path},"))) + { + // the requested parent is one of the user start nodes (or a descendant of one), all children are by definition allowed + children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, ordering: ordering).ToArray(); + return ChildUserAccessEntities(children, userStartNodePaths); + } + + // if one or more of the user start nodes are descendants of the requested parent, find the "next child IDs" in those user start node paths + // - e.g. given the user start node path "-1,2,3,4,5", if the requested parent ID is 3, the "next child ID" is 4. + var userStartNodePathIds = userStartNodePaths.Select(path => path.Split(Constants.CharArrays.Comma).Select(int.Parse).ToArray()).ToArray(); + var allowedChildIds = userStartNodePathIds + .Where(ids => ids.Contains(parentId)) + // given the previous checks, the parent ID can never be the last in the user start node path, so this is safe + .Select(ids => ids[ids.IndexOf(parentId) + 1]) + .Distinct() + .ToArray(); + + totalItems = allowedChildIds.Length; + if (allowedChildIds.Length == 0) + { + // the requested parent is outside the scope of any user start nodes + return []; + } + + // even though we know the IDs of the allowed child entities to fetch, we still use a Query to yield correctly sorted children + IQuery query = _scopeProvider.CreateQuery().Where(x => allowedChildIds.Contains(x.Id)); + children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, query, ordering).ToArray(); + return ChildUserAccessEntities(children, userStartNodePaths); + } + /// public IEnumerable ChildUserAccessEntities(IEnumerable candidateChildren, string[] userStartNodePaths) // child entities for users without root access should include: diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.RootUserAccessEntities.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.RootUserAccessEntities.cs new file mode 100644 index 0000000000..a6ab1ff131 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.RootUserAccessEntities.cs @@ -0,0 +1,85 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services; + +public partial class UserStartNodeEntitiesServiceMediaTests +{ + [Test] + public async Task RootUserAccessEntities_FirstAndLastRoot_YieldsBoth_AsAllowed() + { + var contentStartNodeIds = await CreateUserAndGetStartNodeIds(_mediaByName["1"].Id, _mediaByName["5"].Id); + + var roots = UserStartNodeEntitiesService + .RootUserAccessEntities( + UmbracoObjectTypes.Media, + contentStartNodeIds) + .ToArray(); + + // expected total is 2, because only two items at root ("1" amd "10") are allowed + Assert.AreEqual(2, roots.Length); + Assert.Multiple(() => + { + // first and last content items are the ones allowed + Assert.AreEqual(_mediaByName["1"].Key, roots[0].Entity.Key); + Assert.AreEqual(_mediaByName["5"].Key, roots[1].Entity.Key); + + // explicitly verify the entity sort order, both so we know sorting works, + // and so we know it's actually the first and last item at root + Assert.AreEqual(0, roots[0].Entity.SortOrder); + Assert.AreEqual(4, roots[1].Entity.SortOrder); + + // both are allowed (they are the actual start nodes) + Assert.IsTrue(roots[0].HasAccess); + Assert.IsTrue(roots[1].HasAccess); + }); + } + + [Test] + public async Task RootUserAccessEntities_ChildrenAsStartNode_YieldsChildRoots_AsNotAllowed() + { + var contentStartNodeIds = await CreateUserAndGetStartNodeIds(_mediaByName["1-3"].Id, _mediaByName["3-3"].Id, _mediaByName["5-3"].Id); + + var roots = UserStartNodeEntitiesService + .RootUserAccessEntities( + UmbracoObjectTypes.Media, + contentStartNodeIds) + .ToArray(); + + Assert.AreEqual(3, roots.Length); + Assert.Multiple(() => + { + // the three start nodes are the children of the "1", "3" and "5" roots, respectively, so these are expected as roots + Assert.AreEqual(_mediaByName["1"].Key, roots[0].Entity.Key); + Assert.AreEqual(_mediaByName["3"].Key, roots[1].Entity.Key); + Assert.AreEqual(_mediaByName["5"].Key, roots[2].Entity.Key); + + // all are disallowed - only the children (the actual start nodes) are allowed + Assert.IsTrue(roots.All(r => r.HasAccess is false)); + }); + } + + [Test] + public async Task RootUserAccessEntities_GrandchildrenAsStartNode_YieldsGrandchildRoots_AsNotAllowed() + { + var contentStartNodeIds = await CreateUserAndGetStartNodeIds(_mediaByName["1-2-3"].Id, _mediaByName["2-3-4"].Id, _mediaByName["3-4-5"].Id); + + var roots = UserStartNodeEntitiesService + .RootUserAccessEntities( + UmbracoObjectTypes.Media, + contentStartNodeIds) + .ToArray(); + + Assert.AreEqual(3, roots.Length); + Assert.Multiple(() => + { + // the three start nodes are the grandchildren of the "1", "2" and "3" roots, respectively, so these are expected as roots + Assert.AreEqual(_mediaByName["1"].Key, roots[0].Entity.Key); + Assert.AreEqual(_mediaByName["2"].Key, roots[1].Entity.Key); + Assert.AreEqual(_mediaByName["3"].Key, roots[2].Entity.Key); + + // all are disallowed - only the grandchildren (the actual start nodes) are allowed + Assert.IsTrue(roots.All(r => r.HasAccess is false)); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.childUserAccessEntities.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.childUserAccessEntities.cs new file mode 100644 index 0000000000..4df5ecbab7 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.childUserAccessEntities.cs @@ -0,0 +1,336 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services; + +public partial class UserStartNodeEntitiesServiceMediaTests +{ + [Test] + public async Task ChildUserAccessEntities_FirstAndLastChildOfRoot_YieldsBothInFirstPage_AsAllowed() + { + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1-1"].Id, _mediaByName["1-10"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Media, + mediaStartNodePaths, + _mediaByName["1"].Key, + 0, + 3, + BySortOrder, + out var totalItems) + .ToArray(); + + // expected total is 2, because only two items under "1" are allowed (note the page size is 3 for good measure) + Assert.AreEqual(2, totalItems); + Assert.AreEqual(2, children.Length); + Assert.Multiple(() => + { + // first and last media items are the ones allowed + Assert.AreEqual(_mediaByName["1-1"].Key, children[0].Entity.Key); + Assert.AreEqual(_mediaByName["1-10"].Key, children[1].Entity.Key); + + // explicitly verify the entity sort order, both so we know sorting works, + // and so we know it's actually the first and last item below "1" + Assert.AreEqual(0, children[0].Entity.SortOrder); + Assert.AreEqual(9, children[1].Entity.SortOrder); + + // both are allowed (they are the actual start nodes) + Assert.IsTrue(children[0].HasAccess); + Assert.IsTrue(children[1].HasAccess); + }); + } + + [Test] + public async Task ChildUserAccessEntities_InAndOutOfScope_YieldsOnlyChildrenInScope() + { + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1-5"].Id, _mediaByName["2-10"].Id); + Assert.AreEqual(2, mediaStartNodePaths.Length); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Media, + mediaStartNodePaths, + _mediaByName["2"].Key, + 0, + 10, + BySortOrder, + out var totalItems) + .ToArray(); + + Assert.AreEqual(1, totalItems); + Assert.AreEqual(1, children.Length); + Assert.Multiple(() => + { + // only the "2-10" media item is returned, as "1-5" is out of scope + Assert.AreEqual(_mediaByName["2-10"].Key, children[0].Entity.Key); + Assert.IsTrue(children[0].HasAccess); + }); + } + + [Test] + public async Task ChildUserAccessEntities_OutOfScope_YieldsNothing() + { + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1-5"].Id, _mediaByName["2-10"].Id); + Assert.AreEqual(2, mediaStartNodePaths.Length); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Media, + mediaStartNodePaths, + _mediaByName["3"].Key, + 0, + 10, + BySortOrder, + out var totalItems) + .ToArray(); + + Assert.AreEqual(0, totalItems); + Assert.AreEqual(0, children.Length); + } + + [Test] + public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginate() + { + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths( + _mediaByName["1-1"].Id, + _mediaByName["1-3"].Id, + _mediaByName["1-5"].Id, + _mediaByName["1-7"].Id, + _mediaByName["1-9"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Media, + mediaStartNodePaths, + _mediaByName["1"].Key, + 0, + 2, + BySortOrder, + out var totalItems) + .ToArray(); + + Assert.AreEqual(5, totalItems); + // page size is 2 + Assert.AreEqual(2, children.Length); + Assert.Multiple(() => + { + Assert.AreEqual(_mediaByName["1-1"].Key, children[0].Entity.Key); + Assert.AreEqual(_mediaByName["1-3"].Key, children[1].Entity.Key); + Assert.IsTrue(children[0].HasAccess); + Assert.IsTrue(children[1].HasAccess); + }); + + // next result page + children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Media, + mediaStartNodePaths, + _mediaByName["1"].Key, + 2, + 2, + BySortOrder, + out totalItems) + .ToArray(); + + Assert.AreEqual(5, totalItems); + // page size is still 2 + Assert.AreEqual(2, children.Length); + Assert.Multiple(() => + { + Assert.AreEqual(_mediaByName["1-5"].Key, children[0].Entity.Key); + Assert.AreEqual(_mediaByName["1-7"].Key, children[1].Entity.Key); + Assert.IsTrue(children[0].HasAccess); + Assert.IsTrue(children[1].HasAccess); + }); + + // next result page + children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Media, + mediaStartNodePaths, + _mediaByName["1"].Key, + 4, + 2, + BySortOrder, + out totalItems) + .ToArray(); + + Assert.AreEqual(5, totalItems); + // page size is still 2, but this is the last result page + Assert.AreEqual(1, children.Length); + Assert.Multiple(() => + { + Assert.AreEqual(_mediaByName["1-9"].Key, children[0].Entity.Key); + Assert.IsTrue(children[0].HasAccess); + }); + } + + [Test] + public async Task ChildUserAccessEntities_ChildrenAsStartNode_YieldsAllGrandchildren_AsAllowed() + { + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["3-3"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Media, + mediaStartNodePaths, + _mediaByName["3-3"].Key, + 0, + 100, + BySortOrder, + out var totalItems) + .ToArray(); + + Assert.AreEqual(5, totalItems); + Assert.AreEqual(5, children.Length); + Assert.Multiple(() => + { + // all children of "3-3" should be allowed because "3-3" is a start node + foreach (var childNumber in Enumerable.Range(1, 5)) + { + var child = children[childNumber - 1]; + Assert.AreEqual(_mediaByName[$"3-3-{childNumber}"].Key, child.Entity.Key); + Assert.IsTrue(child.HasAccess); + } + }); + } + + [Test] + public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsGrandchildren_AsAllowed() + { + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["3-3-3"].Id, _mediaByName["3-3-4"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Media, + mediaStartNodePaths, + _mediaByName["3-3"].Key, + 0, + 3, + BySortOrder, + out var totalItems) + .ToArray(); + + Assert.AreEqual(2, totalItems); + Assert.AreEqual(2, children.Length); + Assert.Multiple(() => + { + // the two items are the children of "3-3" - that is, the actual start nodes + Assert.AreEqual(_mediaByName["3-3-3"].Key, children[0].Entity.Key); + Assert.AreEqual(_mediaByName["3-3-4"].Key, children[1].Entity.Key); + + // both are allowed (they are the actual start nodes) + Assert.IsTrue(children[0].HasAccess); + Assert.IsTrue(children[1].HasAccess); + }); + } + + [Test] + public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsChildren_AsNotAllowed() + { + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["3-3-3"].Id, _mediaByName["3-5-3"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Media, + mediaStartNodePaths, + _mediaByName["3"].Key, + 0, + 3, + BySortOrder, + out var totalItems) + .ToArray(); + + Assert.AreEqual(2, totalItems); + Assert.AreEqual(2, children.Length); + Assert.Multiple(() => + { + // the two items are the children of "3" - that is, the parents of the actual start nodes + Assert.AreEqual(_mediaByName["3-3"].Key, children[0].Entity.Key); + Assert.AreEqual(_mediaByName["3-5"].Key, children[1].Entity.Key); + + // both are disallowed - only the two children (the actual start nodes) are allowed + Assert.IsFalse(children[0].HasAccess); + Assert.IsFalse(children[1].HasAccess); + }); + } + + [Test] + public async Task ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOnlyGrandchild() + { + // NOTE: this test proves that start node paths are *not* additive when structural inheritance is in play. + // if one has a start node that is a descendant to another start node, the descendant start node "wins" + // and the ancestor start node is ignored. this differs somewhat from the norm; we normally consider + // permissions additive (which in this case would mean ignoring the descendant rather than the ancestor). + + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["3-3"].Id, _mediaByName["3-3-1"].Id, _mediaByName["3-3-5"].Id); + Assert.AreEqual(2, mediaStartNodePaths.Length); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Media, + mediaStartNodePaths, + _mediaByName["3"].Key, + 0, + 10, + BySortOrder, + out var totalItems) + .ToArray(); + Assert.AreEqual(1, totalItems); + Assert.AreEqual(1, children.Length); + Assert.Multiple(() => + { + Assert.AreEqual(_mediaByName["3-3"].Key, children[0].Entity.Key); + Assert.IsFalse(children[0].HasAccess); + }); + + children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Media, + mediaStartNodePaths, + _mediaByName["3-3"].Key, + 0, + 10, + BySortOrder, + out totalItems) + .ToArray(); + + Assert.AreEqual(2, totalItems); + Assert.AreEqual(2, children.Length); + Assert.Multiple(() => + { + // the two items are the children of "3-3" - that is, the actual start nodes + Assert.AreEqual(_mediaByName["3-3-1"].Key, children[0].Entity.Key); + Assert.AreEqual(_mediaByName["3-3-5"].Key, children[1].Entity.Key); + + Assert.IsTrue(children[0].HasAccess); + Assert.IsTrue(children[1].HasAccess); + }); + } + + [Test] + public async Task ChildUserAccessEntities_ReverseStartNodeOrder_DoesNotAffectResultOrder() + { + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["3-3"].Id, _mediaByName["3-2"].Id, _mediaByName["3-1"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Media, + mediaStartNodePaths, + _mediaByName["3"].Key, + 0, + 10, + BySortOrder, + out var totalItems) + .ToArray(); + Assert.AreEqual(3, totalItems); + Assert.AreEqual(3, children.Length); + Assert.Multiple(() => + { + Assert.AreEqual(_mediaByName["3-1"].Key, children[0].Entity.Key); + Assert.AreEqual(_mediaByName["3-2"].Key, children[1].Entity.Key); + Assert.AreEqual(_mediaByName["3-3"].Key, children[2].Entity.Key); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.cs new file mode 100644 index 0000000000..44a27bd7fb --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.cs @@ -0,0 +1,134 @@ +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] +public partial class UserStartNodeEntitiesServiceMediaTests : UmbracoIntegrationTest +{ + private Dictionary _mediaByName = new (); + private IUserGroup _userGroup; + + private IMediaService MediaService => GetRequiredService(); + + private IMediaTypeService MediaTypeService => GetRequiredService(); + + private IUserGroupService UserGroupService => GetRequiredService(); + + private IUserService UserService => GetRequiredService(); + + private IEntityService EntityService => GetRequiredService(); + + private IUserStartNodeEntitiesService UserStartNodeEntitiesService => GetRequiredService(); + + protected readonly Ordering BySortOrder = Ordering.By("sortOrder"); + + protected override void ConfigureTestServices(IServiceCollection services) + { + base.ConfigureTestServices(services); + services.AddTransient(); + } + + [SetUp] + public async Task SetUpTestAsync() + { + if (_mediaByName.Any()) + { + return; + } + + var mediaType = new MediaTypeBuilder() + .WithAlias("theMediaType") + .Build(); + mediaType.AllowedAsRoot = true; + await MediaTypeService.CreateAsync(mediaType, Constants.Security.SuperUserKey); + mediaType.AllowedContentTypes = [new() { Alias = mediaType.Alias, Key = mediaType.Key }]; + await MediaTypeService.UpdateAsync(mediaType, Constants.Security.SuperUserKey); + + foreach (var rootNumber in Enumerable.Range(1, 5)) + { + var root = new MediaBuilder() + .WithMediaType(mediaType) + .WithName($"{rootNumber}") + .Build(); + MediaService.Save(root); + _mediaByName[root.Name!] = root; + + foreach (var childNumber in Enumerable.Range(1, 10)) + { + var child = new MediaBuilder() + .WithMediaType(mediaType) + .WithName($"{rootNumber}-{childNumber}") + .Build(); + child.SetParent(root); + MediaService.Save(child); + _mediaByName[child.Name!] = child; + + foreach (var grandChildNumber in Enumerable.Range(1, 5)) + { + var grandchild = new MediaBuilder() + .WithMediaType(mediaType) + .WithName($"{rootNumber}-{childNumber}-{grandChildNumber}") + .Build(); + grandchild.SetParent(child); + MediaService.Save(grandchild); + _mediaByName[grandchild.Name!] = grandchild; + } + } + } + + _userGroup = new UserGroupBuilder() + .WithAlias("theGroup") + .WithAllowedSections(["media"]) + .Build(); + _userGroup.StartMediaId = null; + await UserGroupService.CreateAsync(_userGroup, Constants.Security.SuperUserKey); + } + + private async Task CreateUserAndGetStartNodePaths(params int[] startNodeIds) + { + var user = await CreateUser(startNodeIds); + + var mediaStartNodePaths = user.GetMediaStartNodePaths(EntityService, AppCaches.NoCache); + Assert.IsNotNull(mediaStartNodePaths); + + return mediaStartNodePaths; + } + + private async Task CreateUserAndGetStartNodeIds(params int[] startNodeIds) + { + var user = await CreateUser(startNodeIds); + + var mediaStartNodeIds = user.CalculateMediaStartNodeIds(EntityService, AppCaches.NoCache); + Assert.IsNotNull(mediaStartNodeIds); + + return mediaStartNodeIds; + } + + private async Task CreateUser(int[] startNodeIds) + { + var user = new UserBuilder() + .WithName(Guid.NewGuid().ToString("N")) + .WithStartMediaIds(startNodeIds) + .Build(); + UserService.Save(user); + + var attempt = await UserGroupService.AddUsersToUserGroupAsync( + new UsersToUserGroupManipulationModel(_userGroup.Key, [user.Key]), + Constants.Security.SuperUserKey); + + Assert.IsTrue(attempt.Success); + return user; + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.ChildUserAccessEntities.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.ChildUserAccessEntities.cs new file mode 100644 index 0000000000..2272b60bbd --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.ChildUserAccessEntities.cs @@ -0,0 +1,336 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services; + +public partial class UserStartNodeEntitiesServiceTests +{ + [Test] + public async Task ChildUserAccessEntities_FirstAndLastChildOfRoot_YieldsBothInFirstPage_AsAllowed() + { + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1-1"].Id, _contentByName["1-10"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Document, + contentStartNodePaths, + _contentByName["1"].Key, + 0, + 3, + BySortOrder, + out var totalItems) + .ToArray(); + + // expected total is 2, because only two items under "1" are allowed (note the page size is 3 for good measure) + Assert.AreEqual(2, totalItems); + Assert.AreEqual(2, children.Length); + Assert.Multiple(() => + { + // first and last content items are the ones allowed + Assert.AreEqual(_contentByName["1-1"].Key, children[0].Entity.Key); + Assert.AreEqual(_contentByName["1-10"].Key, children[1].Entity.Key); + + // explicitly verify the entity sort order, both so we know sorting works, + // and so we know it's actually the first and last item below "1" + Assert.AreEqual(0, children[0].Entity.SortOrder); + Assert.AreEqual(9, children[1].Entity.SortOrder); + + // both are allowed (they are the actual start nodes) + Assert.IsTrue(children[0].HasAccess); + Assert.IsTrue(children[1].HasAccess); + }); + } + + [Test] + public async Task ChildUserAccessEntities_InAndOutOfScope_YieldsOnlyChildrenInScope() + { + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1-5"].Id, _contentByName["2-10"].Id); + Assert.AreEqual(2, contentStartNodePaths.Length); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Document, + contentStartNodePaths, + _contentByName["2"].Key, + 0, + 10, + BySortOrder, + out var totalItems) + .ToArray(); + + Assert.AreEqual(1, totalItems); + Assert.AreEqual(1, children.Length); + Assert.Multiple(() => + { + // only the "2-10" content item is returned, as "1-5" is out of scope + Assert.AreEqual(_contentByName["2-10"].Key, children[0].Entity.Key); + Assert.IsTrue(children[0].HasAccess); + }); + } + + [Test] + public async Task ChildUserAccessEntities_OutOfScope_YieldsNothing() + { + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1-5"].Id, _contentByName["2-10"].Id); + Assert.AreEqual(2, contentStartNodePaths.Length); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Document, + contentStartNodePaths, + _contentByName["3"].Key, + 0, + 10, + BySortOrder, + out var totalItems) + .ToArray(); + + Assert.AreEqual(0, totalItems); + Assert.AreEqual(0, children.Length); + } + + [Test] + public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginate() + { + var contentStartNodePaths = await CreateUserAndGetStartNodePaths( + _contentByName["1-1"].Id, + _contentByName["1-3"].Id, + _contentByName["1-5"].Id, + _contentByName["1-7"].Id, + _contentByName["1-9"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Document, + contentStartNodePaths, + _contentByName["1"].Key, + 0, + 2, + BySortOrder, + out var totalItems) + .ToArray(); + + Assert.AreEqual(5, totalItems); + // page size is 2 + Assert.AreEqual(2, children.Length); + Assert.Multiple(() => + { + Assert.AreEqual(_contentByName["1-1"].Key, children[0].Entity.Key); + Assert.AreEqual(_contentByName["1-3"].Key, children[1].Entity.Key); + Assert.IsTrue(children[0].HasAccess); + Assert.IsTrue(children[1].HasAccess); + }); + + // next result page + children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Document, + contentStartNodePaths, + _contentByName["1"].Key, + 2, + 2, + BySortOrder, + out totalItems) + .ToArray(); + + Assert.AreEqual(5, totalItems); + // page size is still 2 + Assert.AreEqual(2, children.Length); + Assert.Multiple(() => + { + Assert.AreEqual(_contentByName["1-5"].Key, children[0].Entity.Key); + Assert.AreEqual(_contentByName["1-7"].Key, children[1].Entity.Key); + Assert.IsTrue(children[0].HasAccess); + Assert.IsTrue(children[1].HasAccess); + }); + + // next result page + children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Document, + contentStartNodePaths, + _contentByName["1"].Key, + 4, + 2, + BySortOrder, + out totalItems) + .ToArray(); + + Assert.AreEqual(5, totalItems); + // page size is still 2, but this is the last result page + Assert.AreEqual(1, children.Length); + Assert.Multiple(() => + { + Assert.AreEqual(_contentByName["1-9"].Key, children[0].Entity.Key); + Assert.IsTrue(children[0].HasAccess); + }); + } + + [Test] + public async Task ChildUserAccessEntities_ChildrenAsStartNode_YieldsAllGrandchildren_AsAllowed() + { + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["3-3"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Document, + contentStartNodePaths, + _contentByName["3-3"].Key, + 0, + 100, + BySortOrder, + out var totalItems) + .ToArray(); + + Assert.AreEqual(5, totalItems); + Assert.AreEqual(5, children.Length); + Assert.Multiple(() => + { + // all children of "3-3" should be allowed because "3-3" is a start node + foreach (var childNumber in Enumerable.Range(1, 5)) + { + var child = children[childNumber - 1]; + Assert.AreEqual(_contentByName[$"3-3-{childNumber}"].Key, child.Entity.Key); + Assert.IsTrue(child.HasAccess); + } + }); + } + + [Test] + public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsGrandchildren_AsAllowed() + { + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["3-3-3"].Id, _contentByName["3-3-4"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Document, + contentStartNodePaths, + _contentByName["3-3"].Key, + 0, + 3, + BySortOrder, + out var totalItems) + .ToArray(); + + Assert.AreEqual(2, totalItems); + Assert.AreEqual(2, children.Length); + Assert.Multiple(() => + { + // the two items are the children of "3-3" - that is, the actual start nodes + Assert.AreEqual(_contentByName["3-3-3"].Key, children[0].Entity.Key); + Assert.AreEqual(_contentByName["3-3-4"].Key, children[1].Entity.Key); + + // both are allowed (they are the actual start nodes) + Assert.IsTrue(children[0].HasAccess); + Assert.IsTrue(children[1].HasAccess); + }); + } + + [Test] + public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsChildren_AsNotAllowed() + { + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["3-3-3"].Id, _contentByName["3-5-3"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Document, + contentStartNodePaths, + _contentByName["3"].Key, + 0, + 3, + BySortOrder, + out var totalItems) + .ToArray(); + + Assert.AreEqual(2, totalItems); + Assert.AreEqual(2, children.Length); + Assert.Multiple(() => + { + // the two items are the children of "3" - that is, the parents of the actual start nodes + Assert.AreEqual(_contentByName["3-3"].Key, children[0].Entity.Key); + Assert.AreEqual(_contentByName["3-5"].Key, children[1].Entity.Key); + + // both are disallowed - only the two children (the actual start nodes) are allowed + Assert.IsFalse(children[0].HasAccess); + Assert.IsFalse(children[1].HasAccess); + }); + } + + [Test] + public async Task ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOnlyGrandchild() + { + // NOTE: this test proves that start node paths are *not* additive when structural inheritance is in play. + // if one has a start node that is a descendant to another start node, the descendant start node "wins" + // and the ancestor start node is ignored. this differs somewhat from the norm; we normally consider + // permissions additive (which in this case would mean ignoring the descendant rather than the ancestor). + + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["3-3"].Id, _contentByName["3-3-1"].Id, _contentByName["3-3-5"].Id); + Assert.AreEqual(2, contentStartNodePaths.Length); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Document, + contentStartNodePaths, + _contentByName["3"].Key, + 0, + 10, + BySortOrder, + out var totalItems) + .ToArray(); + Assert.AreEqual(1, totalItems); + Assert.AreEqual(1, children.Length); + Assert.Multiple(() => + { + Assert.AreEqual(_contentByName["3-3"].Key, children[0].Entity.Key); + Assert.IsFalse(children[0].HasAccess); + }); + + children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Document, + contentStartNodePaths, + _contentByName["3-3"].Key, + 0, + 10, + BySortOrder, + out totalItems) + .ToArray(); + + Assert.AreEqual(2, totalItems); + Assert.AreEqual(2, children.Length); + Assert.Multiple(() => + { + // the two items are the children of "3-3" - that is, the actual start nodes + Assert.AreEqual(_contentByName["3-3-1"].Key, children[0].Entity.Key); + Assert.AreEqual(_contentByName["3-3-5"].Key, children[1].Entity.Key); + + Assert.IsTrue(children[0].HasAccess); + Assert.IsTrue(children[1].HasAccess); + }); + } + + [Test] + public async Task ChildUserAccessEntities_ReverseStartNodeOrder_DoesNotAffectResultOrder() + { + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["3-3"].Id, _contentByName["3-2"].Id, _contentByName["3-1"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + UmbracoObjectTypes.Document, + contentStartNodePaths, + _contentByName["3"].Key, + 0, + 10, + BySortOrder, + out var totalItems) + .ToArray(); + Assert.AreEqual(3, totalItems); + Assert.AreEqual(3, children.Length); + Assert.Multiple(() => + { + Assert.AreEqual(_contentByName["3-1"].Key, children[0].Entity.Key); + Assert.AreEqual(_contentByName["3-2"].Key, children[1].Entity.Key); + Assert.AreEqual(_contentByName["3-3"].Key, children[2].Entity.Key); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.RootUserAccessEntities.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.RootUserAccessEntities.cs new file mode 100644 index 0000000000..c73ac2778b --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.RootUserAccessEntities.cs @@ -0,0 +1,85 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services; + +public partial class UserStartNodeEntitiesServiceTests +{ + [Test] + public async Task RootUserAccessEntities_FirstAndLastRoot_YieldsBoth_AsAllowed() + { + var contentStartNodeIds = await CreateUserAndGetStartNodeIds(_contentByName["1"].Id, _contentByName["5"].Id); + + var roots = UserStartNodeEntitiesService + .RootUserAccessEntities( + UmbracoObjectTypes.Document, + contentStartNodeIds) + .ToArray(); + + // expected total is 2, because only two items at root ("1" amd "10") are allowed + Assert.AreEqual(2, roots.Length); + Assert.Multiple(() => + { + // first and last content items are the ones allowed + Assert.AreEqual(_contentByName["1"].Key, roots[0].Entity.Key); + Assert.AreEqual(_contentByName["5"].Key, roots[1].Entity.Key); + + // explicitly verify the entity sort order, both so we know sorting works, + // and so we know it's actually the first and last item at root + Assert.AreEqual(0, roots[0].Entity.SortOrder); + Assert.AreEqual(4, roots[1].Entity.SortOrder); + + // both are allowed (they are the actual start nodes) + Assert.IsTrue(roots[0].HasAccess); + Assert.IsTrue(roots[1].HasAccess); + }); + } + + [Test] + public async Task RootUserAccessEntities_ChildrenAsStartNode_YieldsChildRoots_AsNotAllowed() + { + var contentStartNodeIds = await CreateUserAndGetStartNodeIds(_contentByName["1-3"].Id, _contentByName["3-3"].Id, _contentByName["5-3"].Id); + + var roots = UserStartNodeEntitiesService + .RootUserAccessEntities( + UmbracoObjectTypes.Document, + contentStartNodeIds) + .ToArray(); + + Assert.AreEqual(3, roots.Length); + Assert.Multiple(() => + { + // the three start nodes are the children of the "1", "3" and "5" roots, respectively, so these are expected as roots + Assert.AreEqual(_contentByName["1"].Key, roots[0].Entity.Key); + Assert.AreEqual(_contentByName["3"].Key, roots[1].Entity.Key); + Assert.AreEqual(_contentByName["5"].Key, roots[2].Entity.Key); + + // all are disallowed - only the children (the actual start nodes) are allowed + Assert.IsTrue(roots.All(r => r.HasAccess is false)); + }); + } + + [Test] + public async Task RootUserAccessEntities_GrandchildrenAsStartNode_YieldsGrandchildRoots_AsNotAllowed() + { + var contentStartNodeIds = await CreateUserAndGetStartNodeIds(_contentByName["1-2-3"].Id, _contentByName["2-3-4"].Id, _contentByName["3-4-5"].Id); + + var roots = UserStartNodeEntitiesService + .RootUserAccessEntities( + UmbracoObjectTypes.Document, + contentStartNodeIds) + .ToArray(); + + Assert.AreEqual(3, roots.Length); + Assert.Multiple(() => + { + // the three start nodes are the grandchildren of the "1", "2" and "3" roots, respectively, so these are expected as roots + Assert.AreEqual(_contentByName["1"].Key, roots[0].Entity.Key); + Assert.AreEqual(_contentByName["2"].Key, roots[1].Entity.Key); + Assert.AreEqual(_contentByName["3"].Key, roots[2].Entity.Key); + + // all are disallowed - only the grandchildren (the actual start nodes) are allowed + Assert.IsTrue(roots.All(r => r.HasAccess is false)); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.cs new file mode 100644 index 0000000000..5cc6bff35d --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.cs @@ -0,0 +1,134 @@ +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] +public partial class UserStartNodeEntitiesServiceTests : UmbracoIntegrationTest +{ + private Dictionary _contentByName = new (); + private IUserGroup _userGroup; + + private IContentService ContentService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private IUserGroupService UserGroupService => GetRequiredService(); + + private IUserService UserService => GetRequiredService(); + + private IEntityService EntityService => GetRequiredService(); + + private IUserStartNodeEntitiesService UserStartNodeEntitiesService => GetRequiredService(); + + protected readonly Ordering BySortOrder = Ordering.By("sortOrder"); + + protected override void ConfigureTestServices(IServiceCollection services) + { + base.ConfigureTestServices(services); + services.AddTransient(); + } + + [SetUp] + public async Task SetUpTestAsync() + { + if (_contentByName.Any()) + { + return; + } + + var contentType = new ContentTypeBuilder() + .WithAlias("theContentType") + .Build(); + contentType.AllowedAsRoot = true; + await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); + contentType.AllowedContentTypes = [new() { Alias = contentType.Alias, Key = contentType.Key }]; + await ContentTypeService.UpdateAsync(contentType, Constants.Security.SuperUserKey); + + foreach (var rootNumber in Enumerable.Range(1, 5)) + { + var root = new ContentBuilder() + .WithContentType(contentType) + .WithName($"{rootNumber}") + .Build(); + ContentService.Save(root); + _contentByName[root.Name!] = root; + + foreach (var childNumber in Enumerable.Range(1, 10)) + { + var child = new ContentBuilder() + .WithContentType(contentType) + .WithParent(root) + .WithName($"{rootNumber}-{childNumber}") + .Build(); + ContentService.Save(child); + _contentByName[child.Name!] = child; + + foreach (var grandChildNumber in Enumerable.Range(1, 5)) + { + var grandchild = new ContentBuilder() + .WithContentType(contentType) + .WithParent(child) + .WithName($"{rootNumber}-{childNumber}-{grandChildNumber}") + .Build(); + ContentService.Save(grandchild); + _contentByName[grandchild.Name!] = grandchild; + } + } + } + + _userGroup = new UserGroupBuilder() + .WithAlias("theGroup") + .WithAllowedSections(["content"]) + .Build(); + _userGroup.StartContentId = null; + await UserGroupService.CreateAsync(_userGroup, Constants.Security.SuperUserKey); + } + + private async Task CreateUserAndGetStartNodePaths(params int[] startNodeIds) + { + var user = await CreateUser(startNodeIds); + + var contentStartNodePaths = user.GetContentStartNodePaths(EntityService, AppCaches.NoCache); + Assert.IsNotNull(contentStartNodePaths); + + return contentStartNodePaths; + } + + private async Task CreateUserAndGetStartNodeIds(params int[] startNodeIds) + { + var user = await CreateUser(startNodeIds); + + var contentStartNodeIds = user.CalculateContentStartNodeIds(EntityService, AppCaches.NoCache); + Assert.IsNotNull(contentStartNodeIds); + + return contentStartNodeIds; + } + + private async Task CreateUser(int[] startNodeIds) + { + var user = new UserBuilder() + .WithName(Guid.NewGuid().ToString("N")) + .WithStartContentIds(startNodeIds) + .Build(); + UserService.Save(user); + + var attempt = await UserGroupService.AddUsersToUserGroupAsync( + new UsersToUserGroupManipulationModel(_userGroup.Key, [user.Key]), + Constants.Security.SuperUserKey); + + Assert.IsTrue(attempt.Success); + return user; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 9efd93a4bc..db1e6d27e1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -9,7 +9,7 @@ $(BaseEnablePackageValidation) $(NoWarn),NU5100 - + $(WarningsNotAsErrors),CS0108,SYSLIB0012,CS0618,SA1116,SA1117,CS0162,CS0169,SA1134,SA1405,CS4014,CS1998,CS0649,CS0168 - + @@ -253,5 +253,17 @@ PublishedUrlInfoProviderTestsBase.cs + + UserStartNodeEntitiesServiceTests.cs + + + UserStartNodeEntitiesServiceTests.cs + + + UserStartNodeEntitiesServiceMediaTests.cs + + + UserStartNodeEntitiesServiceMediaTests.cs +