From 820094400aed3e8f5f9df5f95d1894ee5713ac94 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 15 Jul 2024 09:30:26 +0300 Subject: [PATCH 01/96] Tests --- .../Services/NavigationServiceTests.cs | 293 ++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs new file mode 100644 index 000000000000..d58e2fa735f3 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs @@ -0,0 +1,293 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public class NavigationServiceTests : UmbracoIntegrationTest +{ + private IContentTypeService ContentTypeService => GetRequiredService(); + + private IContentService ContentService => GetRequiredService(); + + private INavigationService NavigationService => GetRequiredService(); + + private Content Root { get; set; } + + private Content Child1 { get; set; } + + private Content Grandchild1 { get; set; } + + private Content Grandchild2 { get; set; } + + private Content Child2 { get; set; } + + private Content Grandchild3 { get; set; } + + private Content GreatGrandchild1 { get; set; } + + private Content Child3 { get; set; } + + private Content Grandchild4 { get; set; } + + [SetUp] + public async Task Setup() + { + // Root + // - Child 1 + // - Grandchild 1 + // - Grandchild 2 + // - Child 2 + // - Grandchild 3 + // - Great-grandchild 1 + // - Child 3 + // - Grandchild 4 + + // Doc Type + var 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); + + // Content + Root = ContentBuilder.CreateSimpleContent(contentType, "Root"); + Root.Key = new Guid("E48DD82A-7059-418E-9B82-CDD5205796CF"); + ContentService.Save(Root, Constants.Security.SuperUserId); + + Child1 = ContentBuilder.CreateSimpleContent(contentType, "Child 1", Root.Id); + Child1.Key = new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"); + ContentService.Save(Child1, Constants.Security.SuperUserId); + + Grandchild1 = ContentBuilder.CreateSimpleContent(contentType, "Grandchild 1", Child1.Id); + Grandchild1.Key = new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"); + ContentService.Save(Grandchild1, Constants.Security.SuperUserId); + + Grandchild2 = ContentBuilder.CreateSimpleContent(contentType, "Grandchild 2", Child1.Id); + Grandchild2.Key = new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"); + ContentService.Save(Grandchild2, Constants.Security.SuperUserId); + + Child2 = ContentBuilder.CreateSimpleContent(contentType, "Child 2", Root.Id); + Child2.Key = new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"); + ContentService.Save(Child2, Constants.Security.SuperUserId); + + Grandchild3 = ContentBuilder.CreateSimpleContent(contentType, "Grandchild 3", Child2.Id); + Grandchild3.Key = new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"); + ContentService.Save(Grandchild3, Constants.Security.SuperUserId); + + GreatGrandchild1 = ContentBuilder.CreateSimpleContent(contentType, "Great-grandchild 1", Grandchild3.Id); + GreatGrandchild1.Key = new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"); + ContentService.Save(GreatGrandchild1, Constants.Security.SuperUserId); + + Child3 = ContentBuilder.CreateSimpleContent(contentType, "Child 3", Root.Id); + Child3.Key = new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"); + ContentService.Save(Child3, Constants.Security.SuperUserId); + + Grandchild4 = ContentBuilder.CreateSimpleContent(contentType, "Grandchild 3", Child3.Id); + Grandchild4.Key = new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"); + ContentService.Save(Grandchild4, Constants.Security.SuperUserId); + } + + [Test] + public async Task Cannot_Get_Parent_From_Non_Existing_Content_Key() + { + // Act + var result = await NavigationService.GetParentKeyAsync(Guid.NewGuid()); + + // Assert + Assert.IsNull(result); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", null)] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", "60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", "D63C1621-C74A-4106-8587-817DEE5FB732")] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 3 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", "B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Grandchild 4 + public async Task Can_Get_Parent_From_Existing_Content_Key(Guid childKey, Guid? parentKey) + { + // Act + Guid? result = await NavigationService.GetParentKeyAsync(childKey); + + // Assert + Assert.Multiple(() => + { + if (parentKey is null) + { + Assert.IsNull(result); + } + else + { + Assert.IsNotNull(result); + Assert.AreEqual(parentKey, result); + } + }); + } + + [Test] + public async Task Cannot_Get_Children_From_Non_Existing_Content_Key() + { + // Act + IEnumerable result = await NavigationService.GetChildrenKeysAsync(Guid.NewGuid()); + + // Assert + Assert.IsEmpty(result); + } + + [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 async Task Can_Get_Children_From_Existing_Content_Key(Guid parentKey, int childrenCount) + { + // Act + IEnumerable result = await NavigationService.GetChildrenKeysAsync(parentKey); + + // Assert + Assert.AreEqual(childrenCount, result.Count()); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new[] { "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 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new string[0])] // Grandchild 1 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", new[] { "D63C1621-C74A-4106-8587-817DEE5FB732" })] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", new[] { "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new string[0])] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", new[] { "F381906C-223C-4466-80F7-B63B4EE073F8" })] // Child 3 + public async Task Can_Get_Children_From_Existing_Content_Key_In_Correct_Order(Guid parentKey, string[] children) + { + // Arrange + Guid[] expectedChildren = Array.ConvertAll(children, Guid.Parse); + + // Act + IEnumerable result = await NavigationService.GetChildrenKeysAsync(parentKey); + + // Assert + for (var i = 0; i < expectedChildren.Length; i++) + { + Assert.AreEqual(expectedChildren[i], result.ElementAt(i)); + } + } + + [Test] + public async Task Cannot_Get_Descendants_From_Non_Existing_Content_Key() + { + // Act + IEnumerable result = await NavigationService.GetDescendantsKeysAsync(Guid.NewGuid()); + + // Assert + Assert.IsEmpty(result); + } + + [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 async Task Can_Get_Descendants_From_Existing_Content_Key(Guid parentKey, int descendantsCount) + { + // Act + IEnumerable result = await NavigationService.GetDescendantsKeysAsync(parentKey); + + // Assert + Assert.AreEqual(descendantsCount, result.Count()); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new[] { "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" })] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", new[] { "E856AC03-C23E-4F63-9AA9-681B42A58573", "A1B1B217-B02F-4307-862C-A5E22DB729EB" })] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new string[0])] // Grandchild 1 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", new[] { "D63C1621-C74A-4106-8587-817DEE5FB732", "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", new[] { "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new string[0])] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", new[] { "F381906C-223C-4466-80F7-B63B4EE073F8" })] // Child 3 + public async Task Can_Get_Descendants_From_Existing_Content_Key_In_Correct_Order(Guid parentKey, string[] descendants) + { + // Arrange + Guid[] expectedDescendants = Array.ConvertAll(descendants, Guid.Parse); + + // Act + IEnumerable result = await NavigationService.GetDescendantsKeysAsync(parentKey); + + // Assert + for (var i = 0; i < expectedDescendants.Length; i++) + { + Assert.AreEqual(expectedDescendants[i], result.ElementAt(i)); + } + } + + [Test] + public async Task Cannot_Get_Ancestors_From_Non_Existing_Content_Key() + { + // Act + IEnumerable result = await NavigationService.GetAncestorsKeysAsync(Guid.NewGuid()); + + // Assert + Assert.IsEmpty(result); + } + + [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 async Task Can_Get_Ancestors_From_Existing_Content_Key(Guid childKey, int ancestorsCount) + { + // Act + IEnumerable result = await NavigationService.GetAncestorsKeysAsync(childKey); + + // Assert + Assert.AreEqual(ancestorsCount, result.Count()); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new string[0])] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", new[] { "E48DD82A-7059-418E-9B82-CDD5205796CF" })] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new[] { "C6173927-0C59-4778-825D-D7B9F45D8DDE", "E48DD82A-7059-418E-9B82-CDD5205796CF" })] // Grandchild 1 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new[] { "D63C1621-C74A-4106-8587-817DEE5FB732", "60E0E5C4-084E-4144-A560-7393BEAD2E96", "E48DD82A-7059-418E-9B82-CDD5205796CF" })] // Great-grandchild 1 + public async Task Can_Get_Ancestors_From_Existing_Content_Key_In_Correct_Order(Guid childKey, string[] ancestors) + { + // Arrange + Guid[] expectedAncestors = Array.ConvertAll(ancestors, Guid.Parse); + + // Act + IEnumerable result = await NavigationService.GetAncestorsKeysAsync(childKey); + + // Assert + for (var i = 0; i < expectedAncestors.Length; i++) + { + Assert.AreEqual(expectedAncestors[i], result.ElementAt(i)); + } + } +} From 14b02a5b0cd6901c6dde613dd0409f296405d66c Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 15 Jul 2024 09:35:33 +0300 Subject: [PATCH 02/96] Remove props and use local vars --- .../Services/NavigationServiceTests.cs | 74 +++++++------------ 1 file changed, 28 insertions(+), 46 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs index d58e2fa735f3..131a12ab45c2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs @@ -19,24 +19,6 @@ public class NavigationServiceTests : UmbracoIntegrationTest private INavigationService NavigationService => GetRequiredService(); - private Content Root { get; set; } - - private Content Child1 { get; set; } - - private Content Grandchild1 { get; set; } - - private Content Grandchild2 { get; set; } - - private Content Child2 { get; set; } - - private Content Grandchild3 { get; set; } - - private Content GreatGrandchild1 { get; set; } - - private Content Child3 { get; set; } - - private Content Grandchild4 { get; set; } - [SetUp] public async Task Setup() { @@ -59,48 +41,48 @@ public async Task Setup() await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); // Content - Root = ContentBuilder.CreateSimpleContent(contentType, "Root"); - Root.Key = new Guid("E48DD82A-7059-418E-9B82-CDD5205796CF"); - ContentService.Save(Root, Constants.Security.SuperUserId); + Content root = ContentBuilder.CreateSimpleContent(contentType, "Root"); + root.Key = new Guid("E48DD82A-7059-418E-9B82-CDD5205796CF"); + ContentService.Save(root, Constants.Security.SuperUserId); - Child1 = ContentBuilder.CreateSimpleContent(contentType, "Child 1", Root.Id); - Child1.Key = new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"); - ContentService.Save(Child1, Constants.Security.SuperUserId); + Content child1 = ContentBuilder.CreateSimpleContent(contentType, "Child 1", root.Id); + child1.Key = new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"); + ContentService.Save(child1, Constants.Security.SuperUserId); - Grandchild1 = ContentBuilder.CreateSimpleContent(contentType, "Grandchild 1", Child1.Id); - Grandchild1.Key = new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"); - ContentService.Save(Grandchild1, Constants.Security.SuperUserId); + Content grandchild1 = ContentBuilder.CreateSimpleContent(contentType, "Grandchild 1", child1.Id); + grandchild1.Key = new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"); + ContentService.Save(grandchild1, Constants.Security.SuperUserId); - Grandchild2 = ContentBuilder.CreateSimpleContent(contentType, "Grandchild 2", Child1.Id); - Grandchild2.Key = new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"); - ContentService.Save(Grandchild2, Constants.Security.SuperUserId); + Content grandchild2 = ContentBuilder.CreateSimpleContent(contentType, "Grandchild 2", child1.Id); + grandchild2.Key = new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"); + ContentService.Save(grandchild2, Constants.Security.SuperUserId); - Child2 = ContentBuilder.CreateSimpleContent(contentType, "Child 2", Root.Id); - Child2.Key = new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"); - ContentService.Save(Child2, Constants.Security.SuperUserId); + Content child2 = ContentBuilder.CreateSimpleContent(contentType, "Child 2", root.Id); + child2.Key = new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"); + ContentService.Save(child2, Constants.Security.SuperUserId); - Grandchild3 = ContentBuilder.CreateSimpleContent(contentType, "Grandchild 3", Child2.Id); - Grandchild3.Key = new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"); - ContentService.Save(Grandchild3, Constants.Security.SuperUserId); + Content grandchild3 = ContentBuilder.CreateSimpleContent(contentType, "Grandchild 3", child2.Id); + grandchild3.Key = new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"); + ContentService.Save(grandchild3, Constants.Security.SuperUserId); - GreatGrandchild1 = ContentBuilder.CreateSimpleContent(contentType, "Great-grandchild 1", Grandchild3.Id); - GreatGrandchild1.Key = new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"); - ContentService.Save(GreatGrandchild1, Constants.Security.SuperUserId); + Content greatGrandchild1 = ContentBuilder.CreateSimpleContent(contentType, "Great-grandchild 1", grandchild3.Id); + greatGrandchild1.Key = new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"); + ContentService.Save(greatGrandchild1, Constants.Security.SuperUserId); - Child3 = ContentBuilder.CreateSimpleContent(contentType, "Child 3", Root.Id); - Child3.Key = new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"); - ContentService.Save(Child3, Constants.Security.SuperUserId); + Content child3 = ContentBuilder.CreateSimpleContent(contentType, "Child 3", root.Id); + child3.Key = new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"); + ContentService.Save(child3, Constants.Security.SuperUserId); - Grandchild4 = ContentBuilder.CreateSimpleContent(contentType, "Grandchild 3", Child3.Id); - Grandchild4.Key = new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"); - ContentService.Save(Grandchild4, Constants.Security.SuperUserId); + Content grandchild4 = ContentBuilder.CreateSimpleContent(contentType, "Grandchild 3", child3.Id); + grandchild4.Key = new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"); + ContentService.Save(grandchild4, Constants.Security.SuperUserId); } [Test] public async Task Cannot_Get_Parent_From_Non_Existing_Content_Key() { // Act - var result = await NavigationService.GetParentKeyAsync(Guid.NewGuid()); + Guid? result = await NavigationService.GetParentKeyAsync(Guid.NewGuid()); // Assert Assert.IsNull(result); From e3135e76e2acc1ff7933e4386d913571c73ed755 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 24 Jul 2024 17:36:44 +0200 Subject: [PATCH 03/96] Adding preliminary navigation service and content implementation --- .../Navigation/ContentNavigationService.cs | 243 ++++++++++++++++++ .../Services/Navigation/INavigationService.cs | 24 ++ 2 files changed, 267 insertions(+) create mode 100644 src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/INavigationService.cs diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs new file mode 100644 index 000000000000..a10d86768a34 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs @@ -0,0 +1,243 @@ +using Umbraco.Cms.Core.Models.Navigation; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.Services.Navigation; + +internal class ContentNavigationService : INavigationService +{ + private readonly ICoreScopeProvider _coreScopeProvider; + private readonly INavigationRepository _navigationRepository; + private Dictionary _navigationStructure = new(); + + public ContentNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) + { + _coreScopeProvider = coreScopeProvider; + _navigationRepository = navigationRepository; + } + + public async Task RebuildAsync() + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); + scope.ReadLock(Constants.Locks.ContentTree); + _navigationStructure = _navigationRepository.GetContentNodesByObjectType(Constants.ObjectTypes.Document); + } + + public async Task GetParentKeyAsync(Guid childKey) + => _navigationStructure.TryGetValue(childKey, out var childNode) ? + childNode.Parent?.Key : + null; + + public async Task> GetChildrenKeysAsync(Guid parentKey) + => _navigationStructure.TryGetValue(parentKey, out var parentNode) ? + parentNode.Children.Select(child => child.Key) : + []; + + public async Task> GetDescendantsKeysAsync(Guid parentKey) + { + var descendants = new List(); + + if (_navigationStructure.TryGetValue(parentKey, out var parentNode)) + { + GetDescendantsRecursively(parentNode, descendants); + } + + return descendants; + } + + public async Task> GetAncestorsKeysAsync(Guid childKey) + { + var ancestors = new List(); + + if (_navigationStructure.TryGetValue(childKey, out var childNode) is false) + { + return ancestors; + } + + while (childNode?.Parent is not null) + { + ancestors.Add(childNode.Parent.Key); + childNode = childNode.Parent; + } + + return ancestors; + } + + public async Task> GetSiblingsKeysAsync(Guid key) + { + var siblings = new List(); + + if (_navigationStructure.TryGetValue(key, out NavigationNode? node) is false) + { + return siblings; + } + + if (node.Parent is null) + { + // To find siblings of node at root, we need to iterate over all items and add those whose Parent is null + siblings = _navigationStructure + .Where(kv => kv.Value.Parent is null && kv.Key != key) + .Select(kv => kv.Key) + .ToList(); + return siblings; + } + + IEnumerable childrenKeys = await GetChildrenKeysAsync(node.Parent.Key); + + // Filter out the node itself to get its siblings + siblings = childrenKeys.Where(childKey => childKey != key).ToList(); + + return siblings; + } + + public bool Remove(Guid key) + { + if (_navigationStructure.TryGetValue(key, out NavigationNode? nodeToRemove) is false) + { + return false; // Node doesn't exist + } + + // Remove the node from its parent's children list + if (nodeToRemove.Parent is not null && _navigationStructure.TryGetValue(nodeToRemove.Parent.Key, out var parentNode)) + { + parentNode.Children.Remove(nodeToRemove); + } + + // Recursively remove all descendants + RemoveDescendantsRecursively(nodeToRemove); + + // Remove the node itself + _navigationStructure.Remove(key); + + return true; + } + + public bool Add(Guid key, Guid? parentKey = null) + { + if (_navigationStructure.ContainsKey(key)) + { + return false; // Node with this key already exists + } + + NavigationNode? parentNode = null; + if (parentKey.HasValue) + { + if (_navigationStructure.TryGetValue(parentKey.Value, out parentNode) is false) + { + return false; // Parent node doesn't exist + } + } + + var newNode = new NavigationNode(key); + _navigationStructure[key] = newNode; + + parentNode?.AddChild(newNode); + + return true; + } + + public bool Copy(Guid sourceKey, out Guid copiedNodeKey, Guid? targetParentKey = null) + { + copiedNodeKey = Guid.Empty; // Default value + + if (_navigationStructure.TryGetValue(sourceKey, out var sourceNode) is false) + { + return false; // Source doesn't exist + } + + NavigationNode? targetParentNode = null; + if (targetParentKey.HasValue && _navigationStructure.TryGetValue(targetParentKey.Value, out targetParentNode) is false) + { + return false; // Target parent doesn't exist + } + + var newNodesMap = new Dictionary(); + CopyNodeHierarchyRecursively(sourceNode, targetParentNode, newNodesMap, out copiedNodeKey); + + foreach (var newNode in newNodesMap.Values) + { + _navigationStructure[newNode.Key] = newNode; + } + + return true; + } + + public bool Move(Guid nodeKey, Guid? targetParentKey = null) + { + if (_navigationStructure.TryGetValue(nodeKey, out var nodeToMove) is false) + { + return false; // Node doesn't exist + } + + if (nodeKey == targetParentKey) + { + return false; // Cannot move a node to itself + } + + NavigationNode? targetParentNode = null; + if (targetParentKey.HasValue && _navigationStructure.TryGetValue(targetParentKey.Value, out targetParentNode) is false) + { + return false; // Target parent doesn't exist + } + + // Remove the node from its current parent's children list + if (nodeToMove.Parent is not null && _navigationStructure.TryGetValue(nodeToMove.Parent.Key, out var currentParentNode)) + { + currentParentNode.Children.Remove(nodeToMove); + } + + // Create a new node with the same key, to update the parent + var newNode = new NavigationNode(nodeToMove.Key); + + // Set the new parent for the node (if parent node is null - the node is moved to root) + targetParentNode?.AddChild(newNode); + + // Copy children to the new node + CopyChildren(nodeToMove, newNode); + + _navigationStructure[nodeToMove.Key] = newNode; + + return true; + } + + private void GetDescendantsRecursively(NavigationNode node, List descendants) + { + foreach (NavigationNode child in node.Children) + { + descendants.Add(child.Key); + GetDescendantsRecursively(child, descendants); + } + } + + private void RemoveDescendantsRecursively(NavigationNode node) + { + foreach (NavigationNode child in node.Children) + { + RemoveDescendantsRecursively(child); + _navigationStructure.Remove(child.Key); + } + } + + private void CopyChildren(NavigationNode originalNode, NavigationNode newNode) + { + foreach (NavigationNode child in originalNode.Children) + { + newNode.AddChild(child); + } + } + + private void CopyNodeHierarchyRecursively(NavigationNode sourceNode, NavigationNode? newParent, Dictionary newNodesMap, out Guid topmostCopiedNodeKey) + { + topmostCopiedNodeKey = Guid.NewGuid(); // TODO: pass in the key or get it from the DB? + var newNode = new NavigationNode(topmostCopiedNodeKey); + + newParent?.AddChild(newNode); + + newNodesMap[topmostCopiedNodeKey] = newNode; + + foreach (NavigationNode child in sourceNode.Children) + { + CopyNodeHierarchyRecursively(child, newNode, newNodesMap, out _); + } + } +} diff --git a/src/Umbraco.Core/Services/Navigation/INavigationService.cs b/src/Umbraco.Core/Services/Navigation/INavigationService.cs new file mode 100644 index 000000000000..f56156da8756 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/INavigationService.cs @@ -0,0 +1,24 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface INavigationService +{ + Task GetParentKeyAsync(Guid childKey); + + Task> GetChildrenKeysAsync(Guid parentKey); + + Task> GetDescendantsKeysAsync(Guid parentKey); + + Task> GetAncestorsKeysAsync(Guid childKey); + + Task> GetSiblingsKeysAsync(Guid key); + + Task RebuildAsync(); + + bool Remove(Guid key); + + bool Add(Guid key, Guid? parentKey = null); + + bool Copy(Guid sourceKey, out Guid copiedNodeKey, Guid? targetParentKey = null); + + bool Move(Guid nodeKey, Guid? targetParentKey = null); +} From dc785c002f269dd26b4c32b1ef3f0c6d137e6252 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 24 Jul 2024 17:36:56 +0200 Subject: [PATCH 04/96] Adding preliminary unit tests --- .../Services/NavigationServiceTests.cs | 881 ++++++++++++++++++ 1 file changed, 881 insertions(+) create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs new file mode 100644 index 000000000000..475aa443b680 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs @@ -0,0 +1,881 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.Navigation; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services; + +[TestFixture] +public class NavigationServiceTests +{ + private INavigationService _navigationService; + + private Guid Root { get; set; } + + private Guid Child1 { get; set; } + + private Guid Grandchild1 { get; set; } + + private Guid Grandchild2 { get; set; } + + private Guid Child2 { get; set; } + + private Guid Grandchild3 { get; set; } + + private Guid GreatGrandchild1 { get; set; } + + private Guid Child3 { get; set; } + + private Guid Grandchild4 { get; set; } + + [SetUp] + public void Setup() + { + // Root + // - Child 1 + // - Grandchild 1 + // - Grandchild 2 + // - Child 2 + // - Grandchild 3 + // - Great-grandchild 1 + // - Child 3 + // - Grandchild 4 + + _navigationService = new ContentNavigationService( + 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); + } + + [Test] + public async Task Cannot_Get_Parent_From_Non_Existing_Content_Key() + { + // Act + Guid? result = await _navigationService.GetParentKeyAsync(Guid.NewGuid()); + + // Assert + Assert.IsNull(result); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", null)] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", "60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", "D63C1621-C74A-4106-8587-817DEE5FB732")] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 3 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", "B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Grandchild 4 + public async Task Can_Get_Parent_From_Existing_Content_Key(Guid childKey, Guid? parentKey) + { + // Act + Guid? result = await _navigationService.GetParentKeyAsync(childKey); + + // Assert + Assert.Multiple(() => + { + if (parentKey is null) + { + Assert.IsNull(result); + } + else + { + Assert.IsNotNull(result); + Assert.AreEqual(parentKey, result); + } + }); + } + + [Test] + public async Task Cannot_Get_Children_From_Non_Existing_Content_Key() + { + // Act + IEnumerable result = await _navigationService.GetChildrenKeysAsync(Guid.NewGuid()); + + // Assert + Assert.IsEmpty(result); + } + + [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 async Task Can_Get_Children_From_Existing_Content_Key(Guid parentKey, int childrenCount) + { + // Act + IEnumerable result = await _navigationService.GetChildrenKeysAsync(parentKey); + + // Assert + Assert.AreEqual(childrenCount, result.Count()); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", + new[] + { + "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 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new string[0])] // Grandchild 1 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", new[] { "D63C1621-C74A-4106-8587-817DEE5FB732" })] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", new[] { "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new string[0])] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", new[] { "F381906C-223C-4466-80F7-B63B4EE073F8" })] // Child 3 + public async Task Can_Get_Children_From_Existing_Content_Key_In_Correct_Order(Guid parentKey, string[] children) + { + // Arrange + Guid[] expectedChildren = Array.ConvertAll(children, Guid.Parse); + + // Act + IEnumerable result = await _navigationService.GetChildrenKeysAsync(parentKey); + + // Assert + for (var i = 0; i < expectedChildren.Length; i++) + { + Assert.AreEqual(expectedChildren[i], result.ElementAt(i)); + } + } + + [Test] + public async Task Cannot_Get_Descendants_From_Non_Existing_Content_Key() + { + // Act + IEnumerable result = await _navigationService.GetDescendantsKeysAsync(Guid.NewGuid()); + + // Assert + Assert.IsEmpty(result); + } + + [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 async Task Can_Get_Descendants_From_Existing_Content_Key(Guid parentKey, int descendantsCount) + { + // Act + IEnumerable result = await _navigationService.GetDescendantsKeysAsync(parentKey); + + // Assert + Assert.AreEqual(descendantsCount, result.Count()); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", + new[] + { + "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" + })] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", + new[] { "E856AC03-C23E-4F63-9AA9-681B42A58573", "A1B1B217-B02F-4307-862C-A5E22DB729EB" })] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new string[0])] // Grandchild 1 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", + new[] { "D63C1621-C74A-4106-8587-817DEE5FB732", "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", new[] { "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new string[0])] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", new[] { "F381906C-223C-4466-80F7-B63B4EE073F8" })] // Child 3 + public async Task Can_Get_Descendants_From_Existing_Content_Key_In_Correct_Order(Guid parentKey, string[] descendants) + { + // Arrange + Guid[] expectedDescendants = Array.ConvertAll(descendants, Guid.Parse); + + // Act + IEnumerable result = await _navigationService.GetDescendantsKeysAsync(parentKey); + + // Assert + for (var i = 0; i < expectedDescendants.Length; i++) + { + Assert.AreEqual(expectedDescendants[i], result.ElementAt(i)); + } + } + + [Test] + public async Task Cannot_Get_Ancestors_From_Non_Existing_Content_Key() + { + // Act + IEnumerable result = await _navigationService.GetAncestorsKeysAsync(Guid.NewGuid()); + + // Assert + Assert.IsEmpty(result); + } + + [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 async Task Can_Get_Ancestors_From_Existing_Content_Key(Guid childKey, int ancestorsCount) + { + // Act + IEnumerable result = await _navigationService.GetAncestorsKeysAsync(childKey); + + // Assert + Assert.AreEqual(ancestorsCount, result.Count()); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new string[0])] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", new[] { "E48DD82A-7059-418E-9B82-CDD5205796CF" })] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", + new[] { "C6173927-0C59-4778-825D-D7B9F45D8DDE", "E48DD82A-7059-418E-9B82-CDD5205796CF" })] // Grandchild 1 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", + new[] + { + "D63C1621-C74A-4106-8587-817DEE5FB732", "60E0E5C4-084E-4144-A560-7393BEAD2E96", + "E48DD82A-7059-418E-9B82-CDD5205796CF" + })] // Great-grandchild 1 + public async Task Can_Get_Ancestors_From_Existing_Content_Key_In_Correct_Order(Guid childKey, string[] ancestors) + { + // Arrange + Guid[] expectedAncestors = Array.ConvertAll(ancestors, Guid.Parse); + + // Act + IEnumerable result = await _navigationService.GetAncestorsKeysAsync(childKey); + + // Assert + for (var i = 0; i < expectedAncestors.Length; i++) + { + Assert.AreEqual(expectedAncestors[i], result.ElementAt(i)); + } + } + + [Test] + public async Task Cannot_Get_Siblings_Of_Non_Existing_Content_Key() + { + // Act + IEnumerable result = await _navigationService.GetSiblingsKeysAsync(Guid.NewGuid()); + + // Assert + Assert.IsEmpty(result); + } + + [Test] + public async Task Can_Get_Siblings_Of_Existing_Content_Key_Without_Self() + { + // Arrange + Guid nodeKey = Child1; + + // Act + IEnumerable result = await _navigationService.GetSiblingsKeysAsync(nodeKey); + + // Assert + Assert.Multiple(async () => + { + Assert.IsNotEmpty(result); + Assert.IsFalse(result.Contains(nodeKey)); + }); + } + + [Test] + public async Task Can_Get_Siblings_Of_Existing_Content_Key_At_Content_Root() + { + // Arrange + Guid anotherRoot = new Guid("716380B9-DAA9-4930-A461-95EF39EBAB41"); + _navigationService.Add(anotherRoot); + + // Act + IEnumerable result = await _navigationService.GetSiblingsKeysAsync(anotherRoot); + + // Assert + Assert.Multiple(() => + { + Assert.IsNotEmpty(result); + Assert.AreEqual(1, result.Count()); + Assert.AreEqual(Root, result.First()); + }); + } + + [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 async Task Can_Get_Siblings_Of_Existing_Content_Key(Guid key, int siblingsCount) + { + // Act + IEnumerable result = await _navigationService.GetSiblingsKeysAsync(key); + + // Assert + Assert.AreEqual(siblingsCount, result.Count()); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new string[0])] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", new[] { "60E0E5C4-084E-4144-A560-7393BEAD2E96", "B606E3FF-E070-4D46-8CB9-D31352029FDF" })] // Child 1 - Child 2, Child 3 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new[] { "A1B1B217-B02F-4307-862C-A5E22DB729EB" })] // Grandchild 1 - Grandchild 2 + public async Task Can_Get_Siblings_Of_Existing_Content_Key_In_Correct_Order(Guid childKey, string[] siblings) + { + // Arrange + Guid[] expectedSiblings = Array.ConvertAll(siblings, Guid.Parse); + + // Act + IEnumerable result = await _navigationService.GetSiblingsKeysAsync(childKey); + + // Assert + for (var i = 0; i < expectedSiblings.Length; i++) + { + Assert.AreEqual(expectedSiblings[i], result.ElementAt(i)); + } + } + + [Test] + public void Cannot_Remove_Node_With_Non_Existing_Content_Key() + { + // Act + var result = _navigationService.Remove(Guid.NewGuid()); + + // Assert + Assert.IsFalse(result); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 + public void Removing_Node_Removes_Its_Descendants_As_Well(Guid keyOfNodeToRemove) + { + // Act + var result = _navigationService.Remove(keyOfNodeToRemove); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + Assert.AreEqual(0, (await _navigationService.GetDescendantsKeysAsync(keyOfNodeToRemove)).Count()); + }); + } + + [Test] + public void Cannot_Add_Node_When_Parent_Does_Not_Exist() + { + // Arrange + var newNodeKey = Guid.NewGuid(); + var nonExistentParentKey = Guid.NewGuid(); + + // Act + var result = _navigationService.Add(newNodeKey, nonExistentParentKey); + + // Assert + Assert.IsFalse(result); + } + + [Test] + public void Cannot_Add_When_Node_With_The_Same_Key_Already_Exists() + { + // Act + var result = _navigationService.Add(Child1); + + // Assert + Assert.IsFalse(result); + } + + [Test] + public void Can_Add_Node_To_Content_Root() + { + // Arrange + var newNodeKey = Guid.NewGuid(); + + // Act + var result = _navigationService.Add(newNodeKey); // parentKey is null + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + Assert.AreEqual(null, await _navigationService.GetParentKeyAsync(newNodeKey)); + }); + } + + [Test] + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + public async Task Can_Add_Node_To_Parent(Guid parentKey) + { + // Arrange + var newNodeKey = Guid.NewGuid(); + var currentChildrenCount = (await _navigationService.GetChildrenKeysAsync(parentKey)).Count(); + + // Act + var result = _navigationService.Add(newNodeKey, parentKey); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + var newChildren = await _navigationService.GetChildrenKeysAsync(parentKey); + Assert.AreEqual(currentChildrenCount + 1, newChildren.Count()); + Assert.IsTrue(newChildren.Any(childKey => childKey == newNodeKey)); + }); + } + + [Test] + public void Cannot_Copy_When_Target_Parent_Does_Not_Exist() + { + // Arrange + Guid nodeToCopy = Child1; + var nonExistentTargetParentKey = Guid.NewGuid(); + + // Act + var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, nonExistentTargetParentKey); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.AreEqual(Guid.Empty, copiedNodeKey); + }); + } + + [Test] + public void Cannot_Copy_When_Source_Node_Does_Not_Exist() + { + // Arrange + var nonExistentSourceNodeKey = Guid.NewGuid(); + Guid targetParentKey = Grandchild1; + + // Act + var result = _navigationService.Copy(nonExistentSourceNodeKey, out Guid copiedNodeKey, targetParentKey); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.AreEqual(Guid.Empty, copiedNodeKey); + }); + } + + [Test] + public void Can_Copy_Node_To_Itself() + { + // Arrange + Guid nodeToCopy = GreatGrandchild1; + + // Act + var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreNotEqual(Guid.Empty, copiedNodeKey); + Assert.AreNotEqual(nodeToCopy, copiedNodeKey); + }); + } + + [Test] + public void Can_Copy_Node_To_Content_Root() + { + // Arrange + Guid nodeToCopy = Child1; + + // Act + var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey); // parentKey is null + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + Assert.AreNotEqual(Guid.Empty, copiedNodeKey); + Assert.AreNotEqual(nodeToCopy, copiedNodeKey); + + // Verify the copied node's parent is null (it's been copied to content root) + Guid? copiedNodeParentKey = await _navigationService.GetParentKeyAsync(copiedNodeKey); + Assert.IsNull(copiedNodeParentKey); + }); + } + + [Test] + public void Can_Copy_Node_To_Existing_Target_Parent() + { + // Arrange + Guid nodeToCopy = Grandchild4; + Guid targetParentKey = Child1; + + // Act + var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, targetParentKey); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + Assert.AreNotEqual(Guid.Empty, copiedNodeKey); + Assert.AreNotEqual(nodeToCopy, copiedNodeKey); + + // Verify the node is copied to the correct parent + Guid? copiedNodeParentKey = await _navigationService.GetParentKeyAsync(copiedNodeKey); + Assert.IsNotNull(copiedNodeParentKey); + Assert.AreEqual(targetParentKey, copiedNodeParentKey); + }); + } + + [Test] + public async Task Copying_Node_Does_Not_Update_Source_Node_Parent() + { + // Arrange + Guid nodeToCopy = Grandchild1; + Guid targetParentKey = Child3; + Guid? originalParentKey = await _navigationService.GetParentKeyAsync(nodeToCopy); + + // Act + var result = _navigationService.Copy(nodeToCopy, out _, targetParentKey); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + + // Verify that the original parent is still the same + Guid? currentParentKey = await _navigationService.GetParentKeyAsync(nodeToCopy); + Assert.AreEqual(originalParentKey, currentParentKey); + }); + } + + [Test] + public async Task Copied_Node_Is_Added_To_Its_New_Parent() + { + // Arrange + Guid nodeToCopy = Grandchild2; + Guid targetParentKey = Child2; + var targetParentChildrenCount = (await _navigationService.GetChildrenKeysAsync(targetParentKey)).Count(); + + // Act + var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, targetParentKey); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + + // Verify the node is added to its new parent's children list + IEnumerable children = await _navigationService.GetChildrenKeysAsync(targetParentKey); + CollectionAssert.Contains(children, copiedNodeKey); + Assert.AreEqual(targetParentChildrenCount + 1, children.Count()); + }); + } + + [Test] + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "60E0E5C4-084E-4144-A560-7393BEAD2E96", 0)] // Grandchild 2 to Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", null, 1)] // Grandchild 3 to content root + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "B606E3FF-E070-4D46-8CB9-D31352029FDF", 2)] // Child 2 to Child 3 + public void Copied_Node_Has_The_Same_Amount_Of_Descendants(Guid nodeToCopy, Guid? targetParentKey, int initialDescendantsCount) + { + // Act + var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, targetParentKey); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + + // Get the number of descendants of the copied node + var descendantsCountAfterCopy = (await _navigationService.GetDescendantsKeysAsync(copiedNodeKey)).Count(); + Assert.AreEqual(initialDescendantsCount, descendantsCountAfterCopy); + }); + } + + [Test] + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "E48DD82A-7059-418E-9B82-CDD5205796CF", 8)] // Grandchild 2 to Root + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", "B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Grandchild 3 to Child 3 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "F381906C-223C-4466-80F7-B63B4EE073F8", 0)] // Child 2 to Grandchild 4 + public async Task Number_Of_Target_Parent_Descendants_Updates_When_Copying_Node_With_Descendants(Guid nodeToCopy, Guid targetParentKey, int initialDescendantsCountOfTargetParent) + { + // Arrange + // Get the number of descendants of the node to copy + var descendantsCountOfNodeToCopy = (await _navigationService.GetDescendantsKeysAsync(nodeToCopy)).Count(); + + // Act + var result = _navigationService.Copy(nodeToCopy, out _, targetParentKey); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + + var updatedDescendantsCountOfTargetParent = (await _navigationService.GetDescendantsKeysAsync(targetParentKey)).Count(); + + // Verify the number of descendants of the target parent has increased by the number of descendants of the copied node plus the node itself + Assert.AreEqual(initialDescendantsCountOfTargetParent + descendantsCountOfNodeToCopy + 1, updatedDescendantsCountOfTargetParent); + }); + } + + [Test] + public async Task Copied_Node_Descendants_Have_Different_Keys_Than_Source_Node_Descendants() + { + // Arrange + Guid nodeToCopy = Child2; + Guid targetParentKey = Grandchild4; + IEnumerable sourceDescendants = await _navigationService.GetDescendantsKeysAsync(nodeToCopy); + + // Act + var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, targetParentKey); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + + // Get the descendants of the copied node + IEnumerable copiedDescendants = await _navigationService.GetDescendantsKeysAsync(copiedNodeKey); + + // Ensure all keys of the copied descendants are different from the source descendants + Assert.IsTrue(copiedDescendants.All(copiedDescendantKey => sourceDescendants.Contains(copiedDescendantKey) is false)); + }); + } + + [Test] + public void Cannot_Move_When_Target_Parent_Does_Not_Exist() + { + // Arrange + Guid nodeToMove = Child1; + var nonExistentTargetParentKey = Guid.NewGuid(); + + // Act + var result = _navigationService.Move(nodeToMove, nonExistentTargetParentKey); + + // Assert + Assert.IsFalse(result); + } + + [Test] + public void Cannot_Move_Node_That_Does_Not_Exist() + { + // Arrange + var nonExistentNodeKey = Guid.NewGuid(); + Guid targetParentKey = Child1; + + // Act + var result = _navigationService.Move(nonExistentNodeKey, targetParentKey); + + // Assert + Assert.IsFalse(result); + } + + [Test] + public void Cannot_Move_Node_To_Itself() + { + // Arrange + Guid nodeToMove = Child1; + + // Act + var result = _navigationService.Move(nodeToMove, nodeToMove); + + // Assert + Assert.IsFalse(result); + } + + [Test] + public void Can_Move_Node_To_Content_Root() + { + // Arrange + Guid nodeToMove = Child1; + + // Act + var result = _navigationService.Move(nodeToMove); // parentKey is null + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + + // Verify the node's new parent is null (moved to content root) + Guid? newParentKey = await _navigationService.GetParentKeyAsync(nodeToMove); + Assert.IsNull(newParentKey); + }); + } + + [Test] + public void Can_Move_Node_To_Existing_Target_Parent() + { + // Arrange + Guid nodeToMove = Grandchild4; + Guid targetParentKey = Child1; + + // Act + var result = _navigationService.Move(nodeToMove, targetParentKey); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + + // Verify the node's new parent is updated + Guid? parentKey = await _navigationService.GetParentKeyAsync(nodeToMove); + Assert.IsNotNull(parentKey); + Assert.AreEqual(targetParentKey, parentKey); + }); + } + + [Test] + public async Task Moved_Node_Has_Updated_Parent() + { + // Arrange + Guid nodeToMove = Grandchild1; + Guid targetParentKey = Child2; + Guid? oldParentKey = await _navigationService.GetParentKeyAsync(nodeToMove); + + // Act + var result = _navigationService.Move(nodeToMove, targetParentKey); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + + // Verify the node's new parent is updated + Guid? parentKey = await _navigationService.GetParentKeyAsync(nodeToMove); + Assert.IsNotNull(parentKey); + Assert.AreEqual(targetParentKey, parentKey); + + // Verify that the new parent is different from the old one + Assert.AreNotEqual(oldParentKey, targetParentKey); + }); + } + + [Test] + public async Task Moved_Node_Is_Removed_From_Its_Current_Parent() + { + // Arrange + Guid nodeToMove = Grandchild3; + Guid targetParentKey = Child3; + Guid? oldParentKey = await _navigationService.GetParentKeyAsync(nodeToMove); + var oldParentChildrenCount = (await _navigationService.GetChildrenKeysAsync(oldParentKey.Value)).Count(); + + // Act + var result = _navigationService.Move(nodeToMove, targetParentKey); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + + // Verify the node is removed from its old parent's children list + IEnumerable children = await _navigationService.GetChildrenKeysAsync(oldParentKey.Value); + CollectionAssert.DoesNotContain(children, nodeToMove); + Assert.AreEqual(oldParentChildrenCount - 1, children.Count()); + }); + } + + [Test] + public async Task Moved_Node_Is_Added_To_Its_New_Parent() + { + // Arrange + Guid nodeToMove = Grandchild2; + Guid targetParentKey = Child2; + var targetParentChildrenCount = (await _navigationService.GetChildrenKeysAsync(targetParentKey)).Count(); + + // Act + var result = _navigationService.Move(nodeToMove, targetParentKey); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + + // Verify the node is added to its new parent's children list + IEnumerable children = await _navigationService.GetChildrenKeysAsync(targetParentKey); + CollectionAssert.Contains(children, nodeToMove); + Assert.AreEqual(targetParentChildrenCount + 1, children.Count()); + }); + } + + [Test] + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "60E0E5C4-084E-4144-A560-7393BEAD2E96", 0)] // Grandchild 1 to Child 2 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", null, 1)] // Child 3 to content root + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "C6173927-0C59-4778-825D-D7B9F45D8DDE", 2)] // Child 2 to Child 1 + public void Moved_Node_Has_The_Same_Amount_Of_Descendants(Guid nodeToMove, Guid? targetParentKey, int initialDescendantsCount) + { + // Act + var result = _navigationService.Move(nodeToMove, targetParentKey); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + + // Verify that the number of descendants remain the same after moving the node + var descendantsCountAfterMove = (await _navigationService.GetDescendantsKeysAsync(nodeToMove)).Count(); + Assert.AreEqual(initialDescendantsCount, descendantsCountAfterMove); + }); + } + + [Test] + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "A1B1B217-B02F-4307-862C-A5E22DB729EB", 0)] // Child 3 to Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Child 2 to Child 3 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "60E0E5C4-084E-4144-A560-7393BEAD2E96", 2)] // Grandchild 1 to Child 2 + public async Task Number_Of_Target_Parent_Descendants_Updates_When_Moving_Node_With_Descendants(Guid nodeToMove, Guid targetParentKey, int initialDescendantsCountOfTargetParent) + { + // Arrange + // Get the number of descendants of the node to move + var descendantsCountOfNodeToMove = (await _navigationService.GetDescendantsKeysAsync(nodeToMove)).Count(); + + // Act + var result = _navigationService.Move(nodeToMove, targetParentKey); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result); + + var updatedDescendantsCountOfTargetParent = (await _navigationService.GetDescendantsKeysAsync(targetParentKey)).Count(); + + // Verify the number of descendants of the target parent has increased by the number of descendants of the moved node plus the node itself + Assert.AreEqual(initialDescendantsCountOfTargetParent + descendantsCountOfNodeToMove + 1, updatedDescendantsCountOfTargetParent); + }); + } +} From 2f7761289a7e1b185f94d2209f8529f4aa96bf17 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 24 Jul 2024 18:13:20 +0200 Subject: [PATCH 05/96] Change from async methods --- .../Navigation/ContentNavigationService.cs | 26 +-- .../Services/Navigation/INavigationService.cs | 12 +- .../Services/NavigationServiceTests.cs | 183 +++++++++--------- 3 files changed, 111 insertions(+), 110 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs index a10d86768a34..a2ef988f1d8a 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs @@ -23,17 +23,17 @@ public async Task RebuildAsync() _navigationStructure = _navigationRepository.GetContentNodesByObjectType(Constants.ObjectTypes.Document); } - public async Task GetParentKeyAsync(Guid childKey) - => _navigationStructure.TryGetValue(childKey, out var childNode) ? + public Guid? GetParentKey(Guid childKey) + => _navigationStructure.TryGetValue(childKey, out NavigationNode? childNode) ? childNode.Parent?.Key : null; - public async Task> GetChildrenKeysAsync(Guid parentKey) - => _navigationStructure.TryGetValue(parentKey, out var parentNode) ? + public IEnumerable GetChildrenKeys(Guid parentKey) + => _navigationStructure.TryGetValue(parentKey, out NavigationNode? parentNode) ? parentNode.Children.Select(child => child.Key) : []; - public async Task> GetDescendantsKeysAsync(Guid parentKey) + public IEnumerable GetDescendantsKeys(Guid parentKey) { var descendants = new List(); @@ -45,11 +45,11 @@ public async Task> GetDescendantsKeysAsync(Guid parentKey) return descendants; } - public async Task> GetAncestorsKeysAsync(Guid childKey) + public IEnumerable GetAncestorsKeys(Guid childKey) { var ancestors = new List(); - if (_navigationStructure.TryGetValue(childKey, out var childNode) is false) + if (_navigationStructure.TryGetValue(childKey, out NavigationNode? childNode) is false) { return ancestors; } @@ -63,7 +63,7 @@ public async Task> GetAncestorsKeysAsync(Guid childKey) return ancestors; } - public async Task> GetSiblingsKeysAsync(Guid key) + public IEnumerable GetSiblingsKeys(Guid key) { var siblings = new List(); @@ -82,7 +82,7 @@ public async Task> GetSiblingsKeysAsync(Guid key) return siblings; } - IEnumerable childrenKeys = await GetChildrenKeysAsync(node.Parent.Key); + IEnumerable childrenKeys = GetChildrenKeys(node.Parent.Key); // Filter out the node itself to get its siblings siblings = childrenKeys.Where(childKey => childKey != key).ToList(); @@ -98,7 +98,7 @@ public bool Remove(Guid key) } // Remove the node from its parent's children list - if (nodeToRemove.Parent is not null && _navigationStructure.TryGetValue(nodeToRemove.Parent.Key, out var parentNode)) + if (nodeToRemove.Parent is not null && _navigationStructure.TryGetValue(nodeToRemove.Parent.Key, out NavigationNode? parentNode)) { parentNode.Children.Remove(nodeToRemove); } @@ -140,7 +140,7 @@ public bool Copy(Guid sourceKey, out Guid copiedNodeKey, Guid? targetParentKey = { copiedNodeKey = Guid.Empty; // Default value - if (_navigationStructure.TryGetValue(sourceKey, out var sourceNode) is false) + if (_navigationStructure.TryGetValue(sourceKey, out NavigationNode? sourceNode) is false) { return false; // Source doesn't exist } @@ -154,7 +154,7 @@ public bool Copy(Guid sourceKey, out Guid copiedNodeKey, Guid? targetParentKey = var newNodesMap = new Dictionary(); CopyNodeHierarchyRecursively(sourceNode, targetParentNode, newNodesMap, out copiedNodeKey); - foreach (var newNode in newNodesMap.Values) + foreach (NavigationNode newNode in newNodesMap.Values) { _navigationStructure[newNode.Key] = newNode; } @@ -164,7 +164,7 @@ public bool Copy(Guid sourceKey, out Guid copiedNodeKey, Guid? targetParentKey = public bool Move(Guid nodeKey, Guid? targetParentKey = null) { - if (_navigationStructure.TryGetValue(nodeKey, out var nodeToMove) is false) + if (_navigationStructure.TryGetValue(nodeKey, out NavigationNode? nodeToMove) is false) { return false; // Node doesn't exist } diff --git a/src/Umbraco.Core/Services/Navigation/INavigationService.cs b/src/Umbraco.Core/Services/Navigation/INavigationService.cs index f56156da8756..b66b94601dc7 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationService.cs @@ -2,17 +2,17 @@ namespace Umbraco.Cms.Core.Services.Navigation; public interface INavigationService { - Task GetParentKeyAsync(Guid childKey); + Task RebuildAsync(); - Task> GetChildrenKeysAsync(Guid parentKey); + Guid? GetParentKey(Guid childKey); - Task> GetDescendantsKeysAsync(Guid parentKey); + IEnumerable GetChildrenKeys(Guid parentKey); - Task> GetAncestorsKeysAsync(Guid childKey); + IEnumerable GetDescendantsKeys(Guid parentKey); - Task> GetSiblingsKeysAsync(Guid key); + IEnumerable GetAncestorsKeys(Guid childKey); - Task RebuildAsync(); + IEnumerable GetSiblingsKeys(Guid key); bool Remove(Guid key); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs index 475aa443b680..e47c220234fe 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs @@ -75,10 +75,10 @@ public void Setup() } [Test] - public async Task Cannot_Get_Parent_From_Non_Existing_Content_Key() + public void Cannot_Get_Parent_From_Non_Existing_Content_Key() { // Act - Guid? result = await _navigationService.GetParentKeyAsync(Guid.NewGuid()); + Guid? result = _navigationService.GetParentKey(Guid.NewGuid()); // Assert Assert.IsNull(result); @@ -94,10 +94,10 @@ public async Task Cannot_Get_Parent_From_Non_Existing_Content_Key() [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", "D63C1621-C74A-4106-8587-817DEE5FB732")] // Great-grandchild 1 [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 3 [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", "B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Grandchild 4 - public async Task Can_Get_Parent_From_Existing_Content_Key(Guid childKey, Guid? parentKey) + public void Can_Get_Parent_From_Existing_Content_Key(Guid childKey, Guid? parentKey) { // Act - Guid? result = await _navigationService.GetParentKeyAsync(childKey); + Guid? result = _navigationService.GetParentKey(childKey); // Assert Assert.Multiple(() => @@ -115,10 +115,10 @@ public async Task Can_Get_Parent_From_Existing_Content_Key(Guid childKey, Guid? } [Test] - public async Task Cannot_Get_Children_From_Non_Existing_Content_Key() + public void Cannot_Get_Children_From_Non_Existing_Content_Key() { // Act - IEnumerable result = await _navigationService.GetChildrenKeysAsync(Guid.NewGuid()); + IEnumerable result = _navigationService.GetChildrenKeys(Guid.NewGuid()); // Assert Assert.IsEmpty(result); @@ -134,10 +134,10 @@ public async Task Cannot_Get_Children_From_Non_Existing_Content_Key() [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 async Task Can_Get_Children_From_Existing_Content_Key(Guid parentKey, int childrenCount) + public void Can_Get_Children_From_Existing_Content_Key(Guid parentKey, int childrenCount) { // Act - IEnumerable result = await _navigationService.GetChildrenKeysAsync(parentKey); + IEnumerable result = _navigationService.GetChildrenKeys(parentKey); // Assert Assert.AreEqual(childrenCount, result.Count()); @@ -157,13 +157,13 @@ public async Task Can_Get_Children_From_Existing_Content_Key(Guid parentKey, int [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", new[] { "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Grandchild 3 [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new string[0])] // Great-grandchild 1 [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", new[] { "F381906C-223C-4466-80F7-B63B4EE073F8" })] // Child 3 - public async Task Can_Get_Children_From_Existing_Content_Key_In_Correct_Order(Guid parentKey, string[] children) + public void Can_Get_Children_From_Existing_Content_Key_In_Correct_Order(Guid parentKey, string[] children) { // Arrange Guid[] expectedChildren = Array.ConvertAll(children, Guid.Parse); // Act - IEnumerable result = await _navigationService.GetChildrenKeysAsync(parentKey); + List result = _navigationService.GetChildrenKeys(parentKey).ToList(); // Assert for (var i = 0; i < expectedChildren.Length; i++) @@ -173,10 +173,10 @@ public async Task Can_Get_Children_From_Existing_Content_Key_In_Correct_Order(Gu } [Test] - public async Task Cannot_Get_Descendants_From_Non_Existing_Content_Key() + public void Cannot_Get_Descendants_From_Non_Existing_Content_Key() { // Act - IEnumerable result = await _navigationService.GetDescendantsKeysAsync(Guid.NewGuid()); + IEnumerable result = _navigationService.GetDescendantsKeys(Guid.NewGuid()); // Assert Assert.IsEmpty(result); @@ -193,10 +193,10 @@ public async Task Cannot_Get_Descendants_From_Non_Existing_Content_Key() [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 async Task Can_Get_Descendants_From_Existing_Content_Key(Guid parentKey, int descendantsCount) + public void Can_Get_Descendants_From_Existing_Content_Key(Guid parentKey, int descendantsCount) { // Act - IEnumerable result = await _navigationService.GetDescendantsKeysAsync(parentKey); + IEnumerable result = _navigationService.GetDescendantsKeys(parentKey); // Assert Assert.AreEqual(descendantsCount, result.Count()); @@ -219,13 +219,13 @@ public async Task Can_Get_Descendants_From_Existing_Content_Key(Guid parentKey, [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", new[] { "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Grandchild 3 [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new string[0])] // Great-grandchild 1 [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", new[] { "F381906C-223C-4466-80F7-B63B4EE073F8" })] // Child 3 - public async Task Can_Get_Descendants_From_Existing_Content_Key_In_Correct_Order(Guid parentKey, string[] descendants) + public void Can_Get_Descendants_From_Existing_Content_Key_In_Correct_Order(Guid parentKey, string[] descendants) { // Arrange Guid[] expectedDescendants = Array.ConvertAll(descendants, Guid.Parse); // Act - IEnumerable result = await _navigationService.GetDescendantsKeysAsync(parentKey); + List result = _navigationService.GetDescendantsKeys(parentKey).ToList(); // Assert for (var i = 0; i < expectedDescendants.Length; i++) @@ -235,10 +235,10 @@ public async Task Can_Get_Descendants_From_Existing_Content_Key_In_Correct_Order } [Test] - public async Task Cannot_Get_Ancestors_From_Non_Existing_Content_Key() + public void Cannot_Get_Ancestors_From_Non_Existing_Content_Key() { // Act - IEnumerable result = await _navigationService.GetAncestorsKeysAsync(Guid.NewGuid()); + IEnumerable result = _navigationService.GetAncestorsKeys(Guid.NewGuid()); // Assert Assert.IsEmpty(result); @@ -254,10 +254,10 @@ public async Task Cannot_Get_Ancestors_From_Non_Existing_Content_Key() [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 async Task Can_Get_Ancestors_From_Existing_Content_Key(Guid childKey, int ancestorsCount) + public void Can_Get_Ancestors_From_Existing_Content_Key(Guid childKey, int ancestorsCount) { // Act - IEnumerable result = await _navigationService.GetAncestorsKeysAsync(childKey); + IEnumerable result = _navigationService.GetAncestorsKeys(childKey); // Assert Assert.AreEqual(ancestorsCount, result.Count()); @@ -274,13 +274,13 @@ public async Task Can_Get_Ancestors_From_Existing_Content_Key(Guid childKey, int "D63C1621-C74A-4106-8587-817DEE5FB732", "60E0E5C4-084E-4144-A560-7393BEAD2E96", "E48DD82A-7059-418E-9B82-CDD5205796CF" })] // Great-grandchild 1 - public async Task Can_Get_Ancestors_From_Existing_Content_Key_In_Correct_Order(Guid childKey, string[] ancestors) + public void Can_Get_Ancestors_From_Existing_Content_Key_In_Correct_Order(Guid childKey, string[] ancestors) { // Arrange Guid[] expectedAncestors = Array.ConvertAll(ancestors, Guid.Parse); // Act - IEnumerable result = await _navigationService.GetAncestorsKeysAsync(childKey); + List result = _navigationService.GetAncestorsKeys(childKey).ToList(); // Assert for (var i = 0; i < expectedAncestors.Length; i++) @@ -290,26 +290,26 @@ public async Task Can_Get_Ancestors_From_Existing_Content_Key_In_Correct_Order(G } [Test] - public async Task Cannot_Get_Siblings_Of_Non_Existing_Content_Key() + public void Cannot_Get_Siblings_Of_Non_Existing_Content_Key() { // Act - IEnumerable result = await _navigationService.GetSiblingsKeysAsync(Guid.NewGuid()); + IEnumerable result = _navigationService.GetSiblingsKeys(Guid.NewGuid()); // Assert Assert.IsEmpty(result); } [Test] - public async Task Can_Get_Siblings_Of_Existing_Content_Key_Without_Self() + public void Can_Get_Siblings_Of_Existing_Content_Key_Without_Self() { // Arrange Guid nodeKey = Child1; // Act - IEnumerable result = await _navigationService.GetSiblingsKeysAsync(nodeKey); + List result = _navigationService.GetSiblingsKeys(nodeKey).ToList(); // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsNotEmpty(result); Assert.IsFalse(result.Contains(nodeKey)); @@ -317,20 +317,20 @@ public async Task Can_Get_Siblings_Of_Existing_Content_Key_Without_Self() } [Test] - public async Task Can_Get_Siblings_Of_Existing_Content_Key_At_Content_Root() + public void Can_Get_Siblings_Of_Existing_Content_Key_At_Content_Root() { // Arrange Guid anotherRoot = new Guid("716380B9-DAA9-4930-A461-95EF39EBAB41"); _navigationService.Add(anotherRoot); // Act - IEnumerable result = await _navigationService.GetSiblingsKeysAsync(anotherRoot); + List result = _navigationService.GetSiblingsKeys(anotherRoot).ToList(); // Assert Assert.Multiple(() => { Assert.IsNotEmpty(result); - Assert.AreEqual(1, result.Count()); + Assert.AreEqual(1, result.Count); Assert.AreEqual(Root, result.First()); }); } @@ -345,10 +345,10 @@ public async Task Can_Get_Siblings_Of_Existing_Content_Key_At_Content_Root() [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 async Task Can_Get_Siblings_Of_Existing_Content_Key(Guid key, int siblingsCount) + public void Can_Get_Siblings_Of_Existing_Content_Key(Guid key, int siblingsCount) { // Act - IEnumerable result = await _navigationService.GetSiblingsKeysAsync(key); + IEnumerable result = _navigationService.GetSiblingsKeys(key); // Assert Assert.AreEqual(siblingsCount, result.Count()); @@ -358,13 +358,13 @@ public async Task Can_Get_Siblings_Of_Existing_Content_Key(Guid key, int sibling [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new string[0])] // Root [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", new[] { "60E0E5C4-084E-4144-A560-7393BEAD2E96", "B606E3FF-E070-4D46-8CB9-D31352029FDF" })] // Child 1 - Child 2, Child 3 [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new[] { "A1B1B217-B02F-4307-862C-A5E22DB729EB" })] // Grandchild 1 - Grandchild 2 - public async Task Can_Get_Siblings_Of_Existing_Content_Key_In_Correct_Order(Guid childKey, string[] siblings) + public void Can_Get_Siblings_Of_Existing_Content_Key_In_Correct_Order(Guid childKey, string[] siblings) { // Arrange Guid[] expectedSiblings = Array.ConvertAll(siblings, Guid.Parse); // Act - IEnumerable result = await _navigationService.GetSiblingsKeysAsync(childKey); + List result = _navigationService.GetSiblingsKeys(childKey).ToList(); // Assert for (var i = 0; i < expectedSiblings.Length; i++) @@ -393,10 +393,10 @@ public void Removing_Node_Removes_Its_Descendants_As_Well(Guid keyOfNodeToRemove var result = _navigationService.Remove(keyOfNodeToRemove); // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); - Assert.AreEqual(0, (await _navigationService.GetDescendantsKeysAsync(keyOfNodeToRemove)).Count()); + Assert.AreEqual(0, _navigationService.GetDescendantsKeys(keyOfNodeToRemove).Count()); }); } @@ -434,10 +434,10 @@ public void Can_Add_Node_To_Content_Root() var result = _navigationService.Add(newNodeKey); // parentKey is null // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); - Assert.AreEqual(null, await _navigationService.GetParentKeyAsync(newNodeKey)); + Assert.AreEqual(null, _navigationService.GetParentKey(newNodeKey)); }); } @@ -445,21 +445,22 @@ public void Can_Add_Node_To_Content_Root() [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 - public async Task Can_Add_Node_To_Parent(Guid parentKey) + public void Can_Add_Node_To_Parent(Guid parentKey) { // Arrange var newNodeKey = Guid.NewGuid(); - var currentChildrenCount = (await _navigationService.GetChildrenKeysAsync(parentKey)).Count(); + var currentChildrenCount = _navigationService.GetChildrenKeys(parentKey).Count(); // Act var result = _navigationService.Add(newNodeKey, parentKey); // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); - var newChildren = await _navigationService.GetChildrenKeysAsync(parentKey); - Assert.AreEqual(currentChildrenCount + 1, newChildren.Count()); + + List newChildren = _navigationService.GetChildrenKeys(parentKey).ToList(); + Assert.AreEqual(currentChildrenCount + 1, newChildren.Count); Assert.IsTrue(newChildren.Any(childKey => childKey == newNodeKey)); }); } @@ -528,14 +529,14 @@ public void Can_Copy_Node_To_Content_Root() var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey); // parentKey is null // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); Assert.AreNotEqual(Guid.Empty, copiedNodeKey); Assert.AreNotEqual(nodeToCopy, copiedNodeKey); // Verify the copied node's parent is null (it's been copied to content root) - Guid? copiedNodeParentKey = await _navigationService.GetParentKeyAsync(copiedNodeKey); + Guid? copiedNodeParentKey = _navigationService.GetParentKey(copiedNodeKey); Assert.IsNull(copiedNodeParentKey); }); } @@ -551,61 +552,61 @@ public void Can_Copy_Node_To_Existing_Target_Parent() var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, targetParentKey); // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); Assert.AreNotEqual(Guid.Empty, copiedNodeKey); Assert.AreNotEqual(nodeToCopy, copiedNodeKey); // Verify the node is copied to the correct parent - Guid? copiedNodeParentKey = await _navigationService.GetParentKeyAsync(copiedNodeKey); + Guid? copiedNodeParentKey = _navigationService.GetParentKey(copiedNodeKey); Assert.IsNotNull(copiedNodeParentKey); Assert.AreEqual(targetParentKey, copiedNodeParentKey); }); } [Test] - public async Task Copying_Node_Does_Not_Update_Source_Node_Parent() + public void Copying_Node_Does_Not_Update_Source_Node_Parent() { // Arrange Guid nodeToCopy = Grandchild1; Guid targetParentKey = Child3; - Guid? originalParentKey = await _navigationService.GetParentKeyAsync(nodeToCopy); + Guid? originalParentKey = _navigationService.GetParentKey(nodeToCopy); // Act var result = _navigationService.Copy(nodeToCopy, out _, targetParentKey); // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); // Verify that the original parent is still the same - Guid? currentParentKey = await _navigationService.GetParentKeyAsync(nodeToCopy); + Guid? currentParentKey = _navigationService.GetParentKey(nodeToCopy); Assert.AreEqual(originalParentKey, currentParentKey); }); } [Test] - public async Task Copied_Node_Is_Added_To_Its_New_Parent() + public void Copied_Node_Is_Added_To_Its_New_Parent() { // Arrange Guid nodeToCopy = Grandchild2; Guid targetParentKey = Child2; - var targetParentChildrenCount = (await _navigationService.GetChildrenKeysAsync(targetParentKey)).Count(); + var targetParentChildrenCount = _navigationService.GetChildrenKeys(targetParentKey).Count(); // Act var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, targetParentKey); // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); // Verify the node is added to its new parent's children list - IEnumerable children = await _navigationService.GetChildrenKeysAsync(targetParentKey); + List children = _navigationService.GetChildrenKeys(targetParentKey).ToList(); CollectionAssert.Contains(children, copiedNodeKey); - Assert.AreEqual(targetParentChildrenCount + 1, children.Count()); + Assert.AreEqual(targetParentChildrenCount + 1, children.Count); }); } @@ -619,12 +620,12 @@ public void Copied_Node_Has_The_Same_Amount_Of_Descendants(Guid nodeToCopy, Guid var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, targetParentKey); // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); // Get the number of descendants of the copied node - var descendantsCountAfterCopy = (await _navigationService.GetDescendantsKeysAsync(copiedNodeKey)).Count(); + var descendantsCountAfterCopy = _navigationService.GetDescendantsKeys(copiedNodeKey).Count(); Assert.AreEqual(initialDescendantsCount, descendantsCountAfterCopy); }); } @@ -633,21 +634,21 @@ public void Copied_Node_Has_The_Same_Amount_Of_Descendants(Guid nodeToCopy, Guid [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "E48DD82A-7059-418E-9B82-CDD5205796CF", 8)] // Grandchild 2 to Root [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", "B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Grandchild 3 to Child 3 [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "F381906C-223C-4466-80F7-B63B4EE073F8", 0)] // Child 2 to Grandchild 4 - public async Task Number_Of_Target_Parent_Descendants_Updates_When_Copying_Node_With_Descendants(Guid nodeToCopy, Guid targetParentKey, int initialDescendantsCountOfTargetParent) + public void Number_Of_Target_Parent_Descendants_Updates_When_Copying_Node_With_Descendants(Guid nodeToCopy, Guid targetParentKey, int initialDescendantsCountOfTargetParent) { // Arrange // Get the number of descendants of the node to copy - var descendantsCountOfNodeToCopy = (await _navigationService.GetDescendantsKeysAsync(nodeToCopy)).Count(); + var descendantsCountOfNodeToCopy = _navigationService.GetDescendantsKeys(nodeToCopy).Count(); // Act var result = _navigationService.Copy(nodeToCopy, out _, targetParentKey); // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); - var updatedDescendantsCountOfTargetParent = (await _navigationService.GetDescendantsKeysAsync(targetParentKey)).Count(); + var updatedDescendantsCountOfTargetParent = _navigationService.GetDescendantsKeys(targetParentKey).Count(); // Verify the number of descendants of the target parent has increased by the number of descendants of the copied node plus the node itself Assert.AreEqual(initialDescendantsCountOfTargetParent + descendantsCountOfNodeToCopy + 1, updatedDescendantsCountOfTargetParent); @@ -655,23 +656,23 @@ public async Task Number_Of_Target_Parent_Descendants_Updates_When_Copying_Node_ } [Test] - public async Task Copied_Node_Descendants_Have_Different_Keys_Than_Source_Node_Descendants() + public void Copied_Node_Descendants_Have_Different_Keys_Than_Source_Node_Descendants() { // Arrange Guid nodeToCopy = Child2; Guid targetParentKey = Grandchild4; - IEnumerable sourceDescendants = await _navigationService.GetDescendantsKeysAsync(nodeToCopy); + IEnumerable sourceDescendants = _navigationService.GetDescendantsKeys(nodeToCopy); // Act var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, targetParentKey); // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); // Get the descendants of the copied node - IEnumerable copiedDescendants = await _navigationService.GetDescendantsKeysAsync(copiedNodeKey); + IEnumerable copiedDescendants = _navigationService.GetDescendantsKeys(copiedNodeKey); // Ensure all keys of the copied descendants are different from the source descendants Assert.IsTrue(copiedDescendants.All(copiedDescendantKey => sourceDescendants.Contains(copiedDescendantKey) is false)); @@ -729,12 +730,12 @@ public void Can_Move_Node_To_Content_Root() var result = _navigationService.Move(nodeToMove); // parentKey is null // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); // Verify the node's new parent is null (moved to content root) - Guid? newParentKey = await _navigationService.GetParentKeyAsync(nodeToMove); + Guid? newParentKey = _navigationService.GetParentKey(nodeToMove); Assert.IsNull(newParentKey); }); } @@ -750,35 +751,35 @@ public void Can_Move_Node_To_Existing_Target_Parent() var result = _navigationService.Move(nodeToMove, targetParentKey); // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); // Verify the node's new parent is updated - Guid? parentKey = await _navigationService.GetParentKeyAsync(nodeToMove); + Guid? parentKey = _navigationService.GetParentKey(nodeToMove); Assert.IsNotNull(parentKey); Assert.AreEqual(targetParentKey, parentKey); }); } [Test] - public async Task Moved_Node_Has_Updated_Parent() + public void Moved_Node_Has_Updated_Parent() { // Arrange Guid nodeToMove = Grandchild1; Guid targetParentKey = Child2; - Guid? oldParentKey = await _navigationService.GetParentKeyAsync(nodeToMove); + Guid? oldParentKey = _navigationService.GetParentKey(nodeToMove); // Act var result = _navigationService.Move(nodeToMove, targetParentKey); // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); // Verify the node's new parent is updated - Guid? parentKey = await _navigationService.GetParentKeyAsync(nodeToMove); + Guid? parentKey = _navigationService.GetParentKey(nodeToMove); Assert.IsNotNull(parentKey); Assert.AreEqual(targetParentKey, parentKey); @@ -788,49 +789,49 @@ public async Task Moved_Node_Has_Updated_Parent() } [Test] - public async Task Moved_Node_Is_Removed_From_Its_Current_Parent() + public void Moved_Node_Is_Removed_From_Its_Current_Parent() { // Arrange Guid nodeToMove = Grandchild3; Guid targetParentKey = Child3; - Guid? oldParentKey = await _navigationService.GetParentKeyAsync(nodeToMove); - var oldParentChildrenCount = (await _navigationService.GetChildrenKeysAsync(oldParentKey.Value)).Count(); + Guid? oldParentKey = _navigationService.GetParentKey(nodeToMove); + var oldParentChildrenCount = _navigationService.GetChildrenKeys(oldParentKey!.Value).Count(); // Act var result = _navigationService.Move(nodeToMove, targetParentKey); // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); // Verify the node is removed from its old parent's children list - IEnumerable children = await _navigationService.GetChildrenKeysAsync(oldParentKey.Value); + List children = _navigationService.GetChildrenKeys(oldParentKey.Value).ToList(); CollectionAssert.DoesNotContain(children, nodeToMove); - Assert.AreEqual(oldParentChildrenCount - 1, children.Count()); + Assert.AreEqual(oldParentChildrenCount - 1, children.Count); }); } [Test] - public async Task Moved_Node_Is_Added_To_Its_New_Parent() + public void Moved_Node_Is_Added_To_Its_New_Parent() { // Arrange Guid nodeToMove = Grandchild2; Guid targetParentKey = Child2; - var targetParentChildrenCount = (await _navigationService.GetChildrenKeysAsync(targetParentKey)).Count(); + var targetParentChildrenCount = _navigationService.GetChildrenKeys(targetParentKey).Count(); // Act var result = _navigationService.Move(nodeToMove, targetParentKey); // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); // Verify the node is added to its new parent's children list - IEnumerable children = await _navigationService.GetChildrenKeysAsync(targetParentKey); + List children = _navigationService.GetChildrenKeys(targetParentKey).ToList(); CollectionAssert.Contains(children, nodeToMove); - Assert.AreEqual(targetParentChildrenCount + 1, children.Count()); + Assert.AreEqual(targetParentChildrenCount + 1, children.Count); }); } @@ -844,12 +845,12 @@ public void Moved_Node_Has_The_Same_Amount_Of_Descendants(Guid nodeToMove, Guid? var result = _navigationService.Move(nodeToMove, targetParentKey); // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); // Verify that the number of descendants remain the same after moving the node - var descendantsCountAfterMove = (await _navigationService.GetDescendantsKeysAsync(nodeToMove)).Count(); + var descendantsCountAfterMove = _navigationService.GetDescendantsKeys(nodeToMove).Count(); Assert.AreEqual(initialDescendantsCount, descendantsCountAfterMove); }); } @@ -858,21 +859,21 @@ public void Moved_Node_Has_The_Same_Amount_Of_Descendants(Guid nodeToMove, Guid? [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "A1B1B217-B02F-4307-862C-A5E22DB729EB", 0)] // Child 3 to Grandchild 2 [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Child 2 to Child 3 [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "60E0E5C4-084E-4144-A560-7393BEAD2E96", 2)] // Grandchild 1 to Child 2 - public async Task Number_Of_Target_Parent_Descendants_Updates_When_Moving_Node_With_Descendants(Guid nodeToMove, Guid targetParentKey, int initialDescendantsCountOfTargetParent) + public void Number_Of_Target_Parent_Descendants_Updates_When_Moving_Node_With_Descendants(Guid nodeToMove, Guid targetParentKey, int initialDescendantsCountOfTargetParent) { // Arrange // Get the number of descendants of the node to move - var descendantsCountOfNodeToMove = (await _navigationService.GetDescendantsKeysAsync(nodeToMove)).Count(); + var descendantsCountOfNodeToMove = _navigationService.GetDescendantsKeys(nodeToMove).Count(); // Act var result = _navigationService.Move(nodeToMove, targetParentKey); // Assert - Assert.Multiple(async () => + Assert.Multiple(() => { Assert.IsTrue(result); - var updatedDescendantsCountOfTargetParent = (await _navigationService.GetDescendantsKeysAsync(targetParentKey)).Count(); + var updatedDescendantsCountOfTargetParent = _navigationService.GetDescendantsKeys(targetParentKey).Count(); // Verify the number of descendants of the target parent has increased by the number of descendants of the moved node plus the node itself Assert.AreEqual(initialDescendantsCountOfTargetParent + descendantsCountOfNodeToMove + 1, updatedDescendantsCountOfTargetParent); From b435de313cbbf3ce91234d45fdd9592a1d5416b3 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 24 Jul 2024 19:59:03 +0200 Subject: [PATCH 06/96] Refactor GetParentKey to TryGetParentKey --- .../Navigation/ContentNavigationService.cs | 15 +++- .../Services/Navigation/INavigationService.cs | 2 +- .../Services/NavigationServiceTests.cs | 83 +++++++++++++------ 3 files changed, 68 insertions(+), 32 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs index a2ef988f1d8a..3cfd7a60620f 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs @@ -23,10 +23,17 @@ public async Task RebuildAsync() _navigationStructure = _navigationRepository.GetContentNodesByObjectType(Constants.ObjectTypes.Document); } - public Guid? GetParentKey(Guid childKey) - => _navigationStructure.TryGetValue(childKey, out NavigationNode? childNode) ? - childNode.Parent?.Key : - null; + public bool TryGetParentKey(Guid childKey, out Guid? parentKey) + { + if (_navigationStructure.TryGetValue(childKey, out NavigationNode? childNode)) + { + parentKey = childNode.Parent?.Key; + return true; + } + + parentKey = null; + return false; + } public IEnumerable GetChildrenKeys(Guid parentKey) => _navigationStructure.TryGetValue(parentKey, out NavigationNode? parentNode) ? diff --git a/src/Umbraco.Core/Services/Navigation/INavigationService.cs b/src/Umbraco.Core/Services/Navigation/INavigationService.cs index b66b94601dc7..9468a0cbf6db 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationService.cs @@ -4,7 +4,7 @@ public interface INavigationService { Task RebuildAsync(); - Guid? GetParentKey(Guid childKey); + bool TryGetParentKey(Guid childKey, out Guid? parentKey); IEnumerable GetChildrenKeys(Guid parentKey); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs index e47c220234fe..bc21678d34e8 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs @@ -77,11 +77,18 @@ public void Setup() [Test] public void Cannot_Get_Parent_From_Non_Existing_Content_Key() { + // Arrange + var nonExistingKey = Guid.NewGuid(); + // Act - Guid? result = _navigationService.GetParentKey(Guid.NewGuid()); + var result = _navigationService.TryGetParentKey(nonExistingKey, out Guid? parentKey); // Assert - Assert.IsNull(result); + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsNull(parentKey); + }); } [Test] @@ -94,22 +101,24 @@ public void Cannot_Get_Parent_From_Non_Existing_Content_Key() [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", "D63C1621-C74A-4106-8587-817DEE5FB732")] // Great-grandchild 1 [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 3 [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", "B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Grandchild 4 - public void Can_Get_Parent_From_Existing_Content_Key(Guid childKey, Guid? parentKey) + public void Can_Get_Parent_From_Existing_Content_Key(Guid childKey, Guid? expectedParentKey) { // Act - Guid? result = _navigationService.GetParentKey(childKey); + var result = _navigationService.TryGetParentKey(childKey, out Guid? parentKey); // Assert Assert.Multiple(() => { - if (parentKey is null) + Assert.IsTrue(result); + + if (expectedParentKey is null) { - Assert.IsNull(result); + Assert.IsNull(parentKey); } else { - Assert.IsNotNull(result); - Assert.AreEqual(parentKey, result); + Assert.IsNotNull(parentKey); + Assert.AreEqual(expectedParentKey, parentKey); } }); } @@ -117,8 +126,11 @@ public void Can_Get_Parent_From_Existing_Content_Key(Guid childKey, Guid? parent [Test] public void Cannot_Get_Children_From_Non_Existing_Content_Key() { + // Arrange + var nonExistingKey = Guid.NewGuid(); + // Act - IEnumerable result = _navigationService.GetChildrenKeys(Guid.NewGuid()); + IEnumerable result = _navigationService.GetChildrenKeys(nonExistingKey); // Assert Assert.IsEmpty(result); @@ -175,8 +187,11 @@ public void Can_Get_Children_From_Existing_Content_Key_In_Correct_Order(Guid par [Test] public void Cannot_Get_Descendants_From_Non_Existing_Content_Key() { + // Arrange + var nonExistingKey = Guid.NewGuid(); + // Act - IEnumerable result = _navigationService.GetDescendantsKeys(Guid.NewGuid()); + IEnumerable result = _navigationService.GetDescendantsKeys(nonExistingKey); // Assert Assert.IsEmpty(result); @@ -237,8 +252,11 @@ public void Can_Get_Descendants_From_Existing_Content_Key_In_Correct_Order(Guid [Test] public void Cannot_Get_Ancestors_From_Non_Existing_Content_Key() { + // Arrange + var nonExistingKey = Guid.NewGuid(); + // Act - IEnumerable result = _navigationService.GetAncestorsKeys(Guid.NewGuid()); + IEnumerable result = _navigationService.GetAncestorsKeys(nonExistingKey); // Assert Assert.IsEmpty(result); @@ -292,8 +310,11 @@ public void Can_Get_Ancestors_From_Existing_Content_Key_In_Correct_Order(Guid ch [Test] public void Cannot_Get_Siblings_Of_Non_Existing_Content_Key() { + // Arrange + var nonExistingKey = Guid.NewGuid(); + // Act - IEnumerable result = _navigationService.GetSiblingsKeys(Guid.NewGuid()); + IEnumerable result = _navigationService.GetSiblingsKeys(nonExistingKey); // Assert Assert.IsEmpty(result); @@ -376,8 +397,11 @@ public void Can_Get_Siblings_Of_Existing_Content_Key_In_Correct_Order(Guid child [Test] public void Cannot_Remove_Node_With_Non_Existing_Content_Key() { + // Arrange + var nonExistingKey = Guid.NewGuid(); + // Act - var result = _navigationService.Remove(Guid.NewGuid()); + var result = _navigationService.Remove(nonExistingKey); // Assert Assert.IsFalse(result); @@ -437,7 +461,10 @@ public void Can_Add_Node_To_Content_Root() Assert.Multiple(() => { Assert.IsTrue(result); - Assert.AreEqual(null, _navigationService.GetParentKey(newNodeKey)); + + var parentExists = _navigationService.TryGetParentKey(newNodeKey, out Guid? parentKey); + Assert.IsTrue(parentExists); + Assert.IsNull(parentKey); }); } @@ -536,7 +563,8 @@ public void Can_Copy_Node_To_Content_Root() Assert.AreNotEqual(nodeToCopy, copiedNodeKey); // Verify the copied node's parent is null (it's been copied to content root) - Guid? copiedNodeParentKey = _navigationService.GetParentKey(copiedNodeKey); + var parentExists = _navigationService.TryGetParentKey(copiedNodeKey, out Guid? copiedNodeParentKey); + Assert.IsTrue(parentExists); Assert.IsNull(copiedNodeParentKey); }); } @@ -559,7 +587,8 @@ public void Can_Copy_Node_To_Existing_Target_Parent() Assert.AreNotEqual(nodeToCopy, copiedNodeKey); // Verify the node is copied to the correct parent - Guid? copiedNodeParentKey = _navigationService.GetParentKey(copiedNodeKey); + var parentExists = _navigationService.TryGetParentKey(copiedNodeKey, out Guid? copiedNodeParentKey); + Assert.IsTrue(parentExists); Assert.IsNotNull(copiedNodeParentKey); Assert.AreEqual(targetParentKey, copiedNodeParentKey); }); @@ -571,7 +600,7 @@ public void Copying_Node_Does_Not_Update_Source_Node_Parent() // Arrange Guid nodeToCopy = Grandchild1; Guid targetParentKey = Child3; - Guid? originalParentKey = _navigationService.GetParentKey(nodeToCopy); + _navigationService.TryGetParentKey(nodeToCopy, out Guid? originalParentKey); // Act var result = _navigationService.Copy(nodeToCopy, out _, targetParentKey); @@ -582,7 +611,7 @@ public void Copying_Node_Does_Not_Update_Source_Node_Parent() Assert.IsTrue(result); // Verify that the original parent is still the same - Guid? currentParentKey = _navigationService.GetParentKey(nodeToCopy); + _navigationService.TryGetParentKey(nodeToCopy, out Guid? currentParentKey); Assert.AreEqual(originalParentKey, currentParentKey); }); } @@ -735,7 +764,7 @@ public void Can_Move_Node_To_Content_Root() Assert.IsTrue(result); // Verify the node's new parent is null (moved to content root) - Guid? newParentKey = _navigationService.GetParentKey(nodeToMove); + _navigationService.TryGetParentKey(nodeToMove, out Guid? newParentKey); Assert.IsNull(newParentKey); }); } @@ -756,9 +785,9 @@ public void Can_Move_Node_To_Existing_Target_Parent() Assert.IsTrue(result); // Verify the node's new parent is updated - Guid? parentKey = _navigationService.GetParentKey(nodeToMove); - Assert.IsNotNull(parentKey); - Assert.AreEqual(targetParentKey, parentKey); + _navigationService.TryGetParentKey(nodeToMove, out Guid? newParentKey); + Assert.IsNotNull(newParentKey); + Assert.AreEqual(targetParentKey, newParentKey); }); } @@ -768,7 +797,7 @@ public void Moved_Node_Has_Updated_Parent() // Arrange Guid nodeToMove = Grandchild1; Guid targetParentKey = Child2; - Guid? oldParentKey = _navigationService.GetParentKey(nodeToMove); + _navigationService.TryGetParentKey(nodeToMove, out Guid? oldParentKey); // Act var result = _navigationService.Move(nodeToMove, targetParentKey); @@ -779,9 +808,9 @@ public void Moved_Node_Has_Updated_Parent() Assert.IsTrue(result); // Verify the node's new parent is updated - Guid? parentKey = _navigationService.GetParentKey(nodeToMove); - Assert.IsNotNull(parentKey); - Assert.AreEqual(targetParentKey, parentKey); + _navigationService.TryGetParentKey(nodeToMove, out Guid? newParentKey); + Assert.IsNotNull(newParentKey); + Assert.AreEqual(targetParentKey, newParentKey); // Verify that the new parent is different from the old one Assert.AreNotEqual(oldParentKey, targetParentKey); @@ -794,7 +823,7 @@ public void Moved_Node_Is_Removed_From_Its_Current_Parent() // Arrange Guid nodeToMove = Grandchild3; Guid targetParentKey = Child3; - Guid? oldParentKey = _navigationService.GetParentKey(nodeToMove); + _navigationService.TryGetParentKey(nodeToMove, out Guid? oldParentKey); var oldParentChildrenCount = _navigationService.GetChildrenKeys(oldParentKey!.Value).Count(); // Act From 45c800a0b10f0a83bf6a9fe39ff8c08cab9359b0 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 24 Jul 2024 20:51:01 +0200 Subject: [PATCH 07/96] Refactor GetChildrenKeys to TryGetChildrenKeys --- .../Navigation/ContentNavigationService.cs | 27 ++-- .../Services/Navigation/INavigationService.cs | 2 +- .../Services/NavigationServiceTests.cs | 123 +++++++++++------- 3 files changed, 94 insertions(+), 58 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs index 3cfd7a60620f..fc32d46b4efb 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs @@ -35,10 +35,17 @@ public bool TryGetParentKey(Guid childKey, out Guid? parentKey) return false; } - public IEnumerable GetChildrenKeys(Guid parentKey) - => _navigationStructure.TryGetValue(parentKey, out NavigationNode? parentNode) ? - parentNode.Children.Select(child => child.Key) : - []; + public bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys) + { + if (_navigationStructure.TryGetValue(parentKey, out NavigationNode? parentNode)) + { + childrenKeys = parentNode.Children.Select(child => child.Key); + return true; + } + + childrenKeys = []; + return false; + } public IEnumerable GetDescendantsKeys(Guid parentKey) { @@ -81,18 +88,20 @@ public IEnumerable GetSiblingsKeys(Guid key) if (node.Parent is null) { - // To find siblings of node at root, we need to iterate over all items and add those whose Parent is null + // To find siblings of node at root, we need to iterate over all items and add those which Parent is null siblings = _navigationStructure .Where(kv => kv.Value.Parent is null && kv.Key != key) .Select(kv => kv.Key) .ToList(); + return siblings; } - IEnumerable childrenKeys = GetChildrenKeys(node.Parent.Key); - - // Filter out the node itself to get its siblings - siblings = childrenKeys.Where(childKey => childKey != key).ToList(); + if (TryGetChildrenKeys(node.Parent.Key, out IEnumerable childrenKeys)) + { + // Filter out the node itself to get its siblings + siblings = childrenKeys.Where(childKey => childKey != key).ToList(); + } return siblings; } diff --git a/src/Umbraco.Core/Services/Navigation/INavigationService.cs b/src/Umbraco.Core/Services/Navigation/INavigationService.cs index 9468a0cbf6db..5227506611fa 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationService.cs @@ -6,7 +6,7 @@ public interface INavigationService bool TryGetParentKey(Guid childKey, out Guid? parentKey); - IEnumerable GetChildrenKeys(Guid parentKey); + bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys); IEnumerable GetDescendantsKeys(Guid parentKey); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs index bc21678d34e8..ed50daf6d218 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs @@ -130,10 +130,14 @@ public void Cannot_Get_Children_From_Non_Existing_Content_Key() var nonExistingKey = Guid.NewGuid(); // Act - IEnumerable result = _navigationService.GetChildrenKeys(nonExistingKey); + var result = _navigationService.TryGetChildrenKeys(nonExistingKey, out IEnumerable childrenKeys); // Assert - Assert.IsEmpty(result); + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(childrenKeys); + }); } [Test] @@ -149,10 +153,14 @@ public void Cannot_Get_Children_From_Non_Existing_Content_Key() public void Can_Get_Children_From_Existing_Content_Key(Guid parentKey, int childrenCount) { // Act - IEnumerable result = _navigationService.GetChildrenKeys(parentKey); + var result = _navigationService.TryGetChildrenKeys(parentKey, out IEnumerable childrenKeys); // Assert - Assert.AreEqual(childrenCount, result.Count()); + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(childrenCount, childrenKeys.Count()); + }); } [Test] @@ -175,12 +183,13 @@ public void Can_Get_Children_From_Existing_Content_Key_In_Correct_Order(Guid par Guid[] expectedChildren = Array.ConvertAll(children, Guid.Parse); // Act - List result = _navigationService.GetChildrenKeys(parentKey).ToList(); + _navigationService.TryGetChildrenKeys(parentKey, out IEnumerable childrenKeys); + List childrenList = childrenKeys.ToList(); // Assert for (var i = 0; i < expectedChildren.Length; i++) { - Assert.AreEqual(expectedChildren[i], result.ElementAt(i)); + Assert.AreEqual(expectedChildren[i], childrenList.ElementAt(i)); } } @@ -458,11 +467,12 @@ public void Can_Add_Node_To_Content_Root() var result = _navigationService.Add(newNodeKey); // parentKey is null // Assert + Assert.IsTrue(result); + + var parentExists = _navigationService.TryGetParentKey(newNodeKey, out Guid? parentKey); + Assert.Multiple(() => { - Assert.IsTrue(result); - - var parentExists = _navigationService.TryGetParentKey(newNodeKey, out Guid? parentKey); Assert.IsTrue(parentExists); Assert.IsNull(parentKey); }); @@ -476,19 +486,22 @@ public void Can_Add_Node_To_Parent(Guid parentKey) { // Arrange var newNodeKey = Guid.NewGuid(); - var currentChildrenCount = _navigationService.GetChildrenKeys(parentKey).Count(); + _navigationService.TryGetChildrenKeys(parentKey, out IEnumerable currentChildrenKeys); + var currentChildrenCount = currentChildrenKeys.Count(); // Act var result = _navigationService.Add(newNodeKey, parentKey); // Assert + Assert.IsTrue(result); + + _navigationService.TryGetChildrenKeys(parentKey, out IEnumerable newChildrenKeys); + var newChildrenList = newChildrenKeys.ToList(); + Assert.Multiple(() => { - Assert.IsTrue(result); - - List newChildren = _navigationService.GetChildrenKeys(parentKey).ToList(); - Assert.AreEqual(currentChildrenCount + 1, newChildren.Count); - Assert.IsTrue(newChildren.Any(childKey => childKey == newNodeKey)); + Assert.AreEqual(currentChildrenCount + 1, newChildrenList.Count); + Assert.IsTrue(newChildrenList.Any(childKey => childKey == newNodeKey)); }); } @@ -561,9 +574,13 @@ public void Can_Copy_Node_To_Content_Root() Assert.IsTrue(result); Assert.AreNotEqual(Guid.Empty, copiedNodeKey); Assert.AreNotEqual(nodeToCopy, copiedNodeKey); + }); + + // Verify the copied node's parent is null (it's been copied to content root) + var parentExists = _navigationService.TryGetParentKey(copiedNodeKey, out Guid? copiedNodeParentKey); - // Verify the copied node's parent is null (it's been copied to content root) - var parentExists = _navigationService.TryGetParentKey(copiedNodeKey, out Guid? copiedNodeParentKey); + Assert.Multiple(() => + { Assert.IsTrue(parentExists); Assert.IsNull(copiedNodeParentKey); }); @@ -585,10 +602,13 @@ public void Can_Copy_Node_To_Existing_Target_Parent() Assert.IsTrue(result); Assert.AreNotEqual(Guid.Empty, copiedNodeKey); Assert.AreNotEqual(nodeToCopy, copiedNodeKey); + }); - // Verify the node is copied to the correct parent - var parentExists = _navigationService.TryGetParentKey(copiedNodeKey, out Guid? copiedNodeParentKey); - Assert.IsTrue(parentExists); + // Verify the node is copied to the correct parent + _navigationService.TryGetParentKey(copiedNodeKey, out Guid? copiedNodeParentKey); + + Assert.Multiple(() => + { Assert.IsNotNull(copiedNodeParentKey); Assert.AreEqual(targetParentKey, copiedNodeParentKey); }); @@ -606,14 +626,12 @@ public void Copying_Node_Does_Not_Update_Source_Node_Parent() var result = _navigationService.Copy(nodeToCopy, out _, targetParentKey); // Assert - Assert.Multiple(() => - { - Assert.IsTrue(result); + Assert.IsTrue(result); - // Verify that the original parent is still the same - _navigationService.TryGetParentKey(nodeToCopy, out Guid? currentParentKey); - Assert.AreEqual(originalParentKey, currentParentKey); - }); + // Verify that the original parent is still the same + _navigationService.TryGetParentKey(nodeToCopy, out Guid? currentParentKey); + + Assert.AreEqual(originalParentKey, currentParentKey); } [Test] @@ -622,20 +640,23 @@ public void Copied_Node_Is_Added_To_Its_New_Parent() // Arrange Guid nodeToCopy = Grandchild2; Guid targetParentKey = Child2; - var targetParentChildrenCount = _navigationService.GetChildrenKeys(targetParentKey).Count(); + _navigationService.TryGetChildrenKeys(targetParentKey, out IEnumerable targetParentChildrenKeys); + var targetParentChildrenCount = targetParentChildrenKeys.Count(); // Act var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, targetParentKey); // Assert + Assert.IsTrue(result); + + // Verify the node is added to its new parent's children list + _navigationService.TryGetChildrenKeys(targetParentKey, out IEnumerable childrenKeys); + List childrenList = childrenKeys.ToList(); + Assert.Multiple(() => { - Assert.IsTrue(result); - - // Verify the node is added to its new parent's children list - List children = _navigationService.GetChildrenKeys(targetParentKey).ToList(); - CollectionAssert.Contains(children, copiedNodeKey); - Assert.AreEqual(targetParentChildrenCount + 1, children.Count); + CollectionAssert.Contains(childrenList, copiedNodeKey); + Assert.AreEqual(targetParentChildrenCount + 1, childrenList.Count); }); } @@ -824,20 +845,23 @@ public void Moved_Node_Is_Removed_From_Its_Current_Parent() Guid nodeToMove = Grandchild3; Guid targetParentKey = Child3; _navigationService.TryGetParentKey(nodeToMove, out Guid? oldParentKey); - var oldParentChildrenCount = _navigationService.GetChildrenKeys(oldParentKey!.Value).Count(); + _navigationService.TryGetChildrenKeys(oldParentKey!.Value, out IEnumerable oldParentChildrenKeys); + var oldParentChildrenCount = oldParentChildrenKeys.Count(); // Act var result = _navigationService.Move(nodeToMove, targetParentKey); // Assert + Assert.IsTrue(result); + + // Verify the node is removed from its old parent's children list + _navigationService.TryGetChildrenKeys(oldParentKey.Value, out IEnumerable childrenKeys); + List childrenList = childrenKeys.ToList(); + Assert.Multiple(() => { - Assert.IsTrue(result); - - // Verify the node is removed from its old parent's children list - List children = _navigationService.GetChildrenKeys(oldParentKey.Value).ToList(); - CollectionAssert.DoesNotContain(children, nodeToMove); - Assert.AreEqual(oldParentChildrenCount - 1, children.Count); + CollectionAssert.DoesNotContain(childrenList, nodeToMove); + Assert.AreEqual(oldParentChildrenCount - 1, childrenList.Count); }); } @@ -847,20 +871,23 @@ public void Moved_Node_Is_Added_To_Its_New_Parent() // Arrange Guid nodeToMove = Grandchild2; Guid targetParentKey = Child2; - var targetParentChildrenCount = _navigationService.GetChildrenKeys(targetParentKey).Count(); + _navigationService.TryGetChildrenKeys(targetParentKey, out IEnumerable targetParentChildrenKeys); + var targetParentChildrenCount = targetParentChildrenKeys.Count(); // Act var result = _navigationService.Move(nodeToMove, targetParentKey); // Assert + Assert.IsTrue(result); + + // Verify the node is added to its new parent's children list + _navigationService.TryGetChildrenKeys(targetParentKey, out IEnumerable childrenKeys); + List childrenList = childrenKeys.ToList(); + Assert.Multiple(() => { - Assert.IsTrue(result); - - // Verify the node is added to its new parent's children list - List children = _navigationService.GetChildrenKeys(targetParentKey).ToList(); - CollectionAssert.Contains(children, nodeToMove); - Assert.AreEqual(targetParentChildrenCount + 1, children.Count); + CollectionAssert.Contains(childrenList, nodeToMove); + Assert.AreEqual(targetParentChildrenCount + 1, childrenList.Count); }); } From d55586c9843429938eeab4f66ae6b54c18b866f8 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 24 Jul 2024 21:47:30 +0200 Subject: [PATCH 08/96] Refactor GetDescendantsKeys to TryGetDescendantsKeys --- .../Navigation/ContentNavigationService.cs | 15 +- .../Services/Navigation/INavigationService.cs | 2 +- .../Services/NavigationServiceTests.cs | 132 +++++++++--------- 3 files changed, 79 insertions(+), 70 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs index fc32d46b4efb..c0ddbe430513 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs @@ -31,6 +31,7 @@ public bool TryGetParentKey(Guid childKey, out Guid? parentKey) return true; } + // Child doesn't exist parentKey = null; return false; } @@ -43,20 +44,26 @@ public bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKey return true; } + // Parent doesn't exist childrenKeys = []; return false; } - public IEnumerable GetDescendantsKeys(Guid parentKey) + public bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys) { var descendants = new List(); - if (_navigationStructure.TryGetValue(parentKey, out var parentNode)) + if (_navigationStructure.TryGetValue(parentKey, out NavigationNode? parentNode) is false) { - GetDescendantsRecursively(parentNode, descendants); + // Parent doesn't exist + descendantsKeys = []; + return false; } - return descendants; + GetDescendantsRecursively(parentNode, descendants); + + descendantsKeys = descendants; + return true; } public IEnumerable GetAncestorsKeys(Guid childKey) diff --git a/src/Umbraco.Core/Services/Navigation/INavigationService.cs b/src/Umbraco.Core/Services/Navigation/INavigationService.cs index 5227506611fa..24d1ec62bc1b 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationService.cs @@ -8,7 +8,7 @@ public interface INavigationService bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys); - IEnumerable GetDescendantsKeys(Guid parentKey); + bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys); IEnumerable GetAncestorsKeys(Guid childKey); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs index ed50daf6d218..2d13d8bd439a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs @@ -200,10 +200,14 @@ public void Cannot_Get_Descendants_From_Non_Existing_Content_Key() var nonExistingKey = Guid.NewGuid(); // Act - IEnumerable result = _navigationService.GetDescendantsKeys(nonExistingKey); + var result = _navigationService.TryGetDescendantsKeys(nonExistingKey, out IEnumerable descendantsKeys); // Assert - Assert.IsEmpty(result); + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(descendantsKeys); + }); } [Test] @@ -220,10 +224,14 @@ public void Cannot_Get_Descendants_From_Non_Existing_Content_Key() public void Can_Get_Descendants_From_Existing_Content_Key(Guid parentKey, int descendantsCount) { // Act - IEnumerable result = _navigationService.GetDescendantsKeys(parentKey); + var result = _navigationService.TryGetDescendantsKeys(parentKey, out IEnumerable descendantsKeys); // Assert - Assert.AreEqual(descendantsCount, result.Count()); + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(descendantsCount, descendantsKeys.Count()); + }); } [Test] @@ -249,12 +257,13 @@ public void Can_Get_Descendants_From_Existing_Content_Key_In_Correct_Order(Guid Guid[] expectedDescendants = Array.ConvertAll(descendants, Guid.Parse); // Act - List result = _navigationService.GetDescendantsKeys(parentKey).ToList(); + _navigationService.TryGetDescendantsKeys(parentKey, out IEnumerable descendantsKeys); + List descendantsList = descendantsKeys.ToList(); // Assert for (var i = 0; i < expectedDescendants.Length; i++) { - Assert.AreEqual(expectedDescendants[i], result.ElementAt(i)); + Assert.AreEqual(expectedDescendants[i], descendantsList.ElementAt(i)); } } @@ -426,11 +435,11 @@ public void Removing_Node_Removes_Its_Descendants_As_Well(Guid keyOfNodeToRemove var result = _navigationService.Remove(keyOfNodeToRemove); // Assert - Assert.Multiple(() => - { - Assert.IsTrue(result); - Assert.AreEqual(0, _navigationService.GetDescendantsKeys(keyOfNodeToRemove).Count()); - }); + Assert.IsTrue(result); + + _navigationService.TryGetDescendantsKeys(keyOfNodeToRemove, out IEnumerable descendantsKeys); + + Assert.AreEqual(0, descendantsKeys.Count()); } [Test] @@ -670,14 +679,13 @@ public void Copied_Node_Has_The_Same_Amount_Of_Descendants(Guid nodeToCopy, Guid var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, targetParentKey); // Assert - Assert.Multiple(() => - { - Assert.IsTrue(result); + Assert.IsTrue(result); - // Get the number of descendants of the copied node - var descendantsCountAfterCopy = _navigationService.GetDescendantsKeys(copiedNodeKey).Count(); - Assert.AreEqual(initialDescendantsCount, descendantsCountAfterCopy); - }); + // Get the number of descendants of the copied node + _navigationService.TryGetDescendantsKeys(copiedNodeKey, out IEnumerable descendantsKeys); + var descendantsCountAfterCopy = descendantsKeys.Count(); + + Assert.AreEqual(initialDescendantsCount, descendantsCountAfterCopy); } [Test] @@ -688,21 +696,20 @@ public void Number_Of_Target_Parent_Descendants_Updates_When_Copying_Node_With_D { // Arrange // Get the number of descendants of the node to copy - var descendantsCountOfNodeToCopy = _navigationService.GetDescendantsKeys(nodeToCopy).Count(); + _navigationService.TryGetDescendantsKeys(nodeToCopy, out IEnumerable descendantsKeys); + var descendantsCountOfNodeToCopy = descendantsKeys.Count(); // Act var result = _navigationService.Copy(nodeToCopy, out _, targetParentKey); // Assert - Assert.Multiple(() => - { - Assert.IsTrue(result); + Assert.IsTrue(result); - var updatedDescendantsCountOfTargetParent = _navigationService.GetDescendantsKeys(targetParentKey).Count(); + _navigationService.TryGetDescendantsKeys(targetParentKey, out IEnumerable updatedTargetParentDescendantsKeys); + var updatedDescendantsCountOfTargetParent = updatedTargetParentDescendantsKeys.Count(); - // Verify the number of descendants of the target parent has increased by the number of descendants of the copied node plus the node itself - Assert.AreEqual(initialDescendantsCountOfTargetParent + descendantsCountOfNodeToCopy + 1, updatedDescendantsCountOfTargetParent); - }); + // Verify the number of descendants of the target parent has increased by the number of descendants of the copied node plus the node itself + Assert.AreEqual(initialDescendantsCountOfTargetParent + descendantsCountOfNodeToCopy + 1, updatedDescendantsCountOfTargetParent); } [Test] @@ -711,22 +718,19 @@ public void Copied_Node_Descendants_Have_Different_Keys_Than_Source_Node_Descend // Arrange Guid nodeToCopy = Child2; Guid targetParentKey = Grandchild4; - IEnumerable sourceDescendants = _navigationService.GetDescendantsKeys(nodeToCopy); + _navigationService.TryGetDescendantsKeys(nodeToCopy, out IEnumerable sourceDescendants); // Act var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, targetParentKey); // Assert - Assert.Multiple(() => - { - Assert.IsTrue(result); + Assert.IsTrue(result); - // Get the descendants of the copied node - IEnumerable copiedDescendants = _navigationService.GetDescendantsKeys(copiedNodeKey); + // Get the descendants of the copied node + _navigationService.TryGetDescendantsKeys(copiedNodeKey, out IEnumerable copiedDescendants); - // Ensure all keys of the copied descendants are different from the source descendants - Assert.IsTrue(copiedDescendants.All(copiedDescendantKey => sourceDescendants.Contains(copiedDescendantKey) is false)); - }); + // Ensure all keys of the copied descendants are different from the source descendants + Assert.IsTrue(copiedDescendants.All(copiedDescendantKey => sourceDescendants.Contains(copiedDescendantKey) is false)); } [Test] @@ -780,14 +784,12 @@ public void Can_Move_Node_To_Content_Root() var result = _navigationService.Move(nodeToMove); // parentKey is null // Assert - Assert.Multiple(() => - { - Assert.IsTrue(result); + Assert.IsTrue(result); - // Verify the node's new parent is null (moved to content root) - _navigationService.TryGetParentKey(nodeToMove, out Guid? newParentKey); - Assert.IsNull(newParentKey); - }); + // Verify the node's new parent is null (moved to content root) + _navigationService.TryGetParentKey(nodeToMove, out Guid? newParentKey); + + Assert.IsNull(newParentKey); } [Test] @@ -801,12 +803,13 @@ public void Can_Move_Node_To_Existing_Target_Parent() var result = _navigationService.Move(nodeToMove, targetParentKey); // Assert + Assert.IsTrue(result); + + // Verify the node's new parent is updated + _navigationService.TryGetParentKey(nodeToMove, out Guid? newParentKey); + Assert.Multiple(() => { - Assert.IsTrue(result); - - // Verify the node's new parent is updated - _navigationService.TryGetParentKey(nodeToMove, out Guid? newParentKey); Assert.IsNotNull(newParentKey); Assert.AreEqual(targetParentKey, newParentKey); }); @@ -824,12 +827,13 @@ public void Moved_Node_Has_Updated_Parent() var result = _navigationService.Move(nodeToMove, targetParentKey); // Assert + Assert.IsTrue(result); + + // Verify the node's new parent is updated + _navigationService.TryGetParentKey(nodeToMove, out Guid? newParentKey); + Assert.Multiple(() => { - Assert.IsTrue(result); - - // Verify the node's new parent is updated - _navigationService.TryGetParentKey(nodeToMove, out Guid? newParentKey); Assert.IsNotNull(newParentKey); Assert.AreEqual(targetParentKey, newParentKey); @@ -901,14 +905,13 @@ public void Moved_Node_Has_The_Same_Amount_Of_Descendants(Guid nodeToMove, Guid? var result = _navigationService.Move(nodeToMove, targetParentKey); // Assert - Assert.Multiple(() => - { - Assert.IsTrue(result); + Assert.IsTrue(result); - // Verify that the number of descendants remain the same after moving the node - var descendantsCountAfterMove = _navigationService.GetDescendantsKeys(nodeToMove).Count(); - Assert.AreEqual(initialDescendantsCount, descendantsCountAfterMove); - }); + // Verify that the number of descendants remain the same after moving the node + _navigationService.TryGetDescendantsKeys(nodeToMove, out IEnumerable descendantsKeys); + var descendantsCountAfterMove = descendantsKeys.Count(); + + Assert.AreEqual(initialDescendantsCount, descendantsCountAfterMove); } [Test] @@ -919,20 +922,19 @@ public void Number_Of_Target_Parent_Descendants_Updates_When_Moving_Node_With_De { // Arrange // Get the number of descendants of the node to move - var descendantsCountOfNodeToMove = _navigationService.GetDescendantsKeys(nodeToMove).Count(); + _navigationService.TryGetDescendantsKeys(nodeToMove, out IEnumerable descendantsKeys); + var descendantsCountOfNodeToMove = descendantsKeys.Count(); // Act var result = _navigationService.Move(nodeToMove, targetParentKey); // Assert - Assert.Multiple(() => - { - Assert.IsTrue(result); + Assert.IsTrue(result); - var updatedDescendantsCountOfTargetParent = _navigationService.GetDescendantsKeys(targetParentKey).Count(); + _navigationService.TryGetDescendantsKeys(targetParentKey, out IEnumerable updatedTargetParentDescendantsKeys); + var updatedDescendantsCountOfTargetParent = updatedTargetParentDescendantsKeys.Count(); - // Verify the number of descendants of the target parent has increased by the number of descendants of the moved node plus the node itself - Assert.AreEqual(initialDescendantsCountOfTargetParent + descendantsCountOfNodeToMove + 1, updatedDescendantsCountOfTargetParent); - }); + // Verify the number of descendants of the target parent has increased by the number of descendants of the moved node plus the node itself + Assert.AreEqual(initialDescendantsCountOfTargetParent + descendantsCountOfNodeToMove + 1, updatedDescendantsCountOfTargetParent); } } From d347a00b45d9461739b73a319b302e6e5098c8ef Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 24 Jul 2024 21:58:07 +0200 Subject: [PATCH 09/96] Refactor GetAncestorsKeys to TryGetAncestorsKeys --- .../Navigation/ContentNavigationService.cs | 9 +++++--- .../Services/Navigation/INavigationService.cs | 2 +- .../Services/NavigationServiceTests.cs | 21 +++++++++++++------ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs index c0ddbe430513..c60fd9817ed2 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs @@ -66,13 +66,15 @@ public bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descenda return true; } - public IEnumerable GetAncestorsKeys(Guid childKey) + public bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKeys) { var ancestors = new List(); if (_navigationStructure.TryGetValue(childKey, out NavigationNode? childNode) is false) { - return ancestors; + // Child doesn't exist + ancestorsKeys = []; + return false; } while (childNode?.Parent is not null) @@ -81,7 +83,8 @@ public IEnumerable GetAncestorsKeys(Guid childKey) childNode = childNode.Parent; } - return ancestors; + ancestorsKeys = ancestors; + return true; } public IEnumerable GetSiblingsKeys(Guid key) diff --git a/src/Umbraco.Core/Services/Navigation/INavigationService.cs b/src/Umbraco.Core/Services/Navigation/INavigationService.cs index 24d1ec62bc1b..1865b8e67a7a 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationService.cs @@ -10,7 +10,7 @@ public interface INavigationService bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys); - IEnumerable GetAncestorsKeys(Guid childKey); + bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKeys); IEnumerable GetSiblingsKeys(Guid key); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs index 2d13d8bd439a..a51205117448 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs @@ -274,10 +274,14 @@ public void Cannot_Get_Ancestors_From_Non_Existing_Content_Key() var nonExistingKey = Guid.NewGuid(); // Act - IEnumerable result = _navigationService.GetAncestorsKeys(nonExistingKey); + var result = _navigationService.TryGetAncestorsKeys(nonExistingKey, out IEnumerable ancestorsKeys); // Assert - Assert.IsEmpty(result); + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(ancestorsKeys); + }); } [Test] @@ -293,10 +297,14 @@ public void Cannot_Get_Ancestors_From_Non_Existing_Content_Key() public void Can_Get_Ancestors_From_Existing_Content_Key(Guid childKey, int ancestorsCount) { // Act - IEnumerable result = _navigationService.GetAncestorsKeys(childKey); + var result = _navigationService.TryGetAncestorsKeys(childKey, out IEnumerable ancestorsKeys); // Assert - Assert.AreEqual(ancestorsCount, result.Count()); + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(ancestorsCount, ancestorsKeys.Count()); + }); } [Test] @@ -316,12 +324,13 @@ public void Can_Get_Ancestors_From_Existing_Content_Key_In_Correct_Order(Guid ch Guid[] expectedAncestors = Array.ConvertAll(ancestors, Guid.Parse); // Act - List result = _navigationService.GetAncestorsKeys(childKey).ToList(); + _navigationService.TryGetAncestorsKeys(childKey, out IEnumerable ancestorsKeys); + List ancestorsList = ancestorsKeys.ToList(); // Assert for (var i = 0; i < expectedAncestors.Length; i++) { - Assert.AreEqual(expectedAncestors[i], result.ElementAt(i)); + Assert.AreEqual(expectedAncestors[i], ancestorsList.ElementAt(i)); } } From 1cade451a928a464ffd49155e7dfba59ffa38600 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 24 Jul 2024 22:20:40 +0200 Subject: [PATCH 10/96] Refactor GetSiblingsKeys to TryGetSiblingsKeys --- .../Navigation/ContentNavigationService.cs | 22 +++++------ .../Services/Navigation/INavigationService.cs | 2 +- .../Services/NavigationServiceTests.cs | 38 ++++++++++++------- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs index c60fd9817ed2..e97cb2fa5ac1 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs @@ -87,33 +87,33 @@ public bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKe return true; } - public IEnumerable GetSiblingsKeys(Guid key) + public bool TryGetSiblingsKeys(Guid key, out IEnumerable siblingsKeys) { - var siblings = new List(); + siblingsKeys = []; if (_navigationStructure.TryGetValue(key, out NavigationNode? node) is false) { - return siblings; + return false; // Node doesn't exist } if (node.Parent is null) { - // To find siblings of node at root, we need to iterate over all items and add those which Parent is null - siblings = _navigationStructure + // To find siblings of a node at root level, we need to iterate over all items and add those with null Parent + siblingsKeys = _navigationStructure .Where(kv => kv.Value.Parent is null && kv.Key != key) .Select(kv => kv.Key) .ToList(); - - return siblings; + return true; } - if (TryGetChildrenKeys(node.Parent.Key, out IEnumerable childrenKeys)) + if (TryGetChildrenKeys(node.Parent.Key, out IEnumerable childrenKeys) is false) { - // Filter out the node itself to get its siblings - siblings = childrenKeys.Where(childKey => childKey != key).ToList(); + return false; // Couldn't retrieve children keys } - return siblings; + // Filter out the node itself to get its siblings + siblingsKeys = childrenKeys.Where(childKey => childKey != key).ToList(); + return true; } public bool Remove(Guid key) diff --git a/src/Umbraco.Core/Services/Navigation/INavigationService.cs b/src/Umbraco.Core/Services/Navigation/INavigationService.cs index 1865b8e67a7a..ca05238e1b98 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationService.cs @@ -12,7 +12,7 @@ public interface INavigationService bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKeys); - IEnumerable GetSiblingsKeys(Guid key); + bool TryGetSiblingsKeys(Guid key, out IEnumerable siblingsKeys); bool Remove(Guid key); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs index a51205117448..a5da0705e680 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs @@ -341,10 +341,14 @@ public void Cannot_Get_Siblings_Of_Non_Existing_Content_Key() var nonExistingKey = Guid.NewGuid(); // Act - IEnumerable result = _navigationService.GetSiblingsKeys(nonExistingKey); + var result = _navigationService.TryGetSiblingsKeys(nonExistingKey, out IEnumerable siblingsKeys); // Assert - Assert.IsEmpty(result); + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(siblingsKeys); + }); } [Test] @@ -354,13 +358,15 @@ public void Can_Get_Siblings_Of_Existing_Content_Key_Without_Self() Guid nodeKey = Child1; // Act - List result = _navigationService.GetSiblingsKeys(nodeKey).ToList(); + var result = _navigationService.TryGetSiblingsKeys(nodeKey, out IEnumerable siblingsKeys); + List siblingsList = siblingsKeys.ToList(); // Assert Assert.Multiple(() => { - Assert.IsNotEmpty(result); - Assert.IsFalse(result.Contains(nodeKey)); + Assert.IsTrue(result); + Assert.IsNotEmpty(siblingsList); + Assert.IsFalse(siblingsList.Contains(nodeKey)); }); } @@ -372,14 +378,15 @@ public void Can_Get_Siblings_Of_Existing_Content_Key_At_Content_Root() _navigationService.Add(anotherRoot); // Act - List result = _navigationService.GetSiblingsKeys(anotherRoot).ToList(); + _navigationService.TryGetSiblingsKeys(anotherRoot, out IEnumerable siblingsKeys); + List siblingsList = siblingsKeys.ToList(); // Assert Assert.Multiple(() => { - Assert.IsNotEmpty(result); - Assert.AreEqual(1, result.Count); - Assert.AreEqual(Root, result.First()); + Assert.IsNotEmpty(siblingsList); + Assert.AreEqual(1, siblingsList.Count); + Assert.AreEqual(Root, siblingsList.First()); }); } @@ -396,10 +403,14 @@ public void Can_Get_Siblings_Of_Existing_Content_Key_At_Content_Root() public void Can_Get_Siblings_Of_Existing_Content_Key(Guid key, int siblingsCount) { // Act - IEnumerable result = _navigationService.GetSiblingsKeys(key); + var result = _navigationService.TryGetSiblingsKeys(key, out IEnumerable siblingsKeys); // Assert - Assert.AreEqual(siblingsCount, result.Count()); + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(siblingsCount, siblingsKeys.Count()); + }); } [Test] @@ -412,12 +423,13 @@ public void Can_Get_Siblings_Of_Existing_Content_Key_In_Correct_Order(Guid child Guid[] expectedSiblings = Array.ConvertAll(siblings, Guid.Parse); // Act - List result = _navigationService.GetSiblingsKeys(childKey).ToList(); + _navigationService.TryGetSiblingsKeys(childKey, out IEnumerable siblingsKeys); + List siblingsList = siblingsKeys.ToList(); // Assert for (var i = 0; i < expectedSiblings.Length; i++) { - Assert.AreEqual(expectedSiblings[i], result.ElementAt(i)); + Assert.AreEqual(expectedSiblings[i], siblingsList.ElementAt(i)); } } From 2f646c390c62c08c7aa3e9db766d34e3c640c480 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 24 Jul 2024 22:21:02 +0200 Subject: [PATCH 11/96] Refactor TryGetChildrenKeys --- .../Services/Navigation/ContentNavigationService.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs index e97cb2fa5ac1..f9efefbcc910 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs @@ -38,15 +38,15 @@ public bool TryGetParentKey(Guid childKey, out Guid? parentKey) public bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys) { - if (_navigationStructure.TryGetValue(parentKey, out NavigationNode? parentNode)) + if (_navigationStructure.TryGetValue(parentKey, out NavigationNode? parentNode) is false) { - childrenKeys = parentNode.Children.Select(child => child.Key); - return true; + // Parent doesn't exist + childrenKeys = []; + return false; } - // Parent doesn't exist - childrenKeys = []; - return false; + childrenKeys = parentNode.Children.Select(child => child.Key); + return true; } public bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys) From 75d717c98c90122d81eb514ff510c5c101af5888 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 11:49:56 +0200 Subject: [PATCH 12/96] Initial integration tests --- .../Services/NavigationServiceTests.cs | 464 +++++++++++------- 1 file changed, 300 insertions(+), 164 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs index 131a12ab45c2..1e45517d9ac5 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs @@ -1,6 +1,8 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Tests.Common.Builders; @@ -17,8 +19,30 @@ public class NavigationServiceTests : UmbracoIntegrationTest private IContentService ContentService => GetRequiredService(); + private IContentEditingService ContentEditingService => GetRequiredService(); + private INavigationService NavigationService => GetRequiredService(); + private ContentType ContentType { get; set; } + + private Content Root { get; set; } + + private Content Child1 { get; set; } + + private Content Grandchild1 { get; set; } + + private Content Grandchild2 { get; set; } + + private Content Child2 { get; set; } + + private Content Grandchild3 { get; set; } + + private Content GreatGrandchild1 { get; set; } + + private Content Child3 { get; set; } + + private Content Grandchild4 { get; set; } + [SetUp] public async Task Setup() { @@ -32,244 +56,356 @@ public async Task Setup() // - Child 3 // - Grandchild 4 + // TODO: DELETE + // Root - Guid.Parse("E48DD82A-7059-418E-9B82-CDD5205796CF"); // Root's key + // - Child 1 - Guid.Parse("C6173927-0C59-4778-825D-D7B9F45D8DDE"); // Child 1's key + // - Grandchild 1 - Guid.Parse("E856AC03-C23E-4F63-9AA9-681B42A58573"); // Grandchild 1's key + // - Grandchild 2 - Guid.Parse("A1B1B217-B02F-4307-862C-A5E22DB729EB"); // Grandchild 2's key + // - Child 2 - Guid.Parse("60E0E5C4-084E-4144-A560-7393BEAD2E96"); // Child 2's key + // - Grandchild 3 - Guid.Parse("D63C1621-C74A-4106-8587-817DEE5FB732"); // Grandchild 3's key + // - Great-grandchild 1 - Guid.Parse("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"); // Great-grandchild 1's key + // - Child 3 - Guid.Parse("B606E3FF-E070-4D46-8CB9-D31352029FDF"); // Child 3's key + // - Grandchild 4 - Guid.Parse("F381906C-223C-4466-80F7-B63B4EE073F8"); // Grandchild 4's key + // Doc Type - var 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); + 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); // Content - Content root = ContentBuilder.CreateSimpleContent(contentType, "Root"); - root.Key = new Guid("E48DD82A-7059-418E-9B82-CDD5205796CF"); - ContentService.Save(root, Constants.Security.SuperUserId); + Root = ContentBuilder.CreateSimpleContent(ContentType, "Root"); + Root.Key = new Guid("E48DD82A-7059-418E-9B82-CDD5205796CF"); + ContentService.Save(Root, Constants.Security.SuperUserId); - Content child1 = ContentBuilder.CreateSimpleContent(contentType, "Child 1", root.Id); - child1.Key = new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"); - ContentService.Save(child1, Constants.Security.SuperUserId); + Child1 = ContentBuilder.CreateSimpleContent(ContentType, "Child 1", Root.Id); + Child1.Key = new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"); + ContentService.Save(Child1, Constants.Security.SuperUserId); - Content grandchild1 = ContentBuilder.CreateSimpleContent(contentType, "Grandchild 1", child1.Id); - grandchild1.Key = new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"); - ContentService.Save(grandchild1, Constants.Security.SuperUserId); + Grandchild1 = ContentBuilder.CreateSimpleContent(ContentType, "Grandchild 1", Child1.Id); + Grandchild1.Key = new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"); + ContentService.Save(Grandchild1, Constants.Security.SuperUserId); - Content grandchild2 = ContentBuilder.CreateSimpleContent(contentType, "Grandchild 2", child1.Id); - grandchild2.Key = new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"); - ContentService.Save(grandchild2, Constants.Security.SuperUserId); + Grandchild2 = ContentBuilder.CreateSimpleContent(ContentType, "Grandchild 2", Child1.Id); + Grandchild2.Key = new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"); + ContentService.Save(Grandchild2, Constants.Security.SuperUserId); - Content child2 = ContentBuilder.CreateSimpleContent(contentType, "Child 2", root.Id); - child2.Key = new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"); - ContentService.Save(child2, Constants.Security.SuperUserId); + Child2 = ContentBuilder.CreateSimpleContent(ContentType, "Child 2", Root.Id); + Child2.Key = new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"); + ContentService.Save(Child2, Constants.Security.SuperUserId); - Content grandchild3 = ContentBuilder.CreateSimpleContent(contentType, "Grandchild 3", child2.Id); - grandchild3.Key = new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"); - ContentService.Save(grandchild3, Constants.Security.SuperUserId); + Grandchild3 = ContentBuilder.CreateSimpleContent(ContentType, "Grandchild 3", Child2.Id); + Grandchild3.Key = new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"); + ContentService.Save(Grandchild3, Constants.Security.SuperUserId); - Content greatGrandchild1 = ContentBuilder.CreateSimpleContent(contentType, "Great-grandchild 1", grandchild3.Id); - greatGrandchild1.Key = new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"); - ContentService.Save(greatGrandchild1, Constants.Security.SuperUserId); + GreatGrandchild1 = ContentBuilder.CreateSimpleContent(ContentType, "Great-grandchild 1", Grandchild3.Id); + GreatGrandchild1.Key = new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"); + ContentService.Save(GreatGrandchild1, Constants.Security.SuperUserId); - Content child3 = ContentBuilder.CreateSimpleContent(contentType, "Child 3", root.Id); - child3.Key = new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"); - ContentService.Save(child3, Constants.Security.SuperUserId); + Child3 = ContentBuilder.CreateSimpleContent(ContentType, "Child 3", Root.Id); + Child3.Key = new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"); + ContentService.Save(Child3, Constants.Security.SuperUserId); - Content grandchild4 = ContentBuilder.CreateSimpleContent(contentType, "Grandchild 3", child3.Id); - grandchild4.Key = new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"); - ContentService.Save(grandchild4, Constants.Security.SuperUserId); + Grandchild4 = ContentBuilder.CreateSimpleContent(ContentType, "Grandchild 4", Child3.Id); + Grandchild4.Key = new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"); + ContentService.Save(Grandchild4, Constants.Security.SuperUserId); } + // protected override void CustomTestSetup(IUmbracoBuilder builder) + // { + // builder.Services.AddHostedService(); + // } + [Test] - public async Task Cannot_Get_Parent_From_Non_Existing_Content_Key() + public void Structure_Does_Not_Update_When_Scope_Is_Not_Completed() { - // Act - Guid? result = await NavigationService.GetParentKeyAsync(Guid.NewGuid()); + // Arrange + Content notCreatedRoot = ContentBuilder.CreateSimpleContent(ContentType, "Root 2"); + notCreatedRoot.Key = new Guid("516927E5-8574-497B-B45B-E27EFAB47DE4"); - // Assert - Assert.IsNull(result); - } + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + ContentService.Save(notCreatedRoot, Constants.Security.SuperUserId); // TODO: change to Content editing service + } - [Test] - [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", null)] // Root - [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 1 - [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 1 - [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 2 - [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 2 - [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", "60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Grandchild 3 - [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", "D63C1621-C74A-4106-8587-817DEE5FB732")] // Great-grandchild 1 - [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 3 - [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", "B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Grandchild 4 - public async Task Can_Get_Parent_From_Existing_Content_Key(Guid childKey, Guid? parentKey) - { // Act - Guid? result = await NavigationService.GetParentKeyAsync(childKey); + var result = NavigationService.TryGetSiblingsKeys(notCreatedRoot.Key, out IEnumerable siblingsKeys); // Assert Assert.Multiple(() => { - if (parentKey is null) - { - Assert.IsNull(result); - } - else - { - Assert.IsNotNull(result); - Assert.AreEqual(parentKey, result); - } + Assert.IsFalse(result); + Assert.IsEmpty(siblingsKeys); }); } [Test] - public async Task Cannot_Get_Children_From_Non_Existing_Content_Key() + public async Task Structure_Updates_When_Creating_Content() { - // Act - IEnumerable result = await NavigationService.GetChildrenKeysAsync(Guid.NewGuid()); + // Arrange + NavigationService.TryGetSiblingsKeys(Root.Key, out IEnumerable initialSiblingsKeys); + var initialRootNodeSiblingsCount = initialSiblingsKeys.Count(); - // Assert - Assert.IsEmpty(result); - } + var createModel = new ContentCreateModel + { + ContentTypeKey = ContentType.Key, + ParentKey = Constants.System.RootKey, // Create node at content root + InvariantName = "Test", + }; - [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 async Task Can_Get_Children_From_Existing_Content_Key(Guid parentKey, int childrenCount) - { // Act - IEnumerable result = await NavigationService.GetChildrenKeysAsync(parentKey); + var createAttempt = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Guid createdItemKey = createAttempt.Result.Content!.Key; + + // Verify that the structure has updated by checking the siblings list of the Root once again + NavigationService.TryGetSiblingsKeys(Root.Key, out IEnumerable updatedSiblingsKeys); + List siblingsList = updatedSiblingsKeys.ToList(); // Assert - Assert.AreEqual(childrenCount, result.Count()); + Assert.Multiple(() => + { + Assert.IsNotEmpty(siblingsList); + Assert.AreEqual(initialRootNodeSiblingsCount + 1, siblingsList.Count); + Assert.AreEqual(createdItemKey, siblingsList.First()); + }); } [Test] - [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new[] { "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 - [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new string[0])] // Grandchild 1 - [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", new[] { "D63C1621-C74A-4106-8587-817DEE5FB732" })] // Child 2 - [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", new[] { "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Grandchild 3 - [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new string[0])] // Great-grandchild 1 - [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", new[] { "F381906C-223C-4466-80F7-B63B4EE073F8" })] // Child 3 - public async Task Can_Get_Children_From_Existing_Content_Key_In_Correct_Order(Guid parentKey, string[] children) + public async Task Structure_Does_Not_Update_When_Updating_Content() { // Arrange - Guid[] expectedChildren = Array.ConvertAll(children, Guid.Parse); + Guid nodeToUpdate = Root.Key; - // Act - IEnumerable result = await NavigationService.GetChildrenKeysAsync(parentKey); + // Capture initial state + NavigationService.TryGetParentKey(nodeToUpdate, out Guid? initialParentKey); + NavigationService.TryGetChildrenKeys(nodeToUpdate, out IEnumerable initialChildrenKeys); + NavigationService.TryGetDescendantsKeys(nodeToUpdate, out IEnumerable initialDescendantsKeys); + NavigationService.TryGetAncestorsKeys(nodeToUpdate, out IEnumerable initialAncestorsKeys); + NavigationService.TryGetSiblingsKeys(nodeToUpdate, out IEnumerable initialSiblingsKeys); - // Assert - for (var i = 0; i < expectedChildren.Length; i++) + var updateModel = new ContentUpdateModel { - Assert.AreEqual(expectedChildren[i], result.ElementAt(i)); - } - } + InvariantName = "Updated Root", + }; - [Test] - public async Task Cannot_Get_Descendants_From_Non_Existing_Content_Key() - { // Act - IEnumerable result = await NavigationService.GetDescendantsKeysAsync(Guid.NewGuid()); + var updateAttempt = await ContentEditingService.UpdateAsync(nodeToUpdate, updateModel, Constants.Security.SuperUserKey); + Guid updatedItemKey = updateAttempt.Result.Content!.Key; + + // Capture updated state + var parentExists = NavigationService.TryGetParentKey(updatedItemKey, out Guid? updatedParentKey); + NavigationService.TryGetChildrenKeys(updatedItemKey, out IEnumerable childrenKeysAfterUpdate); + NavigationService.TryGetDescendantsKeys(updatedItemKey, out IEnumerable descendantsKeysAfterUpdate); + NavigationService.TryGetAncestorsKeys(updatedItemKey, out IEnumerable ancestorsKeysAfterUpdate); + NavigationService.TryGetSiblingsKeys(updatedItemKey, out IEnumerable siblingsKeysAfterUpdate); // Assert - Assert.IsEmpty(result); - } + Assert.Multiple(() => + { + // Verify that the item is still present in the navigation structure + Assert.IsTrue(parentExists); - [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 async Task Can_Get_Descendants_From_Existing_Content_Key(Guid parentKey, int descendantsCount) - { - // Act - IEnumerable result = await NavigationService.GetDescendantsKeysAsync(parentKey); + Assert.AreEqual(nodeToUpdate, updatedItemKey); - // Assert - Assert.AreEqual(descendantsCount, result.Count()); + // Verify that nothing's changed + Assert.AreEqual(initialParentKey, updatedParentKey); + CollectionAssert.AreEquivalent(initialChildrenKeys, childrenKeysAfterUpdate); + CollectionAssert.AreEquivalent(initialDescendantsKeys, descendantsKeysAfterUpdate); + CollectionAssert.AreEquivalent(initialAncestorsKeys, ancestorsKeysAfterUpdate); + CollectionAssert.AreEquivalent(initialSiblingsKeys, siblingsKeysAfterUpdate); + }); } + // TODO: test that item exists in recycle bin str. and it is removed from content str; + // TODO: also check that initial siblings count's decreased, + // TODO: and that descendants are still the same (i.e. they've also been moved to recycle bin) + // [Test] + // public async Task Structure_Updates_When_Moving_Content_To_Recycle_Bin() // TODO: Missing implementation + // { + // // Arrange + // Guid nodeToMoveToRecycleBin = Child3.Key; + // NavigationService.TryGetParentKey(nodeToMoveToRecycleBin, out Guid? originalParentKey); + // + // // Act + // await ContentEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey); + // var nodeExists = NavigationService.TryGetParentKey(nodeToMoveToRecycleBin, out Guid? updatedParentKey); // Verify that the item is no longer in the content structure + // var nodeExistsInRecycleBin = NavigationService.TryGetParentKey(nodeToMoveToRecycleBin, out Guid? updatedParentKey); // TODO: use recycle bin str. + // + // // Assert + // Assert.Multiple(() => + // { + // Assert.IsFalse(nodeExists); + // Assert.IsTrue(nodeExistsInRecycleBin); + // Assert.AreNotEqual(originalParentKey, updatedParentKey); + // Assert.IsNull(updatedParentKey); // Verify the node's parent is now located at the root of the recycle bin (null) + // }); + // } + + // TODO: check that the descendants have also been removed from both structures - navigation and trash + // [Test] + // public async Task Structure_Updates_When_Deleting_From_Recycle_Bin() // TODO: Missing implementation + // { + // // Arrange + // Guid nodeToDelete = Child1.Key; + // Guid nodeInRecycleBin = Grandchild4.Key; + // + // // Move nodes to recycle bin + // await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); // Make sure we have an item already in the recycle bin to act as a sibling + // await ContentEditingService.MoveToRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); // Make sure the item is in the recycle bin + // NavigationService.TryGetSiblingsKeys(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); // TODO: call trashed items str. + // + // // Act + // await ContentEditingService.DeleteFromRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); + // NavigationService.TryGetSiblingsKeys(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); // TODO: call trashed items str. + // + // // Assert + // // Verify siblings count has decreased by one + // Assert.AreEqual(initialSiblingsKeys.Count() - 1, updatedSiblingsKeys.Count()); + // } + [Test] - [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new[] { "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" })] // Root - [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", new[] { "E856AC03-C23E-4F63-9AA9-681B42A58573", "A1B1B217-B02F-4307-862C-A5E22DB729EB" })] // Child 1 - [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new string[0])] // Grandchild 1 - [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", new[] { "D63C1621-C74A-4106-8587-817DEE5FB732", "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Child 2 - [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", new[] { "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Grandchild 3 - [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new string[0])] // Great-grandchild 1 - [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", new[] { "F381906C-223C-4466-80F7-B63B4EE073F8" })] // Child 3 - public async Task Can_Get_Descendants_From_Existing_Content_Key_In_Correct_Order(Guid parentKey, string[] descendants) + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Grandchild 1 to Child 2 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", null)] // Child 3 to content root + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 2 to Child 1 + public async Task Structure_Updates_When_Moving_Content(Guid nodeToMove, Guid? targetParentKey) { // Arrange - Guid[] expectedDescendants = Array.ConvertAll(descendants, Guid.Parse); + NavigationService.TryGetParentKey(nodeToMove, out Guid? originalParentKey); // Act - IEnumerable result = await NavigationService.GetDescendantsKeysAsync(parentKey); + var moveAttempt = await ContentEditingService.MoveAsync(nodeToMove, targetParentKey, Constants.Security.SuperUserKey); + + // Verify the node's new parent is updated + NavigationService.TryGetParentKey(moveAttempt.Result!.Key, out Guid? updatedParentKey); // Assert - for (var i = 0; i < expectedDescendants.Length; i++) + Assert.Multiple(() => { - Assert.AreEqual(expectedDescendants[i], result.ElementAt(i)); - } + if (targetParentKey is null) + { + Assert.IsNull(updatedParentKey); + } + else + { + Assert.IsNotNull(updatedParentKey); + } + + Assert.AreNotEqual(originalParentKey, updatedParentKey); + Assert.AreEqual(targetParentKey, updatedParentKey); + }); } + // TODO: test that it is added to its new parent - check parent's children + // TODO: test that it has the same amount of descendants - depending on value of includeDescendants param + // TODO: test that the number of target parent descendants updates when copying node with descendants + // TODO: test that copied node descendants have different keys than source node descendants [Test] - public async Task Cannot_Get_Ancestors_From_Non_Existing_Content_Key() + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2 to itself + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", null)] // Child 2 to content root + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 3 to Child 1 + public async Task Structure_Updates_When_Copying_Content(Guid nodeToCopy, Guid? targetParentKey) { + // Arrange + NavigationService.TryGetParentKey(nodeToCopy, out Guid? sourceParentKey); + // Act - IEnumerable result = await NavigationService.GetAncestorsKeysAsync(Guid.NewGuid()); + var copyAttempt = await ContentEditingService.CopyAsync(nodeToCopy, targetParentKey, false, false, Constants.Security.SuperUserKey); + Guid copiedItemKey = copyAttempt.Result.Key; // Assert - Assert.IsEmpty(result); - } + Assert.AreNotEqual(nodeToCopy, copiedItemKey); - [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 async Task Can_Get_Ancestors_From_Existing_Content_Key(Guid childKey, int ancestorsCount) - { - // Act - IEnumerable result = await NavigationService.GetAncestorsKeysAsync(childKey); + NavigationService.TryGetParentKey(copiedItemKey, out Guid? copiedItemParentKey); - // Assert - Assert.AreEqual(ancestorsCount, result.Count()); + Assert.Multiple(() => + { + if (targetParentKey is null) + { + // Verify the copied node's parent is null (it's been copied to content root) + Assert.IsNull(copiedItemParentKey); + } + else + { + Assert.IsNotNull(copiedItemParentKey); + } + + Assert.AreEqual(targetParentKey, copiedItemParentKey); + Assert.AreNotEqual(sourceParentKey, copiedItemParentKey); + }); } [Test] - [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new string[0])] // Root - [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", new[] { "E48DD82A-7059-418E-9B82-CDD5205796CF" })] // Child 1 - [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new[] { "C6173927-0C59-4778-825D-D7B9F45D8DDE", "E48DD82A-7059-418E-9B82-CDD5205796CF" })] // Grandchild 1 - [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new[] { "D63C1621-C74A-4106-8587-817DEE5FB732", "60E0E5C4-084E-4144-A560-7393BEAD2E96", "E48DD82A-7059-418E-9B82-CDD5205796CF" })] // Great-grandchild 1 - public async Task Can_Get_Ancestors_From_Existing_Content_Key_In_Correct_Order(Guid childKey, string[] ancestors) + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + public async Task Structure_Updates_When_Deleting_Content(Guid nodeToDelete) { // Arrange - Guid[] expectedAncestors = Array.ConvertAll(ancestors, Guid.Parse); + NavigationService.TryGetDescendantsKeys(nodeToDelete, out IEnumerable initialDescendantsKeys); // Act - IEnumerable result = await NavigationService.GetAncestorsKeysAsync(childKey); + var deleteAttempt = await ContentEditingService.DeleteAsync(nodeToDelete, Constants.Security.SuperUserKey); + Guid deletedItemKey = deleteAttempt.Result.Key; // Assert - for (var i = 0; i < expectedAncestors.Length; i++) + var nodeExists = NavigationService.TryGetDescendantsKeys(deletedItemKey, out _); + //var nodeExistsInRecycleBin = NavigationService.TryGetDescendantsKeys(nodeToDelete, out _); // TODO: use recycle bin str. + + Assert.Multiple(() => { - Assert.AreEqual(expectedAncestors[i], result.ElementAt(i)); - } + Assert.AreEqual(nodeToDelete, deletedItemKey); + Assert.IsFalse(nodeExists); + //Assert.IsFalse(nodeExistsInRecycleBin); + + foreach (Guid descendant in initialDescendantsKeys) + { + var descendantExists = NavigationService.TryGetParentKey(descendant, out _); + Assert.IsFalse(descendantExists); + + // var descendantExistsInRecycleBin = NavigationService.TryGetParentKey(descendant, out _); // TODO: use recycle bin str. + // Assert.IsFalse(descendantExistsInRecycleBin); + } + }); } + + // [Test] + // public async Task Structure_Updates_When_Restoring_Content(Guid nodeToRestore, Guid? targetParentKey) // todo: test with target parent null + // { + // // Arrange + // Guid nodeInRecycleBin = GreatGrandchild1.Key; + // + // // Move nodes to recycle bin + // await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); // Make sure we have an item already in the recycle bin to act as a sibling + // await ContentEditingService.MoveToRecycleBinAsync(nodeToRestore, Constants.Security.SuperUserKey); // Make sure the item is in the recycle bin + // NavigationService.TryGetParentKey(nodeToRestore, out Guid? initialParentKey); + // NavigationService.TryGetSiblingsKeys(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); // TODO: call trashed items str. + // + // // Act + // var restoreAttempt = await ContentEditingService.RestoreAsync(nodeToRestore, targetParentKey, Constants.Security.SuperUserKey); + // Guid restoredItemKey = restoreAttempt.Result.Key; + // + // // Assert + // NavigationService.TryGetParentKey(restoredItemKey, out Guid? restoredItemParentKey); + // NavigationService.TryGetSiblingsKeys(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); // TODO: call trashed items str. + // + // Assert.Multiple(() => + // { + // // Verify siblings count has decreased by one + // Assert.AreEqual(initialSiblingsKeys.Count() - 1, updatedSiblingsKeys.Count()); + // + // if (targetParentKey is null) + // { + // Assert.IsNull(restoredItemParentKey); + // } + // else + // { + // Assert.IsNotNull(restoredItemParentKey); + // } + // + // Assert.AreNotEqual(initialParentKey, restoredItemParentKey); + // Assert.AreEqual(targetParentKey, restoredItemParentKey); + // }); + // } } From 9a4565e43c397421cbadecde106e7774da66b84e Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 13:44:52 +0200 Subject: [PATCH 13/96] Use ContentEditingService instead of ContentService --- .../Services/NavigationServiceTests.cs | 108 ++++++++++-------- 1 file changed, 59 insertions(+), 49 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs index 1e45517d9ac5..dd16ab750526 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs @@ -17,31 +17,29 @@ public class NavigationServiceTests : UmbracoIntegrationTest { private IContentTypeService ContentTypeService => GetRequiredService(); - private IContentService ContentService => GetRequiredService(); - private IContentEditingService ContentEditingService => GetRequiredService(); private INavigationService NavigationService => GetRequiredService(); private ContentType ContentType { get; set; } - private Content Root { get; set; } + private IContent Root { get; set; } - private Content Child1 { get; set; } + private IContent Child1 { get; set; } - private Content Grandchild1 { get; set; } + private IContent Grandchild1 { get; set; } - private Content Grandchild2 { get; set; } + private IContent Grandchild2 { get; set; } - private Content Child2 { get; set; } + private IContent Child2 { get; set; } - private Content Grandchild3 { get; set; } + private IContent Grandchild3 { get; set; } - private Content GreatGrandchild1 { get; set; } + private IContent GreatGrandchild1 { get; set; } - private Content Child3 { get; set; } + private IContent Child3 { get; set; } - private Content Grandchild4 { get; set; } + private IContent Grandchild4 { get; set; } [SetUp] public async Task Setup() @@ -76,41 +74,41 @@ public async Task Setup() await ContentTypeService.CreateAsync(ContentType, Constants.Security.SuperUserKey); // Content - Root = ContentBuilder.CreateSimpleContent(ContentType, "Root"); - Root.Key = new Guid("E48DD82A-7059-418E-9B82-CDD5205796CF"); - ContentService.Save(Root, Constants.Security.SuperUserId); + var rootModel = CreateContentCreateModel("Root", new Guid("E48DD82A-7059-418E-9B82-CDD5205796CF")); + var rootCreateAttempt = await ContentEditingService.CreateAsync(rootModel, Constants.Security.SuperUserKey); + Root = rootCreateAttempt.Result.Content!; - Child1 = ContentBuilder.CreateSimpleContent(ContentType, "Child 1", Root.Id); - Child1.Key = new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"); - ContentService.Save(Child1, Constants.Security.SuperUserId); + var child1Model = CreateContentCreateModel("Child 1", new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"), Root.Key); + var child1CreateAttempt = await ContentEditingService.CreateAsync(child1Model, Constants.Security.SuperUserKey); + Child1 = child1CreateAttempt.Result.Content!; - Grandchild1 = ContentBuilder.CreateSimpleContent(ContentType, "Grandchild 1", Child1.Id); - Grandchild1.Key = new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"); - ContentService.Save(Grandchild1, Constants.Security.SuperUserId); + var grandchild1Model = CreateContentCreateModel("Grandchild 1", new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"), Child1.Key); + var grandchild1CreateAttempt = await ContentEditingService.CreateAsync(grandchild1Model, Constants.Security.SuperUserKey); + Grandchild1 = grandchild1CreateAttempt.Result.Content!; - Grandchild2 = ContentBuilder.CreateSimpleContent(ContentType, "Grandchild 2", Child1.Id); - Grandchild2.Key = new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"); - ContentService.Save(Grandchild2, Constants.Security.SuperUserId); + var grandchild2Model = CreateContentCreateModel("Grandchild 2", new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"), Child1.Key); + var grandchild2CreateAttempt = await ContentEditingService.CreateAsync(grandchild2Model, Constants.Security.SuperUserKey); + Grandchild2 = grandchild2CreateAttempt.Result.Content!; - Child2 = ContentBuilder.CreateSimpleContent(ContentType, "Child 2", Root.Id); - Child2.Key = new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"); - ContentService.Save(Child2, Constants.Security.SuperUserId); + var child2Model = CreateContentCreateModel("Child 2", new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"), Root.Key); + var child2CreateAttempt = await ContentEditingService.CreateAsync(child2Model, Constants.Security.SuperUserKey); + Child2 = child2CreateAttempt.Result.Content!; - Grandchild3 = ContentBuilder.CreateSimpleContent(ContentType, "Grandchild 3", Child2.Id); - Grandchild3.Key = new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"); - ContentService.Save(Grandchild3, Constants.Security.SuperUserId); + var grandchild3Model = CreateContentCreateModel("Grandchild 3", new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"), Child2.Key); + var grandchild3CreateAttempt = await ContentEditingService.CreateAsync(grandchild3Model, Constants.Security.SuperUserKey); + Grandchild3 = grandchild3CreateAttempt.Result.Content!; - GreatGrandchild1 = ContentBuilder.CreateSimpleContent(ContentType, "Great-grandchild 1", Grandchild3.Id); - GreatGrandchild1.Key = new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"); - ContentService.Save(GreatGrandchild1, Constants.Security.SuperUserId); + var greatGrandchild1Model = CreateContentCreateModel("Great-grandchild 1", new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"), Grandchild3.Key); + var greatGrandchild1CreateAttempt = await ContentEditingService.CreateAsync(greatGrandchild1Model, Constants.Security.SuperUserKey); + GreatGrandchild1 = greatGrandchild1CreateAttempt.Result.Content!; - Child3 = ContentBuilder.CreateSimpleContent(ContentType, "Child 3", Root.Id); - Child3.Key = new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"); - ContentService.Save(Child3, Constants.Security.SuperUserId); + var child3Model = CreateContentCreateModel("Child 3", new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"), Root.Key); + var child3CreateAttempt = await ContentEditingService.CreateAsync(child3Model, Constants.Security.SuperUserKey); + Child3 = child3CreateAttempt.Result.Content!; - Grandchild4 = ContentBuilder.CreateSimpleContent(ContentType, "Grandchild 4", Child3.Id); - Grandchild4.Key = new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"); - ContentService.Save(Grandchild4, Constants.Security.SuperUserId); + var grandchild4Model = CreateContentCreateModel("Grandchild 4", new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"), Child3.Key); + var grandchild4CreateAttempt = await ContentEditingService.CreateAsync(grandchild4Model, Constants.Security.SuperUserKey); + Grandchild4 = grandchild4CreateAttempt.Result.Content!; } // protected override void CustomTestSetup(IUmbracoBuilder builder) @@ -119,26 +117,29 @@ public async Task Setup() // } [Test] - public void Structure_Does_Not_Update_When_Scope_Is_Not_Completed() + public async Task Structure_Does_Not_Update_When_Scope_Is_Not_Completed() { // Arrange - Content notCreatedRoot = ContentBuilder.CreateSimpleContent(ContentType, "Root 2"); - notCreatedRoot.Key = new Guid("516927E5-8574-497B-B45B-E27EFAB47DE4"); + Guid notCreatedRootKey = new Guid("516927E5-8574-497B-B45B-E27EFAB47DE4"); + + var createModel = new ContentCreateModel + { + ContentTypeKey = ContentType.Key, + ParentKey = Constants.System.RootKey, // Create node at content root + InvariantName = "Root 2", + Key = notCreatedRootKey, + }; using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - ContentService.Save(notCreatedRoot, Constants.Security.SuperUserId); // TODO: change to Content editing service + await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); } // Act - var result = NavigationService.TryGetSiblingsKeys(notCreatedRoot.Key, out IEnumerable siblingsKeys); + var nodeExists = NavigationService.TryGetParentKey(notCreatedRootKey, out _); // Assert - Assert.Multiple(() => - { - Assert.IsFalse(result); - Assert.IsEmpty(siblingsKeys); - }); + Assert.IsFalse(nodeExists); } [Test] @@ -152,7 +153,7 @@ public async Task Structure_Updates_When_Creating_Content() { ContentTypeKey = ContentType.Key, ParentKey = Constants.System.RootKey, // Create node at content root - InvariantName = "Test", + InvariantName = "Root 2", }; // Act @@ -408,4 +409,13 @@ public async Task Structure_Updates_When_Deleting_Content(Guid nodeToDelete) // Assert.AreEqual(targetParentKey, restoredItemParentKey); // }); // } + + private ContentCreateModel CreateContentCreateModel(string name, Guid key, Guid? parentKey = null) + => new() + { + ContentTypeKey = ContentType.Key, + ParentKey = parentKey ?? Constants.System.RootKey, + InvariantName = name, + Key = key, + }; } From 1b3d27d3fc0860dffe69a3a76a82f9ac1e468474 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 13:51:05 +0200 Subject: [PATCH 14/96] Remove INavigationService.Copy implementation and unit tests --- .../Navigation/ContentNavigationService.cs | 41 ---- .../Services/Navigation/INavigationService.cs | 2 - .../Services/NavigationServiceTests.cs | 219 ------------------ 3 files changed, 262 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs index f9efefbcc910..c9d6e01aef23 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs @@ -162,32 +162,6 @@ public bool Add(Guid key, Guid? parentKey = null) return true; } - public bool Copy(Guid sourceKey, out Guid copiedNodeKey, Guid? targetParentKey = null) - { - copiedNodeKey = Guid.Empty; // Default value - - if (_navigationStructure.TryGetValue(sourceKey, out NavigationNode? sourceNode) is false) - { - return false; // Source doesn't exist - } - - NavigationNode? targetParentNode = null; - if (targetParentKey.HasValue && _navigationStructure.TryGetValue(targetParentKey.Value, out targetParentNode) is false) - { - return false; // Target parent doesn't exist - } - - var newNodesMap = new Dictionary(); - CopyNodeHierarchyRecursively(sourceNode, targetParentNode, newNodesMap, out copiedNodeKey); - - foreach (NavigationNode newNode in newNodesMap.Values) - { - _navigationStructure[newNode.Key] = newNode; - } - - return true; - } - public bool Move(Guid nodeKey, Guid? targetParentKey = null) { if (_navigationStructure.TryGetValue(nodeKey, out NavigationNode? nodeToMove) is false) @@ -251,19 +225,4 @@ private void CopyChildren(NavigationNode originalNode, NavigationNode newNode) newNode.AddChild(child); } } - - private void CopyNodeHierarchyRecursively(NavigationNode sourceNode, NavigationNode? newParent, Dictionary newNodesMap, out Guid topmostCopiedNodeKey) - { - topmostCopiedNodeKey = Guid.NewGuid(); // TODO: pass in the key or get it from the DB? - var newNode = new NavigationNode(topmostCopiedNodeKey); - - newParent?.AddChild(newNode); - - newNodesMap[topmostCopiedNodeKey] = newNode; - - foreach (NavigationNode child in sourceNode.Children) - { - CopyNodeHierarchyRecursively(child, newNode, newNodesMap, out _); - } - } } diff --git a/src/Umbraco.Core/Services/Navigation/INavigationService.cs b/src/Umbraco.Core/Services/Navigation/INavigationService.cs index ca05238e1b98..f5a02d2911d1 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationService.cs @@ -18,7 +18,5 @@ public interface INavigationService bool Add(Guid key, Guid? parentKey = null); - bool Copy(Guid sourceKey, out Guid copiedNodeKey, Guid? targetParentKey = null); - bool Move(Guid nodeKey, Guid? targetParentKey = null); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs index a5da0705e680..3c60796c1514 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs @@ -535,225 +535,6 @@ public void Can_Add_Node_To_Parent(Guid parentKey) }); } - [Test] - public void Cannot_Copy_When_Target_Parent_Does_Not_Exist() - { - // Arrange - Guid nodeToCopy = Child1; - var nonExistentTargetParentKey = Guid.NewGuid(); - - // Act - var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, nonExistentTargetParentKey); - - // Assert - Assert.Multiple(() => - { - Assert.IsFalse(result); - Assert.AreEqual(Guid.Empty, copiedNodeKey); - }); - } - - [Test] - public void Cannot_Copy_When_Source_Node_Does_Not_Exist() - { - // Arrange - var nonExistentSourceNodeKey = Guid.NewGuid(); - Guid targetParentKey = Grandchild1; - - // Act - var result = _navigationService.Copy(nonExistentSourceNodeKey, out Guid copiedNodeKey, targetParentKey); - - // Assert - Assert.Multiple(() => - { - Assert.IsFalse(result); - Assert.AreEqual(Guid.Empty, copiedNodeKey); - }); - } - - [Test] - public void Can_Copy_Node_To_Itself() - { - // Arrange - Guid nodeToCopy = GreatGrandchild1; - - // Act - var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey); - - // Assert - Assert.Multiple(() => - { - Assert.IsTrue(result); - Assert.AreNotEqual(Guid.Empty, copiedNodeKey); - Assert.AreNotEqual(nodeToCopy, copiedNodeKey); - }); - } - - [Test] - public void Can_Copy_Node_To_Content_Root() - { - // Arrange - Guid nodeToCopy = Child1; - - // Act - var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey); // parentKey is null - - // Assert - Assert.Multiple(() => - { - Assert.IsTrue(result); - Assert.AreNotEqual(Guid.Empty, copiedNodeKey); - Assert.AreNotEqual(nodeToCopy, copiedNodeKey); - }); - - // Verify the copied node's parent is null (it's been copied to content root) - var parentExists = _navigationService.TryGetParentKey(copiedNodeKey, out Guid? copiedNodeParentKey); - - Assert.Multiple(() => - { - Assert.IsTrue(parentExists); - Assert.IsNull(copiedNodeParentKey); - }); - } - - [Test] - public void Can_Copy_Node_To_Existing_Target_Parent() - { - // Arrange - Guid nodeToCopy = Grandchild4; - Guid targetParentKey = Child1; - - // Act - var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, targetParentKey); - - // Assert - Assert.Multiple(() => - { - Assert.IsTrue(result); - Assert.AreNotEqual(Guid.Empty, copiedNodeKey); - Assert.AreNotEqual(nodeToCopy, copiedNodeKey); - }); - - // Verify the node is copied to the correct parent - _navigationService.TryGetParentKey(copiedNodeKey, out Guid? copiedNodeParentKey); - - Assert.Multiple(() => - { - Assert.IsNotNull(copiedNodeParentKey); - Assert.AreEqual(targetParentKey, copiedNodeParentKey); - }); - } - - [Test] - public void Copying_Node_Does_Not_Update_Source_Node_Parent() - { - // Arrange - Guid nodeToCopy = Grandchild1; - Guid targetParentKey = Child3; - _navigationService.TryGetParentKey(nodeToCopy, out Guid? originalParentKey); - - // Act - var result = _navigationService.Copy(nodeToCopy, out _, targetParentKey); - - // Assert - Assert.IsTrue(result); - - // Verify that the original parent is still the same - _navigationService.TryGetParentKey(nodeToCopy, out Guid? currentParentKey); - - Assert.AreEqual(originalParentKey, currentParentKey); - } - - [Test] - public void Copied_Node_Is_Added_To_Its_New_Parent() - { - // Arrange - Guid nodeToCopy = Grandchild2; - Guid targetParentKey = Child2; - _navigationService.TryGetChildrenKeys(targetParentKey, out IEnumerable targetParentChildrenKeys); - var targetParentChildrenCount = targetParentChildrenKeys.Count(); - - // Act - var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, targetParentKey); - - // Assert - Assert.IsTrue(result); - - // Verify the node is added to its new parent's children list - _navigationService.TryGetChildrenKeys(targetParentKey, out IEnumerable childrenKeys); - List childrenList = childrenKeys.ToList(); - - Assert.Multiple(() => - { - CollectionAssert.Contains(childrenList, copiedNodeKey); - Assert.AreEqual(targetParentChildrenCount + 1, childrenList.Count); - }); - } - - [Test] - [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "60E0E5C4-084E-4144-A560-7393BEAD2E96", 0)] // Grandchild 2 to Child 2 - [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", null, 1)] // Grandchild 3 to content root - [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "B606E3FF-E070-4D46-8CB9-D31352029FDF", 2)] // Child 2 to Child 3 - public void Copied_Node_Has_The_Same_Amount_Of_Descendants(Guid nodeToCopy, Guid? targetParentKey, int initialDescendantsCount) - { - // Act - var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, targetParentKey); - - // Assert - Assert.IsTrue(result); - - // Get the number of descendants of the copied node - _navigationService.TryGetDescendantsKeys(copiedNodeKey, out IEnumerable descendantsKeys); - var descendantsCountAfterCopy = descendantsKeys.Count(); - - Assert.AreEqual(initialDescendantsCount, descendantsCountAfterCopy); - } - - [Test] - [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "E48DD82A-7059-418E-9B82-CDD5205796CF", 8)] // Grandchild 2 to Root - [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", "B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Grandchild 3 to Child 3 - [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "F381906C-223C-4466-80F7-B63B4EE073F8", 0)] // Child 2 to Grandchild 4 - public void Number_Of_Target_Parent_Descendants_Updates_When_Copying_Node_With_Descendants(Guid nodeToCopy, Guid targetParentKey, int initialDescendantsCountOfTargetParent) - { - // Arrange - // Get the number of descendants of the node to copy - _navigationService.TryGetDescendantsKeys(nodeToCopy, out IEnumerable descendantsKeys); - var descendantsCountOfNodeToCopy = descendantsKeys.Count(); - - // Act - var result = _navigationService.Copy(nodeToCopy, out _, targetParentKey); - - // Assert - Assert.IsTrue(result); - - _navigationService.TryGetDescendantsKeys(targetParentKey, out IEnumerable updatedTargetParentDescendantsKeys); - var updatedDescendantsCountOfTargetParent = updatedTargetParentDescendantsKeys.Count(); - - // Verify the number of descendants of the target parent has increased by the number of descendants of the copied node plus the node itself - Assert.AreEqual(initialDescendantsCountOfTargetParent + descendantsCountOfNodeToCopy + 1, updatedDescendantsCountOfTargetParent); - } - - [Test] - public void Copied_Node_Descendants_Have_Different_Keys_Than_Source_Node_Descendants() - { - // Arrange - Guid nodeToCopy = Child2; - Guid targetParentKey = Grandchild4; - _navigationService.TryGetDescendantsKeys(nodeToCopy, out IEnumerable sourceDescendants); - - // Act - var result = _navigationService.Copy(nodeToCopy, out Guid copiedNodeKey, targetParentKey); - - // Assert - Assert.IsTrue(result); - - // Get the descendants of the copied node - _navigationService.TryGetDescendantsKeys(copiedNodeKey, out IEnumerable copiedDescendants); - - // Ensure all keys of the copied descendants are different from the source descendants - Assert.IsTrue(copiedDescendants.All(copiedDescendantKey => sourceDescendants.Contains(copiedDescendantKey) is false)); - } - [Test] public void Cannot_Move_When_Target_Parent_Does_Not_Exist() { From fa89e2324b86198d12771aee952fb68e0f5ce078 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 13:51:34 +0200 Subject: [PATCH 15/96] Rename var --- .../Umbraco.Core/Services/NavigationServiceTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs index 3c60796c1514..c45845ff40d7 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs @@ -499,11 +499,11 @@ public void Can_Add_Node_To_Content_Root() // Assert Assert.IsTrue(result); - var parentExists = _navigationService.TryGetParentKey(newNodeKey, out Guid? parentKey); + var nodeExists = _navigationService.TryGetParentKey(newNodeKey, out Guid? parentKey); Assert.Multiple(() => { - Assert.IsTrue(parentExists); + Assert.IsTrue(nodeExists); Assert.IsNull(parentKey); }); } From 6ffe6d5354d2e203ad991c95d1c6957175734a4a Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 13:52:55 +0200 Subject: [PATCH 16/96] Adding clarification --- .../Umbraco.Core/Services/NavigationServiceTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs index dd16ab750526..6a97ec6b9913 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs @@ -17,6 +17,7 @@ public class NavigationServiceTests : UmbracoIntegrationTest { private IContentTypeService ContentTypeService => GetRequiredService(); + // Testing with IContentEditingService as it calls IContentService underneath private IContentEditingService ContentEditingService => GetRequiredService(); private INavigationService NavigationService => GetRequiredService(); From 083d5228fc17b6f6555008af9552d9f163e62e53 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 14:02:47 +0200 Subject: [PATCH 17/96] Initial ContentNavigationRepository --- .../Implement/ContentNavigationRepository.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs new file mode 100644 index 000000000000..bd2696de96f6 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs @@ -0,0 +1,49 @@ +using NPoco; +using Umbraco.Cms.Core.Models.Navigation; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Factories; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +public class ContentNavigationRepository : INavigationRepository +{ + private readonly IScopeAccessor _scopeAccessor; + + public ContentNavigationRepository(IScopeAccessor scopeAccessor) + => _scopeAccessor = scopeAccessor; + + private IScope? AmbientScope => _scopeAccessor.AmbientScope; + + public Dictionary GetContentNodesByObjectType(Guid objectTypeKey) + { + Sql? sql = AmbientScope?.SqlContext.Sql() + .Select() + .From() + .Where(x => x.NodeObjectType == objectTypeKey && x.Trashed == false) + .OrderBy(x => x.Path); // make sure that we get the parent items first + + IEnumerable navigationDtos = AmbientScope?.Database.Fetch(sql) ?? Enumerable.Empty(); + + //var navigationStructure = NavigationFactory.BuildNavigationDictionary(navigationDtos); + var contentNavigationStructure = NavigationFactory.BuildNavigationDictionary(navigationDtos, dto => dto.Trashed is false); + //var recycleBinContentNavigationStructure = NavigationFactory.BuildNavigationDictionary(navigationDtos, dto => dto.Trashed); + + return contentNavigationStructure; + } + + public Dictionary GetTrashedContentNodesByObjectType(Guid objectTypeKey) + { + Sql? sql = AmbientScope?.SqlContext.Sql() + .Select() + .From() + .Where(x => x.NodeObjectType == objectTypeKey && x.Trashed == true) + .OrderBy(x => x.Path); // make sure that we get the parent items first + + IEnumerable navigationDtos = AmbientScope?.Database.Fetch(sql) ?? Enumerable.Empty(); + + return NavigationFactory.BuildNavigationDictionary(navigationDtos); + } +} From e94974153e424cc7346785b811eb45a18b24db76 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 14:03:32 +0200 Subject: [PATCH 18/96] Initial NavigationFactory --- .../Factories/NavigationFactory.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs new file mode 100644 index 000000000000..812b05aad305 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs @@ -0,0 +1,52 @@ +using Umbraco.Cms.Core.Models.Navigation; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class NavigationFactory +{ + /// + /// Builds a dictionary of NavigationNode objects from a given dataset. + /// + /// The dataset of objects used to build the navigation nodes dictionary. + /// + /// An optional filter function that determines which items to include. If null, all items are included. + /// + /// A dictionary of objects with key corresponding to their unique guid. + public static Dictionary BuildNavigationDictionary(IEnumerable dataset, Func? filterOutItems = null) + { + var nodesStructure = new Dictionary(); + Dictionary idToKeyMap = dataset.ToDictionary(x => x.Id, x => x.Key); + + foreach (NavigationDto dto in dataset) + { + // Filter items based on criteria + if (filterOutItems?.Invoke(dto) is false) + { + continue; + } + + var node = new NavigationNode(dto.Key); + nodesStructure[dto.Key] = node; + + // We don't set the parent for items under root, it will stay null + if (dto.ParentId == -1) + { + continue; + } + + if (idToKeyMap.TryGetValue(dto.ParentId, out Guid parentKey) is false) + { + continue; + } + + // If the parent node exists in the nodesStructure, add the node to the parent's children (parent is set as well) + if (nodesStructure.TryGetValue(parentKey, out NavigationNode? parentNode)) + { + parentNode.AddChild(node); + } + } + + return nodesStructure; + } +} From 354834aa35b4f9d8e9d1d8a6ff9fe879384e56f1 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 14:20:01 +0200 Subject: [PATCH 19/96] Remove filtering from factory --- .../Persistence/Factories/NavigationFactory.cs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs index 812b05aad305..4dd831f15918 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs @@ -9,23 +9,15 @@ internal static class NavigationFactory /// Builds a dictionary of NavigationNode objects from a given dataset. /// /// The dataset of objects used to build the navigation nodes dictionary. - /// - /// An optional filter function that determines which items to include. If null, all items are included. - /// /// A dictionary of objects with key corresponding to their unique guid. - public static Dictionary BuildNavigationDictionary(IEnumerable dataset, Func? filterOutItems = null) + public static Dictionary BuildNavigationDictionary(IEnumerable dataset) { var nodesStructure = new Dictionary(); - Dictionary idToKeyMap = dataset.ToDictionary(x => x.Id, x => x.Key); + var datasetList = dataset.ToList(); + var idToKeyMap = datasetList.ToDictionary(x => x.Id, x => x.Key); - foreach (NavigationDto dto in dataset) + foreach (NavigationDto dto in datasetList) { - // Filter items based on criteria - if (filterOutItems?.Invoke(dto) is false) - { - continue; - } - var node = new NavigationNode(dto.Key); nodesStructure[dto.Key] = node; From f99a757a847db32b44428267178ec4c5e8b46932 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 14:20:43 +0200 Subject: [PATCH 20/96] NavigationRepository and implementation --- .../Repositories/INavigationRepository.cs | 10 +++++++ .../Implement/ContentNavigationRepository.cs | 27 +++++++------------ 2 files changed, 20 insertions(+), 17 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs diff --git a/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs new file mode 100644 index 000000000000..c4a49fd30a8a --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Core.Models.Navigation; + +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface INavigationRepository +{ + public Dictionary GetContentNodesByObjectType(Guid objectTypeKey); + + public Dictionary GetTrashedContentNodesByObjectType(Guid objectTypeKey); +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs index bd2696de96f6..e0fb9e0a1964 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs @@ -19,31 +19,24 @@ public ContentNavigationRepository(IScopeAccessor scopeAccessor) public Dictionary GetContentNodesByObjectType(Guid objectTypeKey) { - Sql? sql = AmbientScope?.SqlContext.Sql() - .Select() - .From() - .Where(x => x.NodeObjectType == objectTypeKey && x.Trashed == false) - .OrderBy(x => x.Path); // make sure that we get the parent items first - - IEnumerable navigationDtos = AmbientScope?.Database.Fetch(sql) ?? Enumerable.Empty(); - - //var navigationStructure = NavigationFactory.BuildNavigationDictionary(navigationDtos); - var contentNavigationStructure = NavigationFactory.BuildNavigationDictionary(navigationDtos, dto => dto.Trashed is false); - //var recycleBinContentNavigationStructure = NavigationFactory.BuildNavigationDictionary(navigationDtos, dto => dto.Trashed); - - return contentNavigationStructure; + IEnumerable navigationDtos = FetchNavigationDtos(objectTypeKey, false); + return NavigationFactory.BuildNavigationDictionary(navigationDtos); } public Dictionary GetTrashedContentNodesByObjectType(Guid objectTypeKey) + { + IEnumerable navigationDtos = FetchNavigationDtos(objectTypeKey, true); + return NavigationFactory.BuildNavigationDictionary(navigationDtos); + } + + private IEnumerable FetchNavigationDtos(Guid objectTypeKey, bool trashed) { Sql? sql = AmbientScope?.SqlContext.Sql() .Select() .From() - .Where(x => x.NodeObjectType == objectTypeKey && x.Trashed == true) + .Where(x => x.NodeObjectType == objectTypeKey && x.Trashed == trashed) .OrderBy(x => x.Path); // make sure that we get the parent items first - IEnumerable navigationDtos = AmbientScope?.Database.Fetch(sql) ?? Enumerable.Empty(); - - return NavigationFactory.BuildNavigationDictionary(navigationDtos); + return AmbientScope?.Database.Fetch(sql) ?? Enumerable.Empty(); } } From e8a2169a6b0d02af259d5ce890612f3fa5a58ac3 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 14:23:18 +0200 Subject: [PATCH 21/96] InitializationService responsible for seeding the in-memory structure --- .../NavigationInitializationService.cs | 26 +++++++++++++++++++ .../UmbracoBuilderExtensions.cs | 2 ++ 2 files changed, 28 insertions(+) create mode 100644 src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs diff --git a/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs b/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs new file mode 100644 index 000000000000..17a2837d6e44 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Hosting; + +namespace Umbraco.Cms.Core.Services.Navigation; + +public class NavigationInitializationService : IHostedLifecycleService +{ + private readonly INavigationService _navigationService; + + public NavigationInitializationService(INavigationService navigationService) + => _navigationService = navigationService; + + public async Task StartingAsync(CancellationToken cancellationToken) + { + await _navigationService.RebuildAsync(); + } + + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 85a9d7ded433..1bab0883a516 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -30,6 +30,7 @@ using Umbraco.Cms.Core.Preview; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.BackgroundJobs; @@ -193,6 +194,7 @@ public static IUmbracoBuilder AddRecurringBackgroundJobs(this IUmbracoBuilder bu builder.Services.AddSingleton(RecurringBackgroundJobHostedService.CreateHostedServiceFactory); builder.Services.AddHostedService(); builder.Services.AddHostedService(); + builder.Services.AddHostedService(); return builder; } From b416a5dbf8efe7405770ed79812eca01fff53a86 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 14:24:52 +0200 Subject: [PATCH 22/96] Register repository and service --- src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs | 4 +++- .../DependencyInjection/UmbracoBuilder.Repositories.cs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 11c9e9109951..077dc83db10a 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -39,6 +39,7 @@ using Umbraco.Cms.Core.Security.Authorization; using Umbraco.Cms.Core.Services.FileSystem; using Umbraco.Cms.Core.Services.ImportExport; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Services.Querying.RecycleBin; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Telemetry; @@ -352,6 +353,7 @@ private void AddCoreServices() Services.AddSingleton(); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); // Register a noop IHtmlSanitizer & IMarkdownSanitizer to be replaced Services.AddUnique(); @@ -398,7 +400,7 @@ private void AddCoreServices() // Segments Services.AddUnique(); - + // definition Import/export Services.AddUnique(); Services.AddUnique(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index f62227ddbaa6..757e1037270a 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DynamicRoot.QuerySteps; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.DynamicRoot.QuerySteps; using Umbraco.Cms.Infrastructure.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Cms.Infrastructure.Services.Implement; @@ -79,6 +79,7 @@ internal static IUmbracoBuilder AddRepositories(this IUmbracoBuilder builder) builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); return builder; } From ab285eeea3a4d75ea1b2cde52387d1917fc6a5b9 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 14:30:39 +0200 Subject: [PATCH 23/96] Adding NavigationDto and NavigationNode --- .../Models/Navigation/NavigationNode.cs | 23 +++++++++++++ .../Persistence/Dtos/NavigationDto.cs | 32 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/Umbraco.Core/Models/Navigation/NavigationNode.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs diff --git a/src/Umbraco.Core/Models/Navigation/NavigationNode.cs b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs new file mode 100644 index 000000000000..5679752b5d32 --- /dev/null +++ b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs @@ -0,0 +1,23 @@ +namespace Umbraco.Cms.Core.Models.Navigation; + +public class NavigationNode +{ + public Guid Key { get; private set; } + + public NavigationNode? Parent { get; private set; } + + public List Children { get; private set; } + + + public NavigationNode(Guid key) + { + Key = key; + Children = new List(); + } + + public void AddChild(NavigationNode child) + { + child.Parent = this; + Children.Add(child); + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs new file mode 100644 index 000000000000..6cdab4870de0 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs @@ -0,0 +1,32 @@ +using NPoco; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +// Used internally for representing the data needed for constructing the in-memory navigation structure. +[TableName(NodeDto.TableName)] +internal class NavigationDto +{ + /// + /// Gets the integer identifier of the entity. + /// + [Column("id")] + public int Id { get; set; } + + /// + /// Gets the Guid unique identifier of the entity. + /// + [Column("uniqueId")] + public Guid Key { get; set; } + + /// + /// Gets the integer identifier of the parent entity. + /// + [Column("parentId")] + public int ParentId { get; set; } + + /// + /// Gets a value indicating whether this entity is in the recycle bin. + /// + [Column("trashed")] + public bool Trashed { get; set; } +} From 8d66ea767ec3cb16760cbdc3631e958a350b02ea Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 14:31:27 +0200 Subject: [PATCH 24/96] Adding INavigationService dependency and Enlist updating navigation structure actions --- src/Umbraco.Core/Services/ContentService.cs | 150 ++++++++++++++++++-- 1 file changed, 135 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index dd6158520354..42bab7c03953 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; @@ -33,25 +34,27 @@ public class ContentService : RepositoryService, IContentService private readonly IShortStringHelper _shortStringHelper; private readonly ICultureImpactFactory _cultureImpactFactory; private readonly IUserIdKeyResolver _userIdKeyResolver; + private readonly INavigationService _navigationService; private IQuery? _queryNotTrashed; #region Constructors public ContentService( - ICoreScopeProvider provider, - ILoggerFactory loggerFactory, - IEventMessagesFactory eventMessagesFactory, - IDocumentRepository documentRepository, - IEntityRepository entityRepository, - IAuditRepository auditRepository, - IContentTypeRepository contentTypeRepository, - IDocumentBlueprintRepository documentBlueprintRepository, - ILanguageRepository languageRepository, - Lazy propertyValidationService, - IShortStringHelper shortStringHelper, - ICultureImpactFactory cultureImpactFactory, - IUserIdKeyResolver userIdKeyResolver) - : base(provider, loggerFactory, eventMessagesFactory) + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IDocumentRepository documentRepository, + IEntityRepository entityRepository, + IAuditRepository auditRepository, + IContentTypeRepository contentTypeRepository, + IDocumentBlueprintRepository documentBlueprintRepository, + ILanguageRepository languageRepository, + Lazy propertyValidationService, + IShortStringHelper shortStringHelper, + ICultureImpactFactory cultureImpactFactory, + IUserIdKeyResolver userIdKeyResolver, + INavigationService navigationService) + : base(provider, loggerFactory, eventMessagesFactory) { _documentRepository = documentRepository; _entityRepository = entityRepository; @@ -63,9 +66,43 @@ public ContentService( _shortStringHelper = shortStringHelper; _cultureImpactFactory = cultureImpactFactory; _userIdKeyResolver = userIdKeyResolver; + _navigationService = navigationService; _logger = loggerFactory.CreateLogger(); } + [Obsolete("Use constructor that takes INavigationService as a parameter, scheduled for removal in V16")] + public ContentService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IDocumentRepository documentRepository, + IEntityRepository entityRepository, + IAuditRepository auditRepository, + IContentTypeRepository contentTypeRepository, + IDocumentBlueprintRepository documentBlueprintRepository, + ILanguageRepository languageRepository, + Lazy propertyValidationService, + IShortStringHelper shortStringHelper, + ICultureImpactFactory cultureImpactFactory, + IUserIdKeyResolver userIdKeyResolver) + : this( + provider, + loggerFactory, + eventMessagesFactory, + documentRepository, + entityRepository, + auditRepository, + contentTypeRepository, + documentBlueprintRepository, + languageRepository, + propertyValidationService, + shortStringHelper, + cultureImpactFactory, + userIdKeyResolver, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + [Obsolete("Use constructor that takes IUserIdKeyResolver as a parameter, scheduled for removal in V15")] public ContentService( ICoreScopeProvider provider, @@ -93,7 +130,8 @@ public ContentService( propertyValidationService, shortStringHelper, cultureImpactFactory, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -1034,6 +1072,11 @@ public OperationResult Save(IContent content, int? userId = null, ContentSchedul // have always changed if it's been saved in the back office but that's not really fail safe. _documentRepository.Save(content); + // Updates in-memory navigation structure - we only handle new items, other updates are not a concern + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.Save-with-contentSchedule", + () => _navigationService.Add(content.Key, GetParent(content)?.Key)); + if (contentSchedule != null) { _documentRepository.PersistContentSchedule(content, contentSchedule); @@ -1097,6 +1140,11 @@ public OperationResult Save(IEnumerable contents, int userId = Constan content.WriterId = userId; _documentRepository.Save(content); + + // Updates in-memory navigation structure - we only handle new items, other updates are not a concern + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.Save", + () => _navigationService.Add(content.Key, GetParent(content)?.Key)); } scope.Notifications.Publish( @@ -2288,6 +2336,18 @@ void DoDelete(IContent c) } DoDelete(content); + + if (content.Trashed) + { + // TODO: call trashed navigation delete + } + else + { + // Updates in-memory navigation structure + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.DeleteLocked", + () => _navigationService.Remove(content.Key)); + } } // TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way @@ -2560,6 +2620,18 @@ private void PerformMoveLocked(IContent content, int parentId, IContent? parent, } } while (total > pageSize); + + if (parentId == Constants.System.RecycleBinContent) + { + // TODO: call trashed navigation move + } + else + { + // Updates in-memory navigation structure + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked", + () => _navigationService.Move(content.Key, parent?.Key)); + } } private void PerformMoveContentLocked(IContent content, int userId, bool? trash) @@ -2663,6 +2735,9 @@ public bool RecycleBinSmells() { EventMessages eventMessages = EventMessagesFactory.Get(); + // keep track of updates (copied item key and parent key) for the in-memory navigation structure + var navigationUpdates = new List>(); + IContent copy = content.DeepCloneWithResetIdentities(); copy.ParentId = parentId; @@ -2699,6 +2774,9 @@ public bool RecycleBinSmells() // save and flush because we need the ID for the recursive Copying events _documentRepository.Save(copy); + // store navigation update information for copied item + navigationUpdates.Add(Tuple.Create(copy.Key, GetParent(copy)?.Key)); + // add permissions if (currentPermissions.Count > 0) { @@ -2750,12 +2828,29 @@ public bool RecycleBinSmells() // save and flush (see above) _documentRepository.Save(descendantCopy); + // store navigation update information for descendants + navigationUpdates.Add(Tuple.Create(descendantCopy.Key, GetParent(descendantCopy)?.Key)); + copies.Add(Tuple.Create(descendant, descendantCopy)); idmap[descendant.Id] = descendantCopy.Id; } } } + if (navigationUpdates.Count > 0) + { + // Updates in-memory navigation structure + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.Copy", + () => + { + foreach (Tuple update in navigationUpdates) + { + _navigationService.Add(update.Item1, update.Item2); + } + }); + } + // not handling tags here, because // - tags should be handled by the content repository // - a copy is unpublished and therefore has no impact on tags in DB @@ -3697,4 +3792,29 @@ public void DeleteBlueprintsOfType(int contentTypeId, int userId = Constants.Sec DeleteBlueprintsOfTypes(new[] { contentTypeId }, userId); #endregion + + /// + /// Enlists an action in the current scope context to update the in-memory navigation structure + /// when the scope completes successfully. + /// + /// The unique key identifying the action to be enlisted. + /// The action to be performed for updating the in-memory navigation structure. + /// Thrown when the scope context is null and therefore cannot be used. + private void UpdateInMemoryNavigationStructure(string enlistingActionKey, Action updateNavigation) + { + IScopeContext? scopeContext = ScopeProvider.Context; + + if (scopeContext is null) + { + throw new NullReferenceException($"The {nameof(scopeContext)} is null and cannot be used."); + } + + scopeContext.Enlist(enlistingActionKey, completed => + { + if (completed) + { + updateNavigation(); + } + }); + } } From 52f6f75fe7ad646e669da37ba218334ddf963df5 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 14:46:40 +0200 Subject: [PATCH 25/96] Documentation --- .../Persistence/Repositories/INavigationRepository.cs | 10 ++++++++++ .../Services/Navigation/INavigationService.cs | 3 +++ .../Implement/ContentNavigationRepository.cs | 2 ++ 3 files changed, 15 insertions(+) diff --git a/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs index c4a49fd30a8a..9908fef85e80 100644 --- a/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs @@ -4,7 +4,17 @@ namespace Umbraco.Cms.Core.Persistence.Repositories; public interface INavigationRepository { + /// + /// Retrieves a dictionary of content nodes based on the object type key. + /// + /// The unique identifier for the object type. + /// A dictionary of navigation nodes where the key is the unique identifier of the node. public Dictionary GetContentNodesByObjectType(Guid objectTypeKey); + /// + /// Retrieves a dictionary of trashed content nodes based on the object type key. + /// + /// The unique identifier for the object type. + /// A dictionary of navigation nodes where the key is the unique identifier of the node. public Dictionary GetTrashedContentNodesByObjectType(Guid objectTypeKey); } diff --git a/src/Umbraco.Core/Services/Navigation/INavigationService.cs b/src/Umbraco.Core/Services/Navigation/INavigationService.cs index f5a02d2911d1..5e3edb11e76c 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationService.cs @@ -1,5 +1,8 @@ namespace Umbraco.Cms.Core.Services.Navigation; +/// +/// Manages navigation-related operations. +/// public interface INavigationService { Task RebuildAsync(); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs index e0fb9e0a1964..d835212c1c36 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs @@ -17,12 +17,14 @@ public ContentNavigationRepository(IScopeAccessor scopeAccessor) private IScope? AmbientScope => _scopeAccessor.AmbientScope; + /// public Dictionary GetContentNodesByObjectType(Guid objectTypeKey) { IEnumerable navigationDtos = FetchNavigationDtos(objectTypeKey, false); return NavigationFactory.BuildNavigationDictionary(navigationDtos); } + /// public Dictionary GetTrashedContentNodesByObjectType(Guid objectTypeKey) { IEnumerable navigationDtos = FetchNavigationDtos(objectTypeKey, true); From 9142148ed15cdddfc917945dee40edfd74704a1c Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 15:00:41 +0200 Subject: [PATCH 26/96] Adding tests for removing descendants as well --- .../Services/NavigationServiceTests.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs index c45845ff40d7..fb28b40fdc76 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs @@ -452,6 +452,9 @@ public void Cannot_Remove_Node_With_Non_Existing_Content_Key() [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 public void Removing_Node_Removes_Its_Descendants_As_Well(Guid keyOfNodeToRemove) { + // Arrange + _navigationService.TryGetDescendantsKeys(keyOfNodeToRemove, out IEnumerable initialDescendantsKeys); + // Act var result = _navigationService.Remove(keyOfNodeToRemove); @@ -460,7 +463,16 @@ public void Removing_Node_Removes_Its_Descendants_As_Well(Guid keyOfNodeToRemove _navigationService.TryGetDescendantsKeys(keyOfNodeToRemove, out IEnumerable descendantsKeys); - Assert.AreEqual(0, descendantsKeys.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(0, descendantsKeys.Count()); + + foreach (Guid descendant in initialDescendantsKeys) + { + var descendantExists = _navigationService.TryGetParentKey(descendant, out _); + Assert.IsFalse(descendantExists); + } + }); } [Test] From 0cd86c52fb5ef41e8ba99d00e842c5223fe929ea Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 15:45:21 +0200 Subject: [PATCH 27/96] Changed to ConcurrentDictionary --- .../Persistence/Repositories/INavigationRepository.cs | 5 +++-- .../Services/Navigation/ContentNavigationService.cs | 9 ++++----- .../Implement/ContentNavigationRepository.cs | 5 +++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs index 9908fef85e80..cd7def878e85 100644 --- a/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using Umbraco.Cms.Core.Models.Navigation; namespace Umbraco.Cms.Core.Persistence.Repositories; @@ -9,12 +10,12 @@ public interface INavigationRepository /// /// The unique identifier for the object type. /// A dictionary of navigation nodes where the key is the unique identifier of the node. - public Dictionary GetContentNodesByObjectType(Guid objectTypeKey); + public ConcurrentDictionary GetContentNodesByObjectType(Guid objectTypeKey); /// /// Retrieves a dictionary of trashed content nodes based on the object type key. /// /// The unique identifier for the object type. /// A dictionary of navigation nodes where the key is the unique identifier of the node. - public Dictionary GetTrashedContentNodesByObjectType(Guid objectTypeKey); + public ConcurrentDictionary GetTrashedContentNodesByObjectType(Guid objectTypeKey); } diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs index c9d6e01aef23..6a83692dd344 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using Umbraco.Cms.Core.Models.Navigation; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; @@ -8,7 +9,7 @@ internal class ContentNavigationService : INavigationService { private readonly ICoreScopeProvider _coreScopeProvider; private readonly INavigationRepository _navigationRepository; - private Dictionary _navigationStructure = new(); + private ConcurrentDictionary _navigationStructure = new(); public ContentNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) { @@ -133,9 +134,7 @@ public bool Remove(Guid key) RemoveDescendantsRecursively(nodeToRemove); // Remove the node itself - _navigationStructure.Remove(key); - - return true; + return _navigationStructure.TryRemove(key, out _); } public bool Add(Guid key, Guid? parentKey = null) @@ -214,7 +213,7 @@ private void RemoveDescendantsRecursively(NavigationNode node) foreach (NavigationNode child in node.Children) { RemoveDescendantsRecursively(child); - _navigationStructure.Remove(child.Key); + _navigationStructure.TryRemove(child.Key, out _); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs index d835212c1c36..74d25cdf3668 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using NPoco; using Umbraco.Cms.Core.Models.Navigation; using Umbraco.Cms.Core.Persistence.Repositories; @@ -18,14 +19,14 @@ public ContentNavigationRepository(IScopeAccessor scopeAccessor) private IScope? AmbientScope => _scopeAccessor.AmbientScope; /// - public Dictionary GetContentNodesByObjectType(Guid objectTypeKey) + public ConcurrentDictionary GetContentNodesByObjectType(Guid objectTypeKey) { IEnumerable navigationDtos = FetchNavigationDtos(objectTypeKey, false); return NavigationFactory.BuildNavigationDictionary(navigationDtos); } /// - public Dictionary GetTrashedContentNodesByObjectType(Guid objectTypeKey) + public ConcurrentDictionary GetTrashedContentNodesByObjectType(Guid objectTypeKey) { IEnumerable navigationDtos = FetchNavigationDtos(objectTypeKey, true); return NavigationFactory.BuildNavigationDictionary(navigationDtos); From 46868fbc289154b1e898b1af7366d6b8cf917d69 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 15:49:41 +0200 Subject: [PATCH 28/96] Remove keys comments for tests --- .../Umbraco.Core/Services/NavigationServiceTests.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs index 6a97ec6b9913..2624389158db 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs @@ -55,17 +55,6 @@ public async Task Setup() // - Child 3 // - Grandchild 4 - // TODO: DELETE - // Root - Guid.Parse("E48DD82A-7059-418E-9B82-CDD5205796CF"); // Root's key - // - Child 1 - Guid.Parse("C6173927-0C59-4778-825D-D7B9F45D8DDE"); // Child 1's key - // - Grandchild 1 - Guid.Parse("E856AC03-C23E-4F63-9AA9-681B42A58573"); // Grandchild 1's key - // - Grandchild 2 - Guid.Parse("A1B1B217-B02F-4307-862C-A5E22DB729EB"); // Grandchild 2's key - // - Child 2 - Guid.Parse("60E0E5C4-084E-4144-A560-7393BEAD2E96"); // Child 2's key - // - Grandchild 3 - Guid.Parse("D63C1621-C74A-4106-8587-817DEE5FB732"); // Grandchild 3's key - // - Great-grandchild 1 - Guid.Parse("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"); // Great-grandchild 1's key - // - Child 3 - Guid.Parse("B606E3FF-E070-4D46-8CB9-D31352029FDF"); // Child 3's key - // - Grandchild 4 - Guid.Parse("F381906C-223C-4466-80F7-B63B4EE073F8"); // Grandchild 4's key - // Doc Type ContentType = ContentTypeBuilder.CreateSimpleContentType("page", "Page"); ContentType.Key = new Guid("DD72B8A6-2CE3-47F0-887E-B695A1A5D086"); From b8071bdf975101a2f72a880c1ca9453d9ea93ac3 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Thu, 25 Jul 2024 15:54:51 +0200 Subject: [PATCH 29/96] Adding documentation --- .../Services/Navigation/NavigationInitializationService.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs b/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs index 17a2837d6e44..71793b9ed9c9 100644 --- a/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs +++ b/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs @@ -2,6 +2,10 @@ namespace Umbraco.Cms.Core.Services.Navigation; +/// +/// Responsible for seeding the in-memory navigation structure at application's startup +/// by rebuild the navigation structure. +/// public class NavigationInitializationService : IHostedLifecycleService { private readonly INavigationService _navigationService; From c51b453a021acae3886c8a1d2c94bdcbc68b35e7 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 26 Jul 2024 08:32:39 +0200 Subject: [PATCH 30/96] Forgotten ConcurrentDictionary change --- .../Persistence/Factories/NavigationFactory.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs index 4dd831f15918..e342c3550dbb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using Umbraco.Cms.Core.Models.Navigation; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -5,14 +6,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories; internal static class NavigationFactory { - /// - /// Builds a dictionary of NavigationNode objects from a given dataset. - /// - /// The dataset of objects used to build the navigation nodes dictionary. - /// A dictionary of objects with key corresponding to their unique guid. - public static Dictionary BuildNavigationDictionary(IEnumerable dataset) +/// +/// Builds a dictionary of NavigationNode objects from a given dataset. +/// +/// The dataset of objects used to build the navigation nodes dictionary. +/// A dictionary of objects with key corresponding to their unique guid. + public static ConcurrentDictionary BuildNavigationDictionary(IEnumerable dataset) { - var nodesStructure = new Dictionary(); + var nodesStructure = new ConcurrentDictionary(); var datasetList = dataset.ToList(); var idToKeyMap = datasetList.ToDictionary(x => x.Id, x => x.Key); From 2b562c6bb8f4dadac80ff8622ffc1ca9dd93f0ee Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 7 Aug 2024 10:21:57 +0200 Subject: [PATCH 31/96] Isolating the operations on the model --- .../Models/Navigation/NavigationNode.cs | 14 ++++++++++---- ...nService.cs => ContentNavigationServiceBase.cs} | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) rename src/Umbraco.Core/Services/Navigation/{ContentNavigationService.cs => ContentNavigationServiceBase.cs} (98%) diff --git a/src/Umbraco.Core/Models/Navigation/NavigationNode.cs b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs index 5679752b5d32..2c3ee7acd9b2 100644 --- a/src/Umbraco.Core/Models/Navigation/NavigationNode.cs +++ b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs @@ -2,22 +2,28 @@ namespace Umbraco.Cms.Core.Models.Navigation; public class NavigationNode { + private List _children; + public Guid Key { get; private set; } public NavigationNode? Parent { get; private set; } - public List Children { get; private set; } - + public IEnumerable Children => _children.AsEnumerable(); public NavigationNode(Guid key) { Key = key; - Children = new List(); + _children = new List(); } public void AddChild(NavigationNode child) { child.Parent = this; - Children.Add(child); + _children.Add(child); + } + + public void RemoveChild(NavigationNode child) + { + _children.Remove(child); } } diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs similarity index 98% rename from src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs rename to src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index 6a83692dd344..c41a1ce2f895 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -127,7 +127,7 @@ public bool Remove(Guid key) // Remove the node from its parent's children list if (nodeToRemove.Parent is not null && _navigationStructure.TryGetValue(nodeToRemove.Parent.Key, out NavigationNode? parentNode)) { - parentNode.Children.Remove(nodeToRemove); + parentNode.RemoveChild(nodeToRemove); } // Recursively remove all descendants @@ -182,7 +182,7 @@ public bool Move(Guid nodeKey, Guid? targetParentKey = null) // Remove the node from its current parent's children list if (nodeToMove.Parent is not null && _navigationStructure.TryGetValue(nodeToMove.Parent.Key, out var currentParentNode)) { - currentParentNode.Children.Remove(nodeToMove); + currentParentNode.RemoveChild(nodeToMove); } // Create a new node with the same key, to update the parent From 01faa032132397b2d490af14ec68d35e92605a3a Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 7 Aug 2024 10:24:09 +0200 Subject: [PATCH 32/96] Splitting the INavigationService to separate the querying from the managing functionality --- .../INavigationManagementService.cs | 20 +++++++++++++++++++ ...nService.cs => INavigationQueryService.cs} | 13 +++--------- 2 files changed, 23 insertions(+), 10 deletions(-) create mode 100644 src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs rename src/Umbraco.Core/Services/Navigation/{INavigationService.cs => INavigationQueryService.cs} (65%) diff --git a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs new file mode 100644 index 000000000000..b74ddcf7b14e --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs @@ -0,0 +1,20 @@ +using Umbraco.Cms.Core.Models.Navigation; + +namespace Umbraco.Cms.Core.Services.Navigation; + +/// +/// Placeholder for sharing logic between the document, document recycle bin, media and media recycle bin services +/// for managing the navigation structure. +/// +public interface INavigationManagementService +{ + Task RebuildAsync(); + + bool Remove(Guid key); + + bool Add(Guid key, Guid? parentKey = null); + + NavigationNode? GetNavigationNode(Guid key); + + bool AddNavigationNode(NavigationNode node, Guid? parentKey = null); +} diff --git a/src/Umbraco.Core/Services/Navigation/INavigationService.cs b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs similarity index 65% rename from src/Umbraco.Core/Services/Navigation/INavigationService.cs rename to src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs index 5e3edb11e76c..6d8d09a7a297 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs @@ -1,12 +1,11 @@ namespace Umbraco.Cms.Core.Services.Navigation; /// -/// Manages navigation-related operations. +/// Placeholder for sharing logic between the document, document recycle bin, media and media recycle bin services +/// for querying the navigation structure. /// -public interface INavigationService +public interface INavigationQueryService { - Task RebuildAsync(); - bool TryGetParentKey(Guid childKey, out Guid? parentKey); bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys); @@ -16,10 +15,4 @@ public interface INavigationService bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKeys); bool TryGetSiblingsKeys(Guid key, out IEnumerable siblingsKeys); - - bool Remove(Guid key); - - bool Add(Guid key, Guid? parentKey = null); - - bool Move(Guid nodeKey, Guid? targetParentKey = null); } From 3808cac82cb43e915db910673b52e45409ca428b Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 7 Aug 2024 10:26:31 +0200 Subject: [PATCH 33/96] Introducing specific navigation services for document, document recycle bin, media and media recycle bin --- src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs | 5 ++++- .../Services/Navigation/IDocumentNavigationService.cs | 6 ++++++ .../Navigation/IDocumentRecycleBinNavigationService.cs | 6 ++++++ .../Services/Navigation/IMediaNavigationService.cs | 6 ++++++ .../Navigation/IMediaRecycleBinNavigationService.cs | 5 +++++ 5 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Core/Services/Navigation/IDocumentNavigationService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/IDocumentRecycleBinNavigationService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/IMediaNavigationService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/IMediaRecycleBinNavigationService.cs diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 077dc83db10a..f7f870505af6 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -353,7 +353,10 @@ private void AddCoreServices() Services.AddSingleton(); Services.AddUnique(); Services.AddUnique(); - Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); // Register a noop IHtmlSanitizer & IMarkdownSanitizer to be replaced Services.AddUnique(); diff --git a/src/Umbraco.Core/Services/Navigation/IDocumentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/IDocumentNavigationService.cs new file mode 100644 index 000000000000..241a92cc90d7 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/IDocumentNavigationService.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IDocumentNavigationService : INavigationQueryService, INavigationManagementService +{ + bool Move(Guid nodeKey, Guid? targetParentKey = null); +} diff --git a/src/Umbraco.Core/Services/Navigation/IDocumentRecycleBinNavigationService.cs b/src/Umbraco.Core/Services/Navigation/IDocumentRecycleBinNavigationService.cs new file mode 100644 index 000000000000..7005862b08a7 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/IDocumentRecycleBinNavigationService.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IDocumentRecycleBinNavigationService : INavigationQueryService, INavigationManagementService +{ +} + diff --git a/src/Umbraco.Core/Services/Navigation/IMediaNavigationService.cs b/src/Umbraco.Core/Services/Navigation/IMediaNavigationService.cs new file mode 100644 index 000000000000..c8347baef9b8 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/IMediaNavigationService.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IMediaNavigationService : INavigationQueryService, INavigationManagementService +{ + bool Move(Guid nodeKey, Guid? targetParentKey = null); +} diff --git a/src/Umbraco.Core/Services/Navigation/IMediaRecycleBinNavigationService.cs b/src/Umbraco.Core/Services/Navigation/IMediaRecycleBinNavigationService.cs new file mode 100644 index 000000000000..114053f1bef8 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/IMediaRecycleBinNavigationService.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IMediaRecycleBinNavigationService : INavigationQueryService, INavigationManagementService +{ +} From d8d345d664697d894219cf79e0394b4192b47d8b Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 7 Aug 2024 10:28:14 +0200 Subject: [PATCH 34/96] Making ContentNavigationService into a base as the functionality will be shared between the document, document recycle bin, media and media recycle bin services --- .../ContentNavigationServiceBase.cs | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index c41a1ce2f895..178f54327a95 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -5,24 +5,19 @@ namespace Umbraco.Cms.Core.Services.Navigation; -internal class ContentNavigationService : INavigationService +internal abstract class ContentNavigationServiceBase { private readonly ICoreScopeProvider _coreScopeProvider; private readonly INavigationRepository _navigationRepository; private ConcurrentDictionary _navigationStructure = new(); - public ContentNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) + protected ContentNavigationServiceBase(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) { _coreScopeProvider = coreScopeProvider; _navigationRepository = navigationRepository; } - public async Task RebuildAsync() - { - using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); - scope.ReadLock(Constants.Locks.ContentTree); - _navigationStructure = _navigationRepository.GetContentNodesByObjectType(Constants.ObjectTypes.Document); - } + public abstract Task RebuildAsync(); public bool TryGetParentKey(Guid childKey, out Guid? parentKey) { @@ -176,7 +171,7 @@ public bool Move(Guid nodeKey, Guid? targetParentKey = null) NavigationNode? targetParentNode = null; if (targetParentKey.HasValue && _navigationStructure.TryGetValue(targetParentKey.Value, out targetParentNode) is false) { - return false; // Target parent doesn't exist + return false; // Target parent doesn't exist } // Remove the node from its current parent's children list @@ -199,6 +194,29 @@ public bool Move(Guid nodeKey, Guid? targetParentKey = null) return true; } + /// + /// Rebuilds the navigation structure based on the specified object type key and whether the items are trashed. + /// Only relevant for items in the content and media trees (which have readLock values of -333 or -334). + /// + /// The read lock value, should be -333 or -334 for content and media trees. + /// The key of the object type to rebuild. + /// Indicates whether the items are in the recycle bin. + protected async Task HandleRebuildAsync(int readLock, Guid objectTypeKey, bool trashed) + { + // This is only relevant for items in the content and media trees + if (readLock != -Constants.Locks.ContentTree && readLock != Constants.Locks.MediaTree) + { + return; + } + + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); + scope.ReadLock(readLock); + + _navigationStructure = trashed + ? _navigationRepository.GetTrashedContentNodesByObjectType(objectTypeKey) + : _navigationRepository.GetContentNodesByObjectType(objectTypeKey); + } + private void GetDescendantsRecursively(NavigationNode node, List descendants) { foreach (NavigationNode child in node.Children) From e59c35d3c8d1a98e648941ec49b9826eb16b73bc Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 7 Aug 2024 10:29:15 +0200 Subject: [PATCH 35/96] Adding the implementations of document, document recycle bin, media and media recycle bin navigation services --- .../Navigation/DocumentNavigationService.cs | 15 +++++++++++++++ .../DocumentRecycleBinNavigationService.cs | 15 +++++++++++++++ .../Services/Navigation/MediaNavigationService.cs | 15 +++++++++++++++ .../MediaRecycleBinNavigationService.cs | 15 +++++++++++++++ 4 files changed, 60 insertions(+) create mode 100644 src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/DocumentRecycleBinNavigationService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/MediaRecycleBinNavigationService.cs diff --git a/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs new file mode 100644 index 000000000000..22921635bb3c --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.Services.Navigation; + +internal sealed class DocumentNavigationService : ContentNavigationServiceBase, IDocumentNavigationService +{ + public DocumentNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) + : base(coreScopeProvider, navigationRepository) + { + } + + public override async Task RebuildAsync() + => await HandleRebuildAsync(Constants.Locks.ContentTree, Constants.ObjectTypes.Document, false); +} diff --git a/src/Umbraco.Core/Services/Navigation/DocumentRecycleBinNavigationService.cs b/src/Umbraco.Core/Services/Navigation/DocumentRecycleBinNavigationService.cs new file mode 100644 index 000000000000..dcf4f04b7d99 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/DocumentRecycleBinNavigationService.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.Services.Navigation; + +internal sealed class DocumentRecycleBinNavigationService : ContentNavigationServiceBase, IDocumentRecycleBinNavigationService +{ + public DocumentRecycleBinNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) + : base(coreScopeProvider, navigationRepository) + { + } + + public override async Task RebuildAsync() + => await HandleRebuildAsync(Constants.Locks.ContentTree, Constants.ObjectTypes.Document, true); +} diff --git a/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs b/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs new file mode 100644 index 000000000000..b81ea155018f --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.Services.Navigation; + +internal sealed class MediaNavigationService : ContentNavigationServiceBase, IMediaNavigationService +{ + public MediaNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) + : base(coreScopeProvider, navigationRepository) + { + } + + public override async Task RebuildAsync() + => await HandleRebuildAsync(Constants.Locks.MediaTree, Constants.ObjectTypes.Media, false); +} diff --git a/src/Umbraco.Core/Services/Navigation/MediaRecycleBinNavigationService.cs b/src/Umbraco.Core/Services/Navigation/MediaRecycleBinNavigationService.cs new file mode 100644 index 000000000000..7a96b749ab5f --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/MediaRecycleBinNavigationService.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.Services.Navigation; + +internal sealed class MediaRecycleBinNavigationService : ContentNavigationServiceBase, IMediaRecycleBinNavigationService +{ + public MediaRecycleBinNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) + : base(coreScopeProvider, navigationRepository) + { + } + + public override async Task RebuildAsync() + => await HandleRebuildAsync(Constants.Locks.MediaTree, Constants.ObjectTypes.Media, true); +} From c323d75ff0d63af1adde0a956d8cef9bdddb69ad Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 7 Aug 2024 10:29:52 +0200 Subject: [PATCH 36/96] Fixing comments --- .../Services/Navigation/INavigationManagementService.cs | 2 +- src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs index b74ddcf7b14e..996ec6cb3146 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs @@ -3,7 +3,7 @@ namespace Umbraco.Cms.Core.Services.Navigation; /// -/// Placeholder for sharing logic between the document, document recycle bin, media and media recycle bin services +/// Placeholder for sharing logic between the document, document recycle bin, media and media recycle bin navigation services /// for managing the navigation structure. /// public interface INavigationManagementService diff --git a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs index 6d8d09a7a297..062f877d5776 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs @@ -1,7 +1,7 @@ namespace Umbraco.Cms.Core.Services.Navigation; /// -/// Placeholder for sharing logic between the document, document recycle bin, media and media recycle bin services +/// Placeholder for sharing logic between the document, document recycle bin, media and media recycle bin navigation services /// for querying the navigation structure. /// public interface INavigationQueryService From 4916a301caa3eaa6ec6483e3b0f8c044657923e4 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 7 Aug 2024 10:30:17 +0200 Subject: [PATCH 37/96] Initializing all 4 collections --- .../NavigationInitializationService.cs | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs b/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs index 71793b9ed9c9..9c0b6a917477 100644 --- a/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs +++ b/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs @@ -3,19 +3,34 @@ namespace Umbraco.Cms.Core.Services.Navigation; /// -/// Responsible for seeding the in-memory navigation structure at application's startup -/// by rebuild the navigation structure. +/// Responsible for seeding the in-memory navigation structures at application's startup +/// by rebuild the navigation structures. /// public class NavigationInitializationService : IHostedLifecycleService { - private readonly INavigationService _navigationService; - - public NavigationInitializationService(INavigationService navigationService) - => _navigationService = navigationService; + private readonly IDocumentNavigationService _documentNavigationService; + private readonly IDocumentRecycleBinNavigationService _documentRecycleBinNavigationService; + private readonly IMediaNavigationService _mediaNavigationService; + private readonly IMediaRecycleBinNavigationService _mediaRecycleBinNavigationService; + + public NavigationInitializationService( + IDocumentNavigationService documentNavigationService, + IDocumentRecycleBinNavigationService documentRecycleBinNavigationService, + IMediaNavigationService mediaNavigationService, + IMediaRecycleBinNavigationService mediaRecycleBinNavigationService) + { + _documentNavigationService = documentNavigationService; + _documentRecycleBinNavigationService = documentRecycleBinNavigationService; + _mediaNavigationService = mediaNavigationService; + _mediaRecycleBinNavigationService = mediaRecycleBinNavigationService; + } public async Task StartingAsync(CancellationToken cancellationToken) { - await _navigationService.RebuildAsync(); + await _documentNavigationService.RebuildAsync(); + await _documentRecycleBinNavigationService.RebuildAsync(); + await _mediaNavigationService.RebuildAsync(); + await _mediaRecycleBinNavigationService.RebuildAsync(); } public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; From 3eb5d863f5223ce6f9df041c36087eec7720e1e6 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 7 Aug 2024 10:33:35 +0200 Subject: [PATCH 38/96] Adapting the navigation unit tests to the base now --- ...ServiceTests.cs => ContentNavigationServiceBaseTests.cs} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/{NavigationServiceTests.cs => ContentNavigationServiceBaseTests.cs} (99%) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs similarity index 99% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs index fb28b40fdc76..173112de0b69 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/NavigationServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs @@ -7,9 +7,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services; [TestFixture] -public class NavigationServiceTests +public class ContentNavigationServiceBaseTests { - private INavigationService _navigationService; + private TestContentNavigationService _navigationService; private Guid Root { get; set; } @@ -42,7 +42,7 @@ public void Setup() // - Child 3 // - Grandchild 4 - _navigationService = new ContentNavigationService( + _navigationService = new TestContentNavigationService( Mock.Of(), Mock.Of()); From 0dfe59ad16db90a4c25d1f5320fa33c4b94187ef Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 7 Aug 2024 10:35:30 +0200 Subject: [PATCH 39/96] Adapting integration tests to specific navigation service --- ...s.cs => DocumentNavigationServiceTests.cs} | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) rename tests/Umbraco.Tests.Integration/Umbraco.Core/Services/{NavigationServiceTests.cs => DocumentNavigationServiceTests.cs} (87%) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs similarity index 87% rename from tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs rename to tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs index 2624389158db..ef529d5f466e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/NavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs @@ -2,6 +2,7 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; @@ -13,14 +14,16 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] -public class NavigationServiceTests : UmbracoIntegrationTest +public class DocumentNavigationServiceTests : UmbracoIntegrationTest { private IContentTypeService ContentTypeService => GetRequiredService(); // Testing with IContentEditingService as it calls IContentService underneath private IContentEditingService ContentEditingService => GetRequiredService(); - private INavigationService NavigationService => GetRequiredService(); + private IDocumentNavigationService DocumentNavigationService => GetRequiredService(); + + private IDocumentRecycleBinNavigationService DocumentRecycleBinNavigationService => GetRequiredService(); private ContentType ContentType { get; set; } @@ -126,7 +129,7 @@ public async Task Structure_Does_Not_Update_When_Scope_Is_Not_Completed() } // Act - var nodeExists = NavigationService.TryGetParentKey(notCreatedRootKey, out _); + var nodeExists = DocumentNavigationService.TryGetParentKey(notCreatedRootKey, out _); // Assert Assert.IsFalse(nodeExists); @@ -136,7 +139,7 @@ public async Task Structure_Does_Not_Update_When_Scope_Is_Not_Completed() public async Task Structure_Updates_When_Creating_Content() { // Arrange - NavigationService.TryGetSiblingsKeys(Root.Key, out IEnumerable initialSiblingsKeys); + DocumentNavigationService.TryGetSiblingsKeys(Root.Key, out IEnumerable initialSiblingsKeys); var initialRootNodeSiblingsCount = initialSiblingsKeys.Count(); var createModel = new ContentCreateModel @@ -151,7 +154,7 @@ public async Task Structure_Updates_When_Creating_Content() Guid createdItemKey = createAttempt.Result.Content!.Key; // Verify that the structure has updated by checking the siblings list of the Root once again - NavigationService.TryGetSiblingsKeys(Root.Key, out IEnumerable updatedSiblingsKeys); + DocumentNavigationService.TryGetSiblingsKeys(Root.Key, out IEnumerable updatedSiblingsKeys); List siblingsList = updatedSiblingsKeys.ToList(); // Assert @@ -170,11 +173,11 @@ public async Task Structure_Does_Not_Update_When_Updating_Content() Guid nodeToUpdate = Root.Key; // Capture initial state - NavigationService.TryGetParentKey(nodeToUpdate, out Guid? initialParentKey); - NavigationService.TryGetChildrenKeys(nodeToUpdate, out IEnumerable initialChildrenKeys); - NavigationService.TryGetDescendantsKeys(nodeToUpdate, out IEnumerable initialDescendantsKeys); - NavigationService.TryGetAncestorsKeys(nodeToUpdate, out IEnumerable initialAncestorsKeys); - NavigationService.TryGetSiblingsKeys(nodeToUpdate, out IEnumerable initialSiblingsKeys); + DocumentNavigationService.TryGetParentKey(nodeToUpdate, out Guid? initialParentKey); + DocumentNavigationService.TryGetChildrenKeys(nodeToUpdate, out IEnumerable initialChildrenKeys); + DocumentNavigationService.TryGetDescendantsKeys(nodeToUpdate, out IEnumerable initialDescendantsKeys); + DocumentNavigationService.TryGetAncestorsKeys(nodeToUpdate, out IEnumerable initialAncestorsKeys); + DocumentNavigationService.TryGetSiblingsKeys(nodeToUpdate, out IEnumerable initialSiblingsKeys); var updateModel = new ContentUpdateModel { @@ -186,17 +189,17 @@ public async Task Structure_Does_Not_Update_When_Updating_Content() Guid updatedItemKey = updateAttempt.Result.Content!.Key; // Capture updated state - var parentExists = NavigationService.TryGetParentKey(updatedItemKey, out Guid? updatedParentKey); - NavigationService.TryGetChildrenKeys(updatedItemKey, out IEnumerable childrenKeysAfterUpdate); - NavigationService.TryGetDescendantsKeys(updatedItemKey, out IEnumerable descendantsKeysAfterUpdate); - NavigationService.TryGetAncestorsKeys(updatedItemKey, out IEnumerable ancestorsKeysAfterUpdate); - NavigationService.TryGetSiblingsKeys(updatedItemKey, out IEnumerable siblingsKeysAfterUpdate); + var nodeExists = DocumentNavigationService.TryGetParentKey(updatedItemKey, out Guid? updatedParentKey); + DocumentNavigationService.TryGetChildrenKeys(updatedItemKey, out IEnumerable childrenKeysAfterUpdate); + DocumentNavigationService.TryGetDescendantsKeys(updatedItemKey, out IEnumerable descendantsKeysAfterUpdate); + DocumentNavigationService.TryGetAncestorsKeys(updatedItemKey, out IEnumerable ancestorsKeysAfterUpdate); + DocumentNavigationService.TryGetSiblingsKeys(updatedItemKey, out IEnumerable siblingsKeysAfterUpdate); // Assert Assert.Multiple(() => { // Verify that the item is still present in the navigation structure - Assert.IsTrue(parentExists); + Assert.IsTrue(nodeExists); Assert.AreEqual(nodeToUpdate, updatedItemKey); @@ -263,13 +266,13 @@ public async Task Structure_Does_Not_Update_When_Updating_Content() public async Task Structure_Updates_When_Moving_Content(Guid nodeToMove, Guid? targetParentKey) { // Arrange - NavigationService.TryGetParentKey(nodeToMove, out Guid? originalParentKey); + DocumentNavigationService.TryGetParentKey(nodeToMove, out Guid? originalParentKey); // Act var moveAttempt = await ContentEditingService.MoveAsync(nodeToMove, targetParentKey, Constants.Security.SuperUserKey); // Verify the node's new parent is updated - NavigationService.TryGetParentKey(moveAttempt.Result!.Key, out Guid? updatedParentKey); + DocumentNavigationService.TryGetParentKey(moveAttempt.Result!.Key, out Guid? updatedParentKey); // Assert Assert.Multiple(() => @@ -299,7 +302,7 @@ public async Task Structure_Updates_When_Moving_Content(Guid nodeToMove, Guid? t public async Task Structure_Updates_When_Copying_Content(Guid nodeToCopy, Guid? targetParentKey) { // Arrange - NavigationService.TryGetParentKey(nodeToCopy, out Guid? sourceParentKey); + DocumentNavigationService.TryGetParentKey(nodeToCopy, out Guid? sourceParentKey); // Act var copyAttempt = await ContentEditingService.CopyAsync(nodeToCopy, targetParentKey, false, false, Constants.Security.SuperUserKey); @@ -308,7 +311,7 @@ public async Task Structure_Updates_When_Copying_Content(Guid nodeToCopy, Guid? // Assert Assert.AreNotEqual(nodeToCopy, copiedItemKey); - NavigationService.TryGetParentKey(copiedItemKey, out Guid? copiedItemParentKey); + DocumentNavigationService.TryGetParentKey(copiedItemKey, out Guid? copiedItemParentKey); Assert.Multiple(() => { @@ -334,14 +337,14 @@ public async Task Structure_Updates_When_Copying_Content(Guid nodeToCopy, Guid? public async Task Structure_Updates_When_Deleting_Content(Guid nodeToDelete) { // Arrange - NavigationService.TryGetDescendantsKeys(nodeToDelete, out IEnumerable initialDescendantsKeys); + DocumentNavigationService.TryGetDescendantsKeys(nodeToDelete, out IEnumerable initialDescendantsKeys); // Act var deleteAttempt = await ContentEditingService.DeleteAsync(nodeToDelete, Constants.Security.SuperUserKey); Guid deletedItemKey = deleteAttempt.Result.Key; // Assert - var nodeExists = NavigationService.TryGetDescendantsKeys(deletedItemKey, out _); + var nodeExists = DocumentNavigationService.TryGetDescendantsKeys(deletedItemKey, out _); //var nodeExistsInRecycleBin = NavigationService.TryGetDescendantsKeys(nodeToDelete, out _); // TODO: use recycle bin str. Assert.Multiple(() => @@ -352,7 +355,7 @@ public async Task Structure_Updates_When_Deleting_Content(Guid nodeToDelete) foreach (Guid descendant in initialDescendantsKeys) { - var descendantExists = NavigationService.TryGetParentKey(descendant, out _); + var descendantExists = DocumentNavigationService.TryGetParentKey(descendant, out _); Assert.IsFalse(descendantExists); // var descendantExistsInRecycleBin = NavigationService.TryGetParentKey(descendant, out _); // TODO: use recycle bin str. From 1ee88661cf3d42c02c4e0f0d3f9c87f71c5aa879 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 7 Aug 2024 10:36:15 +0200 Subject: [PATCH 40/96] Adding test for rebuilding the structure --- .../DocumentNavigationServiceTests.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs index ef529d5f466e..d0987953a657 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs @@ -109,6 +109,50 @@ public async Task Setup() // builder.Services.AddHostedService(); // } + [Test] + public async Task Structure_Can_Rebuild() + { + // Arrange + Guid nodeKey = Root.Key; + + // Capture original built state of DocumentNavigationService + DocumentNavigationService.TryGetParentKey(nodeKey, out Guid? originalParentKey); + DocumentNavigationService.TryGetChildrenKeys(nodeKey, out IEnumerable originalChildrenKeys); + DocumentNavigationService.TryGetDescendantsKeys(nodeKey, out IEnumerable originalDescendantsKeys); + DocumentNavigationService.TryGetAncestorsKeys(nodeKey, out IEnumerable originalAncestorsKeys); + DocumentNavigationService.TryGetSiblingsKeys(nodeKey, out IEnumerable originalSiblingsKeys); + + // Im-memory navigation structure is empty here + var newDocumentNavigationService = new DocumentNavigationService(GetRequiredService(), GetRequiredService()); + var initialNodeExists = newDocumentNavigationService.TryGetParentKey(nodeKey, out _); + + // Act + await newDocumentNavigationService.RebuildAsync(); + + // Capture rebuilt state + var nodeExists = newDocumentNavigationService.TryGetParentKey(nodeKey, out Guid? parentKeyFromRebuild); + newDocumentNavigationService.TryGetChildrenKeys(nodeKey, out IEnumerable childrenKeysFromRebuild); + newDocumentNavigationService.TryGetDescendantsKeys(nodeKey, out IEnumerable descendantsKeysFromRebuild); + newDocumentNavigationService.TryGetAncestorsKeys(nodeKey, out IEnumerable ancestorsKeysFromRebuild); + newDocumentNavigationService.TryGetSiblingsKeys(nodeKey, out IEnumerable siblingsKeysFromRebuild); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(initialNodeExists); + + // Verify that the item is present in the navigation structure after a rebuild + Assert.IsTrue(nodeExists); + + // Verify that we have the same items as in the original built state of DocumentNavigationService + Assert.AreEqual(originalParentKey, parentKeyFromRebuild); + CollectionAssert.AreEquivalent(originalChildrenKeys, childrenKeysFromRebuild); + CollectionAssert.AreEquivalent(originalDescendantsKeys, descendantsKeysFromRebuild); + CollectionAssert.AreEquivalent(originalAncestorsKeys, ancestorsKeysFromRebuild); + CollectionAssert.AreEquivalent(originalSiblingsKeys, siblingsKeysFromRebuild); + }); + } + [Test] public async Task Structure_Does_Not_Update_When_Scope_Is_Not_Completed() { From df4d7ca443a3269633c9f5ac6019a4a48a2f721c Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 7 Aug 2024 10:38:17 +0200 Subject: [PATCH 41/96] Adding implementation for Adding and Getting a node - needed for moving to and restoring from the recycle bin + tests --- .../ContentNavigationServiceBase.cs | 46 +++++ .../ContentNavigationServiceBaseTests.cs | 189 ++++++++++++++++++ 2 files changed, 235 insertions(+) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index 178f54327a95..118d5144a4b3 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -112,6 +112,36 @@ public bool TryGetSiblingsKeys(Guid key, out IEnumerable siblingsKeys) return true; } + public NavigationNode? GetNavigationNode(Guid key) + => _navigationStructure.GetValueOrDefault(key); + + public bool AddNavigationNode(NavigationNode node, Guid? parentKey = null) + { + if (_navigationStructure.ContainsKey(node.Key)) + { + return false; // Node with this key already exists + } + + NavigationNode? parentNode = null; + if (parentKey.HasValue) + { + if (_navigationStructure.TryGetValue(parentKey.Value, out parentNode) is false) + { + return false; // Parent node doesn't exist + } + } + + var newNodesMap = new Dictionary(); + CopyNodeHierarchyRecursively(node, parentNode, newNodesMap); + + foreach (NavigationNode newNode in newNodesMap.Values) + { + _navigationStructure[newNode.Key] = newNode; + } + + return true; + } + public bool Remove(Guid key) { if (_navigationStructure.TryGetValue(key, out NavigationNode? nodeToRemove) is false) @@ -242,4 +272,20 @@ private void CopyChildren(NavigationNode originalNode, NavigationNode newNode) newNode.AddChild(child); } } + + private void CopyNodeHierarchyRecursively(NavigationNode sourceNode, NavigationNode? newParent, Dictionary newNodesMap) + { + // Create a new node with the same key, to update the parent + var newNode = new NavigationNode(sourceNode.Key); + + // Set the new parent for the node (if parent node is null - the node is added to root) + newParent?.AddChild(newNode); + + newNodesMap[sourceNode.Key] = newNode; + + foreach (NavigationNode child in sourceNode.Children) + { + CopyNodeHierarchyRecursively(child, newNode, newNodesMap); + } + } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs index 173112de0b69..378c2f9fb67b 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs @@ -1,5 +1,6 @@ using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Models.Navigation; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Navigation; @@ -751,4 +752,192 @@ public void Number_Of_Target_Parent_Descendants_Updates_When_Moving_Node_With_De // Verify the number of descendants of the target parent has increased by the number of descendants of the moved node plus the node itself Assert.AreEqual(initialDescendantsCountOfTargetParent + descendantsCountOfNodeToMove + 1, updatedDescendantsCountOfTargetParent); } + + [Test] + public void Cannot_Get_Node_From_Non_Existing_Content_Key() + { + // Arrange + var nonExistingKey = Guid.NewGuid(); + + // Act + NavigationNode? result = _navigationService.GetNavigationNode(nonExistingKey); + + // Assert + Assert.IsNull(result); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + public void Can_Get_Node_From_Existing_Content_Key(Guid key) + { + // Arrange + Guid nodeKey = Root; + + // Act + NavigationNode result = _navigationService.GetNavigationNode(nodeKey); + + // Assert + Assert.IsNotNull(result); + } + + [Test] + public void Cannot_Add_Navigation_Node_When_Parent_Does_Not_Exist() + { + // Arrange + var newNodeKey = Guid.NewGuid(); + var newNavigationNode = new NavigationNode(newNodeKey); + var nonExistentParentKey = Guid.NewGuid(); + + // Act + var result = _navigationService.AddNavigationNode(newNavigationNode, nonExistentParentKey); + + // Assert + Assert.IsFalse(result); + } + + [Test] + public void Cannot_Add_Navigation_Node_When_Node_With_The_Same_Key_Already_Exists() + { + // Arrange + NavigationNode existingNode = _navigationService.GetNavigationNode(Child1); + + // Act + var result = _navigationService.AddNavigationNode(existingNode); + + // Assert + Assert.IsFalse(result); + } + + [Test] + public void Can_Add_Navigation_Node_To_Content_Root() + { + // Arrange + var newNodeKey = Guid.NewGuid(); + var newNavigationNode = new NavigationNode(newNodeKey); + + // Act + var result = _navigationService.AddNavigationNode(newNavigationNode); // parentKey is null + + // Assert + Assert.IsTrue(result); + + var nodeExists = _navigationService.TryGetParentKey(newNodeKey, out Guid? parentKey); + + Assert.Multiple(() => + { + Assert.IsTrue(nodeExists); + Assert.IsNull(parentKey); + }); + } + + [Test] + public void Can_Add_Navigation_Node_With_Children_To_Content_Root() + { + // Arrange + var newNodeKey = Guid.NewGuid(); + var child1Key = Guid.NewGuid(); + var child2Key = Guid.NewGuid(); + + var newNavigationNode = new NavigationNode(newNodeKey); + var child1 = new NavigationNode(child1Key); + var child2 = new NavigationNode(child2Key); + + newNavigationNode.AddChild(child1); + newNavigationNode.AddChild(child2); + + // Act + var result = _navigationService.AddNavigationNode(newNavigationNode); // parentKey is null + + // Assert + Assert.IsTrue(result); + + var child1Exists = _navigationService.TryGetParentKey(child1Key, out Guid? child1ParentKey); + var child2Exists = _navigationService.TryGetParentKey(child2Key, out Guid? child2ParentKey); + + Assert.Multiple(() => + { + Assert.IsTrue(child1Exists); + Assert.AreEqual(newNodeKey, child1ParentKey); + + Assert.IsTrue(child2Exists); + Assert.AreEqual(newNodeKey, child2ParentKey); + }); + } + + [Test] + public void Can_Add_Navigation_Node_With_Descendants_To_Content_Root() + { + // Arrange + var newNodeKey = Guid.NewGuid(); + var child1Key = Guid.NewGuid(); + var child2Key = Guid.NewGuid(); + var grandchild1Key = Guid.NewGuid(); + var grandchild2Key = Guid.NewGuid(); + + var newNavigationNode = new NavigationNode(newNodeKey); + var child1 = new NavigationNode(child1Key); + var child2 = new NavigationNode(child2Key); + var grandchild1 = new NavigationNode(grandchild1Key); + var grandchild2 = new NavigationNode(grandchild2Key); + + child1.AddChild(grandchild1); + child2.AddChild(grandchild2); + + newNavigationNode.AddChild(child1); + newNavigationNode.AddChild(child2); + + // Act + var result = _navigationService.AddNavigationNode(newNavigationNode); // parentKey is null + + // Assert + Assert.IsTrue(result); + + _navigationService.TryGetDescendantsKeys(newNodeKey, out IEnumerable descendantsKeys); + + // Assert + Assert.AreEqual(4, descendantsKeys.Count()); + } + + [Test] + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "B606E3FF-E070-4D46-8CB9-D31352029FDF", 0, 1)] // Grandchild 1 to Child 3 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "A1B1B217-B02F-4307-862C-A5E22DB729EB", 2, 0)] // Child 2 to Grandchild 2 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "C6173927-0C59-4778-825D-D7B9F45D8DDE", 1, 2)] // Child 3 to Child 1 + public void Can_Add_Navigation_Node_To_Parent(Guid nodeKeyToAdd, Guid targetParentKey, int descendantsCountOfNodeToAdd, int initialDescendantsCountOfTargetParent) + { + // Arrange + // Retrieve the node to add before removing it from the navigation structure + var nodeToAdd = _navigationService.GetNavigationNode(nodeKeyToAdd); + + // Remove the node to ensure we can re-add it + _navigationService.Remove(nodeKeyToAdd); + + // Act + var result = _navigationService.AddNavigationNode(nodeToAdd, targetParentKey); + + // Assert + Assert.IsTrue(result); + + _navigationService.TryGetDescendantsKeys(nodeKeyToAdd, out IEnumerable addedNodeDescendantsKeys); + _navigationService.TryGetDescendantsKeys(targetParentKey, out IEnumerable updatedTargetParentDescendantsKeys); + + Assert.Multiple(() => + { + Assert.AreEqual(descendantsCountOfNodeToAdd, addedNodeDescendantsKeys.Count()); + + // Verify the number of descendants of the target parent has increased by the number of descendants of the added node plus the node itself + Assert.AreEqual(initialDescendantsCountOfTargetParent + descendantsCountOfNodeToAdd + 1, updatedTargetParentDescendantsKeys.Count()); + }); + } +} + +internal class TestContentNavigationService : ContentNavigationServiceBase +{ + public TestContentNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) + : base(coreScopeProvider, navigationRepository) + { + } + + // Not needed for testing here + public override Task RebuildAsync() => Task.CompletedTask; } From 733938fb0207c5efce0ea94fe503ea4c5a388b4c Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 7 Aug 2024 10:40:43 +0200 Subject: [PATCH 42/96] Updating the document navigation structure from the ContentService --- src/Umbraco.Core/Services/ContentService.cs | 27 ++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 42bab7c03953..0109750fdce4 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -34,7 +34,8 @@ public class ContentService : RepositoryService, IContentService private readonly IShortStringHelper _shortStringHelper; private readonly ICultureImpactFactory _cultureImpactFactory; private readonly IUserIdKeyResolver _userIdKeyResolver; - private readonly INavigationService _navigationService; + private readonly IDocumentNavigationService _documentNavigationService; + private readonly IDocumentRecycleBinNavigationService _documentRecycleBinNavigationService; private IQuery? _queryNotTrashed; #region Constructors @@ -53,7 +54,8 @@ public ContentService( IShortStringHelper shortStringHelper, ICultureImpactFactory cultureImpactFactory, IUserIdKeyResolver userIdKeyResolver, - INavigationService navigationService) + IDocumentNavigationService documentNavigationService, + IDocumentRecycleBinNavigationService documentRecycleBinNavigationService) : base(provider, loggerFactory, eventMessagesFactory) { _documentRepository = documentRepository; @@ -66,11 +68,12 @@ public ContentService( _shortStringHelper = shortStringHelper; _cultureImpactFactory = cultureImpactFactory; _userIdKeyResolver = userIdKeyResolver; - _navigationService = navigationService; + _documentNavigationService = documentNavigationService; + _documentRecycleBinNavigationService = documentRecycleBinNavigationService; _logger = loggerFactory.CreateLogger(); } - [Obsolete("Use constructor that takes INavigationService as a parameter, scheduled for removal in V16")] + [Obsolete("Use non-obsolete constructor. Scheduled for removal in V16.")] public ContentService( ICoreScopeProvider provider, ILoggerFactory loggerFactory, @@ -99,7 +102,8 @@ public ContentService( shortStringHelper, cultureImpactFactory, userIdKeyResolver, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -131,7 +135,8 @@ public ContentService( shortStringHelper, cultureImpactFactory, StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -1075,7 +1080,7 @@ public OperationResult Save(IContent content, int? userId = null, ContentSchedul // Updates in-memory navigation structure - we only handle new items, other updates are not a concern UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.ContentService.Save-with-contentSchedule", - () => _navigationService.Add(content.Key, GetParent(content)?.Key)); + () => _documentNavigationService.Add(content.Key, GetParent(content)?.Key)); if (contentSchedule != null) { @@ -1144,7 +1149,7 @@ public OperationResult Save(IEnumerable contents, int userId = Constan // Updates in-memory navigation structure - we only handle new items, other updates are not a concern UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.ContentService.Save", - () => _navigationService.Add(content.Key, GetParent(content)?.Key)); + () => _documentNavigationService.Add(content.Key, GetParent(content)?.Key)); } scope.Notifications.Publish( @@ -2346,7 +2351,7 @@ void DoDelete(IContent c) // Updates in-memory navigation structure UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.ContentService.DeleteLocked", - () => _navigationService.Remove(content.Key)); + () => _documentNavigationService.Remove(content.Key)); } } @@ -2630,7 +2635,7 @@ private void PerformMoveLocked(IContent content, int parentId, IContent? parent, // Updates in-memory navigation structure UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked", - () => _navigationService.Move(content.Key, parent?.Key)); + () => _documentNavigationService.Move(content.Key, parent?.Key)); } } @@ -2846,7 +2851,7 @@ public bool RecycleBinSmells() { foreach (Tuple update in navigationUpdates) { - _navigationService.Add(update.Item1, update.Item2); + _documentNavigationService.Add(update.Item1, update.Item2); } }); } From 7dff56ac894f187c21dd1d73002ae909ebc9ed8d Mon Sep 17 00:00:00 2001 From: Elitsa Date: Wed, 7 Aug 2024 14:33:19 +0200 Subject: [PATCH 43/96] Fix typo --- .../Services/Navigation/ContentNavigationServiceBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index 118d5144a4b3..03e14d4e0eae 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -234,7 +234,7 @@ public bool Move(Guid nodeKey, Guid? targetParentKey = null) protected async Task HandleRebuildAsync(int readLock, Guid objectTypeKey, bool trashed) { // This is only relevant for items in the content and media trees - if (readLock != -Constants.Locks.ContentTree && readLock != Constants.Locks.MediaTree) + if (readLock != Constants.Locks.ContentTree && readLock != Constants.Locks.MediaTree) { return; } From daea8c4d920f970168db596e446e1960f87f1b24 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 13:06:25 +0200 Subject: [PATCH 44/96] Adding trashed items implementation in base - currently managing 2 structures --- .../ContentNavigationServiceBase.cs | 308 +++++++++++------- 1 file changed, 197 insertions(+), 111 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index 03e14d4e0eae..ebfc49af7d65 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -10,6 +10,7 @@ internal abstract class ContentNavigationServiceBase private readonly ICoreScopeProvider _coreScopeProvider; private readonly INavigationRepository _navigationRepository; private ConcurrentDictionary _navigationStructure = new(); + private ConcurrentDictionary _recycleBinNavigationStructure = new(); protected ContentNavigationServiceBase(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) { @@ -20,97 +21,34 @@ protected ContentNavigationServiceBase(ICoreScopeProvider coreScopeProvider, INa public abstract Task RebuildAsync(); public bool TryGetParentKey(Guid childKey, out Guid? parentKey) - { - if (_navigationStructure.TryGetValue(childKey, out NavigationNode? childNode)) - { - parentKey = childNode.Parent?.Key; - return true; - } - - // Child doesn't exist - parentKey = null; - return false; - } + => TryGetParentKeyFromStructure(_navigationStructure, childKey, out parentKey); public bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys) - { - if (_navigationStructure.TryGetValue(parentKey, out NavigationNode? parentNode) is false) - { - // Parent doesn't exist - childrenKeys = []; - return false; - } - - childrenKeys = parentNode.Children.Select(child => child.Key); - return true; - } + => TryGetChildrenKeysFromStructure(_navigationStructure, parentKey, out childrenKeys); public bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys) - { - var descendants = new List(); - - if (_navigationStructure.TryGetValue(parentKey, out NavigationNode? parentNode) is false) - { - // Parent doesn't exist - descendantsKeys = []; - return false; - } - - GetDescendantsRecursively(parentNode, descendants); - - descendantsKeys = descendants; - return true; - } + => TryGetDescendantsKeysFromStructure(_navigationStructure, parentKey, out descendantsKeys); public bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKeys) - { - var ancestors = new List(); - - if (_navigationStructure.TryGetValue(childKey, out NavigationNode? childNode) is false) - { - // Child doesn't exist - ancestorsKeys = []; - return false; - } - - while (childNode?.Parent is not null) - { - ancestors.Add(childNode.Parent.Key); - childNode = childNode.Parent; - } - - ancestorsKeys = ancestors; - return true; - } + => TryGetAncestorsKeysFromStructure(_navigationStructure, childKey, out ancestorsKeys); public bool TryGetSiblingsKeys(Guid key, out IEnumerable siblingsKeys) - { - siblingsKeys = []; + => TryGetSiblingsKeysFromStructure(_navigationStructure, key, out siblingsKeys); - if (_navigationStructure.TryGetValue(key, out NavigationNode? node) is false) - { - return false; // Node doesn't exist - } + public bool TryGetParentKeyInBin(Guid childKey, out Guid? parentKey) + => TryGetParentKeyFromStructure(_recycleBinNavigationStructure, childKey, out parentKey); - 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 = _navigationStructure - .Where(kv => kv.Value.Parent is null && kv.Key != key) - .Select(kv => kv.Key) - .ToList(); - return true; - } + public bool TryGetChildrenKeysInBin(Guid parentKey, out IEnumerable childrenKeys) + => TryGetChildrenKeysFromStructure(_recycleBinNavigationStructure, parentKey, out childrenKeys); - if (TryGetChildrenKeys(node.Parent.Key, out IEnumerable childrenKeys) is false) - { - return false; // Couldn't retrieve children keys - } + public bool TryGetDescendantsKeysInBin(Guid parentKey, out IEnumerable descendantsKeys) + => TryGetDescendantsKeysFromStructure(_recycleBinNavigationStructure, parentKey, out descendantsKeys); - // Filter out the node itself to get its siblings - siblingsKeys = childrenKeys.Where(childKey => childKey != key).ToList(); - return true; - } + public bool TryGetAncestorsKeysInBin(Guid childKey, out IEnumerable ancestorsKeys) + => TryGetAncestorsKeysFromStructure(_recycleBinNavigationStructure, childKey, out ancestorsKeys); + + public bool TryGetSiblingsKeysInBin(Guid key, out IEnumerable siblingsKeys) + => TryGetSiblingsKeysFromStructure(_recycleBinNavigationStructure, key, out siblingsKeys); public NavigationNode? GetNavigationNode(Guid key) => _navigationStructure.GetValueOrDefault(key); @@ -144,31 +82,20 @@ public bool AddNavigationNode(NavigationNode node, Guid? parentKey = null) public bool Remove(Guid key) { - if (_navigationStructure.TryGetValue(key, out NavigationNode? nodeToRemove) is false) + if (TryRemoveNodeFromParentInStructure(_navigationStructure, key, out NavigationNode? nodeToRemove) is false || nodeToRemove is null) { return false; // Node doesn't exist } - // Remove the node from its parent's children list - if (nodeToRemove.Parent is not null && _navigationStructure.TryGetValue(nodeToRemove.Parent.Key, out NavigationNode? parentNode)) - { - parentNode.RemoveChild(nodeToRemove); - } - - // Recursively remove all descendants - RemoveDescendantsRecursively(nodeToRemove); + // Recursively remove all descendants and add them to recycle bin + AddDescendantsToRecycleBinRecursively(nodeToRemove); - // Remove the node itself - return _navigationStructure.TryRemove(key, out _); + return _recycleBinNavigationStructure.TryAdd(nodeToRemove.Key, nodeToRemove) && + _navigationStructure.TryRemove(key, out _); } public bool Add(Guid key, Guid? parentKey = null) { - if (_navigationStructure.ContainsKey(key)) - { - return false; // Node with this key already exists - } - NavigationNode? parentNode = null; if (parentKey.HasValue) { @@ -179,21 +106,24 @@ public bool Add(Guid key, Guid? parentKey = null) } var newNode = new NavigationNode(key); - _navigationStructure[key] = newNode; + if (_navigationStructure.TryAdd(key, newNode) is false) + { + return false; // Node with this key already exists + } parentNode?.AddChild(newNode); return true; } - public bool Move(Guid nodeKey, Guid? targetParentKey = null) + public bool Move(Guid key, Guid? targetParentKey = null) { - if (_navigationStructure.TryGetValue(nodeKey, out NavigationNode? nodeToMove) is false) + if (_navigationStructure.TryGetValue(key, out NavigationNode? nodeToMove) is false) { return false; // Node doesn't exist } - if (nodeKey == targetParentKey) + if (key == targetParentKey) { return false; // Cannot move a node to itself } @@ -210,18 +140,46 @@ public bool Move(Guid nodeKey, Guid? targetParentKey = null) currentParentNode.RemoveChild(nodeToMove); } - // Create a new node with the same key, to update the parent - var newNode = new NavigationNode(nodeToMove.Key); - // Set the new parent for the node (if parent node is null - the node is moved to root) - targetParentNode?.AddChild(newNode); + targetParentNode?.AddChild(nodeToMove); + + return true; + } - // Copy children to the new node - CopyChildren(nodeToMove, newNode); + public bool RemoveFromBin(Guid key) + { + if (TryRemoveNodeFromParentInStructure(_recycleBinNavigationStructure, key, out NavigationNode? nodeToRemove) is false || nodeToRemove is null) + { + return false; // Node doesn't exist + } - _navigationStructure[nodeToMove.Key] = newNode; + RemoveDescendantsRecursively(nodeToRemove); - return true; + return _recycleBinNavigationStructure.TryRemove(key, out _); + } + + public bool RestoreFromBin(Guid key, Guid? targetParentKey = null) + { + if (_recycleBinNavigationStructure.TryGetValue(key, out NavigationNode? nodeToRestore) is false) + { + return false; // Node doesn't exist + } + + // If a target parent is specified, try to find it in the main structure + NavigationNode? targetParentNode = null; + if (targetParentKey.HasValue && _navigationStructure.TryGetValue(targetParentKey.Value, out targetParentNode) is false) + { + return false; // Target parent doesn't exist + } + + // Set the new parent for the node (if parent node is null - the node is moved to root) + targetParentNode?.AddChild(nodeToRestore); + + // Restore the node and its descendants from the recycle bin to the main structure + RestoreNodeAndDescendantsRecursively(nodeToRestore); + + return _navigationStructure.TryAdd(nodeToRestore.Key, nodeToRestore) && + _recycleBinNavigationStructure.TryRemove(key, out _); } /// @@ -242,9 +200,107 @@ protected async Task HandleRebuildAsync(int readLock, Guid objectTypeKey, bool t using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); scope.ReadLock(readLock); - _navigationStructure = trashed - ? _navigationRepository.GetTrashedContentNodesByObjectType(objectTypeKey) - : _navigationRepository.GetContentNodesByObjectType(objectTypeKey); + if (trashed) + { + _recycleBinNavigationStructure = _navigationRepository.GetTrashedContentNodesByObjectType(objectTypeKey); + } + else + { + _navigationStructure = _navigationRepository.GetContentNodesByObjectType(objectTypeKey); + } + } + + private bool TryGetParentKeyFromStructure(ConcurrentDictionary structure, Guid childKey, out Guid? parentKey) + { + if (structure.TryGetValue(childKey, out NavigationNode? childNode)) + { + parentKey = childNode.Parent?.Key; + return true; + } + + // Child doesn't exist + parentKey = null; + return false; + } + + private bool TryGetChildrenKeysFromStructure(ConcurrentDictionary structure, Guid parentKey, out IEnumerable childrenKeys) + { + if (structure.TryGetValue(parentKey, out NavigationNode? parentNode) is false) + { + // Parent doesn't exist + childrenKeys = []; + return false; + } + + childrenKeys = parentNode.Children.Select(child => child.Key); + return true; + } + + private bool TryGetDescendantsKeysFromStructure(ConcurrentDictionary structure, Guid parentKey, out IEnumerable descendantsKeys) + { + var descendants = new List(); + + if (structure.TryGetValue(parentKey, out NavigationNode? parentNode) is false) + { + // Parent doesn't exist + descendantsKeys = []; + return false; + } + + GetDescendantsRecursively(parentNode, descendants); + + descendantsKeys = descendants; + return true; + } + + private bool TryGetAncestorsKeysFromStructure(ConcurrentDictionary structure, Guid childKey, out IEnumerable ancestorsKeys) + { + var ancestors = new List(); + + if (structure.TryGetValue(childKey, out NavigationNode? childNode) is false) + { + // Child doesn't exist + ancestorsKeys = []; + return false; + } + + while (childNode?.Parent is not null) + { + ancestors.Add(childNode.Parent.Key); + childNode = childNode.Parent; + } + + ancestorsKeys = ancestors; + return true; + } + + private bool TryGetSiblingsKeysFromStructure(ConcurrentDictionary structure, Guid key, out IEnumerable siblingsKeys) + { + siblingsKeys = []; + + if (structure.TryGetValue(key, out NavigationNode? node) is false) + { + return false; // Node doesn't exist + } + + 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) + .Select(kv => kv.Key) + .ToList(); + return true; + } + + if (TryGetChildrenKeys(node.Parent.Key, out IEnumerable childrenKeys) is false) + { + return false; // Couldn't retrieve children keys + } + + // Filter out the node itself to get its siblings + siblingsKeys = childrenKeys.Where(childKey => childKey != key).ToList(); + return true; } private void GetDescendantsRecursively(NavigationNode node, List descendants) @@ -256,12 +312,42 @@ private void GetDescendantsRecursively(NavigationNode node, List descendan } } + private bool TryRemoveNodeFromParentInStructure(ConcurrentDictionary structure, Guid key, out NavigationNode? nodeToRemove) + { + if (structure.TryGetValue(key, out nodeToRemove) is false) + { + return false; // Node doesn't exist + } + + // Remove the node from its parent's children list + if (nodeToRemove.Parent is not null && structure.TryGetValue(nodeToRemove.Parent.Key, out NavigationNode? parentNode)) + { + parentNode.RemoveChild(nodeToRemove); + } + + return true; + } + + private void AddDescendantsToRecycleBinRecursively(NavigationNode node) + { + foreach (NavigationNode child in node.Children) + { + AddDescendantsToRecycleBinRecursively(child); + + // Only remove the child from the main structure if it was successfully added to the recycle bin + if (_recycleBinNavigationStructure.TryAdd(child.Key, child)) + { + _navigationStructure.TryRemove(child.Key, out _); + } + } + } + private void RemoveDescendantsRecursively(NavigationNode node) { foreach (NavigationNode child in node.Children) { RemoveDescendantsRecursively(child); - _navigationStructure.TryRemove(child.Key, out _); + _recycleBinNavigationStructure.TryRemove(child.Key, out _); } } From 0eed393b4ba435a9f8a9d3326eb64d8d547f679e Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 13:09:38 +0200 Subject: [PATCH 45/96] Removing no longer relevant GetNavigationNode and AddNavigationNode --- .../ContentNavigationServiceBase.cs | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index ebfc49af7d65..15f274b9990e 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -50,36 +50,6 @@ public bool TryGetAncestorsKeysInBin(Guid childKey, out IEnumerable ancest public bool TryGetSiblingsKeysInBin(Guid key, out IEnumerable siblingsKeys) => TryGetSiblingsKeysFromStructure(_recycleBinNavigationStructure, key, out siblingsKeys); - public NavigationNode? GetNavigationNode(Guid key) - => _navigationStructure.GetValueOrDefault(key); - - public bool AddNavigationNode(NavigationNode node, Guid? parentKey = null) - { - if (_navigationStructure.ContainsKey(node.Key)) - { - return false; // Node with this key already exists - } - - NavigationNode? parentNode = null; - if (parentKey.HasValue) - { - if (_navigationStructure.TryGetValue(parentKey.Value, out parentNode) is false) - { - return false; // Parent node doesn't exist - } - } - - var newNodesMap = new Dictionary(); - CopyNodeHierarchyRecursively(node, parentNode, newNodesMap); - - foreach (NavigationNode newNode in newNodesMap.Values) - { - _navigationStructure[newNode.Key] = newNode; - } - - return true; - } - public bool Remove(Guid key) { if (TryRemoveNodeFromParentInStructure(_navigationStructure, key, out NavigationNode? nodeToRemove) is false || nodeToRemove is null) From 7a1b9f67b0f5c1742e1ffba6bf29bfac34973bd4 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 13:11:57 +0200 Subject: [PATCH 46/96] Fix removing parent when child is removed supporting methods --- .../Models/Navigation/NavigationNode.cs | 1 + .../ContentNavigationServiceBase.cs | 24 ------------------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/src/Umbraco.Core/Models/Navigation/NavigationNode.cs b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs index 2c3ee7acd9b2..eab9e06bcc48 100644 --- a/src/Umbraco.Core/Models/Navigation/NavigationNode.cs +++ b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs @@ -25,5 +25,6 @@ public void AddChild(NavigationNode child) public void RemoveChild(NavigationNode child) { _children.Remove(child); + child.Parent = null; } } diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index 15f274b9990e..4797e6bfdf76 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -320,28 +320,4 @@ private void RemoveDescendantsRecursively(NavigationNode node) _recycleBinNavigationStructure.TryRemove(child.Key, out _); } } - - private void CopyChildren(NavigationNode originalNode, NavigationNode newNode) - { - foreach (NavigationNode child in originalNode.Children) - { - newNode.AddChild(child); - } - } - - private void CopyNodeHierarchyRecursively(NavigationNode sourceNode, NavigationNode? newParent, Dictionary newNodesMap) - { - // Create a new node with the same key, to update the parent - var newNode = new NavigationNode(sourceNode.Key); - - // Set the new parent for the node (if parent node is null - the node is added to root) - newParent?.AddChild(newNode); - - newNodesMap[sourceNode.Key] = newNode; - - foreach (NavigationNode child in sourceNode.Children) - { - CopyNodeHierarchyRecursively(child, newNode, newNodesMap); - } - } } From 741b96c30e61460b1fbb31bf4371ccb22d888ebc Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 13:12:42 +0200 Subject: [PATCH 47/96] Added restoring functionality --- .../Navigation/ContentNavigationServiceBase.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index 4797e6bfdf76..5f27665c5bc1 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -20,6 +20,7 @@ protected ContentNavigationServiceBase(ICoreScopeProvider coreScopeProvider, INa public abstract Task RebuildAsync(); + // TODO: Maybe do base for querying and another for managing public bool TryGetParentKey(Guid childKey, out Guid? parentKey) => TryGetParentKeyFromStructure(_navigationStructure, childKey, out parentKey); @@ -32,6 +33,7 @@ public bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descenda public bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKeys) => TryGetAncestorsKeysFromStructure(_navigationStructure, childKey, out ancestorsKeys); + // TODO: add siblings and self where "and self" to be a param that you set and you filter out the result based on that public bool TryGetSiblingsKeys(Guid key, out IEnumerable siblingsKeys) => TryGetSiblingsKeysFromStructure(_navigationStructure, key, out siblingsKeys); @@ -320,4 +322,18 @@ private void RemoveDescendantsRecursively(NavigationNode node) _recycleBinNavigationStructure.TryRemove(child.Key, out _); } } + + private void RestoreNodeAndDescendantsRecursively(NavigationNode node) + { + foreach (NavigationNode child in node.Children) + { + RestoreNodeAndDescendantsRecursively(child); + + // Only remove the child from the recycle bin structure if it was successfully added to the main one + if (_navigationStructure.TryAdd(child.Key, child)) + { + _recycleBinNavigationStructure.TryRemove(child.Key, out _); + } + } + } } From aad1d79a6a743c929d0b5c1ca1eedca07462facc Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 13:13:33 +0200 Subject: [PATCH 48/96] Adding Bin functionality to DocumentNavigationService --- .../Services/Navigation/DocumentNavigationService.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs index 22921635bb3c..566da0e6463c 100644 --- a/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs @@ -12,4 +12,7 @@ public DocumentNavigationService(ICoreScopeProvider coreScopeProvider, INavigati public override async Task RebuildAsync() => await HandleRebuildAsync(Constants.Locks.ContentTree, Constants.ObjectTypes.Document, false); + + public async Task RebuildBinAsync() + => await HandleRebuildAsync(Constants.Locks.ContentTree, Constants.ObjectTypes.Document, true); } From 412028947b43762e9ed2a97829864d001b912075 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 13:14:20 +0200 Subject: [PATCH 49/96] Removing Move signature from IDocumentNavigationService --- .../Services/Navigation/IDocumentNavigationService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Core/Services/Navigation/IDocumentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/IDocumentNavigationService.cs index 241a92cc90d7..75d2239c82e6 100644 --- a/src/Umbraco.Core/Services/Navigation/IDocumentNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/IDocumentNavigationService.cs @@ -2,5 +2,4 @@ namespace Umbraco.Cms.Core.Services.Navigation; public interface IDocumentNavigationService : INavigationQueryService, INavigationManagementService { - bool Move(Guid nodeKey, Guid? targetParentKey = null); } From 77a23db29f32e856ebc0d2169ac30872437859c6 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 13:16:11 +0200 Subject: [PATCH 50/96] Adding RecycleBin query and management services --- .../Navigation/INavigationManagementService.cs | 2 +- .../Services/Navigation/INavigationQueryService.cs | 2 +- .../IRecycleBinNavigationManagementService.cs | 10 ++++++++++ .../IRecycleBinNavigationQueryService.cs | 14 ++++++++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationManagementService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs diff --git a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs index 996ec6cb3146..61078d76a9c6 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs @@ -6,7 +6,7 @@ namespace Umbraco.Cms.Core.Services.Navigation; /// Placeholder for sharing logic between the document, document recycle bin, media and media recycle bin navigation services /// for managing the navigation structure. /// -public interface INavigationManagementService +public interface INavigationManagementService : IRecycleBinNavigationManagementService { Task RebuildAsync(); diff --git a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs index 062f877d5776..1cea878344cd 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Core.Services.Navigation; /// Placeholder for sharing logic between the document, document recycle bin, media and media recycle bin navigation services /// for querying the navigation structure. /// -public interface INavigationQueryService +public interface INavigationQueryService : IRecycleBinNavigationQueryService { bool TryGetParentKey(Guid childKey, out Guid? parentKey); diff --git a/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationManagementService.cs new file mode 100644 index 000000000000..eb41eea6c342 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationManagementService.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IRecycleBinNavigationManagementService +{ + Task RebuildBinAsync(); + + bool RemoveFromBin(Guid key); + + bool RestoreFromBin(Guid key, Guid? targetParentKey = null); +} diff --git a/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs new file mode 100644 index 000000000000..45396524a666 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IRecycleBinNavigationQueryService +{ + bool TryGetParentKeyInBin(Guid childKey, out Guid? parentKey); + + bool TryGetChildrenKeysInBin(Guid parentKey, out IEnumerable childrenKeys); + + bool TryGetDescendantsKeysInBin(Guid parentKey, out IEnumerable descendantsKeys); + + bool TryGetAncestorsKeysInBin(Guid childKey, out IEnumerable ancestorsKeys); + + bool TryGetSiblingsKeysInBin(Guid key, out IEnumerable siblingsKeys); +} From d85affff74ebaf6d5cb18ef65eeec6373571d7e5 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 13:17:28 +0200 Subject: [PATCH 51/96] Re-adding Move and removing GetNavigationNode and AddNavigationNode signatures from interface --- .../Services/Navigation/INavigationManagementService.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs index 61078d76a9c6..8669766c90cc 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs @@ -1,5 +1,3 @@ -using Umbraco.Cms.Core.Models.Navigation; - namespace Umbraco.Cms.Core.Services.Navigation; /// @@ -14,7 +12,5 @@ public interface INavigationManagementService : IRecycleBinNavigationManagementS bool Add(Guid key, Guid? parentKey = null); - NavigationNode? GetNavigationNode(Guid key); - - bool AddNavigationNode(NavigationNode node, Guid? parentKey = null); + bool Move(Guid key, Guid? targetParentKey = null); } From 48514466612984dea2373874597dfe7786984914 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 13:18:50 +0200 Subject: [PATCH 52/96] Rebuilding bin structure using _documentNavigationService, instead of _documentRecycleBinNavigationService --- .../Services/Navigation/NavigationInitializationService.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs b/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs index 9c0b6a917477..03ec81269a96 100644 --- a/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs +++ b/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs @@ -28,6 +28,8 @@ public NavigationInitializationService( public async Task StartingAsync(CancellationToken cancellationToken) { await _documentNavigationService.RebuildAsync(); + await _documentNavigationService.RebuildBinAsync(); + await _documentRecycleBinNavigationService.RebuildAsync(); await _mediaNavigationService.RebuildAsync(); await _mediaRecycleBinNavigationService.RebuildAsync(); From da082bf4a17940af65278cc4116765c7ace1cbed Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 13:27:31 +0200 Subject: [PATCH 53/96] Fixing test name --- .../Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs index 378c2f9fb67b..ee741f704745 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs @@ -1,6 +1,5 @@ using Moq; using NUnit.Framework; -using Umbraco.Cms.Core.Models.Navigation; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Navigation; @@ -549,7 +548,7 @@ public void Can_Add_Node_To_Parent(Guid parentKey) } [Test] - public void Cannot_Move_When_Target_Parent_Does_Not_Exist() + public void Cannot_Move_Node_When_Target_Parent_Does_Not_Exist() { // Arrange Guid nodeToMove = Child1; From 81bb6d68e4afbd1a44865ac8bc3862acfef73c2e Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 13:28:14 +0200 Subject: [PATCH 54/96] Adding more tests for remove --- .../ContentNavigationServiceBaseTests.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs index ee741f704745..e093db6fc3a0 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs @@ -446,6 +446,33 @@ public void Cannot_Remove_Node_With_Non_Existing_Content_Key() Assert.IsFalse(result); } + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573")] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 + public void Can_Remove_Node(Guid keyOfNodeToRemove) + { + // Act + var result = _navigationService.Remove(keyOfNodeToRemove); + + // Assert + Assert.IsTrue(result); + + var nodeExists = _navigationService.TryGetParentKey(keyOfNodeToRemove, out Guid? parentKey); + + Assert.Multiple(() => + { + Assert.IsFalse(nodeExists); + Assert.IsNull(parentKey); + }); + } + [Test] [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 @@ -475,6 +502,61 @@ public void Removing_Node_Removes_Its_Descendants_As_Well(Guid keyOfNodeToRemove }); } + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573")] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 + public void Removing_Node_Adds_It_To_Recycle_Bin_Root(Guid keyOfNodeToRemove) + { + // Act + _navigationService.Remove(keyOfNodeToRemove); + + // Assert + var nodeExistsInBin = _navigationService.TryGetParentKeyInBin(keyOfNodeToRemove, out Guid? parentKeyInBin); + + Assert.Multiple(() => + { + Assert.IsTrue(nodeExistsInBin); + Assert.IsNull(parentKeyInBin); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + public void Removing_Node_Adds_Its_Descendants_To_Recycle_Bin_As_Well(Guid keyOfNodeToRemove) + { + // Arrange + _navigationService.TryGetDescendantsKeys(keyOfNodeToRemove, out IEnumerable initialDescendantsKeys); + List initialDescendantsList = initialDescendantsKeys.ToList(); + + // Act + _navigationService.Remove(keyOfNodeToRemove); + + // Assert + var nodeExistsInBin = _navigationService.TryGetDescendantsKeysInBin(keyOfNodeToRemove, out IEnumerable descendantsKeysInBin); + + Assert.Multiple(() => + { + Assert.IsTrue(nodeExistsInBin); + CollectionAssert.AreEqual(initialDescendantsList, descendantsKeysInBin); + + foreach (Guid descendant in initialDescendantsList) + { + _navigationService.TryGetParentKeyInBin(descendant, out Guid? parentKeyInBin); + Assert.IsNotNull(parentKeyInBin); // The descendant kept its initial parent + } + }); + } + [Test] public void Cannot_Add_Node_When_Parent_Does_Not_Exist() { From 4093903317155feea5fc4fde6362db11375a842e Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 13:29:20 +0200 Subject: [PATCH 55/96] Adding tests for restore and removing ones for GetNavigationNode and AddNavigationNode --- .../ContentNavigationServiceBaseTests.cs | 181 ++++++++---------- 1 file changed, 75 insertions(+), 106 deletions(-) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs index e093db6fc3a0..4d1457eea31f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs @@ -835,180 +835,149 @@ public void Number_Of_Target_Parent_Descendants_Updates_When_Moving_Node_With_De } [Test] - public void Cannot_Get_Node_From_Non_Existing_Content_Key() + public void Cannot_Restore_Node_When_Target_Parent_Does_Not_Exist() { // Arrange - var nonExistingKey = Guid.NewGuid(); - - // Act - NavigationNode? result = _navigationService.GetNavigationNode(nonExistingKey); - - // Assert - Assert.IsNull(result); - } - - [Test] - [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root - [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 - public void Can_Get_Node_From_Existing_Content_Key(Guid key) - { - // Arrange - Guid nodeKey = Root; - - // Act - NavigationNode result = _navigationService.GetNavigationNode(nodeKey); - - // Assert - Assert.IsNotNull(result); - } - - [Test] - public void Cannot_Add_Navigation_Node_When_Parent_Does_Not_Exist() - { - // Arrange - var newNodeKey = Guid.NewGuid(); - var newNavigationNode = new NavigationNode(newNodeKey); - var nonExistentParentKey = Guid.NewGuid(); + Guid nodeToRestore = Grandchild1; + var nonExistentTargetParentKey = Guid.NewGuid(); + _navigationService.Remove(nodeToRestore); // Act - var result = _navigationService.AddNavigationNode(newNavigationNode, nonExistentParentKey); + var result = _navigationService.RestoreFromBin(nodeToRestore, nonExistentTargetParentKey); // Assert Assert.IsFalse(result); } [Test] - public void Cannot_Add_Navigation_Node_When_Node_With_The_Same_Key_Already_Exists() + public void Cannot_Restore_Node_That_Does_Not_Exist() { // Arrange - NavigationNode existingNode = _navigationService.GetNavigationNode(Child1); + Guid notDeletedNodeKey = Grandchild4; + Guid targetParentKey = Child3; // Act - var result = _navigationService.AddNavigationNode(existingNode); + var result = _navigationService.RestoreFromBin(notDeletedNodeKey, targetParentKey); // Assert Assert.IsFalse(result); } [Test] - public void Can_Add_Navigation_Node_To_Content_Root() + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", null)] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", "60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", "D63C1621-C74A-4106-8587-817DEE5FB732")] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 3 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", "B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Grandchild 4 + public void Can_Restore_Node_To_Existing_Target_Parent(Guid nodeToRestore, Guid? targetParentKey) { // Arrange - var newNodeKey = Guid.NewGuid(); - var newNavigationNode = new NavigationNode(newNodeKey); + _navigationService.Remove(nodeToRestore); // Act - var result = _navigationService.AddNavigationNode(newNavigationNode); // parentKey is null + var result = _navigationService.RestoreFromBin(nodeToRestore, targetParentKey); // Assert Assert.IsTrue(result); - var nodeExists = _navigationService.TryGetParentKey(newNodeKey, out Guid? parentKey); + // Verify the node's new parent is updated + _navigationService.TryGetParentKey(nodeToRestore, out Guid? parentKeyAfterRestore); Assert.Multiple(() => { - Assert.IsTrue(nodeExists); - Assert.IsNull(parentKey); + if (targetParentKey is null) + { + Assert.IsNull(parentKeyAfterRestore); + } + else + { + Assert.IsNotNull(parentKeyAfterRestore); + } + + Assert.AreEqual(targetParentKey, parentKeyAfterRestore); }); } [Test] - public void Can_Add_Navigation_Node_With_Children_To_Content_Root() + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 1 to Child 1 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", "60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Great-grandchild 1 to Child 2 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 3 to Root + public void Restored_Node_Is_Added_To_Its_Target_Parent(Guid nodeToRestore, Guid targetParentKey) { // Arrange - var newNodeKey = Guid.NewGuid(); - var child1Key = Guid.NewGuid(); - var child2Key = Guid.NewGuid(); - - var newNavigationNode = new NavigationNode(newNodeKey); - var child1 = new NavigationNode(child1Key); - var child2 = new NavigationNode(child2Key); - - newNavigationNode.AddChild(child1); - newNavigationNode.AddChild(child2); + _navigationService.Remove(nodeToRestore); + _navigationService.TryGetChildrenKeys(targetParentKey, out IEnumerable targetParentChildrenKeys); + var targetParentChildrenCount = targetParentChildrenKeys.Count(); // Act - var result = _navigationService.AddNavigationNode(newNavigationNode); // parentKey is null + var result = _navigationService.RestoreFromBin(nodeToRestore, targetParentKey); // Assert Assert.IsTrue(result); - var child1Exists = _navigationService.TryGetParentKey(child1Key, out Guid? child1ParentKey); - var child2Exists = _navigationService.TryGetParentKey(child2Key, out Guid? child2ParentKey); + // Verify the node is added to its target parent's children list + _navigationService.TryGetChildrenKeys(targetParentKey, out IEnumerable childrenKeys); + List childrenList = childrenKeys.ToList(); Assert.Multiple(() => { - Assert.IsTrue(child1Exists); - Assert.AreEqual(newNodeKey, child1ParentKey); - - Assert.IsTrue(child2Exists); - Assert.AreEqual(newNodeKey, child2ParentKey); + CollectionAssert.Contains(childrenList, nodeToRestore); + Assert.AreEqual(targetParentChildrenCount + 1, childrenList.Count); }); } [Test] - public void Can_Add_Navigation_Node_With_Descendants_To_Content_Root() + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 + public void Restored_Node_And_Its_Descendants_Are_Removed_From_Bin(Guid nodeToRestore) { // Arrange - var newNodeKey = Guid.NewGuid(); - var child1Key = Guid.NewGuid(); - var child2Key = Guid.NewGuid(); - var grandchild1Key = Guid.NewGuid(); - var grandchild2Key = Guid.NewGuid(); - - var newNavigationNode = new NavigationNode(newNodeKey); - var child1 = new NavigationNode(child1Key); - var child2 = new NavigationNode(child2Key); - var grandchild1 = new NavigationNode(grandchild1Key); - var grandchild2 = new NavigationNode(grandchild2Key); - - child1.AddChild(grandchild1); - child2.AddChild(grandchild2); - - newNavigationNode.AddChild(child1); - newNavigationNode.AddChild(child2); + _navigationService.Remove(nodeToRestore); + _navigationService.TryGetDescendantsKeysInBin(nodeToRestore, out IEnumerable descendantsKeysInBin); // Act - var result = _navigationService.AddNavigationNode(newNavigationNode); // parentKey is null + _navigationService.RestoreFromBin(nodeToRestore); // Assert - Assert.IsTrue(result); + var nodeExistsInBin = _navigationService.TryGetParentKeyInBin(nodeToRestore, out Guid? parentKeyInBinAfterRestore); - _navigationService.TryGetDescendantsKeys(newNodeKey, out IEnumerable descendantsKeys); + Assert.Multiple(() => + { + Assert.IsFalse(nodeExistsInBin); + Assert.IsNull(parentKeyInBinAfterRestore); - // Assert - Assert.AreEqual(4, descendantsKeys.Count()); + foreach (Guid descendant in descendantsKeysInBin) + { + var descendantExistsInBin = _navigationService.TryGetParentKeyInBin(descendant, out _); + Assert.IsFalse(descendantExistsInBin); + } + }); } [Test] - [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "B606E3FF-E070-4D46-8CB9-D31352029FDF", 0, 1)] // Grandchild 1 to Child 3 - [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "A1B1B217-B02F-4307-862C-A5E22DB729EB", 2, 0)] // Child 2 to Grandchild 2 - [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "C6173927-0C59-4778-825D-D7B9F45D8DDE", 1, 2)] // Child 3 to Child 1 - public void Can_Add_Navigation_Node_To_Parent(Guid nodeKeyToAdd, Guid targetParentKey, int descendantsCountOfNodeToAdd, int initialDescendantsCountOfTargetParent) + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", null, 8)] // Root to content root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", 2)] // Child 1 to Great-grandchild 1 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", "60E0E5C4-084E-4144-A560-7393BEAD2E96", 0)] // Grandchild 4 to Child 2 + public void Restored_Node_Has_The_Same_Amount_Of_Descendants(Guid nodeToRestore, Guid? targetParentKey, int initialDescendantsCount) { // Arrange - // Retrieve the node to add before removing it from the navigation structure - var nodeToAdd = _navigationService.GetNavigationNode(nodeKeyToAdd); - - // Remove the node to ensure we can re-add it - _navigationService.Remove(nodeKeyToAdd); + _navigationService.Remove(nodeToRestore); // Act - var result = _navigationService.AddNavigationNode(nodeToAdd, targetParentKey); + _navigationService.RestoreFromBin(nodeToRestore, targetParentKey); // Assert - Assert.IsTrue(result); - - _navigationService.TryGetDescendantsKeys(nodeKeyToAdd, out IEnumerable addedNodeDescendantsKeys); - _navigationService.TryGetDescendantsKeys(targetParentKey, out IEnumerable updatedTargetParentDescendantsKeys); + // Verify that the number of descendants remain the same after restoring the node + _navigationService.TryGetDescendantsKeys(nodeToRestore, out IEnumerable restoredDescendantsKeys); + var descendantsCountAfterRestore = restoredDescendantsKeys.Count(); - Assert.Multiple(() => - { - Assert.AreEqual(descendantsCountOfNodeToAdd, addedNodeDescendantsKeys.Count()); - - // Verify the number of descendants of the target parent has increased by the number of descendants of the added node plus the node itself - Assert.AreEqual(initialDescendantsCountOfTargetParent + descendantsCountOfNodeToAdd + 1, updatedTargetParentDescendantsKeys.Count()); - }); + Assert.AreEqual(initialDescendantsCount, descendantsCountAfterRestore); } } From 4ce9f95e839ff456e611e9b82f78d75c68273f75 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 13:38:25 +0200 Subject: [PATCH 56/96] Remove comments --- .../Services/Navigation/ContentNavigationServiceBase.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index 5f27665c5bc1..1bc0f023ab5e 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -20,7 +20,6 @@ protected ContentNavigationServiceBase(ICoreScopeProvider coreScopeProvider, INa public abstract Task RebuildAsync(); - // TODO: Maybe do base for querying and another for managing public bool TryGetParentKey(Guid childKey, out Guid? parentKey) => TryGetParentKeyFromStructure(_navigationStructure, childKey, out parentKey); @@ -33,7 +32,6 @@ public bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descenda public bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKeys) => TryGetAncestorsKeysFromStructure(_navigationStructure, childKey, out ancestorsKeys); - // TODO: add siblings and self where "and self" to be a param that you set and you filter out the result based on that public bool TryGetSiblingsKeys(Guid key, out IEnumerable siblingsKeys) => TryGetSiblingsKeysFromStructure(_navigationStructure, key, out siblingsKeys); From ea0aea91f7bb26dc456276c90d155d360c874fdb Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 14:14:49 +0200 Subject: [PATCH 57/96] Removing document and media RecycleBinNavigationService and their interfaces --- .../DocumentRecycleBinNavigationService.cs | 15 --------------- .../IDocumentRecycleBinNavigationService.cs | 6 ------ .../IMediaRecycleBinNavigationService.cs | 5 ----- .../MediaRecycleBinNavigationService.cs | 15 --------------- .../Services/DocumentNavigationServiceTests.cs | 3 +-- 5 files changed, 1 insertion(+), 43 deletions(-) delete mode 100644 src/Umbraco.Core/Services/Navigation/DocumentRecycleBinNavigationService.cs delete mode 100644 src/Umbraco.Core/Services/Navigation/IDocumentRecycleBinNavigationService.cs delete mode 100644 src/Umbraco.Core/Services/Navigation/IMediaRecycleBinNavigationService.cs delete mode 100644 src/Umbraco.Core/Services/Navigation/MediaRecycleBinNavigationService.cs diff --git a/src/Umbraco.Core/Services/Navigation/DocumentRecycleBinNavigationService.cs b/src/Umbraco.Core/Services/Navigation/DocumentRecycleBinNavigationService.cs deleted file mode 100644 index dcf4f04b7d99..000000000000 --- a/src/Umbraco.Core/Services/Navigation/DocumentRecycleBinNavigationService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; - -namespace Umbraco.Cms.Core.Services.Navigation; - -internal sealed class DocumentRecycleBinNavigationService : ContentNavigationServiceBase, IDocumentRecycleBinNavigationService -{ - public DocumentRecycleBinNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) - : base(coreScopeProvider, navigationRepository) - { - } - - public override async Task RebuildAsync() - => await HandleRebuildAsync(Constants.Locks.ContentTree, Constants.ObjectTypes.Document, true); -} diff --git a/src/Umbraco.Core/Services/Navigation/IDocumentRecycleBinNavigationService.cs b/src/Umbraco.Core/Services/Navigation/IDocumentRecycleBinNavigationService.cs deleted file mode 100644 index 7005862b08a7..000000000000 --- a/src/Umbraco.Core/Services/Navigation/IDocumentRecycleBinNavigationService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Umbraco.Cms.Core.Services.Navigation; - -public interface IDocumentRecycleBinNavigationService : INavigationQueryService, INavigationManagementService -{ -} - diff --git a/src/Umbraco.Core/Services/Navigation/IMediaRecycleBinNavigationService.cs b/src/Umbraco.Core/Services/Navigation/IMediaRecycleBinNavigationService.cs deleted file mode 100644 index 114053f1bef8..000000000000 --- a/src/Umbraco.Core/Services/Navigation/IMediaRecycleBinNavigationService.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Umbraco.Cms.Core.Services.Navigation; - -public interface IMediaRecycleBinNavigationService : INavigationQueryService, INavigationManagementService -{ -} diff --git a/src/Umbraco.Core/Services/Navigation/MediaRecycleBinNavigationService.cs b/src/Umbraco.Core/Services/Navigation/MediaRecycleBinNavigationService.cs deleted file mode 100644 index 7a96b749ab5f..000000000000 --- a/src/Umbraco.Core/Services/Navigation/MediaRecycleBinNavigationService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; - -namespace Umbraco.Cms.Core.Services.Navigation; - -internal sealed class MediaRecycleBinNavigationService : ContentNavigationServiceBase, IMediaRecycleBinNavigationService -{ - public MediaRecycleBinNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) - : base(coreScopeProvider, navigationRepository) - { - } - - public override async Task RebuildAsync() - => await HandleRebuildAsync(Constants.Locks.MediaTree, Constants.ObjectTypes.Media, true); -} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs index d0987953a657..8d9cb5059b7a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs @@ -23,8 +23,6 @@ public class DocumentNavigationServiceTests : UmbracoIntegrationTest private IDocumentNavigationService DocumentNavigationService => GetRequiredService(); - private IDocumentRecycleBinNavigationService DocumentRecycleBinNavigationService => GetRequiredService(); - private ContentType ContentType { get; set; } private IContent Root { get; set; } @@ -110,6 +108,7 @@ public async Task Setup() // } [Test] + // TODO: Test that you can rebuild bin structure as well public async Task Structure_Can_Rebuild() { // Arrange From 3391b3f9768185c2161f0adc55093db85904835c Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 14:15:29 +0200 Subject: [PATCH 58/96] Adding media rebuild bin --- src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs b/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs index b81ea155018f..a85d2a16c159 100644 --- a/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs @@ -12,4 +12,7 @@ public MediaNavigationService(ICoreScopeProvider coreScopeProvider, INavigationR public override async Task RebuildAsync() => await HandleRebuildAsync(Constants.Locks.MediaTree, Constants.ObjectTypes.Media, false); + + public async Task RebuildBinAsync() + => await HandleRebuildAsync(Constants.Locks.MediaTree, Constants.ObjectTypes.Media, true); } From b2493a6136b372de6c5da476f5972765ceb72580 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 14:16:02 +0200 Subject: [PATCH 59/96] Fixing initialization with correct interfaces --- .../Navigation/NavigationInitializationService.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs b/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs index 03ec81269a96..551eeb97fbcc 100644 --- a/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs +++ b/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs @@ -9,30 +9,20 @@ namespace Umbraco.Cms.Core.Services.Navigation; public class NavigationInitializationService : IHostedLifecycleService { private readonly IDocumentNavigationService _documentNavigationService; - private readonly IDocumentRecycleBinNavigationService _documentRecycleBinNavigationService; private readonly IMediaNavigationService _mediaNavigationService; - private readonly IMediaRecycleBinNavigationService _mediaRecycleBinNavigationService; - public NavigationInitializationService( - IDocumentNavigationService documentNavigationService, - IDocumentRecycleBinNavigationService documentRecycleBinNavigationService, - IMediaNavigationService mediaNavigationService, - IMediaRecycleBinNavigationService mediaRecycleBinNavigationService) + public NavigationInitializationService(IDocumentNavigationService documentNavigationService, IMediaNavigationService mediaNavigationService) { _documentNavigationService = documentNavigationService; - _documentRecycleBinNavigationService = documentRecycleBinNavigationService; _mediaNavigationService = mediaNavigationService; - _mediaRecycleBinNavigationService = mediaRecycleBinNavigationService; } public async Task StartingAsync(CancellationToken cancellationToken) { await _documentNavigationService.RebuildAsync(); await _documentNavigationService.RebuildBinAsync(); - - await _documentRecycleBinNavigationService.RebuildAsync(); await _mediaNavigationService.RebuildAsync(); - await _mediaRecycleBinNavigationService.RebuildAsync(); + await _mediaNavigationService.RebuildBinAsync(); } public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; From 3ad9ff01144f29b3c34bb676d40cb963542f8563 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 14:16:23 +0200 Subject: [PATCH 60/96] Removing RecycleBinNavigationServices' registration --- src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index f7f870505af6..93b12570ba19 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -354,9 +354,7 @@ private void AddCoreServices() Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); - Services.AddUnique(); Services.AddUnique(); - Services.AddUnique(); // Register a noop IHtmlSanitizer & IMarkdownSanitizer to be replaced Services.AddUnique(); From e582e5f277082a4cc678b4281098c9410f7ae077 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 9 Aug 2024 14:17:41 +0200 Subject: [PATCH 61/96] Remove IDocumentRecycleBinNavigationService dependency --- src/Umbraco.Core/Services/ContentService.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 0109750fdce4..1840ff92b381 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -35,7 +35,6 @@ public class ContentService : RepositoryService, IContentService private readonly ICultureImpactFactory _cultureImpactFactory; private readonly IUserIdKeyResolver _userIdKeyResolver; private readonly IDocumentNavigationService _documentNavigationService; - private readonly IDocumentRecycleBinNavigationService _documentRecycleBinNavigationService; private IQuery? _queryNotTrashed; #region Constructors @@ -54,8 +53,7 @@ public ContentService( IShortStringHelper shortStringHelper, ICultureImpactFactory cultureImpactFactory, IUserIdKeyResolver userIdKeyResolver, - IDocumentNavigationService documentNavigationService, - IDocumentRecycleBinNavigationService documentRecycleBinNavigationService) + IDocumentNavigationService documentNavigationService) : base(provider, loggerFactory, eventMessagesFactory) { _documentRepository = documentRepository; @@ -69,7 +67,6 @@ public ContentService( _cultureImpactFactory = cultureImpactFactory; _userIdKeyResolver = userIdKeyResolver; _documentNavigationService = documentNavigationService; - _documentRecycleBinNavigationService = documentRecycleBinNavigationService; _logger = loggerFactory.CreateLogger(); } @@ -102,8 +99,7 @@ public ContentService( shortStringHelper, cultureImpactFactory, userIdKeyResolver, - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -135,8 +131,7 @@ public ContentService( shortStringHelper, cultureImpactFactory, StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService()) { } From 602480de037760c2f2244e326c8d9963107d555c Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 12 Aug 2024 14:06:15 +0200 Subject: [PATCH 62/96] Updating in-memory nav structure when content updates happen --- src/Umbraco.Core/Services/ContentService.cs | 41 +++++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 1840ff92b381..b4690055f894 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -2339,14 +2339,22 @@ void DoDelete(IContent c) if (content.Trashed) { - // TODO: call trashed navigation delete + // Updates in-memory navigation structure for recycle bin items + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.DeleteLocked-trashed", + () => _documentNavigationService.RemoveFromBin(content.Key)); } else { - // Updates in-memory navigation structure + // Updates in-memory navigation structure for both documents and recycle bin items + // as the item needs to be deleted whether it is in the recycle bin or not UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.ContentService.DeleteLocked", - () => _documentNavigationService.Remove(content.Key)); + () => + { + _documentNavigationService.Remove(content.Key); + _documentNavigationService.RemoveFromBin(content.Key); + }); } } @@ -2572,6 +2580,8 @@ public OperationResult Move(IContent content, int parentId, int userId = Constan // trash indicates whether we are trashing, un-trashing, or not changing anything private void PerformMoveLocked(IContent content, int parentId, IContent? parent, int userId, ICollection<(IContent, string)> moves, bool? trash) { + // Needed to update the in-memory navigation structure + var cameFromRecycleBin = content.ParentId == Constants.System.RecycleBinContent; content.WriterId = userId; content.ParentId = parentId; @@ -2623,14 +2633,29 @@ private void PerformMoveLocked(IContent content, int parentId, IContent? parent, if (parentId == Constants.System.RecycleBinContent) { - // TODO: call trashed navigation move + // Updates in-memory navigation structure for both document items and recycle bin items + // as we are moving to recycle bin + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked-to-recycle-bin", + () => _documentNavigationService.Remove(content.Key)); } else { - // Updates in-memory navigation structure - UpdateInMemoryNavigationStructure( - "Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked", - () => _documentNavigationService.Move(content.Key, parent?.Key)); + if (cameFromRecycleBin) + { + // Updates in-memory navigation structure for both document items and recycle bin items + // as we are restoring from recycle bin + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked-restore", + () => _documentNavigationService.RestoreFromBin(content.Key, parent?.Key)); + } + else + { + // Updates in-memory navigation structure + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked", + () => _documentNavigationService.Move(content.Key, parent?.Key)); + } } } From da0f2b656111a9eeae8e2ae097d0437d3566dc2b Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 12 Aug 2024 14:07:14 +0200 Subject: [PATCH 63/96] Adding the rest of the integration tests --- .../DocumentNavigationServiceTests.cs | 174 +++++++++--------- 1 file changed, 91 insertions(+), 83 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs index 8d9cb5059b7a..6b5caec46edf 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs @@ -258,49 +258,51 @@ public async Task Structure_Does_Not_Update_When_Updating_Content() // TODO: test that item exists in recycle bin str. and it is removed from content str; // TODO: also check that initial siblings count's decreased, // TODO: and that descendants are still the same (i.e. they've also been moved to recycle bin) - // [Test] - // public async Task Structure_Updates_When_Moving_Content_To_Recycle_Bin() // TODO: Missing implementation - // { - // // Arrange - // Guid nodeToMoveToRecycleBin = Child3.Key; - // NavigationService.TryGetParentKey(nodeToMoveToRecycleBin, out Guid? originalParentKey); - // - // // Act - // await ContentEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey); - // var nodeExists = NavigationService.TryGetParentKey(nodeToMoveToRecycleBin, out Guid? updatedParentKey); // Verify that the item is no longer in the content structure - // var nodeExistsInRecycleBin = NavigationService.TryGetParentKey(nodeToMoveToRecycleBin, out Guid? updatedParentKey); // TODO: use recycle bin str. - // - // // Assert - // Assert.Multiple(() => - // { - // Assert.IsFalse(nodeExists); - // Assert.IsTrue(nodeExistsInRecycleBin); - // Assert.AreNotEqual(originalParentKey, updatedParentKey); - // Assert.IsNull(updatedParentKey); // Verify the node's parent is now located at the root of the recycle bin (null) - // }); - // } + [Test] + public async Task Structure_Updates_When_Moving_Content_To_Recycle_Bin() + { + // Arrange + Guid nodeToMoveToRecycleBin = Child3.Key; + DocumentNavigationService.TryGetParentKey(nodeToMoveToRecycleBin, out Guid? originalParentKey); + + // Act + await ContentEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey); + + // Assert + var nodeExists = DocumentNavigationService.TryGetParentKey(nodeToMoveToRecycleBin, out _); // Verify that the item is no longer in the document structure + var nodeExistsInRecycleBin = DocumentNavigationService.TryGetParentKeyInBin(nodeToMoveToRecycleBin, out Guid? updatedParentKeyInRecycleBin); + + Assert.Multiple(() => + { + Assert.IsFalse(nodeExists); + Assert.IsTrue(nodeExistsInRecycleBin); + Assert.AreNotEqual(originalParentKey, updatedParentKeyInRecycleBin); + Assert.IsNull(updatedParentKeyInRecycleBin); // Verify the node's parent is now located at the root of the recycle bin (null) + }); + } // TODO: check that the descendants have also been removed from both structures - navigation and trash - // [Test] - // public async Task Structure_Updates_When_Deleting_From_Recycle_Bin() // TODO: Missing implementation - // { - // // Arrange - // Guid nodeToDelete = Child1.Key; - // Guid nodeInRecycleBin = Grandchild4.Key; - // - // // Move nodes to recycle bin - // await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); // Make sure we have an item already in the recycle bin to act as a sibling - // await ContentEditingService.MoveToRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); // Make sure the item is in the recycle bin - // NavigationService.TryGetSiblingsKeys(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); // TODO: call trashed items str. - // - // // Act - // await ContentEditingService.DeleteFromRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); - // NavigationService.TryGetSiblingsKeys(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); // TODO: call trashed items str. - // - // // Assert - // // Verify siblings count has decreased by one - // Assert.AreEqual(initialSiblingsKeys.Count() - 1, updatedSiblingsKeys.Count()); - // } + [Test] + public async Task Structure_Updates_When_Deleting_From_Recycle_Bin() + { + // Arrange + Guid nodeToDelete = Child1.Key; + Guid nodeInRecycleBin = Grandchild4.Key; + + // Move nodes to recycle bin + await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); // Make sure we have an item already in the recycle bin to act as a sibling + await ContentEditingService.MoveToRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); // Make sure the item is in the recycle bin + DocumentNavigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); + + // Act + await ContentEditingService.DeleteFromRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); + + // Assert + DocumentNavigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); + + // Verify siblings count has decreased by one + Assert.AreEqual(initialSiblingsKeys.Count() - 1, updatedSiblingsKeys.Count()); + } [Test] [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Grandchild 1 to Child 2 @@ -374,6 +376,7 @@ public async Task Structure_Updates_When_Copying_Content(Guid nodeToCopy, Guid? } [Test] + // TODO: Test that the descendants of the node have also been removed from both structures [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 @@ -383,68 +386,73 @@ public async Task Structure_Updates_When_Deleting_Content(Guid nodeToDelete) DocumentNavigationService.TryGetDescendantsKeys(nodeToDelete, out IEnumerable initialDescendantsKeys); // Act + // Deletes the item whether it is in the recycle bin or not var deleteAttempt = await ContentEditingService.DeleteAsync(nodeToDelete, Constants.Security.SuperUserKey); Guid deletedItemKey = deleteAttempt.Result.Key; // Assert var nodeExists = DocumentNavigationService.TryGetDescendantsKeys(deletedItemKey, out _); - //var nodeExistsInRecycleBin = NavigationService.TryGetDescendantsKeys(nodeToDelete, out _); // TODO: use recycle bin str. + var nodeExistsInRecycleBin = DocumentNavigationService.TryGetDescendantsKeysInBin(nodeToDelete, out _); Assert.Multiple(() => { Assert.AreEqual(nodeToDelete, deletedItemKey); Assert.IsFalse(nodeExists); - //Assert.IsFalse(nodeExistsInRecycleBin); + Assert.IsFalse(nodeExistsInRecycleBin); foreach (Guid descendant in initialDescendantsKeys) { var descendantExists = DocumentNavigationService.TryGetParentKey(descendant, out _); Assert.IsFalse(descendantExists); - // var descendantExistsInRecycleBin = NavigationService.TryGetParentKey(descendant, out _); // TODO: use recycle bin str. - // Assert.IsFalse(descendantExistsInRecycleBin); + var descendantExistsInRecycleBin = DocumentNavigationService.TryGetParentKeyInBin(descendant, out _); + Assert.IsFalse(descendantExistsInRecycleBin); } }); } - // [Test] - // public async Task Structure_Updates_When_Restoring_Content(Guid nodeToRestore, Guid? targetParentKey) // todo: test with target parent null - // { - // // Arrange - // Guid nodeInRecycleBin = GreatGrandchild1.Key; - // - // // Move nodes to recycle bin - // await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); // Make sure we have an item already in the recycle bin to act as a sibling - // await ContentEditingService.MoveToRecycleBinAsync(nodeToRestore, Constants.Security.SuperUserKey); // Make sure the item is in the recycle bin - // NavigationService.TryGetParentKey(nodeToRestore, out Guid? initialParentKey); - // NavigationService.TryGetSiblingsKeys(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); // TODO: call trashed items str. - // - // // Act - // var restoreAttempt = await ContentEditingService.RestoreAsync(nodeToRestore, targetParentKey, Constants.Security.SuperUserKey); - // Guid restoredItemKey = restoreAttempt.Result.Key; - // - // // Assert - // NavigationService.TryGetParentKey(restoredItemKey, out Guid? restoredItemParentKey); - // NavigationService.TryGetSiblingsKeys(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); // TODO: call trashed items str. - // - // Assert.Multiple(() => - // { - // // Verify siblings count has decreased by one - // Assert.AreEqual(initialSiblingsKeys.Count() - 1, updatedSiblingsKeys.Count()); - // - // if (targetParentKey is null) - // { - // Assert.IsNull(restoredItemParentKey); - // } - // else - // { - // Assert.IsNotNull(restoredItemParentKey); - // } - // - // Assert.AreNotEqual(initialParentKey, restoredItemParentKey); - // Assert.AreEqual(targetParentKey, restoredItemParentKey); - // }); - // } + [Test] + // TODO: test that descendants are also restored in the right place + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 1 to Child 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 2 to Grandchild 3 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", null)] // Child 3 to content root + public async Task Structure_Updates_When_Restoring_Content(Guid nodeToRestore, Guid? targetParentKey) + { + // Arrange + Guid nodeInRecycleBin = GreatGrandchild1.Key; + + // Move nodes to recycle bin + await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); // Make sure we have an item already in the recycle bin to act as a sibling + await ContentEditingService.MoveToRecycleBinAsync(nodeToRestore, Constants.Security.SuperUserKey); // Make sure the item is in the recycle bin + DocumentNavigationService.TryGetParentKeyInBin(nodeToRestore, out Guid? initialParentKey); + DocumentNavigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); + + // Act + var restoreAttempt = await ContentEditingService.RestoreAsync(nodeToRestore, targetParentKey, Constants.Security.SuperUserKey); + Guid restoredItemKey = restoreAttempt.Result.Key; + + // Assert + DocumentNavigationService.TryGetParentKey(restoredItemKey, out Guid? restoredItemParentKey); + DocumentNavigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); + + Assert.Multiple(() => + { + // Verify siblings count has decreased by one + Assert.AreEqual(initialSiblingsKeys.Count() - 1, updatedSiblingsKeys.Count()); + + if (targetParentKey is null) + { + Assert.IsNull(restoredItemParentKey); + } + else + { + Assert.IsNotNull(restoredItemParentKey); + Assert.AreNotEqual(initialParentKey, restoredItemParentKey); + } + + Assert.AreEqual(targetParentKey, restoredItemParentKey); + }); + } private ContentCreateModel CreateContentCreateModel(string name, Guid key, Guid? parentKey = null) => new() From bc8b1020fc25ca5e279a7a5043c5289d1c10075c Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 12 Aug 2024 15:00:52 +0200 Subject: [PATCH 64/96] Clean up IMediaNavigationService --- src/Umbraco.Core/Services/Navigation/IMediaNavigationService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Core/Services/Navigation/IMediaNavigationService.cs b/src/Umbraco.Core/Services/Navigation/IMediaNavigationService.cs index c8347baef9b8..c7cb057849fd 100644 --- a/src/Umbraco.Core/Services/Navigation/IMediaNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/IMediaNavigationService.cs @@ -2,5 +2,4 @@ namespace Umbraco.Cms.Core.Services.Navigation; public interface IMediaNavigationService : INavigationQueryService, INavigationManagementService { - bool Move(Guid nodeKey, Guid? targetParentKey = null); } From 995fb5b4f9a354ef8eb7062dfbcd0cdb646671cc Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 12 Aug 2024 15:02:22 +0200 Subject: [PATCH 65/96] Fix comments --- .../Services/Navigation/INavigationManagementService.cs | 2 +- src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs index 8669766c90cc..f6b0e31ba2a8 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs @@ -1,7 +1,7 @@ namespace Umbraco.Cms.Core.Services.Navigation; /// -/// Placeholder for sharing logic between the document, document recycle bin, media and media recycle bin navigation services +/// Placeholder for sharing logic between the document and media navigation services /// for managing the navigation structure. /// public interface INavigationManagementService : IRecycleBinNavigationManagementService diff --git a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs index 1cea878344cd..ca7f152b8716 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs @@ -1,7 +1,7 @@ namespace Umbraco.Cms.Core.Services.Navigation; /// -/// Placeholder for sharing logic between the document, document recycle bin, media and media recycle bin navigation services +/// Placeholder for sharing logic between the document and media navigation services /// for querying the navigation structure. /// public interface INavigationQueryService : IRecycleBinNavigationQueryService From 4fc0150f6e045d8518c229efa6fa8790ad055708 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 12 Aug 2024 15:03:58 +0200 Subject: [PATCH 66/96] Remove CustomTestSetup in integration tests as the structure is updated when content updates happen --- .../Umbraco.Core/Services/DocumentNavigationServiceTests.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs index 6b5caec46edf..07bc009b38e3 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs @@ -102,11 +102,6 @@ public async Task Setup() Grandchild4 = grandchild4CreateAttempt.Result.Content!; } - // protected override void CustomTestSetup(IUmbracoBuilder builder) - // { - // builder.Services.AddHostedService(); - // } - [Test] // TODO: Test that you can rebuild bin structure as well public async Task Structure_Can_Rebuild() From 34df56f506e2e8304db233a2888b4616c7d71c5e Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 23 Aug 2024 13:42:32 +0200 Subject: [PATCH 67/96] Adding and fixing comments --- .../INavigationManagementService.cs | 41 +++++++++++++++++++ .../IRecycleBinNavigationManagementService.cs | 30 ++++++++++++++ .../Factories/NavigationFactory.cs | 10 ++--- 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs index f6b0e31ba2a8..c22c9f4b9500 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs @@ -6,11 +6,52 @@ namespace Umbraco.Cms.Core.Services.Navigation; /// public interface INavigationManagementService : IRecycleBinNavigationManagementService { + /// + /// Rebuilds the entire navigation structure by refreshing the navigation tree based + /// on the current state of the underlying repository. + /// Task RebuildAsync(); + /// + /// Removes a node from the main navigation structure and moves it, along with + /// its descendants, to the root of the recycle bin structure. + /// + /// The unique identifier of the node to remove. + /// + /// true if the node and its descendants were successfully removed from the + /// main navigation structure and added to the recycle bin; otherwise, false. + /// bool Remove(Guid key); + /// + /// Adds a new node to the main navigation structure. If a parent key is provided, + /// the new node is added as a child of the specified parent. If no parent key is + /// provided, the new node is added at the root level. + /// + /// The unique identifier of the new node to add. + /// + /// The unique identifier of the parent node. If null, the new node will be added to + /// the root level. + /// + /// + /// true if the node was successfully added to the main navigation structure; + /// otherwise, false. + /// bool Add(Guid key, Guid? parentKey = null); + /// + /// Moves an existing node to a new parent in the main navigation structure. If a + /// target parent key is provided, the node is moved under the specified parent. + /// If no target parent key is provided, the node is moved to the root level. + /// + /// The unique identifier of the node to move. + /// + /// The unique identifier of the new parent node. If null, the node will be moved to + /// the root level. + /// + /// + /// true if the node and its descendants were successfully moved to the new parent + /// in the main navigation structure; otherwise, false. + /// bool Move(Guid key, Guid? targetParentKey = null); } diff --git a/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationManagementService.cs index eb41eea6c342..07c415136291 100644 --- a/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationManagementService.cs +++ b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationManagementService.cs @@ -1,10 +1,40 @@ namespace Umbraco.Cms.Core.Services.Navigation; +/// +/// Placeholder for sharing logic between the document and media navigation services +/// for managing the recycle bin navigation structure. +/// public interface IRecycleBinNavigationManagementService { + /// + /// Rebuilds the recycle bin navigation structure by fetching the latest trashed nodes + /// from the underlying repository. + /// Task RebuildBinAsync(); + /// + /// Permanently removes a node and all of its descendants from the recycle bin navigation structure. + /// + /// The unique identifier of the node to remove. + /// + /// true if the node and its descendants were successfully removed from the recycle bin; + /// otherwise, false. + /// bool RemoveFromBin(Guid key); + /// + /// Restores a node and all of its descendants from the recycle bin navigation structure and moves them back + /// to the main navigation structure. The node can be restored to a specified target parent or to the root + /// level if no parent is specified. + /// + /// The unique identifier of the node to restore from the recycle bin navigation structure. + /// + /// The unique identifier of the target parent node in the main navigation structure to which the node + /// should be restored. If null, the node will be restored to the root level. + /// + /// + /// true if the node and its descendants were successfully restored to the main navigation structure; + /// otherwise, false. + /// bool RestoreFromBin(Guid key, Guid? targetParentKey = null); } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs index e342c3550dbb..e510ecfe1216 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs @@ -6,11 +6,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories; internal static class NavigationFactory { -/// -/// Builds a dictionary of NavigationNode objects from a given dataset. -/// -/// The dataset of objects used to build the navigation nodes dictionary. -/// A dictionary of objects with key corresponding to their unique guid. + /// + /// Builds a dictionary of NavigationNode objects from a given dataset. + /// + /// The dataset of objects used to build the navigation nodes dictionary. + /// A dictionary of objects with key corresponding to their unique guid. public static ConcurrentDictionary BuildNavigationDictionary(IEnumerable dataset) { var nodesStructure = new ConcurrentDictionary(); From 312d0bd239fa43bbb28043f723030069994e147b Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 23 Aug 2024 13:50:41 +0200 Subject: [PATCH 68/96] Making RebuildBinAsync abstract as well --- .../Services/Navigation/ContentNavigationServiceBase.cs | 8 ++++++++ .../Services/Navigation/DocumentNavigationService.cs | 2 +- .../Services/Navigation/MediaNavigationService.cs | 2 +- .../Services/ContentNavigationServiceBaseTests.cs | 3 +++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index 1bc0f023ab5e..b0d3f2552160 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -18,8 +18,16 @@ protected ContentNavigationServiceBase(ICoreScopeProvider coreScopeProvider, INa _navigationRepository = navigationRepository; } + /// + /// Rebuilds the entire main navigation structure. Implementations should define how the structure is rebuilt. + /// public abstract Task RebuildAsync(); + /// + /// Rebuilds the recycle bin navigation structure. Implementations should define how the bin structure is rebuilt. + /// + public abstract Task RebuildBinAsync(); + public bool TryGetParentKey(Guid childKey, out Guid? parentKey) => TryGetParentKeyFromStructure(_navigationStructure, childKey, out parentKey); diff --git a/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs index 566da0e6463c..39103333423a 100644 --- a/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs @@ -13,6 +13,6 @@ public DocumentNavigationService(ICoreScopeProvider coreScopeProvider, INavigati public override async Task RebuildAsync() => await HandleRebuildAsync(Constants.Locks.ContentTree, Constants.ObjectTypes.Document, false); - public async Task RebuildBinAsync() + public override async Task RebuildBinAsync() => await HandleRebuildAsync(Constants.Locks.ContentTree, Constants.ObjectTypes.Document, true); } diff --git a/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs b/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs index a85d2a16c159..dbe8ed54f2ee 100644 --- a/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs @@ -13,6 +13,6 @@ public MediaNavigationService(ICoreScopeProvider coreScopeProvider, INavigationR public override async Task RebuildAsync() => await HandleRebuildAsync(Constants.Locks.MediaTree, Constants.ObjectTypes.Media, false); - public async Task RebuildBinAsync() + public override async Task RebuildBinAsync() => await HandleRebuildAsync(Constants.Locks.MediaTree, Constants.ObjectTypes.Media, true); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs index 4d1457eea31f..32d30a0c9021 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs @@ -990,4 +990,7 @@ public TestContentNavigationService(ICoreScopeProvider coreScopeProvider, INavig // Not needed for testing here public override Task RebuildAsync() => Task.CompletedTask; + + // Not needed for testing here + public override Task RebuildBinAsync() => Task.CompletedTask; } From cd74f1d5c41a8ac3c82cc43ebed32f8536637f7c Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 23 Aug 2024 14:49:08 +0200 Subject: [PATCH 69/96] Adding DocumentNavigationServiceTestsBase --- .../DocumentNavigationServiceTestsBase.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs new file mode 100644 index 000000000000..1dcf6b6a1f16 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs @@ -0,0 +1,51 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public abstract class DocumentNavigationServiceTestsBase : UmbracoIntegrationTest +{ + protected IContentTypeService ContentTypeService => GetRequiredService(); + + // Testing with IContentEditingService as it calls IContentService underneath + protected IContentEditingService ContentEditingService => GetRequiredService(); + + protected IDocumentNavigationService DocumentNavigationService => GetRequiredService(); + + protected ContentType ContentType { get; set; } + + protected IContent Root { get; set; } + + protected IContent Child1 { get; set; } + + protected IContent Grandchild1 { get; set; } + + protected IContent Grandchild2 { get; set; } + + protected IContent Child2 { get; set; } + + protected IContent Grandchild3 { get; set; } + + protected IContent GreatGrandchild1 { get; set; } + + protected IContent Child3 { get; set; } + + protected IContent Grandchild4 { get; set; } + + protected ContentCreateModel CreateContentCreateModel(string name, Guid key, Guid? parentKey = null) + => new() + { + ContentTypeKey = ContentType.Key, + ParentKey = parentKey ?? Constants.System.RootKey, + InvariantName = name, + Key = key, + }; +} From 99cbd103f54a8ba099c41c6ba353bd1695c3e057 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 23 Aug 2024 14:53:48 +0200 Subject: [PATCH 70/96] Splitting DocumentNavigationServiceTests into partial test classes --- .../DocumentNavigationServiceTests.Copy.cs | 46 +++++++++++++++ .../DocumentNavigationServiceTests.Create.cs | 39 ++++++++++++ .../DocumentNavigationServiceTests.Delete.cs | 43 ++++++++++++++ ...gationServiceTests.DeleteFromRecycleBin.cs | 30 ++++++++++ .../DocumentNavigationServiceTests.Move.cs | 39 ++++++++++++ ...NavigationServiceTests.MoveToRecycleBin.cs | 32 ++++++++++ .../DocumentNavigationServiceTests.Rebuild.cs | 59 +++++++++++++++++++ .../DocumentNavigationServiceTests.Restore.cs | 50 ++++++++++++++++ .../DocumentNavigationServiceTests.Update.cs | 54 +++++++++++++++++ .../Umbraco.Tests.Integration.csproj | 27 +++++++++ 10 files changed, 419 insertions(+) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.DeleteFromRecycleBin.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Update.cs diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs new file mode 100644 index 000000000000..374263d41afe --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs @@ -0,0 +1,46 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +// TODO: test that it is added to its new parent - check parent's children +// TODO: test that it has the same amount of descendants - depending on value of includeDescendants param +// TODO: test that the number of target parent descendants updates when copying node with descendants +// TODO: test that copied node descendants have different keys than source node descendants +public partial class DocumentNavigationServiceTests +{ + [Test] + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2 to itself + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", null)] // Child 2 to content root + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 3 to Child 1 + public async Task Structure_Updates_When_Copying_Content(Guid nodeToCopy, Guid? targetParentKey) + { + // Arrange + DocumentNavigationService.TryGetParentKey(nodeToCopy, out Guid? sourceParentKey); + + // Act + var copyAttempt = await ContentEditingService.CopyAsync(nodeToCopy, targetParentKey, false, false, Constants.Security.SuperUserKey); + Guid copiedItemKey = copyAttempt.Result.Key; + + // Assert + Assert.AreNotEqual(nodeToCopy, copiedItemKey); + + DocumentNavigationService.TryGetParentKey(copiedItemKey, out Guid? copiedItemParentKey); + + Assert.Multiple(() => + { + if (targetParentKey is null) + { + // Verify the copied node's parent is null (it's been copied to content root) + Assert.IsNull(copiedItemParentKey); + } + else + { + Assert.IsNotNull(copiedItemParentKey); + } + + Assert.AreEqual(targetParentKey, copiedItemParentKey); + Assert.AreNotEqual(sourceParentKey, copiedItemParentKey); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs new file mode 100644 index 000000000000..d1505b589375 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs @@ -0,0 +1,39 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class DocumentNavigationServiceTests +{ + [Test] + public async Task Structure_Updates_When_Creating_Content() + { + // Arrange + DocumentNavigationService.TryGetSiblingsKeys(Root.Key, out IEnumerable initialSiblingsKeys); + var initialRootNodeSiblingsCount = initialSiblingsKeys.Count(); + + var createModel = new ContentCreateModel + { + ContentTypeKey = ContentType.Key, + ParentKey = Constants.System.RootKey, // Create node at content root + InvariantName = "Root 2", + }; + + // Act + var createAttempt = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Guid createdItemKey = createAttempt.Result.Content!.Key; + + // Verify that the structure has updated by checking the siblings list of the Root once again + DocumentNavigationService.TryGetSiblingsKeys(Root.Key, out IEnumerable updatedSiblingsKeys); + List siblingsList = updatedSiblingsKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.IsNotEmpty(siblingsList); + Assert.AreEqual(initialRootNodeSiblingsCount + 1, siblingsList.Count); + Assert.AreEqual(createdItemKey, siblingsList.First()); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs new file mode 100644 index 000000000000..a056b9788504 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs @@ -0,0 +1,43 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +// TODO: Test that the descendants of the node have also been removed from both structures +public partial class DocumentNavigationServiceTests +{ + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + public async Task Structure_Updates_When_Deleting_Content(Guid nodeToDelete) + { + // Arrange + DocumentNavigationService.TryGetDescendantsKeys(nodeToDelete, out IEnumerable initialDescendantsKeys); + + // Act + // Deletes the item whether it is in the recycle bin or not + var deleteAttempt = await ContentEditingService.DeleteAsync(nodeToDelete, Constants.Security.SuperUserKey); + Guid deletedItemKey = deleteAttempt.Result.Key; + + // Assert + var nodeExists = DocumentNavigationService.TryGetDescendantsKeys(deletedItemKey, out _); + var nodeExistsInRecycleBin = DocumentNavigationService.TryGetDescendantsKeysInBin(nodeToDelete, out _); + + Assert.Multiple(() => + { + Assert.AreEqual(nodeToDelete, deletedItemKey); + Assert.IsFalse(nodeExists); + Assert.IsFalse(nodeExistsInRecycleBin); + + foreach (Guid descendant in initialDescendantsKeys) + { + var descendantExists = DocumentNavigationService.TryGetParentKey(descendant, out _); + Assert.IsFalse(descendantExists); + + var descendantExistsInRecycleBin = DocumentNavigationService.TryGetParentKeyInBin(descendant, out _); + Assert.IsFalse(descendantExistsInRecycleBin); + } + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.DeleteFromRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.DeleteFromRecycleBin.cs new file mode 100644 index 000000000000..e35f2f4ad27e --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.DeleteFromRecycleBin.cs @@ -0,0 +1,30 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +// TODO: check that the descendants have also been removed from both structures - navigation and trash +public partial class DocumentNavigationServiceTests +{ + [Test] + public async Task Structure_Updates_When_Deleting_From_Recycle_Bin() + { + // Arrange + Guid nodeToDelete = Child1.Key; + Guid nodeInRecycleBin = Grandchild4.Key; + + // Move nodes to recycle bin + await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); // Make sure we have an item already in the recycle bin to act as a sibling + await ContentEditingService.MoveToRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); // Make sure the item is in the recycle bin + DocumentNavigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); + + // Act + await ContentEditingService.DeleteFromRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); + + // Assert + DocumentNavigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); + + // Verify siblings count has decreased by one + Assert.AreEqual(initialSiblingsKeys.Count() - 1, updatedSiblingsKeys.Count()); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs new file mode 100644 index 000000000000..f4a8e57e1b4e --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs @@ -0,0 +1,39 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class DocumentNavigationServiceTests +{ + [Test] + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Grandchild 1 to Child 2 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", null)] // Child 3 to content root + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 2 to Child 1 + public async Task Structure_Updates_When_Moving_Content(Guid nodeToMove, Guid? targetParentKey) + { + // Arrange + DocumentNavigationService.TryGetParentKey(nodeToMove, out Guid? originalParentKey); + + // Act + var moveAttempt = await ContentEditingService.MoveAsync(nodeToMove, targetParentKey, Constants.Security.SuperUserKey); + + // Verify the node's new parent is updated + DocumentNavigationService.TryGetParentKey(moveAttempt.Result!.Key, out Guid? updatedParentKey); + + // Assert + Assert.Multiple(() => + { + if (targetParentKey is null) + { + Assert.IsNull(updatedParentKey); + } + else + { + Assert.IsNotNull(updatedParentKey); + } + + Assert.AreNotEqual(originalParentKey, updatedParentKey); + Assert.AreEqual(targetParentKey, updatedParentKey); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs new file mode 100644 index 000000000000..bbab376aa1ec --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs @@ -0,0 +1,32 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +// TODO: also check that initial siblings count's decreased +// TODO: and that descendants are still the same (i.e. they've also been moved to recycle bin) +public partial class DocumentNavigationServiceTests +{ + [Test] + public async Task Structure_Updates_When_Moving_Content_To_Recycle_Bin() + { + // Arrange + Guid nodeToMoveToRecycleBin = Child3.Key; + DocumentNavigationService.TryGetParentKey(nodeToMoveToRecycleBin, out Guid? originalParentKey); + + // Act + await ContentEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey); + + // Assert + var nodeExists = DocumentNavigationService.TryGetParentKey(nodeToMoveToRecycleBin, out _); // Verify that the item is no longer in the document structure + var nodeExistsInRecycleBin = DocumentNavigationService.TryGetParentKeyInBin(nodeToMoveToRecycleBin, out Guid? updatedParentKeyInRecycleBin); + + Assert.Multiple(() => + { + Assert.IsFalse(nodeExists); + Assert.IsTrue(nodeExistsInRecycleBin); + Assert.AreNotEqual(originalParentKey, updatedParentKeyInRecycleBin); + Assert.IsNull(updatedParentKeyInRecycleBin); // Verify the node's parent is now located at the root of the recycle bin (null) + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs new file mode 100644 index 000000000000..82688c167422 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs @@ -0,0 +1,59 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.Navigation; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class DocumentNavigationServiceTests +{ + [Test] + public async Task Structure_Can_Rebuild() + { + // Arrange + Guid nodeKey = Root.Key; + + // Capture original built state of DocumentNavigationService + DocumentNavigationService.TryGetParentKey(nodeKey, out Guid? originalParentKey); + DocumentNavigationService.TryGetChildrenKeys(nodeKey, out IEnumerable originalChildrenKeys); + DocumentNavigationService.TryGetDescendantsKeys(nodeKey, out IEnumerable originalDescendantsKeys); + DocumentNavigationService.TryGetAncestorsKeys(nodeKey, out IEnumerable originalAncestorsKeys); + DocumentNavigationService.TryGetSiblingsKeys(nodeKey, out IEnumerable originalSiblingsKeys); + + // Im-memory navigation structure is empty here + var newDocumentNavigationService = new DocumentNavigationService(GetRequiredService(), GetRequiredService()); + var initialNodeExists = newDocumentNavigationService.TryGetParentKey(nodeKey, out _); + + // Act + await newDocumentNavigationService.RebuildAsync(); + + // Capture rebuilt state + var nodeExists = newDocumentNavigationService.TryGetParentKey(nodeKey, out Guid? parentKeyFromRebuild); + newDocumentNavigationService.TryGetChildrenKeys(nodeKey, out IEnumerable childrenKeysFromRebuild); + newDocumentNavigationService.TryGetDescendantsKeys(nodeKey, out IEnumerable descendantsKeysFromRebuild); + newDocumentNavigationService.TryGetAncestorsKeys(nodeKey, out IEnumerable ancestorsKeysFromRebuild); + newDocumentNavigationService.TryGetSiblingsKeys(nodeKey, out IEnumerable siblingsKeysFromRebuild); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(initialNodeExists); + + // Verify that the item is present in the navigation structure after a rebuild + Assert.IsTrue(nodeExists); + + // Verify that we have the same items as in the original built state of DocumentNavigationService + Assert.AreEqual(originalParentKey, parentKeyFromRebuild); + CollectionAssert.AreEquivalent(originalChildrenKeys, childrenKeysFromRebuild); + CollectionAssert.AreEquivalent(originalDescendantsKeys, descendantsKeysFromRebuild); + CollectionAssert.AreEquivalent(originalAncestorsKeys, ancestorsKeysFromRebuild); + CollectionAssert.AreEquivalent(originalSiblingsKeys, siblingsKeysFromRebuild); + }); + } + + [Test] + // TODO: Test that you can rebuild bin structure as well + public async Task Bin_Structure_Can_Rebuild() + { + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs new file mode 100644 index 000000000000..a765ef24ca4f --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs @@ -0,0 +1,50 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +// TODO: test that descendants are also restored in the right place +public partial class DocumentNavigationServiceTests +{ + [Test] + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 1 to Child 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 2 to Grandchild 3 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", null)] // Child 3 to content root + public async Task Structure_Updates_When_Restoring_Content(Guid nodeToRestore, Guid? targetParentKey) + { + // Arrange + Guid nodeInRecycleBin = GreatGrandchild1.Key; + + // Move nodes to recycle bin + await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); // Make sure we have an item already in the recycle bin to act as a sibling + await ContentEditingService.MoveToRecycleBinAsync(nodeToRestore, Constants.Security.SuperUserKey); // Make sure the item is in the recycle bin + DocumentNavigationService.TryGetParentKeyInBin(nodeToRestore, out Guid? initialParentKey); + DocumentNavigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); + + // Act + var restoreAttempt = await ContentEditingService.RestoreAsync(nodeToRestore, targetParentKey, Constants.Security.SuperUserKey); + Guid restoredItemKey = restoreAttempt.Result.Key; + + // Assert + DocumentNavigationService.TryGetParentKey(restoredItemKey, out Guid? restoredItemParentKey); + DocumentNavigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); + + Assert.Multiple(() => + { + // Verify siblings count has decreased by one + Assert.AreEqual(initialSiblingsKeys.Count() - 1, updatedSiblingsKeys.Count()); + + if (targetParentKey is null) + { + Assert.IsNull(restoredItemParentKey); + } + else + { + Assert.IsNotNull(restoredItemParentKey); + Assert.AreNotEqual(initialParentKey, restoredItemParentKey); + } + + Assert.AreEqual(targetParentKey, restoredItemParentKey); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Update.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Update.cs new file mode 100644 index 000000000000..591bd08de6ee --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Update.cs @@ -0,0 +1,54 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class DocumentNavigationServiceTests +{ + [Test] + public async Task Structure_Does_Not_Update_When_Updating_Content() + { + // Arrange + Guid nodeToUpdate = Root.Key; + + // Capture initial state + DocumentNavigationService.TryGetParentKey(nodeToUpdate, out Guid? initialParentKey); + DocumentNavigationService.TryGetChildrenKeys(nodeToUpdate, out IEnumerable initialChildrenKeys); + DocumentNavigationService.TryGetDescendantsKeys(nodeToUpdate, out IEnumerable initialDescendantsKeys); + DocumentNavigationService.TryGetAncestorsKeys(nodeToUpdate, out IEnumerable initialAncestorsKeys); + DocumentNavigationService.TryGetSiblingsKeys(nodeToUpdate, out IEnumerable initialSiblingsKeys); + + var updateModel = new ContentUpdateModel + { + InvariantName = "Updated Root", + }; + + // Act + var updateAttempt = await ContentEditingService.UpdateAsync(nodeToUpdate, updateModel, Constants.Security.SuperUserKey); + Guid updatedItemKey = updateAttempt.Result.Content!.Key; + + // Capture updated state + var nodeExists = DocumentNavigationService.TryGetParentKey(updatedItemKey, out Guid? updatedParentKey); + DocumentNavigationService.TryGetChildrenKeys(updatedItemKey, out IEnumerable childrenKeysAfterUpdate); + DocumentNavigationService.TryGetDescendantsKeys(updatedItemKey, out IEnumerable descendantsKeysAfterUpdate); + DocumentNavigationService.TryGetAncestorsKeys(updatedItemKey, out IEnumerable ancestorsKeysAfterUpdate); + DocumentNavigationService.TryGetSiblingsKeys(updatedItemKey, out IEnumerable siblingsKeysAfterUpdate); + + // Assert + Assert.Multiple(() => + { + // Verify that the item is still present in the navigation structure + Assert.IsTrue(nodeExists); + + Assert.AreEqual(nodeToUpdate, updatedItemKey); + + // Verify that nothing's changed + Assert.AreEqual(initialParentKey, updatedParentKey); + CollectionAssert.AreEquivalent(initialChildrenKeys, childrenKeysAfterUpdate); + CollectionAssert.AreEquivalent(initialDescendantsKeys, descendantsKeysAfterUpdate); + CollectionAssert.AreEquivalent(initialAncestorsKeys, ancestorsKeysAfterUpdate); + CollectionAssert.AreEquivalent(initialSiblingsKeys, siblingsKeysAfterUpdate); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index b7ab21b0628d..6923de4e8638 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -154,6 +154,33 @@ MediaTypeEditingServiceTests.cs + + DocumentNavigationServiceTests.cs + + + DocumentNavigationServiceTests.cs + + + DocumentNavigationServiceTests.cs + + + DocumentNavigationServiceTests.cs + + + DocumentNavigationServiceTests.cs + + + DocumentNavigationServiceTests.cs + + + DocumentNavigationServiceTests.cs + + + DocumentNavigationServiceTests.cs + + + DocumentNavigationServiceTests.cs + From 33678a8071e81d64d21fca606fa85dc48245190e Mon Sep 17 00:00:00 2001 From: Elitsa Date: Fri, 23 Aug 2024 14:56:23 +0200 Subject: [PATCH 71/96] Cleaning up DocumentNavigationServiceTests since tests have been moved to specific partial classes --- .../DocumentNavigationServiceTests.cs | 366 +----------------- 1 file changed, 1 insertion(+), 365 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs index 07bc009b38e3..d142b203f3ed 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs @@ -2,47 +2,13 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; -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.Tests.Common.Builders; -using Umbraco.Cms.Tests.Common.Testing; -using Umbraco.Cms.Tests.Integration.Testing; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; -[TestFixture] -[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] -public class DocumentNavigationServiceTests : UmbracoIntegrationTest +public partial class DocumentNavigationServiceTests : DocumentNavigationServiceTestsBase { - private IContentTypeService ContentTypeService => GetRequiredService(); - - // Testing with IContentEditingService as it calls IContentService underneath - private IContentEditingService ContentEditingService => GetRequiredService(); - - private IDocumentNavigationService DocumentNavigationService => GetRequiredService(); - - private ContentType ContentType { get; set; } - - private IContent Root { get; set; } - - private IContent Child1 { get; set; } - - private IContent Grandchild1 { get; set; } - - private IContent Grandchild2 { get; set; } - - private IContent Child2 { get; set; } - - private IContent Grandchild3 { get; set; } - - private IContent GreatGrandchild1 { get; set; } - - private IContent Child3 { get; set; } - - private IContent Grandchild4 { get; set; } - [SetUp] public async Task Setup() { @@ -102,51 +68,6 @@ public async Task Setup() Grandchild4 = grandchild4CreateAttempt.Result.Content!; } - [Test] - // TODO: Test that you can rebuild bin structure as well - public async Task Structure_Can_Rebuild() - { - // Arrange - Guid nodeKey = Root.Key; - - // Capture original built state of DocumentNavigationService - DocumentNavigationService.TryGetParentKey(nodeKey, out Guid? originalParentKey); - DocumentNavigationService.TryGetChildrenKeys(nodeKey, out IEnumerable originalChildrenKeys); - DocumentNavigationService.TryGetDescendantsKeys(nodeKey, out IEnumerable originalDescendantsKeys); - DocumentNavigationService.TryGetAncestorsKeys(nodeKey, out IEnumerable originalAncestorsKeys); - DocumentNavigationService.TryGetSiblingsKeys(nodeKey, out IEnumerable originalSiblingsKeys); - - // Im-memory navigation structure is empty here - var newDocumentNavigationService = new DocumentNavigationService(GetRequiredService(), GetRequiredService()); - var initialNodeExists = newDocumentNavigationService.TryGetParentKey(nodeKey, out _); - - // Act - await newDocumentNavigationService.RebuildAsync(); - - // Capture rebuilt state - var nodeExists = newDocumentNavigationService.TryGetParentKey(nodeKey, out Guid? parentKeyFromRebuild); - newDocumentNavigationService.TryGetChildrenKeys(nodeKey, out IEnumerable childrenKeysFromRebuild); - newDocumentNavigationService.TryGetDescendantsKeys(nodeKey, out IEnumerable descendantsKeysFromRebuild); - newDocumentNavigationService.TryGetAncestorsKeys(nodeKey, out IEnumerable ancestorsKeysFromRebuild); - newDocumentNavigationService.TryGetSiblingsKeys(nodeKey, out IEnumerable siblingsKeysFromRebuild); - - // Assert - Assert.Multiple(() => - { - Assert.IsFalse(initialNodeExists); - - // Verify that the item is present in the navigation structure after a rebuild - Assert.IsTrue(nodeExists); - - // Verify that we have the same items as in the original built state of DocumentNavigationService - Assert.AreEqual(originalParentKey, parentKeyFromRebuild); - CollectionAssert.AreEquivalent(originalChildrenKeys, childrenKeysFromRebuild); - CollectionAssert.AreEquivalent(originalDescendantsKeys, descendantsKeysFromRebuild); - CollectionAssert.AreEquivalent(originalAncestorsKeys, ancestorsKeysFromRebuild); - CollectionAssert.AreEquivalent(originalSiblingsKeys, siblingsKeysFromRebuild); - }); - } - [Test] public async Task Structure_Does_Not_Update_When_Scope_Is_Not_Completed() { @@ -172,289 +93,4 @@ public async Task Structure_Does_Not_Update_When_Scope_Is_Not_Completed() // Assert Assert.IsFalse(nodeExists); } - - [Test] - public async Task Structure_Updates_When_Creating_Content() - { - // Arrange - DocumentNavigationService.TryGetSiblingsKeys(Root.Key, out IEnumerable initialSiblingsKeys); - var initialRootNodeSiblingsCount = initialSiblingsKeys.Count(); - - var createModel = new ContentCreateModel - { - ContentTypeKey = ContentType.Key, - ParentKey = Constants.System.RootKey, // Create node at content root - InvariantName = "Root 2", - }; - - // Act - var createAttempt = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); - Guid createdItemKey = createAttempt.Result.Content!.Key; - - // Verify that the structure has updated by checking the siblings list of the Root once again - DocumentNavigationService.TryGetSiblingsKeys(Root.Key, out IEnumerable updatedSiblingsKeys); - List siblingsList = updatedSiblingsKeys.ToList(); - - // Assert - Assert.Multiple(() => - { - Assert.IsNotEmpty(siblingsList); - Assert.AreEqual(initialRootNodeSiblingsCount + 1, siblingsList.Count); - Assert.AreEqual(createdItemKey, siblingsList.First()); - }); - } - - [Test] - public async Task Structure_Does_Not_Update_When_Updating_Content() - { - // Arrange - Guid nodeToUpdate = Root.Key; - - // Capture initial state - DocumentNavigationService.TryGetParentKey(nodeToUpdate, out Guid? initialParentKey); - DocumentNavigationService.TryGetChildrenKeys(nodeToUpdate, out IEnumerable initialChildrenKeys); - DocumentNavigationService.TryGetDescendantsKeys(nodeToUpdate, out IEnumerable initialDescendantsKeys); - DocumentNavigationService.TryGetAncestorsKeys(nodeToUpdate, out IEnumerable initialAncestorsKeys); - DocumentNavigationService.TryGetSiblingsKeys(nodeToUpdate, out IEnumerable initialSiblingsKeys); - - var updateModel = new ContentUpdateModel - { - InvariantName = "Updated Root", - }; - - // Act - var updateAttempt = await ContentEditingService.UpdateAsync(nodeToUpdate, updateModel, Constants.Security.SuperUserKey); - Guid updatedItemKey = updateAttempt.Result.Content!.Key; - - // Capture updated state - var nodeExists = DocumentNavigationService.TryGetParentKey(updatedItemKey, out Guid? updatedParentKey); - DocumentNavigationService.TryGetChildrenKeys(updatedItemKey, out IEnumerable childrenKeysAfterUpdate); - DocumentNavigationService.TryGetDescendantsKeys(updatedItemKey, out IEnumerable descendantsKeysAfterUpdate); - DocumentNavigationService.TryGetAncestorsKeys(updatedItemKey, out IEnumerable ancestorsKeysAfterUpdate); - DocumentNavigationService.TryGetSiblingsKeys(updatedItemKey, out IEnumerable siblingsKeysAfterUpdate); - - // Assert - Assert.Multiple(() => - { - // Verify that the item is still present in the navigation structure - Assert.IsTrue(nodeExists); - - Assert.AreEqual(nodeToUpdate, updatedItemKey); - - // Verify that nothing's changed - Assert.AreEqual(initialParentKey, updatedParentKey); - CollectionAssert.AreEquivalent(initialChildrenKeys, childrenKeysAfterUpdate); - CollectionAssert.AreEquivalent(initialDescendantsKeys, descendantsKeysAfterUpdate); - CollectionAssert.AreEquivalent(initialAncestorsKeys, ancestorsKeysAfterUpdate); - CollectionAssert.AreEquivalent(initialSiblingsKeys, siblingsKeysAfterUpdate); - }); - } - - // TODO: test that item exists in recycle bin str. and it is removed from content str; - // TODO: also check that initial siblings count's decreased, - // TODO: and that descendants are still the same (i.e. they've also been moved to recycle bin) - [Test] - public async Task Structure_Updates_When_Moving_Content_To_Recycle_Bin() - { - // Arrange - Guid nodeToMoveToRecycleBin = Child3.Key; - DocumentNavigationService.TryGetParentKey(nodeToMoveToRecycleBin, out Guid? originalParentKey); - - // Act - await ContentEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey); - - // Assert - var nodeExists = DocumentNavigationService.TryGetParentKey(nodeToMoveToRecycleBin, out _); // Verify that the item is no longer in the document structure - var nodeExistsInRecycleBin = DocumentNavigationService.TryGetParentKeyInBin(nodeToMoveToRecycleBin, out Guid? updatedParentKeyInRecycleBin); - - Assert.Multiple(() => - { - Assert.IsFalse(nodeExists); - Assert.IsTrue(nodeExistsInRecycleBin); - Assert.AreNotEqual(originalParentKey, updatedParentKeyInRecycleBin); - Assert.IsNull(updatedParentKeyInRecycleBin); // Verify the node's parent is now located at the root of the recycle bin (null) - }); - } - - // TODO: check that the descendants have also been removed from both structures - navigation and trash - [Test] - public async Task Structure_Updates_When_Deleting_From_Recycle_Bin() - { - // Arrange - Guid nodeToDelete = Child1.Key; - Guid nodeInRecycleBin = Grandchild4.Key; - - // Move nodes to recycle bin - await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); // Make sure we have an item already in the recycle bin to act as a sibling - await ContentEditingService.MoveToRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); // Make sure the item is in the recycle bin - DocumentNavigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); - - // Act - await ContentEditingService.DeleteFromRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); - - // Assert - DocumentNavigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); - - // Verify siblings count has decreased by one - Assert.AreEqual(initialSiblingsKeys.Count() - 1, updatedSiblingsKeys.Count()); - } - - [Test] - [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Grandchild 1 to Child 2 - [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", null)] // Child 3 to content root - [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 2 to Child 1 - public async Task Structure_Updates_When_Moving_Content(Guid nodeToMove, Guid? targetParentKey) - { - // Arrange - DocumentNavigationService.TryGetParentKey(nodeToMove, out Guid? originalParentKey); - - // Act - var moveAttempt = await ContentEditingService.MoveAsync(nodeToMove, targetParentKey, Constants.Security.SuperUserKey); - - // Verify the node's new parent is updated - DocumentNavigationService.TryGetParentKey(moveAttempt.Result!.Key, out Guid? updatedParentKey); - - // Assert - Assert.Multiple(() => - { - if (targetParentKey is null) - { - Assert.IsNull(updatedParentKey); - } - else - { - Assert.IsNotNull(updatedParentKey); - } - - Assert.AreNotEqual(originalParentKey, updatedParentKey); - Assert.AreEqual(targetParentKey, updatedParentKey); - }); - } - - // TODO: test that it is added to its new parent - check parent's children - // TODO: test that it has the same amount of descendants - depending on value of includeDescendants param - // TODO: test that the number of target parent descendants updates when copying node with descendants - // TODO: test that copied node descendants have different keys than source node descendants - [Test] - [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2 to itself - [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", null)] // Child 2 to content root - [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 3 to Child 1 - public async Task Structure_Updates_When_Copying_Content(Guid nodeToCopy, Guid? targetParentKey) - { - // Arrange - DocumentNavigationService.TryGetParentKey(nodeToCopy, out Guid? sourceParentKey); - - // Act - var copyAttempt = await ContentEditingService.CopyAsync(nodeToCopy, targetParentKey, false, false, Constants.Security.SuperUserKey); - Guid copiedItemKey = copyAttempt.Result.Key; - - // Assert - Assert.AreNotEqual(nodeToCopy, copiedItemKey); - - DocumentNavigationService.TryGetParentKey(copiedItemKey, out Guid? copiedItemParentKey); - - Assert.Multiple(() => - { - if (targetParentKey is null) - { - // Verify the copied node's parent is null (it's been copied to content root) - Assert.IsNull(copiedItemParentKey); - } - else - { - Assert.IsNotNull(copiedItemParentKey); - } - - Assert.AreEqual(targetParentKey, copiedItemParentKey); - Assert.AreNotEqual(sourceParentKey, copiedItemParentKey); - }); - } - - [Test] - // TODO: Test that the descendants of the node have also been removed from both structures - [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root - [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 - [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 - public async Task Structure_Updates_When_Deleting_Content(Guid nodeToDelete) - { - // Arrange - DocumentNavigationService.TryGetDescendantsKeys(nodeToDelete, out IEnumerable initialDescendantsKeys); - - // Act - // Deletes the item whether it is in the recycle bin or not - var deleteAttempt = await ContentEditingService.DeleteAsync(nodeToDelete, Constants.Security.SuperUserKey); - Guid deletedItemKey = deleteAttempt.Result.Key; - - // Assert - var nodeExists = DocumentNavigationService.TryGetDescendantsKeys(deletedItemKey, out _); - var nodeExistsInRecycleBin = DocumentNavigationService.TryGetDescendantsKeysInBin(nodeToDelete, out _); - - Assert.Multiple(() => - { - Assert.AreEqual(nodeToDelete, deletedItemKey); - Assert.IsFalse(nodeExists); - Assert.IsFalse(nodeExistsInRecycleBin); - - foreach (Guid descendant in initialDescendantsKeys) - { - var descendantExists = DocumentNavigationService.TryGetParentKey(descendant, out _); - Assert.IsFalse(descendantExists); - - var descendantExistsInRecycleBin = DocumentNavigationService.TryGetParentKeyInBin(descendant, out _); - Assert.IsFalse(descendantExistsInRecycleBin); - } - }); - } - - [Test] - // TODO: test that descendants are also restored in the right place - [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 1 to Child 1 - [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 2 to Grandchild 3 - [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", null)] // Child 3 to content root - public async Task Structure_Updates_When_Restoring_Content(Guid nodeToRestore, Guid? targetParentKey) - { - // Arrange - Guid nodeInRecycleBin = GreatGrandchild1.Key; - - // Move nodes to recycle bin - await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); // Make sure we have an item already in the recycle bin to act as a sibling - await ContentEditingService.MoveToRecycleBinAsync(nodeToRestore, Constants.Security.SuperUserKey); // Make sure the item is in the recycle bin - DocumentNavigationService.TryGetParentKeyInBin(nodeToRestore, out Guid? initialParentKey); - DocumentNavigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); - - // Act - var restoreAttempt = await ContentEditingService.RestoreAsync(nodeToRestore, targetParentKey, Constants.Security.SuperUserKey); - Guid restoredItemKey = restoreAttempt.Result.Key; - - // Assert - DocumentNavigationService.TryGetParentKey(restoredItemKey, out Guid? restoredItemParentKey); - DocumentNavigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); - - Assert.Multiple(() => - { - // Verify siblings count has decreased by one - Assert.AreEqual(initialSiblingsKeys.Count() - 1, updatedSiblingsKeys.Count()); - - if (targetParentKey is null) - { - Assert.IsNull(restoredItemParentKey); - } - else - { - Assert.IsNotNull(restoredItemParentKey); - Assert.AreNotEqual(initialParentKey, restoredItemParentKey); - } - - Assert.AreEqual(targetParentKey, restoredItemParentKey); - }); - } - - private ContentCreateModel CreateContentCreateModel(string name, Guid key, Guid? parentKey = null) - => new() - { - ContentTypeKey = ContentType.Key, - ParentKey = parentKey ?? Constants.System.RootKey, - InvariantName = name, - Key = key, - }; } From b552b5090356d7179004f09b5bfef20203078d02 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 26 Aug 2024 10:22:13 +0200 Subject: [PATCH 72/96] Reuse a method for creating content in tests --- .../Services/DocumentNavigationServiceTests.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs index d142b203f3ed..7077d891dcc1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs @@ -1,7 +1,6 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Tests.Common.Builders; @@ -74,13 +73,8 @@ public async Task Structure_Does_Not_Update_When_Scope_Is_Not_Completed() // Arrange Guid notCreatedRootKey = new Guid("516927E5-8574-497B-B45B-E27EFAB47DE4"); - var createModel = new ContentCreateModel - { - ContentTypeKey = ContentType.Key, - ParentKey = Constants.System.RootKey, // Create node at content root - InvariantName = "Root 2", - Key = notCreatedRootKey, - }; + // Create node at content root + var createModel = CreateContentCreateModel("Root 2", notCreatedRootKey); using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { From 83db666561c2b1c509c311d782e82181ddf25ff5 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 26 Aug 2024 10:22:31 +0200 Subject: [PATCH 73/96] Change type in test base --- .../Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs index 1dcf6b6a1f16..2b1abc7c84f9 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs @@ -20,7 +20,7 @@ public abstract class DocumentNavigationServiceTestsBase : UmbracoIntegrationTes protected IDocumentNavigationService DocumentNavigationService => GetRequiredService(); - protected ContentType ContentType { get; set; } + protected IContentType ContentType { get; set; } protected IContent Root { get; set; } From 4d0bbd26df7e34fe138de6bf945402fd414a1b3b Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 26 Aug 2024 10:24:01 +0200 Subject: [PATCH 74/96] Adding navigation structure updates in media service --- src/Umbraco.Core/Services/MediaService.cs | 119 +++++++++++++++++++++- 1 file changed, 116 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 755b36ae82ae..165de968c412 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; @@ -28,6 +29,7 @@ public class MediaService : RepositoryService, IMediaService private readonly IEntityRepository _entityRepository; private readonly IShortStringHelper _shortStringHelper; private readonly IUserIdKeyResolver _userIdKeyResolver; + private readonly IMediaNavigationService _mediaNavigationService; private readonly MediaFileManager _mediaFileManager; @@ -43,7 +45,8 @@ public MediaService( IMediaTypeRepository mediaTypeRepository, IEntityRepository entityRepository, IShortStringHelper shortStringHelper, - IUserIdKeyResolver userIdKeyResolver) + IUserIdKeyResolver userIdKeyResolver, + IMediaNavigationService mediaNavigationService) : base(provider, loggerFactory, eventMessagesFactory) { _mediaFileManager = mediaFileManager; @@ -53,6 +56,34 @@ public MediaService( _entityRepository = entityRepository; _shortStringHelper = shortStringHelper; _userIdKeyResolver = userIdKeyResolver; + _mediaNavigationService = mediaNavigationService; + } + + [Obsolete("Use non-obsolete constructor. Scheduled for removal in V16.")] + public MediaService( + ICoreScopeProvider provider, + MediaFileManager mediaFileManager, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IMediaRepository mediaRepository, + IAuditRepository auditRepository, + IMediaTypeRepository mediaTypeRepository, + IEntityRepository entityRepository, + IShortStringHelper shortStringHelper, + IUserIdKeyResolver userIdKeyResolver) + : this( + provider, + mediaFileManager, + loggerFactory, + eventMessagesFactory, + mediaRepository, + auditRepository, + mediaTypeRepository, + entityRepository, + shortStringHelper, + userIdKeyResolver, + StaticServiceProvider.Instance.GetRequiredService()) + { } [Obsolete("Use constructor that takes IUserIdKeyResolver as a parameter, scheduled for removal in V15")] @@ -76,8 +107,8 @@ public MediaService( mediaTypeRepository, entityRepository, shortStringHelper, - StaticServiceProvider.Instance.GetRequiredService() - ) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -769,6 +800,12 @@ public bool HasChildren(int id) media.WriterId = userId; _mediaRepository.Save(media); + + // Updates in-memory navigation structure - we only handle new items, other updates are not a concern + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.MediaService.Save", + () => _mediaNavigationService.Add(media.Key, GetParent(media)?.Key)); + scope.Notifications.Publish(new MediaSavedNotification(media, eventMessages).WithStateFrom(savingNotification)); // TODO: See note about suppressing events in content service scope.Notifications.Publish(new MediaTreeChangeNotification(media, TreeChangeTypes.RefreshNode, eventMessages)); @@ -810,6 +847,11 @@ public bool HasChildren(int id) } _mediaRepository.Save(media); + + // Updates in-memory navigation structure - we only handle new items, other updates are not a concern + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.Save-collection", + () => _mediaNavigationService.Add(media.Key, GetParent(media)?.Key)); } scope.Notifications.Publish(new MediaSavedNotification(mediasA, messages).WithStateFrom(savingNotification)); @@ -881,6 +923,26 @@ void DoDelete(IMedia c) } DoDelete(media); + + if (media.Trashed) + { + // Updates in-memory navigation structure for recycle bin items + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.MediaService.DeleteLocked-trashed", + () => _mediaNavigationService.RemoveFromBin(media.Key)); + } + else + { + // Updates in-memory navigation structure for both media and recycle bin items + // as the item needs to be deleted whether it is in the recycle bin or not + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.MediaService.DeleteLocked", + () => + { + _mediaNavigationService.Remove(media.Key); + _mediaNavigationService.RemoveFromBin(media.Key); + }); + } } //TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way @@ -1070,6 +1132,8 @@ public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int u // trash indicates whether we are trashing, un-trashing, or not changing anything private void PerformMoveLocked(IMedia media, int parentId, IMedia? parent, int userId, ICollection<(IMedia, string)> moves, bool? trash) { + // Needed to update the in-memory navigation structure + var cameFromRecycleBin = media.ParentId == Constants.System.RecycleBinMedia; media.ParentId = parentId; // get the level delta (old pos to new pos) @@ -1115,6 +1179,32 @@ private void PerformMoveLocked(IMedia media, int parentId, IMedia? parent, int u } while (total > pageSize); + if (parentId == Constants.System.RecycleBinMedia) + { + // Updates in-memory navigation structure for both media items and recycle bin items + // as we are moving to recycle bin + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked-to-recycle-bin", + () => _mediaNavigationService.Remove(media.Key)); + } + else + { + if (cameFromRecycleBin) + { + // Updates in-memory navigation structure for both media items and recycle bin items + // as we are restoring from recycle bin + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked-restore", + () => _mediaNavigationService.RestoreFromBin(media.Key, parent?.Key)); + } + else + { + // Updates in-memory navigation structure + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked", + () => _mediaNavigationService.Move(media.Key, parent?.Key)); + } + } } private void PerformMoveMediaLocked(IMedia media, bool? trash) @@ -1422,6 +1512,29 @@ private IMediaType GetMediaType(string mediaTypeAlias) #endregion + /// + /// Enlists an action in the current scope context to update the in-memory navigation structure + /// when the scope completes successfully. + /// + /// The unique key identifying the action to be enlisted. + /// The action to be performed for updating the in-memory navigation structure. + /// Thrown when the scope context is null and therefore cannot be used. + private void UpdateInMemoryNavigationStructure(string enlistingActionKey, Action updateNavigation) + { + IScopeContext? scopeContext = ScopeProvider.Context; + if (scopeContext is null) + { + throw new NullReferenceException($"The {nameof(scopeContext)} is null and cannot be used."); + } + + scopeContext.Enlist(enlistingActionKey, completed => + { + if (completed) + { + updateNavigation(); + } + }); + } } } From 5ec8eccf314ae5f8ec44e9ecced9aaa4efb62fe4 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 26 Aug 2024 10:24:16 +0200 Subject: [PATCH 75/96] Adding MediaNavigationServiceTestsBase --- .../MediaNavigationServiceTestsBase.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTestsBase.cs diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTestsBase.cs new file mode 100644 index 000000000000..c64998c3e07e --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTestsBase.cs @@ -0,0 +1,51 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public abstract class MediaNavigationServiceTestsBase : UmbracoIntegrationTest +{ + protected IMediaTypeService MediaTypeService => GetRequiredService(); + + // Testing with IMediaEditingService as it calls IMediaService underneath + protected IMediaEditingService MediaEditingService => GetRequiredService(); + + protected IMediaNavigationService MediaNavigationService => GetRequiredService(); + + protected IMediaType FolderMediaType { get; set; } + + protected IMediaType ImageMediaType { get; set; } + + protected IMedia Album { get; set; } + + protected IMedia Image1 { get; set; } + + protected IMedia SubAlbum1 { get; set; } + + protected IMedia Image2 { get; set; } + + protected IMedia Image3 { get; set; } + + protected IMedia SubAlbum2 { get; set; } + + protected IMedia SubSubAlbum1 { get; set; } + + protected IMedia Image4 { get; set; } + + protected MediaCreateModel CreateMediaCreateModel(string name, Guid key, Guid mediaTypeKey, Guid? parentKey = null) + => new() + { + ContentTypeKey = mediaTypeKey, + ParentKey = parentKey ?? Constants.System.RootKey, + InvariantName = name, + Key = key, + }; +} From 19404b3985e35674ef6457591e24cb6aa589a5cb Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 26 Aug 2024 10:25:12 +0200 Subject: [PATCH 76/96] Adding integration tests for media nav str --- .../MediaNavigationServiceTests.Create.cs | 9 +++ .../MediaNavigationServiceTests.Delete.cs | 9 +++ ...gationServiceTests.DeleteFromRecycleBin.cs | 9 +++ .../MediaNavigationServiceTests.Move.cs | 9 +++ ...NavigationServiceTests.MoveToRecycleBin.cs | 9 +++ .../MediaNavigationServiceTests.Rebuild.cs | 59 ++++++++++++++ .../MediaNavigationServiceTests.Restore.cs | 9 +++ .../MediaNavigationServiceTests.Update.cs | 9 +++ .../Services/MediaNavigationServiceTests.cs | 79 +++++++++++++++++++ .../Umbraco.Tests.Integration.csproj | 24 ++++++ 10 files changed, 225 insertions(+) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.DeleteFromRecycleBin.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Update.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs new file mode 100644 index 000000000000..1b3b6551a5f6 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs @@ -0,0 +1,9 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs new file mode 100644 index 000000000000..1b3b6551a5f6 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs @@ -0,0 +1,9 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.DeleteFromRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.DeleteFromRecycleBin.cs new file mode 100644 index 000000000000..1b3b6551a5f6 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.DeleteFromRecycleBin.cs @@ -0,0 +1,9 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs new file mode 100644 index 000000000000..1b3b6551a5f6 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs @@ -0,0 +1,9 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs new file mode 100644 index 000000000000..1b3b6551a5f6 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs @@ -0,0 +1,9 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs new file mode 100644 index 000000000000..bdf5459d4922 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs @@ -0,0 +1,59 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.Navigation; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + [Test] + public async Task Structure_Can_Rebuild() + { + // Arrange + Guid nodeKey = Album.Key; + + // Capture original built state of MediaNavigationService + MediaNavigationService.TryGetParentKey(nodeKey, out Guid? originalParentKey); + MediaNavigationService.TryGetChildrenKeys(nodeKey, out IEnumerable originalChildrenKeys); + MediaNavigationService.TryGetDescendantsKeys(nodeKey, out IEnumerable originalDescendantsKeys); + MediaNavigationService.TryGetAncestorsKeys(nodeKey, out IEnumerable originalAncestorsKeys); + MediaNavigationService.TryGetSiblingsKeys(nodeKey, out IEnumerable originalSiblingsKeys); + + // Im-memory navigation structure is empty here + var newMediaNavigationService = new MediaNavigationService(GetRequiredService(), GetRequiredService()); + var initialNodeExists = newMediaNavigationService.TryGetParentKey(nodeKey, out _); + + // Act + await newMediaNavigationService.RebuildAsync(); + + // Capture rebuilt state + var nodeExists = newMediaNavigationService.TryGetParentKey(nodeKey, out Guid? parentKeyFromRebuild); + newMediaNavigationService.TryGetChildrenKeys(nodeKey, out IEnumerable childrenKeysFromRebuild); + newMediaNavigationService.TryGetDescendantsKeys(nodeKey, out IEnumerable descendantsKeysFromRebuild); + newMediaNavigationService.TryGetAncestorsKeys(nodeKey, out IEnumerable ancestorsKeysFromRebuild); + newMediaNavigationService.TryGetSiblingsKeys(nodeKey, out IEnumerable siblingsKeysFromRebuild); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(initialNodeExists); + + // Verify that the item is present in the navigation structure after a rebuild + Assert.IsTrue(nodeExists); + + // Verify that we have the same items as in the original built state of MediaNavigationService + Assert.AreEqual(originalParentKey, parentKeyFromRebuild); + CollectionAssert.AreEquivalent(originalChildrenKeys, childrenKeysFromRebuild); + CollectionAssert.AreEquivalent(originalDescendantsKeys, descendantsKeysFromRebuild); + CollectionAssert.AreEquivalent(originalAncestorsKeys, ancestorsKeysFromRebuild); + CollectionAssert.AreEquivalent(originalSiblingsKeys, siblingsKeysFromRebuild); + }); + } + + [Test] + // TODO: Test that you can rebuild bin structure as well + public async Task Bin_Structure_Can_Rebuild() + { + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs new file mode 100644 index 000000000000..1b3b6551a5f6 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs @@ -0,0 +1,9 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Update.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Update.cs new file mode 100644 index 000000000000..1b3b6551a5f6 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Update.cs @@ -0,0 +1,9 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs new file mode 100644 index 000000000000..e7406a225c67 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs @@ -0,0 +1,79 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests : MediaNavigationServiceTestsBase +{ + [SetUp] + public async Task Setup() + { + // Album + // - Image 1 + // - Sub-album 1 + // - Image 2 + // - Image 3 + // - Sub-album 2 + // - Sub-sub-album 1 + // - Image 4 + + // Media Types + FolderMediaType = MediaTypeService.Get(Constants.Conventions.MediaTypes.Folder); + ImageMediaType = MediaTypeService.Get(Constants.Conventions.MediaTypes.Image); + + // Media + var albumModel = CreateMediaCreateModel("Album", new Guid("1CD97C02-8534-4B72-AE9E-AE52EC94CF31"), FolderMediaType.Key); + var albumCreateAttempt = await MediaEditingService.CreateAsync(albumModel, Constants.Security.SuperUserKey); + Album = albumCreateAttempt.Result.Content!; + + var image1Model = CreateMediaCreateModel("Image 1", new Guid("03976EBE-A942-4F24-9885-9186E99AEF7C"), ImageMediaType.Key, Album.Key); + var image1CreateAttempt = await MediaEditingService.CreateAsync(image1Model, Constants.Security.SuperUserKey); + Image1 = image1CreateAttempt.Result.Content!; + + var subAlbum1Model = CreateMediaCreateModel("Sub-album 1", new Guid("139DC977-E50F-4382-9728-B278C4B7AC6A"), FolderMediaType.Key, Album.Key); + var subAlbum1CreateAttempt = await MediaEditingService.CreateAsync(subAlbum1Model, Constants.Security.SuperUserKey); + SubAlbum1 = subAlbum1CreateAttempt.Result.Content!; + + var image2Model = CreateMediaCreateModel("Image 2", new Guid("3E489C32-9315-42DA-95CE-823D154B09C8"), ImageMediaType.Key, SubAlbum1.Key); + var image2CreateAttempt = await MediaEditingService.CreateAsync(image2Model, Constants.Security.SuperUserKey); + Image2 = image2CreateAttempt.Result.Content!; + + var image3Model = CreateMediaCreateModel("Image 3", new Guid("6176BD70-2CD2-4AEE-A045-084C94E4AFF2"), ImageMediaType.Key, SubAlbum1.Key); + var image3CreateAttempt = await MediaEditingService.CreateAsync(image3Model, Constants.Security.SuperUserKey); + Image3 = image3CreateAttempt.Result.Content!; + + var subAlbum2Model = CreateMediaCreateModel("Sub-album 2", new Guid("DBCAFF2F-BFA4-4744-A948-C290C432D564"), FolderMediaType.Key, Album.Key); + var subAlbum2CreateAttempt = await MediaEditingService.CreateAsync(subAlbum2Model, Constants.Security.SuperUserKey); + SubAlbum2 = subAlbum2CreateAttempt.Result.Content!; + + var subSubAlbum1Model = CreateMediaCreateModel("Sub-sub-album 1", new Guid("E0B23D56-9A0E-4FC4-BD42-834B73B4C7AB"), FolderMediaType.Key, SubAlbum2.Key); + var subSubAlbum1CreateAttempt = await MediaEditingService.CreateAsync(subSubAlbum1Model, Constants.Security.SuperUserKey); + SubSubAlbum1 = subSubAlbum1CreateAttempt.Result.Content!; + + var image4Model = CreateMediaCreateModel("Image 4", new Guid("62BCE72F-8C18-420E-BCAC-112B5ECC95FD"), ImageMediaType.Key, SubSubAlbum1.Key); + var image4CreateAttempt = await MediaEditingService.CreateAsync(image4Model, Constants.Security.SuperUserKey); + Image4 = image4CreateAttempt.Result.Content!; + } + + [Test] + public async Task Structure_Does_Not_Update_When_Scope_Is_Not_Completed() + { + // Arrange + Guid notCreatedAlbumKey = new Guid("860EE748-BC7E-4A13-A1D9-C9160B25AD6E"); + + // Create node at media root + var createModel = CreateMediaCreateModel("Album 2", notCreatedAlbumKey, FolderMediaType.Key); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + } + + // Act + var nodeExists = MediaNavigationService.TryGetParentKey(notCreatedAlbumKey, out _); + + // Assert + Assert.IsFalse(nodeExists); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 6923de4e8638..de8274a94f9d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -181,6 +181,30 @@ DocumentNavigationServiceTests.cs + + MediaNavigationServiceTests.cs + + + MediaNavigationServiceTests.cs + + + MediaNavigationServiceTests.cs + + + MediaNavigationServiceTests.cs + + + MediaNavigationServiceTests.cs + + + MediaNavigationServiceTests.cs + + + MediaNavigationServiceTests.cs + + + MediaNavigationServiceTests.cs + From 907bce66a40e13f2255a852b9ca0f51c121c7c28 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 26 Aug 2024 13:49:33 +0200 Subject: [PATCH 77/96] Remove services as we will have more concrete ones --- .../Services/Navigation/IDocumentNavigationService.cs | 5 ----- .../Services/Navigation/IMediaNavigationService.cs | 5 ----- 2 files changed, 10 deletions(-) delete mode 100644 src/Umbraco.Core/Services/Navigation/IDocumentNavigationService.cs delete mode 100644 src/Umbraco.Core/Services/Navigation/IMediaNavigationService.cs diff --git a/src/Umbraco.Core/Services/Navigation/IDocumentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/IDocumentNavigationService.cs deleted file mode 100644 index 75d2239c82e6..000000000000 --- a/src/Umbraco.Core/Services/Navigation/IDocumentNavigationService.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Umbraco.Cms.Core.Services.Navigation; - -public interface IDocumentNavigationService : INavigationQueryService, INavigationManagementService -{ -} diff --git a/src/Umbraco.Core/Services/Navigation/IMediaNavigationService.cs b/src/Umbraco.Core/Services/Navigation/IMediaNavigationService.cs deleted file mode 100644 index c7cb057849fd..000000000000 --- a/src/Umbraco.Core/Services/Navigation/IMediaNavigationService.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Umbraco.Cms.Core.Services.Navigation; - -public interface IMediaNavigationService : INavigationQueryService, INavigationManagementService -{ -} From 6577bbc141823b4b00beced10c7d6a708c365952 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 26 Aug 2024 13:51:11 +0200 Subject: [PATCH 78/96] Add document and media IXNavigationQueryService and IXNavigationManagementService --- .../Navigation/IDocumentNavigationManagementService.cs | 5 +++++ .../Services/Navigation/IDocumentNavigationQueryService.cs | 5 +++++ .../Services/Navigation/IMediaNavigationManagementService.cs | 5 +++++ .../Services/Navigation/IMediaNavigationQueryService.cs | 5 +++++ 4 files changed, 20 insertions(+) create mode 100644 src/Umbraco.Core/Services/Navigation/IDocumentNavigationManagementService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/IDocumentNavigationQueryService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/IMediaNavigationManagementService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/IMediaNavigationQueryService.cs diff --git a/src/Umbraco.Core/Services/Navigation/IDocumentNavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/IDocumentNavigationManagementService.cs new file mode 100644 index 000000000000..789256329ee5 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/IDocumentNavigationManagementService.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IDocumentNavigationManagementService : INavigationManagementService, IRecycleBinNavigationManagementService +{ +} diff --git a/src/Umbraco.Core/Services/Navigation/IDocumentNavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/IDocumentNavigationQueryService.cs new file mode 100644 index 000000000000..d8fb5e0c2951 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/IDocumentNavigationQueryService.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IDocumentNavigationQueryService : INavigationQueryService, IRecycleBinNavigationQueryService +{ +} diff --git a/src/Umbraco.Core/Services/Navigation/IMediaNavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/IMediaNavigationManagementService.cs new file mode 100644 index 000000000000..95cb1c96168b --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/IMediaNavigationManagementService.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IMediaNavigationManagementService : INavigationManagementService, IRecycleBinNavigationManagementService +{ +} diff --git a/src/Umbraco.Core/Services/Navigation/IMediaNavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/IMediaNavigationQueryService.cs new file mode 100644 index 000000000000..e3f46f421148 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/IMediaNavigationQueryService.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IMediaNavigationQueryService : INavigationQueryService, IRecycleBinNavigationQueryService +{ +} From 2aa39cbfe3f3789d5d430adf9ba0ea73ca1fbfd3 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 26 Aug 2024 13:52:11 +0200 Subject: [PATCH 79/96] Inject ManagementService in ContentService.cs and MediaService.cs --- src/Umbraco.Core/Services/ContentService.cs | 28 ++++++++++----------- src/Umbraco.Core/Services/MediaService.cs | 26 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index b4690055f894..0b629774a3af 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -34,7 +34,7 @@ public class ContentService : RepositoryService, IContentService private readonly IShortStringHelper _shortStringHelper; private readonly ICultureImpactFactory _cultureImpactFactory; private readonly IUserIdKeyResolver _userIdKeyResolver; - private readonly IDocumentNavigationService _documentNavigationService; + private readonly IDocumentNavigationManagementService _documentNavigationManagementService; private IQuery? _queryNotTrashed; #region Constructors @@ -53,7 +53,7 @@ public ContentService( IShortStringHelper shortStringHelper, ICultureImpactFactory cultureImpactFactory, IUserIdKeyResolver userIdKeyResolver, - IDocumentNavigationService documentNavigationService) + IDocumentNavigationManagementService documentNavigationManagementService) : base(provider, loggerFactory, eventMessagesFactory) { _documentRepository = documentRepository; @@ -66,7 +66,7 @@ public ContentService( _shortStringHelper = shortStringHelper; _cultureImpactFactory = cultureImpactFactory; _userIdKeyResolver = userIdKeyResolver; - _documentNavigationService = documentNavigationService; + _documentNavigationManagementService = documentNavigationManagementService; _logger = loggerFactory.CreateLogger(); } @@ -99,7 +99,7 @@ public ContentService( shortStringHelper, cultureImpactFactory, userIdKeyResolver, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -131,7 +131,7 @@ public ContentService( shortStringHelper, cultureImpactFactory, StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -1075,7 +1075,7 @@ public OperationResult Save(IContent content, int? userId = null, ContentSchedul // Updates in-memory navigation structure - we only handle new items, other updates are not a concern UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.ContentService.Save-with-contentSchedule", - () => _documentNavigationService.Add(content.Key, GetParent(content)?.Key)); + () => _documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key)); if (contentSchedule != null) { @@ -1144,7 +1144,7 @@ public OperationResult Save(IEnumerable contents, int userId = Constan // Updates in-memory navigation structure - we only handle new items, other updates are not a concern UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.ContentService.Save", - () => _documentNavigationService.Add(content.Key, GetParent(content)?.Key)); + () => _documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key)); } scope.Notifications.Publish( @@ -2342,7 +2342,7 @@ void DoDelete(IContent c) // Updates in-memory navigation structure for recycle bin items UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.ContentService.DeleteLocked-trashed", - () => _documentNavigationService.RemoveFromBin(content.Key)); + () => _documentNavigationManagementService.RemoveFromBin(content.Key)); } else { @@ -2352,8 +2352,8 @@ void DoDelete(IContent c) "Umbraco.Cms.Core.Services.ContentService.DeleteLocked", () => { - _documentNavigationService.Remove(content.Key); - _documentNavigationService.RemoveFromBin(content.Key); + _documentNavigationManagementService.MoveToBin(content.Key); + _documentNavigationManagementService.RemoveFromBin(content.Key); }); } } @@ -2637,7 +2637,7 @@ private void PerformMoveLocked(IContent content, int parentId, IContent? parent, // as we are moving to recycle bin UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked-to-recycle-bin", - () => _documentNavigationService.Remove(content.Key)); + () => _documentNavigationManagementService.MoveToBin(content.Key)); } else { @@ -2647,14 +2647,14 @@ private void PerformMoveLocked(IContent content, int parentId, IContent? parent, // as we are restoring from recycle bin UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked-restore", - () => _documentNavigationService.RestoreFromBin(content.Key, parent?.Key)); + () => _documentNavigationManagementService.RestoreFromBin(content.Key, parent?.Key)); } else { // Updates in-memory navigation structure UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked", - () => _documentNavigationService.Move(content.Key, parent?.Key)); + () => _documentNavigationManagementService.Move(content.Key, parent?.Key)); } } } @@ -2871,7 +2871,7 @@ public bool RecycleBinSmells() { foreach (Tuple update in navigationUpdates) { - _documentNavigationService.Add(update.Item1, update.Item2); + _documentNavigationManagementService.Add(update.Item1, update.Item2); } }); } diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 0f68adda54b8..ae334447e964 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -29,7 +29,7 @@ public class MediaService : RepositoryService, IMediaService private readonly IEntityRepository _entityRepository; private readonly IShortStringHelper _shortStringHelper; private readonly IUserIdKeyResolver _userIdKeyResolver; - private readonly IMediaNavigationService _mediaNavigationService; + private readonly IMediaNavigationManagementService _mediaNavigationManagementService; private readonly MediaFileManager _mediaFileManager; @@ -46,7 +46,7 @@ public MediaService( IEntityRepository entityRepository, IShortStringHelper shortStringHelper, IUserIdKeyResolver userIdKeyResolver, - IMediaNavigationService mediaNavigationService) + IMediaNavigationManagementService mediaNavigationManagementService) : base(provider, loggerFactory, eventMessagesFactory) { _mediaFileManager = mediaFileManager; @@ -56,7 +56,7 @@ public MediaService( _entityRepository = entityRepository; _shortStringHelper = shortStringHelper; _userIdKeyResolver = userIdKeyResolver; - _mediaNavigationService = mediaNavigationService; + _mediaNavigationManagementService = mediaNavigationManagementService; } [Obsolete("Use non-obsolete constructor. Scheduled for removal in V16.")] @@ -82,7 +82,7 @@ public MediaService( entityRepository, shortStringHelper, userIdKeyResolver, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -108,7 +108,7 @@ public MediaService( entityRepository, shortStringHelper, StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -804,7 +804,7 @@ public bool HasChildren(int id) // Updates in-memory navigation structure - we only handle new items, other updates are not a concern UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.MediaService.Save", - () => _mediaNavigationService.Add(media.Key, GetParent(media)?.Key)); + () => _mediaNavigationManagementService.Add(media.Key, GetParent(media)?.Key)); scope.Notifications.Publish(new MediaSavedNotification(media, eventMessages).WithStateFrom(savingNotification)); // TODO: See note about suppressing events in content service @@ -851,7 +851,7 @@ public bool HasChildren(int id) // Updates in-memory navigation structure - we only handle new items, other updates are not a concern UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.ContentService.Save-collection", - () => _mediaNavigationService.Add(media.Key, GetParent(media)?.Key)); + () => _mediaNavigationManagementService.Add(media.Key, GetParent(media)?.Key)); } scope.Notifications.Publish(new MediaSavedNotification(mediasA, messages).WithStateFrom(savingNotification)); @@ -929,7 +929,7 @@ void DoDelete(IMedia c) // Updates in-memory navigation structure for recycle bin items UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.MediaService.DeleteLocked-trashed", - () => _mediaNavigationService.RemoveFromBin(media.Key)); + () => _mediaNavigationManagementService.RemoveFromBin(media.Key)); } else { @@ -939,8 +939,8 @@ void DoDelete(IMedia c) "Umbraco.Cms.Core.Services.MediaService.DeleteLocked", () => { - _mediaNavigationService.Remove(media.Key); - _mediaNavigationService.RemoveFromBin(media.Key); + _mediaNavigationManagementService.MoveToBin(media.Key); + _mediaNavigationManagementService.RemoveFromBin(media.Key); }); } } @@ -1184,7 +1184,7 @@ private void PerformMoveLocked(IMedia media, int parentId, IMedia? parent, int u // as we are moving to recycle bin UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked-to-recycle-bin", - () => _mediaNavigationService.Remove(media.Key)); + () => _mediaNavigationManagementService.MoveToBin(media.Key)); } else { @@ -1194,14 +1194,14 @@ private void PerformMoveLocked(IMedia media, int parentId, IMedia? parent, int u // as we are restoring from recycle bin UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked-restore", - () => _mediaNavigationService.RestoreFromBin(media.Key, parent?.Key)); + () => _mediaNavigationManagementService.RestoreFromBin(media.Key, parent?.Key)); } else { // Updates in-memory navigation structure UpdateInMemoryNavigationStructure( "Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked", - () => _mediaNavigationService.Move(media.Key, parent?.Key)); + () => _mediaNavigationManagementService.Move(media.Key, parent?.Key)); } } } From d361cce7aa7e97c0605c2eaef5c6286c8c2cf373 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 26 Aug 2024 13:52:49 +0200 Subject: [PATCH 80/96] Change implementation to implement the new services + registration --- .../DependencyInjection/UmbracoBuilder.cs | 11 +++++++---- .../Services/Navigation/DocumentNavigationService.cs | 2 +- .../Services/Navigation/MediaNavigationService.cs | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 9223a9f4b359..c39f05cc5e8c 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -339,8 +339,7 @@ private void AddCoreServices() factory.GetRequiredService(), factory.GetRequiredService(), factory.GetRequiredService(), - factory.GetRequiredService() - )); + factory.GetRequiredService())); Services.AddUnique(); Services.AddUnique(factory => factory.GetRequiredService()); Services.AddUnique(factory => new LocalizedTextService( @@ -353,8 +352,12 @@ private void AddCoreServices() Services.AddSingleton(); Services.AddUnique(); Services.AddUnique(); - Services.AddUnique(); - Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(x => x.GetRequiredService()); + Services.AddUnique(x => x.GetRequiredService()); + Services.AddUnique(); + Services.AddUnique(x => x.GetRequiredService()); + Services.AddUnique(x => x.GetRequiredService()); // Register a noop IHtmlSanitizer & IMarkdownSanitizer to be replaced Services.AddUnique(); diff --git a/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs index 39103333423a..44804d07c686 100644 --- a/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs @@ -3,7 +3,7 @@ namespace Umbraco.Cms.Core.Services.Navigation; -internal sealed class DocumentNavigationService : ContentNavigationServiceBase, IDocumentNavigationService +internal sealed class DocumentNavigationService : ContentNavigationServiceBase, IDocumentNavigationQueryService, IDocumentNavigationManagementService { public DocumentNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) : base(coreScopeProvider, navigationRepository) diff --git a/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs b/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs index dbe8ed54f2ee..62ab5a1617e4 100644 --- a/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs @@ -3,7 +3,7 @@ namespace Umbraco.Cms.Core.Services.Navigation; -internal sealed class MediaNavigationService : ContentNavigationServiceBase, IMediaNavigationService +internal sealed class MediaNavigationService : ContentNavigationServiceBase, IMediaNavigationQueryService, IMediaNavigationManagementService { public MediaNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) : base(coreScopeProvider, navigationRepository) From 2c6f9fa196e3d65bb545fa365375299a9c7f3418 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 26 Aug 2024 13:53:17 +0200 Subject: [PATCH 81/96] Make classes sealed --- src/Umbraco.Core/Models/Navigation/NavigationNode.cs | 2 +- .../Services/Navigation/NavigationInitializationService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Models/Navigation/NavigationNode.cs b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs index eab9e06bcc48..9edf00d6fb66 100644 --- a/src/Umbraco.Core/Models/Navigation/NavigationNode.cs +++ b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Core.Models.Navigation; -public class NavigationNode +public sealed class NavigationNode { private List _children; diff --git a/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs b/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs index 551eeb97fbcc..1a56655a4df2 100644 --- a/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs +++ b/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs @@ -6,7 +6,7 @@ namespace Umbraco.Cms.Core.Services.Navigation; /// Responsible for seeding the in-memory navigation structures at application's startup /// by rebuild the navigation structures. /// -public class NavigationInitializationService : IHostedLifecycleService +public sealed class NavigationInitializationService : IHostedLifecycleService { private readonly IDocumentNavigationService _documentNavigationService; private readonly IMediaNavigationService _mediaNavigationService; From 695e5ade6e626d31d790cc2dbe9a7ff8fbdd277d Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 26 Aug 2024 13:59:18 +0200 Subject: [PATCH 82/96] Inject correct services in InitializationService --- .../NavigationInitializationService.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs b/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs index 1a56655a4df2..aa972e84b65c 100644 --- a/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs +++ b/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs @@ -8,21 +8,21 @@ namespace Umbraco.Cms.Core.Services.Navigation; /// public sealed class NavigationInitializationService : IHostedLifecycleService { - private readonly IDocumentNavigationService _documentNavigationService; - private readonly IMediaNavigationService _mediaNavigationService; + private readonly IDocumentNavigationManagementService _documentNavigationManagementService; + private readonly IMediaNavigationManagementService _mediaNavigationManagementService; - public NavigationInitializationService(IDocumentNavigationService documentNavigationService, IMediaNavigationService mediaNavigationService) + public NavigationInitializationService(IDocumentNavigationManagementService documentNavigationManagementService, IMediaNavigationManagementService mediaNavigationManagementService) { - _documentNavigationService = documentNavigationService; - _mediaNavigationService = mediaNavigationService; + _documentNavigationManagementService = documentNavigationManagementService; + _mediaNavigationManagementService = mediaNavigationManagementService; } public async Task StartingAsync(CancellationToken cancellationToken) { - await _documentNavigationService.RebuildAsync(); - await _documentNavigationService.RebuildBinAsync(); - await _mediaNavigationService.RebuildAsync(); - await _mediaNavigationService.RebuildBinAsync(); + await _documentNavigationManagementService.RebuildAsync(); + await _documentNavigationManagementService.RebuildBinAsync(); + await _mediaNavigationManagementService.RebuildAsync(); + await _mediaNavigationManagementService.RebuildBinAsync(); } public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; From 7c5e71e230bcdb2f891e9f5933a88b0af660e2ff Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 26 Aug 2024 14:02:55 +0200 Subject: [PATCH 83/96] Using the right services in integration tests --- .../DocumentNavigationServiceTests.Copy.cs | 4 ++-- .../DocumentNavigationServiceTests.Create.cs | 4 ++-- .../DocumentNavigationServiceTests.Delete.cs | 10 +++++----- ...gationServiceTests.DeleteFromRecycleBin.cs | 4 ++-- .../DocumentNavigationServiceTests.Move.cs | 4 ++-- ...NavigationServiceTests.MoveToRecycleBin.cs | 6 +++--- .../DocumentNavigationServiceTests.Rebuild.cs | 10 +++++----- .../DocumentNavigationServiceTests.Restore.cs | 8 ++++---- .../DocumentNavigationServiceTests.Update.cs | 20 +++++++++---------- .../DocumentNavigationServiceTests.cs | 2 +- .../DocumentNavigationServiceTestsBase.cs | 2 +- .../MediaNavigationServiceTests.Rebuild.cs | 10 +++++----- .../Services/MediaNavigationServiceTests.cs | 2 +- .../MediaNavigationServiceTestsBase.cs | 2 +- 14 files changed, 44 insertions(+), 44 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs index 374263d41afe..b3ebfa221582 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs @@ -16,7 +16,7 @@ public partial class DocumentNavigationServiceTests public async Task Structure_Updates_When_Copying_Content(Guid nodeToCopy, Guid? targetParentKey) { // Arrange - DocumentNavigationService.TryGetParentKey(nodeToCopy, out Guid? sourceParentKey); + DocumentNavigationQueryService.TryGetParentKey(nodeToCopy, out Guid? sourceParentKey); // Act var copyAttempt = await ContentEditingService.CopyAsync(nodeToCopy, targetParentKey, false, false, Constants.Security.SuperUserKey); @@ -25,7 +25,7 @@ public async Task Structure_Updates_When_Copying_Content(Guid nodeToCopy, Guid? // Assert Assert.AreNotEqual(nodeToCopy, copiedItemKey); - DocumentNavigationService.TryGetParentKey(copiedItemKey, out Guid? copiedItemParentKey); + DocumentNavigationQueryService.TryGetParentKey(copiedItemKey, out Guid? copiedItemParentKey); Assert.Multiple(() => { 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 d1505b589375..2ee6c7cabe3f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs @@ -10,7 +10,7 @@ public partial class DocumentNavigationServiceTests public async Task Structure_Updates_When_Creating_Content() { // Arrange - DocumentNavigationService.TryGetSiblingsKeys(Root.Key, out IEnumerable initialSiblingsKeys); + DocumentNavigationQueryService.TryGetSiblingsKeys(Root.Key, out IEnumerable initialSiblingsKeys); var initialRootNodeSiblingsCount = initialSiblingsKeys.Count(); var createModel = new ContentCreateModel @@ -25,7 +25,7 @@ public async Task Structure_Updates_When_Creating_Content() Guid createdItemKey = createAttempt.Result.Content!.Key; // Verify that the structure has updated by checking the siblings list of the Root once again - DocumentNavigationService.TryGetSiblingsKeys(Root.Key, out IEnumerable updatedSiblingsKeys); + DocumentNavigationQueryService.TryGetSiblingsKeys(Root.Key, out IEnumerable updatedSiblingsKeys); List siblingsList = updatedSiblingsKeys.ToList(); // Assert 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 a056b9788504..5e6e655d7481 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs @@ -13,7 +13,7 @@ public partial class DocumentNavigationServiceTests public async Task Structure_Updates_When_Deleting_Content(Guid nodeToDelete) { // Arrange - DocumentNavigationService.TryGetDescendantsKeys(nodeToDelete, out IEnumerable initialDescendantsKeys); + DocumentNavigationQueryService.TryGetDescendantsKeys(nodeToDelete, out IEnumerable initialDescendantsKeys); // Act // Deletes the item whether it is in the recycle bin or not @@ -21,8 +21,8 @@ public async Task Structure_Updates_When_Deleting_Content(Guid nodeToDelete) Guid deletedItemKey = deleteAttempt.Result.Key; // Assert - var nodeExists = DocumentNavigationService.TryGetDescendantsKeys(deletedItemKey, out _); - var nodeExistsInRecycleBin = DocumentNavigationService.TryGetDescendantsKeysInBin(nodeToDelete, out _); + var nodeExists = DocumentNavigationQueryService.TryGetDescendantsKeys(deletedItemKey, out _); + var nodeExistsInRecycleBin = DocumentNavigationQueryService.TryGetDescendantsKeysInBin(nodeToDelete, out _); Assert.Multiple(() => { @@ -32,10 +32,10 @@ public async Task Structure_Updates_When_Deleting_Content(Guid nodeToDelete) foreach (Guid descendant in initialDescendantsKeys) { - var descendantExists = DocumentNavigationService.TryGetParentKey(descendant, out _); + var descendantExists = DocumentNavigationQueryService.TryGetParentKey(descendant, out _); Assert.IsFalse(descendantExists); - var descendantExistsInRecycleBin = DocumentNavigationService.TryGetParentKeyInBin(descendant, out _); + var descendantExistsInRecycleBin = DocumentNavigationQueryService.TryGetParentKeyInBin(descendant, out _); Assert.IsFalse(descendantExistsInRecycleBin); } }); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.DeleteFromRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.DeleteFromRecycleBin.cs index e35f2f4ad27e..1f9b81936639 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.DeleteFromRecycleBin.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.DeleteFromRecycleBin.cs @@ -16,13 +16,13 @@ public async Task Structure_Updates_When_Deleting_From_Recycle_Bin() // Move nodes to recycle bin await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); // Make sure we have an item already in the recycle bin to act as a sibling await ContentEditingService.MoveToRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); // Make sure the item is in the recycle bin - DocumentNavigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); + DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); // Act await ContentEditingService.DeleteFromRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); // Assert - DocumentNavigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); + DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); // Verify siblings count has decreased by one Assert.AreEqual(initialSiblingsKeys.Count() - 1, updatedSiblingsKeys.Count()); 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 f4a8e57e1b4e..078e06de2bfb 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs @@ -12,13 +12,13 @@ public partial class DocumentNavigationServiceTests public async Task Structure_Updates_When_Moving_Content(Guid nodeToMove, Guid? targetParentKey) { // Arrange - DocumentNavigationService.TryGetParentKey(nodeToMove, out Guid? originalParentKey); + DocumentNavigationQueryService.TryGetParentKey(nodeToMove, out Guid? originalParentKey); // Act var moveAttempt = await ContentEditingService.MoveAsync(nodeToMove, targetParentKey, Constants.Security.SuperUserKey); // Verify the node's new parent is updated - DocumentNavigationService.TryGetParentKey(moveAttempt.Result!.Key, out Guid? updatedParentKey); + DocumentNavigationQueryService.TryGetParentKey(moveAttempt.Result!.Key, out Guid? updatedParentKey); // Assert Assert.Multiple(() => 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 bbab376aa1ec..1bd4bd9d834b 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs @@ -12,14 +12,14 @@ public async Task Structure_Updates_When_Moving_Content_To_Recycle_Bin() { // Arrange Guid nodeToMoveToRecycleBin = Child3.Key; - DocumentNavigationService.TryGetParentKey(nodeToMoveToRecycleBin, out Guid? originalParentKey); + DocumentNavigationQueryService.TryGetParentKey(nodeToMoveToRecycleBin, out Guid? originalParentKey); // Act await ContentEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey); // Assert - var nodeExists = DocumentNavigationService.TryGetParentKey(nodeToMoveToRecycleBin, out _); // Verify that the item is no longer in the document structure - var nodeExistsInRecycleBin = DocumentNavigationService.TryGetParentKeyInBin(nodeToMoveToRecycleBin, out Guid? updatedParentKeyInRecycleBin); + var nodeExists = DocumentNavigationQueryService.TryGetParentKey(nodeToMoveToRecycleBin, out _); // Verify that the item is no longer in the document structure + var nodeExistsInRecycleBin = DocumentNavigationQueryService.TryGetParentKeyInBin(nodeToMoveToRecycleBin, out Guid? updatedParentKeyInRecycleBin); Assert.Multiple(() => { 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 82688c167422..6d70870a3273 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs @@ -14,11 +14,11 @@ public async Task Structure_Can_Rebuild() Guid nodeKey = Root.Key; // Capture original built state of DocumentNavigationService - DocumentNavigationService.TryGetParentKey(nodeKey, out Guid? originalParentKey); - DocumentNavigationService.TryGetChildrenKeys(nodeKey, out IEnumerable originalChildrenKeys); - DocumentNavigationService.TryGetDescendantsKeys(nodeKey, out IEnumerable originalDescendantsKeys); - DocumentNavigationService.TryGetAncestorsKeys(nodeKey, out IEnumerable originalAncestorsKeys); - DocumentNavigationService.TryGetSiblingsKeys(nodeKey, out IEnumerable originalSiblingsKeys); + DocumentNavigationQueryService.TryGetParentKey(nodeKey, out Guid? originalParentKey); + DocumentNavigationQueryService.TryGetChildrenKeys(nodeKey, out IEnumerable originalChildrenKeys); + DocumentNavigationQueryService.TryGetDescendantsKeys(nodeKey, out IEnumerable originalDescendantsKeys); + DocumentNavigationQueryService.TryGetAncestorsKeys(nodeKey, out IEnumerable originalAncestorsKeys); + DocumentNavigationQueryService.TryGetSiblingsKeys(nodeKey, out IEnumerable originalSiblingsKeys); // Im-memory navigation structure is empty here var newDocumentNavigationService = new DocumentNavigationService(GetRequiredService(), GetRequiredService()); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs index a765ef24ca4f..3151fb83e4ab 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs @@ -18,16 +18,16 @@ public async Task Structure_Updates_When_Restoring_Content(Guid nodeToRestore, G // Move nodes to recycle bin await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); // Make sure we have an item already in the recycle bin to act as a sibling await ContentEditingService.MoveToRecycleBinAsync(nodeToRestore, Constants.Security.SuperUserKey); // Make sure the item is in the recycle bin - DocumentNavigationService.TryGetParentKeyInBin(nodeToRestore, out Guid? initialParentKey); - DocumentNavigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); + DocumentNavigationQueryService.TryGetParentKeyInBin(nodeToRestore, out Guid? initialParentKey); + DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); // Act var restoreAttempt = await ContentEditingService.RestoreAsync(nodeToRestore, targetParentKey, Constants.Security.SuperUserKey); Guid restoredItemKey = restoreAttempt.Result.Key; // Assert - DocumentNavigationService.TryGetParentKey(restoredItemKey, out Guid? restoredItemParentKey); - DocumentNavigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); + DocumentNavigationQueryService.TryGetParentKey(restoredItemKey, out Guid? restoredItemParentKey); + DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); Assert.Multiple(() => { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Update.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Update.cs index 591bd08de6ee..8ed720000e37 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Update.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Update.cs @@ -13,11 +13,11 @@ public async Task Structure_Does_Not_Update_When_Updating_Content() Guid nodeToUpdate = Root.Key; // Capture initial state - DocumentNavigationService.TryGetParentKey(nodeToUpdate, out Guid? initialParentKey); - DocumentNavigationService.TryGetChildrenKeys(nodeToUpdate, out IEnumerable initialChildrenKeys); - DocumentNavigationService.TryGetDescendantsKeys(nodeToUpdate, out IEnumerable initialDescendantsKeys); - DocumentNavigationService.TryGetAncestorsKeys(nodeToUpdate, out IEnumerable initialAncestorsKeys); - DocumentNavigationService.TryGetSiblingsKeys(nodeToUpdate, out IEnumerable initialSiblingsKeys); + DocumentNavigationQueryService.TryGetParentKey(nodeToUpdate, out Guid? initialParentKey); + DocumentNavigationQueryService.TryGetChildrenKeys(nodeToUpdate, out IEnumerable initialChildrenKeys); + DocumentNavigationQueryService.TryGetDescendantsKeys(nodeToUpdate, out IEnumerable initialDescendantsKeys); + DocumentNavigationQueryService.TryGetAncestorsKeys(nodeToUpdate, out IEnumerable initialAncestorsKeys); + DocumentNavigationQueryService.TryGetSiblingsKeys(nodeToUpdate, out IEnumerable initialSiblingsKeys); var updateModel = new ContentUpdateModel { @@ -29,11 +29,11 @@ public async Task Structure_Does_Not_Update_When_Updating_Content() Guid updatedItemKey = updateAttempt.Result.Content!.Key; // Capture updated state - var nodeExists = DocumentNavigationService.TryGetParentKey(updatedItemKey, out Guid? updatedParentKey); - DocumentNavigationService.TryGetChildrenKeys(updatedItemKey, out IEnumerable childrenKeysAfterUpdate); - DocumentNavigationService.TryGetDescendantsKeys(updatedItemKey, out IEnumerable descendantsKeysAfterUpdate); - DocumentNavigationService.TryGetAncestorsKeys(updatedItemKey, out IEnumerable ancestorsKeysAfterUpdate); - DocumentNavigationService.TryGetSiblingsKeys(updatedItemKey, out IEnumerable siblingsKeysAfterUpdate); + var nodeExists = DocumentNavigationQueryService.TryGetParentKey(updatedItemKey, out Guid? updatedParentKey); + DocumentNavigationQueryService.TryGetChildrenKeys(updatedItemKey, out IEnumerable childrenKeysAfterUpdate); + DocumentNavigationQueryService.TryGetDescendantsKeys(updatedItemKey, out IEnumerable descendantsKeysAfterUpdate); + DocumentNavigationQueryService.TryGetAncestorsKeys(updatedItemKey, out IEnumerable ancestorsKeysAfterUpdate); + DocumentNavigationQueryService.TryGetSiblingsKeys(updatedItemKey, out IEnumerable siblingsKeysAfterUpdate); // Assert Assert.Multiple(() => diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs index 7077d891dcc1..9fdedc52572e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs @@ -82,7 +82,7 @@ public async Task Structure_Does_Not_Update_When_Scope_Is_Not_Completed() } // Act - var nodeExists = DocumentNavigationService.TryGetParentKey(notCreatedRootKey, out _); + var nodeExists = DocumentNavigationQueryService.TryGetParentKey(notCreatedRootKey, out _); // Assert Assert.IsFalse(nodeExists); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs index 2b1abc7c84f9..d4325f4674cb 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs @@ -18,7 +18,7 @@ public abstract class DocumentNavigationServiceTestsBase : UmbracoIntegrationTes // Testing with IContentEditingService as it calls IContentService underneath protected IContentEditingService ContentEditingService => GetRequiredService(); - protected IDocumentNavigationService DocumentNavigationService => GetRequiredService(); + protected IDocumentNavigationQueryService DocumentNavigationQueryService => GetRequiredService(); protected IContentType ContentType { get; set; } 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 bdf5459d4922..0f36e8b8ec85 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs @@ -14,11 +14,11 @@ public async Task Structure_Can_Rebuild() Guid nodeKey = Album.Key; // Capture original built state of MediaNavigationService - MediaNavigationService.TryGetParentKey(nodeKey, out Guid? originalParentKey); - MediaNavigationService.TryGetChildrenKeys(nodeKey, out IEnumerable originalChildrenKeys); - MediaNavigationService.TryGetDescendantsKeys(nodeKey, out IEnumerable originalDescendantsKeys); - MediaNavigationService.TryGetAncestorsKeys(nodeKey, out IEnumerable originalAncestorsKeys); - MediaNavigationService.TryGetSiblingsKeys(nodeKey, out IEnumerable originalSiblingsKeys); + MediaNavigationQueryService.TryGetParentKey(nodeKey, out Guid? originalParentKey); + MediaNavigationQueryService.TryGetChildrenKeys(nodeKey, out IEnumerable originalChildrenKeys); + MediaNavigationQueryService.TryGetDescendantsKeys(nodeKey, out IEnumerable originalDescendantsKeys); + MediaNavigationQueryService.TryGetAncestorsKeys(nodeKey, out IEnumerable originalAncestorsKeys); + MediaNavigationQueryService.TryGetSiblingsKeys(nodeKey, out IEnumerable originalSiblingsKeys); // Im-memory navigation structure is empty here var newMediaNavigationService = new MediaNavigationService(GetRequiredService(), GetRequiredService()); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs index e7406a225c67..ebef7fe04657 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs @@ -71,7 +71,7 @@ public async Task Structure_Does_Not_Update_When_Scope_Is_Not_Completed() } // Act - var nodeExists = MediaNavigationService.TryGetParentKey(notCreatedAlbumKey, out _); + var nodeExists = MediaNavigationQueryService.TryGetParentKey(notCreatedAlbumKey, out _); // Assert Assert.IsFalse(nodeExists); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTestsBase.cs index c64998c3e07e..6d9e6933208c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTestsBase.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTestsBase.cs @@ -18,7 +18,7 @@ public abstract class MediaNavigationServiceTestsBase : UmbracoIntegrationTest // Testing with IMediaEditingService as it calls IMediaService underneath protected IMediaEditingService MediaEditingService => GetRequiredService(); - protected IMediaNavigationService MediaNavigationService => GetRequiredService(); + protected IMediaNavigationQueryService MediaNavigationQueryService => GetRequiredService(); protected IMediaType FolderMediaType { get; set; } From 08beb058a5545c1d7fda974bfa0776f22a0020aa Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 26 Aug 2024 14:03:26 +0200 Subject: [PATCH 84/96] Adding comments --- .../Services/Navigation/IRecycleBinNavigationQueryService.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs index 45396524a666..0a57f5346c02 100644 --- a/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs +++ b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs @@ -1,5 +1,9 @@ namespace Umbraco.Cms.Core.Services.Navigation; +/// +/// Placeholder for sharing logic between the document and media navigation services +/// for querying the recycle bin navigation structure. +/// public interface IRecycleBinNavigationQueryService { bool TryGetParentKeyInBin(Guid childKey, out Guid? parentKey); From bb149424ed1f6d1f6fd71cc1c1bc915a99b1b765 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 26 Aug 2024 14:04:14 +0200 Subject: [PATCH 85/96] Removing bin interfaces from main navigation ones --- .../Services/Navigation/INavigationManagementService.cs | 2 +- src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs index c22c9f4b9500..ad04b33e7007 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Core.Services.Navigation; /// Placeholder for sharing logic between the document and media navigation services /// for managing the navigation structure. /// -public interface INavigationManagementService : IRecycleBinNavigationManagementService +public interface INavigationManagementService { /// /// Rebuilds the entire navigation structure by refreshing the navigation tree based diff --git a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs index ca7f152b8716..4e28f80bb6cc 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Core.Services.Navigation; /// Placeholder for sharing logic between the document and media navigation services /// for querying the navigation structure. /// -public interface INavigationQueryService : IRecycleBinNavigationQueryService +public interface INavigationQueryService { bool TryGetParentKey(Guid childKey, out Guid? parentKey); From 7215edf1bc4265d501642e12246ec74f97fdd56d Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 26 Aug 2024 14:08:01 +0200 Subject: [PATCH 86/96] Rename Remove to MoveToBin --- .../ContentNavigationServiceBase.cs | 2 +- .../INavigationManagementService.cs | 2 +- .../ContentNavigationServiceBaseTests.cs | 30 +++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index b0d3f2552160..5a12045f63f0 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -58,7 +58,7 @@ public bool TryGetAncestorsKeysInBin(Guid childKey, out IEnumerable ancest public bool TryGetSiblingsKeysInBin(Guid key, out IEnumerable siblingsKeys) => TryGetSiblingsKeysFromStructure(_recycleBinNavigationStructure, key, out siblingsKeys); - public bool Remove(Guid key) + public bool MoveToBin(Guid key) { if (TryRemoveNodeFromParentInStructure(_navigationStructure, key, out NavigationNode? nodeToRemove) is false || nodeToRemove is null) { diff --git a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs index ad04b33e7007..4ab8458f18ef 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs @@ -21,7 +21,7 @@ public interface INavigationManagementService /// true if the node and its descendants were successfully removed from the /// main navigation structure and added to the recycle bin; otherwise, false. /// - bool Remove(Guid key); + bool MoveToBin(Guid key); /// /// Adds a new node to the main navigation structure. If a parent key is provided, diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs index 32d30a0c9021..7d9a2e8397ff 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs @@ -434,13 +434,13 @@ public void Can_Get_Siblings_Of_Existing_Content_Key_In_Correct_Order(Guid child } [Test] - public void Cannot_Remove_Node_With_Non_Existing_Content_Key() + public void Cannot_Move_Node_To_Bin_When_Non_Existing_Content_Key() { // Arrange var nonExistingKey = Guid.NewGuid(); // Act - var result = _navigationService.Remove(nonExistingKey); + var result = _navigationService.MoveToBin(nonExistingKey); // Assert Assert.IsFalse(result); @@ -456,10 +456,10 @@ public void Cannot_Remove_Node_With_Non_Existing_Content_Key() [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 - public void Can_Remove_Node(Guid keyOfNodeToRemove) + public void Can_Move_Node_To_Bin(Guid keyOfNodeToRemove) { // Act - var result = _navigationService.Remove(keyOfNodeToRemove); + var result = _navigationService.MoveToBin(keyOfNodeToRemove); // Assert Assert.IsTrue(result); @@ -477,13 +477,13 @@ public void Can_Remove_Node(Guid keyOfNodeToRemove) [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 - public void Removing_Node_Removes_Its_Descendants_As_Well(Guid keyOfNodeToRemove) + public void Moving_Node_To_Bin_Removes_Its_Descendants_As_Well(Guid keyOfNodeToRemove) { // Arrange _navigationService.TryGetDescendantsKeys(keyOfNodeToRemove, out IEnumerable initialDescendantsKeys); // Act - var result = _navigationService.Remove(keyOfNodeToRemove); + var result = _navigationService.MoveToBin(keyOfNodeToRemove); // Assert Assert.IsTrue(result); @@ -512,10 +512,10 @@ public void Removing_Node_Removes_Its_Descendants_As_Well(Guid keyOfNodeToRemove [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 - public void Removing_Node_Adds_It_To_Recycle_Bin_Root(Guid keyOfNodeToRemove) + public void Moving_Node_To_Bin_Adds_It_To_Recycle_Bin_Root(Guid keyOfNodeToRemove) { // Act - _navigationService.Remove(keyOfNodeToRemove); + _navigationService.MoveToBin(keyOfNodeToRemove); // Assert var nodeExistsInBin = _navigationService.TryGetParentKeyInBin(keyOfNodeToRemove, out Guid? parentKeyInBin); @@ -532,14 +532,14 @@ public void Removing_Node_Adds_It_To_Recycle_Bin_Root(Guid keyOfNodeToRemove) [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 - public void Removing_Node_Adds_Its_Descendants_To_Recycle_Bin_As_Well(Guid keyOfNodeToRemove) + public void Moving_Node_To_Bin_Adds_Its_Descendants_To_Recycle_Bin_As_Well(Guid keyOfNodeToRemove) { // Arrange _navigationService.TryGetDescendantsKeys(keyOfNodeToRemove, out IEnumerable initialDescendantsKeys); List initialDescendantsList = initialDescendantsKeys.ToList(); // Act - _navigationService.Remove(keyOfNodeToRemove); + _navigationService.MoveToBin(keyOfNodeToRemove); // Assert var nodeExistsInBin = _navigationService.TryGetDescendantsKeysInBin(keyOfNodeToRemove, out IEnumerable descendantsKeysInBin); @@ -840,7 +840,7 @@ public void Cannot_Restore_Node_When_Target_Parent_Does_Not_Exist() // Arrange Guid nodeToRestore = Grandchild1; var nonExistentTargetParentKey = Guid.NewGuid(); - _navigationService.Remove(nodeToRestore); + _navigationService.MoveToBin(nodeToRestore); // Act var result = _navigationService.RestoreFromBin(nodeToRestore, nonExistentTargetParentKey); @@ -876,7 +876,7 @@ public void Cannot_Restore_Node_That_Does_Not_Exist() public void Can_Restore_Node_To_Existing_Target_Parent(Guid nodeToRestore, Guid? targetParentKey) { // Arrange - _navigationService.Remove(nodeToRestore); + _navigationService.MoveToBin(nodeToRestore); // Act var result = _navigationService.RestoreFromBin(nodeToRestore, targetParentKey); @@ -909,7 +909,7 @@ public void Can_Restore_Node_To_Existing_Target_Parent(Guid nodeToRestore, Guid? public void Restored_Node_Is_Added_To_Its_Target_Parent(Guid nodeToRestore, Guid targetParentKey) { // Arrange - _navigationService.Remove(nodeToRestore); + _navigationService.MoveToBin(nodeToRestore); _navigationService.TryGetChildrenKeys(targetParentKey, out IEnumerable targetParentChildrenKeys); var targetParentChildrenCount = targetParentChildrenKeys.Count(); @@ -938,7 +938,7 @@ public void Restored_Node_Is_Added_To_Its_Target_Parent(Guid nodeToRestore, Guid public void Restored_Node_And_Its_Descendants_Are_Removed_From_Bin(Guid nodeToRestore) { // Arrange - _navigationService.Remove(nodeToRestore); + _navigationService.MoveToBin(nodeToRestore); _navigationService.TryGetDescendantsKeysInBin(nodeToRestore, out IEnumerable descendantsKeysInBin); // Act @@ -967,7 +967,7 @@ public void Restored_Node_And_Its_Descendants_Are_Removed_From_Bin(Guid nodeToRe public void Restored_Node_Has_The_Same_Amount_Of_Descendants(Guid nodeToRestore, Guid? targetParentKey, int initialDescendantsCount) { // Arrange - _navigationService.Remove(nodeToRestore); + _navigationService.MoveToBin(nodeToRestore); // Act _navigationService.RestoreFromBin(nodeToRestore, targetParentKey); From 6851113cf11f633c70ad4a01f1d1fe1cdbe56f2c Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Thu, 29 Aug 2024 08:47:24 +0200 Subject: [PATCH 87/96] V14 QA added block list editor tests (#16862) * Added tests for blocklistEditor * Added more tets * Removed faker * Added blockTest * Updates * Added tests * Removed dependencies * Fixes * Clean up * Fixed naming * Cleaned up * Bumped version * Added missing semicolons * Added tags * Only runs the new tests * Updates * Bumped version * Fixed tests * Cleaned up * Updated version * Fixes, not done * Fixed tests * Bumped helpers * Bumped helpers * Fixed conflict * Fixed comment * Reverted to run smokeTests * Updated helpers --- .../package-lock.json | 759 +----------------- .../Umbraco.Tests.AcceptanceTest/package.json | 10 +- .../BlockListEditor/BlockListBlocks.spec.ts | 419 ++++++++++ .../BlockListEditor/BlockListEditor.spec.ts | 311 +++++++ 4 files changed, 758 insertions(+), 741 deletions(-) create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListBlocks.spec.ts create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index f7103ba5a478..d3d88edf6068 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -11,20 +11,14 @@ "@umbraco/playwright-testhelpers": "^2.0.0-beta.78", "camelize": "^1.0.0", "dotenv": "^16.3.1", - "faker": "^4.1.0", - "form-data": "^4.0.0", - "node-fetch": "^2.6.7", - "xhr2": "^0.2.1" + "node-fetch": "^2.6.7" }, "devDependencies": { "@playwright/test": "^1.43", "@types/node": "^20.9.0", - "del": "^6.0.0", - "ncp": "^2.0.0", "prompt": "^1.2.0", "tslib": "^2.4.0", - "typescript": "^4.8.3", - "wait-on": "^7.2.0" + "typescript": "^4.8.3" } }, "node_modules/@colors/colors": { @@ -36,96 +30,25 @@ "node": ">=0.1.90" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "dev": true - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@playwright/test": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz", - "integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz", + "integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==", "dev": true, "dependencies": { - "playwright": "1.43.1" + "playwright": "1.46.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" - } - }, - "node_modules/@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^9.0.0" + "node": ">=18" } }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "dev": true - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "dev": true - }, "node_modules/@types/node": { - "version": "20.10.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.7.tgz", - "integrity": "sha512-fRbIKb8C/Y2lXxB5eVMj4IU7xpdox0Lh8bUPEdtLysaylsml1hOOx1+STloRs/B9nf7C6kPRmmg/V7aQW7usNg==", + "version": "20.14.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz", + "integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -148,78 +71,12 @@ "node-fetch": "^2.6.7" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/async": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", "dev": true }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", - "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", - "dev": true, - "dependencies": { - "follow-redirects": "^1.15.4", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/camelize": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", @@ -228,15 +85,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/colors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", @@ -246,23 +94,6 @@ "node": ">=0.1.90" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, "node_modules/cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", @@ -272,57 +103,15 @@ "node": ">=0.4.0" } }, - "node_modules/del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", - "dev": true, - "dependencies": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" } }, "node_modules/eyes": { @@ -334,329 +123,24 @@ "node": "> 0.1.90" } }, - "node_modules/faker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", - "integrity": "sha512-ILKg69P6y/D8/wSmDXw35Ly0re8QzQ8pMfBCflsGiZG2ZjMUNLYNexA6lz5pkmJlepVdsiDFUxYAzPQ9/+iGLA==" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", - "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, - "node_modules/joi": { - "version": "17.11.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", - "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, - "node_modules/ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", - "dev": true, - "bin": { - "ncp": "bin/ncp" - } - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -676,88 +160,34 @@ } } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/playwright": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz", - "integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz", + "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==", "dev": true, "dependencies": { - "playwright-core": "1.43.1" + "playwright-core": "1.46.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz", - "integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz", + "integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==", "dev": true, "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/prompt": { @@ -776,32 +206,6 @@ "node": ">= 6.0.0" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", @@ -814,16 +218,6 @@ "node": ">=0.8" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/revalidator": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", @@ -833,62 +227,6 @@ "node": ">= 0.4.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -898,27 +236,15 @@ "node": "*" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true }, "node_modules/typescript": { @@ -940,25 +266,6 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, - "node_modules/wait-on": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz", - "integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==", - "dev": true, - "dependencies": { - "axios": "^1.6.1", - "joi": "^17.11.0", - "lodash": "^4.17.21", - "minimist": "^1.2.8", - "rxjs": "^7.8.1" - }, - "bin": { - "wait-on": "bin/wait-on" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -998,20 +305,6 @@ "dependencies": { "lodash": "^4.17.14" } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/xhr2": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", - "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==", - "engines": { - "node": ">= 6" - } } } } diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index d6877d558192..170bdac49134 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -13,21 +13,15 @@ "devDependencies": { "@playwright/test": "^1.43", "@types/node": "^20.9.0", - "del": "^6.0.0", - "ncp": "^2.0.0", "prompt": "^1.2.0", "tslib": "^2.4.0", - "typescript": "^4.8.3", - "wait-on": "^7.2.0" + "typescript": "^4.8.3" }, "dependencies": { "@umbraco/json-models-builders": "^2.0.17", "@umbraco/playwright-testhelpers": "^2.0.0-beta.78", "camelize": "^1.0.0", "dotenv": "^16.3.1", - "faker": "^4.1.0", - "form-data": "^4.0.0", - "node-fetch": "^2.6.7", - "xhr2": "^0.2.1" + "node-fetch": "^2.6.7" } } diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListBlocks.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListBlocks.spec.ts new file mode 100644 index 000000000000..737cc34e9f55 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListBlocks.spec.ts @@ -0,0 +1,419 @@ +import {test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const blockListEditorName = 'TestBlockListEditor'; +const elementTypeName = 'BlockListElement'; +const dataTypeName = 'Textstring'; +const groupName = 'testGroup'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockListEditorName); + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSettingsTreeItem('Data Types'); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockListEditorName); +}); + +test('can add a label to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const labelText = 'ThisIsALabel'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.enterBlockLabelText(labelText); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockListEditorName, elementTypeId, labelText)).toBeTruthy(); +}); + +test('can update a label for a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const labelText = 'ThisIsALabel'; + const newLabelText = 'ThisIsANewLabel'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithEditorAppearance(blockListEditorName, elementTypeId, labelText); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockListEditorName, elementTypeId, labelText)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.enterBlockLabelText(newLabelText); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockListEditorName, elementTypeId, newLabelText)).toBeTruthy(); +}); + +test('can remove a label from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const labelText = 'ThisIsALabel'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithEditorAppearance(blockListEditorName, elementTypeId, labelText); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockListEditorName, elementTypeId, labelText)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.enterBlockLabelText(""); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockListEditorName, elementTypeId, "")).toBeTruthy(); +}); + +test('can update overlay size for a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const overlaySize = 'medium'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithEditorAppearance(blockListEditorName, elementTypeId, ""); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.updateBlockOverlaySize(overlaySize); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].editorSize).toEqual(overlaySize); +}); + +test('can open content model in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.openBlockContentModel(); + + // Assert + await umbracoUi.dataType.isElementWorkspaceOpenInBlock(elementTypeName); +}); + +// TODO: Is this an issue? should you be able to remove the contentModel so you have none? +// There is currently frontend issues +test.skip('can remove a content model from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.removeBlockContentModel(); + await umbracoUi.dataType.clickConfirmRemoveButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const blockData = await umbracoApi.dataType.getByName(blockListEditorName); +}); + +test('can add a settings model to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementName = 'SecondElementTest'; + const settingsElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.addBlockSettingsModel(secondElementName); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithSettingsTypeIds(blockListEditorName, [settingsElementTypeId])).toBeTruthy(); +}); + +test('can remove a settings model from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementName = 'SecondElementTest'; + const settingsElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithContentAndSettingsElementType(blockListEditorName, contentElementTypeId, settingsElementTypeId); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithSettingsTypeIds(blockListEditorName, [settingsElementTypeId])).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.removeBlockSettingsModel(); + await umbracoUi.dataType.clickConfirmRemoveButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithSettingsTypeIds(blockListEditorName, [settingsElementTypeId])).toBeFalsy(); +}); + +test('can add a background color to a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const backgroundColor = '#ff0000'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockBackgroundColor(backgroundColor); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].backgroundColor).toEqual(backgroundColor); +}); + +test('can update a background color for a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const backgroundColor = '#ff0000'; + const newBackgroundColor = '#ff4444'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, backgroundColor); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].backgroundColor).toEqual(backgroundColor); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockBackgroundColor(newBackgroundColor); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].backgroundColor).toEqual(newBackgroundColor); +}); + +test('can delete a background color from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const backgroundColor = '#ff0000'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, backgroundColor); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].backgroundColor).toEqual(backgroundColor); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockBackgroundColor(''); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].backgroundColor).toEqual(''); +}); + +test('can add a icon color to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconColor = '#ff0000'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockIconColor(iconColor); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].iconColor).toEqual(iconColor); +}); + +test('can update a icon color for a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconColor = '#ff0000'; + const newIconColor = '#ff4444'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, "", iconColor); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].iconColor).toEqual(iconColor); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockIconColor(newIconColor); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].iconColor).toEqual(newIconColor); +}); + +test('can delete a icon color from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconColor = '#ff0000'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, '', iconColor); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].iconColor).toEqual(iconColor); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockIconColor(''); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].iconColor).toEqual(''); +}); + +// TODO: Currently it is not possible to update a stylesheet to a block +test.skip('can update a custom stylesheet for a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const stylesheetName = 'TestStylesheet.css'; + const stylesheetPath = '/wwwroot/css/' + stylesheetName; + const encodedStylesheetPath = await umbracoApi.stylesheet.encodeStylesheetPath(stylesheetPath); + const secondStylesheetName = 'SecondStylesheet.css'; + const secondStylesheetPath = '/wwwroot/css/' + secondStylesheetName; + const encodedSecondStylesheetPath = await umbracoApi.stylesheet.encodeStylesheetPath(secondStylesheetPath); + await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); + await umbracoApi.stylesheet.ensureNameNotExists(secondStylesheetName); + await umbracoApi.stylesheet.createDefaultStylesheet(stylesheetName); + await umbracoApi.stylesheet.createDefaultStylesheet(secondStylesheetName); + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, '', '', encodedStylesheetPath); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + // Removes first stylesheet + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].stylesheet[0]).toEqual(encodedSecondStylesheetPath); + + // Clean + await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); + await umbracoApi.stylesheet.ensureNameNotExists(secondStylesheetName); +}); + +// TODO: Currently it is not possible to delete a stylesheet to a block +test.skip('can delete a custom stylesheet from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const stylesheetName = 'TestStylesheet.css'; + const stylesheetPath = '/wwwroot/css/' + stylesheetName; + const encodedStylesheetPath = await umbracoApi.stylesheet.encodeStylesheetPath(stylesheetPath); + await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); + await umbracoApi.stylesheet.createDefaultStylesheet(stylesheetName); + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, '', '', encodedStylesheetPath); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].stylesheet[0]).toEqual(encodedStylesheetPath); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickRemoveCustomStylesheetWithName(stylesheetName); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].stylesheet[0]).toBeUndefined(); + + // Clean + await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); +}); + +test('can enable hide content editor in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickBlockListHideContentEditorButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].forceHideContentEditorInOverlay).toEqual(true); +}); + +test('can disable hide content editor in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithHideContentEditor(blockListEditorName, contentElementTypeId, true); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].forceHideContentEditorInOverlay).toEqual(true); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickBlockListHideContentEditorButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].forceHideContentEditorInOverlay).toEqual(false); +}); + +// TODO: Thumbnails are not showing in the UI +test.skip('can add a thumbnail to a block ', {tag: '@smoke'}, async ({page, umbracoApi, umbracoUi}) => { + +}); + +// TODO: Thumbnails are not showing in the UI +test.skip('can remove a thumbnail to a block ', {tag: '@smoke'}, async ({page, umbracoApi, umbracoUi}) => { + +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts new file mode 100644 index 000000000000..24142bca82e6 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts @@ -0,0 +1,311 @@ +import {test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const blockListEditorName = 'TestBlockListEditor'; +const elementTypeName = 'BlockListElement'; +const dataTypeName = 'Textstring'; +const groupName = 'testGroup'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockListEditorName); + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSettingsTreeItem('Data Types'); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockListEditorName); +}); + +test('can create a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const blockListLocatorName = 'Block List'; + const blockListEditorAlias = 'Umbraco.BlockList'; + const blockListEditorUiAlias = 'Umb.PropertyEditorUi.BlockList'; + + // Act + await umbracoUi.dataType.clickActionsMenuAtRoot(); + await umbracoUi.dataType.clickCreateButton(); + await umbracoUi.dataType.clickNewDataTypeThreeDotsButton(); + await umbracoUi.dataType.enterDataTypeName(blockListEditorName); + await umbracoUi.dataType.clickSelectAPropertyEditorButton(); + await umbracoUi.dataType.selectAPropertyEditor(blockListLocatorName); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesNameExist(blockListEditorName)).toBeTruthy(); + const dataTypeData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(dataTypeData.editorAlias).toBe(blockListEditorAlias); + expect(dataTypeData.editorUiAlias).toBe(blockListEditorUiAlias); +}); + +test('can rename a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const wrongName = 'BlockGridEditorTest'; + await umbracoApi.dataType.createEmptyBlockListDataType(wrongName); + + // Act + await umbracoUi.dataType.goToDataType(wrongName); + await umbracoUi.dataType.enterDataTypeName(blockListEditorName); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesNameExist(blockListEditorName)).toBeTruthy(); + expect(await umbracoApi.dataType.doesNameExist(wrongName)).toBeFalsy(); +}); + +test('can delete a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const blockListId = await umbracoApi.dataType.createEmptyBlockListDataType(blockListEditorName); + + // Act + await umbracoUi.dataType.clickRootFolderCaretButton(); + await umbracoUi.dataType.clickActionsMenuForDataType(blockListEditorName); + await umbracoUi.dataType.clickDeleteExactButton(); + await umbracoUi.dataType.clickConfirmToDeleteButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesExist(blockListId)).toBeFalsy(); + await umbracoUi.dataType.isTreeItemVisible(blockListEditorName, false); +}); + +test('can add a block to a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, 'testGroup', dataTypeName, textStringData.id); + await umbracoApi.dataType.createEmptyBlockListDataType(blockListEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickAddBlockButton(); + await umbracoUi.dataType.clickLabelWithName(elementTypeName); + await umbracoUi.dataType.clickChooseButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockListEditorName, [elementTypeId])).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); +}); + +test('can add multiple blocks to a block list editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const secondElementTypeName = 'SecondBlockListElement'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickAddBlockButton(); + await umbracoUi.dataType.clickLabelWithName(secondElementTypeName); + await umbracoUi.dataType.clickChooseButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockListEditorName, [elementTypeId, secondElementTypeId])).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); + await umbracoApi.documentType.ensureNameNotExists(secondElementTypeName); +}); + +test('can remove a block from a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickRemoveBlockWithName(elementTypeName); + await umbracoUi.dataType.clickConfirmRemoveButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockListEditorName, [elementTypeId])).toBeFalsy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); +}); + +test('can add a min and max amount to a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const minAmount = 1; + const maxAmount = 2; + await umbracoApi.dataType.createEmptyBlockListDataType(blockListEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.enterMinAmount(minAmount.toString()); + await umbracoUi.dataType.enterMaxAmount(maxAmount.toString()); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const dataTypeData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(dataTypeData.values[0].value.min).toBe(minAmount); + expect(dataTypeData.values[0].value.max).toBe(maxAmount); +}); + +test('max can not be less than min', async ({umbracoApi, umbracoUi}) => { + // Arrange + const minAmount = 2; + const oldMaxAmount = 2; + const newMaxAmount = 1; + await umbracoApi.dataType.createBlockListDataTypeWithMinAndMaxAmount(blockListEditorName, minAmount, oldMaxAmount); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.enterMaxAmount(newMaxAmount.toString()); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(false); + const dataTypeData = await umbracoApi.dataType.getByName(blockListEditorName); + await umbracoUi.dataType.doesAmountContainErrorMessageWithText('The low value must not be exceed the high value'); + expect(dataTypeData.values[0].value.min).toBe(minAmount); + // The max value should not be updated + expect(dataTypeData.values[0].value.max).toBe(oldMaxAmount); +}); + +test('can enable single block mode', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockListDataTypeWithSingleBlockMode(blockListEditorName, false); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickSingleBlockMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isSingleBlockModeEnabledForBlockList(blockListEditorName, true)).toBeTruthy(); +}); + +test('can disable single block mode', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockListDataTypeWithSingleBlockMode(blockListEditorName, true); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickSingleBlockMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isSingleBlockModeEnabledForBlockList(blockListEditorName, false)).toBeTruthy(); +}); + +test('can enable live editing mode', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockListDataTypeWithLiveEditingMode(blockListEditorName, false); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickLiveEditingMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isLiveEditingModeEnabledForBlockEditor(blockListEditorName, true)).toBeTruthy(); +}); + +test('can disable live editing mode', async ({umbracoApi, umbracoUi}) => { +// Arrange + await umbracoApi.dataType.createBlockListDataTypeWithLiveEditingMode(blockListEditorName, true); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickLiveEditingMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isLiveEditingModeEnabledForBlockEditor(blockListEditorName, false)).toBeTruthy(); +}); + +test('can enable inline editing mode', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockListDataTypeWithInlineEditingMode(blockListEditorName, false); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickInlineEditingMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isInlineEditingModeEnabledForBlockList(blockListEditorName, true)).toBeTruthy(); +}); + +test('can disable inline editing mode', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockListDataTypeWithInlineEditingMode(blockListEditorName, true); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickInlineEditingMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isInlineEditingModeEnabledForBlockList(blockListEditorName, false)).toBeTruthy(); +}); + +test('can add a property editor width', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const propertyWidth = '50%'; + await umbracoApi.dataType.createEmptyBlockListDataType(blockListEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.enterPropertyEditorWidth(propertyWidth); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockListEditorName, propertyWidth)).toBeTruthy(); +}); + +test('can update a property editor width', async ({umbracoApi, umbracoUi}) => { + // Arrange + const oldPropertyWidth = '50%'; + const newPropertyWidth = '100%'; + await umbracoApi.dataType.createBlockListDataTypeWithPropertyEditorWidth(blockListEditorName, oldPropertyWidth); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockListEditorName, oldPropertyWidth)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.enterPropertyEditorWidth(newPropertyWidth); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockListEditorName, newPropertyWidth)).toBeTruthy(); +}); + +test('can remove a property editor width', async ({umbracoApi, umbracoUi}) => { + // Arrange + const propertyWidth = '50%'; + await umbracoApi.dataType.createBlockListDataTypeWithPropertyEditorWidth(blockListEditorName, propertyWidth); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockListEditorName, propertyWidth)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.enterPropertyEditorWidth(''); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockListEditorName, '')).toBeTruthy(); +}); From c277005b62dd9ea143670720da72a9df7c905bd6 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Thu, 29 Aug 2024 10:12:43 +0200 Subject: [PATCH 88/96] improve missingProperties data returned for missing propertie values (#16910) Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --- .../Content/ContentControllerBase.cs | 19 ++++++++++++++++--- .../PropertyValidationResponseModel.cs | 12 ++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Content/PropertyValidationResponseModel.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs index a2277820bfbe..c008dad102d3 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.ContentEditing.Validation; using Umbraco.Cms.Core.Services.OperationStatus; @@ -9,6 +11,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Content; public abstract class ContentControllerBase : ManagementApiControllerBase { + protected IActionResult ContentEditingOperationStatusResult(ContentEditingOperationStatus status) => OperationStatusResult(status, problemDetailsBuilder => status switch { @@ -96,7 +99,8 @@ protected IActionResult ContentEditingOperationStatusResult(); - var missingPropertyAliases = new List(); + + var missingPropertyModels = new List(); foreach (PropertyValidationError validationError in validationResult.ValidationErrors) { TValueModel? requestValue = requestModel.Values.FirstOrDefault(value => @@ -105,7 +109,7 @@ protected IActionResult ContentEditingOperationStatusResult + new() + { + Alias = source.Alias, + Segment = source.Segment, + Culture = source.Culture, + Messages = source.ErrorMessages, + }; } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Content/PropertyValidationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Content/PropertyValidationResponseModel.cs new file mode 100644 index 000000000000..6f8d918c3e8c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Content/PropertyValidationResponseModel.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Content; + +public class PropertyValidationResponseModel +{ + public string[] Messages { get; set; } = Array.Empty(); + + public string Alias { get; set; } = string.Empty; + + public string? Culture { get; set; } + + public string? Segment { get; set; } +} From 590b28110b4135d141112dd0f56bb5fcb4080bf5 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 29 Aug 2024 11:38:24 +0200 Subject: [PATCH 89/96] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index a9d3a4396968..a2fc54b77e99 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit a9d3a4396968e4cc47c1d1cd290ca8b1cf764e12 +Subproject commit a2fc54b77e99de28a0669ab628ecfd7983df7ad8 From 21d2bb67381862e11e6b916da62c6ee4bfb40496 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 2 Sep 2024 10:48:43 +0200 Subject: [PATCH 90/96] Rename initialization service to initialization hosted service --- ...ionService.cs => NavigationInitializationHostedService.cs} | 4 ++-- .../DependencyInjection/UmbracoBuilderExtensions.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/Umbraco.Core/Services/Navigation/{NavigationInitializationService.cs => NavigationInitializationHostedService.cs} (83%) diff --git a/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs b/src/Umbraco.Core/Services/Navigation/NavigationInitializationHostedService.cs similarity index 83% rename from src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs rename to src/Umbraco.Core/Services/Navigation/NavigationInitializationHostedService.cs index aa972e84b65c..274158d40d8a 100644 --- a/src/Umbraco.Core/Services/Navigation/NavigationInitializationService.cs +++ b/src/Umbraco.Core/Services/Navigation/NavigationInitializationHostedService.cs @@ -6,12 +6,12 @@ namespace Umbraco.Cms.Core.Services.Navigation; /// Responsible for seeding the in-memory navigation structures at application's startup /// by rebuild the navigation structures. /// -public sealed class NavigationInitializationService : IHostedLifecycleService +public sealed class NavigationInitializationHostedService : IHostedLifecycleService { private readonly IDocumentNavigationManagementService _documentNavigationManagementService; private readonly IMediaNavigationManagementService _mediaNavigationManagementService; - public NavigationInitializationService(IDocumentNavigationManagementService documentNavigationManagementService, IMediaNavigationManagementService mediaNavigationManagementService) + public NavigationInitializationHostedService(IDocumentNavigationManagementService documentNavigationManagementService, IMediaNavigationManagementService mediaNavigationManagementService) { _documentNavigationManagementService = documentNavigationManagementService; _mediaNavigationManagementService = mediaNavigationManagementService; diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 1bab0883a516..ea404d9703fa 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -194,7 +194,7 @@ public static IUmbracoBuilder AddRecurringBackgroundJobs(this IUmbracoBuilder bu builder.Services.AddSingleton(RecurringBackgroundJobHostedService.CreateHostedServiceFactory); builder.Services.AddHostedService(); builder.Services.AddHostedService(); - builder.Services.AddHostedService(); + builder.Services.AddHostedService(); return builder; } From b3f32aaf1db21772750dc47c1ab9d4ec5c9cfe00 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 2 Sep 2024 15:26:46 +0200 Subject: [PATCH 91/96] Refactor repository to return a collection --- .../Repositories/INavigationRepository.cs | 15 +++++++------- .../Implement/ContentNavigationRepository.cs | 20 ++++++------------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs index cd7def878e85..cc65d637a8b4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs @@ -1,21 +1,20 @@ -using System.Collections.Concurrent; -using Umbraco.Cms.Core.Models.Navigation; +using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Persistence.Repositories; public interface INavigationRepository { /// - /// Retrieves a dictionary of content nodes based on the object type key. + /// Retrieves a collection of content nodes as navigation models based on the object type key. /// /// The unique identifier for the object type. - /// A dictionary of navigation nodes where the key is the unique identifier of the node. - public ConcurrentDictionary GetContentNodesByObjectType(Guid objectTypeKey); + /// A collection of navigation models. + IEnumerable GetContentNodesByObjectType(Guid objectTypeKey); /// - /// Retrieves a dictionary of trashed content nodes based on the object type key. + /// Retrieves a collection of trashed content nodes as navigation models based on the object type key. /// /// The unique identifier for the object type. - /// A dictionary of navigation nodes where the key is the unique identifier of the node. - public ConcurrentDictionary GetTrashedContentNodesByObjectType(Guid objectTypeKey); + /// A collection of navigation models. + IEnumerable GetTrashedContentNodesByObjectType(Guid objectTypeKey); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs index 74d25cdf3668..2f86d0014350 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs @@ -1,9 +1,7 @@ -using System.Collections.Concurrent; using NPoco; -using Umbraco.Cms.Core.Models.Navigation; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; @@ -19,20 +17,14 @@ public ContentNavigationRepository(IScopeAccessor scopeAccessor) private IScope? AmbientScope => _scopeAccessor.AmbientScope; /// - public ConcurrentDictionary GetContentNodesByObjectType(Guid objectTypeKey) - { - IEnumerable navigationDtos = FetchNavigationDtos(objectTypeKey, false); - return NavigationFactory.BuildNavigationDictionary(navigationDtos); - } + public IEnumerable GetContentNodesByObjectType(Guid objectTypeKey) + => FetchNavigationDtos(objectTypeKey, false); /// - public ConcurrentDictionary GetTrashedContentNodesByObjectType(Guid objectTypeKey) - { - IEnumerable navigationDtos = FetchNavigationDtos(objectTypeKey, true); - return NavigationFactory.BuildNavigationDictionary(navigationDtos); - } + public IEnumerable GetTrashedContentNodesByObjectType(Guid objectTypeKey) + => FetchNavigationDtos(objectTypeKey, true); - private IEnumerable FetchNavigationDtos(Guid objectTypeKey, bool trashed) + private IEnumerable FetchNavigationDtos(Guid objectTypeKey, bool trashed) { Sql? sql = AmbientScope?.SqlContext.Sql() .Select() From 2694f3abf02e681a7a9cf1933767ea9ec3648746 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 2 Sep 2024 15:27:39 +0200 Subject: [PATCH 92/96] Add interface for the NavigationDto --- src/Umbraco.Core/Models/INavigationModel.cs | 24 +++++++++++++++++++ .../Persistence/Dtos/NavigationDto.cs | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Core/Models/INavigationModel.cs diff --git a/src/Umbraco.Core/Models/INavigationModel.cs b/src/Umbraco.Core/Models/INavigationModel.cs new file mode 100644 index 000000000000..bc33e22f0fca --- /dev/null +++ b/src/Umbraco.Core/Models/INavigationModel.cs @@ -0,0 +1,24 @@ +namespace Umbraco.Cms.Core.Models; + +public interface INavigationModel +{ + /// + /// Gets or sets the integer identifier of the entity. + /// + int Id { get; set; } + + /// + /// Gets or sets the Guid unique identifier of the entity. + /// + Guid Key { get; set; } + + /// + /// Gets or sets the integer identifier of the parent entity. + /// + int ParentId { get; set; } + + /// + /// Gets or sets a value indicating whether this entity is in the recycle bin. + /// + bool Trashed { get; set; } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs index 6cdab4870de0..4bf04ce5a51c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs @@ -1,10 +1,11 @@ using NPoco; +using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; // Used internally for representing the data needed for constructing the in-memory navigation structure. [TableName(NodeDto.TableName)] -internal class NavigationDto +internal class NavigationDto : INavigationModel { /// /// Gets the integer identifier of the entity. From 6d5a0f9adaf591891b63adaf802be38277ffc184 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 2 Sep 2024 15:28:10 +0200 Subject: [PATCH 93/96] Add constants to bind property names between DTOs --- .../Persistence/Dtos/NodeDto.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs index 5bf3a2620789..2ac62429bad6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs @@ -12,19 +12,26 @@ public class NodeDto { public const string TableName = Constants.DatabaseSchema.Tables.Node; public const int NodeIdSeed = 1060; + + // Public constants to bind properties between DTOs + public const string IdColumnName = "id"; + public const string KeyColumnName = "uniqueId"; + public const string ParentIdColumnName = "parentId"; + public const string TrashedColumnName = "trashed"; + private int? _userId; - [Column("id")] + [Column(IdColumnName)] [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] public int NodeId { get; set; } - [Column("uniqueId")] + [Column(KeyColumnName)] [NullSetting(NullSetting = NullSettings.NotNull)] [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_UniqueId", IncludeColumns = "parentId,level,path,sortOrder,trashed,nodeUser,text,createDate")] [Constraint(Default = SystemMethods.NewGuid)] public Guid UniqueId { get; set; } - [Column("parentId")] + [Column(ParentIdColumnName)] [ForeignKey(typeof(NodeDto))] [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_parentId_nodeObjectType", ForColumns = "parentID,nodeObjectType", IncludeColumns = "trashed,nodeUser,level,path,sortOrder,uniqueID,text,createDate")] public int ParentId { get; set; } @@ -43,7 +50,7 @@ public class NodeDto [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType_trashed_sorted", ForColumns = "nodeObjectType,trashed,sortOrder,id", IncludeColumns = "uniqueID,parentID,level,path,nodeUser,text,createDate")] public int SortOrder { get; set; } - [Column("trashed")] + [Column(TrashedColumnName)] [Constraint(Default = "0")] [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Trashed")] public bool Trashed { get; set; } From d5c3dc2f93cb9fa14c7ba24712c67c91a4565e55 Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 2 Sep 2024 15:28:32 +0200 Subject: [PATCH 94/96] Move factory and fix input type --- .../Factories/NavigationFactory.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) rename src/{Umbraco.Infrastructure/Persistence => Umbraco.Core}/Factories/NavigationFactory.cs (56%) diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs b/src/Umbraco.Core/Factories/NavigationFactory.cs similarity index 56% rename from src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs rename to src/Umbraco.Core/Factories/NavigationFactory.cs index e510ecfe1216..316c6031d692 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/NavigationFactory.cs +++ b/src/Umbraco.Core/Factories/NavigationFactory.cs @@ -1,34 +1,34 @@ using System.Collections.Concurrent; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Navigation; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories; +namespace Umbraco.Cms.Core.Factories; internal static class NavigationFactory { /// /// Builds a dictionary of NavigationNode objects from a given dataset. /// - /// The dataset of objects used to build the navigation nodes dictionary. - /// A dictionary of objects with key corresponding to their unique guid. - public static ConcurrentDictionary BuildNavigationDictionary(IEnumerable dataset) + /// The objects used to build the navigation nodes dictionary. + /// A dictionary of objects with key corresponding to their unique Guid. + public static ConcurrentDictionary BuildNavigationDictionary(IEnumerable entities) { var nodesStructure = new ConcurrentDictionary(); - var datasetList = dataset.ToList(); - var idToKeyMap = datasetList.ToDictionary(x => x.Id, x => x.Key); + var entityList = entities.ToList(); + var idToKeyMap = entityList.ToDictionary(x => x.Id, x => x.Key); - foreach (NavigationDto dto in datasetList) + foreach (INavigationModel entity in entityList) { - var node = new NavigationNode(dto.Key); - nodesStructure[dto.Key] = node; + var node = new NavigationNode(entity.Key); + nodesStructure[entity.Key] = node; // We don't set the parent for items under root, it will stay null - if (dto.ParentId == -1) + if (entity.ParentId == -1) { continue; } - if (idToKeyMap.TryGetValue(dto.ParentId, out Guid parentKey) is false) + if (idToKeyMap.TryGetValue(entity.ParentId, out Guid parentKey) is false) { continue; } From 7bde9beeb910187d77e7ec0a505767f52d304a2a Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 2 Sep 2024 15:28:43 +0200 Subject: [PATCH 95/96] Use constants for column names --- .../Persistence/Dtos/NavigationDto.cs | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs index 4bf04ce5a51c..156a85b19cfc 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs @@ -7,27 +7,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; [TableName(NodeDto.TableName)] internal class NavigationDto : INavigationModel { - /// - /// Gets the integer identifier of the entity. - /// - [Column("id")] + /// + [Column(NodeDto.IdColumnName)] public int Id { get; set; } - /// - /// Gets the Guid unique identifier of the entity. - /// - [Column("uniqueId")] + /// + [Column(NodeDto.KeyColumnName)] public Guid Key { get; set; } - /// - /// Gets the integer identifier of the parent entity. - /// - [Column("parentId")] + /// + [Column(NodeDto.ParentIdColumnName)] public int ParentId { get; set; } - /// - /// Gets a value indicating whether this entity is in the recycle bin. - /// - [Column("trashed")] + /// + [Column(NodeDto.TrashedColumnName)] public bool Trashed { get; set; } } From 1378307cf3ef3dde93ca708f680ca554ac4b2d7e Mon Sep 17 00:00:00 2001 From: Elitsa Date: Mon, 2 Sep 2024 15:29:59 +0200 Subject: [PATCH 96/96] Use factory from base --- .../Navigation/ContentNavigationServiceBase.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index 5a12045f63f0..e5755c8d8775 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -1,4 +1,6 @@ using System.Collections.Concurrent; +using Umbraco.Cms.Core.Factories; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Navigation; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; @@ -178,14 +180,11 @@ protected async Task HandleRebuildAsync(int readLock, Guid objectTypeKey, bool t using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); scope.ReadLock(readLock); - if (trashed) - { - _recycleBinNavigationStructure = _navigationRepository.GetTrashedContentNodesByObjectType(objectTypeKey); - } - else - { - _navigationStructure = _navigationRepository.GetContentNodesByObjectType(objectTypeKey); - } + IEnumerable navigationModels = trashed ? + _navigationRepository.GetTrashedContentNodesByObjectType(objectTypeKey) : + _navigationRepository.GetContentNodesByObjectType(objectTypeKey); + + _navigationStructure = NavigationFactory.BuildNavigationDictionary(navigationModels); } private bool TryGetParentKeyFromStructure(ConcurrentDictionary structure, Guid childKey, out Guid? parentKey)