Apply code style formatting and restore partial modifiers on Avalonia views

Linter/formatter pass across the full codebase. Restores required partial
keyword on AXAML code-behind classes that the formatter incorrectly removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-31 07:58:13 -04:00
parent 55ef854612
commit 41a6b66943
221 changed files with 4274 additions and 3823 deletions

View File

@@ -5,22 +5,19 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
{
/// <summary>
/// Deterministic authentication provider for integration tests.
/// Validates credentials against hardcoded username/password pairs
/// and returns configured role sets per user.
/// Deterministic authentication provider for integration tests.
/// Validates credentials against hardcoded username/password pairs
/// and returns configured role sets per user.
/// </summary>
internal class FakeAuthenticationProvider : IUserAuthenticationProvider, IRoleProvider
{
private readonly Dictionary<string, string> _credentials =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, IReadOnlyList<string>> _roles =
new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, string> _credentials = new(StringComparer.OrdinalIgnoreCase);
public FakeAuthenticationProvider AddUser(string username, string password, params string[] roles)
private readonly Dictionary<string, IReadOnlyList<string>> _roles = new(StringComparer.OrdinalIgnoreCase);
public IReadOnlyList<string> GetUserRoles(string username)
{
_credentials[username] = password;
_roles[username] = roles;
return this;
return _roles.TryGetValue(username, out var roles) ? roles : new[] { AppRoles.ReadOnly };
}
public bool ValidateCredentials(string username, string password)
@@ -28,9 +25,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
return _credentials.TryGetValue(username, out var expected) && expected == password;
}
public IReadOnlyList<string> GetUserRoles(string username)
public FakeAuthenticationProvider AddUser(string username, string password, params string[] roles)
{
return _roles.TryGetValue(username, out var roles) ? roles : new[] { AppRoles.ReadOnly };
_credentials[username] = password;
_roles[username] = roles;
return this;
}
}
}
}

View File

@@ -7,42 +7,43 @@ 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.
/// 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.
/// Gets or sets the hierarchy rows returned to address-space construction logic.
/// </summary>
public event Action? OnGalaxyChanged;
public List<GalaxyObjectInfo> Hierarchy { get; set; } = new();
/// <summary>
/// Gets or sets the hierarchy rows returned to address-space construction logic.
/// Gets or sets the attribute rows returned to address-space construction logic.
/// </summary>
public List<GalaxyObjectInfo> Hierarchy { get; set; } = new List<GalaxyObjectInfo>();
public List<GalaxyAttributeInfo> Attributes { get; set; } = new();
/// <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.
/// 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.
/// 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.
/// 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.
/// Occurs when the fake repository simulates a Galaxy deploy change.
/// </summary>
public event Action? OnGalaxyChanged;
/// <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>
@@ -53,7 +54,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Returns the configured attribute rows or throws to simulate a repository failure.
/// 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>
@@ -64,7 +65,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Returns the configured deploy timestamp or throws to simulate a repository failure.
/// 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>
@@ -75,7 +76,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Returns the configured connection result or throws to simulate a repository failure.
/// 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>
@@ -86,8 +87,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Raises the deploy-change event so tests can trigger rebuild logic.
/// Raises the deploy-change event so tests can trigger rebuild logic.
/// </summary>
public void RaiseGalaxyChanged() => OnGalaxyChanged?.Invoke();
public void RaiseGalaxyChanged()
{
OnGalaxyChanged?.Invoke();
}
}
}
}

View File

@@ -8,54 +8,56 @@ 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.
/// 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;
private readonly ConcurrentDictionary<string, Action<string, Vtq>> _subscriptions =
new(StringComparer.OrdinalIgnoreCase);
/// <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.
/// 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.
/// 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.
/// 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.
/// 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;
/// <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)
@@ -65,7 +67,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Simulates disconnecting from the runtime.
/// Simulates disconnecting from the runtime.
/// </summary>
public Task DisconnectAsync()
{
@@ -74,7 +76,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Stores a subscription callback so later simulated data changes can target it.
/// 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>
@@ -85,7 +87,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Removes a stored subscription callback for the specified tag reference.
/// 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)
@@ -95,7 +97,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Returns the current in-memory VTQ for a tag reference or a bad-quality placeholder when none has been seeded.
/// 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>
@@ -108,7 +110,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Records a write request, optionally updates the in-memory tag table, and returns the configured write result.
/// 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>
@@ -123,7 +125,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Publishes a simulated tag-value change to both the event stream and any stored subscription callback.
/// Releases the fake client. No unmanaged resources are held.
/// </summary>
public void Dispose()
{
}
/// <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>
@@ -135,7 +144,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Raises a simulated connection-state transition for health and reconnect tests.
/// 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>
@@ -144,10 +153,5 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
State = curr;
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(prev, curr));
}
/// <summary>
/// Releases the fake client. No unmanaged resources are held.
/// </summary>
public void Dispose() { }
}
}
}

View File

@@ -8,77 +8,76 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
{
/// <summary>
/// Fake IMxProxy for testing without the MxAccess COM runtime.
/// Simulates connections, subscriptions, data changes, and writes.
/// Fake IMxProxy for testing without the MxAccess COM runtime.
/// Simulates connections, subscriptions, data changes, and writes.
/// </summary>
public class FakeMxProxy : IMxProxy
{
private int _nextHandle = 1;
private int _connectionHandle;
private bool _registered;
private int _nextHandle = 1;
/// <summary>
/// Occurs when the fake proxy publishes a simulated runtime data-change callback to the system under test.
/// Gets the item-handle to tag-reference map built by the test as attributes are registered with the fake runtime.
/// </summary>
public event MxDataChangeHandler? OnDataChange;
public ConcurrentDictionary<int, string> Items { get; } = new();
/// <summary>
/// Occurs when the fake proxy publishes a simulated write-complete callback to the system under test.
/// Gets the item handles currently marked as advised so tests can assert subscription behavior.
/// </summary>
public event MxWriteCompleteHandler? OnWriteComplete;
public ConcurrentDictionary<int, bool> AdvisedItems { get; } = new();
/// <summary>
/// Gets the item-handle to tag-reference map built by the test as attributes are registered with the fake runtime.
/// Gets the values written through the fake runtime so write scenarios can assert the final payload.
/// </summary>
public ConcurrentDictionary<int, string> Items { get; } = new ConcurrentDictionary<int, string>();
public List<(string Address, object Value)> WrittenValues { get; } = new();
/// <summary>
/// Gets the item handles currently marked as advised so tests can assert subscription behavior.
/// Gets a value indicating whether the fake runtime is currently considered registered.
/// </summary>
public ConcurrentDictionary<int, bool> AdvisedItems { get; } = new ConcurrentDictionary<int, bool>();
public bool IsRegistered { get; private set; }
/// <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.
/// 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.
/// 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.
/// 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.
/// 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.
/// 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.
/// 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.
/// 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>
/// 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>
@@ -86,24 +85,24 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
{
RegisterCallCount++;
if (ShouldFailRegister) throw new InvalidOperationException("Register failed (simulated)");
_registered = true;
IsRegistered = true;
_connectionHandle = Interlocked.Increment(ref _nextHandle);
return _connectionHandle;
}
/// <summary>
/// Simulates tearing down the fake MXAccess connection.
/// 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++;
_registered = false;
IsRegistered = false;
_connectionHandle = 0;
}
/// <summary>
/// Simulates resolving a tag reference into a fake runtime item handle.
/// 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>
@@ -116,7 +115,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Simulates removing an item from the fake runtime session.
/// 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>
@@ -126,7 +125,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Marks an item as actively advised so tests can assert subscription activation.
/// 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>
@@ -136,7 +135,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Marks an item as no longer advised so tests can assert subscription teardown.
/// 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>
@@ -146,7 +145,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Simulates a runtime write, records the written value, and optionally raises the write-complete callback.
/// 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>
@@ -170,12 +169,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
status[0].success = 0;
status[0].detail = (short)WriteCompleteStatus;
}
if (!SkipWriteCompleteCallback)
OnWriteComplete?.Invoke(_connectionHandle, itemHandle, ref status);
}
/// <summary>
/// Simulates an MXAccess data change event for a specific item handle.
/// 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>
@@ -186,26 +186,25 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
var status = new MXSTATUS_PROXY[1];
status[0].success = 1;
OnDataChange?.Invoke(_connectionHandle, itemHandle, value, quality,
(object)(timestamp ?? DateTime.UtcNow), ref status);
timestamp ?? DateTime.UtcNow, ref status);
}
/// <summary>
/// Simulates data change for a specific address (finds handle by address).
/// 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)
public void SimulateDataChangeByAddress(string address, object value, int quality = 192,
DateTime? timestamp = null)
{
foreach (var kvp in Items)
{
if (string.Equals(kvp.Value, address, StringComparison.OrdinalIgnoreCase))
{
SimulateDataChange(kvp.Key, value, quality, timestamp);
return;
}
}
}
}
}
}

View File

@@ -8,57 +8,24 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
{
/// <summary>
/// xUnit fixture that manages an OpcUaService lifecycle with automatic port allocation.
/// Guarantees no port conflicts between parallel tests.
///
/// Usage (per-test):
/// var fixture = OpcUaServerFixture.WithFakes();
/// await fixture.InitializeAsync();
/// try { ... } finally { await fixture.DisposeAsync(); }
///
/// Usage (skip COM entirely):
/// var fixture = OpcUaServerFixture.WithFakeMxAccessClient();
/// xUnit fixture that manages an OpcUaService lifecycle with automatic port allocation.
/// Guarantees no port conflicts between parallel tests.
/// Usage (per-test):
/// var fixture = OpcUaServerFixture.WithFakes();
/// await fixture.InitializeAsync();
/// try { ... } finally { await fixture.DisposeAsync(); }
/// Usage (skip COM entirely):
/// var fixture = OpcUaServerFixture.WithFakeMxAccessClient();
/// </summary>
internal class OpcUaServerFixture : IAsyncLifetime
{
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>
/// The fake Galaxy repository injected into the service. Mutate Hierarchy/Attributes
/// then call Service.TriggerRebuild() to simulate a Galaxy redeployment.
/// </summary>
public FakeGalaxyRepository? GalaxyRepository { get; }
/// <summary>
/// The fake MxAccess client injected into the service (when using WithFakeMxAccessClient).
/// </summary>
public FakeMxAccessClient? MxAccessClient { get; }
/// <summary>
/// The fake MxProxy injected into the service (when using WithFakes).
/// </summary>
public FakeMxProxy? MxProxy { get; }
private readonly OpcUaServiceBuilder _builder;
private bool _started;
/// <summary>
/// Initializes a fixture around a prepared service builder and optional fake dependencies.
/// 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>
@@ -79,8 +46,68 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Creates fixture with FakeMxProxy + FakeGalaxyRepository (standard test data).
/// The STA thread and COM interop run against FakeMxProxy.
/// 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>
/// The fake Galaxy repository injected into the service. Mutate Hierarchy/Attributes
/// then call Service.TriggerRebuild() to simulate a Galaxy redeployment.
/// </summary>
public FakeGalaxyRepository? GalaxyRepository { get; }
/// <summary>
/// The fake MxAccess client injected into the service (when using WithFakeMxAccessClient).
/// </summary>
public FakeMxAccessClient? MxAccessClient { get; }
/// <summary>
/// The fake MxProxy injected into the service (when using WithFakes).
/// </summary>
public FakeMxProxy? MxProxy { get; }
/// <summary>
/// Builds and starts the OPC UA service for the current fixture.
/// </summary>
public Task InitializeAsync()
{
Service = _builder.Build();
Service.Start();
_started = true;
return Task.CompletedTask;
}
/// <summary>
/// Stops the OPC UA service when the fixture had previously been started.
/// </summary>
public Task DisposeAsync()
{
if (_started)
try
{
Service.Stop();
}
catch
{
/* swallow cleanup errors */
}
return Task.CompletedTask;
}
/// <summary>
/// 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>
@@ -101,12 +128,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
.WithGalaxyRepository(r)
.WithGalaxyName("TestGalaxy");
return new OpcUaServerFixture(builder, repo: r, mxProxy: p);
return new OpcUaServerFixture(builder, r, mxProxy: p);
}
/// <summary>
/// Creates fixture using FakeMxAccessClient directly — skips STA thread + COM entirely.
/// Fastest option for tests that don't need real COM interop.
/// 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>
@@ -150,31 +177,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
if (authProvider != null)
builder.WithAuthProvider(authProvider);
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();
Service.Start();
_started = true;
return Task.CompletedTask;
}
/// <summary>
/// Stops the OPC UA service when the fixture had previously been started.
/// </summary>
public Task DisposeAsync()
{
if (_started)
{
try { Service.Stop(); }
catch { /* swallow cleanup errors */ }
}
return Task.CompletedTask;
return new OpcUaServerFixture(builder, r, client);
}
}
}
}

View File

@@ -7,12 +7,12 @@ 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.
/// 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.
/// Confirms that the standard fake-backed fixture starts the bridge and tears it down cleanly.
/// </summary>
[Fact]
public async Task WithFakes_StartsAndStops()
@@ -32,7 +32,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Confirms that the fake-client fixture bypasses COM wiring and uses the provided fake runtime client.
/// Confirms that the fake-client fixture bypasses COM wiring and uses the provided fake runtime client.
/// </summary>
[Fact]
public async Task WithFakeMxAccessClient_SkipsCom()
@@ -48,7 +48,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Confirms that separate fixture instances automatically allocate unique OPC UA ports.
/// Confirms that separate fixture instances automatically allocate unique OPC UA ports.
/// </summary>
[Fact]
public async Task MultipleFixtures_GetUniquePortsAutomatically()
@@ -70,7 +70,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Confirms that fixture shutdown completes quickly enough for the integration test suite.
/// Confirms that fixture shutdown completes quickly enough for the integration test suite.
/// </summary>
[Fact]
public async Task Shutdown_CompletesWithin30Seconds()
@@ -86,7 +86,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Confirms that runtime callbacks arriving after shutdown are ignored cleanly.
/// Confirms that runtime callbacks arriving after shutdown are ignored cleanly.
/// </summary>
[Fact]
public async Task Stop_UnhooksNodeManagerFromMxAccessCallbacks()
@@ -101,7 +101,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Confirms that the fake-backed fixture builds the seeded address space and Galaxy statistics.
/// Confirms that the fake-backed fixture builds the seeded address space and Galaxy statistics.
/// </summary>
[Fact]
public async Task WithFakes_BuildsAddressSpace()
@@ -116,4 +116,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
await fixture.DisposeAsync();
}
}
}
}

View File

@@ -9,20 +9,40 @@ using Opc.Ua.Configuration;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
{
/// <summary>
/// OPC UA client helper for integration tests. Connects to a test server,
/// browses, reads, and subscribes to nodes programmatically.
/// OPC UA client helper for integration tests. Connects to a test server,
/// browses, reads, and subscribes to nodes programmatically.
/// </summary>
internal class OpcUaTestClient : IDisposable
{
private Session? _session;
/// <summary>
/// Gets the active OPC UA session used by integration tests once the helper has connected to the bridge.
/// 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").
/// Closes the test session and releases OPC UA client resources.
/// </summary>
public void Dispose()
{
if (_session != null)
{
try
{
_session.Close();
}
catch
{
/* ignore */
}
_session.Dispose();
}
}
/// <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>
@@ -35,7 +55,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Creates a NodeId in the LmxOpcUa namespace using the server's actual namespace index.
/// 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>
@@ -46,7 +66,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Connects the helper to an OPC UA endpoint exposed by the test bridge.
/// 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>
/// <param name="securityMode">The requested message security mode (default: None).</param>
@@ -115,7 +135,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
var endpointConfig = EndpointConfiguration.Create(config);
var configuredEndpoint = new ConfiguredEndpoint(null, endpoint, endpointConfig);
UserIdentity identity = username != null
var identity = username != null
? new UserIdentity(username, password ?? "")
: new UserIdentity();
@@ -130,30 +150,26 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
var endpoints = client.GetEndpoints(null);
foreach (var ep in endpoints)
{
if (ep.SecurityMode == mode && ep.SecurityPolicyUri == SecurityPolicies.Basic256Sha256)
{
ep.EndpointUrl = endpointUrl;
return ep;
}
}
// Fall back to any matching mode
foreach (var ep in endpoints)
{
if (ep.SecurityMode == mode)
{
ep.EndpointUrl = endpointUrl;
return ep;
}
}
throw new InvalidOperationException(
$"No endpoint with security mode {mode} found on {endpointUrl}");
}
/// <summary>
/// Browse children of a node. Returns list of (DisplayName, NodeId, NodeClass).
/// 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>
@@ -170,14 +186,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
var refs = browser.Browse(nodeId);
foreach (var rd in refs)
{
results.Add((rd.DisplayName.Text, ExpandedNodeId.ToNodeId(rd.NodeId, Session.NamespaceUris), rd.NodeClass));
}
results.Add((rd.DisplayName.Text, ExpandedNodeId.ToNodeId(rd.NodeId, Session.NamespaceUris),
rd.NodeClass));
return results;
}
/// <summary>
/// Read a node's value.
/// 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>
@@ -187,7 +202,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Read a specific OPC UA attribute from a node.
/// Read a specific OPC UA attribute from a node.
/// </summary>
/// <param name="nodeId">The node whose attribute should be read.</param>
/// <param name="attributeId">The OPC UA attribute identifier to read.</param>
@@ -215,8 +230,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Write a node's value, optionally using an OPC UA index range for array element writes.
/// Returns the server status code for the write.
/// 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>
@@ -240,8 +255,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
}
/// <summary>
/// Create a subscription with a monitored item on the given node.
/// Returns the subscription and monitored item for inspection.
/// 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>
@@ -268,18 +283,5 @@ 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)
{
try { _session.Close(); }
catch { /* ignore */ }
_session.Dispose();
}
}
}
}
}

View File

@@ -4,65 +4,117 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
{
/// <summary>
/// Reusable test data matching the Galaxy hierarchy from gr/layout.md.
/// Reusable test data matching the Galaxy hierarchy from gr/layout.md.
/// </summary>
public static class TestData
{
/// <summary>
/// Creates the standard Galaxy hierarchy used by integration and wiring tests.
/// 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>
{
new GalaxyObjectInfo { GobjectId = 1, TagName = "DEV", ContainedName = "DEV", BrowseName = "DEV", ParentGobjectId = 0, IsArea = true },
new GalaxyObjectInfo { GobjectId = 2, TagName = "TestArea", ContainedName = "TestArea", BrowseName = "TestArea", ParentGobjectId = 1, IsArea = true },
new GalaxyObjectInfo { GobjectId = 3, TagName = "TestMachine_001", ContainedName = "TestMachine_001", BrowseName = "TestMachine_001", ParentGobjectId = 2, IsArea = false },
new GalaxyObjectInfo { GobjectId = 4, TagName = "DelmiaReceiver_001", ContainedName = "DelmiaReceiver", BrowseName = "DelmiaReceiver", ParentGobjectId = 3, IsArea = false },
new GalaxyObjectInfo { GobjectId = 5, TagName = "MESReceiver_001", ContainedName = "MESReceiver", BrowseName = "MESReceiver", ParentGobjectId = 3, IsArea = false },
new()
{
GobjectId = 1, TagName = "DEV", ContainedName = "DEV", BrowseName = "DEV", ParentGobjectId = 0,
IsArea = true
},
new()
{
GobjectId = 2, TagName = "TestArea", ContainedName = "TestArea", BrowseName = "TestArea",
ParentGobjectId = 1, IsArea = true
},
new()
{
GobjectId = 3, TagName = "TestMachine_001", ContainedName = "TestMachine_001",
BrowseName = "TestMachine_001", ParentGobjectId = 2, IsArea = false
},
new()
{
GobjectId = 4, TagName = "DelmiaReceiver_001", ContainedName = "DelmiaReceiver",
BrowseName = "DelmiaReceiver", ParentGobjectId = 3, IsArea = false
},
new()
{
GobjectId = 5, TagName = "MESReceiver_001", ContainedName = "MESReceiver",
BrowseName = "MESReceiver", ParentGobjectId = 3, IsArea = false
}
};
}
/// <summary>
/// Creates the standard attribute set used by integration and wiring tests.
/// 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>
{
new GalaxyAttributeInfo { GobjectId = 3, TagName = "TestMachine_001", AttributeName = "MachineID", FullTagReference = "TestMachine_001.MachineID", MxDataType = 5, IsArray = false },
new GalaxyAttributeInfo { GobjectId = 3, TagName = "TestMachine_001", AttributeName = "MachineCode", FullTagReference = "TestMachine_001.MachineCode", MxDataType = 5, IsArray = false },
new GalaxyAttributeInfo { GobjectId = 4, TagName = "DelmiaReceiver_001", AttributeName = "DownloadPath", FullTagReference = "DelmiaReceiver_001.DownloadPath", MxDataType = 5, IsArray = false },
new GalaxyAttributeInfo { GobjectId = 4, TagName = "DelmiaReceiver_001", AttributeName = "JobStepNumber", FullTagReference = "DelmiaReceiver_001.JobStepNumber", MxDataType = 2, IsArray = false },
new GalaxyAttributeInfo { GobjectId = 5, TagName = "MESReceiver_001", AttributeName = "MoveInBatchID", FullTagReference = "MESReceiver_001.MoveInBatchID", MxDataType = 5, IsArray = false },
new GalaxyAttributeInfo { GobjectId = 5, TagName = "MESReceiver_001", AttributeName = "MoveInPartNumbers", FullTagReference = "MESReceiver_001.MoveInPartNumbers[]", MxDataType = 5, IsArray = true, ArrayDimension = 50 },
new()
{
GobjectId = 3, TagName = "TestMachine_001", AttributeName = "MachineID",
FullTagReference = "TestMachine_001.MachineID", MxDataType = 5, IsArray = false
},
new()
{
GobjectId = 3, TagName = "TestMachine_001", AttributeName = "MachineCode",
FullTagReference = "TestMachine_001.MachineCode", MxDataType = 5, IsArray = false
},
new()
{
GobjectId = 4, TagName = "DelmiaReceiver_001", AttributeName = "DownloadPath",
FullTagReference = "DelmiaReceiver_001.DownloadPath", MxDataType = 5, IsArray = false
},
new()
{
GobjectId = 4, TagName = "DelmiaReceiver_001", AttributeName = "JobStepNumber",
FullTagReference = "DelmiaReceiver_001.JobStepNumber", MxDataType = 2, IsArray = false
},
new()
{
GobjectId = 5, TagName = "MESReceiver_001", AttributeName = "MoveInBatchID",
FullTagReference = "MESReceiver_001.MoveInBatchID", MxDataType = 5, IsArray = false
},
new()
{
GobjectId = 5, TagName = "MESReceiver_001", AttributeName = "MoveInPartNumbers",
FullTagReference = "MESReceiver_001.MoveInPartNumbers[]", MxDataType = 5, IsArray = true,
ArrayDimension = 50
}
};
}
/// <summary>
/// Creates a minimal hierarchy containing a single object for focused unit tests.
/// 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>
{
new GalaxyObjectInfo { GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false }
new()
{
GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false
}
};
}
/// <summary>
/// Creates a minimal attribute set containing a single scalar attribute for focused unit tests.
/// 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>
{
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "TestAttr", FullTagReference = "TestObj.TestAttr", MxDataType = 5, IsArray = false }
new()
{
GobjectId = 1, TagName = "TestObj", AttributeName = "TestAttr",
FullTagReference = "TestObj.TestAttr", MxDataType = 5, IsArray = false
}
};
}
}
}
}