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:
92
tests/NATS.Server.Tests/Mqtt/MqttModelParityBatch3Tests.cs
Normal file
92
tests/NATS.Server.Tests/Mqtt/MqttModelParityBatch3Tests.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user