test(mqtt): pin status-code half of republish suppression; note Sparkplug bypass

This commit is contained in:
Joseph Doherty
2026-08-16 04:16:35 -04:00
parent 39f67a0fce
commit 50035b36bb
2 changed files with 44 additions and 0 deletions
@@ -1032,6 +1032,13 @@ public sealed class MqttSubscriptionManagerTests
/// <c>DegradeTopic</c>: a Good value followed by a refused subscription must reach the
/// subscriber as Bad, or the client keeps a stale Good forever.
/// </summary>
/// <remarks>
/// This pins the <c>DegradeTopic</c> path specifically — <c>DegradeTopic</c>'s own
/// <c>Raise</c> call was already unconditional before the suppression change and is untouched
/// by it. It does NOT exercise the <c>prior.StatusCode == snapshot.StatusCode</c> comparison
/// added inside <see cref="MqttSubscriptionManager.Deliver"/>; see
/// <see cref="Different_malformed_payloads_with_the_same_null_value_both_raise"/> for that.
/// </remarks>
[Fact]
public async Task A_status_transition_still_raises()
{
@@ -1057,6 +1064,39 @@ public sealed class MqttSubscriptionManagerTests
mgr.Values.Read(OvenTempPath).StatusCode.ShouldBe(BadCommunicationError);
}
/// <summary>
/// Pins the status-code half of <c>Deliver</c>'s suppression comparison
/// (<c>prior.StatusCode == snapshot.StatusCode</c>) through the LIVE
/// <see cref="MqttSubscriptionManager.HandleMessage"/> path, which
/// <see cref="A_status_transition_still_raises"/> does not reach (that test goes through the
/// unconditional <c>DegradeTopic</c> path instead). Two different malformed payloads on the
/// SAME topic each decode to a <c>null</c> value but a DIFFERENT Bad status code — a decode
/// failure (<c>BadDecodingError</c>) followed by a value that decodes but fails type coercion
/// (<c>BadTypeMismatch</c>). A value-only comparison (ignoring status) would see
/// <c>null == null</c> on both transitions and wrongly suppress the second.
/// </summary>
[Fact]
public async Task Different_malformed_payloads_with_the_same_null_value_both_raise()
{
var mgr = NewManager();
mgr.Register([Tag(OvenTempPath, $$"""{"topic":"{{OvenTopic}}","payloadFormat":"Json","jsonPath":"$.value","dataType":"Float64"}""")]);
await mgr.SubscribeAsync([OvenTempPath], TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken);
var raised = new List<DataValueSnapshot>();
mgr.OnDataChange += (_, e) => raised.Add(e.Snapshot);
// Decode failure: not valid JSON at all.
mgr.HandleMessage(OvenTopic, "{not json"u8.ToArray(), retained: false);
// Decodes fine, but the selected value does not fit the declared Float64 type.
mgr.HandleMessage(OvenTopic, """{"value":"not-a-number"}"""u8.ToArray(), retained: false);
raised.Count.ShouldBe(2);
raised[0].StatusCode.ShouldBe(BadDecodingError);
raised[0].Value.ShouldBeNull();
raised[1].StatusCode.ShouldBe(BadTypeMismatch);
raised[1].Value.ShouldBeNull();
}
/// <summary>Records every subscribe call so dedupe / re-subscribe can be asserted without a broker.</summary>
private sealed class FakeTransport : IMqttSubscribeTransport
{