test(galaxy.browser): unit + fake-transport session coverage
This commit is contained in:
@@ -82,6 +82,7 @@
|
||||
</Folder>
|
||||
<Folder Name="/tests/Drivers/">
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests.csproj" />
|
||||
|
||||
+3
@@ -9,6 +9,9 @@
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Commons\ZB.MOM.WW.OtOpcUa.Commons.csproj" />
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser.Tests" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!-- Vendored mxaccessgw .NET client — same DLLs as Driver.Galaxy.
|
||||
See Driver.Galaxy/libs/README.md for the unwinding plan. -->
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
using MxGateway.Client;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Pure-construction + lifecycle coverage of <see cref="GalaxyBrowseSession"/>.
|
||||
/// The session is <c>internal</c>; visibility comes via <c>InternalsVisibleTo</c>
|
||||
/// on the production project.
|
||||
/// <para>
|
||||
/// <b>Blocker:</b> the hierarchy/expand/attribute paths all call into
|
||||
/// <see cref="GalaxyRepositoryClient"/>, which only ships an <c>internal</c>
|
||||
/// transport seam (<c>IGalaxyRepositoryClientTransport</c>) and an <c>internal</c>
|
||||
/// constructor — both keyed via <c>InternalsVisibleTo</c> on the vendored
|
||||
/// <c>MxGateway.Client</c> assembly, and only granted to that repo's own
|
||||
/// <c>MxGateway.Client.Tests</c>. We can't substitute a fake transport from
|
||||
/// here without changing the upstream repo, and the public <c>Create</c>
|
||||
/// factory always opens a real gRPC channel. So in-memory traversal coverage
|
||||
/// (RootAsync / ExpandAsync / AttributesAsync, including the SecurityClass
|
||||
/// mapping) is deferred to the integration suite (Task 17) and the manual
|
||||
/// smoke pass (Task 18) — both of which run the gateway for real.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class GalaxyBrowseSessionTests
|
||||
{
|
||||
/// <summary>Builds a <see cref="GalaxyRepositoryClient"/> bound to an
|
||||
/// unreachable endpoint. No connection is opened — <c>Create</c> just builds the
|
||||
/// gRPC channel object — so this is safe to call without a fixture.</summary>
|
||||
private static GalaxyRepositoryClient NewClient() =>
|
||||
GalaxyRepositoryClient.Create(new MxGatewayClientOptions
|
||||
{
|
||||
Endpoint = new Uri("http://127.0.0.1:1"),
|
||||
ApiKey = "test-key",
|
||||
UseTls = false,
|
||||
ConnectTimeout = TimeSpan.FromSeconds(1),
|
||||
DefaultCallTimeout = TimeSpan.FromSeconds(1),
|
||||
});
|
||||
|
||||
/// <summary>The internal ctor must reject a null client — the production caller
|
||||
/// (the factory in <c>GalaxyDriverBrowser.OpenAsync</c>) hands off ownership of a
|
||||
/// real client and never passes null, but defence-in-depth catches a future caller
|
||||
/// who skips that handoff.</summary>
|
||||
[Fact]
|
||||
public void Constructor_with_null_client_throws_ArgumentNullException()
|
||||
{
|
||||
Should.Throw<ArgumentNullException>(() => new GalaxyBrowseSession(null!));
|
||||
}
|
||||
|
||||
/// <summary>Each session must publish a distinct <see cref="GalaxyBrowseSession.Token"/>
|
||||
/// so the AdminUI registry can disambiguate concurrent browse sessions against the
|
||||
/// same driver config.</summary>
|
||||
[Fact]
|
||||
public async Task Token_is_unique_per_session()
|
||||
{
|
||||
await using var a = new GalaxyBrowseSession(NewClient());
|
||||
await using var b = new GalaxyBrowseSession(NewClient());
|
||||
a.Token.ShouldNotBe(b.Token);
|
||||
a.Token.ShouldNotBe(Guid.Empty);
|
||||
}
|
||||
|
||||
/// <summary><see cref="GalaxyBrowseSession.LastUsedUtc"/> is primed to the
|
||||
/// construction time so the registry reaper has a sensible baseline before the
|
||||
/// first Root/Expand/Attributes call lands.</summary>
|
||||
[Fact]
|
||||
public async Task LastUsedUtc_is_initialized_at_construction()
|
||||
{
|
||||
var before = DateTime.UtcNow;
|
||||
await using var session = new GalaxyBrowseSession(NewClient());
|
||||
var after = DateTime.UtcNow;
|
||||
// Allow generous slop — the field is set inside the ctor body, both bookends
|
||||
// are wall-clock UtcNow, and we only care that it isn't default(DateTime).
|
||||
session.LastUsedUtc.ShouldBeGreaterThanOrEqualTo(before.AddSeconds(-1));
|
||||
session.LastUsedUtc.ShouldBeLessThanOrEqualTo(after.AddSeconds(1));
|
||||
}
|
||||
|
||||
/// <summary><see cref="GalaxyBrowseSession.DisposeAsync"/> is idempotent — the
|
||||
/// registry's reaper may race a client-initiated close, so the second call must
|
||||
/// no-op rather than throw <see cref="ObjectDisposedException"/> or hit the
|
||||
/// already-disposed gRPC channel.</summary>
|
||||
[Fact]
|
||||
public async Task DisposeAsync_is_idempotent()
|
||||
{
|
||||
var session = new GalaxyBrowseSession(NewClient());
|
||||
await session.DisposeAsync();
|
||||
// Second call should silently no-op.
|
||||
await Should.NotThrowAsync(async () => await session.DisposeAsync());
|
||||
}
|
||||
|
||||
/// <summary>After disposal, any <see cref="GalaxyBrowseSession.ExpandAsync"/> call
|
||||
/// must surface <see cref="ObjectDisposedException"/> — not a downstream channel
|
||||
/// fault — so the AdminUI sees a clean "session closed" signal.</summary>
|
||||
[Fact]
|
||||
public async Task ExpandAsync_after_dispose_throws_ObjectDisposedException()
|
||||
{
|
||||
var session = new GalaxyBrowseSession(NewClient());
|
||||
await session.DisposeAsync();
|
||||
await Should.ThrowAsync<ObjectDisposedException>(
|
||||
() => session.ExpandAsync("anything", TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
/// <summary><see cref="GalaxyBrowseSession.ExpandAsync"/> must reject a tag that
|
||||
/// hasn't been seen by a prior Root/Expand call — the cache is the source of
|
||||
/// truth, and silently returning [] would mask AdminUI bugs that browse with a
|
||||
/// stale path.</summary>
|
||||
[Fact]
|
||||
public async Task ExpandAsync_unknown_tag_throws_ArgumentException()
|
||||
{
|
||||
await using var session = new GalaxyBrowseSession(NewClient());
|
||||
// No RootAsync call ⇒ cache is empty ⇒ any tag is unknown.
|
||||
await Should.ThrowAsync<ArgumentException>(
|
||||
() => session.ExpandAsync("Galaxy.Unknown", TestContext.Current.CancellationToken));
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit-only coverage of <see cref="GalaxyDriverBrowser"/>'s pre-connect validation.
|
||||
/// These tests do not require a live mxaccessgw endpoint and are safe to run without
|
||||
/// the gateway fixture — they exercise the JSON deserialization + validation paths
|
||||
/// that run before <c>GalaxyRepositoryClient.Create</c> + <c>TestConnectionAsync</c>.
|
||||
/// The factory's transport-construction path is covered by the integration suite
|
||||
/// (Task 17) and manual smoke (Task 18) since both require a real gateway.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class GalaxyDriverBrowserTests
|
||||
{
|
||||
private readonly GalaxyDriverBrowser _sut = new();
|
||||
|
||||
/// <summary>The DriverType key must match the AdminUI's persisted "Galaxy" value
|
||||
/// so the factory wire-up picks the right browser implementation.</summary>
|
||||
[Fact]
|
||||
public void DriverType_is_Galaxy() => _sut.DriverType.ShouldBe("Galaxy");
|
||||
|
||||
/// <summary>An empty Gateway.Endpoint must fail fast with a clear, endpoint-mentioning
|
||||
/// message rather than surfacing a downstream gRPC URI parse error.</summary>
|
||||
[Fact]
|
||||
public async Task OpenAsync_with_empty_endpoint_throws_InvalidOperationException()
|
||||
{
|
||||
var json = """{"Gateway":{"Endpoint":"","ApiKeySecretRef":"dev:k"},"MxAccess":{"ClientName":"X"},"Repository":{},"Reconnect":{}}""";
|
||||
var ex = await Should.ThrowAsync<InvalidOperationException>(
|
||||
() => _sut.OpenAsync(json, TestContext.Current.CancellationToken));
|
||||
ex.Message.ShouldContain("Endpoint");
|
||||
}
|
||||
|
||||
/// <summary>An empty MxAccess.ClientName must fail fast — refused so the gateway
|
||||
/// side doesn't see anonymous browse sessions during triage.</summary>
|
||||
[Fact]
|
||||
public async Task OpenAsync_with_empty_clientName_throws_InvalidOperationException()
|
||||
{
|
||||
var json = """{"Gateway":{"Endpoint":"http://127.0.0.1:1","ApiKeySecretRef":"dev:k"},"MxAccess":{"ClientName":""},"Repository":{},"Reconnect":{}}""";
|
||||
var ex = await Should.ThrowAsync<InvalidOperationException>(
|
||||
() => _sut.OpenAsync(json, TestContext.Current.CancellationToken));
|
||||
ex.Message.ShouldContain("ClientName");
|
||||
}
|
||||
|
||||
/// <summary>A JSON literal that deserializes to null must fail fast with a
|
||||
/// "deserialized to null" message — never a downstream NRE.</summary>
|
||||
[Fact]
|
||||
public async Task OpenAsync_with_null_json_throws_InvalidOperationException()
|
||||
{
|
||||
var ex = await Should.ThrowAsync<InvalidOperationException>(
|
||||
() => _sut.OpenAsync("null", TestContext.Current.CancellationToken));
|
||||
ex.Message.ShouldContain("null");
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3"/>
|
||||
<PackageReference Include="Shouldly"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser.csproj"/>
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts.csproj"/>
|
||||
<ProjectReference Include="..\..\..\src\Core\ZB.MOM.WW.OtOpcUa.Commons\ZB.MOM.WW.OtOpcUa.Commons.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Vendored mxaccessgw client + contracts DLLs. The Browser project under test
|
||||
holds the same binary references; the explicit duplicates here let us
|
||||
construct a GalaxyRepositoryClient against an unreachable endpoint for
|
||||
dispose-idempotency coverage, and make MxGateway types visible to the
|
||||
test assembly. See
|
||||
..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\libs\README.md for
|
||||
the unwinding plan. -->
|
||||
<Reference Include="MxGateway.Client">
|
||||
<HintPath>..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\libs\MxGateway.Client.dll</HintPath>
|
||||
<Private>true</Private>
|
||||
</Reference>
|
||||
<Reference Include="MxGateway.Contracts">
|
||||
<HintPath>..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\libs\MxGateway.Contracts.dll</HintPath>
|
||||
<Private>true</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user