Files
scadalink-design/tests/ScadaLink.CentralUI.Tests/Forms/OpcUaEndpointEditorTests.cs
Joseph Doherty 22d91c858a feat(ui): Layer E2 OpcUaEndpointEditor gains Authentication / Advanced / Deadband sections
Three new sections inserted into <OpcUaEndpointEditor>:

1. Authentication (between the existing Connection row and Timing)
   - 'Enable Authentication' button when Config.UserIdentity is null
   - TokenType select (Anonymous / UsernamePassword / X509Certificate)
   - Conditional Username + Password inputs for UsernamePassword
   - Conditional Certificate path + Certificate password for X509Certificate
   - 'Remove Authentication' button

2. Advanced subscription (after the existing Subscription row)
   - Subscription display name (text)
   - Subscription priority (number 0-255)
   - Timestamps to return (Source / Server / Both select)
   - Discard oldest (checkbox)

3. Deadband filter (after Advanced subscription)
   - 'Enable Deadband' button when Config.Deadband is null
   - Type select (Absolute / Percent), Value number input
   - 'Remove Deadband' button

EnableAuthentication and EnableDeadband helpers complement EnableHeartbeat.
All new fields use the existing RenderFieldError helper for validator errors.

82/82 CentralUI tests pass (the 10 new editor tests drove the design).
2026-05-12 02:30:06 -04:00

232 lines
7.6 KiB
C#

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<OpcUaEndpointEditor>(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<OpcUaEndpointEditor>(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<OpcUaEndpointEditor>(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<OpcUaEndpointEditor>(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<OpcUaEndpointEditor>(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<OpcUaEndpointEditor>(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<OpcUaEndpointEditor>(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<OpcUaEndpointEditor>(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<OpcUaEndpointEditor>(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<OpcUaEndpointEditor>(p => p.Add(c => c.Config, config));
// <label>Username</label> 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<OpcUaEndpointEditor>(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<OpcUaEndpointEditor>(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<OpcUaEndpointEditor>(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<OpcUaEndpointEditor>(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<OpcUaEndpointEditor>(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<OpcUaEndpointEditor>(p => p
.Add(c => c.Config, config)
.Add(c => c.Errors, errors));
Assert.Contains("Username is required", cut.Markup);
}
}