using Moq; using ScadaLink.Commons.Entities.Instances; using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Interfaces.Services; using ScadaLink.TemplateEngine.Services; namespace ScadaLink.TemplateEngine.Tests.Services; public class AreaServiceTests { private readonly Mock _repoMock = new(); private readonly Mock _auditMock = new(); private readonly AreaService _sut; public AreaServiceTests() { _sut = new AreaService(_repoMock.Object, _auditMock.Object); } [Fact] public async Task CreateArea_ValidInput_ReturnsSuccess() { _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny())) .ReturnsAsync(new List()); _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny())) .ReturnsAsync(1); var result = await _sut.CreateAreaAsync("Building A", 1, null, "admin"); Assert.True(result.IsSuccess); Assert.Equal("Building A", result.Value.Name); Assert.Equal(1, result.Value.SiteId); } [Fact] public async Task CreateArea_DuplicateName_ReturnsFailure() { _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny())) .ReturnsAsync(new List { new("Building A") { Id = 1, SiteId = 1, ParentAreaId = null } }); var result = await _sut.CreateAreaAsync("Building A", 1, null, "admin"); Assert.True(result.IsFailure); Assert.Contains("already exists", result.Error); } [Fact] public async Task CreateArea_WithParent_ValidatesParentBelongsToSite() { _repoMock.Setup(r => r.GetAreaByIdAsync(5, It.IsAny())) .ReturnsAsync(new Area("Parent") { Id = 5, SiteId = 99 }); // Different site! var result = await _sut.CreateAreaAsync("Child", 1, 5, "admin"); Assert.True(result.IsFailure); Assert.Contains("does not belong", result.Error); } [Fact] public async Task DeleteArea_WithAssignedInstances_ReturnsFailure() { _repoMock.Setup(r => r.GetAreaByIdAsync(1, It.IsAny())) .ReturnsAsync(new Area("Building A") { Id = 1, SiteId = 1 }); _repoMock.Setup(r => r.GetInstancesBySiteIdAsync(1, It.IsAny())) .ReturnsAsync(new List { new("Inst1") { Id = 1, AreaId = 1, SiteId = 1 } }); _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny())) .ReturnsAsync(new List { new("Building A") { Id = 1, SiteId = 1 } }); var result = await _sut.DeleteAreaAsync(1, "admin"); Assert.True(result.IsFailure); Assert.Contains("instance(s) are assigned", result.Error); } [Fact] public async Task DeleteArea_WithChildAreas_ReturnsFailure() { _repoMock.Setup(r => r.GetAreaByIdAsync(1, It.IsAny())) .ReturnsAsync(new Area("Parent") { Id = 1, SiteId = 1 }); _repoMock.Setup(r => r.GetInstancesBySiteIdAsync(1, It.IsAny())) .ReturnsAsync(new List()); _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny())) .ReturnsAsync(new List { new("Parent") { Id = 1, SiteId = 1 }, new("Child") { Id = 2, SiteId = 1, ParentAreaId = 1 } }); var result = await _sut.DeleteAreaAsync(1, "admin"); Assert.True(result.IsFailure); Assert.Contains("child areas", result.Error); } [Fact] public async Task DeleteArea_NoConstraints_Success() { _repoMock.Setup(r => r.GetAreaByIdAsync(1, It.IsAny())) .ReturnsAsync(new Area("Empty") { Id = 1, SiteId = 1 }); _repoMock.Setup(r => r.GetInstancesBySiteIdAsync(1, It.IsAny())) .ReturnsAsync(new List()); _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny())) .ReturnsAsync(new List { new("Empty") { Id = 1, SiteId = 1 } }); _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny())) .ReturnsAsync(1); var result = await _sut.DeleteAreaAsync(1, "admin"); Assert.True(result.IsSuccess); _repoMock.Verify(r => r.DeleteAreaAsync(1, It.IsAny()), Times.Once); } [Fact] public async Task MoveArea_ToOtherArea_Succeeds() { // Move 'Leaf' from under 'A' to under 'B'. _repoMock.Setup(r => r.GetAreaByIdAsync(3, It.IsAny())) .ReturnsAsync(new Area("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 }); _repoMock.Setup(r => r.GetAreaByIdAsync(2, It.IsAny())) .ReturnsAsync(new Area("B") { Id = 2, SiteId = 1 }); _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny())) .ReturnsAsync(new List { new("A") { Id = 1, SiteId = 1 }, new("B") { Id = 2, SiteId = 1 }, new("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 } }); _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny())).ReturnsAsync(1); var result = await _sut.MoveAreaAsync(3, 2, "admin"); Assert.True(result.IsSuccess); Assert.Equal(2, result.Value.ParentAreaId); _repoMock.Verify(r => r.UpdateAreaAsync(It.Is(a => a.Id == 3 && a.ParentAreaId == 2), It.IsAny()), Times.Once); _auditMock.Verify(a => a.LogAsync("admin", "Move", "Area", "3", "Leaf", It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task MoveArea_ToSiteRoot_Succeeds() { // Move 'Leaf' from under 'A' to site root (null parent). _repoMock.Setup(r => r.GetAreaByIdAsync(3, It.IsAny())) .ReturnsAsync(new Area("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 }); _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny())) .ReturnsAsync(new List { new("A") { Id = 1, SiteId = 1 }, new("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 } }); _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny())).ReturnsAsync(1); var result = await _sut.MoveAreaAsync(3, null, "admin"); Assert.True(result.IsSuccess); Assert.Null(result.Value.ParentAreaId); } [Fact] public async Task MoveArea_ToSelf_Fails() { _repoMock.Setup(r => r.GetAreaByIdAsync(1, It.IsAny())) .ReturnsAsync(new Area("A") { Id = 1, SiteId = 1 }); var result = await _sut.MoveAreaAsync(1, 1, "admin"); Assert.True(result.IsFailure); Assert.Contains("its own parent", result.Error); } [Fact] public async Task MoveArea_ToDescendant_FailsWithCycleError() { // Tree: 1 -> 2 -> 3. Try to move 1 under 3. _repoMock.Setup(r => r.GetAreaByIdAsync(1, It.IsAny())) .ReturnsAsync(new Area("Root") { Id = 1, SiteId = 1 }); _repoMock.Setup(r => r.GetAreaByIdAsync(3, It.IsAny())) .ReturnsAsync(new Area("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 2 }); _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny())) .ReturnsAsync(new List { new("Root") { Id = 1, SiteId = 1 }, new("Mid") { Id = 2, SiteId = 1, ParentAreaId = 1 }, new("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 2 } }); var result = await _sut.MoveAreaAsync(1, 3, "admin"); Assert.True(result.IsFailure); Assert.Contains("descendants", result.Error); } [Fact] public async Task MoveArea_DifferentSite_Fails() { _repoMock.Setup(r => r.GetAreaByIdAsync(1, It.IsAny())) .ReturnsAsync(new Area("A") { Id = 1, SiteId = 1 }); _repoMock.Setup(r => r.GetAreaByIdAsync(99, It.IsAny())) .ReturnsAsync(new Area("Foreign") { Id = 99, SiteId = 2 }); var result = await _sut.MoveAreaAsync(1, 99, "admin"); Assert.True(result.IsFailure); Assert.Contains("same site", result.Error); } [Fact] public async Task MoveArea_NameCollidesAtNewParent_Fails() { // 'Leaf' under parent 1; a sibling 'Leaf' already exists under parent 2. _repoMock.Setup(r => r.GetAreaByIdAsync(3, It.IsAny())) .ReturnsAsync(new Area("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 }); _repoMock.Setup(r => r.GetAreaByIdAsync(2, It.IsAny())) .ReturnsAsync(new Area("B") { Id = 2, SiteId = 1 }); _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny())) .ReturnsAsync(new List { new("A") { Id = 1, SiteId = 1 }, new("B") { Id = 2, SiteId = 1 }, new("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 }, new("Leaf") { Id = 4, SiteId = 1, ParentAreaId = 2 } }); var result = await _sut.MoveAreaAsync(3, 2, "admin"); Assert.True(result.IsFailure); Assert.Contains("already exists", result.Error); } [Fact] public async Task MoveArea_SameParent_NoOpSuccess() { _repoMock.Setup(r => r.GetAreaByIdAsync(3, It.IsAny())) .ReturnsAsync(new Area("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 }); _repoMock.Setup(r => r.GetAreaByIdAsync(1, It.IsAny())) .ReturnsAsync(new Area("A") { Id = 1, SiteId = 1 }); _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny())) .ReturnsAsync(new List { new("A") { Id = 1, SiteId = 1 }, new("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 } }); var result = await _sut.MoveAreaAsync(3, 1, "admin"); Assert.True(result.IsSuccess); _repoMock.Verify(r => r.UpdateAreaAsync(It.IsAny(), It.IsAny()), Times.Never); _auditMock.Verify(a => a.LogAsync(It.IsAny(), "Move", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task MoveArea_TargetParentMissing_Fails() { _repoMock.Setup(r => r.GetAreaByIdAsync(3, It.IsAny())) .ReturnsAsync(new Area("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 }); _repoMock.Setup(r => r.GetAreaByIdAsync(999, It.IsAny())) .ReturnsAsync((Area?)null); var result = await _sut.MoveAreaAsync(3, 999, "admin"); Assert.True(result.IsFailure); Assert.Contains("not found", result.Error); } [Fact] public async Task DeleteArea_InstancesInDescendants_Blocked() { // Area hierarchy: Area1 -> Area2 -> Area3 // Instance assigned to Area3, trying to delete Area1 should be blocked _repoMock.Setup(r => r.GetAreaByIdAsync(1, It.IsAny())) .ReturnsAsync(new Area("Root") { Id = 1, SiteId = 1 }); _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny())) .ReturnsAsync(new List { new("Root") { Id = 1, SiteId = 1 }, new("Mid") { Id = 2, SiteId = 1, ParentAreaId = 1 }, new("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 2 } }); _repoMock.Setup(r => r.GetInstancesBySiteIdAsync(1, It.IsAny())) .ReturnsAsync(new List { new("DeepInstance") { Id = 10, AreaId = 3, SiteId = 1 } }); var result = await _sut.DeleteAreaAsync(1, "admin"); Assert.True(result.IsFailure); Assert.Contains("instance(s) are assigned", result.Error); } }