Trees: Expanding sibling endpoints to include all entities with trees (#20150)

* Adding member types sibling endpoints

* Introducing sibling endpoint for Partial Views and logic.

* Introducing sibling endpoint for stylesheets

* Introducing sibling endpoint for scripts

* Introducing FileSystemTreeServiceBase.cs

* Introducing interfaces for implementation specific services

* Introducing services for specific trees

* Modifying controller bases to fit new interface and logic.

* Obsoleting old constructors related to PartialView

* Obsoleting ctors related to Stylesheets

* Obsoleting ctors related to scripts

* Adding tests for scriptsTreeService

* Adding tests for siblings

* Removing unused dependencies

* Removing signs and replacing it with flags

* Fixing breaking changes by obsoletion

* Fixing more breaking changes

* Registering missing service

* Fixing breaking changes again

* Changing name of method GetSiblingsViewModels

* Rewritten tests for less bloat and less duplicate code

* Expanding tests to include other methods from service

* Test refactoring: avoided populating file systems that weren't under test, updated encapsulation, renaming, further re-use.

* Management API: Expanding the existing sibling endpoints to support trashed entities (#20154)

* Refactoring existing logic to include trashed items

* Including tests for trashed entities

* Groundwork for trashed siblings

* Documents trashed siblings endpoint

* Controller for Media trashed items

* Expanding tests to include a test for trashed siblings

* Code review corrections

* Resolving code review

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
Nicklas Kramer
2025-09-23 11:17:25 +02:00
committed by GitHub
parent f379c9bbdd
commit 8213da1b77
39 changed files with 1163 additions and 69 deletions

View File

@@ -0,0 +1,83 @@
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Tests.Common.TestHelpers;
using Umbraco.Cms.Tests.Integration.Testing;
namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services.Trees;
public abstract class FileSystemTreeServiceTestsBase : UmbracoIntegrationTest
{
protected FileSystems FileSystems { get; private set; }
protected IFileSystem TestFileSystem { get; private set; }
protected abstract string FileSystemPath { get; }
protected IHostingEnvironment HostingEnvironment => GetRequiredService<IHostingEnvironment>();
[SetUp]
public void SetUpFileSystem()
{
TestFileSystem = new PhysicalFileSystem(IOHelper, HostingEnvironment, LoggerFactory.CreateLogger<PhysicalFileSystem>(), HostingEnvironment.MapPathWebRoot(FileSystemPath), HostingEnvironment.ToAbsolute(FileSystemPath));
FileSystems = FileSystemsCreator.CreateTestFileSystems(
LoggerFactory,
IOHelper,
GetRequiredService<IOptions<GlobalSettings>>(),
HostingEnvironment,
GetPartialViewsFileSystem(),
GetStylesheetsFileSystem(),
GetScriptsFileSystem(),
null);
for (int i = 0; i < 10; i++)
{
using var stream = CreateStream(Path.Join("tests"));
TestFileSystem.AddFile($"file{i}", stream);
}
}
private static Stream CreateStream(string contents = null)
{
if (string.IsNullOrEmpty(contents))
{
contents = "/* test */";
}
var bytes = Encoding.UTF8.GetBytes(contents);
return new MemoryStream(bytes);
}
protected virtual IFileSystem? GetPartialViewsFileSystem() => null;
protected virtual IFileSystem? GetStylesheetsFileSystem() => null;
protected virtual IFileSystem? GetScriptsFileSystem() => null;
[TearDown]
public void TearDownFileSystem()
{
Purge(TestFileSystem, string.Empty);
FileSystems = null;
}
private static void Purge(IFileSystem fs, string path)
{
var files = fs.GetFiles(path, "*");
foreach (var file in files)
{
fs.DeleteFile(file);
}
var dirs = fs.GetDirectories(path);
foreach (var dir in dirs)
{
Purge(fs, dir);
fs.DeleteDirectory(dir);
}
}
}

View File

@@ -0,0 +1,54 @@
using NUnit.Framework;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services.Trees;
public class PartialViewTreeServiceTests : FileSystemTreeServiceTestsBase
{
protected override string FileSystemPath => Constants.SystemDirectories.PartialViews;
protected override IFileSystem? GetPartialViewsFileSystem() => TestFileSystem;
[Test]
public void Can_Get_Siblings_From_PartialView_Tree_Service()
{
var service = new PartialViewTreeService(FileSystems);
FileSystemTreeItemPresentationModel[] treeModel = service.GetSiblingsViewModels("file5", 1, 1, out long before, out var after);
int index = Array.FindIndex(treeModel, item => item.Name == "file5");
Assert.AreEqual(treeModel[index].Name, "file5");
Assert.AreEqual(treeModel[index - 1].Name, "file4");
Assert.AreEqual(treeModel[index + 1].Name, "file6");
Assert.That(treeModel.Length == 3);
Assert.AreEqual(after, 3);
Assert.AreEqual(before, 4);
}
[Test]
public void Can_Get_Ancestors_From_StyleSheet_Tree_Service()
{
var service = new PartialViewTreeService(FileSystems);
var path = Path.Join("tests", "file5");
FileSystemTreeItemPresentationModel[] treeModel = service.GetAncestorModels(path, true);
Assert.IsNotEmpty(treeModel);
Assert.AreEqual(treeModel.Length, 2);
Assert.AreEqual(treeModel[0].Name, "tests");
}
[Test]
public void Can_Get_PathViewModels_From_StyleSheet_Tree_Service()
{
var service = new PartialViewTreeService(FileSystems);
FileSystemTreeItemPresentationModel[] treeModels = service.GetPathViewModels(string.Empty, 0, Int32.MaxValue, out var totalItems);
Assert.IsNotEmpty(treeModels);
Assert.AreEqual(treeModels.Length, totalItems);
}
}

View File

@@ -0,0 +1,53 @@
using NUnit.Framework;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services.Trees;
public class ScriptTreeServiceTests : FileSystemTreeServiceTestsBase
{
protected override string FileSystemPath => GlobalSettings.UmbracoScriptsPath;
protected override IFileSystem? GetScriptsFileSystem() => TestFileSystem;
[Test]
public void Can_Get_Siblings_From_Script_Tree_Service()
{
var service = new ScriptTreeService(FileSystems);
FileSystemTreeItemPresentationModel[] treeModel = service.GetSiblingsViewModels("file5", 1, 1, out long before, out var after);
int index = Array.FindIndex(treeModel, item => item.Name == "file5");
Assert.AreEqual(treeModel[index].Name, "file5");
Assert.AreEqual(treeModel[index - 1].Name, "file4");
Assert.AreEqual(treeModel[index + 1].Name, "file6");
Assert.That(treeModel.Length == 3);
Assert.AreEqual(after, 3);
Assert.AreEqual(before, 4);
}
[Test]
public void Can_Get_Ancestors_From_StyleSheet_Tree_Service()
{
var service = new ScriptTreeService(FileSystems);
var path = Path.Join("tests", "file5");
FileSystemTreeItemPresentationModel[] treeModel = service.GetAncestorModels(path, true);
Assert.IsNotEmpty(treeModel);
Assert.AreEqual(treeModel.Length, 2);
Assert.AreEqual(treeModel[0].Name, "tests");
}
[Test]
public void Can_Get_PathViewModels_From_StyleSheet_Tree_Service()
{
var service = new ScriptTreeService(FileSystems);
FileSystemTreeItemPresentationModel[] treeModels = service.GetPathViewModels(string.Empty, 0, Int32.MaxValue, out var totalItems);
Assert.IsNotEmpty(treeModels);
Assert.AreEqual(treeModels.Length, totalItems);
}
}

View File

@@ -0,0 +1,53 @@
using NUnit.Framework;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services.Trees;
public class StyleSheetTreeServiceTests : FileSystemTreeServiceTestsBase
{
protected override string FileSystemPath => GlobalSettings.UmbracoCssPath;
protected override IFileSystem? GetStylesheetsFileSystem() => TestFileSystem;
[Test]
public void Can_Get_Siblings_From_StyleSheet_Tree_Service()
{
var service = new StyleSheetTreeService(FileSystems);
FileSystemTreeItemPresentationModel[] treeModel = service.GetSiblingsViewModels("file5", 1, 1, out long before, out var after);
int index = Array.FindIndex(treeModel, item => item.Name == "file5");
Assert.AreEqual(treeModel[index].Name, "file5");
Assert.AreEqual(treeModel[index - 1].Name, "file4");
Assert.AreEqual(treeModel[index + 1].Name, "file6");
Assert.That(treeModel.Length == 3);
Assert.AreEqual(after, 3);
Assert.AreEqual(before, 4);
}
[Test]
public void Can_Get_Ancestors_From_StyleSheet_Tree_Service()
{
var service = new StyleSheetTreeService(FileSystems);
var path = Path.Join("tests", "file5");
FileSystemTreeItemPresentationModel[] treeModel = service.GetAncestorModels(path, true);
Assert.IsNotEmpty(treeModel);
Assert.AreEqual(treeModel.Length, 2);
Assert.AreEqual(treeModel[0].Name, "tests");
}
[Test]
public void Can_Get_PathViewModels_From_StyleSheet_Tree_Service()
{
var service = new StyleSheetTreeService(FileSystems);
FileSystemTreeItemPresentationModel[] treeModels = service.GetPathViewModels(string.Empty, 0, Int32.MaxValue, out var totalItems);
Assert.IsNotEmpty(treeModels);
Assert.AreEqual(treeModels.Length, totalItems);
}
}

View File

@@ -972,6 +972,27 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
Assert.IsTrue(result[2].Key == children[3].Key);
}
[Test]
public void EntityService_Siblings_Returns_Trashed_Siblings()
{
ContentService.EmptyRecycleBin();
var children = CreateDocumentSiblingsTestData();
for (int i = 0; i <= 3; i++)
{
ContentService.MoveToRecycleBin(children[i]);
}
var result = EntityService.GetTrashedSiblings(children[1].Key, [UmbracoObjectTypes.Document], 1, 1, out long totalBefore, out long totalAfter).ToArray();
Assert.AreEqual(0, totalBefore);
Assert.AreEqual(1, totalAfter);
Assert.AreEqual(3, result.Length);
Assert.IsTrue(result[0].Key == children[0].Key);
Assert.IsTrue(result[1].Key == children[1].Key);
Assert.IsTrue(result[2].Key == children[2].Key);
Assert.IsFalse(result.Any(x => x.Key == children[3].Key));
}
[Test]
public void EntityService_Siblings_SkipsFilteredEntities_UsingFilterWithSet()
{