Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Services/TemplateDeletionServiceTests.cs
T
Joseph Doherty c7774b7ec2 perf(templateengine): alarm-override, folder-delete and template-delete checks read lean template projections (review 2026-10-21 04/P6, 08/P1)
InstanceService.SetAlarmOverrideAsync and TemplateDeletionService move to the
no-tracking, body-free GetAllTemplatesForAnalysisAsync; TemplateFolderService
counts folder contents from GetTemplateStubsAsync.
2026-10-22 06:33:44 -04:00

212 lines
9.8 KiB
C#

using Moq;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services;
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Services;
public class TemplateDeletionServiceTests
{
private readonly Mock<ITemplateEngineRepository> _repoMock = new();
private readonly TemplateDeletionService _sut;
/// <summary>Initializes the test class with a mocked repository and the deletion service under test.</summary>
public TemplateDeletionServiceTests()
{
_sut = new TemplateDeletionService(_repoMock.Object);
}
/// <summary>Verifies that a template with no references can be deleted.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CanDeleteTemplate_NoReferences_ReturnsSuccess()
{
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Template("Orphan") { Id = 1 });
_repoMock.Setup(r => r.GetInstancesByTemplateIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Instance>());
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Template> { new("Orphan") { Id = 1 } });
_repoMock.Setup(r => r.GetCompositionsByTemplateIdAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<TemplateComposition>());
var result = await _sut.CanDeleteTemplateAsync(1);
Assert.True(result.IsSuccess);
// Review 2026-10-21 04/P6 + 08/P1: the reference checks read ids, names and
// composition edges only — the no-tracking, body-free analysis projection.
_repoMock.Verify(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()), Times.Never);
}
/// <summary>Verifies that a template with instances cannot be deleted.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CanDeleteTemplate_WithInstances_ReturnsFailure()
{
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Template("Used") { Id = 1 });
_repoMock.Setup(r => r.GetInstancesByTemplateIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Instance>
{
new("Inst1") { Id = 1 },
new("Inst2") { Id = 2 }
});
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Template> { new("Used") { Id = 1 } });
_repoMock.Setup(r => r.GetCompositionsByTemplateIdAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<TemplateComposition>());
var result = await _sut.CanDeleteTemplateAsync(1);
Assert.True(result.IsFailure);
Assert.Contains("2 instance(s)", result.Error);
Assert.Contains("Inst1", result.Error);
}
/// <summary>Verifies that a template with child templates cannot be deleted.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CanDeleteTemplate_WithChildTemplates_ReturnsFailure()
{
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Template("Base") { Id = 1 });
_repoMock.Setup(r => r.GetInstancesByTemplateIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Instance>());
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Template>
{
new("Base") { Id = 1 },
new("Child") { Id = 2, ParentTemplateId = 1 }
});
_repoMock.Setup(r => r.GetCompositionsByTemplateIdAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<TemplateComposition>());
var result = await _sut.CanDeleteTemplateAsync(1);
Assert.True(result.IsFailure);
Assert.Contains("child template(s)", result.Error);
Assert.Contains("Child", result.Error);
}
/// <summary>Verifies that a template composed by other templates cannot be deleted.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CanDeleteTemplate_ComposedByOthers_ReturnsFailure()
{
var composer = new Template("Composer") { Id = 2 };
composer.Compositions.Add(new TemplateComposition("PumpModule") { ComposedTemplateId = 1 });
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Template("Module") { Id = 1 });
_repoMock.Setup(r => r.GetInstancesByTemplateIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Instance>());
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Template>
{
new("Module") { Id = 1 },
composer
});
var result = await _sut.CanDeleteTemplateAsync(1);
Assert.True(result.IsFailure);
Assert.Contains("compose it", result.Error);
Assert.Contains("Composer", result.Error);
}
/// <summary>Verifies that checking deletability does not issue a composition query per template.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CanDeleteTemplate_DoesNotIssuePerTemplateCompositionQuery()
{
// The composition check must read the Compositions navigation
// already loaded by GetAllTemplatesForAnalysisAsync rather than issuing one
// GetCompositionsByTemplateIdAsync round-trip per template.
var templates = new List<Template>
{
new("Module") { Id = 1 },
new("A") { Id = 2 },
new("B") { Id = 3 },
new("C") { Id = 4 },
};
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Template("Module") { Id = 1 });
_repoMock.Setup(r => r.GetInstancesByTemplateIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Instance>());
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(templates);
var result = await _sut.CanDeleteTemplateAsync(1);
Assert.True(result.IsSuccess);
_repoMock.Verify(r => r.GetCompositionsByTemplateIdAsync(
It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Never);
}
/// <summary>Verifies that checking a missing template returns a failure.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CanDeleteTemplate_NotFound_ReturnsFailure()
{
_repoMock.Setup(r => r.GetTemplateByIdAsync(999, It.IsAny<CancellationToken>()))
.ReturnsAsync((Template?)null);
var result = await _sut.CanDeleteTemplateAsync(999);
Assert.True(result.IsFailure);
Assert.Contains("not found", result.Error);
}
/// <summary>Verifies that a template is deleted when all constraints are met.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeleteTemplate_AllConstraintsMet_Deletes()
{
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Template("Safe") { Id = 1 });
_repoMock.Setup(r => r.GetInstancesByTemplateIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Instance>());
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Template> { new("Safe") { Id = 1 } });
_repoMock.Setup(r => r.GetCompositionsByTemplateIdAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<TemplateComposition>());
_repoMock.Setup(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(1);
var result = await _sut.DeleteTemplateAsync(1);
Assert.True(result.IsSuccess);
_repoMock.Verify(r => r.DeleteTemplateAsync(1, It.IsAny<CancellationToken>()), Times.Once);
}
/// <summary>Verifies that all errors are reported when multiple constraints are violated.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CanDeleteTemplate_MultipleConstraints_AllErrorsReported()
{
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Template("Busy") { Id = 1 });
_repoMock.Setup(r => r.GetInstancesByTemplateIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Instance> { new("Inst1") { Id = 1 } });
var composer = new Template("Composer") { Id = 3 };
composer.Compositions.Add(new TemplateComposition("Module") { ComposedTemplateId = 1 });
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Template>
{
new("Busy") { Id = 1 },
new("Child") { Id = 2, ParentTemplateId = 1 },
composer
});
var result = await _sut.CanDeleteTemplateAsync(1);
Assert.True(result.IsFailure);
// All three constraint types should be mentioned
Assert.Contains("instance(s)", result.Error);
Assert.Contains("child template(s)", result.Error);
Assert.Contains("compose it", result.Error);
}
}