using Bunit; using ScadaLink.Commons.Types.DataConnections; using ScadaLink.Commons.Types.Flattening; using ScadaLink.Commons.Validators; using OpcUaEndpointEditor = ScadaLink.CentralUI.Components.Forms.OpcUaEndpointEditor; namespace ScadaLink.CentralUI.Tests.Forms; public class OpcUaEndpointEditorTests : BunitContext { [Fact] public void Renders_All_Four_Section_Labels() { var config = new OpcUaEndpointConfig(); var cut = Render(p => p .Add(c => c.Config, config) .Add(c => c.Title, "Primary Endpoint")); Assert.Contains("Primary Endpoint", cut.Markup); Assert.Contains("Timing", cut.Markup); Assert.Contains("Subscription", cut.Markup); Assert.Contains("Heartbeat", cut.Markup); } [Fact] public void Binding_MutatesPassedConfigInstance() { var config = new OpcUaEndpointConfig(); var cut = Render(p => p.Add(c => c.Config, config)); cut.Find("input[type='text']").Change("opc.tcp://new-host:4840"); Assert.Equal("opc.tcp://new-host:4840", config.EndpointUrl); } [Fact] public void EnableHeartbeat_CreatesSubObject() { var config = new OpcUaEndpointConfig(); var cut = Render(p => p.Add(c => c.Config, config)); Assert.Null(config.Heartbeat); cut.FindAll("button").First(b => b.TextContent.Contains("Enable Heartbeat")).Click(); Assert.NotNull(config.Heartbeat); } [Fact] public void RemoveHeartbeat_NullsSubObject() { var config = new OpcUaEndpointConfig { Heartbeat = new OpcUaHeartbeatConfig { TagPath = "Hb", MaxSilenceSeconds = 30 } }; var cut = Render(p => p.Add(c => c.Config, config)); cut.FindAll("button").First(b => b.TextContent.Contains("Remove Heartbeat")).Click(); Assert.Null(config.Heartbeat); } [Fact] public void Errors_Parameter_RendersPerFieldRedText() { var config = new OpcUaEndpointConfig { EndpointUrl = "" }; var errors = OpcUaEndpointConfigValidator.Validate(config, "Primary."); var cut = Render(p => p .Add(c => c.Config, config) .Add(c => c.Errors, errors)); Assert.Contains("Endpoint URL is required.", cut.Markup); Assert.Contains("text-danger", cut.Markup); } [Fact] public void IsLegacy_True_RendersWarningBanner() { var cut = Render(p => p .Add(c => c.Config, new OpcUaEndpointConfig()) .Add(c => c.IsLegacy, true)); Assert.Contains("alert-warning", cut.Markup); Assert.Contains("migrated from a legacy format", cut.Markup); } // ── Layer E: new editor sections ── [Fact] public void Renders_Authentication_Section_Label() { var cut = Render(p => p.Add(c => c.Config, new OpcUaEndpointConfig())); Assert.Contains("Authentication", cut.Markup); } [Fact] public void EnableAuthentication_CreatesUserIdentitySubObject() { var config = new OpcUaEndpointConfig(); var cut = Render(p => p.Add(c => c.Config, config)); Assert.Null(config.UserIdentity); cut.FindAll("button").First(b => b.TextContent.Contains("Enable Authentication")).Click(); Assert.NotNull(config.UserIdentity); Assert.Equal(OpcUaUserTokenType.Anonymous, config.UserIdentity!.TokenType); } [Fact] public void RemoveAuthentication_NullsUserIdentity() { var config = new OpcUaEndpointConfig { UserIdentity = new OpcUaUserIdentityConfig { TokenType = OpcUaUserTokenType.UsernamePassword, Username = "alice", Password = "secret" } }; var cut = Render(p => p.Add(c => c.Config, config)); cut.FindAll("button").First(b => b.TextContent.Contains("Remove Authentication")).Click(); Assert.Null(config.UserIdentity); } [Fact] public void UsernamePassword_RendersUsernameAndPasswordInputs() { var config = new OpcUaEndpointConfig { UserIdentity = new OpcUaUserIdentityConfig { TokenType = OpcUaUserTokenType.UsernamePassword } }; var cut = Render(p => p.Add(c => c.Config, config)); // only renders for the UsernamePassword branch Assert.Contains(">Username<", cut.Markup); Assert.Contains(">Password<", cut.Markup); Assert.DoesNotContain(">Certificate path<", cut.Markup); } [Fact] public void X509Certificate_RendersCertificateFields() { var config = new OpcUaEndpointConfig { UserIdentity = new OpcUaUserIdentityConfig { TokenType = OpcUaUserTokenType.X509Certificate } }; var cut = Render(p => p.Add(c => c.Config, config)); Assert.Contains(">Certificate path<", cut.Markup); Assert.Contains(">Certificate password<", cut.Markup); Assert.DoesNotContain(">Username<", cut.Markup); } [Fact] public void AnonymousTokenType_ShowsNoExtraFields() { var config = new OpcUaEndpointConfig { UserIdentity = new OpcUaUserIdentityConfig { TokenType = OpcUaUserTokenType.Anonymous } }; var cut = Render(p => p.Add(c => c.Config, config)); Assert.DoesNotContain(">Username<", cut.Markup); Assert.DoesNotContain(">Certificate path<", cut.Markup); } [Fact] public void EnableDeadband_CreatesDeadbandSubObject() { var config = new OpcUaEndpointConfig(); var cut = Render(p => p.Add(c => c.Config, config)); Assert.Null(config.Deadband); cut.FindAll("button").First(b => b.TextContent.Contains("Enable Deadband")).Click(); Assert.NotNull(config.Deadband); } [Fact] public void RemoveDeadband_NullsDeadband() { var config = new OpcUaEndpointConfig { Deadband = new OpcUaDeadbandConfig { Type = OpcUaDeadbandType.Percent, Value = 1.5 } }; var cut = Render(p => p.Add(c => c.Config, config)); cut.FindAll("button").First(b => b.TextContent.Contains("Remove Deadband")).Click(); Assert.Null(config.Deadband); } [Fact] public void AdvancedSubscription_Section_Renders() { var config = new OpcUaEndpointConfig(); var cut = Render(p => p.Add(c => c.Config, config)); Assert.Contains("Discard oldest", cut.Markup); Assert.Contains("Subscription display name", cut.Markup); Assert.Contains("Subscription priority", cut.Markup); Assert.Contains("Timestamps to return", cut.Markup); } [Fact] public void UserIdentityError_RendersPerFieldUnderUsername() { var config = new OpcUaEndpointConfig { UserIdentity = new OpcUaUserIdentityConfig { TokenType = OpcUaUserTokenType.UsernamePassword, Username = "" } }; var errors = OpcUaEndpointConfigValidator.Validate(config, "Primary."); var cut = Render(p => p .Add(c => c.Config, config) .Add(c => c.Errors, errors)); Assert.Contains("Username is required", cut.Markup); } }