using System.Reflection; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests; /// /// Pins the MQTT project layering that Gitea #512 restored: .Browser depends on the shared /// .Contracts + .Transport leaves, never on the runtime .Driver. /// /// /// /// Why this is worth a test. .Browser used to reference the runtime driver, and the /// AdminUI references .Browser — so the AdminUI's build/deploy graph transitively contained /// the whole MQTT driver runtime plus Core, Polly.Core and Serilog. Nothing failed; the cost was /// invisible. A reference re-added out of convenience would be invisible in exactly the same way, /// which is precisely what a guard is for. /// /// /// What it can and cannot catch. It asserts on emitted assembly references, so it fires when /// the browser actually uses a driver-runtime type — the case that matters. A /// ProjectReference re-added but never used would not trip it (the compiler emits no /// reference), so the csproj comment remains the primary record of the decision. /// /// /// That gap is now covered by DriverBrowserLayeringTests (Commons.Tests), which /// scans the csproj graph of every *.Browser project in the tree — added for Gitea #526, /// after the identical violation was found sitting in Driver.Sql.Browser with no guard /// watching it, because this file was written for the one driver that had an issue filed. What /// stays here is the MQTT-specific half: the .Transport leaf shape and the /// MQTTnet-free-ness of .Contracts. /// /// [Trait("Category", "Unit")] public sealed class MqttBrowserLayeringTests { private const string BrowserAssembly = "ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Browser"; private const string DriverRuntimeAssembly = "ZB.MOM.WW.OtOpcUa.Driver.Mqtt"; private const string ContractsAssembly = "ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Contracts"; private const string TransportAssembly = "ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Transport"; private static string[] BrowserReferences() { var browser = Assembly.Load(BrowserAssembly); return [.. browser.GetReferencedAssemblies().Select(a => a.Name!)]; } [Fact] public void Browser_does_not_reference_the_runtime_driver() => BrowserReferences().ShouldNotContain( DriverRuntimeAssembly, "the MQTT browser is back to referencing the runtime driver — every other bespoke browser " + "in this repo (OpcUaClient, Galaxy) depends on its .Contracts leaf only, and this reference " + "reaches the AdminUI. If shared connection policy is needed, put it in .Transport; if shared " + "protocol/codec is needed, put it in .Contracts. Do NOT copy the TLS/CA-pin logic."); [Fact] public void Browser_reaches_the_shared_leaves_instead() { // The positive control. Asserting only the absence above would also pass if the browser stopped // referencing anything MQTT at all — e.g. if this test loaded the wrong assembly, or the type // names drifted — so pin what it SHOULD depend on in the same breath. var references = BrowserReferences(); references.ShouldContain(ContractsAssembly); references.ShouldContain(TransportAssembly); } [Fact] public void Transport_stays_a_leaf_that_does_not_reach_back_into_the_runtime_driver() { // .Transport holds connection POLICY only. If it ever referenced the runtime driver, routing the // browser through it would restore the coupling this whole change removed, one hop further out. var references = Assembly.Load(TransportAssembly) .GetReferencedAssemblies() .Select(a => a.Name!) .ToArray(); references.ShouldNotContain(DriverRuntimeAssembly); references.ShouldNotContain(BrowserAssembly); } [Fact] public void Contracts_stays_transport_free() => Assembly.Load(ContractsAssembly) .GetReferencedAssemblies() .Select(a => a.Name!) .ShouldNotContain( "MQTTnet", "Driver.Mqtt.Contracts is deliberately transport-free — it carries the Sparkplug wire " + "schema and codec, and is referenced by ControlPlane, which has no business with a " + "broker client. MQTTnet-typed connection policy belongs in .Transport."); }