refactor: extract NATS.Server.Mqtt.Tests project
Move 29 MQTT test files from NATS.Server.Tests into a dedicated NATS.Server.Mqtt.Tests project. Update namespaces, add InternalsVisibleTo, and replace Task.Delay calls with PollHelper.WaitUntilAsync for proper synchronization.
This commit is contained in:
384
tests/NATS.Server.Mqtt.Tests/Mqtt/MqttTopicMappingParityTests.cs
Normal file
384
tests/NATS.Server.Mqtt.Tests/Mqtt/MqttTopicMappingParityTests.cs
Normal file
@@ -0,0 +1,384 @@
|
||||
// Ports MQTT topic/subject conversion behavior from Go reference:
|
||||
// golang/nats-server/server/mqtt_test.go — TestMQTTTopicAndSubjectConversion,
|
||||
// TestMQTTFilterConversion, TestMQTTTopicWithDot, TestMQTTSubjectWildcardStart
|
||||
// golang/nats-server/server/mqtt.go — mqttTopicToNATSPubSubject, mqttFilterToNATSSubject,
|
||||
// natsSubjectToMQTTTopic, mqttToNATSSubjectConversion
|
||||
|
||||
namespace NATS.Server.Mqtt.Tests.Mqtt;
|
||||
|
||||
/// <summary>
|
||||
/// Tests MQTT topic to NATS subject conversion and vice versa, porting the
|
||||
/// Go TestMQTTTopicAndSubjectConversion and TestMQTTFilterConversion tests.
|
||||
/// These are pure-logic conversion tests -- no server needed.
|
||||
/// </summary>
|
||||
public class MqttTopicMappingParityTests
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// Helper: MQTT topic -> NATS subject conversion
|
||||
// Mirrors Go: mqttTopicToNATSPubSubject / mqttToNATSSubjectConversion(mt, false)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static string MqttTopicToNatsSubject(string mqttTopic)
|
||||
{
|
||||
var mt = mqttTopic.AsSpan();
|
||||
var res = new List<char>(mt.Length + 10);
|
||||
|
||||
var end = mt.Length - 1;
|
||||
for (var i = 0; i < mt.Length; i++)
|
||||
{
|
||||
switch (mt[i])
|
||||
{
|
||||
case '/':
|
||||
if (i == 0 || (res.Count > 0 && res[^1] == '.'))
|
||||
{
|
||||
res.Add('/');
|
||||
res.Add('.');
|
||||
}
|
||||
else if (i == end || mt[i + 1] == '/')
|
||||
{
|
||||
res.Add('.');
|
||||
res.Add('/');
|
||||
}
|
||||
else
|
||||
{
|
||||
res.Add('.');
|
||||
}
|
||||
|
||||
break;
|
||||
case ' ':
|
||||
throw new FormatException("spaces not supported in MQTT topic");
|
||||
case '.':
|
||||
res.Add('/');
|
||||
res.Add('/');
|
||||
break;
|
||||
case '+':
|
||||
case '#':
|
||||
throw new FormatException("wildcards not allowed in publish topic");
|
||||
default:
|
||||
res.Add(mt[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (res.Count > 0 && res[^1] == '.')
|
||||
{
|
||||
res.Add('/');
|
||||
}
|
||||
|
||||
return new string(res.ToArray());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helper: MQTT filter -> NATS subject conversion (wildcards allowed)
|
||||
// Mirrors Go: mqttFilterToNATSSubject / mqttToNATSSubjectConversion(filter, true)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static string MqttFilterToNatsSubject(string mqttFilter)
|
||||
{
|
||||
var mt = mqttFilter.AsSpan();
|
||||
var res = new List<char>(mt.Length + 10);
|
||||
|
||||
var end = mt.Length - 1;
|
||||
for (var i = 0; i < mt.Length; i++)
|
||||
{
|
||||
switch (mt[i])
|
||||
{
|
||||
case '/':
|
||||
if (i == 0 || (res.Count > 0 && res[^1] == '.'))
|
||||
{
|
||||
res.Add('/');
|
||||
res.Add('.');
|
||||
}
|
||||
else if (i == end || mt[i + 1] == '/')
|
||||
{
|
||||
res.Add('.');
|
||||
res.Add('/');
|
||||
}
|
||||
else
|
||||
{
|
||||
res.Add('.');
|
||||
}
|
||||
|
||||
break;
|
||||
case ' ':
|
||||
throw new FormatException("spaces not supported in MQTT topic");
|
||||
case '.':
|
||||
res.Add('/');
|
||||
res.Add('/');
|
||||
break;
|
||||
case '+':
|
||||
res.Add('*');
|
||||
break;
|
||||
case '#':
|
||||
res.Add('>');
|
||||
break;
|
||||
default:
|
||||
res.Add(mt[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (res.Count > 0 && res[^1] == '.')
|
||||
{
|
||||
res.Add('/');
|
||||
}
|
||||
|
||||
return new string(res.ToArray());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helper: NATS subject -> MQTT topic conversion
|
||||
// Mirrors Go: natsSubjectToMQTTTopic
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static string NatsSubjectToMqttTopic(string natsSubject)
|
||||
{
|
||||
var subject = natsSubject.AsSpan();
|
||||
var topic = new char[subject.Length];
|
||||
var end = subject.Length - 1;
|
||||
var j = 0;
|
||||
for (var i = 0; i < subject.Length; i++)
|
||||
{
|
||||
switch (subject[i])
|
||||
{
|
||||
case '/':
|
||||
if (i < end)
|
||||
{
|
||||
var c = subject[i + 1];
|
||||
if (c == '.' || c == '/')
|
||||
{
|
||||
topic[j] = c == '.' ? '/' : '.';
|
||||
j++;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case '.':
|
||||
topic[j] = '/';
|
||||
j++;
|
||||
break;
|
||||
default:
|
||||
topic[j] = subject[i];
|
||||
j++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new string(topic, 0, j);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Go: TestMQTTTopicAndSubjectConversion server/mqtt_test.go:1779
|
||||
// =========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData("/", "/./")]
|
||||
[InlineData("//", "/././")]
|
||||
[InlineData("///", "/./././")]
|
||||
[InlineData("////", "/././././")]
|
||||
[InlineData("foo", "foo")]
|
||||
[InlineData("/foo", "/.foo")]
|
||||
[InlineData("//foo", "/./.foo")]
|
||||
[InlineData("///foo", "/././.foo")]
|
||||
[InlineData("///foo/", "/././.foo./")]
|
||||
[InlineData("///foo//", "/././.foo././")]
|
||||
[InlineData("///foo///", "/././.foo./././")]
|
||||
[InlineData("//.foo.//", "/././/foo//././")]
|
||||
[InlineData("foo/bar", "foo.bar")]
|
||||
[InlineData("/foo/bar", "/.foo.bar")]
|
||||
[InlineData("/foo/bar/", "/.foo.bar./")]
|
||||
[InlineData("foo/bar/baz", "foo.bar.baz")]
|
||||
[InlineData("/foo/bar/baz", "/.foo.bar.baz")]
|
||||
[InlineData("/foo/bar/baz/", "/.foo.bar.baz./")]
|
||||
[InlineData("bar/", "bar./")]
|
||||
[InlineData("bar//", "bar././")]
|
||||
[InlineData("bar///", "bar./././")]
|
||||
[InlineData("foo//bar", "foo./.bar")]
|
||||
[InlineData("foo///bar", "foo././.bar")]
|
||||
[InlineData("foo////bar", "foo./././.bar")]
|
||||
[InlineData(".", "//")]
|
||||
[InlineData("..", "////")]
|
||||
[InlineData("...", "//////")]
|
||||
[InlineData("./", "//./")]
|
||||
[InlineData(".//.", "//././/")]
|
||||
[InlineData("././.", "//.//.//")]
|
||||
[InlineData("././/.", "//.//././/")]
|
||||
[InlineData(".foo", "//foo")]
|
||||
[InlineData("foo.", "foo//")]
|
||||
[InlineData(".foo.", "//foo//")]
|
||||
[InlineData("foo../bar/", "foo////.bar./")]
|
||||
[InlineData("foo../bar/.", "foo////.bar.//")]
|
||||
[InlineData("/foo/", "/.foo./")]
|
||||
[InlineData("./foo/.", "//.foo.//")]
|
||||
[InlineData("foo.bar/baz", "foo//bar.baz")]
|
||||
public void Topic_to_nats_subject_converts_correctly(string mqttTopic, string expectedNatsSubject)
|
||||
{
|
||||
// Go: mqttTopicToNATSPubSubject server/mqtt_test.go:1779
|
||||
var natsSubject = MqttTopicToNatsSubject(mqttTopic);
|
||||
natsSubject.ShouldBe(expectedNatsSubject);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/", "/./")]
|
||||
[InlineData("//", "/././")]
|
||||
[InlineData("foo", "foo")]
|
||||
[InlineData("foo/bar", "foo.bar")]
|
||||
[InlineData("/foo/bar", "/.foo.bar")]
|
||||
[InlineData(".", "//")]
|
||||
[InlineData(".foo", "//foo")]
|
||||
[InlineData("foo.", "foo//")]
|
||||
[InlineData("foo.bar/baz", "foo//bar.baz")]
|
||||
[InlineData("foo//bar", "foo./.bar")]
|
||||
[InlineData("/foo/", "/.foo./")]
|
||||
public void Topic_round_trips_through_nats_subject_and_back(string mqttTopic, string natsSubject)
|
||||
{
|
||||
// Go: TestMQTTTopicAndSubjectConversion verifies round-trip server/mqtt_test.go:1843
|
||||
var converted = MqttTopicToNatsSubject(mqttTopic);
|
||||
converted.ShouldBe(natsSubject);
|
||||
|
||||
var backToMqtt = NatsSubjectToMqttTopic(converted);
|
||||
backToMqtt.ShouldBe(mqttTopic);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("foo/+", "wildcards not allowed")]
|
||||
[InlineData("foo/#", "wildcards not allowed")]
|
||||
[InlineData("foo bar", "not supported")]
|
||||
public void Topic_to_nats_subject_rejects_invalid_topics(string mqttTopic, string expectedErrorSubstring)
|
||||
{
|
||||
// Go: TestMQTTTopicAndSubjectConversion error cases server/mqtt_test.go:1826
|
||||
var ex = Should.Throw<FormatException>(() => MqttTopicToNatsSubject(mqttTopic));
|
||||
ex.Message.ShouldContain(expectedErrorSubstring, Case.Insensitive);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Go: TestMQTTFilterConversion server/mqtt_test.go:1852
|
||||
// =========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData("+", "*")]
|
||||
[InlineData("/+", "/.*")]
|
||||
[InlineData("+/", "*./")]
|
||||
[InlineData("/+/", "/.*./")]
|
||||
[InlineData("foo/+", "foo.*")]
|
||||
[InlineData("foo/+/", "foo.*./")]
|
||||
[InlineData("foo/+/bar", "foo.*.bar")]
|
||||
[InlineData("foo/+/+", "foo.*.*")]
|
||||
[InlineData("foo/+/+/", "foo.*.*./")]
|
||||
[InlineData("foo/+/+/bar", "foo.*.*.bar")]
|
||||
[InlineData("foo//+", "foo./.*")]
|
||||
[InlineData("foo//+/", "foo./.*./")]
|
||||
[InlineData("foo//+//", "foo./.*././")]
|
||||
[InlineData("foo//+//bar", "foo./.*./.bar")]
|
||||
[InlineData("foo///+///bar", "foo././.*././.bar")]
|
||||
[InlineData("foo.bar///+///baz", "foo//bar././.*././.baz")]
|
||||
public void Filter_single_level_wildcard_converts_plus_to_star(string mqttFilter, string expectedNatsSubject)
|
||||
{
|
||||
// Go: TestMQTTFilterConversion single level wildcard server/mqtt_test.go:1860
|
||||
var natsSubject = MqttFilterToNatsSubject(mqttFilter);
|
||||
natsSubject.ShouldBe(expectedNatsSubject);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("#", ">")]
|
||||
[InlineData("/#", "/.>")]
|
||||
[InlineData("/foo/#", "/.foo.>")]
|
||||
[InlineData("foo/#", "foo.>")]
|
||||
[InlineData("foo//#", "foo./.>")]
|
||||
[InlineData("foo///#", "foo././.>")]
|
||||
[InlineData("foo/bar/#", "foo.bar.>")]
|
||||
[InlineData("foo/bar.baz/#", "foo.bar//baz.>")]
|
||||
public void Filter_multi_level_wildcard_converts_hash_to_greater_than(string mqttFilter, string expectedNatsSubject)
|
||||
{
|
||||
// Go: TestMQTTFilterConversion multi level wildcard server/mqtt_test.go:1877
|
||||
var natsSubject = MqttFilterToNatsSubject(mqttFilter);
|
||||
natsSubject.ShouldBe(expectedNatsSubject);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Go: TestMQTTTopicWithDot server/mqtt_test.go:7674
|
||||
// =========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData("foo//bar", "foo.bar")]
|
||||
[InlineData("//foo", ".foo")]
|
||||
[InlineData("foo//", "foo.")]
|
||||
[InlineData("//", ".")]
|
||||
public void Nats_subject_with_slash_slash_converts_to_mqtt_dot(string natsSubject, string expectedMqttTopic)
|
||||
{
|
||||
// Go: natsSubjectToMQTTTopic converts '//' back to '.'
|
||||
var mqttTopic = NatsSubjectToMqttTopic(natsSubject);
|
||||
mqttTopic.ShouldBe(expectedMqttTopic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Nats_subject_dot_becomes_mqtt_topic_slash()
|
||||
{
|
||||
// Go: basic '.' -> '/' conversion
|
||||
var result = NatsSubjectToMqttTopic("foo.bar.baz");
|
||||
result.ShouldBe("foo/bar/baz");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Additional conversion edge cases
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void Empty_topic_converts_to_empty_subject()
|
||||
{
|
||||
var result = MqttTopicToNatsSubject(string.Empty);
|
||||
result.ShouldBe(string.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Single_character_topic_converts_identity()
|
||||
{
|
||||
var result = MqttTopicToNatsSubject("a");
|
||||
result.ShouldBe("a");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Nats_subject_to_mqtt_topic_simple_passes_through()
|
||||
{
|
||||
var result = NatsSubjectToMqttTopic("foo");
|
||||
result.ShouldBe("foo");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Filter_conversion_preserves_mixed_wildcards()
|
||||
{
|
||||
var result = MqttFilterToNatsSubject("+/foo/#");
|
||||
result.ShouldBe("*.foo.>");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("+", "*")]
|
||||
[InlineData("+/foo", "*.foo")]
|
||||
[InlineData("+/+", "*.*")]
|
||||
[InlineData("#", ">")]
|
||||
public void Filter_starting_with_wildcard_converts_correctly(string mqttFilter, string expectedNatsSubject)
|
||||
{
|
||||
// Go: TestMQTTSubjectWildcardStart server/mqtt_test.go:7552
|
||||
var result = MqttFilterToNatsSubject(mqttFilter);
|
||||
result.ShouldBe(expectedNatsSubject);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Go: TestMQTTPublishTopicErrors server/mqtt_test.go:4084
|
||||
// =========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData("foo/+")]
|
||||
[InlineData("foo/#")]
|
||||
public void Publish_topic_with_wildcards_throws(string mqttTopic)
|
||||
{
|
||||
Should.Throw<FormatException>(() => MqttTopicToNatsSubject(mqttTopic));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Publish_topic_with_space_throws()
|
||||
{
|
||||
Should.Throw<FormatException>(() => MqttTopicToNatsSubject("foo bar"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user