feat(m9/T23a): folder sibling reorder (ReorderFolderAsync + command + handler)

This commit is contained in:
Joseph Doherty
2026-06-18 11:00:57 -04:00
parent 0bd5e0986f
commit e3bc19c673
6 changed files with 245 additions and 2 deletions
@@ -2,6 +2,7 @@ using Moq;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services;
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Services;
@@ -236,4 +237,165 @@ public class TemplateFolderServiceTests
Assert.True(result.IsFailure);
Assert.Contains("1 template", result.Error);
}
// ========================================================================
// ReorderFolderAsync — sibling reorder by SortOrder (Up = lower, Down = higher)
// ========================================================================
[Fact]
public async Task ReorderFolder_Up_SwapsSortOrderWithPreviousSibling()
{
// Siblings under root: A(0), B(1), C(2). Move B up -> swaps with A.
var a = new TemplateFolder("A") { Id = 1, ParentFolderId = null, SortOrder = 0 };
var b = new TemplateFolder("B") { Id = 2, ParentFolderId = null, SortOrder = 1 };
var c = new TemplateFolder("C") { Id = 3, ParentFolderId = null, SortOrder = 2 };
_repoMock.Setup(r => r.GetFolderByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(b);
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<TemplateFolder> { a, b, c });
var result = await _sut.ReorderFolderAsync(2, ReorderDirection.Up, "admin");
Assert.True(result.IsSuccess);
Assert.Equal(0, b.SortOrder);
Assert.Equal(1, a.SortOrder);
Assert.Equal(2, c.SortOrder);
}
[Fact]
public async Task ReorderFolder_Down_SwapsSortOrderWithNextSibling()
{
// Siblings under root: A(0), B(1), C(2). Move B down -> swaps with C.
var a = new TemplateFolder("A") { Id = 1, ParentFolderId = null, SortOrder = 0 };
var b = new TemplateFolder("B") { Id = 2, ParentFolderId = null, SortOrder = 1 };
var c = new TemplateFolder("C") { Id = 3, ParentFolderId = null, SortOrder = 2 };
_repoMock.Setup(r => r.GetFolderByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(b);
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<TemplateFolder> { a, b, c });
var result = await _sut.ReorderFolderAsync(2, ReorderDirection.Down, "admin");
Assert.True(result.IsSuccess);
Assert.Equal(0, a.SortOrder);
Assert.Equal(2, b.SortOrder);
Assert.Equal(1, c.SortOrder);
}
[Fact]
public async Task ReorderFolder_UpAtTop_IsNoOp()
{
var a = new TemplateFolder("A") { Id = 1, ParentFolderId = null, SortOrder = 0 };
var b = new TemplateFolder("B") { Id = 2, ParentFolderId = null, SortOrder = 1 };
_repoMock.Setup(r => r.GetFolderByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(a);
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<TemplateFolder> { a, b });
var result = await _sut.ReorderFolderAsync(1, ReorderDirection.Up, "admin");
Assert.True(result.IsSuccess);
Assert.Equal(0, a.SortOrder);
Assert.Equal(1, b.SortOrder);
// No-op: nothing persisted, nothing audited.
_repoMock.Verify(r => r.UpdateFolderAsync(It.IsAny<TemplateFolder>(), It.IsAny<CancellationToken>()), Times.Never);
_repoMock.Verify(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Never);
_auditMock.Verify(a => a.LogAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<object?>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task ReorderFolder_DownAtBottom_IsNoOp()
{
var a = new TemplateFolder("A") { Id = 1, ParentFolderId = null, SortOrder = 0 };
var b = new TemplateFolder("B") { Id = 2, ParentFolderId = null, SortOrder = 1 };
_repoMock.Setup(r => r.GetFolderByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(b);
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<TemplateFolder> { a, b });
var result = await _sut.ReorderFolderAsync(2, ReorderDirection.Down, "admin");
Assert.True(result.IsSuccess);
Assert.Equal(0, a.SortOrder);
Assert.Equal(1, b.SortOrder);
_repoMock.Verify(r => r.UpdateFolderAsync(It.IsAny<TemplateFolder>(), It.IsAny<CancellationToken>()), Times.Never);
_repoMock.Verify(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task ReorderFolder_OnlyReordersWithinSameParent()
{
// root: A(0), B(1); under parent 99: X(0), Y(1).
// Moving X down must swap with Y, NOT with any root sibling.
var a = new TemplateFolder("A") { Id = 1, ParentFolderId = null, SortOrder = 0 };
var b = new TemplateFolder("B") { Id = 2, ParentFolderId = null, SortOrder = 1 };
var x = new TemplateFolder("X") { Id = 10, ParentFolderId = 99, SortOrder = 0 };
var y = new TemplateFolder("Y") { Id = 11, ParentFolderId = 99, SortOrder = 1 };
_repoMock.Setup(r => r.GetFolderByIdAsync(10, It.IsAny<CancellationToken>())).ReturnsAsync(x);
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<TemplateFolder> { a, b, x, y });
var result = await _sut.ReorderFolderAsync(10, ReorderDirection.Down, "admin");
Assert.True(result.IsSuccess);
Assert.Equal(1, x.SortOrder);
Assert.Equal(0, y.SortOrder);
// Root siblings untouched.
Assert.Equal(0, a.SortOrder);
Assert.Equal(1, b.SortOrder);
}
[Fact]
public async Task ReorderFolder_ThreeSiblings_DownThenUp_YieldsExpectedOrder()
{
// A(0), B(1), C(2). Move A down -> B,A,C ; then move A (now order 1) down -> B,C,A.
var a = new TemplateFolder("A") { Id = 1, ParentFolderId = null, SortOrder = 0 };
var b = new TemplateFolder("B") { Id = 2, ParentFolderId = null, SortOrder = 1 };
var c = new TemplateFolder("C") { Id = 3, ParentFolderId = null, SortOrder = 2 };
var all = new List<TemplateFolder> { a, b, c };
_repoMock.Setup(r => r.GetFolderByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(a);
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>())).ReturnsAsync(all);
var first = await _sut.ReorderFolderAsync(1, ReorderDirection.Down, "admin");
Assert.True(first.IsSuccess);
// Order by SortOrder: B(0), A(1), C(2)
var ordered1 = all.OrderBy(f => f.SortOrder).Select(f => f.Name).ToList();
Assert.Equal(new[] { "B", "A", "C" }, ordered1);
var second = await _sut.ReorderFolderAsync(1, ReorderDirection.Down, "admin");
Assert.True(second.IsSuccess);
// Order by SortOrder: B(0), C(1), A(2)
var ordered2 = all.OrderBy(f => f.SortOrder).Select(f => f.Name).ToList();
Assert.Equal(new[] { "B", "C", "A" }, ordered2);
}
[Fact]
public async Task ReorderFolder_NotFound_ReturnsFailure()
{
_repoMock.Setup(r => r.GetFolderByIdAsync(99, It.IsAny<CancellationToken>()))
.ReturnsAsync((TemplateFolder?)null);
var result = await _sut.ReorderFolderAsync(99, ReorderDirection.Up, "admin");
Assert.True(result.IsFailure);
Assert.Contains("not found", result.Error);
}
[Fact]
public async Task ReorderFolder_Persists_And_WritesAudit()
{
var a = new TemplateFolder("A") { Id = 1, ParentFolderId = null, SortOrder = 0 };
var b = new TemplateFolder("B") { Id = 2, ParentFolderId = null, SortOrder = 1 };
_repoMock.Setup(r => r.GetFolderByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(b);
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<TemplateFolder> { a, b });
var result = await _sut.ReorderFolderAsync(2, ReorderDirection.Up, "admin");
Assert.True(result.IsSuccess);
// Both swapped siblings persisted.
_repoMock.Verify(r => r.UpdateFolderAsync(a, It.IsAny<CancellationToken>()), Times.Once);
_repoMock.Verify(r => r.UpdateFolderAsync(b, It.IsAny<CancellationToken>()), Times.Once);
_repoMock.Verify(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
// Audit entry written (mirrors Move/Rename audit assertions).
_auditMock.Verify(au => au.LogAsync("admin", "Reorder", "TemplateFolder", "2", "B",
It.IsAny<object?>(), It.IsAny<CancellationToken>()), Times.Once);
}
}