feat(transport): serialize site/connection/instance entities<->DTOs (M8 B2)
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Transport.Serialization;
|
||||
@@ -406,4 +408,214 @@ public sealed class EntitySerializerTests
|
||||
Assert.Equal(DataType.Double, rtAttr.DataType);
|
||||
Assert.Null(rtAttr.ElementDataType);
|
||||
}
|
||||
|
||||
// --- M8 (B2): site / data-connection / instance mapping ------------------
|
||||
|
||||
/// <summary>
|
||||
/// Builds a populated aggregate: one template (so the instance's TemplateName
|
||||
/// resolves), one site, one OPC UA data connection on that site, and one
|
||||
/// instance with one of each child collection (attribute/alarm/native-alarm-
|
||||
/// source override + a connection binding pointing at the data connection).
|
||||
/// </summary>
|
||||
private static EntityAggregate MakePopulatedSiteAggregate()
|
||||
{
|
||||
var template = new Template("Pump") { Id = 7 };
|
||||
|
||||
var site = new Site("North Plant", "north")
|
||||
{
|
||||
Id = 3,
|
||||
Description = "primary",
|
||||
NodeAAddress = "akka://sys@10.0.0.1:4053",
|
||||
NodeBAddress = "akka://sys@10.0.0.2:4053",
|
||||
GrpcNodeAAddress = "https://10.0.0.1:8083",
|
||||
GrpcNodeBAddress = "https://10.0.0.2:8083",
|
||||
};
|
||||
|
||||
var conn = new DataConnection("PlcLink", "OpcUa", siteId: 3)
|
||||
{
|
||||
Id = 11,
|
||||
FailoverRetryCount = 5,
|
||||
PrimaryConfiguration = "{\"endpoint\":\"opc.tcp://plc1:4840\",\"password\":\"s3cret\"}",
|
||||
BackupConfiguration = "{\"endpoint\":\"opc.tcp://plc2:4840\"}",
|
||||
};
|
||||
|
||||
var inst = new Instance("Pump-001")
|
||||
{
|
||||
Id = 21,
|
||||
TemplateId = 7,
|
||||
SiteId = 3,
|
||||
State = InstanceState.Enabled,
|
||||
};
|
||||
inst.AttributeOverrides.Add(new InstanceAttributeOverride("Setpoint")
|
||||
{
|
||||
InstanceId = 21,
|
||||
OverrideValue = "55",
|
||||
});
|
||||
inst.AlarmOverrides.Add(new InstanceAlarmOverride("HighPressure")
|
||||
{
|
||||
InstanceId = 21,
|
||||
TriggerConfigurationOverride = "{\"hi\":90}",
|
||||
PriorityLevelOverride = 1,
|
||||
});
|
||||
inst.NativeAlarmSourceOverrides.Add(new InstanceNativeAlarmSourceOverride("PlcAlarms")
|
||||
{
|
||||
InstanceId = 21,
|
||||
SourceReferenceOverride = "ns=2;s=Pump1",
|
||||
});
|
||||
inst.ConnectionBindings.Add(new InstanceConnectionBinding("Pressure")
|
||||
{
|
||||
InstanceId = 21,
|
||||
DataConnectionId = 11,
|
||||
DataSourceReferenceOverride = "ns=2;s=Pump1.P",
|
||||
});
|
||||
|
||||
return MakeEmptyAggregate() with
|
||||
{
|
||||
Templates = new[] { template },
|
||||
Sites = new[] { site },
|
||||
DataConnections = new[] { conn },
|
||||
Instances = new[] { inst },
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToDto_maps_site_connection_instance_with_secrets_and_name_links()
|
||||
{
|
||||
var aggregate = MakePopulatedSiteAggregate();
|
||||
|
||||
var dto = new EntitySerializer().ToBundleContent(aggregate);
|
||||
|
||||
// Site: straight field copy, referenced by SiteIdentifier downstream.
|
||||
var dtoSite = Assert.Single(dto.Sites);
|
||||
Assert.Equal("north", dtoSite.SiteIdentifier);
|
||||
Assert.Equal("North Plant", dtoSite.Name);
|
||||
Assert.Equal("akka://sys@10.0.0.1:4053", dtoSite.NodeAAddress);
|
||||
Assert.Equal("https://10.0.0.2:8083", dtoSite.GrpcNodeBAddress);
|
||||
|
||||
// Data connection: owning site resolved to SiteIdentifier; the protocol
|
||||
// config lands in the SecretsBlock — NOT in any public field of the DTO.
|
||||
var dtoConn = Assert.Single(dto.DataConnections);
|
||||
Assert.Equal("north", dtoConn.SiteIdentifier);
|
||||
Assert.Equal("PlcLink", dtoConn.Name);
|
||||
Assert.Equal("OpcUa", dtoConn.Protocol);
|
||||
Assert.Equal(5, dtoConn.FailoverRetryCount);
|
||||
Assert.NotNull(dtoConn.Secrets);
|
||||
Assert.Equal(
|
||||
"{\"endpoint\":\"opc.tcp://plc1:4840\",\"password\":\"s3cret\"}",
|
||||
dtoConn.Secrets!.Values["PrimaryConfiguration"]);
|
||||
Assert.Equal(
|
||||
"{\"endpoint\":\"opc.tcp://plc2:4840\"}",
|
||||
dtoConn.Secrets.Values["BackupConfiguration"]);
|
||||
// The secret must not leak into any non-secret string property of the DTO.
|
||||
Assert.DoesNotContain("s3cret", dtoConn.SiteIdentifier);
|
||||
Assert.DoesNotContain("s3cret", dtoConn.Name);
|
||||
Assert.DoesNotContain("s3cret", dtoConn.Protocol);
|
||||
|
||||
// Instance: template + site resolved by name/identifier (never numeric id).
|
||||
var dtoInst = Assert.Single(dto.Instances);
|
||||
Assert.Equal("Pump-001", dtoInst.UniqueName);
|
||||
Assert.Equal("Pump", dtoInst.TemplateName);
|
||||
Assert.Equal("north", dtoInst.SiteIdentifier);
|
||||
Assert.Equal(InstanceState.Enabled, dtoInst.State);
|
||||
// Areas don't travel yet (no Areas collection on the aggregate).
|
||||
Assert.Null(dtoInst.AreaName);
|
||||
|
||||
// All four child collections survive.
|
||||
var dtoAttr = Assert.Single(dtoInst.AttributeOverrides);
|
||||
Assert.Equal("Setpoint", dtoAttr.AttributeName);
|
||||
Assert.Equal("55", dtoAttr.OverrideValue);
|
||||
|
||||
var dtoAlarm = Assert.Single(dtoInst.AlarmOverrides);
|
||||
Assert.Equal("HighPressure", dtoAlarm.AlarmCanonicalName);
|
||||
Assert.Equal("{\"hi\":90}", dtoAlarm.TriggerConfigurationOverride);
|
||||
Assert.Equal(1, dtoAlarm.PriorityLevelOverride);
|
||||
|
||||
var dtoNative = Assert.Single(dtoInst.NativeAlarmSourceOverrides);
|
||||
Assert.Equal("PlcAlarms", dtoNative.SourceCanonicalName);
|
||||
Assert.Equal("ns=2;s=Pump1", dtoNative.SourceReferenceOverride);
|
||||
|
||||
// Binding carries the RESOLVED connection NAME (not the numeric FK id).
|
||||
var dtoBinding = Assert.Single(dtoInst.ConnectionBindings);
|
||||
Assert.Equal("Pressure", dtoBinding.AttributeName);
|
||||
Assert.Equal("PlcLink", dtoBinding.ConnectionName);
|
||||
Assert.Equal("ns=2;s=Pump1.P", dtoBinding.DataSourceReferenceOverride);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToDto_omits_secret_key_when_backup_configuration_is_empty()
|
||||
{
|
||||
var aggregate = MakeEmptyAggregate() with
|
||||
{
|
||||
Sites = new[] { new Site("S", "s") { Id = 1 } },
|
||||
DataConnections = new[]
|
||||
{
|
||||
new DataConnection("c", "OpcUa", siteId: 1)
|
||||
{
|
||||
Id = 1,
|
||||
PrimaryConfiguration = "{\"e\":\"x\"}",
|
||||
BackupConfiguration = null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var dto = new EntitySerializer().ToBundleContent(aggregate);
|
||||
|
||||
var dtoConn = Assert.Single(dto.DataConnections);
|
||||
Assert.NotNull(dtoConn.Secrets);
|
||||
Assert.True(dtoConn.Secrets!.Values.ContainsKey("PrimaryConfiguration"));
|
||||
// Null source value => key omitted entirely (not an empty string).
|
||||
Assert.False(dtoConn.Secrets.Values.ContainsKey("BackupConfiguration"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Roundtrip_site_connection_instance_restores_config_from_secrets_and_binding_name()
|
||||
{
|
||||
var sut = new EntitySerializer();
|
||||
var dto = sut.ToBundleContent(MakePopulatedSiteAggregate());
|
||||
|
||||
var rt = sut.FromBundleContent(dto);
|
||||
|
||||
// Site reconstructed with a synthetic id; identity + addresses preserved.
|
||||
var rtSite = Assert.Single(rt.Sites);
|
||||
Assert.Equal("north", rtSite.SiteIdentifier);
|
||||
Assert.Equal("North Plant", rtSite.Name);
|
||||
Assert.Equal("akka://sys@10.0.0.1:4053", rtSite.NodeAAddress);
|
||||
|
||||
// Data connection: protocol config restored from the SecretsBlock; owning
|
||||
// site FK resolved to the reconstructed site's synthetic id.
|
||||
var rtConn = Assert.Single(rt.DataConnections);
|
||||
Assert.Equal("PlcLink", rtConn.Name);
|
||||
Assert.Equal("OpcUa", rtConn.Protocol);
|
||||
Assert.Equal(5, rtConn.FailoverRetryCount);
|
||||
Assert.Equal(rtSite.Id, rtConn.SiteId);
|
||||
Assert.Equal(
|
||||
"{\"endpoint\":\"opc.tcp://plc1:4840\",\"password\":\"s3cret\"}",
|
||||
rtConn.PrimaryConfiguration);
|
||||
Assert.Equal("{\"endpoint\":\"opc.tcp://plc2:4840\"}", rtConn.BackupConfiguration);
|
||||
|
||||
// Instance: template + site FKs resolved to the reconstructed synthetic ids;
|
||||
// Area never travels so AreaId stays null.
|
||||
var rtInst = Assert.Single(rt.Instances);
|
||||
Assert.Equal("Pump-001", rtInst.UniqueName);
|
||||
Assert.Equal(InstanceState.Enabled, rtInst.State);
|
||||
Assert.Equal(Assert.Single(rt.Templates).Id, rtInst.TemplateId);
|
||||
Assert.Equal(rtSite.Id, rtInst.SiteId);
|
||||
Assert.Null(rtInst.AreaId);
|
||||
|
||||
// Child overrides survive the round-trip.
|
||||
Assert.Equal("Setpoint", Assert.Single(rtInst.AttributeOverrides).AttributeName);
|
||||
Assert.Equal("HighPressure", Assert.Single(rtInst.AlarmOverrides).AlarmCanonicalName);
|
||||
Assert.Equal("PlcAlarms", Assert.Single(rtInst.NativeAlarmSourceOverrides).SourceCanonicalName);
|
||||
|
||||
// The binding's resolved ConnectionName is preserved on the wire DTO so the
|
||||
// importer (D1) can resolve it to a target-DB FK at apply time. The
|
||||
// reconstructed entity carries AttributeName + override but leaves the
|
||||
// numeric DataConnectionId at its unresolved placeholder (0).
|
||||
var dtoBinding = Assert.Single(Assert.Single(dto.Instances).ConnectionBindings);
|
||||
Assert.Equal("PlcLink", dtoBinding.ConnectionName);
|
||||
var rtBinding = Assert.Single(rtInst.ConnectionBindings);
|
||||
Assert.Equal("Pressure", rtBinding.AttributeName);
|
||||
Assert.Equal("ns=2;s=Pump1.P", rtBinding.DataSourceReferenceOverride);
|
||||
Assert.Equal(0, rtBinding.DataConnectionId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user