using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Hosting; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; /// /// Task #220 — covers the DriverConfig JSON contract that /// parses when the bootstrap /// pipeline (task #248) materialises FOCAS DriverInstance rows. Pure unit tests, no pipe /// or CNC required. /// [Trait("Category", "Unit")] public sealed class FocasDriverFactoryExtensionsTests { [Fact] public void Register_adds_FOCAS_entry_to_registry() { var registry = new DriverFactoryRegistry(); FocasDriverFactoryExtensions.Register(registry); registry.TryGet("FOCAS").ShouldNotBeNull(); } [Fact] public void Register_is_case_insensitive_via_registry() { var registry = new DriverFactoryRegistry(); FocasDriverFactoryExtensions.Register(registry); registry.TryGet("focas").ShouldNotBeNull(); registry.TryGet("Focas").ShouldNotBeNull(); } [Fact] public void CreateInstance_with_ipc_backend_and_valid_config_returns_FocasDriver() { const string json = """ { "Backend": "ipc", "PipeName": "OtOpcUaFocasHost", "SharedSecret": "secret-for-test", "ConnectTimeoutMs": 5000, "Series": "Thirty_i", "TimeoutMs": 3000, "Devices": [ { "HostAddress": "focas://10.0.0.5:8193", "DeviceName": "Lathe1" } ], "Tags": [ { "Name": "Override", "DeviceHostAddress": "focas://10.0.0.5:8193", "Address": "R100", "DataType": "Int32", "Writable": true } ] } """; var driver = FocasDriverFactoryExtensions.CreateInstance("focas-0", json); driver.ShouldNotBeNull(); driver.DriverInstanceId.ShouldBe("focas-0"); driver.DriverType.ShouldBe("FOCAS"); } [Fact] public void CreateInstance_defaults_Backend_to_ipc_when_unspecified() { // No "Backend" key → defaults to ipc → requires PipeName + SharedSecret. const string json = """ { "PipeName": "p", "SharedSecret": "s" } """; var driver = FocasDriverFactoryExtensions.CreateInstance("focas-default", json); driver.DriverType.ShouldBe("FOCAS"); } [Fact] public void CreateInstance_ipc_backend_missing_PipeName_throws() { const string json = """{ "Backend": "ipc", "SharedSecret": "s" }"""; Should.Throw( () => FocasDriverFactoryExtensions.CreateInstance("focas-missing-pipe", json)) .Message.ShouldContain("PipeName"); } [Fact] public void CreateInstance_ipc_backend_missing_SharedSecret_throws() { const string json = """{ "Backend": "ipc", "PipeName": "p" }"""; Should.Throw( () => FocasDriverFactoryExtensions.CreateInstance("focas-missing-secret", json)) .Message.ShouldContain("SharedSecret"); } [Fact] public void CreateInstance_fwlib_backend_does_not_require_pipe_fields() { // Direct in-process Fwlib32 path. No pipe config needed; driver connects the DLL // natively on first use. const string json = """{ "Backend": "fwlib" }"""; var driver = FocasDriverFactoryExtensions.CreateInstance("focas-fwlib", json); driver.DriverInstanceId.ShouldBe("focas-fwlib"); } [Fact] public void CreateInstance_unimplemented_backend_yields_driver_that_fails_fast_on_use() { // Useful for staging DriverInstance rows in the config DB before the Host is // actually deployed — the server boots but reads/writes surface clear errors. const string json = """{ "Backend": "unimplemented" }"""; var driver = FocasDriverFactoryExtensions.CreateInstance("focas-unimpl", json); driver.DriverInstanceId.ShouldBe("focas-unimpl"); } [Fact] public void CreateInstance_unknown_backend_throws_with_expected_list() { const string json = """{ "Backend": "gibberish", "PipeName": "p", "SharedSecret": "s" }"""; Should.Throw( () => FocasDriverFactoryExtensions.CreateInstance("focas-bad-backend", json)) .Message.ShouldContain("gibberish"); } [Fact] public void CreateInstance_rejects_unknown_Series() { const string json = """ { "Backend": "fwlib", "Series": "NotARealSeries" } """; Should.Throw( () => FocasDriverFactoryExtensions.CreateInstance("focas-bad-series", json)) .Message.ShouldContain("NotARealSeries"); } [Fact] public void CreateInstance_rejects_tag_with_missing_DataType() { const string json = """ { "Backend": "fwlib", "Devices": [{ "HostAddress": "focas://1.1.1.1:8193" }], "Tags": [{ "Name": "Broken", "DeviceHostAddress": "focas://1.1.1.1:8193", "Address": "R1" }] } """; Should.Throw( () => FocasDriverFactoryExtensions.CreateInstance("focas-bad-tag", json)) .Message.ShouldContain("DataType"); } [Fact] public void CreateInstance_null_or_whitespace_args_rejected() { Should.Throw( () => FocasDriverFactoryExtensions.CreateInstance("", "{}")); Should.Throw( () => FocasDriverFactoryExtensions.CreateInstance("id", "")); } [Fact] public void Register_twice_throws() { var registry = new DriverFactoryRegistry(); FocasDriverFactoryExtensions.Register(registry); Should.Throw( () => FocasDriverFactoryExtensions.Register(registry)); } }