using System.Text.Json; using System.Text.Json.Serialization; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Forms; using ZB.MOM.WW.OtOpcUa.Driver.S7; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests; // v3 Batch-1 migration: the S7 driver page dropped its pre-declared Tags editor — tags now live in // the Raw tree (/raw, Batch 2), and S7DriverOptions.Tags (the typed S7TagDefinition list) + // S7DriverForm.S7TagRow were retired (the driver now consumes RawTags). The former tag-editor and // tag-serialization tests (S7TagRow_*, TagList_SerializeRoundTrip_PreservesTags, and the tag legs of // the round-trip tests) were removed. The connection/protocol-field + enum-serialization round-trip // coverage (CpuType, S7DataType) is retained below unchanged. public sealed class S7DriverPageFormSerializationTests { private static readonly JsonSerializerOptions _opts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false, }; [Fact] public void RoundTrip_PreservesKnownFields() { var original = new S7DriverOptions { Host = "10.0.0.5", Port = 102, CpuType = S7CpuType.S71200, Rack = 0, Slot = 1, Timeout = TimeSpan.FromSeconds(10), Probe = new S7ProbeOptions { Enabled = false, Interval = TimeSpan.FromSeconds(15), Timeout = TimeSpan.FromSeconds(3), }, ProbeTimeoutSeconds = 30, }; var json = JsonSerializer.Serialize(original, _opts); var back = JsonSerializer.Deserialize(json, _opts); back.ShouldNotBeNull(); back.Host.ShouldBe("10.0.0.5"); back.Port.ShouldBe(102); back.CpuType.ShouldBe(S7CpuType.S71200); back.Rack.ShouldBe((short)0); back.Slot.ShouldBe((short)1); back.Timeout.ShouldBe(TimeSpan.FromSeconds(10)); back.Probe.ShouldNotBeNull(); back.Probe.Enabled.ShouldBeFalse(); back.Probe.Interval.ShouldBe(TimeSpan.FromSeconds(15)); back.Probe.Timeout.ShouldBe(TimeSpan.FromSeconds(3)); back.ProbeTimeoutSeconds.ShouldBe(30); } [Fact] public void Deserialize_DropsUnknownFields() { var jsonWithExtra = """{"unknownField":"old-value","probeTimeoutSeconds":12}"""; var optsSkip = new JsonSerializerOptions(_opts) { UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, }; var back = JsonSerializer.Deserialize(jsonWithExtra, optsSkip); back.ShouldNotBeNull(); back.ProbeTimeoutSeconds.ShouldBe(12); } [Fact] public void FormModel_RoundTrip_PreservesEditableFields() { var opts = new S7DriverOptions { Host = "192.168.1.50", Port = 102, CpuType = S7CpuType.S7300, Rack = 0, Slot = 2, Timeout = TimeSpan.FromSeconds(7), Probe = new S7ProbeOptions { Enabled = true, Interval = TimeSpan.FromSeconds(8), Timeout = TimeSpan.FromSeconds(4), }, ProbeTimeoutSeconds = 20, }; var form = S7DriverForm.FormModel.FromOptions(opts); var roundTripped = form.ToOptions(); roundTripped.Host.ShouldBe("192.168.1.50"); roundTripped.Port.ShouldBe(102); roundTripped.CpuType.ShouldBe(S7CpuType.S7300); roundTripped.Rack.ShouldBe((short)0); roundTripped.Slot.ShouldBe((short)2); roundTripped.Timeout.ShouldBe(TimeSpan.FromSeconds(7)); roundTripped.Probe.Enabled.ShouldBeTrue(); roundTripped.Probe.Interval.ShouldBe(TimeSpan.FromSeconds(8)); roundTripped.Probe.Timeout.ShouldBe(TimeSpan.FromSeconds(4)); roundTripped.ProbeTimeoutSeconds.ShouldBe(20); } [Fact] public void FormModel_RoundTrip_PreservesTsapOverrides() { var opts = new S7DriverOptions { Host = "192.168.1.50", LocalTsap = "01.00", RemoteTsap = "03.01", }; var back = S7DriverForm.FormModel.FromOptions(opts).ToOptions(); back.LocalTsap.ShouldBe("01.00"); back.RemoteTsap.ShouldBe("03.01"); back.ConnectionType.ShouldBeNull(); var withType = S7DriverForm.FormModel.FromOptions(new S7DriverOptions { ConnectionType = S7ConnectionType.OP }).ToOptions(); withType.ConnectionType.ShouldBe(S7ConnectionType.OP); // Defaults stay absent: a blank form must not invent an override. var blank = S7DriverForm.FormModel.FromOptions(new S7DriverOptions()).ToOptions(); blank.LocalTsap.ShouldBeNull(); blank.RemoteTsap.ShouldBeNull(); blank.ConnectionType.ShouldBeNull(); } [Fact] public void ConnectionType_serialises_as_a_string_the_factory_binds() { var json = JsonSerializer.Serialize( new S7DriverOptions { ConnectionType = S7ConnectionType.PG }, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Converters = { new JsonStringEnumConverter() } }); json.ShouldContain("\"connectionType\":\"PG\""); } [Fact] public void Numeric_tsap_loads_into_the_form_model_instead_of_falling_back_to_defaults() { // The factory accepts a JSON number; the form binds the same document the same way, so a Save // cannot silently replace the authored config with defaults. var opts = JsonSerializer.Deserialize( """{"remoteTsap":769,"localTsap":"01.00","cpuType":"S7300"}""", new JsonSerializerOptions { PropertyNameCaseInsensitive = true, Converters = { new JsonStringEnumConverter() } }); opts.ShouldNotBeNull(); var back = S7DriverForm.FormModel.FromOptions(opts).ToOptions(); back.RemoteTsap.ShouldBe("03.01"); back.LocalTsap.ShouldBe("01.00"); back.CpuType.ShouldBe(S7CpuType.S7300); } [Fact] public void Form_reports_the_factory_conflict_error_for_remote_tsap_plus_connection_type() { var form = new S7DriverForm.FormModel { RemoteTsap = "03.01", ConnectionTypeText = "PG" }; form.TsapConflictError().ShouldNotBeNull().ShouldContain(S7DriverOptions.TsapConflictDetail); new S7DriverForm.FormModel { RemoteTsap = "03.01" }.TsapConflictError().ShouldBeNull(); new S7DriverForm.FormModel { ConnectionTypeText = "PG" }.TsapConflictError().ShouldBeNull(); } }