using System.Text.Json; using MQTTnet; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests; /// /// The operator-visible half of the refused-CONNACK defect. /// pins that MqttConnection.ConnectAsync raises the rejection; this pins the thing an /// operator actually sees — that a driver whose broker refused the CONNECT does not seal /// green. /// /// /// Worth its own suite because the two failures are independent. The connection could classify a /// rejection perfectly and the driver could still report Healthy, simply by not letting the /// failure reach InitializeCoreAsync's catch — which is exactly the shape the Task-13 live /// fixture found (a wrong broker password produced DriverState.Healthy / /// HostState.Running on a session that never authenticated). /// [Trait("Category", "Unit")] public sealed class MqttDriverConnectRejectionTests { /// /// THE regression pin. A configured-wrong password must fail the deploy loudly, not produce a /// healthy driver that will never receive a single value. /// [Fact] public async Task InitializeAsync_BrokerRefusesTheCredentials_IsNotHealthy() { using var broker = new MiniBroker { ConnAckReturnCode = MiniBroker.ReturnCodeNotAuthorized }; var options = BrokerOptions(broker.Port); await using var driver = new MqttDriver(options, "d", null); var ex = await Should.ThrowAsync( async () => await driver.InitializeAsync(Json(options), TestContext.Current.CancellationToken)); ex.ResultCode.ShouldBe(MqttClientConnectResultCode.NotAuthorized); var health = driver.GetHealth(); health.State.ShouldNotBe(DriverState.Healthy, "a driver the broker refused must never seal green"); health.State.ShouldBe(DriverState.Faulted); health.LastError.ShouldNotBeNullOrWhiteSpace(); health.LastError!.ShouldContain("rejected the credentials"); } /// /// The transient counterpart: still a failed initialize (the driver has no session), but it must /// not be reported as the same unrecoverable shape — a broker restarting is not a misconfigured /// deployment. /// [Fact] public async Task InitializeAsync_BrokerTemporarilyUnavailable_FailsAsTransient() { using var broker = new MiniBroker { ConnAckReturnCode = MiniBroker.ReturnCodeServerUnavailable }; var options = BrokerOptions(broker.Port); await using var driver = new MqttDriver(options, "d", null); var ex = await Should.ThrowAsync( async () => await driver.InitializeAsync(Json(options), TestContext.Current.CancellationToken)); ex.IsUnrecoverable.ShouldBeFalse(); driver.GetHealth().State.ShouldNotBe(DriverState.Healthy); } /// /// The control. Without it, every assertion above would still hold if the driver simply never /// went healthy against this fake broker — the suite would be green and prove nothing. /// [Fact] public async Task InitializeAsync_BrokerAcceptsTheConnect_IsHealthy() { using var broker = new MiniBroker(); var options = BrokerOptions(broker.Port); await using var driver = new MqttDriver(options, "d", null); await driver.InitializeAsync(Json(options), TestContext.Current.CancellationToken); driver.GetHealth().State.ShouldBe(DriverState.Healthy); } private static MqttDriverOptions BrokerOptions(int port) => new() { Mode = MqttMode.Plain, Host = "127.0.0.1", Port = port, UseTls = false, ProtocolVersion = MqttProtocolVersion.V311, Username = "svc", Password = "pw", KeepAliveSeconds = 300, ConnectTimeoutSeconds = 5, ReconnectMinBackoffSeconds = 1, ReconnectMaxBackoffSeconds = 2, }; private static string Json(MqttDriverOptions options) => JsonSerializer.Serialize(options, MqttJson.Options); }