From 05514cdd08a3ac5819cca499b22100880b551c53 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 8 Apr 2026 08:23:46 +0200 Subject: [PATCH 1/3] Add unit tests for ContentPermissionService. --- .../Services/ContentPermissionServiceTests.cs | 382 ++++++++++++++++++ 1 file changed, 382 insertions(+) create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentPermissionServiceTests.cs diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentPermissionServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentPermissionServiceTests.cs new file mode 100644 index 000000000000..b2f20be8800c --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentPermissionServiceTests.cs @@ -0,0 +1,382 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.AuthorizationStatus; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services; + +[TestFixture] +public class ContentPermissionServiceTests +{ + private Mock _contentServiceMock; + private Mock _entityServiceMock; + private Mock _userServiceMock; + private Mock _languageServiceMock; + private IContentPermissionService _sut; + + [SetUp] + public void SetUp() + { + _contentServiceMock = new Mock(); + _entityServiceMock = new Mock(); + _userServiceMock = new Mock(); + _languageServiceMock = new Mock(); + _sut = new ContentPermissionService( + _contentServiceMock.Object, + _entityServiceMock.Object, + _userServiceMock.Object, + AppCaches.Disabled, + _languageServiceMock.Object); + } + + [Test] + public async Task Can_Authorize_Access_By_Path() + { + // Arrange + var contentKey = Guid.NewGuid(); + var user = CreateUser(); + + _entityServiceMock + .Setup(x => x.GetAllPaths(UmbracoObjectTypes.Document, new[] { contentKey })) + .Returns([CreateTreeEntityPath(contentKey, 1234, "-1,1234,5678")]); + + SetupPermissions(user, "-1,1234,5678", ["A"]); + + // Act + var result = await _sut.AuthorizeAccessAsync(user, contentKey, "A"); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.Success)); + } + + [Test] + public async Task Cannot_Authorize_Access_When_Content_Not_Found() + { + // Arrange + var contentKey = Guid.NewGuid(); + var user = CreateUser(); + + _entityServiceMock + .Setup(x => x.GetAllPaths(UmbracoObjectTypes.Document, new[] { contentKey })) + .Returns([]); + + // Act + var result = await _sut.AuthorizeAccessAsync(user, contentKey, "A"); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.NotFound)); + } + + [Test] + public async Task Cannot_Authorize_Access_Without_Path_Access() + { + // Arrange + var contentKey = Guid.NewGuid(); + var user = CreateUser(startContentId: 9876); + + _entityServiceMock + .Setup(x => x.GetAllPaths(UmbracoObjectTypes.Document, new[] { contentKey })) + .Returns([CreateTreeEntityPath(contentKey, 1234, "-1,1234,5678")]); + + _entityServiceMock + .Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) + .Returns([CreateTreeEntityPath(Guid.NewGuid(), 9876, "-1,9876")]); + + // Act + var result = await _sut.AuthorizeAccessAsync(user, contentKey, "A"); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.UnauthorizedMissingPathAccess)); + } + + [Test] + public async Task Cannot_Authorize_Access_Without_Required_Permission() + { + // Arrange + var contentKey = Guid.NewGuid(); + var user = CreateUser(); + + _entityServiceMock + .Setup(x => x.GetAllPaths(UmbracoObjectTypes.Document, new[] { contentKey })) + .Returns([CreateTreeEntityPath(contentKey, 1234, "-1,1234,5678")]); + + SetupPermissions(user, "-1,1234,5678", ["A", "B", "C"]); + + // Act + var result = await _sut.AuthorizeAccessAsync(user, contentKey, "F"); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.UnauthorizedMissingPermissionAccess)); + } + + [Test] + public async Task Can_Authorize_Access_With_Required_Permission() + { + // Arrange + var contentKey = Guid.NewGuid(); + var user = CreateUser(); + + _entityServiceMock + .Setup(x => x.GetAllPaths(UmbracoObjectTypes.Document, new[] { contentKey })) + .Returns([CreateTreeEntityPath(contentKey, 1234, "-1,1234,5678")]); + + SetupPermissions(user, "-1,1234,5678", ["A", "F", "C"]); + + // Act + var result = await _sut.AuthorizeAccessAsync(user, contentKey, "F"); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.Success)); + } + + [Test] + public async Task Can_Authorize_Access_With_Empty_Keys() + { + // Arrange + var user = CreateUser(); + + // Act + var result = await _sut.AuthorizeAccessAsync(user, [], new HashSet { "A" }); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.Success)); + } + + [Test] + public async Task Can_Authorize_Root_Access_By_Path() + { + // Arrange + var user = CreateUser(); + + SetupPermissions(user, Constants.System.RootString, ["A"]); + + // Act + var result = await _sut.AuthorizeRootAccessAsync(user, "A"); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.Success)); + } + + [Test] + public async Task Cannot_Authorize_Root_Access_By_Path() + { + // Arrange + var user = CreateUser(startContentId: 1234); + + _entityServiceMock + .Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) + .Returns([CreateTreeEntityPath(Guid.NewGuid(), 1234, "-1,1234")]); + + // Act + var result = await _sut.AuthorizeRootAccessAsync(user, "A"); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.UnauthorizedMissingRootAccess)); + } + + [Test] + public async Task Can_Authorize_Root_Access_By_Permission() + { + // Arrange + var user = CreateUser(); + + SetupPermissions(user, Constants.System.RootString, ["A"]); + + // Act + var result = await _sut.AuthorizeRootAccessAsync(user, "A"); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.Success)); + } + + [Test] + public async Task Cannot_Authorize_Root_Access_By_Permission() + { + // Arrange + var user = CreateUser(); + + SetupPermissions(user, Constants.System.RootString, ["A"]); + + // Act + var result = await _sut.AuthorizeRootAccessAsync(user, "B"); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.UnauthorizedMissingPermissionAccess)); + } + + [Test] + public async Task Can_Authorize_Bin_Access_By_Path() + { + // Arrange + var user = CreateUser(); + + SetupPermissions(user, Constants.System.RecycleBinContentString, ["A"]); + + // Act + var result = await _sut.AuthorizeBinAccessAsync(user, "A"); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.Success)); + } + + [Test] + public async Task Cannot_Authorize_Bin_Access_By_Path() + { + // Arrange + var user = CreateUser(startContentId: 1234); + + _entityServiceMock + .Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) + .Returns([CreateTreeEntityPath(Guid.NewGuid(), 1234, "-1,1234")]); + + // Act + var result = await _sut.AuthorizeBinAccessAsync(user, "A"); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.UnauthorizedMissingBinAccess)); + } + + [Test] + public async Task Can_Authorize_Bin_Access_By_Permission() + { + // Arrange + var user = CreateUser(); + + SetupPermissions(user, Constants.System.RecycleBinContentString, ["A"]); + + // Act + var result = await _sut.AuthorizeBinAccessAsync(user, "A"); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.Success)); + } + + [Test] + public async Task Cannot_Authorize_Bin_Access_By_Permission() + { + // Arrange + var user = CreateUser(); + + SetupPermissions(user, Constants.System.RecycleBinContentString, ["A"]); + + // Act + var result = await _sut.AuthorizeBinAccessAsync(user, "B"); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.UnauthorizedMissingPermissionAccess)); + } + + [Test] + public async Task Can_Authorize_Culture_Access_When_Group_Has_All_Languages() + { + // Arrange + var user = CreateUserWithAllLanguageAccess(); + + // Act + var result = await _sut.AuthorizeCultureAccessAsync(user, new HashSet { "en-US", "da-DK" }); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.Success)); + } + + [Test] + public async Task Can_Authorize_Culture_Access_When_User_Has_Language() + { + // Arrange + var languageId = 1; + var user = CreateUserWithLanguageAccess(languageId); + + _languageServiceMock + .Setup(x => x.GetIsoCodesByIdsAsync(It.Is>(ids => ids.Contains(languageId)))) + .ReturnsAsync(["en-US"]); + + // Act + var result = await _sut.AuthorizeCultureAccessAsync(user, new HashSet { "en-US" }); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.Success)); + } + + [Test] + public async Task Cannot_Authorize_Culture_Access_When_User_Lacks_Language() + { + // Arrange + var languageId = 1; + var user = CreateUserWithLanguageAccess(languageId); + + _languageServiceMock + .Setup(x => x.GetIsoCodesByIdsAsync(It.Is>(ids => ids.Contains(languageId)))) + .ReturnsAsync(["en-US"]); + + // Act + var result = await _sut.AuthorizeCultureAccessAsync(user, new HashSet { "da-DK" }); + + // Assert + Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.UnauthorizedMissingCulture)); + } + + private static IUser CreateUser(int id = 0, int? startContentId = null, bool withUserGroup = true) + { + var builder = new UserBuilder() + .WithId(id) + .WithStartContentIds(startContentId.HasValue ? [startContentId.Value] : []); + + if (withUserGroup) + { + builder = builder + .AddUserGroup() + .WithId(1) + .WithName("admin") + .WithAlias("admin") + .Done(); + } + + return builder.Build(); + } + + private static IUser CreateUserWithAllLanguageAccess() + { + var userGroupMock = new Mock(); + userGroupMock.Setup(x => x.HasAccessToAllLanguages).Returns(true); + userGroupMock.Setup(x => x.AllowedLanguages).Returns(new HashSet()); + userGroupMock.Setup(x => x.StartContentId).Returns(-1); + userGroupMock.Setup(x => x.StartMediaId).Returns(-1); + + var userMock = new Mock(); + userMock.Setup(x => x.Groups).Returns([userGroupMock.Object]); + return userMock.Object; + } + + private static IUser CreateUserWithLanguageAccess(int languageId) + { + var userGroupMock = new Mock(); + userGroupMock.Setup(x => x.HasAccessToAllLanguages).Returns(false); + userGroupMock.Setup(x => x.AllowedLanguages).Returns(new HashSet { languageId }); + userGroupMock.Setup(x => x.StartContentId).Returns(-1); + userGroupMock.Setup(x => x.StartMediaId).Returns(-1); + + var userMock = new Mock(); + userMock.Setup(x => x.Groups).Returns([userGroupMock.Object]); + return userMock.Object; + } + + private void SetupPermissions(IUser user, string path, string[] assignedPermissions) + { + var permissions = new EntityPermissionCollection { new(9876, 1234, assignedPermissions.ToHashSet()) }; + var permissionSet = new EntityPermissionSet(1234, permissions); + _userServiceMock.Setup(x => x.GetPermissionsForPath(user, path)).Returns(permissionSet); + } + + private static TreeEntityPath CreateTreeEntityPath(Guid key, int id, string path) + => Mock.Of(e => e.Key == key && e.Id == id && e.Path == path); +} From 80f08f7d6956e779d80faf0751edabe5d001dd56 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 8 Apr 2026 08:59:17 +0200 Subject: [PATCH 2/3] Addressed code review feedback. --- .../Services/ContentPermissionServiceTests.cs | 38 ++----------------- 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentPermissionServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentPermissionServiceTests.cs index b2f20be8800c..411607907bd6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentPermissionServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentPermissionServiceTests.cs @@ -48,7 +48,7 @@ public async Task Can_Authorize_Access_By_Path() _entityServiceMock .Setup(x => x.GetAllPaths(UmbracoObjectTypes.Document, new[] { contentKey })) - .Returns([CreateTreeEntityPath(contentKey, 1234, "-1,1234,5678")]); + .Returns([CreateTreeEntityPath(contentKey, 5678, "-1,1234,5678")]); SetupPermissions(user, "-1,1234,5678", ["A"]); @@ -86,7 +86,7 @@ public async Task Cannot_Authorize_Access_Without_Path_Access() _entityServiceMock .Setup(x => x.GetAllPaths(UmbracoObjectTypes.Document, new[] { contentKey })) - .Returns([CreateTreeEntityPath(contentKey, 1234, "-1,1234,5678")]); + .Returns([CreateTreeEntityPath(contentKey, 5678, "-1,1234,5678")]); _entityServiceMock .Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) @@ -108,7 +108,7 @@ public async Task Cannot_Authorize_Access_Without_Required_Permission() _entityServiceMock .Setup(x => x.GetAllPaths(UmbracoObjectTypes.Document, new[] { contentKey })) - .Returns([CreateTreeEntityPath(contentKey, 1234, "-1,1234,5678")]); + .Returns([CreateTreeEntityPath(contentKey, 5678, "-1,1234,5678")]); SetupPermissions(user, "-1,1234,5678", ["A", "B", "C"]); @@ -128,7 +128,7 @@ public async Task Can_Authorize_Access_With_Required_Permission() _entityServiceMock .Setup(x => x.GetAllPaths(UmbracoObjectTypes.Document, new[] { contentKey })) - .Returns([CreateTreeEntityPath(contentKey, 1234, "-1,1234,5678")]); + .Returns([CreateTreeEntityPath(contentKey, 5678, "-1,1234,5678")]); SetupPermissions(user, "-1,1234,5678", ["A", "F", "C"]); @@ -184,21 +184,6 @@ public async Task Cannot_Authorize_Root_Access_By_Path() Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.UnauthorizedMissingRootAccess)); } - [Test] - public async Task Can_Authorize_Root_Access_By_Permission() - { - // Arrange - var user = CreateUser(); - - SetupPermissions(user, Constants.System.RootString, ["A"]); - - // Act - var result = await _sut.AuthorizeRootAccessAsync(user, "A"); - - // Assert - Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.Success)); - } - [Test] public async Task Cannot_Authorize_Root_Access_By_Permission() { @@ -246,21 +231,6 @@ public async Task Cannot_Authorize_Bin_Access_By_Path() Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.UnauthorizedMissingBinAccess)); } - [Test] - public async Task Can_Authorize_Bin_Access_By_Permission() - { - // Arrange - var user = CreateUser(); - - SetupPermissions(user, Constants.System.RecycleBinContentString, ["A"]); - - // Act - var result = await _sut.AuthorizeBinAccessAsync(user, "A"); - - // Assert - Assert.That(result, Is.EqualTo(ContentAuthorizationStatus.Success)); - } - [Test] public async Task Cannot_Authorize_Bin_Access_By_Permission() { From 7666ed3a32ca3cce5c8709977fb9b7f655676b03 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 8 Apr 2026 14:54:39 +0200 Subject: [PATCH 3/3] Used constants for IDs and paths in tests. --- .../Services/ContentPermissionServiceTests.cs | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentPermissionServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentPermissionServiceTests.cs index 411607907bd6..71c7ec63d60e 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentPermissionServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentPermissionServiceTests.cs @@ -18,6 +18,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services; [TestFixture] public class ContentPermissionServiceTests { + private const int ContentNodeId = 5678; + private const string ContentNodePath = "-1,1234,5678"; + private const int UserStartNodeId = 1234; + private const string UserStartNodePath = "-1,1234"; + private const int UnrelatedStartNodeId = 9876; + private Mock _contentServiceMock; private Mock _entityServiceMock; private Mock _userServiceMock; @@ -48,9 +54,9 @@ public async Task Can_Authorize_Access_By_Path() _entityServiceMock .Setup(x => x.GetAllPaths(UmbracoObjectTypes.Document, new[] { contentKey })) - .Returns([CreateTreeEntityPath(contentKey, 5678, "-1,1234,5678")]); + .Returns([CreateTreeEntityPath(contentKey, ContentNodeId, ContentNodePath)]); - SetupPermissions(user, "-1,1234,5678", ["A"]); + SetupPermissions(user, ContentNodePath, ["A"]); // Act var result = await _sut.AuthorizeAccessAsync(user, contentKey, "A"); @@ -82,15 +88,15 @@ public async Task Cannot_Authorize_Access_Without_Path_Access() { // Arrange var contentKey = Guid.NewGuid(); - var user = CreateUser(startContentId: 9876); + var user = CreateUser(startContentId: UnrelatedStartNodeId); _entityServiceMock .Setup(x => x.GetAllPaths(UmbracoObjectTypes.Document, new[] { contentKey })) - .Returns([CreateTreeEntityPath(contentKey, 5678, "-1,1234,5678")]); + .Returns([CreateTreeEntityPath(contentKey, ContentNodeId, ContentNodePath)]); _entityServiceMock .Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) - .Returns([CreateTreeEntityPath(Guid.NewGuid(), 9876, "-1,9876")]); + .Returns([CreateTreeEntityPath(Guid.NewGuid(), UnrelatedStartNodeId, $"-1,{UnrelatedStartNodeId}")]); // Act var result = await _sut.AuthorizeAccessAsync(user, contentKey, "A"); @@ -108,9 +114,9 @@ public async Task Cannot_Authorize_Access_Without_Required_Permission() _entityServiceMock .Setup(x => x.GetAllPaths(UmbracoObjectTypes.Document, new[] { contentKey })) - .Returns([CreateTreeEntityPath(contentKey, 5678, "-1,1234,5678")]); + .Returns([CreateTreeEntityPath(contentKey, ContentNodeId, ContentNodePath)]); - SetupPermissions(user, "-1,1234,5678", ["A", "B", "C"]); + SetupPermissions(user, ContentNodePath, ["A", "B", "C"]); // Act var result = await _sut.AuthorizeAccessAsync(user, contentKey, "F"); @@ -128,9 +134,9 @@ public async Task Can_Authorize_Access_With_Required_Permission() _entityServiceMock .Setup(x => x.GetAllPaths(UmbracoObjectTypes.Document, new[] { contentKey })) - .Returns([CreateTreeEntityPath(contentKey, 5678, "-1,1234,5678")]); + .Returns([CreateTreeEntityPath(contentKey, ContentNodeId, ContentNodePath)]); - SetupPermissions(user, "-1,1234,5678", ["A", "F", "C"]); + SetupPermissions(user, ContentNodePath, ["A", "F", "C"]); // Act var result = await _sut.AuthorizeAccessAsync(user, contentKey, "F"); @@ -171,11 +177,11 @@ public async Task Can_Authorize_Root_Access_By_Path() public async Task Cannot_Authorize_Root_Access_By_Path() { // Arrange - var user = CreateUser(startContentId: 1234); + var user = CreateUser(startContentId: UserStartNodeId); _entityServiceMock .Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) - .Returns([CreateTreeEntityPath(Guid.NewGuid(), 1234, "-1,1234")]); + .Returns([CreateTreeEntityPath(Guid.NewGuid(), UserStartNodeId, UserStartNodePath)]); // Act var result = await _sut.AuthorizeRootAccessAsync(user, "A"); @@ -218,11 +224,11 @@ public async Task Can_Authorize_Bin_Access_By_Path() public async Task Cannot_Authorize_Bin_Access_By_Path() { // Arrange - var user = CreateUser(startContentId: 1234); + var user = CreateUser(startContentId: UserStartNodeId); _entityServiceMock .Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) - .Returns([CreateTreeEntityPath(Guid.NewGuid(), 1234, "-1,1234")]); + .Returns([CreateTreeEntityPath(Guid.NewGuid(), UserStartNodeId, UserStartNodePath)]); // Act var result = await _sut.AuthorizeBinAccessAsync(user, "A");