fix(adminui): S7 typed page no longer wipes Tags on save

- S7DriverPage.FormModel now preserves Tags through Form ↔ Options
  translation (was hard-coding Tags = [] on every save, silently
  destroying any tag list that operators had configured).
- Add FormModel_RoundTrip tests for OpcUaClient and Historian
  mirror classes — both were translating Options ↔ form-model
  entirely untested.
- Surface S7 Tags in the round-trip test so this regression
  can't reach merge again.
This commit is contained in:
Joseph Doherty
2026-05-28 10:06:43 -04:00
parent a971db3ee5
commit c4086c243c
4 changed files with 124 additions and 2 deletions
@@ -1,7 +1,9 @@
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers;
using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests;
@@ -79,4 +81,76 @@ public sealed class OpcUaClientDriverPageFormSerializationTests
back.ShouldNotBeNull();
back.ProbeTimeoutSeconds.ShouldBe(20);
}
[Fact]
public void FormModel_RoundTrip_PreservesAllFields()
{
// Construct options with non-default values for every editable property plus
// non-empty EndpointUrls and UnsMappingTable — both are "read-only" in the form
// but must survive the FormModel translation unchanged.
var endpointUrls = new List<string> { "opc.tcp://primary:4840", "opc.tcp://backup:4840" };
var unsMappingTable = new Dictionary<string, string>
{
["Line1/"] = "Site/Area1/Line1",
["Line2/"] = "Site/Area1/Line2",
};
var original = new OpcUaClientDriverOptions
{
EndpointUrl = "opc.tcp://fallback:4840",
EndpointUrls = endpointUrls,
ApplicationUri = "urn:test:OtOpcUa:GatewayClient",
SessionName = "TestSession",
SecurityMode = OpcUaSecurityMode.SignAndEncrypt,
SecurityPolicy = OpcUaSecurityPolicy.Basic256Sha256,
AuthType = OpcUaAuthType.Username,
Username = "opcuser",
Password = "p@ssw0rd",
UserCertificatePath = @"C:\certs\user.pfx",
UserCertificatePassword = "certpass",
PerEndpointConnectTimeout = TimeSpan.FromSeconds(4),
Timeout = TimeSpan.FromSeconds(12),
SessionTimeout = TimeSpan.FromSeconds(150),
KeepAliveInterval = TimeSpan.FromSeconds(7),
ReconnectPeriod = TimeSpan.FromSeconds(8),
AutoAcceptCertificates = true,
BrowseRoot = "i=85",
MaxDiscoveredNodes = 3000,
MaxBrowseDepth = 5,
TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform,
UnsMappingTable = unsMappingTable,
ProbeTimeoutSeconds = 25,
};
var form = OpcUaClientDriverPage.OpcUaClientFormModel.FromRecord(original);
var result = form.ToRecord();
result.EndpointUrl.ShouldBe("opc.tcp://fallback:4840");
result.EndpointUrls.Count.ShouldBe(2);
result.EndpointUrls[0].ShouldBe("opc.tcp://primary:4840");
result.EndpointUrls[1].ShouldBe("opc.tcp://backup:4840");
result.ApplicationUri.ShouldBe("urn:test:OtOpcUa:GatewayClient");
result.SessionName.ShouldBe("TestSession");
result.SecurityMode.ShouldBe(OpcUaSecurityMode.SignAndEncrypt);
result.SecurityPolicy.ShouldBe(OpcUaSecurityPolicy.Basic256Sha256);
result.AuthType.ShouldBe(OpcUaAuthType.Username);
result.Username.ShouldBe("opcuser");
result.Password.ShouldBe("p@ssw0rd");
result.UserCertificatePath.ShouldBe(@"C:\certs\user.pfx");
result.UserCertificatePassword.ShouldBe("certpass");
result.PerEndpointConnectTimeout.ShouldBe(TimeSpan.FromSeconds(4));
result.Timeout.ShouldBe(TimeSpan.FromSeconds(12));
result.SessionTimeout.ShouldBe(TimeSpan.FromSeconds(150));
result.KeepAliveInterval.ShouldBe(TimeSpan.FromSeconds(7));
result.ReconnectPeriod.ShouldBe(TimeSpan.FromSeconds(8));
result.AutoAcceptCertificates.ShouldBeTrue();
result.BrowseRoot.ShouldBe("i=85");
result.MaxDiscoveredNodes.ShouldBe(3000);
result.MaxBrowseDepth.ShouldBe(5);
result.TargetNamespaceKind.ShouldBe(OpcUaTargetNamespaceKind.SystemPlatform);
result.UnsMappingTable.Count.ShouldBe(2);
result.UnsMappingTable["Line1/"].ShouldBe("Site/Area1/Line1");
result.UnsMappingTable["Line2/"].ShouldBe("Site/Area1/Line2");
result.ProbeTimeoutSeconds.ShouldBe(25);
}
}