From abe8832e9e72fe91b3c6cb43f03aeae38f99c91f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 16 Jun 2026 17:33:15 -0400 Subject: [PATCH] feat(template): stamp ElementDataType on instance attribute overrides Set existingOverride.ElementDataType and newOverride.ElementDataType from templateAttr.ElementDataType in both the update and create branches of SetAttributeOverrideAsync, so the persisted InstanceAttributeOverride row always carries the element type for later central normalizer use (#93/M3). --- .../Services/InstanceService.cs | 4 +- .../Services/InstanceServiceTests.cs | 100 ++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/InstanceService.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/InstanceService.cs index 1c69a847..1f859bca 100644 --- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/InstanceService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/InstanceService.cs @@ -172,6 +172,7 @@ public class InstanceService if (existingOverride != null) { existingOverride.OverrideValue = overrideValue; + existingOverride.ElementDataType = templateAttr.ElementDataType; await _repository.UpdateInstanceAttributeOverrideAsync(existingOverride, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); @@ -185,7 +186,8 @@ public class InstanceService var newOverride = new InstanceAttributeOverride(attributeName) { InstanceId = instanceId, - OverrideValue = overrideValue + OverrideValue = overrideValue, + ElementDataType = templateAttr.ElementDataType }; await _repository.AddInstanceAttributeOverrideAsync(newOverride, cancellationToken); diff --git a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Services/InstanceServiceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Services/InstanceServiceTests.cs index b1cbc66b..b7e0a7fc 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Services/InstanceServiceTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Services/InstanceServiceTests.cs @@ -269,6 +269,106 @@ public class InstanceServiceTests _repoMock.Verify(r => r.AddInstanceConnectionBindingAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); } + // --- NJ-2: ElementDataType stamping on SetAttributeOverrideAsync --- + + [Fact] + public async Task SetAttributeOverride_CreatePath_ListAttribute_StampsElementDataType() + { + 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("Tags") + { + IsLocked = false, + DataType = DataType.List, + ElementDataType = DataType.String + } + }); + _repoMock.Setup(r => r.GetOverridesByInstanceIdAsync(1, It.IsAny())) + .ReturnsAsync(new List()); // no existing override → create path + _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny())) + .ReturnsAsync(1); + + InstanceAttributeOverride? captured = null; + _repoMock.Setup(r => r.AddInstanceAttributeOverrideAsync(It.IsAny(), It.IsAny())) + .Callback((o, _) => captured = o); + + var result = await _sut.SetAttributeOverrideAsync(1, "Tags", "[\"a\"]", "admin"); + + Assert.True(result.IsSuccess); + Assert.NotNull(captured); + Assert.Equal(DataType.String, captured.ElementDataType); + } + + [Fact] + public async Task SetAttributeOverride_UpdatePath_ListAttribute_StampsElementDataType() + { + 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("Tags") + { + IsLocked = false, + DataType = DataType.List, + ElementDataType = DataType.String + } + }); + + var existingOverride = new InstanceAttributeOverride("Tags") { Id = 42, InstanceId = 1, OverrideValue = "[\"old\"]" }; + _repoMock.Setup(r => r.GetOverridesByInstanceIdAsync(1, It.IsAny())) + .ReturnsAsync(new List { existingOverride }); // pre-existing → update path + _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny())) + .ReturnsAsync(1); + + InstanceAttributeOverride? captured = null; + _repoMock.Setup(r => r.UpdateInstanceAttributeOverrideAsync(It.IsAny(), It.IsAny())) + .Callback((o, _) => captured = o); + + var result = await _sut.SetAttributeOverrideAsync(1, "Tags", "[\"new\"]", "admin"); + + Assert.True(result.IsSuccess); + Assert.NotNull(captured); + Assert.Equal(DataType.String, captured.ElementDataType); + } + + [Fact] + public async Task SetAttributeOverride_CreatePath_ScalarAttribute_ElementDataTypeIsNull() + { + 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, + DataType = DataType.Float, + ElementDataType = null + } + }); + _repoMock.Setup(r => r.GetOverridesByInstanceIdAsync(1, It.IsAny())) + .ReturnsAsync(new List()); + _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny())) + .ReturnsAsync(1); + + InstanceAttributeOverride? captured = null; + _repoMock.Setup(r => r.AddInstanceAttributeOverrideAsync(It.IsAny(), It.IsAny())) + .Callback((o, _) => captured = o); + + var result = await _sut.SetAttributeOverrideAsync(1, "Threshold", "3.14", "admin"); + + Assert.True(result.IsSuccess); + Assert.NotNull(captured); + Assert.Null(captured.ElementDataType); + } + [Fact] public async Task AssignToArea_AreaInDifferentSite_ReturnsFailure() {