using Shouldly; using Xunit; using ZB.MOM.WW.Secrets.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser.Tests; /// /// Unit-only coverage of '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 GalaxyRepositoryClient.Create + TestConnectionAsync. /// 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. /// [Trait("Category", "Unit")] public sealed class GalaxyDriverBrowserTests { // These unit tests exercise only the pre-connect validation paths (empty endpoint, // blank client name) that throw BEFORE the API-key secret: arm is resolved, so a // null-returning stub resolver suffices — it is never consulted. private readonly GalaxyDriverBrowser _sut = new(new StubSecretResolver()); /// A no-op resolver: every name resolves to null. Never consulted by these tests. private sealed class StubSecretResolver : ISecretResolver { /// public Task GetAsync(SecretName name, CancellationToken ct) => Task.FromResult(null); } /// The DriverType key must match the AdminUI's persisted "GalaxyMxGateway" value /// so the factory wire-up picks the right browser implementation. [Fact] public void DriverType_is_GalaxyMxGateway() => _sut.DriverType.ShouldBe("GalaxyMxGateway"); /// An empty Gateway.Endpoint must fail fast with a clear, endpoint-mentioning /// message rather than surfacing a downstream gRPC URI parse error. [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( () => _sut.OpenAsync(json, TestContext.Current.CancellationToken)); ex.Message.ShouldContain("Endpoint"); } /// An empty MxAccess.ClientName must fail fast — refused so the gateway /// side doesn't see anonymous browse sessions during triage. [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( () => _sut.OpenAsync(json, TestContext.Current.CancellationToken)); ex.Message.ShouldContain("ClientName"); } /// A JSON literal that deserializes to null must fail fast with a /// "deserialized to null" message — never a downstream NRE. [Fact] public async Task OpenAsync_with_null_json_throws_InvalidOperationException() { var ex = await Should.ThrowAsync( () => _sut.OpenAsync("null", TestContext.Current.CancellationToken)); ex.Message.ShouldContain("null"); } }