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.Commons.Interfaces.Services; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; 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 InstanceServiceTests { private readonly Mock _repoMock = new(); private readonly Mock _auditMock = new(); private readonly InstanceService _sut; /// Initializes the test with a mocked repository and audit service and the service under test. public InstanceServiceTests() { _sut = new InstanceService(_repoMock.Object, _auditMock.Object); } /// Verifies that creating an instance with valid input returns success. /// A task that represents the asynchronous operation. [Fact] public async Task CreateInstance_ValidInput_ReturnsSuccess() { _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny())) .ReturnsAsync(new Template("TestTemplate") { Id = 1 }); _repoMock.Setup(r => r.GetInstanceByUniqueNameAsync("Inst1", It.IsAny())) .ReturnsAsync((Instance?)null); _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny())) .ReturnsAsync(1); var result = await _sut.CreateInstanceAsync("Inst1", 1, 1, null, "admin"); Assert.True(result.IsSuccess); Assert.Equal("Inst1", result.Value.UniqueName); Assert.Equal(InstanceState.Disabled, result.Value.State); // Starts disabled _repoMock.Verify(r => r.AddInstanceAsync(It.IsAny(), It.IsAny()), Times.Once); } /// Verifies that creating an instance with a duplicate name returns a failure. /// A task that represents the asynchronous operation. [Fact] public async Task CreateInstance_DuplicateName_ReturnsFailure() { _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny())) .ReturnsAsync(new Template("T") { Id = 1 }); _repoMock.Setup(r => r.GetInstanceByUniqueNameAsync("Inst1", It.IsAny())) .ReturnsAsync(new Instance("Inst1") { Id = 99 }); var result = await _sut.CreateInstanceAsync("Inst1", 1, 1, null, "admin"); Assert.True(result.IsFailure); Assert.Contains("already exists", result.Error); } /// Verifies that creating an instance from a missing template returns a failure. /// A task that represents the asynchronous operation. [Fact] public async Task CreateInstance_MissingTemplate_ReturnsFailure() { _repoMock.Setup(r => r.GetTemplateByIdAsync(999, It.IsAny())) .ReturnsAsync((Template?)null); var result = await _sut.CreateInstanceAsync("Inst1", 999, 1, null, "admin"); Assert.True(result.IsFailure); Assert.Contains("not found", result.Error); } /// Verifies that overriding a locked attribute returns a failure. /// A task that represents the asynchronous operation. [Fact] public async Task SetAttributeOverride_LockedAttribute_ReturnsFailure() { var instance = new Instance("Inst1") { Id = 1, TemplateId = 1 }; _repoMock.Setup(r => r.GetInstanceByIdAsync(1, It.IsAny())) .ReturnsAsync(instance); _repoMock.Setup(r => r.GetAttributesByTemplateIdAsync(1, It.IsAny())) .ReturnsAsync(new List { new("LockedAttr") { IsLocked = true } }); var result = await _sut.SetAttributeOverrideAsync(1, "LockedAttr", "new", "admin"); Assert.True(result.IsFailure); Assert.Contains("locked", result.Error, StringComparison.OrdinalIgnoreCase); } /// Verifies that overriding a non-existent attribute returns a failure. /// A task that represents the asynchronous operation. [Fact] public async Task SetAttributeOverride_NonExistentAttribute_ReturnsFailure() { var instance = new Instance("Inst1") { Id = 1, TemplateId = 1 }; _repoMock.Setup(r => r.GetInstanceByIdAsync(1, It.IsAny())) .ReturnsAsync(instance); _repoMock.Setup(r => r.GetAttributesByTemplateIdAsync(1, It.IsAny())) .ReturnsAsync(new List()); var result = await _sut.SetAttributeOverrideAsync(1, "Missing", "value", "admin"); Assert.True(result.IsFailure); Assert.Contains("does not exist", result.Error); } /// Verifies that overriding an unlocked attribute returns success. /// A task that represents the asynchronous operation. [Fact] public async Task SetAttributeOverride_UnlockedAttribute_ReturnsSuccess() { var instance = new Instance("Inst1") { Id = 1, TemplateId = 1 }; _repoMock.Setup(r => r.GetInstanceByIdAsync(1, It.IsAny())) .ReturnsAsync(instance); _repoMock.Setup(r => r.GetAttributesByTemplateIdAsync(1, It.IsAny())) .ReturnsAsync(new List { new("Threshold") { IsLocked = false } }); _repoMock.Setup(r => r.GetOverridesByInstanceIdAsync(1, It.IsAny())) .ReturnsAsync(new List()); _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny())) .ReturnsAsync(1); var result = await _sut.SetAttributeOverrideAsync(1, "Threshold", "99", "admin"); Assert.True(result.IsSuccess); Assert.Equal("Threshold", result.Value.AttributeName); Assert.Equal("99", result.Value.OverrideValue); } /// /// ScadaBridge#105, end to end for the single-attribute path the Central UI uses /// (): whatever it stores must be /// applied by the real flattener. The exact-case row is the positive control; a name /// differing from the member only by case is refused with a hint and nothing is stored. /// /// The override name the caller sends. /// Whether the write is expected to succeed. /// A task that represents the asynchronous operation. [Theory] [InlineData("Threshold", true)] [InlineData("threshold", false)] [InlineData("THRESHOLD", false)] public async Task SetAttributeOverride_EveryStoredOverride_IsAppliedByTheFlattener(string requestedName, bool accepted) { var template = new Template("Base") { Id = 1 }; template.Attributes.Add(new TemplateAttribute("Threshold") { TemplateId = 1, DataType = DataType.Double, Value = "50.0" }); var instance = new Instance("Inst1") { Id = 1, TemplateId = 1, SiteId = 1 }; _repoMock.Setup(r => r.GetInstanceByIdAsync(1, It.IsAny())).ReturnsAsync(instance); _repoMock.Setup(r => r.GetAttributesByTemplateIdAsync(1, It.IsAny())) .ReturnsAsync(template.Attributes.ToList()); _repoMock.Setup(r => r.GetOverridesByInstanceIdAsync(1, It.IsAny())) .ReturnsAsync(new List()); _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny())).ReturnsAsync(1); var stored = new List(); _repoMock.Setup(r => r.AddInstanceAttributeOverrideAsync(It.IsAny(), It.IsAny())) .Callback((o, _) => stored.Add(o)) .Returns(Task.CompletedTask); var result = await _sut.SetAttributeOverrideAsync(1, requestedName, "75.0", "admin"); foreach (var o in stored) instance.AttributeOverrides.Add(o); var flattened = new ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening.FlatteningService().Flatten( instance, [template], new Dictionary>(), new Dictionary>(), new Dictionary()); Assert.True(flattened.IsSuccess); foreach (var o in stored) Assert.True( flattened.Value.Attributes.Any(a => a.CanonicalName == o.AttributeName && a.Source == "Override"), $"Override '{o.AttributeName}' was stored but the flattener dropped it as unresolved."); var threshold = Assert.Single(flattened.Value.Attributes, a => a.CanonicalName == "Threshold"); if (accepted) { Assert.True(result.IsSuccess); Assert.Equal("75.0", threshold.Value); Assert.Equal("Override", threshold.Source); } else { Assert.True(result.IsFailure); Assert.Contains("did you mean 'Threshold'", result.Error); Assert.Empty(stored); Assert.Equal("50.0", threshold.Value); } } /// ScadaBridge#105: an ambiguous case-insensitive match (two candidates differing only by case) gets no hint. [Fact] public void CaseMismatchHint_NamesOnlyAnUnambiguousMatch() { Assert.Contains("did you mean 'Setpoint'", OverrideNameMatching.CaseMismatchHint("setpoint", ["Setpoint", "Other"])); Assert.Equal(string.Empty, OverrideNameMatching.CaseMismatchHint("setpoint", ["Setpoint", "SETPOINT"])); Assert.Equal(string.Empty, OverrideNameMatching.CaseMismatchHint("missing", ["Setpoint"])); } // --- SetAlarmOverrideAsync validation regression --- private static Template TemplateWithAlarms(int id, params TemplateAlarm[] alarms) { var t = new Template($"T{id}") { Id = id }; foreach (var a in alarms) { a.TemplateId = id; t.Alarms.Add(a); } return t; } /// Verifies that overriding a non-existent alarm returns a failure. /// A task that represents the asynchronous operation. [Fact] public async Task SetAlarmOverride_NonExistentAlarm_ReturnsFailure() { var instance = new Instance("Inst1") { Id = 1, TemplateId = 1 }; _repoMock.Setup(r => r.GetInstanceByIdAsync(1, It.IsAny())) .ReturnsAsync(instance); _repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny())) .ReturnsAsync(new List