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);
}