From e935d2a915cc1115319fd31fe6f0a7f5f806c3ac Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Mon, 18 Nov 2024 12:43:55 +0100 Subject: [PATCH] E2E Test Coverage Part 1 (#208) --- .github/workflows/release-drafter.yml | 8 +- .../Client/DisconnectOptionsBuilder.cs | 165 +++++++++++++ Source/HiveMQtt/Client/HiveMQClient.cs | 2 +- .../Client/HiveMQClientOptionsBuilder.cs | 37 +++ Source/HiveMQtt/Client/HiveMQClientUtil.cs | 2 +- .../Client/Options/DisconnectOptions.cs | 31 ++- .../Client/Options/HiveMQClientOptions.cs | 1 + .../HiveMQtt/Client/PublishMessageBuilder.cs | 19 ++ .../Client/Transport/WebSocketTransport.cs | 2 +- Source/HiveMQtt/MQTT5/ControlPacket.cs | 16 +- .../HiveMQtt/MQTT5/Packets/ConnectPacket.cs | 2 +- .../MQTT5/Packets/DisconnectPacket.cs | 33 ++- .../HiveMQtt/MQTT5/Packets/PubCompPacket.cs | 2 +- Source/HiveMQtt/MQTT5/Packets/PubRecPacket.cs | 2 +- Source/HiveMQtt/MQTT5/Packets/PubRelPacket.cs | 2 +- .../HiveMQtt/MQTT5/Packets/PublishPacket.cs | 4 +- .../HiveMQClient/Plan/Utf8LimitTest.cs | 220 ++++++++++++++++++ Tests/HiveMQtt.Test/HiveMQtt.Test.csproj | 5 + 18 files changed, 536 insertions(+), 17 deletions(-) create mode 100644 Source/HiveMQtt/Client/DisconnectOptionsBuilder.cs create mode 100644 Tests/HiveMQtt.Test/HiveMQClient/Plan/Utf8LimitTest.cs diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 704e8223..0f570df0 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -2,15 +2,19 @@ name: Release Drafter on: push: - # branches to consider in the event; optional, defaults to all branches: - main +permissions: + contents: read + jobs: update_release_draft: + permissions: + contents: write + pull-requests: write runs-on: ubuntu-latest steps: - # Drafts your next Release notes as Pull Requests are merged into "master" - uses: release-drafter/release-drafter@v6.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Source/HiveMQtt/Client/DisconnectOptionsBuilder.cs b/Source/HiveMQtt/Client/DisconnectOptionsBuilder.cs new file mode 100644 index 00000000..7e9de450 --- /dev/null +++ b/Source/HiveMQtt/Client/DisconnectOptionsBuilder.cs @@ -0,0 +1,165 @@ +/* + * Copyright 2024-present HiveMQ and the HiveMQ Community + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace HiveMQtt.Client; + +using HiveMQtt.Client.Options; +using HiveMQtt.MQTT5.ReasonCodes; + +public class DisconnectOptionsBuilder +{ + private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + + private readonly DisconnectOptions options; + + public DisconnectOptionsBuilder() => this.options = new DisconnectOptions(); + + /// + /// Sets the session expiry interval for the disconnect. + /// + /// The session expiry interval in seconds. + /// The builder instance. + public DisconnectOptionsBuilder WithSessionExpiryInterval(int sessionExpiryInterval) + { + this.options.SessionExpiryInterval = sessionExpiryInterval; + return this; + } + + /// + /// Sets the reason code for the disconnect. + /// + /// The reason code for the disconnect. + /// The builder instance. + public DisconnectOptionsBuilder WithReasonCode(DisconnectReasonCode reasonCode) + { + this.options.ReasonCode = reasonCode; + return this; + } + + /// + /// Sets the reason string for the disconnect. + /// + /// The reason string for the disconnect. + /// The builder instance. + /// Thrown if the reason string is not between 1 and 65535 characters. + /// Thrown if the reason string is null. + public DisconnectOptionsBuilder WithReasonString(string reasonString) + { + if (reasonString is null) + { + Logger.Error("Reason string cannot be null."); + throw new ArgumentNullException(nameof(reasonString)); + } + + if (reasonString.Length is < 1 or > 65535) + { + Logger.Error("Reason string must be between 1 and 65535 characters."); + throw new ArgumentException("Reason string must be between 1 and 65535 characters."); + } + + this.options.ReasonString = reasonString; + return this; + } + + /// + /// Adds a user property to the disconnect. + /// + /// The key for the user property. + /// The value for the user property. + /// The builder instance. + /// Thrown if the key or value is not between 1 and 65535 characters. + /// Thrown if the key or value is null. + public DisconnectOptionsBuilder WithUserProperty(string key, string value) + { + if (key is null) + { + Logger.Error("User property key cannot be null."); + throw new ArgumentNullException(nameof(key)); + } + + if (value is null) + { + Logger.Error("User property value cannot be null."); + throw new ArgumentNullException(nameof(value)); + } + + if (key.Length is < 1 or > 65535) + { + Logger.Error("User property key must be between 1 and 65535 characters."); + throw new ArgumentException("User property key must be between 1 and 65535 characters."); + } + + if (value.Length is < 1 or > 65535) + { + Logger.Error("User property value must be between 1 and 65535 characters."); + throw new ArgumentException("User property value must be between 1 and 65535 characters."); + } + + this.options.UserProperties.Add(key, value); + return this; + } + + /// + /// Adds a dictionary of user properties to the disconnect. + /// + /// The dictionary of user properties to add. + /// The builder instance. + /// Thrown if a key or value is not between 1 and 65535 characters. + /// Thrown if the key or value is null. + public DisconnectOptionsBuilder WithUserProperties(Dictionary properties) + { + foreach (var property in properties) + { + if (property.Key is null) + { + Logger.Error("User property key cannot be null."); + throw new ArgumentNullException(nameof(properties)); + } + + if (property.Value is null) + { + Logger.Error("User property value cannot be null."); + throw new ArgumentNullException(nameof(properties)); + } + + if (property.Key.Length is < 1 or > 65535) + { + Logger.Error("User property key must be between 1 and 65535 characters."); + throw new ArgumentException("User property key must be between 1 and 65535 characters."); + } + + if (property.Value.Length is < 1 or > 65535) + { + Logger.Error("User property value must be between 1 and 65535 characters."); + throw new ArgumentException("User property value must be between 1 and 65535 characters."); + } + + this.options.UserProperties.Add(property.Key, property.Value); + } + + return this; + } + + /// + /// Builds the SubscribeOptions based on the previous calls. This + /// step will also run validation on the final SubscribeOptions. + /// + /// The constructed SubscribeOptions instance. + public DisconnectOptions Build() + { + this.options.Validate(); + return this.options; + } +} diff --git a/Source/HiveMQtt/Client/HiveMQClient.cs b/Source/HiveMQtt/Client/HiveMQClient.cs index 8a2d4c64..305605d3 100644 --- a/Source/HiveMQtt/Client/HiveMQClient.cs +++ b/Source/HiveMQtt/Client/HiveMQClient.cs @@ -153,7 +153,7 @@ public async Task DisconnectAsync(DisconnectOptions? options = null) // Fire the corresponding event this.BeforeDisconnectEventLauncher(); - var disconnectPacket = new DisconnectPacket + var disconnectPacket = new DisconnectPacket(options) { DisconnectReasonCode = options.ReasonCode, }; diff --git a/Source/HiveMQtt/Client/HiveMQClientOptionsBuilder.cs b/Source/HiveMQtt/Client/HiveMQClientOptionsBuilder.cs index 7c9db906..ad7e514a 100644 --- a/Source/HiveMQtt/Client/HiveMQClientOptionsBuilder.cs +++ b/Source/HiveMQtt/Client/HiveMQClientOptionsBuilder.cs @@ -125,6 +125,12 @@ public HiveMQClientOptionsBuilder WithPort(int port) /// The HiveMQClientOptionsBuilder instance. public HiveMQClientOptionsBuilder WithClientId(string clientId) { + if (clientId.Length is < 0 or > 65535) + { + Logger.Error("Client Id must be between 0 and 65535 characters."); + throw new ArgumentException("Client Id must be between 0 and 65535 characters."); + } + this.options.ClientId = clientId; return this; } @@ -275,8 +281,15 @@ public HiveMQClientOptionsBuilder WithKeepAlive(int keepAlive) /// /// The authentication method. /// The HiveMQClientOptionsBuilder instance. + /// Thrown when the authentication method is not between 1 and 65535 characters. public HiveMQClientOptionsBuilder WithAuthenticationMethod(string method) { + if (method.Length is < 1 or > 65535) + { + Logger.Error("Authentication method must be between 1 and 65535 characters."); + throw new ArgumentException("Authentication method must be between 1 and 65535 characters."); + } + this.options.AuthenticationMethod = method; return this; } @@ -309,6 +322,18 @@ public HiveMQClientOptionsBuilder WithAuthenticationData(byte[] data) /// The HiveMQClientOptionsBuilder instance. public HiveMQClientOptionsBuilder WithUserProperty(string key, string value) { + if (key.Length is < 1 or > 65535) + { + Logger.Error("User property key must be between 1 and 65535 characters."); + throw new ArgumentException("User property key must be between 1 and 65535 characters."); + } + + if (value.Length is < 1 or > 65535) + { + Logger.Error("User property value must be between 1 and 65535 characters."); + throw new ArgumentException("User property value must be between 1 and 65535 characters."); + } + this.options.UserProperties.Add(key, value); return this; } @@ -432,6 +457,12 @@ public HiveMQClientOptionsBuilder WithSessionExpiryInterval(int sessionExpiryInt /// The HiveMQClientOptionsBuilder instance. public HiveMQClientOptionsBuilder WithUserName(string username) { + if (username.Length is < 0 or > 65535) + { + Logger.Error("Username must be between 0 and 65535 characters."); + throw new ArgumentException("Username must be between 0 and 65535 characters."); + } + this.options.UserName = username; return this; } @@ -462,6 +493,12 @@ public HiveMQClientOptionsBuilder WithUserName(string username) /// The HiveMQClientOptionsBuilder instance. public HiveMQClientOptionsBuilder WithPassword(string password) { + if (password.Length is < 0 or > 65535) + { + Logger.Error("Password must be between 0 and 65535 characters."); + throw new ArgumentException("Password must be between 0 and 65535 characters."); + } + this.options.Password = password; return this; } diff --git a/Source/HiveMQtt/Client/HiveMQClientUtil.cs b/Source/HiveMQtt/Client/HiveMQClientUtil.cs index 63bd166b..4faa0675 100644 --- a/Source/HiveMQtt/Client/HiveMQClientUtil.cs +++ b/Source/HiveMQtt/Client/HiveMQClientUtil.cs @@ -173,7 +173,7 @@ protected virtual void Dispose(bool disposing) Logger.Trace("HiveMQClient Dispose: Disconnecting connected client."); _ = Task.Run(async () => await this.DisconnectAsync().ConfigureAwait(false)); } - } + } // Call the appropriate methods to clean up // unmanaged resources here. diff --git a/Source/HiveMQtt/Client/Options/DisconnectOptions.cs b/Source/HiveMQtt/Client/Options/DisconnectOptions.cs index 62add6d7..19db92cf 100644 --- a/Source/HiveMQtt/Client/Options/DisconnectOptions.cs +++ b/Source/HiveMQtt/Client/Options/DisconnectOptions.cs @@ -15,6 +15,7 @@ */ namespace HiveMQtt.Client.Options; +using HiveMQtt.Client.Exceptions; using HiveMQtt.MQTT5.ReasonCodes; /// @@ -41,7 +42,7 @@ public DisconnectOptions() /// to the indicated value. The value represents the session expiration time /// in seconds. /// - public int? SessionExpiry { get; set; } + public int? SessionExpiryInterval { get; set; } /// /// Gets or sets the reason string for the disconnection. This is a human readable @@ -53,4 +54,32 @@ public DisconnectOptions() /// Gets or sets the user properties for the disconnection. /// public Dictionary UserProperties { get; set; } + + /// + /// Validate that the options in this instance are valid. + /// + /// The exception raised if some value is out of range or invalid. + public void Validate() + { + // Validate SessionExpiry is non-negative if provided + if (this.SessionExpiryInterval.HasValue && this.SessionExpiryInterval < 0) + { + throw new HiveMQttClientException("Session expiry must be a non-negative value."); + } + + // Validate ReasonString length (assuming max length of 65535 characters) + if (this.ReasonString != null && this.ReasonString.Length > 65535) + { + throw new HiveMQttClientException("Reason string must not exceed 65535 characters."); + } + + // Validate UserProperties for null keys or values + foreach (var kvp in this.UserProperties) + { + if (kvp.Key == null || kvp.Value == null) + { + throw new HiveMQttClientException("User properties must not have null keys or values."); + } + } + } } diff --git a/Source/HiveMQtt/Client/Options/HiveMQClientOptions.cs b/Source/HiveMQtt/Client/Options/HiveMQClientOptions.cs index e46f825b..ea525374 100644 --- a/Source/HiveMQtt/Client/Options/HiveMQClientOptions.cs +++ b/Source/HiveMQtt/Client/Options/HiveMQClientOptions.cs @@ -63,6 +63,7 @@ public HiveMQClientOptions() /// Examples: /// ws://localhost:8000/mqtt /// wss://localhost:8884/mqtt + /// . /// /// public string WebSocketServer { get; set; } diff --git a/Source/HiveMQtt/Client/PublishMessageBuilder.cs b/Source/HiveMQtt/Client/PublishMessageBuilder.cs index 979bcd85..3e0a5a80 100644 --- a/Source/HiveMQtt/Client/PublishMessageBuilder.cs +++ b/Source/HiveMQtt/Client/PublishMessageBuilder.cs @@ -62,8 +62,27 @@ public PublishMessageBuilder WithPayload(string payload) /// /// The topic. /// The builder instance. + /// Thrown when the topic is null or empty. + /// Thrown when the topic length is greater than 65535 characters. + /// Thrown when the topic contains wildcard characters. public PublishMessageBuilder WithTopic(string topic) { + if (string.IsNullOrEmpty(topic)) + { + throw new ArgumentException("Topic must not be null or empty."); + } + + if (topic.Length is < 1 or > 65535) + { + throw new ArgumentException("Topic must be between 1 and 65535 characters."); + } + + // The topic string should not contain any wildcard characters. + if (topic.Contains('+') || topic.Contains('#')) + { + throw new ArgumentException("Topic must not contain wildcard characters. Use TopicFilter instead."); + } + this.message.Topic = topic; return this; } diff --git a/Source/HiveMQtt/Client/Transport/WebSocketTransport.cs b/Source/HiveMQtt/Client/Transport/WebSocketTransport.cs index ceb77dcb..dba208be 100644 --- a/Source/HiveMQtt/Client/Transport/WebSocketTransport.cs +++ b/Source/HiveMQtt/Client/Transport/WebSocketTransport.cs @@ -123,8 +123,8 @@ public override async Task ReadAsync(CancellationToken canc Logger.Trace($"Received {ms.Length} bytes"); + // Development // ms.Seek(0, SeekOrigin.Begin); - if (result.MessageType == WebSocketMessageType.Binary) { // Prepare the result and return diff --git a/Source/HiveMQtt/MQTT5/ControlPacket.cs b/Source/HiveMQtt/MQTT5/ControlPacket.cs index b2039c86..6b1cd1d4 100644 --- a/Source/HiveMQtt/MQTT5/ControlPacket.cs +++ b/Source/HiveMQtt/MQTT5/ControlPacket.cs @@ -154,15 +154,27 @@ protected static int EncodeUTF8String(MemoryStream stream, string s) { if (reader.TryReadBigEndian(out short stringLength)) { - var array = new byte[stringLength]; + var length = (ushort)stringLength; + + // Ensure the string length is non-negative and within a reasonable range + if (length < 0 || length > reader.Remaining) + { + throw new MQTTProtocolException("DecodeUTF8String: Invalid UTF-8 string length"); + } + + var array = new byte[length]; var span = new Span(array); - for (var i = 0; i < stringLength; i++) + for (var i = 0; i < length; i++) { if (reader.TryRead(out var outValue)) { span[i] = outValue; } + else + { + throw new MQTTProtocolException("DecodeUTF8String: Unexpected end of data"); + } } return Encoding.UTF8.GetString(span.ToArray()); diff --git a/Source/HiveMQtt/MQTT5/Packets/ConnectPacket.cs b/Source/HiveMQtt/MQTT5/Packets/ConnectPacket.cs index 893b3084..da4bfcaa 100644 --- a/Source/HiveMQtt/MQTT5/Packets/ConnectPacket.cs +++ b/Source/HiveMQtt/MQTT5/Packets/ConnectPacket.cs @@ -121,7 +121,7 @@ internal void GatherConnectFlagsAndProperties() this.clientOptions.Validate(); this.flags = 0x0; - if (this.clientOptions.CleanStart is true) + if (this.clientOptions.CleanStart) { this.flags |= 0x2; } diff --git a/Source/HiveMQtt/MQTT5/Packets/DisconnectPacket.cs b/Source/HiveMQtt/MQTT5/Packets/DisconnectPacket.cs index 61b2c399..ac9c91fc 100644 --- a/Source/HiveMQtt/MQTT5/Packets/DisconnectPacket.cs +++ b/Source/HiveMQtt/MQTT5/Packets/DisconnectPacket.cs @@ -17,6 +17,7 @@ namespace HiveMQtt.MQTT5.Packets; using System.Buffers; using System.IO; +using HiveMQtt.Client.Options; using HiveMQtt.MQTT5.ReasonCodes; /// @@ -25,8 +26,28 @@ namespace HiveMQtt.MQTT5.Packets; /// public class DisconnectPacket : ControlPacket { - public DisconnectPacket() + public DisconnectPacket(DisconnectOptions options) { + this.DisconnectReasonCode = options.ReasonCode; + + if (options.ReasonString != null) + { + if (options.ReasonString.Length is < 1 or > 65535) + { + throw new ArgumentException("Reason string must be between 1 and 65535 characters."); + } + } + + this.Properties.ReasonString = options.ReasonString; + if (options.SessionExpiryInterval.HasValue) + { + this.Properties.SessionExpiryInterval = (uint)options.SessionExpiryInterval; + } + + if (options.UserProperties != null) + { + this.Properties.UserProperties = options.UserProperties; + } } public DisconnectPacket(ReadOnlySequence packetData) => this.Decode(packetData); @@ -41,18 +62,24 @@ public DisconnectPacket() /// An array of bytes ready to be sent. public byte[] Encode() { - using (var stream = new MemoryStream(8)) + using (var stream = new MemoryStream()) { + // Variable Header - starts at byte 2 stream.Position = 2; - // Variable Header - starts at byte 2 + // Disconnect Reason Code stream.WriteByte((byte)this.DisconnectReasonCode); + // Properties + this.EncodeProperties(stream); + // Disconnect has no payload // Fixed Header - Add to the beginning of the stream var remainingLength = stream.Length - 2; + // Go back to the beginning of the stream and write + // the first two bytes. stream.Position = 0; stream.WriteByte((byte)ControlPacketType.Disconnect << 4); EncodeVariableByteInteger(stream, (int)remainingLength); diff --git a/Source/HiveMQtt/MQTT5/Packets/PubCompPacket.cs b/Source/HiveMQtt/MQTT5/Packets/PubCompPacket.cs index 79fb3bde..814c090e 100644 --- a/Source/HiveMQtt/MQTT5/Packets/PubCompPacket.cs +++ b/Source/HiveMQtt/MQTT5/Packets/PubCompPacket.cs @@ -45,7 +45,7 @@ public byte[] Encode() using (var vhStream = new MemoryStream()) { // Variable Header - ControlPacket.EncodeTwoByteInteger(vhStream, this.PacketIdentifier); + EncodeTwoByteInteger(vhStream, this.PacketIdentifier); vhStream.WriteByte((byte)this.ReasonCode); this.EncodeProperties(vhStream); diff --git a/Source/HiveMQtt/MQTT5/Packets/PubRecPacket.cs b/Source/HiveMQtt/MQTT5/Packets/PubRecPacket.cs index 97532b26..777aa5ff 100644 --- a/Source/HiveMQtt/MQTT5/Packets/PubRecPacket.cs +++ b/Source/HiveMQtt/MQTT5/Packets/PubRecPacket.cs @@ -45,7 +45,7 @@ public byte[] Encode() using (var vhStream = new MemoryStream()) { // Variable Header - ControlPacket.EncodeTwoByteInteger(vhStream, this.PacketIdentifier); + EncodeTwoByteInteger(vhStream, this.PacketIdentifier); vhStream.WriteByte((byte)this.ReasonCode); this.EncodeProperties(vhStream); diff --git a/Source/HiveMQtt/MQTT5/Packets/PubRelPacket.cs b/Source/HiveMQtt/MQTT5/Packets/PubRelPacket.cs index dbc052d5..74b170d3 100644 --- a/Source/HiveMQtt/MQTT5/Packets/PubRelPacket.cs +++ b/Source/HiveMQtt/MQTT5/Packets/PubRelPacket.cs @@ -45,7 +45,7 @@ public byte[] Encode() using (var vhStream = new MemoryStream()) { // Variable Header - ControlPacket.EncodeTwoByteInteger(vhStream, this.PacketIdentifier); + EncodeTwoByteInteger(vhStream, this.PacketIdentifier); vhStream.WriteByte((byte)this.ReasonCode); this.EncodeProperties(vhStream); diff --git a/Source/HiveMQtt/MQTT5/Packets/PublishPacket.cs b/Source/HiveMQtt/MQTT5/Packets/PublishPacket.cs index cdeaa4ec..572febca 100644 --- a/Source/HiveMQtt/MQTT5/Packets/PublishPacket.cs +++ b/Source/HiveMQtt/MQTT5/Packets/PublishPacket.cs @@ -293,7 +293,7 @@ public byte[] Encode() var byte1 = (byte)ControlPacketType.Publish << 4; // DUP Flag - if (this.Message.Duplicate is true) + if (this.Message.Duplicate) { byte1 |= 0x8; } @@ -309,7 +309,7 @@ public byte[] Encode() } // Retain Flag - if (this.Message.Retain is true) + if (this.Message.Retain) { byte1 |= 0x1; } diff --git a/Tests/HiveMQtt.Test/HiveMQClient/Plan/Utf8LimitTest.cs b/Tests/HiveMQtt.Test/HiveMQClient/Plan/Utf8LimitTest.cs new file mode 100644 index 00000000..f653c6ea --- /dev/null +++ b/Tests/HiveMQtt.Test/HiveMQClient/Plan/Utf8LimitTest.cs @@ -0,0 +1,220 @@ +namespace HiveMQtt.Test.HiveMQClient.Plan; + +using FluentAssertions; +using HiveMQtt.Client; +using HiveMQtt.Client.ReasonCodes; +using HiveMQtt.MQTT5.Types; +using NUnit.Framework; + +[TestFixture] +public class Utf8LimitTest +{ + [Test] + public void ClientId_Should_Allow_0_To_65535_Characters() + { + var clientId = GenerateUtf8String(65535); + var options = new HiveMQClientOptionsBuilder() + .WithClientId(clientId) + .Build(); + var client = new HiveMQClient(options); + + client.Should().NotBeNull(); + } + + [Test] + public void ClientId_Should_Disallow_Exceeding_65535_Characters() + { + var clientId = GenerateUtf8String(65536); + Action act = () => new HiveMQClientOptionsBuilder().WithClientId(clientId).Build(); + + act.Should().Throw().WithMessage("Client Id must be between 0 and 65535 characters."); + } + + [Test] + public void UserPropertiesKey_And_Value_Should_Allow_0_To_65535_Characters() + { + var key = GenerateUtf8String(65535); + var value = GenerateUtf8String(65535); + var options = new HiveMQClientOptionsBuilder() + .WithUserProperty(key, value) + .Build(); + var client = new HiveMQClient(options); + + client.Should().NotBeNull(); + } + + [Test] + public void LastWillAndTestament_ResponseTopic_Should_Allow_0_To_65535_Characters() + { + var responseTopic = GenerateUtf8String(65535); + var lwt = new LastWillAndTestamentBuilder() + .WithTopic("last/will") + .WithPayload("last will message") + .WithQualityOfServiceLevel(QualityOfService.AtLeastOnceDelivery) + .WithResponseTopic(responseTopic) + .Build(); + + var options = new HiveMQClientOptionsBuilder() + .WithLastWillAndTestament(lwt) + .Build(); + + var client = new HiveMQClient(options); + + client.Should().NotBeNull(); + } + + [Test] + public async Task Publish_ResponseTopic_Should_Allow_0_To_65535_Characters_Async() + { + var responseTopic = GenerateUtf8String(65535); + var publishMessage = new PublishMessageBuilder() + .WithTopic("test/publish") + .WithPayload("test message") + .WithQualityOfService(QualityOfService.AtMostOnceDelivery) + .WithResponseTopic(responseTopic) + .Build(); + + var options = new HiveMQClientOptionsBuilder().Build(); + var client = new HiveMQClient(options); + + client.Should().NotBeNull(); + + var publishResult = await client.PublishAsync(publishMessage).ConfigureAwait(false); + + publishResult.Should().NotBeNull(); + publishResult.ReasonCode().Should().Be((int)QoS1ReasonCode.Success); + } + + [Test] + public void UserName_Should_Allow_0_To_65535_Characters() + { + var userName = GenerateUtf8String(65535); + var options = new HiveMQClientOptionsBuilder() + .WithUserName(userName) + .Build(); + var client = new HiveMQClient(options); + + client.Should().NotBeNull(); + } + + [Test] + public void LastWillAndTestament_ContentType_Should_Allow_0_To_65535_Characters() + { + var contentType = GenerateUtf8String(65535); + var lwt = new LastWillAndTestamentBuilder() + .WithTopic("last/will") + .WithPayload("last will message") + .WithQualityOfServiceLevel(QualityOfService.AtLeastOnceDelivery) + .WithContentType(contentType) + .Build(); + + var options = new HiveMQClientOptionsBuilder() + .WithLastWillAndTestament(lwt) + .Build(); + + var client = new HiveMQClient(options); + + client.Should().NotBeNull(); + } + + [Test] + public async Task Publish_ContentType_Should_Allow_0_To_65535_Characters_Async() + { + var contentType = GenerateUtf8String(65535); + var publishMessage = new PublishMessageBuilder() + .WithTopic("test/publish") + .WithPayload("test message") + .WithQualityOfService(QualityOfService.AtMostOnceDelivery) + .WithContentType(contentType) + .Build(); + + var options = new HiveMQClientOptionsBuilder().Build(); + var client = new HiveMQClient(options); + + client.Should().NotBeNull(); + + var publishResult = await client.PublishAsync(publishMessage).ConfigureAwait(false); + + publishResult.Should().NotBeNull(); + publishResult.ReasonCode().Should().Be((int)QoS1ReasonCode.Success); + } + + [Test] + public async Task Disconnect_ReasonString_Should_Allow_0_To_65535_Characters_Async() + { + var reasonString = GenerateUtf8String(65535); + var disconnectOptions = new DisconnectOptionsBuilder() + .WithReasonString(reasonString) + .Build(); + + var options = new HiveMQClientOptionsBuilder().Build(); + var client = new HiveMQClient(options); + + client.Should().NotBeNull(); + + // Now connect, test, disconnect and test + var connectResult = await client.ConnectAsync().ConfigureAwait(false); + connectResult.Should().NotBeNull(); + + var disconnectResult = await client.DisconnectAsync(disconnectOptions).ConfigureAwait(false); + disconnectResult.Should().BeTrue(); + } + + [Test] + public async Task Connect_Authentication_Method_Should_Allow_0_To_65535_Characters_Async() + { + var authenticationMethod = GenerateUtf8String(65535); + var options = new HiveMQClientOptionsBuilder() + .WithAuthenticationMethod(authenticationMethod) + .Build(); + var client = new HiveMQClient(options); + + client.Should().NotBeNull(); + + var connectResult = await client.ConnectAsync().ConfigureAwait(false); + connectResult.Should().NotBeNull(); + + var disconnectResult = await client.DisconnectAsync().ConfigureAwait(false); + disconnectResult.Should().BeTrue(); + } + + [Test] + public async Task Publish_Topic_Should_Allow_0_To_65535_Characters_Async() + { + var topic = GenerateUtf8String(65535); + var publishMessage = new PublishMessageBuilder() + .WithTopic(topic) + .WithPayload("test message") + .WithQualityOfService(QualityOfService.AtMostOnceDelivery) + .Build(); + + var options = new HiveMQClientOptionsBuilder().Build(); + var client = new HiveMQClient(options); + client.Should().NotBeNull(); + + var publishResult = await client.PublishAsync(publishMessage).ConfigureAwait(false); + + publishResult.Should().NotBeNull(); + publishResult.ReasonCode().Should().Be((int)QoS1ReasonCode.Success); + } + + [Test] + public void Publish_Topic_Should_Not_Contain_Wildcards() + { + var topic = "test/+/topic#"; + + // This should raise an ArgumentException + Action act = () => + { + var publishMessage = new PublishMessageBuilder() + .WithTopic(topic) + .WithPayload("test message") + .WithQualityOfService(QualityOfService.AtMostOnceDelivery) + .Build(); + }; + + act.Should().Throw().WithMessage("Topic must not contain wildcard characters. Use TopicFilter instead."); + } + + private static string GenerateUtf8String(int length) => new string('a', length); +} diff --git a/Tests/HiveMQtt.Test/HiveMQtt.Test.csproj b/Tests/HiveMQtt.Test/HiveMQtt.Test.csproj index 9b0d61ee..1f2318bf 100644 --- a/Tests/HiveMQtt.Test/HiveMQtt.Test.csproj +++ b/Tests/HiveMQtt.Test/HiveMQtt.Test.csproj @@ -28,4 +28,9 @@ PreserveNewest + + + + +