402 lines
18 KiB
C#
402 lines
18 KiB
C#
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;
|
|
|
|
public class TemplateFolderServiceTests
|
|
{
|
|
private readonly Mock<ITemplateEngineRepository> _repoMock = new();
|
|
private readonly Mock<IAuditService> _auditMock = new();
|
|
private readonly TemplateFolderService _sut;
|
|
|
|
public TemplateFolderServiceTests()
|
|
{
|
|
_sut = new TemplateFolderService(_repoMock.Object, _auditMock.Object);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateFolder_ValidInput_ReturnsSuccess()
|
|
{
|
|
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<TemplateFolder>());
|
|
|
|
var result = await _sut.CreateFolderAsync("Dev", null, "admin");
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Equal("Dev", result.Value.Name);
|
|
Assert.Null(result.Value.ParentFolderId);
|
|
_repoMock.Verify(r => r.AddFolderAsync(It.IsAny<TemplateFolder>(), It.IsAny<CancellationToken>()), Times.Once);
|
|
_repoMock.Verify(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateFolder_EmptyName_ReturnsFailure()
|
|
{
|
|
var result = await _sut.CreateFolderAsync(" ", null, "admin");
|
|
|
|
Assert.True(result.IsFailure);
|
|
Assert.Contains("required", result.Error, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateFolder_DuplicateSiblingName_CaseInsensitive_ReturnsFailure()
|
|
{
|
|
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<TemplateFolder>
|
|
{
|
|
new("Dev") { Id = 1, ParentFolderId = null }
|
|
});
|
|
|
|
var result = await _sut.CreateFolderAsync("dev", null, "admin");
|
|
|
|
Assert.True(result.IsFailure);
|
|
Assert.Contains("already exists", result.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateFolder_ParentNotFound_ReturnsFailure()
|
|
{
|
|
_repoMock.Setup(r => r.GetFolderByIdAsync(99, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((TemplateFolder?)null);
|
|
|
|
var result = await _sut.CreateFolderAsync("Sub", 99, "admin");
|
|
|
|
Assert.True(result.IsFailure);
|
|
Assert.Contains("not found", result.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RenameFolder_ValidInput_ReturnsSuccess()
|
|
{
|
|
var folder = new TemplateFolder("Old") { Id = 1, ParentFolderId = null };
|
|
_repoMock.Setup(r => r.GetFolderByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(folder);
|
|
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<TemplateFolder> { folder });
|
|
|
|
var result = await _sut.RenameFolderAsync(1, "New", "admin");
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Equal("New", result.Value.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RenameFolder_NotFound_ReturnsFailure()
|
|
{
|
|
_repoMock.Setup(r => r.GetFolderByIdAsync(99, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((TemplateFolder?)null);
|
|
|
|
var result = await _sut.RenameFolderAsync(99, "New", "admin");
|
|
|
|
Assert.True(result.IsFailure);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RenameFolder_DuplicateSibling_ReturnsFailure()
|
|
{
|
|
var folder = new TemplateFolder("Old") { Id = 1, ParentFolderId = null };
|
|
var sibling = new TemplateFolder("Other") { Id = 2, ParentFolderId = null };
|
|
_repoMock.Setup(r => r.GetFolderByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(folder);
|
|
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<TemplateFolder> { folder, sibling });
|
|
|
|
var result = await _sut.RenameFolderAsync(1, "Other", "admin");
|
|
|
|
Assert.True(result.IsFailure);
|
|
Assert.Contains("already exists", result.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MoveFolder_ValidParent_ReturnsSuccess()
|
|
{
|
|
var f1 = new TemplateFolder("A") { Id = 1, ParentFolderId = null };
|
|
var f2 = new TemplateFolder("B") { Id = 2, ParentFolderId = null };
|
|
_repoMock.Setup(r => r.GetFolderByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(f1);
|
|
_repoMock.Setup(r => r.GetFolderByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(f2);
|
|
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<TemplateFolder> { f1, f2 });
|
|
|
|
var result = await _sut.MoveFolderAsync(1, 2, "admin");
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Equal(2, result.Value.ParentFolderId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MoveFolder_OntoSelf_ReturnsFailure()
|
|
{
|
|
var f1 = new TemplateFolder("A") { Id = 1 };
|
|
_repoMock.Setup(r => r.GetFolderByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(f1);
|
|
|
|
var result = await _sut.MoveFolderAsync(1, 1, "admin");
|
|
|
|
Assert.True(result.IsFailure);
|
|
Assert.Contains("cycle", result.Error, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MoveFolder_OntoDescendant_ReturnsFailure()
|
|
{
|
|
// A -> B -> C; attempting to move A under C must fail.
|
|
var fa = new TemplateFolder("A") { Id = 1, ParentFolderId = null };
|
|
var fb = new TemplateFolder("B") { Id = 2, ParentFolderId = 1 };
|
|
var fc = new TemplateFolder("C") { Id = 3, ParentFolderId = 2 };
|
|
_repoMock.Setup(r => r.GetFolderByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(fa);
|
|
_repoMock.Setup(r => r.GetFolderByIdAsync(3, It.IsAny<CancellationToken>())).ReturnsAsync(fc);
|
|
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<TemplateFolder> { fa, fb, fc });
|
|
|
|
var result = await _sut.MoveFolderAsync(1, 3, "admin");
|
|
|
|
Assert.True(result.IsFailure);
|
|
Assert.Contains("cycle", result.Error, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MoveFolder_ToRoot_ReturnsSuccess()
|
|
{
|
|
var f = new TemplateFolder("Sub") { Id = 1, ParentFolderId = 99 };
|
|
_repoMock.Setup(r => r.GetFolderByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(f);
|
|
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<TemplateFolder> { f });
|
|
|
|
var result = await _sut.MoveFolderAsync(1, null, "admin");
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Null(result.Value.ParentFolderId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MoveFolder_PreExistingCycleInGraph_ReturnsFailure_DoesNotInfiniteLoop()
|
|
{
|
|
// Manufactured malformed graph: X.parent=Y, Y.parent=X. We move Z under X.
|
|
// The ancestor walk would loop forever without a guard.
|
|
var x = new TemplateFolder("X") { Id = 1, ParentFolderId = 2 };
|
|
var y = new TemplateFolder("Y") { Id = 2, ParentFolderId = 1 };
|
|
var z = new TemplateFolder("Z") { Id = 3, ParentFolderId = null };
|
|
_repoMock.Setup(r => r.GetFolderByIdAsync(3, It.IsAny<CancellationToken>())).ReturnsAsync(z);
|
|
_repoMock.Setup(r => r.GetFolderByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(x);
|
|
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<TemplateFolder> { x, y, z });
|
|
|
|
var result = await _sut.MoveFolderAsync(3, 1, "admin");
|
|
|
|
Assert.True(result.IsFailure);
|
|
Assert.Contains("cycle", result.Error, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteFolder_Empty_ReturnsSuccess()
|
|
{
|
|
var f = new TemplateFolder("Empty") { Id = 1 };
|
|
_repoMock.Setup(r => r.GetFolderByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(f);
|
|
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<TemplateFolder> { f });
|
|
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<Template>());
|
|
|
|
var result = await _sut.DeleteFolderAsync(1, "admin");
|
|
|
|
Assert.True(result.IsSuccess);
|
|
_repoMock.Verify(r => r.DeleteFolderAsync(1, It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteFolder_HasChildFolders_ReturnsFailure_WithCounts()
|
|
{
|
|
var parent = new TemplateFolder("P") { Id = 1 };
|
|
var child = new TemplateFolder("C") { Id = 2, ParentFolderId = 1 };
|
|
_repoMock.Setup(r => r.GetFolderByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(parent);
|
|
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<TemplateFolder> { parent, child });
|
|
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<Template>());
|
|
|
|
var result = await _sut.DeleteFolderAsync(1, "admin");
|
|
|
|
Assert.True(result.IsFailure);
|
|
Assert.Contains("1 subfolder", result.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteFolder_HasTemplates_ReturnsFailure_WithCounts()
|
|
{
|
|
var f = new TemplateFolder("P") { Id = 1 };
|
|
var t = new Template("X") { Id = 5, FolderId = 1 };
|
|
_repoMock.Setup(r => r.GetFolderByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(f);
|
|
_repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<TemplateFolder> { f });
|
|
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<Template> { t });
|
|
|
|
var result = await _sut.DeleteFolderAsync(1, "admin");
|
|
|
|
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);
|
|
}
|
|
}
|