using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; /// /// Gitea #551: gained an accept gate for its /// rejection diagnosis — see ModbusInspectAcceptGateTests's remarks for why both /// directions are pinned. /// /// FOCAS is the one factory that deliberately does NOT return empty on accept: two of its warnings /// are advisories about blobs the mapper ACCEPTS but the driver still cannot serve as /// authored — an unparseable FOCAS address (#540, dropped at the driver's own init) and an authored /// writable:true (the wire is read-only). Folding FOCAS into the plain "accept ⇒ empty" /// shape would have silently deleted both, so they are pinned here alongside the new rejections. /// /// public sealed class FocasInspectAcceptGateTests { private const string RawPath = "cell/focas/dev1/T1"; /// Blobs the mapper ACCEPTS and that carry no advisory — the gate must be silent. public static TheoryData CleanAndAccepted() => [ """{"address":"R100","dataType":"Int32"}""", """{"address":"X0.0","dataType":"Bit"}""", """{"address":"PARAM:1020"}""", """{"address":"FIXED:Identity/SeriesNumber","dataType":"String"}""", ]; /// Blobs the mapper REJECTS — the gate must produce at least one warning for each. public static TheoryData Rejected() => [ """{"dataType":"Int32"}""", // no address """{"address":"","dataType":"Int32"}""", // blank address """{"address":100}""", // address not a string """{"address":"R100","dataType":"Intt32"}""", // typo'd dataType ]; [Theory] [MemberData(nameof(CleanAndAccepted))] public void A_clean_config_the_mapper_accepts_draws_no_warning(string tagConfig) { FocasTagDefinitionFactory.FromTagConfig(tagConfig, RawPath, out _).ShouldBeTrue("fixture must be accepted"); FocasTagDefinitionFactory.Inspect(tagConfig).ShouldBeEmpty(); } [Theory] [MemberData(nameof(Rejected))] public void A_config_the_mapper_rejects_is_never_passed_over_in_silence(string tagConfig) { FocasTagDefinitionFactory.FromTagConfig(tagConfig, RawPath, out _).ShouldBeFalse("fixture must be rejected"); FocasTagDefinitionFactory.Inspect(tagConfig).ShouldNotBeEmpty(); } [Fact] public void An_absent_address_is_named() => FocasTagDefinitionFactory.Inspect("""{"dataType":"Int32"}""") .ShouldContain(w => w.Contains("'address'")); // ---- The two advisories the accept gate must NOT swallow ---- [Fact] public void An_unparseable_address_still_warns_even_though_the_mapper_accepts_it() { const string Blob = """{"address":"not-an-address","dataType":"Int32"}"""; FocasTagDefinitionFactory.FromTagConfig(Blob, RawPath, out _).ShouldBeTrue("the mapper accepts any non-blank address"); FocasTagDefinitionFactory.Inspect(Blob).ShouldContain(w => w.Contains("not parseable")); } [Fact] public void An_authored_writable_still_warns_even_though_the_mapper_accepts_it() { const string Blob = """{"address":"R100","dataType":"Int32","writable":true}"""; FocasTagDefinitionFactory.FromTagConfig(Blob, RawPath, out _).ShouldBeTrue("the mapper accepts writable:true"); FocasTagDefinitionFactory.Inspect(Blob).ShouldContain(w => w.Contains("read-only")); } /// A rejected blob still gets its advisories — the two diagnoses are independent. [Fact] public void A_rejected_config_reports_the_rejection_and_the_advisory_together() { var warnings = FocasTagDefinitionFactory.Inspect( """{"address":"not-an-address","dataType":"Intt32","writable":true}"""); warnings.ShouldContain(w => w.Contains("dataType")); warnings.ShouldContain(w => w.Contains("not parseable")); warnings.ShouldContain(w => w.Contains("read-only")); } [Fact] public void A_blank_reference_draws_nothing() => FocasTagDefinitionFactory.Inspect(" ").ShouldBeEmpty(); [Fact] public void Unparseable_json_names_the_json() => FocasTagDefinitionFactory.Inspect("{oops").ShouldContain(w => w.Contains("not valid JSON")); }