Expand XML docs across bridge and test code
This commit is contained in:
@@ -8,8 +8,16 @@ using ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Integration tests that exercise the real Galaxy repository queries against the test database configuration.
|
||||
/// </summary>
|
||||
public class GalaxyRepositoryServiceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads repository configuration from the integration test settings and controls whether extended attributes are enabled.
|
||||
/// </summary>
|
||||
/// <param name="extendedAttributes">A value indicating whether the extended attribute query path should be enabled.</param>
|
||||
/// <returns>The repository configuration used by the integration test.</returns>
|
||||
private static GalaxyRepositoryConfiguration LoadConfig(bool extendedAttributes = false)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
@@ -22,6 +30,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the standard attribute query returns rows from the repository.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_StandardMode_ReturnsRows()
|
||||
{
|
||||
@@ -35,6 +46,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
results.ShouldAllBe(r => r.PrimitiveName == "" && r.AttributeSource == "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the extended attribute query returns more rows than the standard query path.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_ExtendedMode_ReturnsMoreRows()
|
||||
{
|
||||
@@ -49,6 +63,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
extendedResults.Count.ShouldBeGreaterThan(standardResults.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the extended attribute query includes both primitive and dynamic attribute sources.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_ExtendedMode_IncludesPrimitiveAttributes()
|
||||
{
|
||||
@@ -61,6 +78,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
results.ShouldContain(r => r.AttributeSource == "dynamic");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that extended mode populates attribute-source metadata across the result set.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_ExtendedMode_PrimitiveNamePopulated()
|
||||
{
|
||||
@@ -76,6 +96,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
results.ShouldAllBe(r => r.AttributeSource == "primitive" || r.AttributeSource == "dynamic");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that standard-mode results always include fully qualified tag references.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_StandardMode_AllHaveFullTagReference()
|
||||
{
|
||||
@@ -88,6 +111,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
results.ShouldAllBe(r => r.FullTagReference.Contains("."));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that extended-mode results always include fully qualified tag references.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_ExtendedMode_AllHaveFullTagReference()
|
||||
{
|
||||
|
||||
@@ -3,8 +3,14 @@ using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Placeholder integration test that keeps the integration test project wired into the solution.
|
||||
/// </summary>
|
||||
public class SampleIntegrationTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the integration test assembly is executing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Placeholder_ShouldPass()
|
||||
{
|
||||
|
||||
@@ -5,8 +5,15 @@ using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that application configuration binds correctly from appsettings and that validation catches invalid bridge settings.
|
||||
/// </summary>
|
||||
public class ConfigurationLoadingTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads the application configuration from the repository appsettings file for binding tests.
|
||||
/// </summary>
|
||||
/// <returns>The bound application configuration snapshot.</returns>
|
||||
private static AppConfiguration LoadFromJson()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
@@ -21,6 +28,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the OPC UA section binds the endpoint and session settings expected by the bridge.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OpcUa_Section_BindsCorrectly()
|
||||
{
|
||||
@@ -33,6 +43,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
config.OpcUa.SessionTimeoutMinutes.ShouldBe(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the MXAccess section binds runtime timeout and reconnect settings correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MxAccess_Section_BindsCorrectly()
|
||||
{
|
||||
@@ -46,6 +59,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
config.MxAccess.ProbeStaleThresholdSeconds.ShouldBe(60);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the Galaxy repository section binds connection and polling settings correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GalaxyRepository_Section_BindsCorrectly()
|
||||
{
|
||||
@@ -56,6 +72,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
config.GalaxyRepository.ExtendedAttributes.ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that extended-attribute loading defaults to disabled when not configured.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GalaxyRepository_ExtendedAttributes_DefaultsFalse()
|
||||
{
|
||||
@@ -63,6 +82,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
config.ExtendedAttributes.ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the extended-attribute flag can be enabled through configuration binding.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GalaxyRepository_ExtendedAttributes_BindsFromJson()
|
||||
{
|
||||
@@ -76,6 +98,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
config.ExtendedAttributes.ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the dashboard section binds operator-dashboard settings correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Dashboard_Section_BindsCorrectly()
|
||||
{
|
||||
@@ -85,6 +110,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
config.Dashboard.RefreshIntervalSeconds.ShouldBe(10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the default configuration objects start with the expected bridge defaults.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DefaultValues_AreCorrect()
|
||||
{
|
||||
@@ -95,6 +123,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
config.Dashboard.Enabled.ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a valid configuration passes startup validation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_ValidConfig_ReturnsTrue()
|
||||
{
|
||||
@@ -102,6 +133,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
ConfigurationValidator.ValidateAndLog(config).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that an invalid OPC UA port is rejected by startup validation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_InvalidPort_ReturnsFalse()
|
||||
{
|
||||
@@ -110,6 +144,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that an empty Galaxy name is rejected because the bridge requires a namespace target.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_EmptyGalaxyName_ReturnsFalse()
|
||||
{
|
||||
|
||||
@@ -4,8 +4,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies default and extended-field behavior for Galaxy attribute metadata objects.
|
||||
/// </summary>
|
||||
public class GalaxyAttributeInfoTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that a default attribute metadata object starts with empty strings for its text fields.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DefaultValues_AreEmpty()
|
||||
{
|
||||
@@ -18,6 +24,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
info.DataTypeName.ShouldBe("");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that primitive-name and attribute-source fields can be populated for extended metadata rows.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ExtendedFields_CanBeSet()
|
||||
{
|
||||
@@ -30,6 +39,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
info.AttributeSource.ShouldBe("primitive");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that standard attribute rows leave the extended metadata fields empty.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StandardAttributes_HaveEmptyExtendedFields()
|
||||
{
|
||||
|
||||
@@ -5,8 +5,16 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies how Galaxy MX data types are mapped into OPC UA and CLR types by the bridge.
|
||||
/// </summary>
|
||||
public class MxDataTypeMapperTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that known Galaxy MX data types map to the expected OPC UA data type node identifiers.
|
||||
/// </summary>
|
||||
/// <param name="mxDataType">The Galaxy MX data type code.</param>
|
||||
/// <param name="expectedNodeId">The expected OPC UA data type node identifier.</param>
|
||||
[Theory]
|
||||
[InlineData(1, 1u)] // Boolean
|
||||
[InlineData(2, 6u)] // Integer → Int32
|
||||
@@ -25,6 +33,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
MxDataTypeMapper.MapToOpcUaDataType(mxDataType).ShouldBe(expectedNodeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown MX data types default to the OPC UA string data type.
|
||||
/// </summary>
|
||||
/// <param name="mxDataType">The unsupported MX data type code.</param>
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(99)]
|
||||
@@ -34,6 +46,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
MxDataTypeMapper.MapToOpcUaDataType(mxDataType).ShouldBe(12u); // String
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that known MX data types map to the expected CLR runtime types.
|
||||
/// </summary>
|
||||
/// <param name="mxDataType">The Galaxy MX data type code.</param>
|
||||
/// <param name="expectedType">The expected CLR type used by the bridge.</param>
|
||||
[Theory]
|
||||
[InlineData(1, typeof(bool))]
|
||||
[InlineData(2, typeof(int))]
|
||||
@@ -50,18 +67,27 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
MxDataTypeMapper.MapToClrType(mxDataType).ShouldBe(expectedType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown MX data types default to the CLR string type.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToClrType_UnknownDefaultsToString()
|
||||
{
|
||||
MxDataTypeMapper.MapToClrType(999).ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the boolean MX type reports the expected OPC UA type name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetOpcUaTypeName_Boolean()
|
||||
{
|
||||
MxDataTypeMapper.GetOpcUaTypeName(1).ShouldBe("Boolean");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown MX types report the fallback OPC UA type name of string.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetOpcUaTypeName_Unknown_ReturnsString()
|
||||
{
|
||||
|
||||
@@ -4,8 +4,16 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the operator-facing error messages and quality mappings derived from MXAccess error codes.
|
||||
/// </summary>
|
||||
public class MxErrorCodesTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that known MXAccess error codes produce readable operator-facing descriptions.
|
||||
/// </summary>
|
||||
/// <param name="code">The MXAccess error code.</param>
|
||||
/// <param name="expectedSubstring">A substring expected in the returned description.</param>
|
||||
[Theory]
|
||||
[InlineData(1008, "Invalid reference")]
|
||||
[InlineData(1012, "Wrong data type")]
|
||||
@@ -18,6 +26,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
MxErrorCodes.GetMessage(code).ShouldContain(expectedSubstring);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown MXAccess error codes are reported as unknown while preserving the numeric code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetMessage_UnknownCode_ReturnsUnknown()
|
||||
{
|
||||
@@ -25,6 +36,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
MxErrorCodes.GetMessage(9999).ShouldContain("9999");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that known MXAccess error codes map to the expected bridge quality values.
|
||||
/// </summary>
|
||||
/// <param name="code">The MXAccess error code.</param>
|
||||
/// <param name="expected">The expected bridge quality value.</param>
|
||||
[Theory]
|
||||
[InlineData(1008, Quality.BadConfigError)]
|
||||
[InlineData(1012, Quality.BadConfigError)]
|
||||
@@ -37,6 +53,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
MxErrorCodes.MapToQuality(code).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown MXAccess error codes map to the generic bad quality bucket.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToQuality_UnknownCode_ReturnsBad()
|
||||
{
|
||||
|
||||
@@ -4,8 +4,16 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the mapping between MXAccess quality codes, bridge quality values, and OPC UA status codes.
|
||||
/// </summary>
|
||||
public class QualityMapperTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that bad-family MXAccess quality values map to the expected bridge quality values.
|
||||
/// </summary>
|
||||
/// <param name="input">The raw MXAccess quality code.</param>
|
||||
/// <param name="expected">The bridge quality value expected for the code.</param>
|
||||
[Theory]
|
||||
[InlineData(0, Quality.Bad)]
|
||||
[InlineData(4, Quality.BadConfigError)]
|
||||
@@ -16,6 +24,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
QualityMapper.MapFromMxAccessQuality(input).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that uncertain-family MXAccess quality values map to the expected bridge quality values.
|
||||
/// </summary>
|
||||
/// <param name="input">The raw MXAccess quality code.</param>
|
||||
/// <param name="expected">The bridge quality value expected for the code.</param>
|
||||
[Theory]
|
||||
[InlineData(64, Quality.Uncertain)]
|
||||
[InlineData(68, Quality.UncertainLastUsable)]
|
||||
@@ -25,6 +38,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
QualityMapper.MapFromMxAccessQuality(input).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that good-family MXAccess quality values map to the expected bridge quality values.
|
||||
/// </summary>
|
||||
/// <param name="input">The raw MXAccess quality code.</param>
|
||||
/// <param name="expected">The bridge quality value expected for the code.</param>
|
||||
[Theory]
|
||||
[InlineData(192, Quality.Good)]
|
||||
[InlineData(216, Quality.GoodLocalOverride)]
|
||||
@@ -33,48 +51,72 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
QualityMapper.MapFromMxAccessQuality(input).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown bad-family values collapse to the generic bad quality bucket.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapFromMxAccess_UnknownBadValue_ReturnsBad()
|
||||
{
|
||||
QualityMapper.MapFromMxAccessQuality(63).ShouldBe(Quality.Bad);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown uncertain-family values collapse to the generic uncertain quality bucket.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapFromMxAccess_UnknownUncertainValue_ReturnsUncertain()
|
||||
{
|
||||
QualityMapper.MapFromMxAccessQuality(100).ShouldBe(Quality.Uncertain);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown good-family values collapse to the generic good quality bucket.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapFromMxAccess_UnknownGoodValue_ReturnsGood()
|
||||
{
|
||||
QualityMapper.MapFromMxAccessQuality(200).ShouldBe(Quality.Good);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the generic good quality maps to the OPC UA good status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToOpcUa_Good_Returns0()
|
||||
{
|
||||
QualityMapper.MapToOpcUaStatusCode(Quality.Good).ShouldBe(0x00000000u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the generic bad quality maps to the OPC UA bad status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToOpcUa_Bad_Returns80000000()
|
||||
{
|
||||
QualityMapper.MapToOpcUaStatusCode(Quality.Bad).ShouldBe(0x80000000u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that communication failures map to the OPC UA bad communication-failure status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToOpcUa_BadCommFailure()
|
||||
{
|
||||
QualityMapper.MapToOpcUaStatusCode(Quality.BadCommFailure).ShouldBe(0x80050000u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the generic uncertain quality maps to the OPC UA uncertain status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToOpcUa_Uncertain()
|
||||
{
|
||||
QualityMapper.MapToOpcUaStatusCode(Quality.Uncertain).ShouldBe(0x40000000u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that good quality values are classified correctly by the quality extension helpers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void QualityExtensions_IsGood()
|
||||
{
|
||||
@@ -83,6 +125,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
Quality.Good.IsUncertain().ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that bad quality values are classified correctly by the quality extension helpers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void QualityExtensions_IsBad()
|
||||
{
|
||||
@@ -90,6 +135,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
Quality.Bad.IsGood().ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that uncertain quality values are classified correctly by the quality extension helpers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void QualityExtensions_IsUncertain()
|
||||
{
|
||||
|
||||
@@ -17,6 +17,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.EndToEnd
|
||||
/// </summary>
|
||||
public class FullDataFlowTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the fake-backed bridge can start, build the address space, and expose coherent status data end to end.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FullDataFlow_EndToEnd()
|
||||
{
|
||||
|
||||
@@ -8,8 +8,14 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the polling service that detects Galaxy deploy changes and triggers address-space rebuilds.
|
||||
/// </summary>
|
||||
public class ChangeDetectionServiceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the first poll always triggers an initial rebuild notification.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task FirstPoll_AlwaysTriggers()
|
||||
{
|
||||
@@ -26,6 +32,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
|
||||
service.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that repeated polls with the same deploy timestamp do not retrigger rebuilds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SameTimestamp_DoesNotTriggerAgain()
|
||||
{
|
||||
@@ -42,6 +51,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
|
||||
service.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a changed deploy timestamp triggers another rebuild notification.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ChangedTimestamp_TriggersAgain()
|
||||
{
|
||||
@@ -62,6 +74,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
|
||||
service.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that transient polling failures do not crash the service and allow later recovery.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task FailedPoll_DoesNotCrash_RetriesNext()
|
||||
{
|
||||
@@ -88,6 +103,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
|
||||
service.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that stopping the service before it starts is a harmless no-op.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Stop_BeforeStart_DoesNotThrow()
|
||||
{
|
||||
|
||||
@@ -6,40 +6,88 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// In-memory Galaxy repository used by tests to control hierarchy rows, attribute rows, and deploy metadata without SQL access.
|
||||
/// </summary>
|
||||
public class FakeGalaxyRepository : IGalaxyRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when the fake repository simulates a Galaxy deploy change.
|
||||
/// </summary>
|
||||
public event Action? OnGalaxyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hierarchy rows returned to address-space construction logic.
|
||||
/// </summary>
|
||||
public List<GalaxyObjectInfo> Hierarchy { get; set; } = new List<GalaxyObjectInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the attribute rows returned to address-space construction logic.
|
||||
/// </summary>
|
||||
public List<GalaxyAttributeInfo> Attributes { get; set; } = new List<GalaxyAttributeInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the deploy timestamp returned to change-detection logic.
|
||||
/// </summary>
|
||||
public DateTime? LastDeployTime { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether connection checks should report success.
|
||||
/// </summary>
|
||||
public bool ConnectionSucceeds { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether repository calls should throw to simulate database failures.
|
||||
/// </summary>
|
||||
public bool ShouldThrow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the configured hierarchy rows or throws to simulate a repository failure.
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token ignored by the in-memory fake.</param>
|
||||
/// <returns>The configured hierarchy rows.</returns>
|
||||
public Task<List<GalaxyObjectInfo>> GetHierarchyAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (ShouldThrow) throw new Exception("Simulated DB failure");
|
||||
return Task.FromResult(Hierarchy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the configured attribute rows or throws to simulate a repository failure.
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token ignored by the in-memory fake.</param>
|
||||
/// <returns>The configured attribute rows.</returns>
|
||||
public Task<List<GalaxyAttributeInfo>> GetAttributesAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (ShouldThrow) throw new Exception("Simulated DB failure");
|
||||
return Task.FromResult(Attributes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the configured deploy timestamp or throws to simulate a repository failure.
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token ignored by the in-memory fake.</param>
|
||||
/// <returns>The configured deploy timestamp.</returns>
|
||||
public Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (ShouldThrow) throw new Exception("Simulated DB failure");
|
||||
return Task.FromResult(LastDeployTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the configured connection result or throws to simulate a repository failure.
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token ignored by the in-memory fake.</param>
|
||||
/// <returns>The configured connection result.</returns>
|
||||
public Task<bool> TestConnectionAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (ShouldThrow) throw new Exception("Simulated DB failure");
|
||||
return Task.FromResult(ConnectionSucceeds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the deploy-change event so tests can trigger rebuild logic.
|
||||
/// </summary>
|
||||
public void RaiseGalaxyChanged() => OnGalaxyChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,44 +7,99 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// In-memory IMxAccessClient used by tests to drive connection, read, write, and subscription scenarios without COM runtime dependencies.
|
||||
/// </summary>
|
||||
public class FakeMxAccessClient : IMxAccessClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the connection state returned to the system under test.
|
||||
/// </summary>
|
||||
public ConnectionState State { get; set; } = ConnectionState.Connected;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of active subscriptions currently stored by the fake client.
|
||||
/// </summary>
|
||||
public int ActiveSubscriptionCount => _subscriptions.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the reconnect count exposed to health and dashboard tests.
|
||||
/// </summary>
|
||||
public int ReconnectCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when tests explicitly simulate a connection-state transition.
|
||||
/// </summary>
|
||||
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when tests publish a simulated runtime value change.
|
||||
/// </summary>
|
||||
public event Action<string, Vtq>? OnTagValueChanged;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Action<string, Vtq>> _subscriptions = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the in-memory tag-value table returned by fake reads.
|
||||
/// </summary>
|
||||
public ConcurrentDictionary<string, Vtq> TagValues { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the values written through the fake client so tests can assert write behavior.
|
||||
/// </summary>
|
||||
public List<(string Tag, object Value)> WrittenValues { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the result returned by fake writes to simulate success or failure.
|
||||
/// </summary>
|
||||
public bool WriteResult { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Simulates establishing a healthy runtime connection.
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token that is ignored by the in-memory fake.</param>
|
||||
public Task ConnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
State = ConnectionState.Connected;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates disconnecting from the runtime.
|
||||
/// </summary>
|
||||
public Task DisconnectAsync()
|
||||
{
|
||||
State = ConnectionState.Disconnected;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores a subscription callback so later simulated data changes can target it.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The Galaxy attribute reference to monitor.</param>
|
||||
/// <param name="callback">The callback that should receive simulated value changes.</param>
|
||||
public Task SubscribeAsync(string fullTagReference, Action<string, Vtq> callback)
|
||||
{
|
||||
_subscriptions[fullTagReference] = callback;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a stored subscription callback for the specified tag reference.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The Galaxy attribute reference to stop monitoring.</param>
|
||||
public Task UnsubscribeAsync(string fullTagReference)
|
||||
{
|
||||
_subscriptions.TryRemove(fullTagReference, out _);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current in-memory VTQ for a tag reference or a bad-quality placeholder when none has been seeded.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The Galaxy attribute reference to read.</param>
|
||||
/// <param name="ct">A cancellation token that is ignored by the in-memory fake.</param>
|
||||
/// <returns>The seeded VTQ value or a bad not-connected VTQ when the tag was not populated.</returns>
|
||||
public Task<Vtq> ReadAsync(string fullTagReference, CancellationToken ct = default)
|
||||
{
|
||||
if (TagValues.TryGetValue(fullTagReference, out var vtq))
|
||||
@@ -52,6 +107,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
return Task.FromResult(Vtq.Bad(Quality.BadNotConnected));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a write request, optionally updates the in-memory tag table, and returns the configured write result.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The Galaxy attribute reference being written.</param>
|
||||
/// <param name="value">The value supplied by the code under test.</param>
|
||||
/// <param name="ct">A cancellation token that is ignored by the in-memory fake.</param>
|
||||
/// <returns>A completed task returning the configured write outcome.</returns>
|
||||
public Task<bool> WriteAsync(string fullTagReference, object value, CancellationToken ct = default)
|
||||
{
|
||||
WrittenValues.Add((fullTagReference, value));
|
||||
@@ -60,6 +122,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
return Task.FromResult(WriteResult);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a simulated tag-value change to both the event stream and any stored subscription callback.
|
||||
/// </summary>
|
||||
/// <param name="address">The Galaxy attribute reference whose value changed.</param>
|
||||
/// <param name="vtq">The value, timestamp, and quality payload to publish.</param>
|
||||
public void SimulateDataChange(string address, Vtq vtq)
|
||||
{
|
||||
OnTagValueChanged?.Invoke(address, vtq);
|
||||
@@ -67,12 +134,20 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
callback(address, vtq);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises a simulated connection-state transition for health and reconnect tests.
|
||||
/// </summary>
|
||||
/// <param name="prev">The previous connection state.</param>
|
||||
/// <param name="curr">The new connection state.</param>
|
||||
public void RaiseConnectionStateChanged(ConnectionState prev, ConnectionState curr)
|
||||
{
|
||||
State = curr;
|
||||
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(prev, curr));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the fake client. No unmanaged resources are held.
|
||||
/// </summary>
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,21 +17,71 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
private int _connectionHandle;
|
||||
private bool _registered;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the fake proxy publishes a simulated runtime data-change callback to the system under test.
|
||||
/// </summary>
|
||||
public event MxDataChangeHandler? OnDataChange;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the fake proxy publishes a simulated write-complete callback to the system under test.
|
||||
/// </summary>
|
||||
public event MxWriteCompleteHandler? OnWriteComplete;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item-handle to tag-reference map built by the test as attributes are registered with the fake runtime.
|
||||
/// </summary>
|
||||
public ConcurrentDictionary<int, string> Items { get; } = new ConcurrentDictionary<int, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item handles currently marked as advised so tests can assert subscription behavior.
|
||||
/// </summary>
|
||||
public ConcurrentDictionary<int, bool> AdvisedItems { get; } = new ConcurrentDictionary<int, bool>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the values written through the fake runtime so write scenarios can assert the final payload.
|
||||
/// </summary>
|
||||
public List<(string Address, object Value)> WrittenValues { get; } = new List<(string, object)>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the fake runtime is currently considered registered.
|
||||
/// </summary>
|
||||
public bool IsRegistered => _registered;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of times the system under test attempted to register with the fake runtime.
|
||||
/// </summary>
|
||||
public int RegisterCallCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of times the system under test attempted to unregister from the fake runtime.
|
||||
/// </summary>
|
||||
public int UnregisterCallCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether registration should fail to exercise connection-error paths.
|
||||
/// </summary>
|
||||
public bool ShouldFailRegister { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether writes should fail to exercise runtime write-error paths.
|
||||
/// </summary>
|
||||
public bool ShouldFailWrite { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the fake should suppress the write-complete callback for timeout scenarios.
|
||||
/// </summary>
|
||||
public bool SkipWriteCompleteCallback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the status code returned in the simulated write-complete callback.
|
||||
/// </summary>
|
||||
public int WriteCompleteStatus { get; set; } = 0; // 0 = success
|
||||
|
||||
/// <summary>
|
||||
/// Simulates the MXAccess registration handshake and returns a synthetic connection handle.
|
||||
/// </summary>
|
||||
/// <param name="clientName">The client name supplied by the code under test.</param>
|
||||
/// <returns>A synthetic connection handle for subsequent fake operations.</returns>
|
||||
public int Register(string clientName)
|
||||
{
|
||||
RegisterCallCount++;
|
||||
@@ -41,6 +91,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
return _connectionHandle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates tearing down the fake MXAccess connection.
|
||||
/// </summary>
|
||||
/// <param name="handle">The connection handle supplied by the code under test.</param>
|
||||
public void Unregister(int handle)
|
||||
{
|
||||
UnregisterCallCount++;
|
||||
@@ -48,6 +102,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
_connectionHandle = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates resolving a tag reference into a fake runtime item handle.
|
||||
/// </summary>
|
||||
/// <param name="handle">The synthetic connection handle.</param>
|
||||
/// <param name="address">The Galaxy attribute reference being registered.</param>
|
||||
/// <returns>A synthetic item handle.</returns>
|
||||
public int AddItem(int handle, string address)
|
||||
{
|
||||
var itemHandle = Interlocked.Increment(ref _nextHandle);
|
||||
@@ -55,21 +115,43 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
return itemHandle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates removing an item from the fake runtime session.
|
||||
/// </summary>
|
||||
/// <param name="handle">The synthetic connection handle.</param>
|
||||
/// <param name="itemHandle">The synthetic item handle to remove.</param>
|
||||
public void RemoveItem(int handle, int itemHandle)
|
||||
{
|
||||
Items.TryRemove(itemHandle, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks an item as actively advised so tests can assert subscription activation.
|
||||
/// </summary>
|
||||
/// <param name="handle">The synthetic connection handle.</param>
|
||||
/// <param name="itemHandle">The synthetic item handle being monitored.</param>
|
||||
public void AdviseSupervisory(int handle, int itemHandle)
|
||||
{
|
||||
AdvisedItems[itemHandle] = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks an item as no longer advised so tests can assert subscription teardown.
|
||||
/// </summary>
|
||||
/// <param name="handle">The synthetic connection handle.</param>
|
||||
/// <param name="itemHandle">The synthetic item handle no longer being monitored.</param>
|
||||
public void UnAdviseSupervisory(int handle, int itemHandle)
|
||||
{
|
||||
AdvisedItems.TryRemove(itemHandle, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates a runtime write, records the written value, and optionally raises the write-complete callback.
|
||||
/// </summary>
|
||||
/// <param name="handle">The synthetic connection handle.</param>
|
||||
/// <param name="itemHandle">The synthetic item handle to write.</param>
|
||||
/// <param name="value">The value supplied by the system under test.</param>
|
||||
/// <param name="securityClassification">The security classification supplied with the write request.</param>
|
||||
public void Write(int handle, int itemHandle, object value, int securityClassification)
|
||||
{
|
||||
if (ShouldFailWrite) throw new InvalidOperationException("Write failed (simulated)");
|
||||
@@ -95,6 +177,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// <summary>
|
||||
/// Simulates an MXAccess data change event for a specific item handle.
|
||||
/// </summary>
|
||||
/// <param name="itemHandle">The synthetic item handle that should receive the new value.</param>
|
||||
/// <param name="value">The value to publish to the system under test.</param>
|
||||
/// <param name="quality">The runtime quality code to send with the value.</param>
|
||||
/// <param name="timestamp">The optional timestamp to send with the value; defaults to the current UTC time.</param>
|
||||
public void SimulateDataChange(int itemHandle, object value, int quality = 192, DateTime? timestamp = null)
|
||||
{
|
||||
var status = new MXSTATUS_PROXY[1];
|
||||
@@ -106,6 +192,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// <summary>
|
||||
/// Simulates data change for a specific address (finds handle by address).
|
||||
/// </summary>
|
||||
/// <param name="address">The Galaxy attribute reference whose registered handle should receive the new value.</param>
|
||||
/// <param name="value">The value to publish to the system under test.</param>
|
||||
/// <param name="quality">The runtime quality code to send with the value.</param>
|
||||
/// <param name="timestamp">The optional timestamp to send with the value; defaults to the current UTC time.</param>
|
||||
public void SimulateDataChangeByAddress(string address, object value, int quality = 192, DateTime? timestamp = null)
|
||||
{
|
||||
foreach (var kvp in Items)
|
||||
|
||||
@@ -21,8 +21,19 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
{
|
||||
private static int _nextPort = 16000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the started service instance managed by the fixture.
|
||||
/// </summary>
|
||||
public OpcUaService Service { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the OPC UA port assigned to this fixture instance.
|
||||
/// </summary>
|
||||
public int OpcUaPort { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the OPC UA endpoint URL exposed by the fixture.
|
||||
/// </summary>
|
||||
public string EndpointUrl => $"opc.tcp://localhost:{OpcUaPort}/LmxOpcUa";
|
||||
|
||||
/// <summary>
|
||||
@@ -44,6 +55,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
private readonly OpcUaServiceBuilder _builder;
|
||||
private bool _started;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a fixture around a prepared service builder and optional fake dependencies.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder used to construct the service under test.</param>
|
||||
/// <param name="repo">The optional fake Galaxy repository exposed to tests.</param>
|
||||
/// <param name="mxClient">The optional fake MXAccess client exposed to tests.</param>
|
||||
/// <param name="mxProxy">The optional fake MXAccess proxy exposed to tests.</param>
|
||||
private OpcUaServerFixture(OpcUaServiceBuilder builder,
|
||||
FakeGalaxyRepository? repo = null,
|
||||
FakeMxAccessClient? mxClient = null,
|
||||
@@ -62,6 +80,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// Creates fixture with FakeMxProxy + FakeGalaxyRepository (standard test data).
|
||||
/// The STA thread and COM interop run against FakeMxProxy.
|
||||
/// </summary>
|
||||
/// <param name="proxy">An optional fake proxy to inject; otherwise a default fake is created.</param>
|
||||
/// <param name="repo">An optional fake repository to inject; otherwise standard test data is used.</param>
|
||||
/// <returns>A fixture configured to exercise the COM-style runtime path.</returns>
|
||||
public static OpcUaServerFixture WithFakes(
|
||||
FakeMxProxy? proxy = null,
|
||||
FakeGalaxyRepository? repo = null)
|
||||
@@ -85,6 +106,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// Creates fixture using FakeMxAccessClient directly — skips STA thread + COM entirely.
|
||||
/// Fastest option for tests that don't need real COM interop.
|
||||
/// </summary>
|
||||
/// <param name="mxClient">An optional fake MXAccess client to inject; otherwise a default fake is created.</param>
|
||||
/// <param name="repo">An optional fake repository to inject; otherwise standard test data is used.</param>
|
||||
/// <returns>A fixture configured to exercise the direct fake-client path.</returns>
|
||||
public static OpcUaServerFixture WithFakeMxAccessClient(
|
||||
FakeMxAccessClient? mxClient = null,
|
||||
FakeGalaxyRepository? repo = null)
|
||||
@@ -104,6 +128,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
return new OpcUaServerFixture(builder, repo: r, mxClient: client);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds and starts the OPC UA service for the current fixture.
|
||||
/// </summary>
|
||||
public Task InitializeAsync()
|
||||
{
|
||||
Service = _builder.Build();
|
||||
@@ -112,6 +139,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the OPC UA service when the fixture had previously been started.
|
||||
/// </summary>
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
if (_started)
|
||||
|
||||
@@ -6,8 +6,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the reusable OPC UA server fixture used by integration and wiring tests.
|
||||
/// </summary>
|
||||
public class OpcUaServerFixtureTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the standard fake-backed fixture starts the bridge and tears it down cleanly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WithFakes_StartsAndStops()
|
||||
{
|
||||
@@ -25,6 +31,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the fake-client fixture bypasses COM wiring and uses the provided fake runtime client.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WithFakeMxAccessClient_SkipsCom()
|
||||
{
|
||||
@@ -38,6 +47,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that separate fixture instances automatically allocate unique OPC UA ports.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MultipleFixtures_GetUniquePortsAutomatically()
|
||||
{
|
||||
@@ -57,6 +69,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
await fixture2.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that fixture shutdown completes quickly enough for the integration test suite.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Shutdown_CompletesWithin30Seconds()
|
||||
{
|
||||
@@ -70,6 +85,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
sw.Elapsed.TotalSeconds.ShouldBeLessThan(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the fake-backed fixture builds the seeded address space and Galaxy statistics.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WithFakes_BuildsAddressSpace()
|
||||
{
|
||||
|
||||
@@ -16,11 +16,16 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
{
|
||||
private Session? _session;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active OPC UA session used by integration tests once the helper has connected to the bridge.
|
||||
/// </summary>
|
||||
public Session Session => _session ?? throw new InvalidOperationException("Not connected");
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the namespace index for a given namespace URI (e.g., "urn:TestGalaxy:LmxOpcUa").
|
||||
/// </summary>
|
||||
/// <param name="galaxyName">The Galaxy name whose OPC UA namespace should be resolved on the test server.</param>
|
||||
/// <returns>The namespace index assigned by the server for the requested Galaxy namespace.</returns>
|
||||
public ushort GetNamespaceIndex(string galaxyName = "TestGalaxy")
|
||||
{
|
||||
var nsUri = $"urn:{galaxyName}:LmxOpcUa";
|
||||
@@ -32,11 +37,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// <summary>
|
||||
/// Creates a NodeId in the LmxOpcUa namespace using the server's actual namespace index.
|
||||
/// </summary>
|
||||
/// <param name="identifier">The string identifier for the node inside the Galaxy namespace.</param>
|
||||
/// <param name="galaxyName">The Galaxy name whose namespace should be used for the node identifier.</param>
|
||||
/// <returns>A node identifier that targets the requested node on the test server.</returns>
|
||||
public NodeId MakeNodeId(string identifier, string galaxyName = "TestGalaxy")
|
||||
{
|
||||
return new NodeId(identifier, GetNamespaceIndex(galaxyName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects the helper to an OPC UA endpoint exposed by the test bridge.
|
||||
/// </summary>
|
||||
/// <param name="endpointUrl">The OPC UA endpoint URL to connect to.</param>
|
||||
public async Task ConnectAsync(string endpointUrl)
|
||||
{
|
||||
var config = new ApplicationConfiguration
|
||||
@@ -87,6 +99,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// <summary>
|
||||
/// Browse children of a node. Returns list of (DisplayName, NodeId, NodeClass).
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node whose hierarchical children should be browsed.</param>
|
||||
/// <returns>The child nodes exposed beneath the requested node.</returns>
|
||||
public async Task<List<(string Name, NodeId NodeId, NodeClass NodeClass)>> BrowseAsync(NodeId nodeId)
|
||||
{
|
||||
var results = new List<(string, NodeId, NodeClass)>();
|
||||
@@ -109,6 +123,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// <summary>
|
||||
/// Read a node's value.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node whose current value should be read from the server.</param>
|
||||
/// <returns>The OPC UA data value returned by the server.</returns>
|
||||
public DataValue Read(NodeId nodeId)
|
||||
{
|
||||
return Session.ReadValue(nodeId);
|
||||
@@ -118,6 +134,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// Write a node's value, optionally using an OPC UA index range for array element writes.
|
||||
/// Returns the server status code for the write.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node whose value should be written.</param>
|
||||
/// <param name="value">The value to send to the server.</param>
|
||||
/// <param name="indexRange">An optional OPC UA index range used for array element writes.</param>
|
||||
/// <returns>The server status code returned for the write request.</returns>
|
||||
public StatusCode Write(NodeId nodeId, object value, string? indexRange = null)
|
||||
{
|
||||
var nodesToWrite = new WriteValueCollection
|
||||
@@ -139,6 +159,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// Create a subscription with a monitored item on the given node.
|
||||
/// Returns the subscription and monitored item for inspection.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node whose value changes should be monitored.</param>
|
||||
/// <param name="intervalMs">The publishing and sampling interval, in milliseconds, for the test subscription.</param>
|
||||
/// <returns>The created subscription and monitored item pair for later assertions and cleanup.</returns>
|
||||
public async Task<(Subscription Sub, MonitoredItem Item)> SubscribeAsync(
|
||||
NodeId nodeId, int intervalMs = 250)
|
||||
{
|
||||
@@ -162,6 +185,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
return (subscription, item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the test session and releases OPC UA client resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_session != null)
|
||||
|
||||
@@ -8,6 +8,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// </summary>
|
||||
public static class TestData
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the standard Galaxy hierarchy used by integration and wiring tests.
|
||||
/// </summary>
|
||||
/// <returns>The standard hierarchy rows for the fake repository.</returns>
|
||||
public static List<GalaxyObjectInfo> CreateStandardHierarchy()
|
||||
{
|
||||
return new List<GalaxyObjectInfo>
|
||||
@@ -20,6 +24,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the standard attribute set used by integration and wiring tests.
|
||||
/// </summary>
|
||||
/// <returns>The standard attribute rows for the fake repository.</returns>
|
||||
public static List<GalaxyAttributeInfo> CreateStandardAttributes()
|
||||
{
|
||||
return new List<GalaxyAttributeInfo>
|
||||
@@ -33,6 +41,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a minimal hierarchy containing a single object for focused unit tests.
|
||||
/// </summary>
|
||||
/// <returns>A minimal hierarchy row set.</returns>
|
||||
public static List<GalaxyObjectInfo> CreateMinimalHierarchy()
|
||||
{
|
||||
return new List<GalaxyObjectInfo>
|
||||
@@ -41,6 +53,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a minimal attribute set containing a single scalar attribute for focused unit tests.
|
||||
/// </summary>
|
||||
/// <returns>A minimal attribute row set.</returns>
|
||||
public static List<GalaxyAttributeInfo> CreateMinimalAttributes()
|
||||
{
|
||||
return new List<GalaxyAttributeInfo>
|
||||
|
||||
@@ -16,6 +16,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
/// </summary>
|
||||
public class AddressSpaceRebuildTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the initial browsed hierarchy matches the seeded Galaxy model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Browse_ReturnsInitialHierarchy()
|
||||
{
|
||||
@@ -38,6 +41,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that adding a Galaxy object and rebuilding exposes the new node to OPC UA clients.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Browse_AfterAddingObject_NewNodeAppears()
|
||||
{
|
||||
@@ -81,6 +87,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that removing a Galaxy object and rebuilding removes the node from the OPC UA hierarchy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Browse_AfterRemovingObject_NodeDisappears()
|
||||
{
|
||||
@@ -114,6 +123,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that subscriptions on deleted nodes receive a bad-quality notification after rebuild.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subscribe_RemovedNode_PublishesBadQuality()
|
||||
{
|
||||
@@ -160,6 +172,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that subscriptions on surviving nodes continue to work after a partial rebuild.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subscribe_SurvivingNode_StillWorksAfterRebuild()
|
||||
{
|
||||
@@ -196,6 +211,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that adding a Galaxy attribute and rebuilding exposes a new OPC UA variable.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Browse_AddAttribute_NewVariableAppears()
|
||||
{
|
||||
@@ -230,6 +248,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that removing a Galaxy attribute and rebuilding removes the OPC UA variable.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Browse_RemoveAttribute_VariableDisappears()
|
||||
{
|
||||
@@ -260,6 +281,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that rebuilds preserve subscription bookkeeping for nodes that survive the metadata refresh.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Rebuild_PreservesSubscriptionBookkeeping_ForSurvivingNodes()
|
||||
{
|
||||
|
||||
@@ -8,8 +8,14 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies OPC UA indexed array writes against the bridge's whole-array runtime update behavior.
|
||||
/// </summary>
|
||||
public class ArrayWriteTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that writing a single array element updates the correct slot while preserving the rest of the array.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_SingleArrayElement_UpdatesWholeArrayValue()
|
||||
{
|
||||
|
||||
@@ -19,6 +19,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
{
|
||||
// ── Subscription Sync ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that multiple OPC UA clients subscribed to the same tag all receive the same runtime update.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MultipleClients_SubscribeToSameTag_AllReceiveDataChanges()
|
||||
{
|
||||
@@ -70,6 +73,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that one client disconnecting does not stop remaining clients from receiving updates.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Client_Disconnects_OtherClientsStillReceive()
|
||||
{
|
||||
@@ -119,6 +125,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that one client unsubscribing does not interrupt delivery to other subscribed clients.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Client_Unsubscribes_OtherClientsStillReceive()
|
||||
{
|
||||
@@ -159,6 +168,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that clients subscribed to different tags only receive updates for their own monitored data.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MultipleClients_SubscribeToDifferentTags_EachGetsOwnData()
|
||||
{
|
||||
@@ -206,6 +218,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
|
||||
// ── Concurrent Operation Tests ────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that concurrent browse operations from several clients all complete successfully.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConcurrentBrowseFromMultipleClients_AllSucceed()
|
||||
{
|
||||
@@ -246,6 +261,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that concurrent browse requests return consistent results across clients.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConcurrentBrowse_AllReturnSameResults()
|
||||
{
|
||||
@@ -283,6 +301,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that simultaneous browse and subscribe operations do not interfere with one another.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConcurrentBrowseAndSubscribe_NoInterference()
|
||||
{
|
||||
@@ -318,6 +339,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that concurrent subscribe, read, and browse operations complete without deadlocking the server.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConcurrentSubscribeAndRead_NoDeadlock()
|
||||
{
|
||||
@@ -355,6 +379,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that repeated client churn does not leave the server in an unstable state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RapidConnectDisconnect_ServerStaysStable()
|
||||
{
|
||||
|
||||
@@ -5,8 +5,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.Metrics;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies operation timing aggregation, rolling buffers, and success tracking used by the bridge metrics subsystem.
|
||||
/// </summary>
|
||||
public class PerformanceMetricsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that a fresh metrics collector reports no statistics.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EmptyState_ReturnsZeroStatistics()
|
||||
{
|
||||
@@ -15,6 +21,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
stats.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that repeated operation recordings update total and successful execution counts.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RecordOperation_TracksCounts()
|
||||
{
|
||||
@@ -29,6 +38,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
stats["Read"].SuccessRate.ShouldBe(0.5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that min, max, and average timing values are calculated from recorded operations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RecordOperation_TracksMinMaxAverage()
|
||||
{
|
||||
@@ -43,6 +55,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
stats.AverageMilliseconds.ShouldBe(20);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the 95th percentile is calculated from the recorded timing sample.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void P95_CalculatedCorrectly()
|
||||
{
|
||||
@@ -54,6 +69,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
stats.Percentile95Milliseconds.ShouldBe(95);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the rolling buffer keeps the most recent operation durations for percentile calculations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RollingBuffer_EvictsOldEntries()
|
||||
{
|
||||
@@ -67,6 +85,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
stats.Percentile95Milliseconds.ShouldBeGreaterThan(1000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a timing scope records an operation when disposed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BeginOperation_TimingScopeRecordsOnDispose()
|
||||
{
|
||||
@@ -85,6 +106,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
stats["Test"].AverageMilliseconds.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a timing scope can mark an operation as failed before disposal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BeginOperation_SetSuccessFalse()
|
||||
{
|
||||
@@ -100,6 +124,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
stats.SuccessCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that looking up an unknown operation returns no metrics bucket.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetMetrics_UnknownOperation_ReturnsNull()
|
||||
{
|
||||
@@ -107,6 +134,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
metrics.GetMetrics("NonExistent").ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that operation names are tracked without case sensitivity.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OperationNames_AreCaseInsensitive()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,9 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies MXAccess client connection lifecycle behavior, including transitions, registration, and reconnect handling.
|
||||
/// </summary>
|
||||
public class MxAccessClientConnectionTests : IDisposable
|
||||
{
|
||||
private readonly StaComThread _staThread;
|
||||
@@ -19,6 +22,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
private readonly MxAccessClient _client;
|
||||
private readonly List<(ConnectionState Previous, ConnectionState Current)> _stateChanges = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the connection test fixture with a fake runtime proxy and state-change recorder.
|
||||
/// </summary>
|
||||
public MxAccessClientConnectionTests()
|
||||
{
|
||||
_staThread = new StaComThread();
|
||||
@@ -30,6 +36,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client.ConnectionStateChanged += (_, e) => _stateChanges.Add((e.PreviousState, e.CurrentState));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the connection test fixture and its supporting resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_client.Dispose();
|
||||
@@ -37,12 +46,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_metrics.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a newly created MXAccess client starts in the disconnected state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void InitialState_IsDisconnected()
|
||||
{
|
||||
_client.State.ShouldBe(ConnectionState.Disconnected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that connecting drives the expected disconnected-to-connecting-to-connected transitions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connect_TransitionsToConnected()
|
||||
{
|
||||
@@ -53,6 +68,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_stateChanges.ShouldContain(s => s.Previous == ConnectionState.Connecting && s.Current == ConnectionState.Connected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a successful connect registers exactly once with the runtime proxy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connect_RegistersCalled()
|
||||
{
|
||||
@@ -60,6 +78,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_proxy.RegisterCallCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that disconnecting drives the expected shutdown transitions back to disconnected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Disconnect_TransitionsToDisconnected()
|
||||
{
|
||||
@@ -71,6 +92,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_stateChanges.ShouldContain(s => s.Current == ConnectionState.Disconnected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that disconnecting unregisters the runtime proxy session.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Disconnect_UnregistersCalled()
|
||||
{
|
||||
@@ -79,6 +103,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_proxy.UnregisterCallCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that registration failures move the client into the error state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectFails_TransitionsToError()
|
||||
{
|
||||
@@ -88,6 +115,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client.State.ShouldBe(ConnectionState.Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that repeated connect calls do not perform duplicate runtime registrations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DoubleConnect_NoOp()
|
||||
{
|
||||
@@ -96,6 +126,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_proxy.RegisterCallCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that reconnect increments the reconnect counter and restores the connected state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Reconnect_IncrementsCount()
|
||||
{
|
||||
|
||||
@@ -10,12 +10,18 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the background connectivity monitor used to reconnect the MXAccess bridge after faults or stale probes.
|
||||
/// </summary>
|
||||
public class MxAccessClientMonitorTests : IDisposable
|
||||
{
|
||||
private readonly StaComThread _staThread;
|
||||
private readonly FakeMxProxy _proxy;
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the monitor test fixture with a shared STA thread, fake proxy, and metrics collector.
|
||||
/// </summary>
|
||||
public MxAccessClientMonitorTests()
|
||||
{
|
||||
_staThread = new StaComThread();
|
||||
@@ -24,12 +30,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_metrics = new PerformanceMetrics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the monitor test fixture resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_staThread.Dispose();
|
||||
_metrics.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the monitor reconnects the client after an observed disconnect.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Monitor_ReconnectsOnDisconnect()
|
||||
{
|
||||
@@ -54,6 +66,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the monitor can be started and stopped without throwing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Monitor_StopsOnCancel()
|
||||
{
|
||||
@@ -69,6 +84,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a stale probe tag triggers a reconnect when monitoring is enabled.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Monitor_ProbeStale_ForcesReconnect()
|
||||
{
|
||||
@@ -93,6 +111,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that fresh probe updates prevent unnecessary reconnects.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Monitor_ProbeDataChange_PreventsStaleReconnect()
|
||||
{
|
||||
@@ -122,6 +143,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that enabling the monitor without a probe tag does not trigger false reconnects.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Monitor_NoProbeConfigured_NoFalseReconnect()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,9 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies MXAccess client read and write behavior against the fake runtime proxy used by the bridge.
|
||||
/// </summary>
|
||||
public class MxAccessClientReadWriteTests : IDisposable
|
||||
{
|
||||
private readonly StaComThread _staThread;
|
||||
@@ -18,6 +21,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
private readonly MxAccessClient _client;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the COM-threaded MXAccess test fixture with a fake runtime proxy and metrics collector.
|
||||
/// </summary>
|
||||
public MxAccessClientReadWriteTests()
|
||||
{
|
||||
_staThread = new StaComThread();
|
||||
@@ -28,6 +34,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client = new MxAccessClient(_staThread, _proxy, config, _metrics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the MXAccess client fixture and its supporting STA thread and metrics collector.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_client.Dispose();
|
||||
@@ -35,6 +44,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_metrics.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that reads fail with bad-not-connected quality when the runtime session is offline.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_NotConnected_ReturnsBad()
|
||||
{
|
||||
@@ -42,6 +54,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
result.Quality.ShouldBe(Quality.BadNotConnected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a runtime data-change callback completes a pending read with the published value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_ReturnsValueOnDataChange()
|
||||
{
|
||||
@@ -59,6 +74,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
result.Quality.ShouldBe(Quality.Good);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that reads time out with bad communication-failure quality when the runtime never responds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_Timeout_ReturnsBadCommFailure()
|
||||
{
|
||||
@@ -69,6 +87,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
result.Quality.ShouldBe(Quality.BadCommFailure);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that timed-out reads are recorded as failed read operations in the metrics collector.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_Timeout_RecordsFailedMetrics()
|
||||
{
|
||||
@@ -83,6 +104,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
stats["Read"].SuccessCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that writes are rejected when the runtime session is not connected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_NotConnected_ReturnsFalse()
|
||||
{
|
||||
@@ -90,6 +114,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
result.ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that successful runtime write acknowledgments return success and record the written payload.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_Success_ReturnsTrue()
|
||||
{
|
||||
@@ -101,6 +128,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_proxy.WrittenValues.ShouldContain(w => w.Address == "TestTag.Attr" && (int)w.Value == 42);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that MXAccess error codes on write completion are surfaced as failed writes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_ErrorCode_ReturnsFalse()
|
||||
{
|
||||
@@ -111,6 +141,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
result.ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that write timeouts are recorded as failed write operations in the metrics collector.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_Timeout_ReturnsFalse_AndRecordsFailedMetrics()
|
||||
{
|
||||
@@ -126,6 +159,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
stats["Write"].SuccessCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that successful reads contribute a read entry to the metrics collector.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_RecordsMetrics()
|
||||
{
|
||||
@@ -141,6 +177,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
stats["Read"].TotalCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that writes contribute a write entry to the metrics collector.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_RecordsMetrics()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,9 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies how the MXAccess client manages persistent subscriptions, reconnect replay, and probe-tag behavior.
|
||||
/// </summary>
|
||||
public class MxAccessClientSubscriptionTests : IDisposable
|
||||
{
|
||||
private readonly StaComThread _staThread;
|
||||
@@ -18,6 +21,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
private readonly MxAccessClient _client;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the subscription test fixture with a fake runtime proxy and STA thread.
|
||||
/// </summary>
|
||||
public MxAccessClientSubscriptionTests()
|
||||
{
|
||||
_staThread = new StaComThread();
|
||||
@@ -27,6 +33,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client = new MxAccessClient(_staThread, _proxy, new MxAccessConfiguration(), _metrics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the subscription test fixture and its supporting resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_client.Dispose();
|
||||
@@ -34,6 +43,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_metrics.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that subscribing creates a runtime item, advises it, and increments the active subscription count.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subscribe_CreatesItemAndAdvises()
|
||||
{
|
||||
@@ -45,6 +57,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client.ActiveSubscriptionCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unsubscribing clears the active subscription count after a tag was previously monitored.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Unsubscribe_RemovesItemAndUnadvises()
|
||||
{
|
||||
@@ -55,6 +70,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client.ActiveSubscriptionCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that runtime data changes are delivered to the per-subscription callback.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task OnDataChange_InvokesCallback()
|
||||
{
|
||||
@@ -70,6 +88,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
received.Value.Quality.ShouldBe(Quality.Good);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that runtime data changes are also delivered to the client's global tag-change event.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task OnDataChange_InvokesGlobalHandler()
|
||||
{
|
||||
@@ -84,6 +105,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
globalAddr.ShouldBe("TestTag.Attr");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that stored subscriptions are replayed after reconnect so live updates resume automatically.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task StoredSubscriptions_ReplayedAfterReconnect()
|
||||
{
|
||||
@@ -102,6 +126,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
callbackInvoked.ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that one-shot reads do not remove persistent subscriptions when the client reconnects.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task OneShotRead_DoesNotRemovePersistentSubscription_OnReconnect()
|
||||
{
|
||||
@@ -122,6 +149,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client.ActiveSubscriptionCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that transient writes do not prevent later removal of a persistent subscription.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task OneShotWrite_DoesNotBreakPersistentUnsubscribe()
|
||||
{
|
||||
@@ -138,6 +168,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_proxy.Items.Values.ShouldNotContain("TestTag.Attr");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the configured probe tag is subscribed during connect so connectivity monitoring can start immediately.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ProbeTag_SubscribedOnConnect()
|
||||
{
|
||||
@@ -152,6 +185,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the probe tag cannot be unsubscribed accidentally because it is reserved for connection monitoring.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ProbeTag_ProtectedFromUnsubscribe()
|
||||
{
|
||||
|
||||
@@ -7,18 +7,30 @@ using ZB.MOM.WW.LmxOpcUa.Host.MxAccess;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the single-threaded apartment worker used to marshal COM calls for the MXAccess bridge.
|
||||
/// </summary>
|
||||
public class StaComThreadTests : IDisposable
|
||||
{
|
||||
private readonly StaComThread _thread;
|
||||
|
||||
/// <summary>
|
||||
/// Starts a fresh STA thread instance for each test.
|
||||
/// </summary>
|
||||
public StaComThreadTests()
|
||||
{
|
||||
_thread = new StaComThread();
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the STA thread after each test.
|
||||
/// </summary>
|
||||
public void Dispose() => _thread.Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that queued work runs on a thread configured for STA apartment state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_ExecutesOnStaThread()
|
||||
{
|
||||
@@ -26,6 +38,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
apartmentState.ShouldBe(ApartmentState.STA);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that action delegates run to completion on the STA thread.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_Action_Completes()
|
||||
{
|
||||
@@ -34,6 +49,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
executed.ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that function delegates can return results from the STA thread.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_Func_ReturnsResult()
|
||||
{
|
||||
@@ -41,6 +59,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
result.ShouldBe(42);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that exceptions thrown on the STA thread propagate back to the caller.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_PropagatesException()
|
||||
{
|
||||
@@ -48,6 +69,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_thread.RunAsync(() => throw new InvalidOperationException("test error")));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that disposing the STA thread stops it from accepting additional work.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Dispose_Stops_Thread()
|
||||
{
|
||||
@@ -59,6 +83,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
Should.Throw<ObjectDisposedException>(() => thread.RunAsync(() => { }).GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that multiple queued work items all execute successfully on the STA thread.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MultipleWorkItems_ExecuteInOrder()
|
||||
{
|
||||
|
||||
@@ -6,8 +6,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies how bridge VTQ values are translated to and from OPC UA data values for the published namespace.
|
||||
/// </summary>
|
||||
public class DataValueConverterTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that boolean runtime values are preserved when converted to OPC UA data values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_Boolean()
|
||||
{
|
||||
@@ -17,6 +23,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
Opc.Ua.StatusCode.IsGood(dv.StatusCode).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that integer runtime values are preserved when converted to OPC UA data values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_Int32()
|
||||
{
|
||||
@@ -25,6 +34,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
dv.Value.ShouldBe(42);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that float runtime values are preserved when converted to OPC UA data values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_Float()
|
||||
{
|
||||
@@ -33,6 +45,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
dv.Value.ShouldBe(3.14f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that double runtime values are preserved when converted to OPC UA data values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_Double()
|
||||
{
|
||||
@@ -41,6 +56,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
dv.Value.ShouldBe(3.14159);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that string runtime values are preserved when converted to OPC UA data values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_String()
|
||||
{
|
||||
@@ -49,6 +67,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
dv.Value.ShouldBe("hello");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that UTC timestamps remain UTC when a VTQ is converted for OPC UA clients.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_DateTime_IsUtc()
|
||||
{
|
||||
@@ -58,6 +79,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
((DateTime)dv.Value).Kind.ShouldBe(DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that elapsed-time values are exposed to OPC UA clients in seconds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_TimeSpan_ConvertedToSeconds()
|
||||
{
|
||||
@@ -66,6 +90,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
dv.Value.ShouldBe(150.0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that string arrays remain arrays when exposed through OPC UA.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_StringArray()
|
||||
{
|
||||
@@ -75,6 +102,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
dv.Value.ShouldBe(arr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that integer arrays remain arrays when exposed through OPC UA.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_IntArray()
|
||||
{
|
||||
@@ -84,6 +114,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
dv.Value.ShouldBe(arr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that bad runtime quality is translated to a bad OPC UA status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_BadQuality_MapsToStatusCode()
|
||||
{
|
||||
@@ -92,6 +125,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
Opc.Ua.StatusCode.IsBad(dv.StatusCode).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that uncertain runtime quality is translated to an uncertain OPC UA status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_UncertainQuality()
|
||||
{
|
||||
@@ -100,6 +136,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
Opc.Ua.StatusCode.IsUncertain(dv.StatusCode).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that null runtime values remain null when converted for OPC UA.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_NullValue()
|
||||
{
|
||||
@@ -108,6 +147,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
dv.Value.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a data value can round-trip back into a VTQ without losing the process value or quality.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ToVtq_RoundTrip()
|
||||
{
|
||||
|
||||
@@ -6,8 +6,15 @@ using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the in-memory address-space model built from Galaxy hierarchy and attribute rows.
|
||||
/// </summary>
|
||||
public class LmxNodeManagerBuildTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates representative Galaxy hierarchy and attribute rows for address-space builder tests.
|
||||
/// </summary>
|
||||
/// <returns>The hierarchy and attribute rows used by the tests.</returns>
|
||||
private static (List<GalaxyObjectInfo> hierarchy, List<GalaxyAttributeInfo> attributes) CreateTestData()
|
||||
{
|
||||
var hierarchy = new List<GalaxyObjectInfo>
|
||||
@@ -29,6 +36,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
return (hierarchy, attributes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that object and variable counts are computed correctly from the seeded Galaxy model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_CreatesCorrectNodeCounts()
|
||||
{
|
||||
@@ -39,6 +49,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
model.VariableCount.ShouldBe(4); // MachineID, DownloadPath, JobStepNumber, BatchItems
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that runtime tag references are populated for every published variable.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_TagReferencesPopulated()
|
||||
{
|
||||
@@ -51,6 +64,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
model.NodeIdToTagReference.ContainsKey("TestMachine_001.BatchItems[]").ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that array attributes are represented in the tag-reference map.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_ArrayVariable_HasCorrectInfo()
|
||||
{
|
||||
@@ -60,6 +76,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
model.NodeIdToTagReference.ContainsKey("TestMachine_001.BatchItems[]").ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that Galaxy areas are not counted as object nodes in the resulting model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_Areas_AreNotCountedAsObjects()
|
||||
{
|
||||
@@ -73,6 +92,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
model.ObjectCount.ShouldBe(1); // Only Obj1, not Area1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that only top-level Galaxy nodes are returned as roots in the model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_RootNodes_AreTopLevel()
|
||||
{
|
||||
@@ -86,6 +108,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
model.RootNodes.Count.ShouldBe(1); // Only Root1 is a root
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that variables for multiple MX data types are included in the model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_DataTypeMappings()
|
||||
{
|
||||
|
||||
@@ -6,8 +6,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies rebuild behavior by comparing address-space models before and after metadata changes.
|
||||
/// </summary>
|
||||
public class LmxNodeManagerRebuildTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that rebuilding with new metadata replaces the old tag-reference set.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rebuild_NewBuild_ReplacesOldData()
|
||||
{
|
||||
@@ -40,6 +46,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
model2.NodeIdToTagReference.ContainsKey("NewObj.NewAttr").ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that object counts are recalculated from the latest rebuild input.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rebuild_UpdatesNodeCounts()
|
||||
{
|
||||
@@ -59,6 +68,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
model2.ObjectCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that empty metadata produces an empty address-space model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EmptyHierarchy_ProducesEmptyModel()
|
||||
{
|
||||
|
||||
@@ -6,8 +6,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies translation between bridge quality values and OPC UA status codes.
|
||||
/// </summary>
|
||||
public class OpcUaQualityMapperTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that good bridge quality maps to an OPC UA good status.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Good_MapsToGoodStatusCode()
|
||||
{
|
||||
@@ -15,6 +21,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
StatusCode.IsGood(sc).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that bad bridge quality maps to an OPC UA bad status.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Bad_MapsToBadStatusCode()
|
||||
{
|
||||
@@ -22,6 +31,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
StatusCode.IsBad(sc).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that uncertain bridge quality maps to an OPC UA uncertain status.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Uncertain_MapsToUncertainStatusCode()
|
||||
{
|
||||
@@ -29,6 +41,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
StatusCode.IsUncertain(sc).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that communication failures map to a bad OPC UA status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BadCommFailure_MapsCorrectly()
|
||||
{
|
||||
@@ -36,6 +51,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
StatusCode.IsBad(sc).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the OPC UA good status maps back to bridge good quality.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromStatusCode_Good()
|
||||
{
|
||||
@@ -43,6 +61,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
q.ShouldBe(Quality.Good);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the OPC UA bad status maps back to bridge bad quality.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromStatusCode_Bad()
|
||||
{
|
||||
@@ -50,6 +71,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
q.ShouldBe(Quality.Bad);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the OPC UA uncertain status maps back to bridge uncertain quality.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromStatusCode_Uncertain()
|
||||
{
|
||||
|
||||
@@ -3,8 +3,14 @@ using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Placeholder unit test that keeps the unit test project wired into the solution.
|
||||
/// </summary>
|
||||
public class SampleTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the unit test assembly is executing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Placeholder_ShouldPass()
|
||||
{
|
||||
|
||||
@@ -7,10 +7,16 @@ using ZB.MOM.WW.LmxOpcUa.Host.Status;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies how the dashboard health service classifies bridge health from connection state and metrics.
|
||||
/// </summary>
|
||||
public class HealthCheckServiceTests
|
||||
{
|
||||
private readonly HealthCheckService _sut = new();
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a disconnected runtime is reported as unhealthy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NotConnected_ReturnsUnhealthy()
|
||||
{
|
||||
@@ -20,6 +26,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
result.Message.ShouldContain("not connected");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a connected runtime with no metrics history is still considered healthy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Connected_NoMetrics_ReturnsHealthy()
|
||||
{
|
||||
@@ -28,6 +37,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
result.Color.ShouldBe("green");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that good success-rate metrics keep the service in a healthy state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Connected_GoodMetrics_ReturnsHealthy()
|
||||
{
|
||||
@@ -39,6 +51,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
result.Status.ShouldBe("Healthy");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that poor operation success rates degrade the reported health state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Connected_LowSuccessRate_ReturnsDegraded()
|
||||
{
|
||||
@@ -53,18 +68,27 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
result.Color.ShouldBe("yellow");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the boolean health helper reports true when the runtime is connected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsHealthy_Connected_ReturnsTrue()
|
||||
{
|
||||
_sut.IsHealthy(ConnectionState.Connected, null).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the boolean health helper reports false when the runtime is disconnected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsHealthy_Disconnected_ReturnsFalse()
|
||||
{
|
||||
_sut.IsHealthy(ConnectionState.Disconnected, null).ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the error connection state is treated as unhealthy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Error_ReturnsUnhealthy()
|
||||
{
|
||||
@@ -72,6 +96,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
result.Status.ShouldBe("Unhealthy");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the reconnecting state is treated as unhealthy while recovery is in progress.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Reconnecting_ReturnsUnhealthy()
|
||||
{
|
||||
|
||||
@@ -9,8 +9,14 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the HTML, JSON, and health snapshots generated for the operator status dashboard.
|
||||
/// </summary>
|
||||
public class StatusReportServiceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the generated HTML contains every dashboard panel expected by operators.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_ContainsAllPanels()
|
||||
{
|
||||
@@ -25,6 +31,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
html.ShouldContain("Footer");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the generated HTML includes the configured auto-refresh meta tag.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_ContainsMetaRefresh()
|
||||
{
|
||||
@@ -33,6 +42,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
html.ShouldContain("meta http-equiv='refresh' content='10'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the connection panel renders the current runtime connection state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_ConnectionPanel_ShowsState()
|
||||
{
|
||||
@@ -41,6 +53,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
html.ShouldContain("Connected");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the Galaxy panel renders the bridged Galaxy name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_GalaxyPanel_ShowsName()
|
||||
{
|
||||
@@ -49,6 +64,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
html.ShouldContain("TestGalaxy");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the operations table renders the expected performance metric headers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_OperationsTable_ShowsHeaders()
|
||||
{
|
||||
@@ -62,6 +80,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
html.ShouldContain("P95 (ms)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the footer renders timestamp and version information.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_Footer_ContainsTimestampAndVersion()
|
||||
{
|
||||
@@ -71,6 +92,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
html.ShouldContain("Version:");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the generated JSON includes the major dashboard sections.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateJson_Deserializes()
|
||||
{
|
||||
@@ -86,6 +110,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
json.ShouldContain("Footer");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the report service reports healthy when the runtime connection is up.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsHealthy_WhenConnected_ReturnsTrue()
|
||||
{
|
||||
@@ -93,6 +120,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
sut.IsHealthy().ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the report service reports unhealthy when the runtime connection is down.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsHealthy_WhenDisconnected_ReturnsFalse()
|
||||
{
|
||||
@@ -102,6 +132,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
sut.IsHealthy().ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a status report service preloaded with representative runtime, Galaxy, and metrics data.
|
||||
/// </summary>
|
||||
/// <returns>A configured status report service for dashboard assertions.</returns>
|
||||
private static StatusReportService CreateService()
|
||||
{
|
||||
var mxClient = new FakeMxAccessClient();
|
||||
|
||||
@@ -9,12 +9,18 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the lightweight HTTP dashboard host that exposes bridge status to operators.
|
||||
/// </summary>
|
||||
public class StatusWebServerTests : IDisposable
|
||||
{
|
||||
private readonly StatusWebServer _server;
|
||||
private readonly HttpClient _client;
|
||||
private readonly int _port;
|
||||
|
||||
/// <summary>
|
||||
/// Starts a status web server on a random test port and prepares an HTTP client for endpoint assertions.
|
||||
/// </summary>
|
||||
public StatusWebServerTests()
|
||||
{
|
||||
_port = new Random().Next(18000, 19000);
|
||||
@@ -26,12 +32,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
_client = new HttpClient { BaseAddress = new Uri($"http://localhost:{_port}") };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the test HTTP client and stops the status web server.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_client.Dispose();
|
||||
_server.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the dashboard root responds with HTML content.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Root_ReturnsHtml200()
|
||||
{
|
||||
@@ -40,6 +52,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
response.Content.Headers.ContentType?.MediaType.ShouldBe("text/html");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the JSON status endpoint responds successfully.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ApiStatus_ReturnsJson200()
|
||||
{
|
||||
@@ -48,6 +63,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
response.Content.Headers.ContentType?.MediaType.ShouldBe("application/json");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the health endpoint returns HTTP 200 when the bridge is healthy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ApiHealth_Returns200WhenHealthy()
|
||||
{
|
||||
@@ -58,6 +76,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
body.ShouldContain("healthy");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown dashboard routes return HTTP 404.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UnknownPath_Returns404()
|
||||
{
|
||||
@@ -65,6 +86,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unsupported HTTP methods are rejected with HTTP 405.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task PostMethod_Returns405()
|
||||
{
|
||||
@@ -72,6 +96,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that cache-control headers disable caching for dashboard responses.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CacheHeaders_Present()
|
||||
{
|
||||
@@ -80,6 +107,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
response.Headers.CacheControl?.NoStore.ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the server can be started and stopped cleanly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StartStop_DoesNotThrow()
|
||||
{
|
||||
|
||||
@@ -16,6 +16,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
/// </summary>
|
||||
public class ChangeDetectionToRebuildWiringTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that a changed deploy timestamp causes the change-detection pipeline to raise another rebuild signal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ChangedTimestamp_TriggersRebuild()
|
||||
{
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
/// </summary>
|
||||
public class MxAccessToNodeManagerWiringTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that a simulated data change reaches the global tag-value-changed event.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DataChange_ReachesGlobalHandler()
|
||||
{
|
||||
@@ -33,6 +36,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
receivedVtq.Value.Quality.ShouldBe(Quality.Good);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a simulated data change reaches the stored per-tag subscription callback.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DataChange_ReachesSubscriptionCallback()
|
||||
{
|
||||
|
||||
@@ -13,6 +13,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
/// </summary>
|
||||
public class OpcUaReadToMxAccessWiringTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the resolved OPC UA read path uses the expected full Galaxy tag reference.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_ResolvesCorrectTagReference()
|
||||
{
|
||||
|
||||
@@ -14,6 +14,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
/// </summary>
|
||||
public class OpcUaWriteToMxAccessWiringTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the resolved OPC UA write path targets the expected Galaxy tag reference and payload.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_SendsCorrectTagAndValue()
|
||||
{
|
||||
|
||||
@@ -14,6 +14,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
/// </summary>
|
||||
public class ServiceStartupSequenceTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that startup with fake dependencies creates the expected bridge components and state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Start_WithFakes_AllComponentsCreated()
|
||||
{
|
||||
@@ -71,6 +74,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that when MXAccess is initially unavailable, the background monitor reconnects it later.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Start_WhenMxAccessIsInitiallyDown_MonitorReconnectsInBackground()
|
||||
{
|
||||
|
||||
@@ -15,6 +15,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
/// </summary>
|
||||
public class ShutdownCompletesTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that a started service can shut down within the required time budget.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Shutdown_CompletesWithin30Seconds()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user