f79d13e2d8
Task 9. Adds MqttDriverFactoryExtensions (DriverTypeName = DriverTypeNames.Mqtt, direct deserialization of MqttDriverOptions — no intermediate DTO, mirroring OpcUaClientDriverFactoryExtensions), the DriverTypeNames.Mqtt constant, and both host wiring sites: the factory in DriverFactoryBootstrap.Register and the probe in AddOtOpcUaDriverProbes (the admin-node path Program.cs calls in its hasAdmin block — a probe wired only on driver nodes makes Test-connect silently dead). Carried-forward items: - Converges every MQTT config-parsing seam onto ONE JsonSerializerOptions — MqttJson.Options, in .Contracts alongside MqttDriverOptions. It replaces the probe's internal JsonOpts and the browser's separate private copy; the factory, the probe, MqttDriver.ParseOptions and MqttDriverBrowser now all parse through it. .Contracts is the only assembly all four consumers reference, and the browser's reference to the runtime .Driver project is a documented layering exception scheduled for removal — anchoring the options there would resurrect the duplicate the day it goes away. - Replaces the "Mqtt" literals in MqttDriverProbe, MqttDriverBrowser and MqttDriver with the constant (string value unchanged). - Tightens MqttDriverProbeTests.ProbeAsync_EnumAsName: it asserted only Ok == false + non-empty message, which is also exactly what a JSON-parse failure produces — so it stayed green under the very regression it names. It now asserts the probe got past the parse and reached the network. Falsifiability: deleting JsonStringEnumConverter from MqttJson.Options reddens 9 tests across 4 suites, including the tightened probe test (message becomes "Config JSON is invalid: The JSON value could not be converted to MqttProtocolVersion") — which the pre-fix assertions would have passed. Also references the MQTT driver from Core.Abstractions.Tests so DriverTypeNamesGuardTests' reflective bin scan discovers the new factory and the constant/factory parity check stays honest. Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
152 lines
5.9 KiB
C#
152 lines
5.9 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests;
|
|
|
|
/// <summary>
|
|
/// Tests for <see cref="MqttDriverFactoryExtensions"/> — the seam that turns a persisted
|
|
/// <c>DriverInstance.DriverConfig</c> blob into a live <see cref="MqttDriver"/>. Mirrors
|
|
/// <c>OpcUaClientDriverFactoryExtensions</c>' shape (direct deserialization into the options
|
|
/// record, no intermediate DTO).
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class MqttDriverFactoryExtensionsTests
|
|
{
|
|
[Fact]
|
|
public void Register_ThenCreate_BuildsMqttDriver()
|
|
{
|
|
var registry = new DriverFactoryRegistry();
|
|
MqttDriverFactoryExtensions.Register(registry);
|
|
|
|
var factory = registry.TryGet(MqttDriverFactoryExtensions.DriverTypeName);
|
|
factory.ShouldNotBeNull();
|
|
|
|
var driver = factory("d1", """{"host":"h","port":1883,"mode":"Plain"}""");
|
|
|
|
driver.ShouldBeOfType<MqttDriver>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The factory's registration key and the canonical constant must be the same string. A
|
|
/// drift here is invisible at compile time and silently breaks dispatch — the repo's
|
|
/// <c>ModbusTcp</c>/<c>Modbus</c> incident.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DriverTypeName_MatchesConstant()
|
|
=> MqttDriverFactoryExtensions.DriverTypeName.ShouldBe(DriverTypeNames.Mqtt);
|
|
|
|
/// <summary>
|
|
/// Third independent pin on the literal itself: both the constant and the factory name could
|
|
/// be renamed together and still agree with each other while disagreeing with every persisted
|
|
/// <c>DriverInstance.DriverType</c> row.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DriverTypeName_IsExactlyMqtt()
|
|
{
|
|
MqttDriverFactoryExtensions.DriverTypeName.ShouldBe("Mqtt");
|
|
DriverTypeNames.Mqtt.ShouldBe("Mqtt");
|
|
DriverTypeNames.All.ShouldContain("Mqtt");
|
|
}
|
|
|
|
/// <summary>
|
|
/// <c>rawTags</c> is the ONLY source of the driver's discoverable node set (plain MQTT has no
|
|
/// browsable address space), so a factory that parsed everything else correctly but dropped
|
|
/// this array would produce a driver that connects, subscribes to nothing and materializes
|
|
/// nothing — green everywhere, dark in production.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateInstance_CarriesRawTagsThroughToDiscovery()
|
|
{
|
|
const string tagConfig = """{"topic":"f/t","payloadFormat":"Raw","dataType":"String"}""";
|
|
var json = JsonSerializer.Serialize(new
|
|
{
|
|
host = "h",
|
|
port = 1883,
|
|
mode = "Plain",
|
|
rawTags = new[]
|
|
{
|
|
new { rawPath = "Plant/Mqtt/dev1/Temp", tagConfig, writeIdempotent = false },
|
|
},
|
|
});
|
|
|
|
var driver = MqttDriverFactoryExtensions.CreateInstance("d1", json);
|
|
|
|
var builder = new RecordingBuilder();
|
|
await driver.DiscoverAsync(builder, CancellationToken.None);
|
|
|
|
builder.VariableFullNames.ShouldBe(["Plant/Mqtt/dev1/Temp"]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enum-valued knobs must bind from their string NAMES — the repo's documented systemic
|
|
/// enum-serialization bug is an AdminUI-authored config whose enum field one seam can read
|
|
/// and another cannot. Removing <see cref="JsonStringEnumConverter"/> from
|
|
/// <see cref="MqttJson.Options"/> makes this throw.
|
|
/// </summary>
|
|
[Fact]
|
|
public void CreateInstance_EnumsAuthoredAsNames_Bind()
|
|
{
|
|
const string json = """
|
|
{"host":"h","port":8883,"mode":"SparkplugB","protocolVersion":"V311",
|
|
"sparkplug":{"groupId":"Plant1","hostId":"h1"}}
|
|
""";
|
|
|
|
var driver = MqttDriverFactoryExtensions.CreateInstance("d1", json);
|
|
|
|
driver.ShouldBeOfType<MqttDriver>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Direct pin on the single shared instance: the enum converter is what the test above
|
|
/// exercises behaviourally, and this asserts it structurally so a reader can see the one
|
|
/// instance every MQTT config seam parses through.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SharedJsonOptions_CarryTheStringEnumConverter()
|
|
=> MqttJson.Options.Converters.ShouldContain(c => c is JsonStringEnumConverter);
|
|
|
|
[Fact]
|
|
public void CreateInstance_BlankConfig_Throws()
|
|
=> Should.Throw<ArgumentException>(() => MqttDriverFactoryExtensions.CreateInstance("d1", " "));
|
|
|
|
[Fact]
|
|
public void CreateInstance_JsonNullLiteral_ThrowsInvalidOperation()
|
|
=> Should.Throw<InvalidOperationException>(() => MqttDriverFactoryExtensions.CreateInstance("d1", "null"));
|
|
|
|
/// <summary>
|
|
/// Minimal discovery recorder — local to this suite because the driver test project does not
|
|
/// reference <c>Commons</c>, where the runtime's capturing builder lives (same reason
|
|
/// <c>MqttDriverDiscoveryTests</c> carries its own).
|
|
/// </summary>
|
|
private sealed class RecordingBuilder : IAddressSpaceBuilder
|
|
{
|
|
public List<string> VariableFullNames { get; } = [];
|
|
|
|
public IAddressSpaceBuilder Folder(string browseName, string displayName) => this;
|
|
|
|
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
|
|
{
|
|
VariableFullNames.Add(attributeInfo.FullName);
|
|
return new Handle(attributeInfo.FullName);
|
|
}
|
|
|
|
public void AddProperty(string browseName, DriverDataType dataType, object? value) { }
|
|
|
|
private sealed class Handle(string fullRef) : IVariableHandle
|
|
{
|
|
public string FullReference => fullRef;
|
|
|
|
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
|
|
}
|
|
|
|
private sealed class NullSink : IAlarmConditionSink
|
|
{
|
|
public void OnTransition(AlarmEventArgs args) { }
|
|
}
|
|
}
|
|
}
|