diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceRefactoringTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceRefactoringTests.cs index 874d4b909c..b5df7e6a90 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceRefactoringTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceRefactoringTests.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; @@ -1037,6 +1038,193 @@ internal sealed class ContentServiceRefactoringTests : UmbracoIntegrationTestWit #endregion + #region Phase 8 - Exposed Interface Methods Tests + + /// + /// Phase 8 Test: Verifies PerformMoveLocked returns non-null collection. + /// + [Test] + public void PerformMoveLocked_ReturnsNonNullCollection() + { + // Arrange + var moveService = GetRequiredService(); + var content = ContentBuilder.CreateSimpleContent(ContentType, "MoveTest", -1); + ContentService.Save(content); + + // Create a destination parent + var destination = ContentBuilder.CreateSimpleContent(ContentType, "Destination", -1); + ContentService.Save(destination); + + // Act + IReadOnlyCollection<(IContent Content, string OriginalPath)> result; + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + result = moveService.PerformMoveLocked(content, destination.Id, null, Constants.Security.SuperUserId, null); + scope.Complete(); + } + + // Assert + Assert.That(result, Is.Not.Null, "PerformMoveLocked should return non-null collection"); + } + + /// + /// Phase 8 Test: Verifies PerformMoveLocked includes moved item in returned collection. + /// + [Test] + public void PerformMoveLocked_IncludesMovedItemInCollection() + { + // Arrange + var moveService = GetRequiredService(); + var content = ContentBuilder.CreateSimpleContent(ContentType, "MoveItem", -1); + ContentService.Save(content); + var contentId = content.Id; + + var destination = ContentBuilder.CreateSimpleContent(ContentType, "Destination", -1); + ContentService.Save(destination); + var destinationId = destination.Id; + + // Act + IReadOnlyCollection<(IContent Content, string OriginalPath)> result; + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + result = moveService.PerformMoveLocked(content, destinationId, null, Constants.Security.SuperUserId, null); + scope.Complete(); + } + + // Assert + Assert.That(result.Count, Is.GreaterThan(0), "Should return at least one move event"); + Assert.That(result.Any(e => e.Content.Id == contentId), Is.True, + "Moved item should be included in returned collection"); + } + + /// + /// Phase 8 Test: Verifies PerformMoveLocked handles nested hierarchy correctly. + /// + [Test] + public void PerformMoveLocked_HandlesNestedHierarchy() + { + // Arrange + var moveService = GetRequiredService(); + + // Create parent with child + var parent = ContentBuilder.CreateSimpleContent(ContentType, "ParentToMove", -1); + ContentService.Save(parent); + + var child = ContentBuilder.CreateSimpleContent(ContentType, "Child", parent.Id); + ContentService.Save(child); + + // Create destination + var destination = ContentBuilder.CreateSimpleContent(ContentType, "Destination", -1); + ContentService.Save(destination); + + // Act + IReadOnlyCollection<(IContent Content, string OriginalPath)> result; + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + result = moveService.PerformMoveLocked(parent, destination.Id, null, Constants.Security.SuperUserId, null); + scope.Complete(); + } + + // Assert + Assert.That(result.Count, Is.GreaterThan(0), "Should return move events for hierarchy"); + + // Verify parent was moved + var movedParent = ContentService.GetById(parent.Id); + Assert.That(movedParent!.ParentId, Is.EqualTo(destination.Id), "Parent should be moved to destination"); + } + + /// + /// Phase 8 Test: Verifies DeleteLocked handles content with no descendants. + /// + [Test] + public void DeleteLocked_HandlesEmptyTree() + { + // Arrange + var crudService = GetRequiredService(); + var content = ContentBuilder.CreateSimpleContent(ContentType, "DeleteTest", -1); + ContentService.Save(content); + var contentId = content.Id; + + // Act & Assert - Should not throw + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + var eventMessages = new EventMessages(); + Assert.DoesNotThrow(() => crudService.DeleteLocked(scope, content, eventMessages)); + scope.Complete(); + } + + // Verify deletion + Assert.That(ContentService.GetById(contentId), Is.Null, "Content should be deleted"); + } + + /// + /// Phase 8 Test: Verifies DeleteLocked handles content with descendants. + /// + [Test] + public void DeleteLocked_HandlesLargeTree() + { + // Arrange + var crudService = GetRequiredService(); + + // Create parent with multiple children + var parent = ContentBuilder.CreateSimpleContent(ContentType, "ParentToDelete", -1); + ContentService.Save(parent); + + var child1 = ContentBuilder.CreateSimpleContent(ContentType, "Child1", parent.Id); + var child2 = ContentBuilder.CreateSimpleContent(ContentType, "Child2", parent.Id); + var child3 = ContentBuilder.CreateSimpleContent(ContentType, "Child3", parent.Id); + ContentService.Save(child1); + ContentService.Save(child2); + ContentService.Save(child3); + + var parentId = parent.Id; + var child1Id = child1.Id; + var child2Id = child2.Id; + var child3Id = child3.Id; + + // Act + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + var eventMessages = new EventMessages(); + crudService.DeleteLocked(scope, parent, eventMessages); + scope.Complete(); + } + + // Assert - All should be deleted + Assert.That(ContentService.GetById(parentId), Is.Null, "Parent should be deleted"); + Assert.That(ContentService.GetById(child1Id), Is.Null, "Child1 should be deleted"); + Assert.That(ContentService.GetById(child2Id), Is.Null, "Child2 should be deleted"); + Assert.That(ContentService.GetById(child3Id), Is.Null, "Child3 should be deleted"); + } + + /// + /// Phase 8 Test: Verifies DeleteLocked throws exception for null content. + /// Note: Current implementation throws NullReferenceException rather than ArgumentNullException. + /// This test documents the actual behavior. + /// + [Test] + public void DeleteLocked_ThrowsForNullContent() + { + // Arrange + var crudService = GetRequiredService(); + + // Act & Assert + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + var eventMessages = new EventMessages(); + Assert.Throws(() => + crudService.DeleteLocked(scope, null!, eventMessages)); + } + } + + #endregion + /// /// Notification handler that tracks the order of notifications for test verification. ///