Fix E2E test gaps and add comprehensive E2E + parity test suites

- Fix pull consumer fetch: send original stream subject in HMSG (not inbox)
  so NATS client distinguishes data messages from control messages
- Fix MaxAge expiry: add background timer in StreamManager for periodic pruning
- Fix JetStream wire format: Go-compatible anonymous objects with string enums,
  proper offset-based pagination for stream/consumer list APIs
- Add 42 E2E black-box tests (core messaging, auth, TLS, accounts, JetStream)
- Add ~1000 parity tests across all subsystems (gaps closure)
- Update gap inventory docs to reflect implementation status
This commit is contained in:
Joseph Doherty
2026-03-12 14:09:23 -04:00
parent 79c1ee8776
commit c30e67a69d
226 changed files with 17801 additions and 709 deletions

View File

@@ -0,0 +1,92 @@
using NATS.Server.Mqtt;
namespace NATS.Server.Tests.Mqtt;
public class MqttModelParityBatch3Tests
{
[Fact]
public void Mqtt_helper_models_cover_go_core_shapes()
{
var jsa = new MqttJsa
{
AccountName = "A",
ReplyPrefix = "$MQTT.JSA.A",
Domain = "D1",
};
var pubMsg = new MqttJsPubMsg
{
Subject = "$MQTT.msgs.s1",
Payload = new byte[] { 1, 2, 3 },
ReplyTo = "$MQTT.JSA.A.reply",
};
var delete = new MqttRetMsgDel
{
Topic = "devices/x",
Sequence = 123,
};
var persisted = new MqttPersistedSession
{
ClientId = "c1",
LastPacketId = 7,
MaxAckPending = 1024,
};
var retainedRef = new MqttRetainedMessageRef
{
StreamSequence = 88,
Subject = "$MQTT.rmsgs.devices/x",
};
var sub = new MqttSub
{
Filter = "devices/+",
Qos = 1,
JsDur = "DUR-c1",
Prm = true,
Reserved = false,
};
var filter = new MqttFilter
{
Filter = "devices/#",
Qos = 1,
TopicToken = "devices",
};
var parsedHeader = new MqttParsedPublishNatsHeader
{
Subject = "devices/x",
Mapped = "devices.y",
IsPublish = true,
IsPubRel = false,
};
jsa.AccountName.ShouldBe("A");
pubMsg.Payload.ShouldBe(new byte[] { 1, 2, 3 });
delete.Sequence.ShouldBe(123UL);
persisted.MaxAckPending.ShouldBe(1024);
retainedRef.StreamSequence.ShouldBe(88UL);
sub.JsDur.ShouldBe("DUR-c1");
filter.TopicToken.ShouldBe("devices");
parsedHeader.IsPublish.ShouldBeTrue();
}
[Fact]
public void Retained_message_model_includes_origin_flags_and_source_fields()
{
var msg = new MqttRetainedMessage(
Topic: "devices/x",
Payload: new byte[] { 0x41, 0x42 },
Origin: "origin-a",
Flags: 0b_0000_0011,
Source: "src-a");
msg.Topic.ShouldBe("devices/x");
msg.Origin.ShouldBe("origin-a");
msg.Flags.ShouldBe((byte)0b_0000_0011);
msg.Source.ShouldBe("src-a");
}
}

View File

@@ -0,0 +1,74 @@
using System.Text;
using NATS.Server.Mqtt;
namespace NATS.Server.Tests.Mqtt;
public class MqttProtocolConstantsParityBatch1Tests
{
[Fact]
public void Constants_match_mqtt_go_reference_values()
{
MqttProtocolConstants.SubscribeFlags.ShouldBe((byte)0x02);
MqttProtocolConstants.ConnAckAccepted.ShouldBe((byte)0x00);
MqttProtocolConstants.ConnAckUnacceptableProtocolVersion.ShouldBe((byte)0x01);
MqttProtocolConstants.ConnAckIdentifierRejected.ShouldBe((byte)0x02);
MqttProtocolConstants.ConnAckServerUnavailable.ShouldBe((byte)0x03);
MqttProtocolConstants.ConnAckBadUserNameOrPassword.ShouldBe((byte)0x04);
MqttProtocolConstants.ConnAckNotAuthorized.ShouldBe((byte)0x05);
MqttProtocolConstants.MaxPayloadSize.ShouldBe(268_435_455);
MqttProtocolConstants.DefaultAckWait.ShouldBe(TimeSpan.FromSeconds(30));
MqttProtocolConstants.MaxAckTotalLimit.ShouldBe(0xFFFF);
}
[Fact]
public void ParseSubscribe_accepts_required_subscribe_flags()
{
var payload = CreateSubscribePayload(packetId: 7, ("sport/tennis/#", 1));
var info = MqttBinaryDecoder.ParseSubscribe(payload, flags: MqttProtocolConstants.SubscribeFlags);
info.PacketId.ShouldBe((ushort)7);
info.Filters.Count.ShouldBe(1);
info.Filters[0].TopicFilter.ShouldBe("sport/tennis/#");
info.Filters[0].QoS.ShouldBe((byte)1);
}
[Fact]
public void ParseSubscribe_rejects_invalid_subscribe_flags()
{
var payload = CreateSubscribePayload(packetId: 5, ("topic/one", 0));
var ex = Should.Throw<FormatException>(() => MqttBinaryDecoder.ParseSubscribe(payload, flags: 0x00));
ex.Message.ShouldContain("invalid fixed-header flags");
}
private static byte[] CreateSubscribePayload(ushort packetId, params (string Topic, byte Qos)[] filters)
{
using var ms = new MemoryStream();
using var writer = new BinaryWriter(ms);
WriteUInt16BigEndian(writer, packetId);
foreach (var (topic, qos) in filters)
{
WriteString(writer, topic);
writer.Write(qos);
}
return ms.ToArray();
}
private static void WriteString(BinaryWriter writer, string value)
{
var bytes = Encoding.UTF8.GetBytes(value);
WriteUInt16BigEndian(writer, (ushort)bytes.Length);
writer.Write(bytes);
}
private static void WriteUInt16BigEndian(BinaryWriter writer, ushort value)
{
writer.Write((byte)(value >> 8));
writer.Write((byte)(value & 0xFF));
}
}

View File

@@ -0,0 +1,91 @@
using System.Text;
using NATS.Server.Mqtt;
namespace NATS.Server.Tests.Mqtt;
public class MqttProtocolConstantsParityBatch2Tests
{
[Fact]
public void Extended_constants_match_go_reference_values()
{
MqttProtocolConstants.MultiLevelSidSuffix.ShouldBe(" fwc");
MqttProtocolConstants.Prefix.ShouldBe("$MQTT.");
MqttProtocolConstants.SubPrefix.ShouldBe("$MQTT.sub.");
MqttProtocolConstants.StreamName.ShouldBe("$MQTT_msgs");
MqttProtocolConstants.StreamSubjectPrefix.ShouldBe("$MQTT.msgs.");
MqttProtocolConstants.RetainedMsgsStreamName.ShouldBe("$MQTT_rmsgs");
MqttProtocolConstants.RetainedMsgsStreamSubject.ShouldBe("$MQTT.rmsgs.");
MqttProtocolConstants.SessStreamName.ShouldBe("$MQTT_sess");
MqttProtocolConstants.SessStreamSubjectPrefix.ShouldBe("$MQTT.sess.");
MqttProtocolConstants.SessionsStreamNamePrefix.ShouldBe("$MQTT_sess_");
MqttProtocolConstants.QoS2IncomingMsgsStreamName.ShouldBe("$MQTT_qos2in");
MqttProtocolConstants.QoS2IncomingMsgsStreamSubjectPrefix.ShouldBe("$MQTT.qos2.in.");
MqttProtocolConstants.OutStreamName.ShouldBe("$MQTT_out");
MqttProtocolConstants.OutSubjectPrefix.ShouldBe("$MQTT.out.");
MqttProtocolConstants.PubRelSubjectPrefix.ShouldBe("$MQTT.out.pubrel.");
MqttProtocolConstants.PubRelDeliverySubjectPrefix.ShouldBe("$MQTT.deliver.pubrel.");
MqttProtocolConstants.PubRelConsumerDurablePrefix.ShouldBe("$MQTT_PUBREL_");
MqttProtocolConstants.JSARepliesPrefix.ShouldBe("$MQTT.JSA.");
MqttProtocolConstants.JSAIdTokenPos.ShouldBe(3);
MqttProtocolConstants.JSATokenPos.ShouldBe(4);
MqttProtocolConstants.JSAClientIDPos.ShouldBe(5);
MqttProtocolConstants.JSAStreamCreate.ShouldBe("SC");
MqttProtocolConstants.JSAStreamUpdate.ShouldBe("SU");
MqttProtocolConstants.JSAStreamLookup.ShouldBe("SL");
MqttProtocolConstants.JSAStreamDel.ShouldBe("SD");
MqttProtocolConstants.JSAConsumerCreate.ShouldBe("CC");
MqttProtocolConstants.JSAConsumerLookup.ShouldBe("CL");
MqttProtocolConstants.JSAConsumerDel.ShouldBe("CD");
MqttProtocolConstants.JSAMsgStore.ShouldBe("MS");
MqttProtocolConstants.JSAMsgLoad.ShouldBe("ML");
MqttProtocolConstants.JSAMsgDelete.ShouldBe("MD");
MqttProtocolConstants.JSASessPersist.ShouldBe("SP");
MqttProtocolConstants.JSARetainedMsgDel.ShouldBe("RD");
MqttProtocolConstants.JSAStreamNames.ShouldBe("SN");
MqttProtocolConstants.SparkbNBirth.ShouldBe("NBIRTH");
MqttProtocolConstants.SparkbDBirth.ShouldBe("DBIRTH");
MqttProtocolConstants.SparkbNDeath.ShouldBe("NDEATH");
MqttProtocolConstants.SparkbDDeath.ShouldBe("DDEATH");
Encoding.ASCII.GetString(MqttProtocolConstants.SparkbNamespaceTopicPrefix).ShouldBe("spBv1.0/");
Encoding.ASCII.GetString(MqttProtocolConstants.SparkbCertificatesTopicPrefix).ShouldBe("$sparkplug/certificates/");
MqttProtocolConstants.NatsHeaderPublish.ShouldBe("Nmqtt-Pub");
MqttProtocolConstants.NatsRetainedMessageTopic.ShouldBe("Nmqtt-RTopic");
MqttProtocolConstants.NatsRetainedMessageOrigin.ShouldBe("Nmqtt-ROrigin");
MqttProtocolConstants.NatsRetainedMessageFlags.ShouldBe("Nmqtt-RFlags");
MqttProtocolConstants.NatsRetainedMessageSource.ShouldBe("Nmqtt-RSource");
MqttProtocolConstants.NatsPubRelHeader.ShouldBe("Nmqtt-PubRel");
MqttProtocolConstants.NatsHeaderSubject.ShouldBe("Nmqtt-Subject");
MqttProtocolConstants.NatsHeaderMapped.ShouldBe("Nmqtt-Mapped");
}
[Fact]
public void WriteString_writes_length_prefixed_utf8()
{
var encoded = MqttPacketWriter.WriteString("MQTT");
encoded.Length.ShouldBe(6);
encoded[0].ShouldBe((byte)0x00);
encoded[1].ShouldBe((byte)0x04);
Encoding.UTF8.GetString(encoded.AsSpan(2)).ShouldBe("MQTT");
}
[Fact]
public void WriteBytes_writes_length_prefixed_binary_payload()
{
var encoded = MqttPacketWriter.WriteBytes(new byte[] { 0xAA, 0xBB, 0xCC });
encoded.ShouldBe(new byte[] { 0x00, 0x03, 0xAA, 0xBB, 0xCC });
}
[Fact]
public void WriteBytes_rejects_payload_larger_than_uint16()
{
var payload = new byte[ushort.MaxValue + 1];
Should.Throw<ArgumentOutOfRangeException>(() => MqttPacketWriter.WriteBytes(payload));
}
}