V16: Siblings endpoints (#19657)

* PoC implementation

* Move to controller base

* Implement solution that seems worse, but works better

* Don't require parent key in repository method

* Fix typos

* Add siblings for data type, media type and media

* Add endpoint for template

* Add DocumentType and DocumentBlueprint controllers

* Fix naming

* Fix case if siblings are under root

* Take item ordering into account

not all entities are ordered by sort order

* Add default implementation

* Fix parentkey

* Add tests

* Format optimizations for split view

* Add test covered requirement to description

* Cover positive case and make test case output more readable

* reduce allocations

* Clarify test

---------

Co-authored-by: Migaroez <geusens@gmail.com>
This commit is contained in:
Mole
2025-07-07 14:53:42 +02:00
committed by GitHub
parent b5195ed8eb
commit 9baf04026e
15 changed files with 476 additions and 2 deletions

View File

@@ -8,7 +8,7 @@ using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.ContentTypeEditing;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Tests.Common.Attributes;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Testing;
@@ -931,6 +931,98 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
}
[Test]
public void EntityService_Siblings_ReturnsExpectedSiblings()
{
var children = CreateSiblingsTestData();
var taget = children[1];
var result = EntityService.GetSiblings(taget.Key, UmbracoObjectTypes.Document, 1, 1).ToArray();
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);
}
[Test]
public void EntityService_Siblings_SkipsTrashedEntities()
{
var children = CreateSiblingsTestData();
var trash = children[1];
ContentService.MoveToRecycleBin(trash);
var taget = children[2];
var result = EntityService.GetSiblings(taget.Key, UmbracoObjectTypes.Document, 1, 1).ToArray();
Assert.AreEqual(3, result.Length);
Assert.IsFalse(result.Any(x => x.Key == trash.Key));
Assert.IsTrue(result[0].Key == children[0].Key);
Assert.IsTrue(result[1].Key == children[2].Key);
Assert.IsTrue(result[2].Key == children[3].Key);
}
[Test]
public void EntityService_Siblings_RespectsOrdering()
{
var children = CreateSiblingsTestData();
// Order the children by name to ensure the ordering works when differing from the default sort order, the name is a GUID.
children = children.OrderBy(x => x.Name).ToList();
var taget = children[1];
var result = EntityService.GetSiblings(taget.Key, UmbracoObjectTypes.Document, 1, 1, Ordering.By(nameof(NodeDto.Text))).ToArray();
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);
}
[Test]
public void EntityService_Siblings_IgnoresOutOfBoundsLower()
{
var children = CreateSiblingsTestData();
var taget = children[1];
var result = EntityService.GetSiblings(taget.Key, UmbracoObjectTypes.Document, 100, 1).ToArray();
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);
}
[Test]
public void EntityService_Siblings_IgnoresOutOfBoundsUpper()
{
var children = CreateSiblingsTestData();
var taget = children[^2];
var result = EntityService.GetSiblings(taget.Key, UmbracoObjectTypes.Document, 1, 100).ToArray();
Assert.AreEqual(3, result.Length);
Assert.IsTrue(result[^1].Key == children[^1].Key);
Assert.IsTrue(result[^2].Key == children[^2].Key);
Assert.IsTrue(result[^3].Key == children[^3].Key);
}
private List<Content> CreateSiblingsTestData()
{
var contentType = ContentTypeService.Get("umbTextpage");
var root = ContentBuilder.CreateSimpleContent(contentType);
ContentService.Save(root);
var children = new List<Content>();
for (int i = 0; i < 10; i++)
{
var child = ContentBuilder.CreateSimpleContent(contentType, Guid.NewGuid().ToString(), root);
ContentService.Save(child);
children.Add(child);
}
return children;
}
private static bool _isSetup;
private int _folderId;

View File

@@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services;
[TestFixture]
public class EntityServiceTests
{
[TestCase(1, 1, false, TestName = "Siblings_Index_Validation_Valid")]
[TestCase(-1, 1, true, TestName = "Siblings_Index_Validation_InvalidBefore")]
[TestCase(1, -1, true, TestName = "Siblings_Index_Validation_InvalidAfter")]
[TestCase(-1, -1, true, TestName = "Siblings_Index_Validation_InvalidBeforeAndAfter")]
public void Siblings_Index_Validation(int before, int after, bool shouldThrow)
{
var sut = CreateEntityService();
if (shouldThrow)
{
Assert.Throws<ArgumentOutOfRangeException>(() => sut.GetSiblings(Guid.NewGuid(), UmbracoObjectTypes.Document, before, after));
}
}
private EntityService CreateEntityService() =>
new(
Mock.Of<ICoreScopeProvider>(),
Mock.Of<ILoggerFactory>(),
Mock.Of<IEventMessagesFactory>(),
Mock.Of<IIdKeyMap>(),
Mock.Of<IEntityRepository>());
}