diff --git a/iothub/service/src/Amqp/AmqpCbsSessionHandler.cs b/iothub/service/src/Amqp/AmqpCbsSessionHandler.cs
index 240143d043..6747d051b7 100644
--- a/iothub/service/src/Amqp/AmqpCbsSessionHandler.cs
+++ b/iothub/service/src/Amqp/AmqpCbsSessionHandler.cs
@@ -5,7 +5,6 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Amqp;
-using Microsoft.Azure.Devices.Common;
namespace Microsoft.Azure.Devices.Amqp
{
@@ -13,7 +12,7 @@ namespace Microsoft.Azure.Devices.Amqp
/// Handles a single CBS session (for SAS token renewal) including the inital authentication and scheduling all subsequent
/// authentication attempts.
///
- internal sealed class AmqpCbsSessionHandler : IDisposable
+ internal class AmqpCbsSessionHandler : IDisposable
{
// There is no AmqpSession object to track here because it is encapsulated by the AmqpCbsLink class.
private readonly IotHubConnectionProperties _credential;
@@ -26,6 +25,8 @@ internal sealed class AmqpCbsSessionHandler : IDisposable
private static readonly TimeSpan s_defaultOperationTimeout = TimeSpan.FromMinutes(1);
private readonly IOThreadTimerSlim _refreshTokenTimer;
+ protected AmqpCbsSessionHandler() { }
+
public AmqpCbsSessionHandler(IotHubConnectionProperties credential, EventHandler connectionLossHandler)
{
_credential = credential;
@@ -35,10 +36,11 @@ public AmqpCbsSessionHandler(IotHubConnectionProperties credential, EventHandler
///
/// Opens the session, then opens the CBS links, then sends the initial authentication message.
+ /// Marked virtual for unit testing purposes only.
///
/// The connection to attach this session to.
/// The cancellation token.
- public async Task OpenAsync(AmqpConnection connection, CancellationToken cancellationToken)
+ public virtual async Task OpenAsync(AmqpConnection connection, CancellationToken cancellationToken)
{
if (Logging.IsEnabled)
Logging.Enter(this, $"Opening CBS session.");
@@ -82,7 +84,6 @@ public void Close()
/// Returns true if this session and its CBS link are open. Returns false otherwise.
///
/// True if this session and its CBS link are open. False otherwise.
-
public bool IsOpen()
{
return _cbsLink != null;
diff --git a/iothub/service/src/Amqp/AmqpConnectionHandler.cs b/iothub/service/src/Amqp/AmqpConnectionHandler.cs
index 9cda7b5b78..e40824e052 100644
--- a/iothub/service/src/Amqp/AmqpConnectionHandler.cs
+++ b/iothub/service/src/Amqp/AmqpConnectionHandler.cs
@@ -21,7 +21,7 @@ namespace Microsoft.Azure.Devices.Amqp
///
/// This class intentionally abstracts away details about sessions and links for simplicity at the service client level.
///
- internal sealed class AmqpConnectionHandler : IDisposable
+ internal class AmqpConnectionHandler : IDisposable
{
private static readonly AmqpVersion s_amqpVersion_1_0_0 = new(1, 0, 0);
@@ -41,6 +41,35 @@ internal sealed class AmqpConnectionHandler : IDisposable
// The current delivery tag. Increments after each send operation to give a unique value.
private int _sendingDeliveryTag;
+
+ ///
+ /// Creates an instance of this class. Provided for unit testing purposes only.
+ ///
+ protected internal AmqpConnectionHandler()
+ { }
+
+ ///
+ /// Creates an instance of this class. Provided for unit testing purposes only.
+ ///
+ internal AmqpConnectionHandler(
+ IotHubConnectionProperties credential,
+ IotHubTransportProtocol protocol,
+ string linkAddress,
+ IotHubServiceClientOptions options,
+ EventHandler connectionLossHandler,
+ AmqpCbsSessionHandler cbsSession,
+ AmqpSessionHandler workerSession)
+ {
+ _credential = credential;
+ _useWebSocketOnly = protocol == IotHubTransportProtocol.WebSocket;
+ _linkAddress = linkAddress;
+ _options = options;
+ _connectionLossHandler = connectionLossHandler;
+ _cbsSession = cbsSession;
+ _workerSession = workerSession;
+
+ _sendingDeliveryTag = 0;
+ }
internal AmqpConnectionHandler(
IotHubConnectionProperties credential,
@@ -64,9 +93,10 @@ internal AmqpConnectionHandler(
///
/// Returns true if this connection, its sessions and its sessions' links are all open.
/// Returns false otherwise.
+ /// Marked virtual for unit testing purposes only.
///
/// True if this connection, its sessions and its sessions' links are all open. False otherwise.
- internal bool IsOpen => _connection != null
+ internal virtual bool IsOpen => _connection != null
&& _connection.State == AmqpObjectState.Opened
&& _cbsSession != null
&& _cbsSession.IsOpen()
@@ -76,9 +106,10 @@ internal AmqpConnectionHandler(
///
/// Opens the AMQP connection. This involves creating the needed TCP or Websocket transport and
/// then opening all the required sessions and links.
+ /// Marked virtual for unit testing purposes only.
///
/// The cancellation token.
- internal async Task OpenAsync(CancellationToken cancellationToken)
+ internal virtual async Task OpenAsync(CancellationToken cancellationToken)
{
if (Logging.IsEnabled)
Logging.Enter(this, "Opening amqp connection.", nameof(OpenAsync));
@@ -185,9 +216,10 @@ internal async Task OpenAsync(CancellationToken cancellationToken)
///
/// Closes the AMQP connection. This closes all the open links and sessions prior to closing the connection.
+ /// Marked virtual for unit testing purposes only.
///
/// The cancellation token.
- internal async Task CloseAsync(CancellationToken cancellationToken)
+ internal virtual async Task CloseAsync(CancellationToken cancellationToken)
{
if (Logging.IsEnabled)
Logging.Enter(this, "Closing amqp connection.", nameof(CloseAsync));
@@ -233,7 +265,7 @@ internal async Task CloseAsync(CancellationToken cancellationToken)
///
/// The message to send.
/// The cancellation token.
- internal async Task SendAsync(AmqpMessage message, CancellationToken cancellationToken)
+ internal virtual async Task SendAsync(AmqpMessage message, CancellationToken cancellationToken)
{
ArraySegment deliveryTag = GetNextDeliveryTag();
return await _workerSession.SendAsync(message, deliveryTag, cancellationToken).ConfigureAwait(false);
diff --git a/iothub/service/src/Amqp/AmqpSessionHandler.cs b/iothub/service/src/Amqp/AmqpSessionHandler.cs
index 765b99cc93..a402700672 100644
--- a/iothub/service/src/Amqp/AmqpSessionHandler.cs
+++ b/iothub/service/src/Amqp/AmqpSessionHandler.cs
@@ -13,7 +13,7 @@ namespace Microsoft.Azure.Devices.Amqp
/// Handles a single AMQP session that holds the sender or receiver link that does the "work"
/// for the AMQP connection (receiving file upload notification, sending cloud to device messages, etc).
///
- internal sealed class AmqpSessionHandler
+ internal class AmqpSessionHandler
{
private readonly AmqpSendingLinkHandler _sendingLinkHandler;
private readonly AmqpReceivingLinkHandler _receivingLinkHandler;
@@ -22,6 +22,12 @@ internal sealed class AmqpSessionHandler
private AmqpSession _session;
+ ///
+ /// Creates an instance of this class. Provided for unit testing purposes only.
+ ///
+ protected internal AmqpSessionHandler()
+ { }
+
///
/// Construct an AMQP session for handling sending cloud to device messaging, receiving file
/// upload notifications or receiving feedback messages.
@@ -74,10 +80,11 @@ internal bool IsOpen
///
/// Opens the session and then opens the worker link.
+ /// Marked virtual for unit testing purposes only.
///
/// The connection to open this session on.
/// The timeout for the open operation.
- internal async Task OpenAsync(AmqpConnection connection, CancellationToken cancellationToken)
+ internal virtual async Task OpenAsync(AmqpConnection connection, CancellationToken cancellationToken)
{
if (Logging.IsEnabled)
Logging.Enter(this, "Opening worker session.", nameof(OpenAsync));
@@ -146,11 +153,12 @@ internal async Task CloseAsync(CancellationToken cancellationToken)
///
/// Sends the cloud to device message via the worker link.
+ /// Marked virtual for unit testing purposes only.
///
/// The message to send.
/// The message delivery tag. Used for correlating messages and acknowledgements.
/// The cancellation token.
- internal async Task SendAsync(AmqpMessage message, ArraySegment deliveryTag, CancellationToken cancellationToken)
+ internal virtual async Task SendAsync(AmqpMessage message, ArraySegment deliveryTag, CancellationToken cancellationToken)
{
if (_sendingLinkHandler == null)
{
diff --git a/iothub/service/src/Authentication/IotHubConnectionProperties.cs b/iothub/service/src/Authentication/IotHubConnectionProperties.cs
index 230b46a827..86c43d3a40 100644
--- a/iothub/service/src/Authentication/IotHubConnectionProperties.cs
+++ b/iothub/service/src/Authentication/IotHubConnectionProperties.cs
@@ -49,9 +49,14 @@ internal static string GetIotHubName(string hostName)
}
int index = hostName.IndexOf(IotHubConnectionStringConstants.HostNameSeparator, StringComparison.OrdinalIgnoreCase);
- string iotHubName = index >= 0
- ? hostName.Substring(0, index)
- : hostName;
+
+ // throw if hostname is invalid format
+ if (index < 0)
+ {
+ throw new ArgumentException("Invalid host name format. Host names should be delimited by periods. E.g, \"IOTHUB_NAME.azure-devices.net\" for public endpoints.");
+ }
+
+ string iotHubName = hostName.Substring(0, index);
return iotHubName;
}
}
diff --git a/iothub/service/src/Authentication/IotHubTokenCredentialProperties.cs b/iothub/service/src/Authentication/IotHubTokenCredentialProperties.cs
index cf521db37a..92a96c856c 100644
--- a/iothub/service/src/Authentication/IotHubTokenCredentialProperties.cs
+++ b/iothub/service/src/Authentication/IotHubTokenCredentialProperties.cs
@@ -12,7 +12,7 @@ namespace Microsoft.Azure.Devices
///
/// The properties required for authentication to IoT hub using a token credential.
///
- internal sealed class IotHubTokenCredentialProperties : IotHubConnectionProperties
+ internal class IotHubTokenCredentialProperties : IotHubConnectionProperties
{
private const string TokenType = "Bearer";
private static readonly string[] s_iotHubAadTokenScopes = new string[] { "https://iothubs.azure.net/.default" };
@@ -21,6 +21,13 @@ internal sealed class IotHubTokenCredentialProperties : IotHubConnectionProperti
private readonly object _tokenLock = new();
private AccessToken? _cachedAccessToken;
+ // Creates an instance of this class. Provided for unit testing purposes only.
+ protected internal IotHubTokenCredentialProperties(string hostName, TokenCredential credential, AccessToken? accessToken) : base(hostName)
+ {
+ _credential = credential;
+ _cachedAccessToken = accessToken;
+ }
+
public IotHubTokenCredentialProperties(string hostName, TokenCredential credential) : base(hostName)
{
_credential = credential;
diff --git a/iothub/service/src/DigitalTwin/DigitalTwinsClient.cs b/iothub/service/src/DigitalTwin/DigitalTwinsClient.cs
index a066d65e57..1cbffbd64b 100644
--- a/iothub/service/src/DigitalTwin/DigitalTwinsClient.cs
+++ b/iothub/service/src/DigitalTwin/DigitalTwinsClient.cs
@@ -106,10 +106,9 @@ await _internalRetryHandler
}
throw new IotHubServiceException(ex.Message, HttpStatusCode.RequestTimeout, IotHubServiceErrorCode.Unknown, null, ex);
}
- catch (Exception ex)
+ catch (Exception ex) when (Logging.IsEnabled)
{
- if (Logging.IsEnabled)
- Logging.Error(this, $"Getting digital twin with Id {digitalTwinId} threw an exception: {ex}", nameof(GetAsync));
+ Logging.Error(this, $"Getting digital twin with Id {digitalTwinId} threw an exception: {ex}", nameof(GetAsync));
throw;
}
finally
@@ -206,10 +205,9 @@ await _internalRetryHandler
}
throw new IotHubServiceException(ex.Message, HttpStatusCode.RequestTimeout, IotHubServiceErrorCode.Unknown, null, ex);
}
- catch (Exception ex)
+ catch (Exception ex) when (Logging.IsEnabled)
{
- if (Logging.IsEnabled)
- Logging.Error(this, $"Updating digital twin with Id {digitalTwinId} threw an exception: {ex}", nameof(UpdateAsync));
+ Logging.Error(this, $"Updating digital twin with Id {digitalTwinId} threw an exception: {ex}", nameof(UpdateAsync));
throw;
}
finally
@@ -292,10 +290,9 @@ await _internalRetryHandler
}
throw new IotHubServiceException(ex.Message, HttpStatusCode.RequestTimeout, IotHubServiceErrorCode.Unknown, null, ex);
}
- catch (Exception ex)
+ catch (Exception ex) when (Logging.IsEnabled)
{
- if (Logging.IsEnabled)
- Logging.Error(this, $"Invoking command on digital twin with Id {digitalTwinId} threw an exception: {ex}", nameof(InvokeCommandAsync));
+ Logging.Error(this, $"Invoking command on digital twin with Id {digitalTwinId} threw an exception: {ex}", nameof(InvokeCommandAsync));
throw;
}
finally
@@ -381,10 +378,9 @@ await _internalRetryHandler
}
throw new IotHubServiceException(ex.Message, HttpStatusCode.RequestTimeout, IotHubServiceErrorCode.Unknown, null, ex);
}
- catch (Exception ex)
+ catch (Exception ex) when (Logging.IsEnabled)
{
- if (Logging.IsEnabled)
- Logging.Error(this, $"Invoking component command on digital twin with Id {digitalTwinId} threw an exception: {ex}", nameof(InvokeComponentCommandAsync));
+ Logging.Error(this, $"Invoking component command on digital twin with Id {digitalTwinId} threw an exception: {ex}", nameof(InvokeComponentCommandAsync));
throw;
}
finally
diff --git a/iothub/service/src/DigitalTwin/Models/InvokeDigitalTwinCommandResponse.cs b/iothub/service/src/DigitalTwin/Models/InvokeDigitalTwinCommandResponse.cs
index 897b9ee5fa..2ef5a47caf 100644
--- a/iothub/service/src/DigitalTwin/Models/InvokeDigitalTwinCommandResponse.cs
+++ b/iothub/service/src/DigitalTwin/Models/InvokeDigitalTwinCommandResponse.cs
@@ -9,7 +9,7 @@ namespace Microsoft.Azure.Devices
public class InvokeDigitalTwinCommandResponse
{
///
- /// This constructor is for deserialization and unit test mocking purposes.
+ /// This constructor is for unit test mocking purposes.
///
///
/// To unit test methods that use this type as a response, inherit from this class and give it a constructor
diff --git a/iothub/service/src/DigitalTwin/Models/UpdateDigitalTwinOptions.cs b/iothub/service/src/DigitalTwin/Models/UpdateDigitalTwinOptions.cs
index 932a0b59c4..190eb9dfe7 100644
--- a/iothub/service/src/DigitalTwin/Models/UpdateDigitalTwinOptions.cs
+++ b/iothub/service/src/DigitalTwin/Models/UpdateDigitalTwinOptions.cs
@@ -11,7 +11,7 @@ namespace Microsoft.Azure.Devices
public class UpdateDigitalTwinOptions
{
///
- /// A string representing a weak ETag for the entity that this request performs an operation against, as per RFC7232.
+ /// A weak ETag for the entity that this request performs an operation against, as per RFC7232.
///
///
/// The request's operation is performed only if this ETag matches the value maintained by the server,
diff --git a/iothub/service/src/Exceptions/IotHubServiceException.cs b/iothub/service/src/Exceptions/IotHubServiceException.cs
index 2a6d7e22b7..412ef8c770 100644
--- a/iothub/service/src/Exceptions/IotHubServiceException.cs
+++ b/iothub/service/src/Exceptions/IotHubServiceException.cs
@@ -58,6 +58,7 @@ public IotHubServiceException(
///
/// The that holds the serialized object data about the exception being thrown.
/// The that contains contextual information about the source or destination.
+ /// When the provided is null.
protected IotHubServiceException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
@@ -95,6 +96,7 @@ protected IotHubServiceException(SerializationInfo info, StreamingContext contex
///
/// The that holds the serialized object data about the exception being thrown.
/// The that contains contextual information about the source or destination.
+ /// When the provided is null.
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
diff --git a/iothub/service/src/Feedback/MessageFeedbackProcessorClient.cs b/iothub/service/src/Feedback/MessageFeedbackProcessorClient.cs
index 0ce53528ba..dc22cfeee3 100644
--- a/iothub/service/src/Feedback/MessageFeedbackProcessorClient.cs
+++ b/iothub/service/src/Feedback/MessageFeedbackProcessorClient.cs
@@ -29,6 +29,22 @@ public class MessageFeedbackProcessorClient : IDisposable
protected MessageFeedbackProcessorClient()
{ }
+ ///
+ /// Creates an instance of this class. Provided for unit testing purposes only.
+ ///
+ internal MessageFeedbackProcessorClient(
+ string hostName,
+ IotHubConnectionProperties credentialProvider,
+ IotHubServiceClientOptions options,
+ RetryHandler retryHandler,
+ AmqpConnectionHandler amqpConnection)
+ {
+ _hostName = hostName;
+ _credentialProvider = credentialProvider;
+ _internalRetryHandler = retryHandler;
+ _amqpConnection = amqpConnection;
+ }
+
internal MessageFeedbackProcessorClient(
string hostName,
IotHubConnectionProperties credentialProvider,
diff --git a/iothub/service/src/Feedback/Models/FeedbackBatch.cs b/iothub/service/src/Feedback/Models/FeedbackBatch.cs
index 697c0ab28d..6ba25b055c 100644
--- a/iothub/service/src/Feedback/Models/FeedbackBatch.cs
+++ b/iothub/service/src/Feedback/Models/FeedbackBatch.cs
@@ -15,7 +15,7 @@ namespace Microsoft.Azure.Devices
public class FeedbackBatch
{
///
- /// This constructor is for deserialization and unit test mocking purposes.
+ /// This constructor is for unit test mocking purposes.
///
///
/// To unit test methods that use this type as a response, inherit from this class and give it a constructor
diff --git a/iothub/service/src/FileUpload/FileUploadNotificationProcessorClient.cs b/iothub/service/src/FileUpload/FileUploadNotificationProcessorClient.cs
index 6ff76bb113..ecf8666575 100644
--- a/iothub/service/src/FileUpload/FileUploadNotificationProcessorClient.cs
+++ b/iothub/service/src/FileUpload/FileUploadNotificationProcessorClient.cs
@@ -27,6 +27,21 @@ public class FileUploadNotificationProcessorClient : IDisposable
protected FileUploadNotificationProcessorClient()
{ }
+ ///
+ /// Creates an instance of this class. Provided for unit testing purposes only.
+ ///
+ internal FileUploadNotificationProcessorClient(
+ string hostName,
+ IotHubConnectionProperties credentialProvider,
+ RetryHandler retryHandler,
+ AmqpConnectionHandler amqpConnection)
+ {
+ _hostName = hostName;
+ _credentialProvider = credentialProvider;
+ _internalRetryHandler = retryHandler;
+ _amqpConnection = amqpConnection;
+ }
+
internal FileUploadNotificationProcessorClient(
string hostName,
IotHubConnectionProperties credentialProvider,
diff --git a/iothub/service/src/FileUpload/Models/FileUploadNotification.cs b/iothub/service/src/FileUpload/Models/FileUploadNotification.cs
index 85057d69b6..8f66b3f39d 100644
--- a/iothub/service/src/FileUpload/Models/FileUploadNotification.cs
+++ b/iothub/service/src/FileUpload/Models/FileUploadNotification.cs
@@ -30,9 +30,8 @@ protected internal FileUploadNotification()
///
/// URI path of the uploaded file.
///
- // TODO: consider changing this to System.Uri before GA
[JsonProperty("blobUri")]
- public string BlobUriPath { get; protected internal set; }
+ public Uri BlobUriPath { get; protected internal set; }
///
/// Name of the uploaded file.
diff --git a/iothub/service/src/Jobs/ScheduledJobsClient.cs b/iothub/service/src/Jobs/ScheduledJobsClient.cs
index 4714e21624..798e12cc98 100644
--- a/iothub/service/src/Jobs/ScheduledJobsClient.cs
+++ b/iothub/service/src/Jobs/ScheduledJobsClient.cs
@@ -97,10 +97,9 @@ await _internalRetryHandler
}
throw new IotHubServiceException(ex.Message, HttpStatusCode.RequestTimeout, IotHubServiceErrorCode.Unknown, null, ex);
}
- catch (Exception ex)
+ catch (Exception ex) when (Logging.IsEnabled)
{
- if (Logging.IsEnabled)
- Logging.Error(this, $"Getting job {jobId} threw an exception: {ex}", nameof(GetAsync));
+ Logging.Error(this, $"Getting job {jobId} threw an exception: {ex}", nameof(GetAsync));
throw;
}
finally
@@ -182,10 +181,9 @@ await _internalRetryHandler
}
throw new IotHubServiceException(ex.Message, HttpStatusCode.RequestTimeout, IotHubServiceErrorCode.Unknown, null, ex);
}
- catch (Exception ex)
+ catch (Exception ex) when (Logging.IsEnabled)
{
- if (Logging.IsEnabled)
- Logging.Error(this, $"Canceling job {jobId} threw an exception: {ex}", nameof(CancelAsync));
+ Logging.Error(this, $"Canceling job {jobId} threw an exception: {ex}", nameof(CancelAsync));
throw;
}
finally
@@ -264,10 +262,9 @@ await _internalRetryHandler
}
throw new IotHubServiceException(ex.Message, HttpStatusCode.RequestTimeout, IotHubServiceErrorCode.Unknown, null, ex);
}
- catch (Exception ex)
+ catch (Exception ex) when (Logging.IsEnabled)
{
- if (Logging.IsEnabled)
- Logging.Error(this, $"Scheduling direct method job {scheduledJobsOptions.JobId} threw an exception: {ex}", nameof(ScheduleDirectMethodAsync));
+ Logging.Error(this, $"Scheduling direct method job {scheduledJobsOptions.JobId} threw an exception: {ex}", nameof(ScheduleDirectMethodAsync));
throw;
}
finally
@@ -354,10 +351,9 @@ await _internalRetryHandler
}
throw new IotHubServiceException(ex.Message, HttpStatusCode.RequestTimeout, IotHubServiceErrorCode.Unknown, null, ex);
}
- catch (Exception ex)
+ catch (Exception ex) when (Logging.IsEnabled)
{
- if (Logging.IsEnabled)
- Logging.Error(this, $"Scheduling twin update {scheduledJobsOptions.JobId} threw an exception: {ex}", nameof(ScheduleTwinUpdateAsync));
+ Logging.Error(this, $"Scheduling twin update {scheduledJobsOptions.JobId} threw an exception: {ex}", nameof(ScheduleTwinUpdateAsync));
throw;
}
finally
diff --git a/iothub/service/src/Messaging/MessagesClient.cs b/iothub/service/src/Messaging/MessagesClient.cs
index 7bdc4d377d..81fee2bd74 100644
--- a/iothub/service/src/Messaging/MessagesClient.cs
+++ b/iothub/service/src/Messaging/MessagesClient.cs
@@ -38,6 +38,21 @@ protected MessagesClient()
{
}
+ ///
+ /// Creates an instance of this class. Provided for unit testing purposes only.
+ ///
+ internal MessagesClient(
+ string hostName,
+ IotHubConnectionProperties credentialProvider,
+ RetryHandler retryHandler,
+ AmqpConnectionHandler amqpConnection)
+ {
+ _hostName = hostName;
+ _credentialProvider = credentialProvider;
+ _internalRetryHandler = retryHandler;
+ _amqpConnection = amqpConnection;
+ }
+
internal MessagesClient(
string hostName,
IotHubConnectionProperties credentialProvider,
diff --git a/iothub/service/src/Query/QueryClient.cs b/iothub/service/src/Query/QueryClient.cs
index 476742b033..bd2ba0a019 100644
--- a/iothub/service/src/Query/QueryClient.cs
+++ b/iothub/service/src/Query/QueryClient.cs
@@ -144,10 +144,9 @@ await _internalRetryHandler
}
throw new IotHubServiceException(ex.Message, HttpStatusCode.RequestTimeout, IotHubServiceErrorCode.Unknown, null, ex);
}
- catch (Exception ex)
+ catch (Exception ex) when (Logging.IsEnabled)
{
- if (Logging.IsEnabled)
- Logging.Error(this, $"Creating query threw an exception: {ex}", nameof(CreateAsync));
+ Logging.Error(this, $"Creating query threw an exception: {ex}", nameof(CreateAsync));
throw;
}
finally
@@ -228,10 +227,9 @@ await _internalRetryHandler
}
throw new IotHubServiceException(ex.Message, HttpStatusCode.RequestTimeout, IotHubServiceErrorCode.Unknown, null, ex);
}
- catch (Exception ex)
+ catch (Exception ex) when (Logging.IsEnabled)
{
- if (Logging.IsEnabled)
- Logging.Error(this, $"Creating query with jobType: {options?.JobType}, jobStatus: {options?.JobStatus}, pageSize: {options?.PageSize} threw an exception: {ex}", nameof(CreateAsync));
+ Logging.Error(this, $"Creating query with jobType: {options?.JobType}, jobStatus: {options?.JobStatus}, pageSize: {options?.PageSize} threw an exception: {ex}", nameof(CreateAsync));
throw;
}
finally
diff --git a/iothub/service/src/Utilities/Logging.cs b/iothub/service/src/Utilities/Logging.cs
index 15bdbbb51a..7e83a17dfa 100644
--- a/iothub/service/src/Utilities/Logging.cs
+++ b/iothub/service/src/Utilities/Logging.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using System.Globalization;
using System.Runtime.CompilerServices;
@@ -11,6 +12,8 @@
namespace Microsoft.Azure.Devices
{
+ /// Logging is not part of the core functionality of the client, so we will not concern ourselves with code coverage of this class.
+ [ExcludeFromCodeCoverage]
internal sealed partial class Logging : EventSource
{
/// The single event source instance to use for all logging.
diff --git a/iothub/service/tests/Amqp/AmqpCbsSessionHandlerTests.cs b/iothub/service/tests/Amqp/AmqpCbsSessionHandlerTests.cs
new file mode 100644
index 0000000000..67034fa300
--- /dev/null
+++ b/iothub/service/tests/Amqp/AmqpCbsSessionHandlerTests.cs
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.Core;
+using FluentAssertions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.Azure.Devices.Tests.Amqp
+{
+ [TestClass]
+ [TestCategory("Unit")]
+ public class AmqpCbsSessionHandlerTests
+ {
+ private const string HostName = "contoso.azure-devices.net";
+ private static void ConnectionLossHandler(object sender, EventArgs e) { }
+
+ [TestMethod]
+ public void AmqpCbsSessionHandler_OpenAsync_IsOpenIsTrue()
+ {
+ // arrange
+ var mockCredential = new Mock();
+ var tokenCredentialProperties = new IotHubTokenCredentialProperties(HostName, mockCredential.Object);
+
+ using var cbsSessionHandler = new MockableAmqpCbsSessionHandler(tokenCredentialProperties, ConnectionLossHandler);
+ var mockAmqpCbsLink = new Mock();
+
+ mockAmqpCbsLink
+ .Setup(l => l.SendTokenAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(DateTime.UtcNow.AddMinutes(11)); // the returned value does not matter in this context.
+
+ // act
+ Func act = async () => await cbsSessionHandler.OpenAsync(mockAmqpCbsLink.Object, CancellationToken.None).ConfigureAwait(false);
+
+ // assert
+ act.Should().NotThrowAsync();
+ cbsSessionHandler.IsOpen().Should().BeTrue();
+ }
+
+ [TestMethod]
+ public void AmqpCbsSessionHandler_OpenAsync_Cancelled_ThrowsOperationCanceledException()
+ {
+ // arrange
+ var mockCredential = new Mock();
+ var tokenCredentialProperties = new IotHubTokenCredentialProperties(HostName, mockCredential.Object);
+
+ using var cbsSessionHandler = new MockableAmqpCbsSessionHandler(tokenCredentialProperties, ConnectionLossHandler);
+ var mockAmqpCbsLink = new Mock();
+
+ var ct = new CancellationToken(true);
+
+ // act
+ Func act = async () => await cbsSessionHandler.OpenAsync(mockAmqpCbsLink.Object, ct);
+
+ // assert
+ act.Should().ThrowAsync();
+ }
+ }
+}
diff --git a/iothub/service/tests/Amqp/AmqpClientHelperTests.cs b/iothub/service/tests/Amqp/AmqpClientHelperTests.cs
new file mode 100644
index 0000000000..9695e4354f
--- /dev/null
+++ b/iothub/service/tests/Amqp/AmqpClientHelperTests.cs
@@ -0,0 +1,270 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.Azure.Amqp;
+using Microsoft.Azure.Amqp.Framing;
+using Microsoft.Azure.Devices.Amqp;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json;
+
+namespace Microsoft.Azure.Devices.Tests.Amqp
+{
+ [TestClass]
+ [TestCategory("Unit")]
+ public class AmqpClientHelperTests
+ {
+ private const string UnknownErrorMessage = "Unknown error.";
+ private const string AmqpLinkReleaseMessage = "AMQP link released.";
+
+ [TestMethod]
+ public void AmqpClientHelper_ToIotHubClientContract_TimeoutException_ReturnsIoTHubServiceException()
+ {
+ // arrange - act
+ const string timeoutExceptionMessage = "TimeoutException occurred";
+ var timeoutException = new TimeoutException(timeoutExceptionMessage);
+
+ var returnedException = (IotHubServiceException)AmqpClientHelper.ToIotHubClientContract(timeoutException);
+
+ // assert
+ returnedException.Message.Should().Be(timeoutExceptionMessage);
+ returnedException.IsTransient.Should().BeTrue();
+ returnedException.StatusCode.Should().Be(HttpStatusCode.RequestTimeout);
+ returnedException.ErrorCode.Should().Be(IotHubServiceErrorCode.Unknown);
+ }
+
+ [TestMethod]
+ public void AmqpClientHelper_ToIotHubClientContract_UnauthorizedAccessException_ReturnsToIHubServiceException()
+ {
+ // arrange - act
+ const string unauthorizedAccessExceptionMessage = "UnauthorizedAccessException occurred";
+ var unauthorizedAccessException = new UnauthorizedAccessException(unauthorizedAccessExceptionMessage);
+
+ var returnedException = (IotHubServiceException)AmqpClientHelper.ToIotHubClientContract(unauthorizedAccessException);
+
+ // assert
+ returnedException.Message.Should().Be(unauthorizedAccessExceptionMessage);
+ returnedException.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
+ returnedException.ErrorCode.Should().Be(IotHubServiceErrorCode.IotHubUnauthorizedAccess);
+ }
+
+ [TestMethod]
+ [DataRow("amqp:not-found", HttpStatusCode.NotFound, IotHubServiceErrorCode.DeviceNotFound)]
+ [DataRow(AmqpConstants.Vendor + ":timeout", HttpStatusCode.RequestTimeout, IotHubServiceErrorCode.Unknown)]
+ [DataRow("amqp:unauthorized-access", HttpStatusCode.Unauthorized, IotHubServiceErrorCode.IotHubUnauthorizedAccess)]
+ [DataRow("amqp:link:message-size-exceeded", HttpStatusCode.RequestEntityTooLarge, IotHubServiceErrorCode.MessageTooLarge)]
+ [DataRow("amqp:resource-limit-exceeded", HttpStatusCode.Forbidden, IotHubServiceErrorCode.DeviceMaximumQueueDepthExceeded)]
+ [DataRow(AmqpConstants.Vendor + ":device-already-exists", HttpStatusCode.Conflict, IotHubServiceErrorCode.DeviceAlreadyExists)]
+ [DataRow(AmqpConstants.Vendor + ":device-container-throttled", (HttpStatusCode)429, IotHubServiceErrorCode.ThrottlingException)]
+ [DataRow(AmqpConstants.Vendor + ":quota-exceeded", HttpStatusCode.Forbidden, IotHubServiceErrorCode.IotHubQuotaExceeded)]
+ [DataRow(AmqpConstants.Vendor + ":precondition-failed", HttpStatusCode.PreconditionFailed, IotHubServiceErrorCode.PreconditionFailed)]
+ [DataRow(AmqpConstants.Vendor + ":iot-hub-suspended", HttpStatusCode.BadRequest, IotHubServiceErrorCode.IotHubSuspended)]
+ public void AmqpClientHelper_ToIotHubClientContract_Success(string amqpErrorCode, HttpStatusCode statusCode, IotHubServiceErrorCode errorCode)
+ {
+ // arrange - act
+ const string expectedTrackingId = "TrackingId1234";
+
+ var error = new Error
+ {
+ Condition = amqpErrorCode,
+ Info = new Fields
+ {
+ { AmqpsConstants.TrackingId, expectedTrackingId }
+ },
+ Description = amqpErrorCode
+ };
+ string expectedMessage = $"{error.Description}\r\nTracking Id:{expectedTrackingId}";
+
+ var amqpException = new AmqpException(error);
+ var returnedException = (IotHubServiceException)AmqpClientHelper.ToIotHubClientContract(amqpException);
+
+ // assert
+ returnedException.StatusCode.Should().Be(statusCode);
+ returnedException.Message.Should().Be(expectedMessage);
+ returnedException.ErrorCode.Should().Be(errorCode);
+ returnedException.TrackingId.Should().Be(expectedTrackingId);
+ }
+
+ [TestMethod]
+ public void AmqpClientHelper_ToIotHubClientContract_NullError_NullInnerException_ReturnsUnknownError()
+ {
+ // arrange - act
+ IotHubServiceException returnedException = AmqpClientHelper.ToIotHubClientContract(null, null); // null error and innerException
+
+ // assert
+ returnedException.Message.Should().Be(UnknownErrorMessage);
+ }
+
+ [TestMethod]
+ public void AmqpClientHelper_ValidateContentType_OnValidationFailure_ThrowsInvalidOperationException()
+ {
+ // arrange
+ using var amqpMessage = AmqpMessage.Create();
+ amqpMessage.Properties.ContentType = "application/json";
+
+ // act
+ Action act = () => AmqpClientHelper.ValidateContentType(amqpMessage, AmqpsConstants.BatchedFeedbackContentType);
+
+ // assert
+ act.Should().Throw();
+ }
+
+ [TestMethod]
+ public void AmqpClientHelper_ValidateContentType_Success()
+ {
+ // arrange
+ using var amqpMessage = AmqpMessage.Create();
+ amqpMessage.Properties.ContentType = AmqpsConstants.BatchedFeedbackContentType;
+
+ // act
+ Action act = () => AmqpClientHelper.ValidateContentType(amqpMessage, AmqpsConstants.BatchedFeedbackContentType);
+
+ // assert
+ act.Should().NotThrow();
+ }
+
+ [TestMethod]
+ public void AmqpClientHelper_GetExceptionFromOutcome_NullOutcome_ReturnsIotHubServiceException_WithUnknownMessage()
+ {
+ // arrange - act
+ var act = (IotHubServiceException)AmqpClientHelper.GetExceptionFromOutcome(null);
+
+ // assert
+ act.Message.Should().Be(UnknownErrorMessage);
+ }
+
+ [TestMethod]
+ public void AmqpClientHelper_GetExceptionFromOutcome_RejectedOutcome_ReturnsIotHubServiceException_WithRejectedMessage()
+ {
+ // arrange
+ var outcome = new Rejected();
+
+ // act
+ var act = (IotHubServiceException)AmqpClientHelper.GetExceptionFromOutcome(outcome);
+
+ // assert
+ act.Message.Should().Be(UnknownErrorMessage);
+ }
+
+ [TestMethod]
+ public void AmqpClientHelper_GetExceptionFromOutcome_ReleasedOutcome_ReturnsOperationCanceledException()
+ {
+ // arrange
+ var outcome = new Released();
+
+ // act
+ var act = (OperationCanceledException)AmqpClientHelper.GetExceptionFromOutcome(outcome);
+
+ // assert
+ act.Message.Should().Be(AmqpLinkReleaseMessage);
+ }
+
+
+ [TestMethod]
+ public void AmqpClientHelper_GetExceptionFromOutcome_NonRejectedOutcome_NonReleased_ReturnsIotHubServiceException_WithUnknownMessage()
+ {
+ // arrange
+ var outcome = new Accepted();
+
+ // act
+ var act = (IotHubServiceException)AmqpClientHelper.GetExceptionFromOutcome(outcome);
+
+ // assert
+ act.Message.Should().Be(UnknownErrorMessage);
+ }
+
+ [TestMethod]
+ public async Task AmqpClientHelper_GetObjectFromAmqpMessageAsync_FeedbackRecordType_Success()
+ {
+ // arrange
+ const string originalMessageId1 = "1";
+ const string originalMessageId2 = "2";
+ const string deviceGenerationId1 = "d1";
+ const string deviceGenerationId2 = "d2";
+ const string deviceId1 = "deviceId1234";
+ const string deviceId2 = "deviceId5678";
+ DateTime enqueuedOnUtc1 = DateTime.UtcNow;
+ DateTime enqueuedOnUtc2 = DateTime.UtcNow.AddMinutes(2);
+ FeedbackStatusCode statusCode1 = FeedbackStatusCode.Success;
+ FeedbackStatusCode statusCode2 = FeedbackStatusCode.Expired;
+
+ var dataList = new List
+ {
+ new FeedbackRecord
+ {
+ OriginalMessageId = originalMessageId1,
+ DeviceGenerationId = deviceGenerationId1,
+ DeviceId = deviceId1,
+ EnqueuedOnUtc = enqueuedOnUtc1,
+ StatusCode = statusCode1
+ },
+ new FeedbackRecord
+ {
+ OriginalMessageId = originalMessageId2,
+ DeviceGenerationId = deviceGenerationId2,
+ DeviceId = deviceId2,
+ EnqueuedOnUtc = enqueuedOnUtc2,
+ StatusCode = statusCode2
+ },
+ };
+
+ string jsonString = JsonConvert.SerializeObject(dataList);
+
+ var message = new Message(Encoding.UTF8.GetBytes(jsonString));
+
+ using AmqpMessage amqpMessage = MessageConverter.MessageToAmqpMessage(message);
+ amqpMessage.Properties.ContentType = AmqpsConstants.BatchedFeedbackContentType;
+
+ // act
+ IEnumerable feedbackRecords = await AmqpClientHelper.GetObjectFromAmqpMessageAsync>(amqpMessage).ConfigureAwait(false);
+
+ // assert
+ feedbackRecords.Count().Should().Be(dataList.Count);
+ FeedbackRecord feedbackRecord1 = feedbackRecords.ElementAt(0);
+ FeedbackRecord feedbackRecord2 = feedbackRecords.ElementAt(1);
+
+ feedbackRecord1.OriginalMessageId.Should().Be(originalMessageId1);
+ feedbackRecord2.OriginalMessageId.Should().Be(originalMessageId2);
+ feedbackRecord1.DeviceGenerationId.Should().Be(deviceGenerationId1);
+ feedbackRecord2.DeviceGenerationId.Should().Be(deviceGenerationId2);
+ feedbackRecord1.DeviceId.Should().Be(deviceId1);
+ feedbackRecord2.DeviceId.Should().Be(deviceId2);
+ feedbackRecord1.EnqueuedOnUtc.Should().Be(enqueuedOnUtc1);
+ feedbackRecord2.EnqueuedOnUtc.Should().Be(enqueuedOnUtc2);
+ feedbackRecord1.StatusCode.Should().Be(statusCode1);
+ feedbackRecord2.StatusCode.Should().Be(statusCode2);
+ }
+
+ [TestMethod]
+ public void AmqpClientHelper_GetErrorContextFromException_Success()
+ {
+ // arrange
+ const string amqpErrorCode = "amqp:not-found";
+ const string trackingId = "Trackingid1234";
+
+ var error = new Error
+ {
+ Condition = amqpErrorCode,
+ Info = new Fields
+ {
+ { AmqpsConstants.TrackingId, trackingId }
+ },
+ Description = amqpErrorCode
+ };
+ var amqpException = new AmqpException(error);
+
+ // act
+ ErrorContext act = AmqpClientHelper.GetErrorContextFromException(amqpException);
+
+ // assert
+ act.IotHubServiceException.Message.Should().Be(error.ToString());
+ act.IotHubServiceException.InnerException.Should().BeEquivalentTo(amqpException);
+ }
+ }
+}
diff --git a/iothub/service/tests/Amqp/AmqpConnectionHandlerTests.cs b/iothub/service/tests/Amqp/AmqpConnectionHandlerTests.cs
new file mode 100644
index 0000000000..709459e0d9
--- /dev/null
+++ b/iothub/service/tests/Amqp/AmqpConnectionHandlerTests.cs
@@ -0,0 +1,151 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.Core;
+using FluentAssertions;
+using Microsoft.Azure.Amqp;
+using Microsoft.Azure.Amqp.Framing;
+using Microsoft.Azure.Devices.Amqp;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.Azure.Devices.Tests.Amqp
+{
+ [TestClass]
+ [TestCategory("Unit")]
+ public class AmqpConnectionHandlerTests
+ {
+ private const string HostName = "contoso.azure-devices.net";
+ private static readonly IIotHubServiceRetryPolicy s_RetryPolicy = new IotHubServiceNoRetry();
+ private static readonly IotHubServiceClientOptions s_options = new()
+ {
+ Protocol = IotHubTransportProtocol.Tcp,
+ RetryPolicy = s_RetryPolicy
+ };
+ private readonly EventHandler ConnectionLossHandler = (object sender, EventArgs e) => { };
+
+ [TestMethod]
+ public async Task AmqpConnectionHandler_SendAsync()
+ {
+ // arrange
+ using var amqpMessage = AmqpMessage.Create();
+
+ var mockCredential = new Mock();
+ var tokenCredentialProperties = new IotHubTokenCredentialProperties(HostName, mockCredential.Object);
+ var mockAmqpConnection = new Mock();
+ var mockWorkerSession = new Mock();
+ var mockAmqpCbsSessionHelper = new Mock();
+
+ using var connectionHandler = new AmqpConnectionHandler(
+ tokenCredentialProperties,
+ IotHubTransportProtocol.Tcp,
+ HostName,
+ s_options,
+ ConnectionLossHandler,
+ mockAmqpCbsSessionHelper.Object,
+ mockWorkerSession.Object);
+
+ Outcome outcomeToReturn = new Accepted();
+
+ mockWorkerSession
+ .Setup(ws => ws.SendAsync(It.IsAny(), It.IsAny>(), It.IsAny()))
+ .ReturnsAsync(outcomeToReturn);
+
+ // act
+ Outcome act = await connectionHandler.SendAsync(amqpMessage, CancellationToken.None).ConfigureAwait(false);
+
+ // assert
+ act.Should().BeEquivalentTo(new Accepted());
+ }
+
+ [TestMethod]
+ public void AmqpConnectionHandler_OpenAsync_Cancelled_ThrowsOperationCanceledException()
+ {
+ // arrange
+ using var amqpMessage = AmqpMessage.Create();
+
+ var mockCredential = new Mock();
+ var tokenCredentialProperties = new IotHubTokenCredentialProperties(HostName, mockCredential.Object);
+ var mockWorkerSession = new Mock();
+ var mockAmqpCbsSessionHelper = new Mock();
+
+ using var connectionHandler = new AmqpConnectionHandler(
+ tokenCredentialProperties,
+ IotHubTransportProtocol.Tcp,
+ HostName,
+ s_options,
+ ConnectionLossHandler,
+ mockAmqpCbsSessionHelper.Object,
+ mockWorkerSession.Object);
+
+ var ct = new CancellationToken(true);
+
+ // act
+ Func act = async () => await connectionHandler.OpenAsync(ct);
+
+ // assert
+ act.Should().ThrowAsync();
+ }
+
+ [TestMethod]
+ public void AmqpConnectionHandler_SendAsync_Cancelled_ThrowsOperationCanceledException()
+ {
+ // arrange
+ using var amqpMessage = AmqpMessage.Create();
+
+ var mockCredential = new Mock();
+ var tokenCredentialProperties = new IotHubTokenCredentialProperties(HostName, mockCredential.Object);
+ var mockWorkerSession = new Mock();
+ var mockAmqpCbsSessionHelper = new Mock();
+
+ using var connectionHandler = new AmqpConnectionHandler(
+ tokenCredentialProperties,
+ IotHubTransportProtocol.Tcp,
+ HostName,
+ s_options,
+ ConnectionLossHandler,
+ mockAmqpCbsSessionHelper.Object,
+ mockWorkerSession.Object);
+
+ var ct = new CancellationToken(true);
+
+ // act
+ Func act = async () => await connectionHandler.SendAsync(amqpMessage, ct);
+
+ // assert
+ act.Should().ThrowAsync();
+ }
+
+ [TestMethod]
+ public void AmqpConnectionHandler_CloseAsync_Cancelled_ThrowsOperationCanceledException()
+ {
+ // arrange
+ using var amqpMessage = AmqpMessage.Create();
+
+ var mockCredential = new Mock();
+ var tokenCredentialProperties = new IotHubTokenCredentialProperties(HostName, mockCredential.Object);
+ var mockWorkerSession = new Mock();
+ var mockAmqpCbsSessionHelper = new Mock();
+
+ using var connectionHandler = new AmqpConnectionHandler(
+ tokenCredentialProperties,
+ IotHubTransportProtocol.Tcp,
+ HostName,
+ s_options,
+ ConnectionLossHandler,
+ mockAmqpCbsSessionHelper.Object,
+ mockWorkerSession.Object);
+
+ var ct = new CancellationToken(true);
+
+ // act
+ Func act = async () => await connectionHandler.CloseAsync(ct);
+
+ // assert
+ act.Should().ThrowAsync();
+ }
+ }
+}
diff --git a/iothub/service/tests/Amqp/MockableAmqpCbsLink.cs b/iothub/service/tests/Amqp/MockableAmqpCbsLink.cs
new file mode 100644
index 0000000000..754a901141
--- /dev/null
+++ b/iothub/service/tests/Amqp/MockableAmqpCbsLink.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Azure.Devices.Tests.Amqp
+{
+ public class MockableAmqpCbsLink
+ {
+ ///
+ /// Since AmqpCbsLink from Amqp library is not overridable, this mockable class was created.
+ ///
+ public MockableAmqpCbsLink() { }
+
+ internal virtual Task SendTokenAsync(IotHubConnectionProperties credential, Uri amqpEndpoint, string audience, string resource, string[] strings, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(DateTime.UtcNow.AddMinutes(15));
+ }
+ }
+}
diff --git a/iothub/service/tests/Amqp/MockableAmqpCbsSessionHandler.cs b/iothub/service/tests/Amqp/MockableAmqpCbsSessionHandler.cs
new file mode 100644
index 0000000000..83152f8bf1
--- /dev/null
+++ b/iothub/service/tests/Amqp/MockableAmqpCbsSessionHandler.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Azure.Amqp;
+using Microsoft.Azure.Devices.Amqp;
+
+namespace Microsoft.Azure.Devices.Tests.Amqp
+{
+ ///
+ /// To use MockableAmqpCbsLink, this class was created.
+ ///
+ internal class MockableAmqpCbsSessionHandler : AmqpCbsSessionHandler
+ {
+ private readonly IotHubConnectionProperties _credential;
+ private MockableAmqpCbsLink _cbsLink;
+ private readonly EventHandler _connectionLossHandler;
+
+ public MockableAmqpCbsSessionHandler(IotHubConnectionProperties credential, EventHandler connectionLossHandler)
+ : base(credential, connectionLossHandler)
+ {
+ _credential = credential;
+ _connectionLossHandler = connectionLossHandler;
+ }
+
+ public async Task OpenAsync(MockableAmqpCbsLink cbsLink, CancellationToken cancellationToken)
+ {
+ _cbsLink = cbsLink;
+ await SendCbsTokenAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task SendCbsTokenAsync(CancellationToken cancellationToken)
+ {
+ Uri amqpEndpoint = new UriBuilder(AmqpConstants.SchemeAmqps, _credential.HostName, AmqpConstants.DefaultSecurePort).Uri;
+
+ string audience = amqpEndpoint.AbsoluteUri;
+ string resource = amqpEndpoint.AbsoluteUri;
+
+ await _cbsLink.SendTokenAsync(
+ _credential,
+ amqpEndpoint,
+ audience,
+ resource,
+ _credential.AmqpAudience.ToArray(),
+ cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ public new bool IsOpen()
+ {
+ return _cbsLink != null;
+ }
+ }
+}
diff --git a/iothub/service/tests/Amqp/Properties/AssemblyInfo.cs b/iothub/service/tests/Amqp/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..5bf8d6b092
--- /dev/null
+++ b/iothub/service/tests/Amqp/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
diff --git a/iothub/service/tests/Authentication/IotHubTokenCredentialPropertiesTests.cs b/iothub/service/tests/Authentication/IotHubTokenCredentialPropertiesTests.cs
new file mode 100644
index 0000000000..ad99a884d0
--- /dev/null
+++ b/iothub/service/tests/Authentication/IotHubTokenCredentialPropertiesTests.cs
@@ -0,0 +1,122 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.Core;
+using FluentAssertions;
+using Microsoft.Azure.Amqp;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.Azure.Devices.Tests.Authentication
+{
+ [TestClass]
+ [TestCategory("Unit")]
+ public class IotHubTokenCredentialPropertiesTests
+ {
+ private const string HostName = "contoso.azure-devices.net";
+ private const string TokenType = "Bearer";
+ private const string TokenValue = "token";
+
+ [TestMethod]
+ public void IotHubTokenCredentialProperties_GetAuthorizationHeader_CloseToExpiry_GeneratesToken()
+ {
+ // arrange
+ var mockCredential = new Mock();
+
+ string expectedAuthorizationHeader = $"{TokenType} {TokenValue}";
+ DateTime expiryDate = DateTime.UtcNow; // Close to expiry
+ var testAccessToken = new AccessToken(TokenValue, expiryDate);
+
+ var tokenCredentialProperties = new IotHubTokenCredentialProperties(HostName, mockCredential.Object);
+
+ mockCredential
+ .Setup(c => c.GetToken(It.IsAny(), It.IsAny()))
+ .Returns(new AccessToken(TokenValue, expiryDate));
+
+ // act
+ string act() => tokenCredentialProperties.GetAuthorizationHeader();
+
+ // assert
+ act().Should().Be(expectedAuthorizationHeader);
+ mockCredential.Verify(x => x.GetToken(It.IsAny(), It.IsAny()), Times.Once);
+ }
+
+ [TestMethod]
+ public void IotHubTokenCredentialProperties_GetAuthorizationHeader_NotCloseToExpiry_DoesNotGeneratesToken()
+ {
+ // arrange
+ var mockCredential = new Mock();
+
+ string expectedAuthorizationHeader = $"{TokenType} {TokenValue}";
+ DateTime expiryDate = DateTime.UtcNow.AddMinutes(11); // Not close to expiry
+ var testAccessToken = new AccessToken(TokenValue, expiryDate);
+
+ var tokenCredentialProperties = new IotHubTokenCredentialProperties(HostName, mockCredential.Object, testAccessToken);
+
+ mockCredential
+ .Setup(c => c.GetToken(It.IsAny(), It.IsAny()))
+ .Returns(new AccessToken(TokenValue, expiryDate));
+
+ // act
+ string act() => tokenCredentialProperties.GetAuthorizationHeader();
+
+ // assert
+ act().Should().Be(expectedAuthorizationHeader);
+ mockCredential.Verify(x => x.GetToken(It.IsAny(), It.IsAny()), Times.Never);
+ }
+
+ [TestMethod]
+ public void IotHubTokenCredentialProperties_GetAuthorizationHeader_NullTokenValue_GeneratesToken()
+ {
+ // arrange
+ var mockCredential = new Mock();
+
+ string expectedAuthorizationHeader = $"{TokenType} ";
+ DateTime expiryDate = DateTime.UtcNow.AddMinutes(11);
+
+ var tokenCredentialProperties = new IotHubTokenCredentialProperties(HostName, mockCredential.Object, null); // null cached access token
+
+ mockCredential
+ .Setup(c => c.GetToken(It.IsAny(), It.IsAny()))
+ .Returns(new AccessToken(null, expiryDate));
+
+ // act
+ string act() => tokenCredentialProperties.GetAuthorizationHeader();
+
+ // assert
+ act().Should().Be(expectedAuthorizationHeader);
+ mockCredential.Verify(x => x.GetToken(It.IsAny(), It.IsAny()), Times.Once);
+ }
+
+ [TestMethod]
+ public async Task IotHubTokenCredentialProperties_GetTokenAsync_Ok()
+ {
+ // arrange
+ var mockCredential = new Mock();
+ var tokenCredentialProperties = new IotHubTokenCredentialProperties(HostName, mockCredential.Object);
+
+ string expectedAuthorizationHeader = $"{TokenType}";
+ DateTimeOffset expiryDateTimeOffset = DateTimeOffset.UtcNow;
+
+ var accessTokenToReturn = new AccessToken(TokenValue, expiryDateTimeOffset);
+
+ mockCredential
+ .Setup(t => t.GetTokenAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(accessTokenToReturn);
+
+ // act
+ CbsToken token = await tokenCredentialProperties.GetTokenAsync(null, null, null).ConfigureAwait(false);
+
+ // assert
+ token.TokenValue.Should().Be($"{TokenType} {TokenValue}");
+ token.TokenType.Should().Be(TokenType);
+
+ int timeDelta = Math.Abs((int)(token.ExpiresAtUtc - expiryDateTimeOffset).TotalSeconds);
+ timeDelta.Should().BeLessThan(1);
+ mockCredential.Verify(x => x.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once);
+ }
+ }
+}
diff --git a/iothub/service/tests/Configurations/ConfigurationsClientTests.cs b/iothub/service/tests/Configurations/ConfigurationsClientTests.cs
new file mode 100644
index 0000000000..c6cc7f2378
--- /dev/null
+++ b/iothub/service/tests/Configurations/ConfigurationsClientTests.cs
@@ -0,0 +1,608 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Net;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using System.Threading;
+using FluentAssertions;
+using Azure;
+
+namespace Microsoft.Azure.Devices.Tests.Configurations
+{
+ [TestClass]
+ [TestCategory("Unit")]
+ public class ConfigurationsClientTests
+ {
+ private const string HostName = "contoso.azure-devices.net";
+ private static readonly string s_validMockAuthenticationHeaderValue = $"SharedAccessSignature sr={HostName}&sig=thisIsFake&se=000000&skn=registryRead";
+
+ private static readonly Uri s_httpUri = new($"https://{HostName}");
+ private static readonly RetryHandler s_retryHandler = new(new IotHubServiceNoRetry());
+
+ [TestMethod]
+ public async Task ConfigurationClients_CreateAsync()
+ {
+ //arrange
+ string configurationId = Guid.NewGuid().ToString().ToLower(); // Configuration Id characters must be all lower-case.
+ var configuration = new Configuration(configurationId)
+ {
+ Priority = 1,
+ Labels = { { "testLabelName", "testLabelValue " } },
+ TargetCondition = "deviceId = testId"
+ };
+
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+ var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, "");
+ using var mockHttpResponse = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = HttpMessageHelper.SerializePayload(configuration)
+ };
+ var mockHttpClient = new Mock();
+ mockHttpClient
+ .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(mockHttpResponse);
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await configurationsClient.CreateAsync(configuration).ConfigureAwait(false);
+
+ // assert
+ await act.Should().NotThrowAsync().ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ public async Task ConfigurationClients_CreateAsync_NullConfigurationIdThrows()
+ {
+ // arrange
+ var mockCredentialProvider = new Mock();
+ var mockHttpRequestFactory = new Mock();
+ var mockHttpClient = new Mock();
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory.Object,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await configurationsClient.CreateAsync(null).ConfigureAwait(false);
+
+ // assert
+ await act.Should().ThrowAsync().ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ public async Task ConfigurationClients_CreateAsync_NullConfigurationThrows()
+ {
+ // arrange
+ var mockCredentialProvider = new Mock();
+ var mockHttpRequestFactory = new Mock();
+ var mockHttpClient = new Mock();
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory.Object,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await configurationsClient.CreateAsync(null).ConfigureAwait(false);
+
+ // assert
+ await act.Should().ThrowAsync().ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ public async Task ConfigurationClients_GetAsync()
+ {
+ //arrange
+ string configurationId = Guid.NewGuid().ToString().ToLower(); // Configuration Id characters must be all lower-case.
+ var configuration = new Configuration(configurationId)
+ {
+ Priority = 2,
+ Labels = { { "testLabelName", "testLabelValue " } },
+ TargetCondition = "deviceId = testId"
+ };
+
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+ var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, "");
+ using var mockHttpResponse = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = HttpMessageHelper.SerializePayload(configuration)
+ };
+ var mockHttpClient = new Mock();
+ mockHttpClient
+ .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(mockHttpResponse);
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await configurationsClient.GetAsync(configurationId).ConfigureAwait(false);
+
+ // assert
+ await act.Should().NotThrowAsync().ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ public async Task ConfigurationClients_GetAsync_NullConfigurationIdThrows()
+ {
+ // arrange
+ var mockCredentialProvider = new Mock();
+ var mockHttpRequestFactory = new Mock();
+ var mockHttpClient = new Mock();
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory.Object,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await configurationsClient.GetAsync(null).ConfigureAwait(false);
+
+ // assert
+ await act.Should().ThrowAsync().ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ public async Task ConfigurationClients_GetAsync_NullConfigurationThrows()
+ {
+ // arrange
+ var mockCredentialProvider = new Mock();
+ var mockHttpRequestFactory = new Mock();
+ var mockHttpClient = new Mock();
+ var invalidConfigurationId = "Configuration Id";
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory.Object,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await configurationsClient.GetAsync(invalidConfigurationId).ConfigureAwait(false);
+
+ // assert
+ await act.Should().ThrowAsync().ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ public async Task ConfigurationClients_GetAsync_NegativeMaxCountThrows()
+ {
+ // arrange
+ var mockCredentialProvider = new Mock();
+ var mockHttpRequestFactory = new Mock();
+ var mockHttpClient = new Mock();
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory.Object,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await configurationsClient.GetAsync(-1).ConfigureAwait(false);
+
+ // assert
+ await act.Should().ThrowAsync().ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ public async Task ConfigurationClients_GetAsync_WithMaxCount()
+ {
+ // arrange
+ string configurationId1 = Guid.NewGuid().ToString().ToLower(); // Configuration Id characters must be all lower-case.
+ string configurationId2 = Guid.NewGuid().ToString().ToLower(); // Configuration Id characters must be all lower-case.
+ var configuration1 = new Configuration(configurationId1)
+ {
+ Priority = 2,
+ Labels = { { "testLabelName", "testLabelValue " } },
+ TargetCondition = "deviceId = testId"
+ };
+ var configuration2 = new Configuration(configurationId2)
+ {
+ Priority = 1,
+ Labels = new Dictionary
+ {
+ { "App", "Mongo" }
+ }
+ };
+ var configurationsToReturn = new List { configuration1, configuration2 };
+
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+ var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, "");
+ using var mockHttpResponse = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = HttpMessageHelper.SerializePayload(configurationsToReturn)
+ };
+ var mockHttpClient = new Mock();
+ mockHttpClient
+ .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(mockHttpResponse);
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory,
+ s_retryHandler);
+
+ // act
+ var configurationsResult = await configurationsClient.GetAsync(2).ConfigureAwait(false);
+
+ // assert
+ configurationsResult.ElementAt(0).Id.Should().Be(configurationId1);
+ configurationsResult.ElementAt(1).Id.Should().Be(configurationId2);
+ }
+
+ [TestMethod]
+ public async Task ConfigurationClients_SetAsync()
+ {
+ // arrange
+ string configurationId = Guid.NewGuid().ToString().ToLower(); // Configuration Id characters must be all lower-case.
+
+ var configurationToReturn = new Configuration(configurationId)
+ {
+ Priority = 1,
+ Labels = new Dictionary
+ {
+ { "App", "Mongo" }
+ },
+ ETag = new ETag("123")
+ };
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+ var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, "");
+ using var mockHttpResponse = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = HttpMessageHelper.SerializePayload(configurationToReturn),
+ };
+ var mockHttpClient = new Mock();
+ mockHttpClient
+ .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(mockHttpResponse);
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory,
+ s_retryHandler);
+
+ // act
+ var returnedConfiguration = await configurationsClient.SetAsync(configurationToReturn).ConfigureAwait(false);
+
+ // assert
+ returnedConfiguration.Id.Should().Be(configurationId);
+ mockHttpClient.VerifyAll();
+ }
+
+ [TestMethod]
+ public async Task ConfigurationClients_SetAsync_NullConfigurationThrows()
+ {
+ // arrange
+ var mockCredentialProvider = new Mock();
+ var mockHttpRequestFactory = new Mock();
+ var mockHttpClient = new Mock();
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory.Object,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await configurationsClient.SetAsync(null).ConfigureAwait(false);
+
+ // assert
+ await act.Should().ThrowAsync().ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ public async Task ConfigurationClients_SetAsync_WithOnlyIfUnchangedTrueWithETag()
+ {
+ // arrange
+ string configurationId = Guid.NewGuid().ToString().ToLower(); // Configuration Id characters must be all lower-case.
+ var configurationToReturnWithoutETag = new Configuration(configurationId)
+ {
+ Priority = 1,
+ Labels = new Dictionary
+ {
+ { "App", "Mongo" }
+ },
+ ETag = new ETag("123")
+ };
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+ var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, "");
+ using var mockHttpResponse = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = HttpMessageHelper.SerializePayload(configurationToReturnWithoutETag),
+ };
+ var mockHttpClient = new Mock();
+ mockHttpClient
+ .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(mockHttpResponse);
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await configurationsClient.SetAsync(configurationToReturnWithoutETag, true).ConfigureAwait(false);
+
+ // assert
+ await act.Should().NotThrowAsync().ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ public async Task ConfigurationClients_DeleteAsync()
+ {
+ // arrange
+ string configurationId = Guid.NewGuid().ToString().ToLower(); // Configuration Id characters must be all lower-case.
+ var configuration = new Configuration(configurationId)
+ {
+ Priority = 1,
+ Labels = new Dictionary
+ {
+ { "App", "Mongo" }
+ }
+ };
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+ var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, "");
+ using var mockHttpResponse = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.NoContent
+ };
+ var mockHttpClient = new Mock();
+ mockHttpClient
+ .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(mockHttpResponse);
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await configurationsClient.DeleteAsync(configuration).ConfigureAwait(false);
+
+ // assert
+ await act.Should().NotThrowAsync().ConfigureAwait(false);
+ }
+
+
+ [TestMethod]
+ public async Task ConfigurationClients_DeleteAsync_NullConfigurationIdThrows()
+ {
+ // arrange
+ var mockCredentialProvider = new Mock();
+ var mockHttpRequestFactory = new Mock();
+ var mockHttpClient = new Mock();
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory.Object,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await configurationsClient.DeleteAsync(null, false).ConfigureAwait(false);
+
+ // assert
+ await act.Should().ThrowAsync().ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ public async Task ConfigurationClients_DeleteAsync_InvalidConfigurationIdThrows()
+ {
+ // arrange
+ var mockCredentialProvider = new Mock();
+ var mockHttpRequestFactory = new Mock();
+ var mockHttpClient = new Mock();
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory.Object,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await configurationsClient.DeleteAsync("Invalid Id").ConfigureAwait(false);
+
+ // assert
+ await act.Should().ThrowAsync().ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ public async Task ConfigurationClients_ApplyConfigurationContentOnDeviceAsync_NullDeviceIdThrows()
+ {
+ // arrange
+ var configurationContent = new ConfigurationContent
+ {
+ ModulesContent = new Dictionary>
+ {
+ {
+ "Module1", new Dictionary
+ {
+ { "setting1", "value1" },
+ { "setting2", "value2" }
+ }
+ },
+ {
+ "Module2", new Dictionary
+ {
+ { "settings3", true },
+ { "settings4", 123 }
+ }
+ }
+ }
+ };
+ var mockCredentialProvider = new Mock();
+ var mockHttpRequestFactory = new Mock();
+ var mockHttpClient = new Mock();
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory.Object,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await configurationsClient.ApplyConfigurationContentOnDeviceAsync(null, configurationContent).ConfigureAwait(false);
+
+ // assert
+ await act.Should().ThrowAsync().ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ public async Task ConfigurationClients_ApplyConfigurationContentOnDeviceAsync_NullConfigurationContentThrows()
+ {
+ // arrange
+ var mockCredentialProvider = new Mock();
+ var mockHttpRequestFactory = new Mock();
+ var mockHttpClient = new Mock();
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory.Object,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await configurationsClient.ApplyConfigurationContentOnDeviceAsync("1234", null).ConfigureAwait(false);
+
+ // assert
+ await act.Should().ThrowAsync().ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ public async Task ConfigurationClients_ApplyConfigurationContentOnDeviceAsync_EmptyDeviceIdThrows()
+ {
+ // arrange
+ var mockCredentialProvider = new Mock();
+ var mockHttpRequestFactory = new Mock();
+ var mockHttpClient = new Mock();
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory.Object,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await configurationsClient.ApplyConfigurationContentOnDeviceAsync(string.Empty, null).ConfigureAwait(false);
+
+ // assert
+ await act.Should().ThrowAsync().ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ public async Task ConfigurationClients_ApplyConfigurationContentOnDeviceAsync()
+ {
+ // arrange
+ var deviceId = Guid.NewGuid().ToString();
+ var configurationContent = new ConfigurationContent
+ {
+ ModulesContent = new Dictionary>
+ {
+ {
+ "Module1", new Dictionary
+ {
+ { "setting1", "value1" },
+ { "setting2", "value2" }
+ }
+ },
+ {
+ "Module2", new Dictionary
+ {
+ { "settings3", true },
+ { "settings4", 123 }
+ }
+ }
+ }
+ };
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+ var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, "");
+ using var mockHttpResponse = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK
+ };
+ var mockHttpClient = new Mock();
+ mockHttpClient
+ .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(mockHttpResponse);
+
+ var configurationsClient = new ConfigurationsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await configurationsClient.ApplyConfigurationContentOnDeviceAsync(deviceId, configurationContent).ConfigureAwait(false);
+
+ // assert
+ await act.Should().NotThrowAsync().ConfigureAwait(false);
+ }
+ }
+}
diff --git a/iothub/service/tests/ConnectionString/ConnectionStringTests.cs b/iothub/service/tests/ConnectionString/ConnectionStringTests.cs
index 679f38e7e2..ed1f78bb4a 100644
--- a/iothub/service/tests/ConnectionString/ConnectionStringTests.cs
+++ b/iothub/service/tests/ConnectionString/ConnectionStringTests.cs
@@ -2,7 +2,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
+using System.Threading.Tasks;
using FluentAssertions;
+using Microsoft.Azure.Amqp;
using Microsoft.Azure.Devices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -21,6 +23,8 @@ public void IotHubConnectionStringBuilderTest()
hubCs.SharedAccessKey.Should().NotBeNull();
hubCs.SharedAccessKeyName.Should().NotBeNull();
hubCs.SharedAccessSignature.Should().BeNull();
+ hubCs.GetPassword().Should().NotBeNull();
+ hubCs.GetAuthorizationHeader().Should().NotBeNull();
cs = "HostName=acme.azure-devices.net;CredentialType=SharedAccessSignature;SharedAccessKeyName=AllAccessKey;SharedAccessSignature=SharedAccessSignature sr=dh%3a%2f%2facme.azure-devices.net&sig=dGVzdFN0cmluZzU=&se=87824124985&skn=AllAccessKey";
hubCs = IotHubConnectionStringParser.Parse(cs);
@@ -28,6 +32,29 @@ public void IotHubConnectionStringBuilderTest()
hubCs.SharedAccessKey.Should().BeNull();
hubCs.SharedAccessKeyName.Should().NotBeNull();
hubCs.SharedAccessSignature.Should().NotBeNull();
+ hubCs.GetPassword().Should().BeEquivalentTo(hubCs.SharedAccessSignature);
+ hubCs.GetAuthorizationHeader().Should().BeEquivalentTo(hubCs.SharedAccessSignature);
+ }
+
+ [TestMethod]
+ public void IotHubConnectionStringBuildToken_Succeeds()
+ {
+ string cs = "HostName=acme.azure-devices.net;SharedAccessKeyName=AllAccessKey;SharedAccessKey=dGVzdFN0cmluZzE=";
+ IotHubConnectionString hubCs = IotHubConnectionStringParser.Parse(cs);
+
+ // Builds new SAS under internally
+ string password = hubCs.GetAuthorizationHeader();
+ password.Should().NotBeNull();
+ }
+
+ [TestMethod]
+ public async Task IotHubConnectionStringGetTokenAysnc_Succeeds()
+ {
+ string cs = "HostName=acme.azure-devices.net;SharedAccessKeyName=AllAccessKey;SharedAccessKey=dGVzdFN0cmluZzE=";
+ IotHubConnectionString hubCs = IotHubConnectionStringParser.Parse(cs);
+
+ CbsToken token = await hubCs.GetTokenAsync(null, null, null);
+ token.Should().NotBeNull();
}
}
}
diff --git a/iothub/service/tests/DeviceAuthenticationTests.cs b/iothub/service/tests/DeviceAuthenticationTests.cs
index 8215e96617..ea8fcd9e01 100644
--- a/iothub/service/tests/DeviceAuthenticationTests.cs
+++ b/iothub/service/tests/DeviceAuthenticationTests.cs
@@ -23,7 +23,7 @@ public class DeviceAuthenticationTests
private static readonly RetryHandler s_retryHandler = new(new IotHubServiceNoRetry());
[TestMethod]
- public async Task DeviceAuthenticationGoodAuthConfigTest1()
+ public async Task DeviceAuthentication_GeneratedSymmetricKeysAuthConfigTest()
{
var deviceGoodAuthConfig = new Device("123")
{
@@ -60,7 +60,7 @@ public async Task DeviceAuthenticationGoodAuthConfigTest1()
}
[TestMethod]
- public async Task DeviceAuthenticationGoodAuthConfigTest2()
+ public async Task DeviceAuthentication_CertificateAuthConfigTest_WithMatchingSecondaryThumbprint()
{
var deviceGoodAuthConfig = new Device("123")
{
@@ -97,7 +97,7 @@ public async Task DeviceAuthenticationGoodAuthConfigTest2()
}
[TestMethod]
- public async Task DeviceAuthenticationGoodAuthConfigTest3()
+ public async Task DeviceAuthentication_CertificateAuthConfigTest_NullSecondaryThumbprint()
{
var deviceGoodAuthConfig = new Device("123")
{
@@ -134,7 +134,7 @@ public async Task DeviceAuthenticationGoodAuthConfigTest3()
}
[TestMethod]
- public async Task DeviceAuthenticationGoodAuthConfigTest4()
+ public async Task DeviceAuthentication_CertificateAuthConfigTest_NullPrimaryThumbprint()
{
var deviceGoodAuthConfig = new Device("123")
{
@@ -171,7 +171,7 @@ public async Task DeviceAuthenticationGoodAuthConfigTest4()
}
[TestMethod]
- public async Task DeviceAuthenticationGoodAuthConfigTest5()
+ public async Task DeviceAuthentication_GeneratedSymmetricKeysAuthConfigTest_NullThumbprint()
{
var deviceGoodAuthConfig = new Device("123")
{
@@ -208,9 +208,9 @@ public async Task DeviceAuthenticationGoodAuthConfigTest5()
}
[TestMethod]
- public async Task DeviceAuthenticationGoodAuthConfigTest6()
+ public async Task DeviceAuthentication_CertificateAuthConfigTest_NullSymmetricKey()
{
- var deviceBadAuthConfig = new Device("123")
+ var deviceGoodAuthConfig = new Device("123")
{
ConnectionState = ClientConnectionState.Connected,
Authentication = new AuthenticationMechanism
@@ -224,7 +224,7 @@ public async Task DeviceAuthenticationGoodAuthConfigTest6()
},
};
- HttpContent mockContent = HttpMessageHelper.SerializePayload(deviceBadAuthConfig);
+ HttpContent mockContent = HttpMessageHelper.SerializePayload(deviceGoodAuthConfig);
using var mockHttpResponse = new HttpResponseMessage();
mockHttpResponse.Content = mockContent;
mockHttpResponse.StatusCode = HttpStatusCode.OK;
@@ -241,13 +241,13 @@ public async Task DeviceAuthenticationGoodAuthConfigTest6()
mockHttpRequestFactory,
s_retryHandler);
- await devicesClient.CreateAsync(deviceBadAuthConfig).ConfigureAwait(false);
+ await devicesClient.CreateAsync(deviceGoodAuthConfig).ConfigureAwait(false);
}
[TestMethod]
- public async Task DeviceAuthenticationGoodAuthSHA256()
+ public async Task DeviceAuthentication_CertificateAuthConfigTest_Sha256Thumbprint()
{
- var deviceBadAuthConfig = new Device("123")
+ var deviceGoodAuthConfig = new Device("123")
{
ConnectionState = ClientConnectionState.Connected,
Authentication = new AuthenticationMechanism
@@ -261,7 +261,7 @@ public async Task DeviceAuthenticationGoodAuthSHA256()
},
};
- HttpContent mockContent = HttpMessageHelper.SerializePayload(deviceBadAuthConfig);
+ HttpContent mockContent = HttpMessageHelper.SerializePayload(deviceGoodAuthConfig);
using var mockHttpResponse = new HttpResponseMessage();
mockHttpResponse.Content = mockContent;
mockHttpResponse.StatusCode = HttpStatusCode.OK;
@@ -278,13 +278,13 @@ public async Task DeviceAuthenticationGoodAuthSHA256()
mockHttpRequestFactory,
s_retryHandler);
- await devicesClient.CreateAsync(deviceBadAuthConfig).ConfigureAwait(false);
+ await devicesClient.CreateAsync(deviceGoodAuthConfig).ConfigureAwait(false);
}
[TestMethod]
- public async Task DeviceAuthenticationIsCertificateAuthority()
+ public async Task DeviceAuthentication_CertificateAuthConfig_NullSymmetricKeyAndThumbprint()
{
- var deviceBadThumbprint = new Device("123")
+ var deviceWithoutThumbprint = new Device("123")
{
ConnectionState = ClientConnectionState.Connected,
Authentication = new AuthenticationMechanism
@@ -295,7 +295,7 @@ public async Task DeviceAuthenticationIsCertificateAuthority()
},
};
- HttpContent mockContent = HttpMessageHelper.SerializePayload(deviceBadThumbprint);
+ HttpContent mockContent = HttpMessageHelper.SerializePayload(deviceWithoutThumbprint);
using var mockHttpResponse = new HttpResponseMessage();
mockHttpResponse.Content = mockContent;
mockHttpResponse.StatusCode = HttpStatusCode.OK;
@@ -312,7 +312,7 @@ public async Task DeviceAuthenticationIsCertificateAuthority()
mockHttpRequestFactory,
s_retryHandler);
- await devicesClient.CreateAsync(deviceBadThumbprint).ConfigureAwait(false);
+ await devicesClient.CreateAsync(deviceWithoutThumbprint).ConfigureAwait(false);
}
}
}
diff --git a/iothub/service/tests/DigitalTwin/DigitalTwinGetResponseTests.cs b/iothub/service/tests/DigitalTwin/DigitalTwinGetResponseTests.cs
new file mode 100644
index 0000000000..135ca299ff
--- /dev/null
+++ b/iothub/service/tests/DigitalTwin/DigitalTwinGetResponseTests.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Azure;
+using FluentAssertions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.Azure.Devices.Tests.DigitalTwin
+{
+ [TestClass]
+ [TestCategory("Unit")]
+ public class DigitalTwinGetResponseTests
+ {
+ [TestMethod]
+ public void DigitalTwinGetResponse_Ctor_Ok()
+ {
+ // arrange - act
+ const string twinId = "twin1234";
+ const string modelId = "model1234";
+
+ var simpleBasicDigitalTwin = new BasicDigitalTwin
+ {
+ Id = twinId,
+ Metadata = new DigitalTwinMetadata
+ {
+ ModelId = modelId
+ }
+ };
+
+ var eTag = new ETag("1234");
+ var digitalTwinGetResponse = new DigitalTwinGetResponse(simpleBasicDigitalTwin, eTag);
+
+ // assert
+ digitalTwinGetResponse.DigitalTwin.Should().Be(simpleBasicDigitalTwin);
+ digitalTwinGetResponse.ETag.Should().Be(eTag);
+ digitalTwinGetResponse.DigitalTwin.Id.Should().Be(twinId);
+ digitalTwinGetResponse.DigitalTwin.Metadata.ModelId.Should().Be(modelId);
+ digitalTwinGetResponse.DigitalTwin.CustomProperties.Should().NotBeNull();
+ }
+ }
+}
diff --git a/iothub/service/tests/DigitalTwin/DigitalTwinUpdateResponseTests.cs b/iothub/service/tests/DigitalTwin/DigitalTwinUpdateResponseTests.cs
new file mode 100644
index 0000000000..c99d95cef8
--- /dev/null
+++ b/iothub/service/tests/DigitalTwin/DigitalTwinUpdateResponseTests.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Net.Http;
+using System.Net;
+using System.Threading.Tasks;
+using System.Threading;
+using System;
+using Azure;
+using FluentAssertions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.Azure.Devices.Tests.DigitalTwin
+{
+ [TestClass]
+ [TestCategory("Unit")]
+ public class DigitalTwinUpdateResponseTests
+ {
+ [TestMethod]
+ public void DigitalTwinUpdateResponse_Ctor_DefaultProperties()
+ {
+ // arrange - act
+ var digitalTwinUpdateResponse = new DigitalTwinUpdateResponse();
+
+ // assert
+ digitalTwinUpdateResponse.ETag.Should().Be(default(ETag));
+ digitalTwinUpdateResponse.Location.Should().BeNull();
+ }
+
+ [TestMethod]
+ public void DigitalTwinUpdateResponse_Ctor_SetsProperties()
+ {
+ // arrange - act
+ var eTag = new ETag("1234");
+ string location = "https://contoso.azure-devices.net/digitaltwins/sampleDevice?api-version=2021-04-12";
+ var digitalTwinUpdateResponse = new DigitalTwinUpdateResponse(eTag, location);
+
+ // assert
+ digitalTwinUpdateResponse.ETag.Should().Be(eTag);
+ digitalTwinUpdateResponse.Location.Should().Be(location);
+ }
+ }
+}
diff --git a/iothub/service/tests/DigitalTwin/InvokeDigitalTwinCommandOptionsTests.cs b/iothub/service/tests/DigitalTwin/InvokeDigitalTwinCommandOptionsTests.cs
new file mode 100644
index 0000000000..bd4320872a
--- /dev/null
+++ b/iothub/service/tests/DigitalTwin/InvokeDigitalTwinCommandOptionsTests.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json;
+
+namespace Microsoft.Azure.Devices.Tests.DigitalTwin
+{
+ [TestClass]
+ [TestCategory("Unit")]
+ public class InvokeDigitalTwinCommandOptionsTests
+ {
+ [TestMethod]
+ public void InvokeDigitalTwinCommandOptions_PropertySetsAndGets()
+ {
+ // arrange - act
+ int samplePayload = 1;
+ var connectTime = TimeSpan.FromSeconds(1);
+ var requestTimeout = TimeSpan.FromSeconds(1);
+ var options = new InvokeDigitalTwinCommandOptions
+ {
+ Payload = JsonConvert.SerializeObject(samplePayload),
+ ConnectTimeout = connectTime,
+ ResponseTimeout = requestTimeout
+ };
+
+ // assert
+ JsonConvert.DeserializeObject(options.Payload).Should().Be(samplePayload);
+ options.ConnectTimeout.Should().Be(connectTime);
+ options.ResponseTimeout.Should().Be(requestTimeout);
+ }
+
+ [TestMethod]
+ public void InvokeDigitalTwinCommandOptions_Ctor_DefaultProperties()
+ {
+ // arrange - act
+ var options = new InvokeDigitalTwinCommandOptions();
+
+ // assert
+ options.Payload.Should().Be(null);
+ options.ConnectTimeout.Should().Be(null);
+ options.ResponseTimeout.Should().Be(null);
+ }
+ }
+}
diff --git a/iothub/service/tests/DigitalTwin/InvokeDigitalTwinCommandResponseTests.cs b/iothub/service/tests/DigitalTwin/InvokeDigitalTwinCommandResponseTests.cs
new file mode 100644
index 0000000000..9ba9fb43ea
--- /dev/null
+++ b/iothub/service/tests/DigitalTwin/InvokeDigitalTwinCommandResponseTests.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.Azure.Devices.Tests.DigitalTwin
+{
+ [TestClass]
+ [TestCategory("Unit")]
+ public class InvokeDigitalTwinCommandResponseTests
+ {
+ [TestMethod]
+ public void InvokeDigitalTwinCommandResponse_PropertySetsAndGets()
+ {
+ // arrange - act
+ const int Status = 0;
+ const string Payload = "Hello World";
+ const string RequestId = "1234";
+ var invokeDigitalTwinCommandResponse = new InvokeDigitalTwinCommandResponse
+ {
+ Status= Status,
+ Payload= Payload,
+ RequestId = RequestId
+ };
+
+ // assert
+ invokeDigitalTwinCommandResponse.Status.Should().Be(0);
+ invokeDigitalTwinCommandResponse.Payload.Should().Be(Payload);
+ invokeDigitalTwinCommandResponse.RequestId.Should().Be(RequestId);
+ }
+ }
+}
diff --git a/iothub/service/tests/DigitalTwin/UpdateDigitalTwinOptionsTests.cs b/iothub/service/tests/DigitalTwin/UpdateDigitalTwinOptionsTests.cs
new file mode 100644
index 0000000000..758208de14
--- /dev/null
+++ b/iothub/service/tests/DigitalTwin/UpdateDigitalTwinOptionsTests.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Azure;
+using FluentAssertions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.Azure.Devices.Tests.DigitalTwin
+{
+ [TestClass]
+ [TestCategory("Unit")]
+ public class UpdateDigitalTwinOptionsTests
+ {
+ [TestMethod]
+ public void UpdateDigitalTwinOptions_Ctor_DefaultProperties()
+ {
+ // arrange - act
+ var updateDigitalTwinOptions = new UpdateDigitalTwinOptions();
+
+ // assert
+ updateDigitalTwinOptions.IfMatch.Should().Be(ETag.All);
+ }
+
+ [TestMethod]
+ public void UpdateDigitalTwinOptions_PropertySetsAndGets()
+ {
+ // arrange - act
+ var testETag = new ETag("1234");
+ var updateDigitalTwinOptions = new UpdateDigitalTwinOptions
+ {
+ IfMatch = new ETag("1234")
+ };
+
+ // assert
+ updateDigitalTwinOptions.IfMatch.Should().BeEquivalentTo(testETag);
+ }
+ }
+}
diff --git a/iothub/service/tests/DigitalTwinsClientTests.cs b/iothub/service/tests/DigitalTwinsClientTests.cs
new file mode 100644
index 0000000000..e0b8b028fb
--- /dev/null
+++ b/iothub/service/tests/DigitalTwinsClientTests.cs
@@ -0,0 +1,482 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure;
+using FluentAssertions;
+using Microsoft.Azure.Amqp;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Newtonsoft.Json;
+
+namespace Microsoft.Azure.Devices.Tests
+{
+ [TestClass]
+ [TestCategory("Unit")]
+ public class DigitalTwinsClientTests
+ {
+ private const string HostName = "contoso.azure-devices.net";
+ private static readonly string s_validMockAuthenticationHeaderValue = $"SharedAccessSignature sr={HostName}&sig=thisIsFake&se=000000&skn=registryRead";
+ private static readonly string s_connectionString = $"HostName={HostName};SharedAccessKeyName=iothubowner;SharedAccessKey=dGVzdFN0cmluZzE=";
+ private static readonly string s_Etag = "\"AAAAAAAAAAE=\"";
+
+
+ private static readonly Uri s_httpUri = new($"https://{HostName}");
+ private static readonly RetryHandler s_retryHandler = new(new IotHubServiceNoRetry());
+ private static IotHubServiceClientOptions s_options = new()
+ {
+ Protocol = IotHubTransportProtocol.Tcp,
+ RetryPolicy = new IotHubServiceNoRetry()
+ };
+
+ private static readonly string s_expectedLocation = "https://contoso.azure-devices.net/digitaltwins/foo?api-version=2021-04-12";
+
+ [TestMethod]
+ public async Task DigitalTwinsClient_GetAsync()
+ {
+ // arrange
+ string digitalTwinId = "foo";
+ var digitalTwin = new BasicDigitalTwin
+ {
+ Id = digitalTwinId,
+ };
+
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+ var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, "");
+
+ using var mockHttpResponse = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = HttpMessageHelper.SerializePayload(digitalTwin),
+ };
+ mockHttpResponse.Headers.Add("ETag", s_Etag);
+
+ var mockHttpClient = new Mock();
+ mockHttpClient
+ .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(mockHttpResponse);
+
+ var digitalTwinsClient = new DigitalTwinsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory,
+ s_retryHandler);
+
+ // act
+ DigitalTwinGetResponse result = await digitalTwinsClient.GetAsync(digitalTwinId).ConfigureAwait(false);
+
+ // assert
+ result.DigitalTwin.Id.Should().Be(digitalTwinId);
+ result.ETag.ToString().Should().Be(s_Etag);
+ }
+
+ [TestMethod]
+ public async Task DigitalTwinsClient_GetAsync_EmptyTwinIdThrows()
+ {
+ // arrange
+ string digitalTwinId = null;
+ using var serviceClient = new IotHubServiceClient(s_connectionString);
+
+ // act
+ Func act = async () => await serviceClient.DigitalTwins.GetAsync(digitalTwinId);
+
+ // assert
+ await act.Should().ThrowAsync();
+ }
+
+ [TestMethod]
+ public async Task DigitalTwinsClient_GetAsync_DigitalTwinNotFound_ThrowsIotHubServiceException()
+ {
+ // arrange
+ string digitalTwinId = "foo";
+ var digitalTwin = new BasicDigitalTwin
+ {
+ Id = digitalTwinId,
+ };
+ var responseMessage = new ErrorPayload2
+ {
+ Message = "test",
+ ExceptionMessage = "test"
+ };
+
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+ var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, "");
+
+ using var mockHttpResponse = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.NotFound,
+ Content = HttpMessageHelper.SerializePayload(responseMessage),
+ };
+
+ var mockHttpClient = new Mock();
+ mockHttpClient
+ .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(mockHttpResponse);
+
+ var digitalTwinsClient = new DigitalTwinsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await digitalTwinsClient.GetAsync(digitalTwinId);
+
+ // assert
+ var error = await act.Should().ThrowAsync();
+ error.And.IsTransient.Should().BeFalse();
+ error.And.StatusCode.Should().Be(HttpStatusCode.NotFound);
+ }
+
+ [TestMethod]
+ public async Task DigitalTwinsClient_UpdateAsync()
+ {
+ // arrange
+ string digitalTwinId = "foo";
+ var digitalTwin = new BasicDigitalTwin
+ {
+ Id = digitalTwinId,
+ };
+ var contents = new Dictionary
+ {
+ { "temperature", 8 }
+ };
+ var jsonPatch = new JsonPatchDocument();
+ jsonPatch.AppendAdd("bar", contents);
+
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+ var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, "");
+
+ using var mockHttpResponse = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.Accepted,
+ Content = HttpMessageHelper.SerializePayload(digitalTwin),
+ };
+ mockHttpResponse.Headers.Add("ETag", s_Etag);
+ mockHttpResponse.Headers.Add("Location", s_expectedLocation);
+
+ var mockHttpClient = new Mock();
+ mockHttpClient
+ .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(mockHttpResponse);
+
+ var digitalTwinsClient = new DigitalTwinsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory,
+ s_retryHandler);
+
+ // act
+ DigitalTwinUpdateResponse response = await digitalTwinsClient.UpdateAsync(digitalTwinId, jsonPatch.ToString());
+
+ // assert
+ response.Location.Should().Be(s_expectedLocation);
+ response.ETag.ToString().Should().Be(s_Etag);
+ }
+
+ [TestMethod]
+ [DataRow(null, "foo")]
+ [DataRow("bar", null)]
+ [DataRow(null, null)]
+ public async Task DigitalTwinsClient_UpdateAsync_NullParamater_Throws(string digitalTwinId, string jsonPatch)
+ {
+ // arrange
+ using var serviceClient = new IotHubServiceClient(s_connectionString);
+
+ // act
+ Func act = async () => await serviceClient.DigitalTwins.UpdateAsync(digitalTwinId, jsonPatch);
+
+ // assert
+ await act.Should().ThrowAsync();
+ }
+
+ [TestMethod]
+ public async Task DigitalTwinsClient_UpdateAsync_DigitalTwinNotFound_ThrowsIotHubServiceException()
+ {
+ // arrange
+ string jsonPatch = "test";
+ string digitalTwinId = "foo";
+ var digitalTwin = new BasicDigitalTwin
+ {
+ Id = digitalTwinId,
+ };
+ var responseMessage = new ErrorPayload2
+ {
+ Message = "test",
+ ExceptionMessage = "test"
+ };
+
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+ var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, "");
+
+ using var mockHttpResponse = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.NotFound,
+ Content = HttpMessageHelper.SerializePayload(responseMessage),
+ };
+
+ var mockHttpClient = new Mock();
+ mockHttpClient
+ .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(mockHttpResponse);
+
+ var digitalTwinsClient = new DigitalTwinsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await digitalTwinsClient.UpdateAsync(digitalTwinId, jsonPatch);
+
+ // assert
+ var error = await act.Should().ThrowAsync();
+ error.And.IsTransient.Should().BeFalse();
+ error.And.StatusCode.Should().Be(HttpStatusCode.NotFound);
+ }
+
+ [TestMethod]
+ public async Task DigitalTwinsClient_InvokeCommandAsync()
+ {
+ // arrange
+ string digitalTwinId = "foo";
+ string commandName = "foo";
+ var digitalTwin = new BasicDigitalTwin
+ {
+ Id = digitalTwinId,
+ };
+
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+ var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, "");
+
+ using var mockHttpResponse = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = HttpMessageHelper.SerializePayload(digitalTwin),
+ };
+ mockHttpResponse.Headers.Add("x-ms-command-statuscode", "200");
+ mockHttpResponse.Headers.Add("x-ms-request-id", "201");
+
+ var mockHttpClient = new Mock();
+ mockHttpClient
+ .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(mockHttpResponse);
+
+ var digitalTwinsClient = new DigitalTwinsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory,
+ s_retryHandler);
+
+ // act
+ InvokeDigitalTwinCommandResponse response = await digitalTwinsClient.InvokeCommandAsync(digitalTwinId, commandName).ConfigureAwait(false);
+
+ // assert
+ response.Status.Should().Be(200);
+ }
+
+ [TestMethod]
+ [DataRow(null, "foo")]
+ [DataRow("bar", null)]
+ [DataRow(null, null)]
+ public async Task DigitalTwinsClient_InvokeCommandAsync_NullParamater_Throws(string digitalTwinId, string commandName)
+ {
+ // arrange
+ using var serviceClient = new IotHubServiceClient(s_connectionString);
+
+ // act
+ Func act = async () => await serviceClient.DigitalTwins.InvokeCommandAsync(digitalTwinId, commandName);
+
+ // assert
+ await act.Should().ThrowAsync();
+ }
+
+ [TestMethod]
+ public async Task DigitalTwinsClient_InvokeCommandAysnc_DigitalTwinNotFound_ThrowsIotHubServiceException()
+ {
+ // arrange
+ string commandName = "test";
+ string digitalTwinId = "foo";
+ var digitalTwin = new BasicDigitalTwin
+ {
+ Id = digitalTwinId,
+ };
+ var responseMessage = new ErrorPayload2
+ {
+ Message = "test",
+ ExceptionMessage = "test"
+ };
+
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+ var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, "");
+
+ using var mockHttpResponse = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.NotFound,
+ Content = HttpMessageHelper.SerializePayload(responseMessage),
+ };
+
+ var mockHttpClient = new Mock();
+ mockHttpClient
+ .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(mockHttpResponse);
+
+ var digitalTwinsClient = new DigitalTwinsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await digitalTwinsClient.InvokeCommandAsync(digitalTwinId, commandName);
+
+ // assert
+ var error = await act.Should().ThrowAsync();
+ error.And.IsTransient.Should().BeFalse();
+ error.And.StatusCode.Should().Be(HttpStatusCode.NotFound);
+ }
+
+ [TestMethod]
+ public async Task DigitalTwinsClient_InvokeComponentCommandAsync()
+ {
+ // arrange
+ string digitalTwinId = "foo";
+ string commandName = "foo";
+ string componentName = "bar";
+ var digitalTwin = new BasicDigitalTwin
+ {
+ Id = digitalTwinId,
+ };
+
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+ var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, "");
+
+ using var mockHttpResponse = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = HttpMessageHelper.SerializePayload(digitalTwin),
+ };
+ mockHttpResponse.Headers.Add("x-ms-command-statuscode", "200");
+ mockHttpResponse.Headers.Add("x-ms-request-id", "201");
+
+ var mockHttpClient = new Mock();
+ mockHttpClient
+ .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(mockHttpResponse);
+
+ var digitalTwinsClient = new DigitalTwinsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory,
+ s_retryHandler);
+
+ // act
+ InvokeDigitalTwinCommandResponse response = await digitalTwinsClient.InvokeComponentCommandAsync(digitalTwinId, componentName, commandName);
+
+ // assert
+ response.Status.Should().Be(200);
+ }
+
+ [TestMethod]
+ [DataRow(null, "foo", null)]
+ [DataRow("bar", null, null)]
+ [DataRow(null, null, "baz")]
+ [DataRow(null, null, null)]
+ public async Task DigitalTwinsClient_InvokeComponentCommandAsync_NullParamater_Throws(string digitalTwinId, string commandName, string componentName)
+ {
+ // arrange
+ using var serviceClient = new IotHubServiceClient(s_connectionString);
+
+ // act
+ Func act = async () => await serviceClient.DigitalTwins.InvokeComponentCommandAsync(digitalTwinId, componentName, commandName);
+
+ // assert
+ await act.Should().ThrowAsync();
+ }
+
+ [TestMethod]
+ public async Task DigitalTwinsClient_InvokeComponentCommandAsync_DigitalTwinNotFound_ThrowsIotHubServiceException()
+ {
+ // arrange
+ string commandName = "test";
+ string componentName = "test";
+ string digitalTwinId = "foo";
+ var digitalTwin = new BasicDigitalTwin
+ {
+ Id = digitalTwinId,
+ };
+ var responseMessage = new ErrorPayload2
+ {
+ Message = "test",
+ ExceptionMessage = "test"
+ };
+
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+ var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, "");
+
+ using var mockHttpResponse = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.NotFound,
+ Content = HttpMessageHelper.SerializePayload(responseMessage),
+ };
+
+ var mockHttpClient = new Mock();
+ mockHttpClient
+ .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(mockHttpResponse);
+
+ var digitalTwinsClient = new DigitalTwinsClient(
+ HostName,
+ mockCredentialProvider.Object,
+ mockHttpClient.Object,
+ mockHttpRequestFactory,
+ s_retryHandler);
+
+ // act
+ Func act = async () => await digitalTwinsClient.InvokeComponentCommandAsync(digitalTwinId, componentName, commandName);
+
+ // assert
+ var error = await act.Should().ThrowAsync();
+ error.And.IsTransient.Should().BeFalse();
+ error.And.StatusCode.Should().Be(HttpStatusCode.NotFound);
+ }
+ }
+}
diff --git a/iothub/service/tests/DirectMethod/DirectMethodClientResponseTests.cs b/iothub/service/tests/DirectMethod/DirectMethodClientResponseTests.cs
index d9c9350b4f..4781ebd804 100644
--- a/iothub/service/tests/DirectMethod/DirectMethodClientResponseTests.cs
+++ b/iothub/service/tests/DirectMethod/DirectMethodClientResponseTests.cs
@@ -14,13 +14,32 @@ namespace Microsoft.Azure.Devices.Tests.DirectMethod
[TestCategory("Unit")]
public class DirectMethodClientResponseTests
{
+ [TestMethod]
+ public void DirectMethodClientResponse_Get_PayloadAsString()
+ {
+ // arrage
+ const int expectedStatus = 200;
+ DateTimeOffset expectedPayload = DateTimeOffset.UtcNow;
+ var source = new DirectMethodClientResponse
+ {
+ Status = expectedStatus,
+ JsonPayload = new JRaw(JsonConvert.SerializeObject(expectedPayload)),
+ };
+
+ // act
+ string payloadAsStringDmcr = source.PayloadAsString;
+ string payloadAsStringManualConvert = source.JsonPayload.ToString();
+
+ // assert
+ payloadAsStringDmcr.Should().Be(payloadAsStringManualConvert);
+ }
+
[TestMethod]
public void DirectMethodClientResponse_Payload_DateTimeOffset()
{
// arrange
-
const int expectedStatus = 200;
- DateTimeOffset expectedPayload = DateTimeOffset.UtcNow;
+ var expectedPayload = DateTimeOffset.UtcNow;
var source = new DirectMethodClientResponse
{
Status = expectedStatus,
@@ -32,7 +51,6 @@ public void DirectMethodClientResponse_Payload_DateTimeOffset()
DirectMethodClientResponse dmcr = JsonConvert.DeserializeObject(body);
// assert
-
dmcr.Status.Should().Be(expectedStatus);
dmcr.TryGetPayload(out DateTimeOffset actual).Should().BeTrue();
actual.Should().Be(expectedPayload);
@@ -42,7 +60,6 @@ public void DirectMethodClientResponse_Payload_DateTimeOffset()
public void DirectMethodClientResponse_Payload_Int()
{
// arrange
-
const int expectedStatus = 200;
int expectedPayload = int.MaxValue;
var source = new DirectMethodClientResponse
@@ -56,7 +73,6 @@ public void DirectMethodClientResponse_Payload_Int()
DirectMethodClientResponse dmcr = JsonConvert.DeserializeObject(body);
// assert
-
dmcr.Status.Should().Be(expectedStatus);
dmcr.TryGetPayload(out int actual).Should().BeTrue();
actual.Should().Be(expectedPayload);
@@ -66,7 +82,6 @@ public void DirectMethodClientResponse_Payload_Int()
public void DirectMethodClientResponse_Payload_IntList()
{
// arrange
-
const int expectedStatus = 200;
var expectedPayload = new List { 1, 2, 3 };
var source = new DirectMethodClientResponse
@@ -80,7 +95,6 @@ public void DirectMethodClientResponse_Payload_IntList()
DirectMethodClientResponse dmcr = JsonConvert.DeserializeObject(body);
// assert
-
dmcr.Status.Should().Be(expectedStatus);
dmcr.TryGetPayload(out List actual).Should().BeTrue();
actual.Should().BeEquivalentTo(expectedPayload);
@@ -90,7 +104,6 @@ public void DirectMethodClientResponse_Payload_IntList()
public void DirectMethodClientResponse_Payload_Bool()
{
// arrange
-
const int expectedStatus = 200;
bool expectedPayload = true;
var source = new DirectMethodClientResponse
@@ -113,7 +126,6 @@ public void DirectMethodClientResponse_Payload_Bool()
public void DirectMethodClientResponse_Payload_String()
{
// arrange
-
const int expectedStatus = 200;
string expectedPayload = "The quick brown fox jumped over the lazy dog.";
var source = new DirectMethodClientResponse
@@ -127,7 +139,6 @@ public void DirectMethodClientResponse_Payload_String()
DirectMethodClientResponse dmcr = JsonConvert.DeserializeObject(body);
// assert
-
dmcr.Status.Should().Be(expectedStatus);
dmcr.TryGetPayload(out string actual).Should().BeTrue();
actual.Should().Be(expectedPayload);
@@ -137,7 +148,6 @@ public void DirectMethodClientResponse_Payload_String()
public void DirectMethodClientResponse_Payload_TimeSpan()
{
// arrange
-
const int expectedStatus = 200;
var expectedPayload = TimeSpan.FromSeconds(30);
var source = new DirectMethodClientResponse
@@ -151,7 +161,6 @@ public void DirectMethodClientResponse_Payload_TimeSpan()
DirectMethodClientResponse dmcr = JsonConvert.DeserializeObject(body);
// assert
-
dmcr.Status.Should().Be(expectedStatus);
dmcr.TryGetPayload(out TimeSpan actual).Should().BeTrue();
actual.Should().Be(expectedPayload);
@@ -161,7 +170,6 @@ public void DirectMethodClientResponse_Payload_TimeSpan()
public void DirectMethodClientResponse_Payload_CustomType()
{
// arrange
-
const int expectedStatus = 200;
var expectedPayload = new CustomType { CustomInt = 4, CustomString = "bar" };
var source = new DirectMethodClientResponse
@@ -175,13 +183,43 @@ public void DirectMethodClientResponse_Payload_CustomType()
DirectMethodClientResponse dmcr = JsonConvert.DeserializeObject(body);
// assert
-
dmcr.Status.Should().Be(expectedStatus);
dmcr.TryGetPayload(out CustomType actual).Should().BeTrue();
actual.CustomInt.Should().Be(expectedPayload.CustomInt);
actual.CustomString.Should().Be(expectedPayload.CustomString);
}
+ [TestMethod]
+ public void DirectMethodClientResponse_Payload_Null()
+ {
+ // arrange
+ const int expectedStatus = 200;
+ var source = new DirectMethodClientResponse
+ {
+ Status = expectedStatus,
+ JsonPayload = null,
+ };
+
+ // act and assert
+ source.TryGetPayload(out string _).Should().BeFalse();
+ }
+
+ [TestMethod]
+ public void DirectMethodClientResponse_Payload_ThrowsException()
+ {
+ // arrange
+ const int expectedStatus = 200;
+ var source = new DirectMethodClientResponse
+ {
+ Status = expectedStatus,
+ JsonPayload = new JRaw(JsonConvert.SerializeObject(TimeSpan.FromSeconds(30)))
+ };
+
+ // act and assert
+ // deliberately throw serialzation exception to ensure TryGetPayload() returns false
+ source.TryGetPayload(out string[] _).Should().BeFalse();
+ }
+
private class CustomType
{
[JsonProperty("customInt")]
diff --git a/iothub/service/tests/DirectMethod/DirectMethodServiceRequestTests.cs b/iothub/service/tests/DirectMethod/DirectMethodServiceRequestTests.cs
index ecfcd0b55b..b399743401 100644
--- a/iothub/service/tests/DirectMethod/DirectMethodServiceRequestTests.cs
+++ b/iothub/service/tests/DirectMethod/DirectMethodServiceRequestTests.cs
@@ -39,5 +39,40 @@ public void DirectMethodServiceRequest_Ctor_SetsMethodName()
// assert
dmcr.MethodName.Should().Be(expected);
}
+
+ [TestMethod]
+ public void DirectMethodServiceRequest_ConnectionResponseTimeout()
+ {
+ // arrange
+ var expectedTimeout = TimeSpan.FromSeconds(1);
+ var dcmr = new DirectMethodServiceRequest("setTelemetryInterval")
+ {
+ ConnectionTimeout = expectedTimeout,
+ ResponseTimeout = expectedTimeout,
+ Payload = "test"
+ };
+
+ // act + assert
+ dcmr.ConnectionTimeout.Should().Be(expectedTimeout);
+ dcmr.ResponseTimeout.Should().Be(expectedTimeout);
+ dcmr.Payload.Should().Be("test");
+
+ dcmr.ResponseTimeoutInSeconds.Should().Be(1);
+ dcmr.ConnectionTimeoutInSeconds.Should().Be(1);
+ }
+
+ [TestMethod]
+ public void DirectMethodServiceRequest_ConnectionResponseTimeout_ShouldBeNull()
+ {
+ // arrange
+ var expectedTimeout = TimeSpan.FromSeconds(1);
+ var dcmr = new DirectMethodServiceRequest("123")
+ {
+ Payload = "test"
+ };
+
+ dcmr.ResponseTimeoutInSeconds.Should().Be(null);
+ dcmr.ConnectionTimeoutInSeconds.Should().Be(null);
+ }
}
}
diff --git a/iothub/service/tests/ExceptionHandlingHelperTests.cs b/iothub/service/tests/Exceptions/ExceptionHandlingHelperTests.cs
similarity index 99%
rename from iothub/service/tests/ExceptionHandlingHelperTests.cs
rename to iothub/service/tests/Exceptions/ExceptionHandlingHelperTests.cs
index 20fe20c72e..9f32a604cd 100644
--- a/iothub/service/tests/ExceptionHandlingHelperTests.cs
+++ b/iothub/service/tests/Exceptions/ExceptionHandlingHelperTests.cs
@@ -9,7 +9,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
-namespace Microsoft.Azure.Devices.Tests
+namespace Microsoft.Azure.Devices.Tests.Exceptions
{
[TestClass]
[TestCategory("Unit")]
diff --git a/iothub/service/tests/Exceptions/IotHubServiceExceptionTests.cs b/iothub/service/tests/Exceptions/IotHubServiceExceptionTests.cs
new file mode 100644
index 0000000000..c94387a9e8
--- /dev/null
+++ b/iothub/service/tests/Exceptions/IotHubServiceExceptionTests.cs
@@ -0,0 +1,154 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Net;
+using System.Runtime.Serialization;
+using FluentAssertions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.Azure.Devices.Tests.Exceptions
+{
+ [TestClass]
+ [TestCategory("Unit")]
+ public class IotHubServiceExceptionTests
+ {
+ private const string Message = "sample message";
+
+ [TestMethod]
+ [DataRow(IotHubServiceErrorCode.DeviceNotFound)]
+ [DataRow(IotHubServiceErrorCode.InvalidProtocolVersion)]
+ [DataRow(IotHubServiceErrorCode.Unknown)]
+ [DataRow(IotHubServiceErrorCode.InvalidOperation)]
+ [DataRow(IotHubServiceErrorCode.ArgumentInvalid)]
+ [DataRow(IotHubServiceErrorCode.ArgumentNull)]
+ [DataRow(IotHubServiceErrorCode.IotHubFormatError)]
+ [DataRow(IotHubServiceErrorCode.DeviceDefinedMultipleTimes)]
+ [DataRow(IotHubServiceErrorCode.ModuleNotFound)]
+ [DataRow(IotHubServiceErrorCode.BulkRegistryOperationFailure)]
+ [DataRow(IotHubServiceErrorCode.IotHubSuspended)]
+ [DataRow(IotHubServiceErrorCode.IotHubUnauthorizedAccess)]
+ [DataRow(IotHubServiceErrorCode.DeviceMaximumQueueDepthExceeded)]
+ [DataRow(IotHubServiceErrorCode.DeviceAlreadyExists)]
+ [DataRow(IotHubServiceErrorCode.ModuleAlreadyExistsOnDevice)]
+ [DataRow(IotHubServiceErrorCode.MessageTooLarge)]
+ [DataRow(IotHubServiceErrorCode.TooManyDevices)]
+ [DataRow(IotHubServiceErrorCode.PreconditionFailed)]
+ public void IotHubServiceException_Ctor_WithNonTransientErrorCode_Ok(IotHubServiceErrorCode errorCode)
+ {
+ // arrange - act
+ HttpStatusCode statusCode = HttpStatusCode.Accepted; // set for simplicity
+
+ var exception = new IotHubServiceException(
+ Message,
+ statusCode,
+ errorCode);
+
+ // assert
+ exception.StatusCode.Should().Be(statusCode);
+ exception.Message.Should().Be(Message);
+ exception.IsTransient.Should().BeFalse();
+ exception.TrackingId.Should().BeNull();
+ exception.ErrorCode.Should().Be(errorCode);
+ }
+
+ [TestMethod]
+ [DataRow(IotHubServiceErrorCode.DeviceNotOnline)]
+ [DataRow(IotHubServiceErrorCode.ServerError)]
+ [DataRow(IotHubServiceErrorCode.IotHubQuotaExceeded)]
+ [DataRow(IotHubServiceErrorCode.ServiceUnavailable)]
+ [DataRow(IotHubServiceErrorCode.ThrottlingException)]
+ public void IotHubServiceException_Ctor_WithTransientErrorCode_Ok(IotHubServiceErrorCode errorCode)
+ {
+ // arrange - act
+ HttpStatusCode statusCode = HttpStatusCode.Accepted; // set for simplicity
+
+ var exception = new IotHubServiceException(
+ Message,
+ statusCode,
+ errorCode);
+
+ // assert
+ exception.StatusCode.Should().Be(statusCode);
+ exception.Message.Should().Be(Message);
+ exception.IsTransient.Should().BeTrue();
+ exception.TrackingId.Should().BeNull();
+ exception.ErrorCode.Should().Be(errorCode);
+ }
+
+ [TestMethod]
+ public void IotHubServiceException_UntrackedErrorCode_NotHttpStatusCodeRequestTimeout_IsNotTransient()
+ {
+ // arrange - act
+ HttpStatusCode statusCode = HttpStatusCode.MovedPermanently; // not HttpStatusCode.RequestTimeout
+
+ var exception = new IotHubServiceException(
+ Message,
+ statusCode,
+ IotHubServiceErrorCode.Unknown);
+
+ // assert
+ exception.StatusCode.Should().Be(statusCode);
+ exception.Message.Should().Be(Message);
+ exception.IsTransient.Should().BeFalse();
+ exception.TrackingId.Should().BeNull();
+ }
+
+ [TestMethod]
+ public void IotHubServiceException_UntrackedErrorCode_HttpStatusCodeRequestTimeout_IsTransient()
+ {
+ // arrange - act
+ HttpStatusCode statusCode = HttpStatusCode.RequestTimeout;
+
+ var exception = new IotHubServiceException(
+ Message,
+ statusCode,
+ IotHubServiceErrorCode.Unknown);
+
+ // assert
+ exception.StatusCode.Should().Be(statusCode);
+ exception.Message.Should().Be(Message);
+ exception.IsTransient.Should().BeTrue();
+ exception.TrackingId.Should().BeNull();
+ }
+
+ [TestMethod]
+ public void IotHubServiceException_GetObjectData_NullInfoThrows()
+ {
+ // arrange - act
+ HttpStatusCode statusCode = HttpStatusCode.RequestTimeout;
+
+ var exception = new IotHubServiceException(
+ Message,
+ statusCode,
+ IotHubServiceErrorCode.Unknown);
+ var sctx = new StreamingContext();
+
+ // act
+ Action act = () => exception.GetObjectData(null, sctx);
+
+ // assert
+ act.Should().Throw();
+ }
+
+ [TestMethod]
+ public void IotHubServiceException_GetObjectData_Ok()
+ {
+ // arrange - act
+ HttpStatusCode statusCode = HttpStatusCode.RequestTimeout;
+
+ var exception = new IotHubServiceException(
+ Message,
+ statusCode,
+ IotHubServiceErrorCode.Unknown);
+ var sInfo = new SerializationInfo(GetType(), new FormatterConverter());
+ var sctx = new StreamingContext();
+
+ // act
+ Action act = () => exception.GetObjectData(sInfo, sctx);
+
+ // assert
+ act.Should().NotThrow(); // With proper parameters, act should not throw
+ }
+ }
+}
diff --git a/iothub/service/tests/Feedback/FeedbackBatchTests.cs b/iothub/service/tests/Feedback/FeedbackBatchTests.cs
new file mode 100644
index 0000000000..d236de6208
--- /dev/null
+++ b/iothub/service/tests/Feedback/FeedbackBatchTests.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.Azure.Amqp;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.Azure.Devices.Tests.Feedback
+{
+ [TestClass]
+ [TestCategory("Unit")]
+ public class FeedbackBatchTests
+ {
+ [TestMethod]
+ public void FeedBackBatch_PropertySetsAndGets()
+ {
+ // arrange
+ FeedbackRecord[] feedbackRecords =
+ {
+ new FeedbackRecord(),
+ new FeedbackRecord()
+ };
+
+ var expectedEnqueuedOnUtc = new DateTime(2008, 5, 1, 8, 30, 52);
+ string expectedHubName = "testhub";
+
+ var feedbackBatch = new FeedbackBatch
+ {
+ EnqueuedOnUtc = expectedEnqueuedOnUtc,
+ Records = feedbackRecords,
+ IotHubHostName = expectedHubName
+ };
+
+ // assert
+ feedbackBatch.EnqueuedOnUtc.Should().Be(expectedEnqueuedOnUtc);
+ feedbackBatch.Records.Should().HaveCount(feedbackRecords.Length);
+ feedbackBatch.IotHubHostName.Should().Be(expectedHubName);
+ }
+ }
+}
diff --git a/iothub/service/tests/Feedback/MessageFeedbackProcessorClientTests.cs b/iothub/service/tests/Feedback/MessageFeedbackProcessorClientTests.cs
new file mode 100644
index 0000000000..9652e3021f
--- /dev/null
+++ b/iothub/service/tests/Feedback/MessageFeedbackProcessorClientTests.cs
@@ -0,0 +1,113 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.Azure.Devices.Amqp;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.Azure.Devices.Tests.Feedback
+{
+ [TestClass]
+ [TestCategory("Unit")]
+ public class MessageFeedbackProcessorClientTests
+ {
+ private const string HostName = "contoso.azure-devices.net";
+ private static readonly string s_connectionString = $"HostName={HostName};SharedAccessKeyName=iothubowner;SharedAccessKey=dGVzdFN0cmluZzE=";
+ private static readonly string s_validMockAuthenticationHeaderValue = $"SharedAccessSignature sr={HostName}&sig=thisIsFake&se=000000&skn=registryRead";
+
+ private static IIotHubServiceRetryPolicy noRetryPolicy = new IotHubServiceNoRetry();
+ private static IotHubServiceClientOptions s_options = new()
+ {
+ Protocol = IotHubTransportProtocol.Tcp,
+ RetryPolicy = noRetryPolicy
+ };
+ private static readonly RetryHandler s_retryHandler = new(new IotHubServiceNoRetry());
+
+ [TestMethod]
+ public async Task MessageFeedbackProcessorClient_OpenAsync_NotSettingMessageFeedbackProcessorThrows()
+ {
+ // arrange
+ using var serviceClient = new IotHubServiceClient(
+ s_connectionString,
+ s_options);
+
+ // act
+ Func act = async () => await serviceClient.MessageFeedback.OpenAsync().ConfigureAwait(false);
+
+ // assert
+ await act.Should().ThrowAsync();
+ }
+
+ [TestMethod]
+ public async Task MessageFeedbackProcessorClient_OpenAsync_Cancelled_ThrowsOperationCanceledException()
+ {
+ // arrange
+ using var serviceClient = new IotHubServiceClient(
+ s_connectionString,
+ s_options);
+
+ // act
+ var ct = new CancellationToken(true);
+ Func act = async () => await serviceClient.MessageFeedback.OpenAsync(ct);
+
+ // assert
+ await act.Should().ThrowAsync();
+ }
+
+ [TestMethod]
+ public async Task MessageFeedbackProcessorClient_OpenAsync_Ok()
+ {
+ // arrange
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+
+ var mockAmqpConnectionHandler = new Mock();
+
+ mockAmqpConnectionHandler
+ .Setup(x => x.OpenAsync(It.IsAny()))
+ .Returns(Task.CompletedTask);
+
+ using var messageFeedbackProcessorClient = new MessageFeedbackProcessorClient(
+ HostName,
+ mockCredentialProvider.Object,
+ s_options,
+ s_retryHandler,
+ mockAmqpConnectionHandler.Object);
+
+ AcknowledgementType messageFeedbackProcessor(FeedbackBatch FeedbackBatch) => AcknowledgementType.Complete;
+
+ messageFeedbackProcessorClient.MessageFeedbackProcessor = messageFeedbackProcessor;
+
+ var ct = new CancellationToken(false);
+
+ // act
+ Func act = async () => await messageFeedbackProcessorClient.OpenAsync(ct).ConfigureAwait(false);
+
+ // assert
+ await act.Should().NotThrowAsync().ConfigureAwait(false);
+ mockAmqpConnectionHandler.Verify(x => x.OpenAsync(ct), Times.Once());
+ }
+
+ [TestMethod]
+ public async Task MessageFeedbackProcessorClient_CloseAsync_Cancelled_ThrowsOperationCanceledException()
+ {
+ // arrange
+ using var serviceClient = new IotHubServiceClient(
+ s_connectionString,
+ s_options);
+
+ // act
+ var ct = new CancellationToken(true);
+ Func act = async () => await serviceClient.MessageFeedback.CloseAsync(ct);
+
+ // assert
+ await act.Should().ThrowAsync();
+ }
+ }
+}
diff --git a/iothub/service/tests/FileUpload/FileUploadNotificationProcessorClientTests.cs b/iothub/service/tests/FileUpload/FileUploadNotificationProcessorClientTests.cs
new file mode 100644
index 0000000000..d46931a7f3
--- /dev/null
+++ b/iothub/service/tests/FileUpload/FileUploadNotificationProcessorClientTests.cs
@@ -0,0 +1,154 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.Azure.Devices.Amqp;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Microsoft.Azure.Devices.Tests.FileUpload
+{
+ [TestClass]
+ [TestCategory("Unit")]
+ public class FileUploadNotificationProcessorClientTests
+ {
+ private const string HostName = "contoso.azure-devices.net";
+ private static readonly string s_connectionString = $"HostName={HostName};SharedAccessKeyName=iothubowner;SharedAccessKey=dGVzdFN0cmluZzE=";
+ private static readonly string s_validMockAuthenticationHeaderValue = $"SharedAccessSignature sr={HostName}&sig=thisIsFake&se=000000&skn=registryRead";
+
+ private static IIotHubServiceRetryPolicy noRetryPolicy = new IotHubServiceNoRetry();
+ private static IotHubServiceClientOptions s_options = new ()
+ {
+ Protocol = IotHubTransportProtocol.Tcp,
+ RetryPolicy = noRetryPolicy
+ };
+
+ private static readonly Uri s_httpUri = new($"https://{HostName}");
+ private static readonly RetryHandler s_retryHandler = new(new IotHubServiceNoRetry());
+
+ [TestMethod]
+ public async Task FileUploadNotificationProcessorClient_OpenAsync_NotSettingFileUploadNotificationProcessorThrows()
+ {
+ // arrange
+ using var serviceClient = new IotHubServiceClient(
+ s_connectionString,
+ s_options);
+
+ // act
+ Func act = async () => await serviceClient.FileUploadNotifications.OpenAsync().ConfigureAwait(false);
+
+ // assert
+ await act.Should().ThrowAsync();
+ }
+
+ [TestMethod]
+ public async Task FileUploadNotificationProcessorClient_OpenAsync_Cancelled_ThrowsOperationCanceledException()
+ {
+ // arrange
+ using var serviceClient = new IotHubServiceClient(
+ s_connectionString,
+ s_options);
+
+ AcknowledgementType OnFileUploadNotificationReceived(FileUploadNotification fileUploadNotification) => AcknowledgementType.Abandon;
+
+ serviceClient.FileUploadNotifications.FileUploadNotificationProcessor = OnFileUploadNotificationReceived;
+
+ // act
+ var ct = new CancellationToken(true);
+ Func act = async () => await serviceClient.FileUploadNotifications.OpenAsync(ct);
+
+ // assert
+ await act.Should().ThrowAsync();
+ }
+
+ [TestMethod]
+ public async Task FileUploadNotificationProcessorClient_CloseAsync_Cancelled_ThrowsOperationCanceledException()
+ {
+ // arrange
+ using var serviceClient = new IotHubServiceClient(
+ s_connectionString,
+ s_options);
+
+ AcknowledgementType OnFileUploadNotificationReceived(FileUploadNotification fileUploadNotification) => AcknowledgementType.Abandon;
+
+ serviceClient.FileUploadNotifications.FileUploadNotificationProcessor = OnFileUploadNotificationReceived;
+
+ // act
+ var ct = new CancellationToken(true);
+ Func act = async () => await serviceClient.FileUploadNotifications.CloseAsync(ct);
+
+ // assert
+ await act.Should().ThrowAsync();
+ }
+
+ [TestMethod]
+ public async Task FileUploadNotificationProcessorClient_OpenAsync()
+ {
+ // arrange
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+
+ var mockAmqpConnectionHandler = new Mock();
+
+ mockAmqpConnectionHandler
+ .Setup(x => x.OpenAsync(It.IsAny()))
+ .Returns(Task.CompletedTask);
+
+ using var fileUploadNotificationProcessorClient = new FileUploadNotificationProcessorClient(
+ HostName,
+ mockCredentialProvider.Object,
+ s_retryHandler,
+ mockAmqpConnectionHandler.Object);
+
+ AcknowledgementType OnFileUploadNotificationReceived(FileUploadNotification fileUploadNotification) => AcknowledgementType.Abandon;
+
+ var ct = new CancellationToken(false);
+
+ fileUploadNotificationProcessorClient.FileUploadNotificationProcessor = OnFileUploadNotificationReceived;
+
+ // act
+ Func act = async () => await fileUploadNotificationProcessorClient.OpenAsync().ConfigureAwait(false);
+
+ // assert
+ await act.Should().NotThrowAsync().ConfigureAwait(false);
+ mockAmqpConnectionHandler.Verify(x => x.OpenAsync(ct), Times.Once());
+ }
+
+ [TestMethod]
+ public async Task FileUploadNotificationProcessorClient_CloseAsync()
+ {
+ // arrange
+ var mockCredentialProvider = new Mock();
+ mockCredentialProvider
+ .Setup(getCredential => getCredential.GetAuthorizationHeader())
+ .Returns(s_validMockAuthenticationHeaderValue);
+
+ var mockAmqpConnectionHandler = new Mock();
+
+ mockAmqpConnectionHandler
+ .Setup(x => x.CloseAsync(It.IsAny()))
+ .Returns(Task.CompletedTask);
+
+ using var fileUploadNotificationProcessorClient = new FileUploadNotificationProcessorClient(
+ HostName,
+ mockCredentialProvider.Object,
+ s_retryHandler,
+ mockAmqpConnectionHandler.Object);
+
+ var ct = new CancellationToken(false);
+
+ // act
+ Func act = async () => await fileUploadNotificationProcessorClient.CloseAsync(ct).ConfigureAwait(false);
+
+ // assert
+ await act.Should().NotThrowAsync().ConfigureAwait(false);
+ mockAmqpConnectionHandler.Verify(x => x.CloseAsync(ct), Times.Once());
+ }
+ }
+}
diff --git a/iothub/service/tests/IotHubConnectionPropertiesTests.cs b/iothub/service/tests/IotHubConnectionPropertiesTests.cs
index 3ba9c652e6..69dff6e445 100644
--- a/iothub/service/tests/IotHubConnectionPropertiesTests.cs
+++ b/iothub/service/tests/IotHubConnectionPropertiesTests.cs
@@ -5,6 +5,7 @@
using System.Text;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
namespace Microsoft.Azure.Devices.Tests
{
@@ -26,5 +27,32 @@ public void IotHubConnectionPropertiesGetHubNameTest(string hostName, string exp
// assert
hubName.Should().Be(expectedHubName);
}
+
+ [TestMethod]
+ public void IotHubConnectionStringProperties_InvalidHostnameFormat()
+ {
+ // arrange
+ // invalid hostname format
+ string hostname = "5acme";
+
+ //act
+ Action act = () => _ = IotHubConnectionProperties.GetIotHubName(hostname);
+
+ // assert
+ act.Should().Throw();
+ }
+
+ [TestMethod]
+ public void IotHubConnectionPropertiesPropertiesAreSet()
+ {
+ // arrange - act
+ string hostName = "acme.azure-devices.net";
+ var connectionProperties = new Mock(hostName);
+
+ // assert
+ connectionProperties.Object.HostName.Should().NotBeNull();
+ connectionProperties.Object.IotHubName.Should().NotBeNull();
+ connectionProperties.Object.AmqpAudience.Should().NotBeNull();
+ }
}
}
diff --git a/iothub/service/tests/IotHubSasCredentialPropertiesTests.cs b/iothub/service/tests/IotHubSasCredentialPropertiesTests.cs
index 6f7571341d..8f8e3b9053 100644
--- a/iothub/service/tests/IotHubSasCredentialPropertiesTests.cs
+++ b/iothub/service/tests/IotHubSasCredentialPropertiesTests.cs
@@ -52,7 +52,6 @@ public async Task TestCbsTokenGeneration_Succeeds()
var iotHubSasCredentialProperties = new IotHubSasCredentialProperties(_hostName, azureSasCredential);
// act
-
CbsToken cbsToken = await iotHubSasCredentialProperties.GetTokenAsync(null, null, null).ConfigureAwait(false);
azureSasCredential.Update(updatedToken);
CbsToken updatedCbsToken = await iotHubSasCredentialProperties.GetTokenAsync(null, null, null).ConfigureAwait(false);
@@ -76,18 +75,11 @@ public async Task TestCbsTokenGeneration_InvalidExpirationDateTimeFormat_Fails()
var azureSasCredential = new AzureSasCredential(token);
var iotHubSasCredentialProperties = new IotHubSasCredentialProperties(_hostName, azureSasCredential);
- try
- {
- // act
- await iotHubSasCredentialProperties.GetTokenAsync(null, null, null).ConfigureAwait(false);
-
- Assert.Fail("The parsing of seconds from string to long should have caused an exception.");
- }
- catch (InvalidOperationException ex)
- {
- // assert
- ex.Message.Should().Be($"Invalid seconds from epoch time on {nameof(AzureSasCredential)} signature.");
- }
+ // act
+ Func