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 act = async () => await iotHubSasCredentialProperties.GetTokenAsync(null, null, null).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync(); } [TestMethod] @@ -103,18 +95,29 @@ public async Task TestCbsTokenGeneration_MissingExpiration_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 missing expiry on the SAS token should have caused an exception."); - } - catch (InvalidOperationException ex) - { - // assert - ex.Message.Should().Be($"There is no expiration time on {nameof(AzureSasCredential)} signature."); - } + // act + Func act = async () => await iotHubSasCredentialProperties.GetTokenAsync(null, null, null).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync(); + } + + [TestMethod] + public void TestCbsTokenGeneration_GetAuthorizationHeader_Validate() + { + // arrange + string token = string.Format( + CultureInfo.InvariantCulture, + "SharedAccessSignature sr={0}&sig={1}", + WebUtility.UrlEncode(_hostName), + WebUtility.UrlEncode("signature")); + + // act + var azureSasCredential = new AzureSasCredential(token); + var iotHubSasCredentialProperties = new IotHubSasCredentialProperties(_hostName, azureSasCredential); + + // assert + iotHubSasCredentialProperties.GetAuthorizationHeader().Should().Be($"SharedAccessSignature sr={_hostName}&sig=signature"); } } } diff --git a/iothub/service/tests/IotHubServiceClientTests.cs b/iothub/service/tests/IotHubServiceClientTests.cs new file mode 100644 index 0000000000..84c7549234 --- /dev/null +++ b/iothub/service/tests/IotHubServiceClientTests.cs @@ -0,0 +1,111 @@ +// 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.Tasks; +using Azure; +using FluentAssertions; +using Microsoft.Azure.Amqp; +using Microsoft.Azure.Devices; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Microsoft.Azure.Devices.Tests +{ + [TestClass] + [TestCategory("Unit")] + public class IotHubServiceClientTests + { + [TestMethod] + public void IotHubServiceClient_SubClients_NotNull() + { + // arrange - act + string cs = "HostName=acme.azure-devices.net;SharedAccessKeyName=AllAccessKey;SharedAccessKey=dGVzdFN0cmluZzE="; + using var serviceClient = new IotHubServiceClient(cs); + + // assert + serviceClient.Devices.Should().NotBeNull(); + serviceClient.Modules.Should().NotBeNull(); + serviceClient.Configurations.Should().NotBeNull(); + serviceClient.DirectMethods.Should().NotBeNull(); + serviceClient.Query.Should().NotBeNull(); + serviceClient.ScheduledJobs.Should().NotBeNull(); + serviceClient.DigitalTwins.Should().NotBeNull(); + serviceClient.Twins.Should().NotBeNull(); + serviceClient.MessageFeedback.Should().NotBeNull(); + serviceClient.FileUploadNotifications.Should().NotBeNull(); + serviceClient.Messages.Should().NotBeNull(); + } + + [TestMethod] + public void IotHubServiceClient_CreateSubClientsWithAad() + { + // arrange + string hostName = "acme.azure-devices.net"; + var tokenCrediential = new TestTokenCredential(new DateTimeOffset(DateTime.Now + TimeSpan.FromHours(1))); + + // act + using var serviceClient = new IotHubServiceClient(hostName, tokenCrediential); + + // assert + serviceClient.Devices.Should().NotBeNull(); + serviceClient.Modules.Should().NotBeNull(); + serviceClient.Configurations.Should().NotBeNull(); + serviceClient.DirectMethods.Should().NotBeNull(); + serviceClient.Query.Should().NotBeNull(); + serviceClient.ScheduledJobs.Should().NotBeNull(); + serviceClient.DigitalTwins.Should().NotBeNull(); + serviceClient.Twins.Should().NotBeNull(); + serviceClient.MessageFeedback.Should().NotBeNull(); + serviceClient.FileUploadNotifications.Should().NotBeNull(); + serviceClient.Messages.Should().NotBeNull(); + } + + [TestMethod] + public void IotHubServiceClient_CreateSubClientsWithSasToken() + { + // arrange + string hostName = "acme.azure-devices.net"; + var sasCredential = new AzureSasCredential("test"); + + // act + using var serviceClient = new IotHubServiceClient(hostName, sasCredential); + + // assert + serviceClient.Devices.Should().NotBeNull(); + serviceClient.Modules.Should().NotBeNull(); + serviceClient.Configurations.Should().NotBeNull(); + serviceClient.DirectMethods.Should().NotBeNull(); + serviceClient.Query.Should().NotBeNull(); + serviceClient.ScheduledJobs.Should().NotBeNull(); + serviceClient.DigitalTwins.Should().NotBeNull(); + serviceClient.Twins.Should().NotBeNull(); + serviceClient.MessageFeedback.Should().NotBeNull(); + serviceClient.FileUploadNotifications.Should().NotBeNull(); + serviceClient.Messages.Should().NotBeNull(); + } + + [TestMethod] + public void IotHubServiceClient_NullParameters_Throws() + { + // arrange + string hostName = null; + var sasCredential = new AzureSasCredential("test"); + + // act + Action act = () => _ = new IotHubServiceClient(hostName, sasCredential); + + // assert + act.Should().Throw(); + + // rearrange + string connectionString = null; + + // act + act = () => _ = new IotHubServiceClient(connectionString); + + // assert + act.Should().Throw(); + } + } +} diff --git a/iothub/service/tests/Jobs/CloudToDeviceMethodScheduledJobTests.cs b/iothub/service/tests/Jobs/CloudToDeviceMethodScheduledJobTests.cs new file mode 100644 index 0000000000..bb7e8ea87d --- /dev/null +++ b/iothub/service/tests/Jobs/CloudToDeviceMethodScheduledJobTests.cs @@ -0,0 +1,29 @@ +// 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.Jobs +{ + [TestClass] + [TestCategory("Unit")] + public class CloudToDeviceMethodScheduledJobTests + { + [TestMethod] + public void CloudToDeviceMethodScheduledJob_Ctor_Ok() + { + // arrange - act + var request = new DirectMethodServiceRequest("TestMethod"); + var job = new CloudToDeviceMethodScheduledJob(request); + + // assert + job.DirectMethodRequest.Should().BeEquivalentTo(request); + } + } +} diff --git a/iothub/service/tests/Jobs/JobQueryOptionsTests.cs b/iothub/service/tests/Jobs/JobQueryOptionsTests.cs new file mode 100644 index 0000000000..67bc89c8c7 --- /dev/null +++ b/iothub/service/tests/Jobs/JobQueryOptionsTests.cs @@ -0,0 +1,39 @@ +// 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.Jobs +{ + [TestClass] + [TestCategory("Unit")] + public class JobQueryOptionsTests + { + [TestMethod] + public void JobQueryOptions_FieldValueInitializations() + { + // arrange + var options = new JobQueryOptions(); + + // assert + options.JobType.Should().BeNull(); + options.JobStatus.Should().BeNull(); + + // rearrange + var jobType = new JobType(); + var jobStatus = new JobStatus(); + options.JobType = jobType; + options.JobStatus = jobStatus; + + // reassert + options.JobType.Should().Be(jobType); + options.JobStatus.Should().Be(jobStatus); + } + } +} diff --git a/iothub/service/tests/Jobs/JobRequestTests.cs b/iothub/service/tests/Jobs/JobRequestTests.cs new file mode 100644 index 0000000000..9016691224 --- /dev/null +++ b/iothub/service/tests/Jobs/JobRequestTests.cs @@ -0,0 +1,75 @@ +// 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 Castle.Components.DictionaryAdapter.Xml; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; + +namespace Microsoft.Azure.Devices.Tests.Jobs +{ + [TestClass] + [TestCategory("Unit")] + public class JobRequestTests + { + const long MaxExecutionTime = 5L; + private static DirectMethodServiceRequest s_directMethodRequest = new("update"); + private static ClientTwin s_updateTwin = new("TestTwin"); + private static DateTimeOffset s_startOn = new(new DateTime()); + + [TestMethod] + public void JobRequest_FieldInitialization() + { + // arrange + var request = new JobRequest(); + + // act + long? maxExecutionTimeInSeconds = request.MaxExecutionTimeInSeconds; + + // assert + maxExecutionTimeInSeconds.Should().BeNull(); + + // rearrange + request.MaxExecutionTimeInSeconds = MaxExecutionTime; + + // assert + request.MaxExecutionTimeInSeconds.Should().Be(MaxExecutionTime); + } + + [TestMethod] + public void JobRequest_SerializesCorrectly() + { + // arrange + var request = new JobRequest() + { + JobId = "TestJob", + JobType = JobType.ScheduleDeviceMethod, + DirectMethodRequest = s_directMethodRequest, + UpdateTwin = s_updateTwin, + QueryCondition = "TestQuery", + StartOn = s_startOn, + MaxExecutionTime = new TimeSpan() + }; + + // act + var settings = new JsonSerializerSettings(); + JobRequest deserializedRequest = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(request, settings)); + + // assert + deserializedRequest.Should().NotBeNull(); + deserializedRequest.JobId.Should().Be("TestJob"); + deserializedRequest.JobType.Should().Be(JobType.ScheduleDeviceMethod); + deserializedRequest.DirectMethodRequest.Should().BeEquivalentTo(s_directMethodRequest); + deserializedRequest.UpdateTwin.Should().BeEquivalentTo(s_updateTwin); + deserializedRequest.QueryCondition.Should().Be("TestQuery"); + deserializedRequest.StartOn.Should().Be(s_startOn); + deserializedRequest.MaxExecutionTime.Should().Be(new TimeSpan()); + deserializedRequest.MaxExecutionTimeInSeconds.Should().Be(new TimeSpan().Seconds); + } + } +} diff --git a/iothub/service/tests/Jobs/ScheduledJobsOptionsTests.cs b/iothub/service/tests/Jobs/ScheduledJobsOptionsTests.cs new file mode 100644 index 0000000000..247d057dbd --- /dev/null +++ b/iothub/service/tests/Jobs/ScheduledJobsOptionsTests.cs @@ -0,0 +1,55 @@ +// 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 Azure.Core; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; + +namespace Microsoft.Azure.Devices.Tests.Jobs +{ + [TestClass] + [TestCategory("Unit")] + public class ScheduledJobsOptionsTests + { + const int MaxExecutionTime = 1; + + [TestMethod] + public void ScheduledJobsOptions_FieldInitialization() + { + // arrange + var options = new ScheduledJobsOptions(); + + // act + options.MaxExecutionTime = TimeSpan.FromSeconds(MaxExecutionTime); + + // assert + options.MaxExecutionTimeInSeconds.Should().Be(MaxExecutionTime); + } + + [TestMethod] + public void ScheduledJobOptions_SerializesCorrectly() + { + // arrange + var options = new ScheduledJobsOptions + { + JobId = "TestJob", + MaxExecutionTime = TimeSpan.FromSeconds(MaxExecutionTime), + }; + + // act + var settings = new JsonSerializerSettings(); + ScheduledJobsOptions deserializedRequest = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(options, settings)); + + // assert + deserializedRequest.Should().NotBeNull(); + deserializedRequest.JobId.Should().Be("TestJob"); + deserializedRequest.MaxExecutionTime.Should().Be(TimeSpan.FromSeconds(MaxExecutionTime)); + } + } +} diff --git a/iothub/service/tests/MessageTests.cs b/iothub/service/tests/MessageTests.cs deleted file mode 100644 index f99fdacef9..0000000000 --- a/iothub/service/tests/MessageTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -// 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.Text; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Microsoft.Azure.Devices.Api.Test -{ - [TestClass] - [TestCategory("Unit")] - public class MessageTests - { - [TestMethod] - public void ConstructorTakingPayloadTest() - { - string payloadString = "Hello, World!"; - byte[] payloadBytes = Encoding.UTF8.GetBytes(payloadString); - var msg = new Message(payloadBytes); - msg.Payload.Should().BeEquivalentTo(payloadBytes); - } - - [TestMethod] - public void ConstructorTakingEmptyByteArrayTest() - { - var msg = new Message(Array.Empty()); - msg.Payload.Should().NotBeNull(); - msg.Payload.Length.Should().Be(0); - } - } -} diff --git a/iothub/service/tests/Messaging/MessageClientTests.cs b/iothub/service/tests/Messaging/MessageClientTests.cs new file mode 100644 index 0000000000..8c778a4e9e --- /dev/null +++ b/iothub/service/tests/Messaging/MessageClientTests.cs @@ -0,0 +1,358 @@ +// 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.Net.Http; +using System.Text; +using System.Threading; +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 Moq; + +namespace Microsoft.Azure.Devices.Tests.Messaging +{ + [TestClass] + [TestCategory("Unit")] + public class MessageClientTests + { + 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 readonly Uri s_httpUri = new($"https://{HostName}"); + + private static IIotHubServiceRetryPolicy noRetryPolicy = new IotHubServiceNoRetry(); + private static IotHubServiceClientOptions s_options = new() + { + Protocol = IotHubTransportProtocol.Tcp, + RetryPolicy = noRetryPolicy + }; + private readonly RetryHandler s_retryHandler = new(new IotHubServiceNoRetry()); + + [TestMethod] + public async Task MessagesClient_OpenAsync_Cancelled_ThrowsOperationCanceledException() + { + // arrange + using var serviceClient = new IotHubServiceClient( + s_connectionString, + s_options); + + // act + var ct = new CancellationToken(true); + Func act = async () => await serviceClient.Messages.OpenAsync(ct); + + // assert + await act.Should().ThrowAsync(); + } + + [TestMethod] + public async Task MessagesClient_CloseAsync_Cancelled_ThrowsOperationCanceledException() + { + // arrange + using var serviceClient = new IotHubServiceClient( + s_connectionString, + s_options); + + // act + var ct = new CancellationToken(true); + Func act = async () => await serviceClient.Messages.CloseAsync(ct); + + // assert + await act.Should().ThrowAsync(); + } + + [TestMethod] + public async Task MessagesClient_SendAsync_WithModule_NullDeviceIdThrows() + { + // arrange + string payloadString = "Hello, World!"; + byte[] payloadBytes = Encoding.UTF8.GetBytes(payloadString); + var mockCredentialProvider = new Mock(); + + var msg = new Message(payloadBytes); + + using var serviceClient = new IotHubServiceClient( + s_connectionString, + s_options); + + // act + Func act = async () => await serviceClient.Messages.SendAsync(null, "moduleId123", msg); + + // assert + await act.Should().ThrowAsync(); + } + + [TestMethod] + public async Task MessagesClient_SendAsync_WithModule_NullModuleIdThrows() + { + // arrange + string payloadString = "Hello, World!"; + byte[] payloadBytes = Encoding.UTF8.GetBytes(payloadString); + var mockCredentialProvider = new Mock(); + + var msg = new Message(payloadBytes); + + using var serviceClient = new IotHubServiceClient( + s_connectionString, + s_options); + + // act + Func act = async () => await serviceClient.Messages.SendAsync("deviceId123", null, msg); + + // assert + await act.Should().ThrowAsync(); + } + + [TestMethod] + [DataRow(null, "moduleId123")] + [DataRow("deviceId123", null)] + public async Task MessagesClient_SendAsync_NullParamsThrows(string deviceId, string moduleId) + { + // arrange + string payloadString = "Hello, World!"; + byte[] payloadBytes = Encoding.UTF8.GetBytes(payloadString); + var mockCredentialProvider = new Mock(); + var msg = new Message(payloadBytes); + + using var serviceClient = new IotHubServiceClient( + s_connectionString, + s_options); + // act + Func act = async () => await serviceClient.Messages.SendAsync(deviceId, moduleId, msg); + // assert + await act.Should().ThrowAsync(); + } + + [TestMethod] + [DataRow(" ", "moduleId123")] + [DataRow("deviceId123", " ")] + [DataRow("", "moduleId123")] + [DataRow("deviceId123", "")] + public async Task MessagesClient_SendAsync_EmptyAndSpaceInParamsThrows(string deviceId, string moduleId) + { + // arrange + string payloadString = "Hello, World!"; + byte[] payloadBytes = Encoding.UTF8.GetBytes(payloadString); + var msg = new Message(payloadBytes); + + // arrange + using var serviceClient = new IotHubServiceClient( + s_connectionString, + s_options); + + // act + Func act = async () => await serviceClient.Messages.SendAsync(deviceId, moduleId, msg); + + // assert + await act.Should().ThrowAsync(); + } + + [TestMethod] + public async Task MessageClient_SendAsync_WithoutExplicitOpenAsync_ThrowsIotHubServiceException() + { + // arrange + string payloadString = "Hello, World!"; + byte[] payloadBytes = Encoding.UTF8.GetBytes(payloadString); + var msg = new Message(payloadBytes); + + using var serviceClient = new IotHubServiceClient( + s_connectionString, + s_options); + + // act + Func act = async () => await serviceClient.Messages.SendAsync("deviceId123", msg); + + // assert + await act.Should().ThrowAsync(); + } + + [TestMethod] + public async Task MessageClient_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 messagesClient = new MessagesClient( + HostName, + mockCredentialProvider.Object, + s_retryHandler, + mockAmqpConnectionHandler.Object); + var ct = new CancellationToken(false); + + // act + Func act = async () => await messagesClient.OpenAsync().ConfigureAwait(false); + + // assert + await act.Should().NotThrowAsync().ConfigureAwait(false); + mockAmqpConnectionHandler.Verify(x => x.OpenAsync(ct), Times.Once()); + } + + [TestMethod] + public async Task MessageClient_PurgeMessageQueueAsync_NullDeviceIdThrows() + { + // arrange + using var serviceClient = new IotHubServiceClient( + s_connectionString, + s_options); + + // act + Func act = async () => await serviceClient.Messages.PurgeMessageQueueAsync(null); + + // assert + await act.Should().ThrowAsync(); + } + + [TestMethod] + public async Task MessageClient_PurgeMessageQueueAsync_EmptyDeviceIdThrows() + { + // arrange + using var serviceClient = new IotHubServiceClient( + s_connectionString, + s_options); + + // act + Func act = async () => await serviceClient.Messages.PurgeMessageQueueAsync(string.Empty); + + // assert + await act.Should().ThrowAsync(); + } + + [TestMethod] + public async Task MessageClient_PurgeMessage() + { + // arrange + string deviceId = "deviceId123"; + int totalMessagesPurged = 1; + + var mockCredentialProvider = new Mock(); + mockCredentialProvider + .Setup(getCredential => getCredential.GetAuthorizationHeader()) + .Returns(s_validMockAuthenticationHeaderValue); + + var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, ""); + + var purgeMessageQueueResultToReturn = new PurgeMessageQueueResult + { + DeviceId = deviceId, + TotalMessagesPurged = totalMessagesPurged + }; + + using var mockHttpResponse = new HttpResponseMessage + { + Content = HttpMessageHelper.SerializePayload(purgeMessageQueueResultToReturn), + StatusCode = HttpStatusCode.OK, + }; + + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + using var messageClient = new MessagesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_options, + s_retryHandler); + + // act + PurgeMessageQueueResult result = await messageClient.PurgeMessageQueueAsync(deviceId); + + // assert + result.DeviceId.Should().Be(deviceId); + result.TotalMessagesPurged.Should().Be(totalMessagesPurged); + } + + [TestMethod] + public async Task MessageClient_SendAsync() + { + // arrange + string payloadString = "Hello, World!"; + byte[] payloadBytes = Encoding.UTF8.GetBytes(payloadString); + var msg = new Message(payloadBytes); + + var mockCredentialProvider = new Mock(); + mockCredentialProvider + .Setup(getCredential => getCredential.GetAuthorizationHeader()) + .Returns(s_validMockAuthenticationHeaderValue); + + var mockAmqpConnectionHandler = new Mock(); + + mockAmqpConnectionHandler + .Setup(x => x.IsOpen) + .Returns(true); + + Outcome outcomeToReturn = new Accepted(); + + mockAmqpConnectionHandler + .Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(outcomeToReturn)); + + using var messagesClient = new MessagesClient( + HostName, + mockCredentialProvider.Object, + s_retryHandler, + mockAmqpConnectionHandler.Object); + + Func act = async () => await messagesClient.SendAsync("deviceId123", msg).ConfigureAwait(false); + + // assert + await act.Should().NotThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task MessageClient_SendAsync_DescriptiorCodeNotAcceptedThrows() + { + // arrange + string payloadString = "Hello, World!"; + byte[] payloadBytes = Encoding.UTF8.GetBytes(payloadString); + var msg = new Message(payloadBytes); + + 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); + + mockAmqpConnectionHandler + .Setup(x => x.IsOpen) + .Returns(true); + + Outcome outcomeToReturn = new Rejected(); + + mockAmqpConnectionHandler + .Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(outcomeToReturn)); + + using var messagesClient = new MessagesClient( + HostName, + mockCredentialProvider.Object, + s_retryHandler, + mockAmqpConnectionHandler.Object); + + Func act = async () => await messagesClient.SendAsync("deviceId123", msg).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + } +} diff --git a/iothub/service/tests/Messaging/MessageTests.cs b/iothub/service/tests/Messaging/MessageTests.cs new file mode 100644 index 0000000000..a66a9bb985 --- /dev/null +++ b/iothub/service/tests/Messaging/MessageTests.cs @@ -0,0 +1,103 @@ +// 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.Text; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Azure.Devices.Tests.Messaging +{ + [TestClass] + [TestCategory("Unit")] + public class MessageTests + { + [TestMethod] + public void ConstructorTakingPayloadTest() + { + string payloadString = "Hello, World!"; + byte[] payloadBytes = Encoding.UTF8.GetBytes(payloadString); + var msg = new Message(payloadBytes); + msg.Payload.Should().BeEquivalentTo(payloadBytes); + msg.HasPayload.Should().BeTrue(); + } + + [TestMethod] + public void ConstructorTakingEmptyByteArrayTest() + { + var msg = new Message(Array.Empty()); + msg.Payload.Should().NotBeNull(); + msg.Payload.Length.Should().Be(0); + } + + [TestMethod] + public void Message_DefaultPayload_NotNull() + { + var msg = new Message(); + msg.Payload.Should().NotBeNull(); + msg.Payload.Should().BeEquivalentTo(Array.Empty()); + msg.Payload.Length.Should().Be(0); + msg.HasPayload.Should().BeFalse(); + } + + [TestMethod] + public void Message_Properties_DefaultNotNull() + { + var msg = new Message(); + msg.Properties.Should().NotBeNull(); + } + + [TestMethod] + public void Message_SystemProperties_DefaultNotNull() + { + var msg = new Message(); + msg.SystemProperties.Should().NotBeNull(); + } + + [TestMethod] + public void Message_Construct() + { + string payload = Guid.NewGuid().ToString(); + string messageId = Guid.NewGuid().ToString(); + string userId = Guid.NewGuid().ToString(); + string to = Guid.NewGuid().ToString(); + string correlationId = Guid.NewGuid().ToString(); + string lockToken = Guid.NewGuid().ToString(); + const string messageSchema = "default@v1"; + const string contentType = "text/plain"; + const string contentEncoding = "utf-8"; + DateTimeOffset createdOnUtc = DateTimeOffset.UtcNow; + DateTimeOffset enqueuedOnUtc = DateTimeOffset.UtcNow; + DateTimeOffset expiresOnUtc = DateTimeOffset.MaxValue; + + var message = new Message(Encoding.UTF8.GetBytes(payload)) + { + MessageId = messageId, + UserId = userId, + To = to, + ExpiresOnUtc = expiresOnUtc, + CorrelationId = correlationId, + LockToken = lockToken, + MessageSchema = messageSchema, + ContentType = contentType, + ContentEncoding = contentEncoding, + Ack = DeliveryAcknowledgement.PositiveOnly, + CreatedOnUtc = createdOnUtc, + EnqueuedOnUtc = enqueuedOnUtc + }; + + message.MessageId.Should().Be(messageId); + message.UserId.Should().Be(userId); + message.To.Should().Be(to); + message.ExpiresOnUtc.Should().Be(expiresOnUtc); + message.CorrelationId.Should().Be(correlationId); + message.LockToken.Should().Be(lockToken); + message.MessageSchema.Should().Be(messageSchema); + message.ContentType.Should().Be(contentType); + message.Ack.Should().Be(DeliveryAcknowledgement.PositiveOnly); + message.CreatedOnUtc.Should().Be(createdOnUtc); + message.EnqueuedOnUtc.Should().Be(enqueuedOnUtc); + } + } +} diff --git a/iothub/service/tests/QueryClientTests.cs b/iothub/service/tests/QueryClientTests.cs new file mode 100644 index 0000000000..477fc63ace --- /dev/null +++ b/iothub/service/tests/QueryClientTests.cs @@ -0,0 +1,213 @@ +// 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; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Newtonsoft.Json; +using FluentAssertions; +using System.Collections; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Azure.Devices.Tests +{ + [TestClass] + [TestCategory("Unit")] + public class QueryClientTests + { + 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 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() + }; + + [TestMethod] + public async Task QueryClient_CreateAsync() + { + // arrange + string query = "select * from devices where deviceId = 'foo'"; + var twin = new ClientTwin("foo"); + + 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(new List { twin }), + }; + mockHttpResponse.Headers.Add("ETag", "\"AAAAAAAAAAE=\""); + + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var queryClient = new QueryClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + QueryResponse response = await queryClient.CreateAsync(query); + + // assert + response.CurrentPage.First().DeviceId.Should().Be("foo"); + } + + [TestMethod] + public async Task QueryClient_CreateAsync_NullParamterThrows() + { + // arrange + using var serviceClient = new IotHubServiceClient(s_connectionString); + + // act + Func act = async () => await serviceClient.Query.CreateAsync(null); + + // assert + await act.Should().ThrowAsync(); + } + + [TestMethod] + public async Task QueryClient_CreateAsync_IotHubNotFound_ThrowsIotHubServiceException() + { + // arrange + 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 queryClient = new QueryClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + // query returns HttpStatusCode.NotFound + Func act = async () => await queryClient.CreateAsync("SELECT * FROM devices"); + + // assert + var error = await act.Should().ThrowAsync(); + error.And.IsTransient.Should().BeFalse(); + error.And.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task QueryClient_CreateJobsQueryAsync() + { + // arrange + var job = new ScheduledJob(); + 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(new List { job }), + }; + mockHttpResponse.Headers.Add("ETag", "\"AAAAAAAAAAE=\""); + + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var queryClient = new QueryClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + Func act = async () => await queryClient.CreateJobsQueryAsync(); + + // assert + await act.Should().NotThrowAsync(); + } + + [TestMethod] + public async Task QueryClient_CreateJobsQuery_IotHubNotFound_ThrowsIotHubServiceException() + { + // arrange + 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 queryClient = new QueryClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + Func act = async () => await queryClient.CreateJobsQueryAsync(); + + // 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/QueryResponseTests.cs b/iothub/service/tests/QueryResponseTests.cs new file mode 100644 index 0000000000..89c1706a69 --- /dev/null +++ b/iothub/service/tests/QueryResponseTests.cs @@ -0,0 +1,48 @@ +// 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 Moq; + +namespace Microsoft.Azure.Devices.Tests +{ + [TestClass] + [TestCategory("Unit")] + public class QueryResponseTests + { + [TestMethod] + public async Task QueryResponse_MoveNextAsync() + { + // arrange + string query = "select * from devices where deviceId = 'foo'"; + var twin1 = new ClientTwin("foo"); + var twin2 = new ClientTwin("foo"); + var queryClient = new Mock(); + + // act + var response = new QueryResponse( + queryClient.Object, + query, + new List { twin1, twin2 }, + "", + 5); + + var expectedResponses = new List { twin1, twin2 }; + + // assert + for (int i = 0; i < expectedResponses.Count; i++) + { + await response.MoveNextAsync(); + ClientTwin queriedTwin = response.Current; + queriedTwin.Should().NotBeNull(); + queriedTwin.DeviceId.Should().Be(expectedResponses[i].DeviceId); + } + } + } +} diff --git a/iothub/service/tests/Registry/ClientTwinTests.cs b/iothub/service/tests/Registry/ClientTwinTests.cs index a58c0e413d..181a690528 100644 --- a/iothub/service/tests/Registry/ClientTwinTests.cs +++ b/iothub/service/tests/Registry/ClientTwinTests.cs @@ -294,6 +294,29 @@ public void ClientTwin_Properties_DeserializesComplexTwin() maxTempSinceLastRebootMetadata.LastUpdatedOnUtc.Should().Be(reportedPropertyLastUpdated); } + [TestMethod] + public void ClientTwin_Properties_ContainsTest() + { + // arrange + const string reportedStringKey = nameof(reportedStringKey); + const string reportedStringValue = nameof(reportedStringValue); + + var twin = new ClientTwin + { + Properties = + { + Reported = + { + [reportedStringKey] = reportedStringValue, + }, + } + }; + + ClientTwinProperties twinPropertiesReported = twin.Properties.Reported; + bool contains = twinPropertiesReported.Contains(nameof(reportedStringKey)); + contains.Should().BeTrue(); + } + private class CustomType { [JsonProperty("customInt")] diff --git a/iothub/service/tests/Registry/DevicesClientTests.cs b/iothub/service/tests/Registry/DevicesClientTests.cs index 9b83068967..bdb5bc2e79 100644 --- a/iothub/service/tests/Registry/DevicesClientTests.cs +++ b/iothub/service/tests/Registry/DevicesClientTests.cs @@ -3,14 +3,17 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; +using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using Azure; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; +using static System.Net.WebRequestMethods; namespace Microsoft.Azure.Devices.Tests { @@ -177,6 +180,134 @@ public async Task DevicesClient_CreateAsync_EmptyListThrows() await act.Should().ThrowAsync().ConfigureAwait(false); } + [TestMethod] + public async Task DevicesClient_CreateAsync() + { + // arrange + var goodDevice = new Device("123") { ConnectionState = ClientConnectionState.Connected }; + + 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(new BulkRegistryOperationResult()), + }; + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + Func act = async () => await devicesClient.CreateAsync(new List { goodDevice }).ConfigureAwait(false); + + // assert + await act.Should().NotThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DevicesClient_CreateWithTwinAsync() + { + // arrange + var testTagName = "DevicesClient_Tag"; + var testTagValue = 100; + var goodDevice1 = new Device("123") { ConnectionState = ClientConnectionState.Connected }; + var clientTwin1 = new ClientTwin("123") + { + Tags = { { testTagName, testTagValue } }, + }; + + 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(new BulkRegistryOperationResult()), + }; + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + Func act = async () => await devicesClient.CreateWithTwinAsync(goodDevice1, clientTwin1).ConfigureAwait(false); + + // assert + await act.Should().NotThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DevicesClient_CreateWithTwinAsync_NullDeviceThrows() + { + // arrange + var testTagName = "DevicesClient_Tag"; + var testTagValue = 100; + var clientTwin1 = new ClientTwin("123") + { + Tags = { { testTagName, testTagValue } }, + }; + + var mockCredentialProvider = new Mock(); + var mockHttpRequestFactory = new Mock(); + var mockHttpClient = new Mock(); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory.Object, + s_retryHandler); + + // act + Func act = async () => await devicesClient.CreateWithTwinAsync(null, clientTwin1).ConfigureAwait(false); + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + + [TestMethod] + public async Task DevicesClient_CreateWithTwinAsync_NullClientTwinThrows() + { + // arrange + var goodDevice1 = new Device("123") { ConnectionState = ClientConnectionState.Connected }; + + var mockCredentialProvider = new Mock(); + var mockHttpRequestFactory = new Mock(); + var mockHttpClient = new Mock(); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory.Object, + s_retryHandler); + + // act + Func act = async () => await devicesClient.CreateWithTwinAsync(goodDevice1, null).ConfigureAwait(false); + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + [TestMethod] public async Task DevicesClient_SetAsync() { @@ -205,7 +336,7 @@ public async Task DevicesClient_SetAsync() s_retryHandler); // act - Device returnedDevice = await devicesClient.SetAsync(deviceToReturn).ConfigureAwait(false); + var returnedDevice = await devicesClient.SetAsync(deviceToReturn).ConfigureAwait(false); // assert returnedDevice.Id.Should().Be(deviceToReturn.Id); @@ -558,5 +689,525 @@ public async Task DevicesClient_DeleteAsync_OnlyIfUnchangedTrueHasEtags() // assert await act.Should().NotThrowAsync().ConfigureAwait(false); } + + [TestMethod] + public async Task DevicesClient_GetJobAsync() + { + // arrange + var jobId = "sampleJob"; + var jobStatus = JobStatus.Completed; + + var mockCredentialProvider = new Mock(); + mockCredentialProvider + .Setup(getCredential => getCredential.GetAuthorizationHeader()) + .Returns(s_validMockAuthenticationHeaderValue); + var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, ""); + var jobToReturn = new IotHubJobResponse{ JobId = jobId, Status = jobStatus }; + using var mockHttpResponse = new HttpResponseMessage + { + Content = HttpMessageHelper.SerializePayload(jobToReturn), + StatusCode = HttpStatusCode.OK, + }; + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + var jobsResult = await devicesClient.GetJobAsync(jobId).ConfigureAwait(false); + + // assert + jobsResult.JobId.Should().Be(jobId); + jobsResult.Status.Should().Be(jobStatus); + mockHttpClient.VerifyAll(); + } + + [TestMethod] + public async Task DevicesClient_GetJobAsync_NullJobIdThrows() + { + // arrange + var mockCredentialProvider = new Mock(); + var mockHttpRequestFactory = new Mock(); + var mockHttpClient = new Mock(); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory.Object, + s_retryHandler); + + // act + Func act = async () => await devicesClient.GetJobAsync(null).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DevicesClient_GetJobAsync_EmptyJobIdThrows() + { + // arrange + var mockCredentialProvider = new Mock(); + var mockHttpRequestFactory = new Mock(); + var mockHttpClient = new Mock(); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory.Object, + s_retryHandler); + + // act + Func act = async () => await devicesClient.GetJobAsync("").ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DevicesClient_GetJobAsync_InvalidJobIdThrows() + { + // arrange + var mockCredentialProvider = new Mock(); + var mockHttpRequestFactory = new Mock(); + var mockHttpClient = new Mock(); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory.Object, + s_retryHandler); + + // act + Func act = async () => await devicesClient.GetJobAsync("sample job").ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DevicesClient_GetJobsAsync() + { + // arrange + var jobId = "sampleJob"; + var jobStatus = JobStatus.Completed; + + var mockCredentialProvider = new Mock(); + mockCredentialProvider + .Setup(getCredential => getCredential.GetAuthorizationHeader()) + .Returns(s_validMockAuthenticationHeaderValue); + var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, ""); + var jobsToReturn = new List { new IotHubJobResponse { JobId = jobId, Status = jobStatus } }; + using var mockHttpResponse = new HttpResponseMessage + { + Content = HttpMessageHelper.SerializePayload(jobsToReturn), + StatusCode = HttpStatusCode.OK, + }; + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + var jobsResult = await devicesClient.GetJobsAsync().ConfigureAwait(false); + + // assert + jobsResult.ElementAt(0).JobId.Should().Be(jobId); + jobsResult.ElementAt(0).Status.Should().Be(jobStatus); + mockHttpClient.VerifyAll(); + } + + [TestMethod] + public async Task DevicesClient_CancelJobAsync_NullJobIdThrows() + { + // arrange + var mockCredentialProvider = new Mock(); + var mockHttpRequestFactory = new Mock(); + var mockHttpClient = new Mock(); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory.Object, + s_retryHandler); + + // act + Func act = async () => await devicesClient.CancelJobAsync(null).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DevicesClient_CancelJobAsync_EmptyJobIdThrows() + { + // arrange + var mockCredentialProvider = new Mock(); + var mockHttpRequestFactory = new Mock(); + var mockHttpClient = new Mock(); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory.Object, + s_retryHandler); + + // act + Func act = async () => await devicesClient.CancelJobAsync("").ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DevicesClient_CancelJobAsync_InvalidJobIdThrows() + { + // arrange + var mockCredentialProvider = new Mock(); + var mockHttpRequestFactory = new Mock(); + var mockHttpClient = new Mock(); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory.Object, + s_retryHandler); + + // act + Func act = async () => await devicesClient.CancelJobAsync("sample job").ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DevicesClient_GetModulesAsync() + { + // arrange + var module1 = new Module("1234", "module1"); + var module2 = new Module("1234", "module2"); + + var mockCredentialProvider = new Mock(); + mockCredentialProvider + .Setup(getCredential => getCredential.GetAuthorizationHeader()) + .Returns(s_validMockAuthenticationHeaderValue); + var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, ""); + var modulesToReturn = new List() { module1, module2 }; + using var mockHttpResponse = new HttpResponseMessage + { + Content = HttpMessageHelper.SerializePayload(modulesToReturn), + StatusCode = HttpStatusCode.OK, + }; + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + var modulesResult = await devicesClient.GetModulesAsync("1234").ConfigureAwait(false); + + // assert + modulesResult.Count().Should().Be(2); + mockHttpClient.VerifyAll(); + } + + + [TestMethod] + public async Task DevicesClient_GetModulesAsync_NullDeviceId_Throws() + { + // arrange + var mockCredentialProvider = new Mock(); + var mockHttpRequestFactory = new Mock(); + var mockHttpClient = new Mock(); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory.Object, + s_retryHandler); + + // act + Func act = async () => await devicesClient.CancelJobAsync(null).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DevicesClient_GetModulesAsync_EmptyDeviceId_Throws() + { + // arrange + var mockCredentialProvider = new Mock(); + var mockHttpRequestFactory = new Mock(); + var mockHttpClient = new Mock(); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory.Object, + s_retryHandler); + + // act + Func act = async () => await devicesClient.CancelJobAsync("").ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DevicesClient_GetRegistryStatisticsAsync() + { + // arrange + var mockCredentialProvider = new Mock(); + mockCredentialProvider + .Setup(getCredential => getCredential.GetAuthorizationHeader()) + .Returns(s_validMockAuthenticationHeaderValue); + var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, ""); + var registryStatisticsToReturn = new RegistryStatistics + { + TotalDeviceCount = 100, + EnabledDeviceCount = 80, + DisabledDeviceCount = 20 + }; + + using var mockHttpResponse = new HttpResponseMessage + { + Content = HttpMessageHelper.SerializePayload(registryStatisticsToReturn), + StatusCode = HttpStatusCode.OK, + }; + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + RegistryStatistics statistics = await devicesClient.GetRegistryStatisticsAsync(); + statistics.Should().NotBeNull(); + statistics.EnabledDeviceCount.Should().Be(80); + statistics.TotalDeviceCount.Should().Be(100); + statistics.DisabledDeviceCount.Should().Be(20); + } + + [TestMethod] + public async Task DevicesClient_GetServiceStatisticsAsync() + { + // arrange + var mockCredentialProvider = new Mock(); + mockCredentialProvider + .Setup(getCredential => getCredential.GetAuthorizationHeader()) + .Returns(s_validMockAuthenticationHeaderValue); + var mockHttpRequestFactory = new HttpRequestMessageFactory(s_httpUri, ""); + var serviceStatisticsToReturn = new ServiceStatistics + { + ConnectedDeviceCount = 100 + }; + + using var mockHttpResponse = new HttpResponseMessage + { + Content = HttpMessageHelper.SerializePayload(serviceStatisticsToReturn), + StatusCode = HttpStatusCode.OK, + }; + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + ServiceStatistics statistics = await devicesClient.GetServiceStatisticsAsync(); + statistics.Should().NotBeNull(); + statistics.ConnectedDeviceCount.Should().Be(100); + } + + [TestMethod] + public async Task DevicesClient_ImportAsync_nullJobParametersThrows() + { + // arrange + var mockCredentialProvider = new Mock(); + var mockHttpRequestFactory = new Mock(); + var mockHttpClient = new Mock(); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory.Object, + s_retryHandler); + + // act + Func act = async () => await devicesClient.ImportAsync(null).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DevicesClient_ImportAsync_MissingContainerNameInJobParametersThrows() + { + // arrange + ImportJobProperties badImportJobProperties = new ImportJobProperties + { + OutputBlobContainerUri = new Uri("https://myaccount.blob.core.windows.net/") + }; + + var mockCredentialProvider = new Mock(); + var mockHttpRequestFactory = new Mock(); + var mockHttpClient = new Mock(); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory.Object, + s_retryHandler); + + // act + Func act = async () => await devicesClient.ImportAsync(badImportJobProperties).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DevicesClient_ImportAsync_EmptyContainerNameInJobParametersThrows() + { + // arrange + ImportJobProperties badImportJobProperties = new ImportJobProperties + { + OutputBlobContainerUri = new Uri("https://myaccount.blob.core.windows.net/ ") + }; + var mockCredentialProvider = new Mock(); + var mockHttpRequestFactory = new Mock(); + var mockHttpClient = new Mock(); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory.Object, + s_retryHandler); + + // act + Func act = async () => await devicesClient.ImportAsync(badImportJobProperties).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DevicesClient_ExportAsync_nullJobParametersThrows() + { + // arrange + var mockCredentialProvider = new Mock(); + var mockHttpRequestFactory = new Mock(); + var mockHttpClient = new Mock(); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory.Object, + s_retryHandler); + + // act + Func act = async () => await devicesClient.ExportAsync(null).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DevicesClient_ExportAsync_MissingContainerNameInJobParametersThrows() + { + // arrange + ImportJobProperties badImportJobProperties = new ImportJobProperties + { + OutputBlobContainerUri = new Uri("https://myaccount.blob.core.windows.net/") + }; + + var mockCredentialProvider = new Mock(); + var mockHttpRequestFactory = new Mock(); + var mockHttpClient = new Mock(); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory.Object, + s_retryHandler); + + // act + Func act = async () => await devicesClient.ImportAsync(badImportJobProperties).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DevicesClient_ExportAsync_EmptyContainerNameInJobParametersThrows() + { + + // arrange + ImportJobProperties badImportJobProperties = new ImportJobProperties + { + OutputBlobContainerUri = new Uri("https://myaccount.blob.core.windows.net/ ") + }; + var mockCredentialProvider = new Mock(); + var mockHttpRequestFactory = new Mock(); + var mockHttpClient = new Mock(); + + var devicesClient = new DevicesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory.Object, + s_retryHandler); + + // act + Func act = async () => await devicesClient.ImportAsync(badImportJobProperties).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } } -} \ No newline at end of file +} diff --git a/iothub/service/tests/Registry/ModulesClientTests.cs b/iothub/service/tests/Registry/ModulesClientTests.cs new file mode 100644 index 0000000000..2b177a390a --- /dev/null +++ b/iothub/service/tests/Registry/ModulesClientTests.cs @@ -0,0 +1,300 @@ +// 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.Net; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using System.Threading; +using Azure; + +namespace Microsoft.Azure.Devices.Tests.Registry +{ + [TestClass] + [TestCategory("Unit")] + public class ModulesClientTests + { + 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 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() + }; + + [TestMethod] + public async Task ModulesClient_CreateAsync_NullModuleThrows() + { + // arrange + using var serviceClient = new IotHubServiceClient( + s_connectionString, + s_options); + + // act + Func act = async () => await serviceClient.Modules.CreateAsync((Module)null).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task ModulesClient_CreateAsync() + { + // arrange + var module = new Module("device123", "module123") + { + ConnectionState = ClientConnectionState.Connected + }; + 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(module) + }; + + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var modulesClient = new ModulesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + Func act = async () => await modulesClient.CreateAsync(module).ConfigureAwait(false); + + // assert + await act.Should().NotThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + [DataRow(null, "moduleId123")] + [DataRow("deviceId123", null)] + + public async Task ModulesClient_GetAsync_NullParamsThrows(string deviceId, string moduleId) + { + // arrange + using var serviceClient = new IotHubServiceClient( + s_connectionString, + s_options); + + // act + Func act = async () => await serviceClient.Modules.GetAsync(deviceId, moduleId).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + [DataRow(" ", "moduleId123")] + [DataRow("deviceId123", " ")] + [DataRow("", "moduleId123")] + [DataRow("deviceId123", "")] + public async Task ModulesClient_GetAsync_EmptyParamsThrows(string deviceId, string moduleId) + { + // arrange + using var serviceClient = new IotHubServiceClient( + s_connectionString, + s_options); + + // act + Func act = async () => await serviceClient.Modules.GetAsync(deviceId, moduleId).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task ModulesClient_GetAsync() + { + // arrange + string moduleId = "moduleId123"; + string deviceId = "deviceId123"; + + var moduleToReturn = new Module("deviceId123", "moduleId123") + { + ConnectionState = ClientConnectionState.Connected + }; + + 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(moduleToReturn) + }; + + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var modulesClient = new ModulesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + Func act = async () => await modulesClient.GetAsync(deviceId, moduleId).ConfigureAwait(false); + + // assert + await act.Should().NotThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task ModulesClient_SetAsync_NullModuleThrows() + { + // arrange + using var serviceClient = new IotHubServiceClient( + s_connectionString, + s_options); + + // act + Func act = async () => await serviceClient.Modules.SetAsync((Module)null).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task ModulesClient_SetAsync() + { + // arrange + var moduleToReplace = new Module("deviceId123", "moduleId123") + { + ConnectionState = ClientConnectionState.Disconnected, + ETag = new ETag("45678") + }; + + var replacedModule = new Module("deviceId123", "moduleId123") + { + ConnectionState = ClientConnectionState.Connected, + ETag = new ETag("12345") + }; + + 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(replacedModule) + }; + + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var modulesClient = new ModulesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + Func act = async () => await modulesClient.SetAsync(moduleToReplace).ConfigureAwait(false); + + // assert + await act.Should().NotThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + [DataRow(null, "moduleId123")] + [DataRow("deviceId123", null)] + public async Task ModulesClient_DeleteAsync_NullParamsThrows(string deviceId, string moduleId) + { + // arrange + using var serviceClient = new IotHubServiceClient( + s_connectionString, + s_options); + + // act + Func act = async () => await serviceClient.Modules.DeleteAsync(deviceId, moduleId).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + [DataRow(" ", "moduleId123")] + [DataRow("deviceId123", " ")] + public async Task ModulesClient_DeleteAsync_EmptyParamsThrows(string deviceId, string moduleId) + { + // arrange + using var serviceClient = new IotHubServiceClient( + s_connectionString, + s_options); + + // act + Func act = async () => await serviceClient.Modules.GetAsync(deviceId, moduleId).ConfigureAwait(false); + + // assert + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task ModulesClient_DeleteAsync() + { + // arrange + var module = new Module("deviceId123", "moduleId123") + { + ConnectionState = ClientConnectionState.Disconnected, + ETag = new ETag("45678") + }; + + 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 modulesClient = new ModulesClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + Func act = async () => await modulesClient.DeleteAsync("deviceId123", "moduleId123").ConfigureAwait(false); + + // assert + await act.Should().NotThrowAsync().ConfigureAwait(false); + } + } +} diff --git a/iothub/service/tests/Registry/TwinsClientTests.cs b/iothub/service/tests/Registry/TwinsClientTests.cs index 8399e69f01..7e64bd4e83 100644 --- a/iothub/service/tests/Registry/TwinsClientTests.cs +++ b/iothub/service/tests/Registry/TwinsClientTests.cs @@ -24,6 +24,160 @@ public class TwinsClientTests private static Uri s_httpUri = new($"https://{HostName}"); private static RetryHandler s_retryHandler = new(new IotHubServiceNoRetry()); + [TestMethod] + public async Task TwinsClient_GetTwin_Device() + { + // arrange + string deviceId = "123"; + var goodTwin = new ClientTwin(deviceId); + 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(goodTwin) + }; + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var twinsClient = new TwinsClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + Func act = async () => await twinsClient.GetAsync(deviceId); + + // assert + await act.Should().NotThrowAsync(); + } + + [TestMethod] + public async Task TwinsClient_GetTwin_Module() + { + // arrange + string deviceId = "123"; + string moduleId = "234"; + var goodTwin = new ClientTwin(deviceId) + { + ModelId = moduleId + }; + + 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(goodTwin) + }; + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var twinsClient = new TwinsClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + Func act = async () => await twinsClient.GetAsync(deviceId, moduleId); + + // assert + await act.Should().NotThrowAsync(); + } + + [TestMethod] + public async Task TwinsClient_UpdateAsync() + { + // arrange + string deviceId = "123"; + string moduleId = "234"; + var goodTwin = new ClientTwin(deviceId); + + 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(goodTwin) + }; + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var twinsClient = new TwinsClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + Func getTwin = async () => await twinsClient.GetAsync(deviceId); + goodTwin.ModelId = moduleId; + Func updateTwin = async () => await twinsClient.UpdateAsync(deviceId, goodTwin); + + // assert + await updateTwin.Should().NotThrowAsync(); + } + + [TestMethod] + public async Task TwinsClient_UpdateAsync_ModuleId() + { + // arrange + string deviceId = "123"; + string moduleId = "234"; + var goodTwin = new ClientTwin(deviceId) + { + ModelId = moduleId + }; + + 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(goodTwin) + }; + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var twinsClient = new TwinsClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + // act + Func act = async () => await twinsClient.UpdateAsync(deviceId, moduleId, goodTwin); + + // assert + await act.Should().NotThrowAsync(); + } + [TestMethod] public async Task TwinsClient_UpdateAsync_BadTwinIdThrows() { @@ -186,5 +340,81 @@ public async Task TwinsClient_UpdateAsync_OnlyIfUnchangedFalseNoEtag() // assert await act.Should().NotThrowAsync().ConfigureAwait(false); } + + [TestMethod] + public async Task TwinsClient_ReplaceAsync() + { + // arrange + var goodTwin1 = new ClientTwin("123"); + var goodTwin2 = new ClientTwin("234"); + 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(goodTwin2) + }; + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var twinsClient = new TwinsClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + + // act + Func act = async () => await twinsClient.ReplaceAsync("123", goodTwin2); + + // assert + await act.Should().NotThrowAsync(); + } + + [TestMethod] + public async Task TwinsClient_ReplaceAsync_ModuleId() + { + // arrange + var goodTwin1 = new ClientTwin("123") + { + ModuleId = "321" + }; + + var goodTwin2 = new ClientTwin("234"); + 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(new BulkRegistryOperationResult()) + }; + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var twinsClient = new TwinsClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + s_retryHandler); + + + // act + Func act = async () => await twinsClient.ReplaceAsync("123", "321", goodTwin2); + + // assert + await act.Should().NotThrowAsync(); + } } -} \ No newline at end of file +} diff --git a/iothub/service/tests/RetryPolicies/IotHubServiceExponentialBackoffRetryPolicyTests.cs b/iothub/service/tests/RetryPolicies/IotHubServiceExponentialBackoffRetryPolicyTests.cs index 1e2d011825..dfecd7a21a 100644 --- a/iothub/service/tests/RetryPolicies/IotHubServiceExponentialBackoffRetryPolicyTests.cs +++ b/iothub/service/tests/RetryPolicies/IotHubServiceExponentialBackoffRetryPolicyTests.cs @@ -55,5 +55,26 @@ public void ExponentialBackoffRetryPolicy_IsExponential(uint retryCount) // assert delay.TotalMilliseconds.Should().BeApproximately(Math.Pow(2, exponent), 100); } + + [TestMethod] + public void ExponentialBackoffRetryPolicy_ShouldRetry_UseJitter() + { + // arrange + const uint MaxRetryAttempts = 70; + const uint currentRetryCount = 50; + + var exponentialBackoffJitter = new IotHubServiceExponentialBackoffRetryPolicy(MaxRetryAttempts, TimeSpan.FromDays(365), true); + var exponentialBackoffStandard = new IotHubServiceExponentialBackoffRetryPolicy(MaxRetryAttempts, TimeSpan.FromDays(365), false); + + // act + exponentialBackoffJitter.ShouldRetry(currentRetryCount, new IotHubServiceException(""), out TimeSpan jitterDelay); + exponentialBackoffStandard.ShouldRetry(currentRetryCount, new IotHubServiceException(""), out TimeSpan regularDelay); + + // assert + double min = regularDelay.TotalMilliseconds * 0.95d; + double max = regularDelay.TotalMilliseconds * 1.05d; + + jitterDelay.TotalMilliseconds.Should().BeInRange(min, max); + } } } diff --git a/iothub/service/tests/RetryPolicies/IotHubServiceFixedDelayRetryPolicyTests.cs b/iothub/service/tests/RetryPolicies/IotHubServiceFixedDelayRetryPolicyTests.cs index 3e836d5445..9127a1a303 100644 --- a/iothub/service/tests/RetryPolicies/IotHubServiceFixedDelayRetryPolicyTests.cs +++ b/iothub/service/tests/RetryPolicies/IotHubServiceFixedDelayRetryPolicyTests.cs @@ -33,5 +33,36 @@ public void FixedDelayRetryPolicy_IsFixedDelay(uint retryCount) // assert retryInterval.Should().Be(expected); } + + [TestMethod] + public void FixedDelayRetryPolicy_UseJitter() + { + // arrange + uint retryCount = 10; + var expected = TimeSpan.FromSeconds(10); + var retryPolicy = new IotHubServiceFixedDelayRetryPolicy(0, expected, true); + + // act + retryPolicy.ShouldRetry(retryCount, new IotHubServiceException("") { IsTransient = true }, out TimeSpan retryInterval); + + // assert + retryInterval.Should().BeCloseTo(expected, TimeSpan.FromMilliseconds(500)); + } + + [TestMethod] + public void FixedDelayRetryPolicy_ShouldRetry_IsFalse() + { + // arrange + uint retryCount = 10; + var expected = TimeSpan.FromSeconds(10); + var retryPolicy = new IotHubServiceFixedDelayRetryPolicy(0, expected); + + // act + // should return false since exception is not transient + bool shouldRetry = retryPolicy.ShouldRetry(retryCount, new IotHubServiceException("") { IsTransient = false }, out TimeSpan retryDelay); + + // assert + shouldRetry.Should().BeFalse(); + } } } diff --git a/iothub/service/tests/RetryPolicies/IotHubServiceIncrementalDelayRetryPolicyTests.cs b/iothub/service/tests/RetryPolicies/IotHubServiceIncrementalDelayRetryPolicyTests.cs index 759a881bf3..d6d9a3e9f5 100644 --- a/iothub/service/tests/RetryPolicies/IotHubServiceIncrementalDelayRetryPolicyTests.cs +++ b/iothub/service/tests/RetryPolicies/IotHubServiceIncrementalDelayRetryPolicyTests.cs @@ -30,5 +30,36 @@ public void IncrementalDelayRetryPolicy_IncrementsInSteps() retryInterval.TotalSeconds.Should().Be(step.TotalSeconds * i); } } + + [TestMethod] + public void IncrementalRetryPolicy_UseJitter() + { + // arrange + var step = TimeSpan.FromSeconds(1); + var retryPolicy = new IotHubServiceIncrementalDelayRetryPolicy(0, step, TimeSpan.FromMinutes(100), true); + + // act + for (uint i = 1; i < 10; ++i) + { + // assert + // note -- provide range of 0.06 instead of 0.05 to account for precision loss + retryPolicy.ShouldRetry(i, new IotHubServiceException("") { IsTransient = true }, out TimeSpan retryInterval); + retryInterval.TotalSeconds.Should().BeApproximately(step.TotalSeconds * i, step.TotalSeconds * i * 0.06); + } + } + + [TestMethod] + public void IncrementalRetryPolicy_ShouldRetry_IsFalse() + { + // arrange + var step = TimeSpan.FromSeconds(1); + var retryPolicy = new IotHubServiceIncrementalDelayRetryPolicy(0, step, TimeSpan.FromMinutes(100), true); + + // act + bool shouldRetry = retryPolicy.ShouldRetry(0, new Exception(), out TimeSpan retryInterval); + + // assert + shouldRetry.Should().BeFalse(); + } } } diff --git a/iothub/service/tests/RetryPolicies/IotHubServiceRetryPolicyBaseTests.cs b/iothub/service/tests/RetryPolicies/IotHubServiceRetryPolicyBaseTests.cs index e5bc3a7100..975938e289 100644 --- a/iothub/service/tests/RetryPolicies/IotHubServiceRetryPolicyBaseTests.cs +++ b/iothub/service/tests/RetryPolicies/IotHubServiceRetryPolicyBaseTests.cs @@ -95,7 +95,6 @@ public void RetryPolicyBase_UpdateWithJitter_IgnoresAtThresholdOf50(double baseT [TestMethod] [DataRow(.1d)] - [DataRow(.25d)] [DataRow(.5d)] [DataRow(1d)] [DataRow(10d)] @@ -107,7 +106,7 @@ public void RetryPolicyBase_UpdateWithJitter_NearValue(double seconds) var retryPolicy = new IotHubServiceTestRetryPolicy(0); var duration = TimeSpan.FromSeconds(seconds); double min = duration.TotalMilliseconds * .95d; - double max = duration.TotalMilliseconds * 1.05d; + double max = duration.TotalMilliseconds * 1.06d; // act TimeSpan actual = retryPolicy.UpdateWithJitter(duration.TotalMilliseconds); diff --git a/iothub/service/tests/ScheduledJobsClientTests.cs b/iothub/service/tests/ScheduledJobsClientTests.cs new file mode 100644 index 0000000000..03fa1e080e --- /dev/null +++ b/iothub/service/tests/ScheduledJobsClientTests.cs @@ -0,0 +1,491 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using FluentAssertions; +using System.Runtime.CompilerServices; + +namespace Microsoft.Azure.Devices.Tests +{ + [TestClass] + [TestCategory("Unit")] + public class ScheduledJobsClientTests + { + 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_toSelect = "Devices"; + private static readonly string s_jobId = "foo"; + private static readonly string s_deviceId = "bar"; + private static readonly TimeSpan s_jobTimeSpan = new(10); + + 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() + }; + + [TestMethod] + public async Task ScheduledJobsClient_GetAsync() + { + // arrange + var scheduledJob = new ScheduledJob + { + JobId = s_jobId, + DeviceId = s_deviceId, + MaxExecutionTime = s_jobTimeSpan, + }; + + 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(scheduledJob), + }; + mockHttpResponse.Headers.Add("ETag", "\"AAAAAAAAAAE=\""); + + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var queryClient = new Mock(); + + var scheduledJobsClient = new ScheduledJobsClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + queryClient.Object, + s_retryHandler); + + // act + ScheduledJob jobResponse = await scheduledJobsClient.GetAsync(s_jobId); + + // assert + jobResponse.JobId.Should().Be(s_jobId); + jobResponse.DeviceId.Should().Be(s_deviceId); + jobResponse.MaxExecutionTimeInSeconds.Should().Be(s_jobTimeSpan.Seconds); + } + + [TestMethod] + public async Task ScheduledJobsClient_GetAsync_NullArgumentThrows() + { + // arrange + using var serviceClient = new IotHubServiceClient(s_connectionString); + + // act + Func act = async () => await serviceClient.ScheduledJobs.GetAsync(null); + + // assert + await act.Should().ThrowAsync(); + } + + [TestMethod] + public async Task ScheduledJobsClient_GetAsync_JobNotFound_ThrowsIotHubServiceException() + { + 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 queryClient = new Mock(); + + var scheduledJobsClient = new ScheduledJobsClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + queryClient.Object, + s_retryHandler); + + // act + Func act = async() => await scheduledJobsClient.GetAsync("foo"); + + // assert + var error = await act.Should().ThrowAsync(); + error.And.IsTransient.Should().BeFalse(); + error.And.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task ScheduledJobsClient_CancelAsync() + { + // arrange + var scheduledJob = new ScheduledJob + { + JobId = s_jobId, + DeviceId = s_deviceId, + MaxExecutionTime = s_jobTimeSpan, + }; + + 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(scheduledJob), + }; + mockHttpResponse.Headers.Add("ETag", "\"AAAAAAAAAAE=\""); + + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var queryClient = new Mock(); + + var scheduledJobsClient = new ScheduledJobsClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + queryClient.Object, + s_retryHandler); + + // act + ScheduledJob jobResponse = await scheduledJobsClient.CancelAsync("foo"); + + // assert + jobResponse.JobId.Should().Be(s_jobId); + jobResponse.DeviceId.Should().Be(s_deviceId); + jobResponse.MaxExecutionTimeInSeconds.Should().Be(s_jobTimeSpan.Seconds); + } + + [TestMethod] + public async Task ScheduledJobsClient_CancelAsync_NullParameterThrows() + { + // arrange + using var serviceClient = new IotHubServiceClient(s_connectionString); + + // act + Func act = async () => await serviceClient.ScheduledJobs.CancelAsync(null); + + // assert + await act.Should().ThrowAsync(); + } + + [TestMethod] + public async Task ScheduledJobsClient_CancelAsync_JobNotFound_ThrowsIotHubServiceException() + { + // arrange + 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 queryClient = new Mock(); + + var scheduledJobsClient = new ScheduledJobsClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + queryClient.Object, + s_retryHandler); + + // act + Func act = async() => await scheduledJobsClient.CancelAsync("foo"); + + // assert + var error = await act.Should().ThrowAsync(); + error.And.IsTransient.Should().BeFalse(); + error.And.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task ScheduledJobsClient_ScheduleDirectMethodAsync() + { + // arrange + var scheduledJob = new ScheduledJob + { + JobId = s_jobId, + DeviceId = s_deviceId, + MaxExecutionTime = s_jobTimeSpan, + }; + + var directMethodRequest = new DirectMethodServiceRequest("foo"); + var startTime = new DateTimeOffset(); + var scheduledJobsOptions = new ScheduledJobsOptions(); + + 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(scheduledJob), + }; + mockHttpResponse.Headers.Add("ETag", "\"AAAAAAAAAAE=\""); + + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var queryClient = new Mock(); + + var scheduledJobsClient = new ScheduledJobsClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + queryClient.Object, + s_retryHandler); + + // act + ScheduledJob returnedJob = await scheduledJobsClient.ScheduleDirectMethodAsync( + $"SELECT * FROM {s_toSelect}", + directMethodRequest, + startTime, + scheduledJobsOptions); + + // assert + returnedJob.JobId.Should().Be(s_jobId); + returnedJob.DeviceId.Should().Be(s_deviceId); + returnedJob.MaxExecutionTimeInSeconds.Should().Be(s_jobTimeSpan.Seconds); + } + + [TestMethod] + public async Task ScheduledJobsClient_ScheduleDirectMethodAsync_NullParamterThrows() + { + // arrange + using var serviceClient = new IotHubServiceClient(s_connectionString); + + // act + Func act = async () => await serviceClient.ScheduledJobs.ScheduleDirectMethodAsync( + null, + null, + new DateTimeOffset(DateTime.UtcNow), + new ScheduledJobsOptions()); + + // assert + await act.Should().ThrowAsync(); + } + + [TestMethod] + public async Task ScheduledJobsClient_ScheduleDirectMethodAsync_JobNotFound_ThrowsIotHubServiceException() + { + // arrange + 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 queryClient = new Mock(); + + var scheduledJobsClient = new ScheduledJobsClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + queryClient.Object, + s_retryHandler); + + // act + Func act = async () => await scheduledJobsClient.ScheduleDirectMethodAsync( + $"SELECT * FROM {s_toSelect}", + new DirectMethodServiceRequest("bar"), + new DateTimeOffset(DateTime.UtcNow), + new ScheduledJobsOptions()); + + // assert + var error = await act.Should().ThrowAsync(); + error.And.IsTransient.Should().BeFalse(); + error.And.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task ScheduledJobsClient_ScheduleTwinUpdateAsync() + { + // arrange + var scheduledJob = new ScheduledJob + { + JobId = s_jobId, + DeviceId = s_deviceId, + MaxExecutionTime = s_jobTimeSpan, + }; + + 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(scheduledJob), + }; + mockHttpResponse.Headers.Add("ETag", "\"AAAAAAAAAAE=\""); + + var mockHttpClient = new Mock(); + mockHttpClient + .Setup(restOp => restOp.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockHttpResponse); + + var queryClient = new Mock(); + + var scheduledJobsClient = new ScheduledJobsClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + queryClient.Object, + s_retryHandler); + + // act + ScheduledJob jobResponse = await scheduledJobsClient.ScheduleTwinUpdateAsync( + $"SELECT * FROM {s_toSelect}", + new ClientTwin(), + new DateTimeOffset(DateTime.UtcNow)); + + // assert + jobResponse.JobId.Should().Be(s_jobId); + jobResponse.DeviceId.Should().Be(s_deviceId); + jobResponse.MaxExecutionTimeInSeconds.Should().Be(s_jobTimeSpan.Seconds); + } + + [TestMethod] + public async Task ScheduledJobsClient_ScheduleTwinUpdateAysnc_NullParamterThrows() + { + // arrange + using var serviceClient = new IotHubServiceClient(s_connectionString); + + // act + Func act = async () => await serviceClient.ScheduledJobs.ScheduleTwinUpdateAsync( + $"SELECT * FROM {s_toSelect}", + null, + new DateTimeOffset(DateTime.UtcNow)); + + // assert + await act.Should().ThrowAsync(); + } + + [TestMethod] + public async Task ScheduledJobsClient_ScheduleTwinUpdateAysnc_JobNotFound_ThrowsIotHubServiceException() + { + // arrange + 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 queryClient = new Mock(); + + var scheduledJobsClient = new ScheduledJobsClient( + HostName, + mockCredentialProvider.Object, + mockHttpClient.Object, + mockHttpRequestFactory, + queryClient.Object, + s_retryHandler); + + // act + Func act = async () => await scheduledJobsClient.ScheduleTwinUpdateAsync( + $"SELECT * FROM {s_toSelect}", + new ClientTwin(), + new DateTimeOffset(DateTime.UtcNow)); + + // 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/SerializationTests.cs b/iothub/service/tests/SerializationTests.cs index 6987dfb1c5..c03c910627 100644 --- a/iothub/service/tests/SerializationTests.cs +++ b/iothub/service/tests/SerializationTests.cs @@ -1,6 +1,10 @@ // 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 Azure; +using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; @@ -11,60 +15,240 @@ namespace Microsoft.Azure.Devices.Tests public class SerializationTests { [TestMethod] - public void Twin_JsonDateParse_Ok() + public void ClientTwin_JsonParse_Ok() { - const string jsonString = @" -{ - ""deviceId"": ""test"", - ""etag"": ""AAAAAAAAAAM="", - ""version"": 5, - ""status"": ""enabled"", - ""statusUpdateTime"": ""2018-06-29T21:17:08.7759733"", - ""connectionState"": ""Connected"", - ""lastActivityTime"": ""2018-06-29T21:17:08.7759733"", -}"; - - JsonConvert.DeserializeObject(jsonString); + // arrange - act + var clientTwin = new ClientTwin("test") + { + ETag = new ETag("AAAAAAAAAAM="), + Version = 5, + Status = ClientStatus.Enabled, + StatusUpdatedOnUtc = new DateTimeOffset(2023, 1, 20, 8, 6, 32, new TimeSpan(1, 0, 0)), + ConnectionState = ClientConnectionState.Connected, + LastActiveOnUtc = new DateTimeOffset(2023, 1, 19, 8, 6, 32, new TimeSpan(1, 0, 0)), + }; + + string clientTwinSerialized = JsonConvert.SerializeObject(clientTwin); + + ClientTwin ct = JsonConvert.DeserializeObject(clientTwinSerialized); + + // assert + ct.Should().BeEquivalentTo(clientTwin); } [TestMethod] - public void Configuration_TestWithSchema_Ok() + public void Configuration_JsonParse_Ok() { - const string jsonString = @" -{ - ""id"": ""aa"", - ""schemaVersion"": ""1.0"", - ""content"": { - ""modulesContent"": { - ""$edgeAgent"": { - ""properties.desired"": { - ""schemaVersion"": ""1.0"" - } + // arrange - act + const string ExpectedSchemaVersion = "1.0"; + var configuration = new Configuration("aa") + { + Content = new ConfigurationContent + { + ModulesContent = + { + { + "edgeAgent", new Dictionary { { "properties.desired", "test" } } + } + } + } + }; + string configurationSerialized = JsonConvert.SerializeObject(configuration); + Configuration c = JsonConvert.DeserializeObject(configurationSerialized); + + // assert + c.SchemaVersion.Should().Be(ExpectedSchemaVersion); + c.Should().BeEquivalentTo(configuration); } - } - } -}"; - JsonConvert.DeserializeObject(jsonString); + [TestMethod] + public void ImportConfiguration_JsonParse_Ok() + { + // arrange - act + var importConfiguration = new ImportConfiguration("aa") + { + Id = "aa", + ImportMode = ConfigurationImportMode.CreateOrUpdateIfMatchETag + }; + string importConfigurationSerialized = JsonConvert.SerializeObject(importConfiguration); + + ImportConfiguration ic = JsonConvert.DeserializeObject(importConfigurationSerialized); + + // assert + ic.Should().BeEquivalentTo(importConfiguration); } [TestMethod] - public void Configuration_TestNullSchema_Ok() + public void FeedbackRecord_JsonParse_Ok() { - const string jsonString = @" -{ - ""id"": ""aa"", - ""content"": { - ""modulesContent"": { - ""$edgeAgent"": { - ""properties.desired"": { - } + // arrange - act + var feedbackRecord = new FeedbackRecord + { + OriginalMessageId = "1", + DeviceGenerationId = "2", + DeviceId = "testDeviceId", + EnqueuedOnUtc = new DateTimeOffset(2023, 1, 20, 8, 6, 32, new TimeSpan(1, 0, 0)), + StatusCode = FeedbackStatusCode.Success, + Description = "Success" + }; + string feedbackRecordSerialized = JsonConvert.SerializeObject(feedbackRecord); + + // assert + FeedbackRecord fr = JsonConvert.DeserializeObject(feedbackRecordSerialized); + fr.Should().BeEquivalentTo(feedbackRecord); } - } - } -}"; - JsonConvert.DeserializeObject(jsonString); + [TestMethod] + public void FileUploadNotification_JsonParse_Ok() + { + // arrange - act + var fileUploadNotification = new FileUploadNotification + { + DeviceId = "testDeviceId", + BlobName = "testBlob", + BlobUriPath = new Uri("https://myaccount.blob.core.windows.net"), + BlobSizeInBytes = 50, + LastUpdatedOnUtc = new DateTimeOffset(2023, 1, 19, 8, 7, 32, new TimeSpan(1, 0, 0)), + EnqueuedOnUtc = new DateTimeOffset(2023, 1, 20, 8, 6, 32, new TimeSpan(1, 0, 0)) + }; + + string fileUploadNotificationSerialized = JsonConvert.SerializeObject(fileUploadNotification); + + FileUploadNotification fun = JsonConvert.DeserializeObject(fileUploadNotificationSerialized); + + // assert + fun.Should().BeEquivalentTo(fileUploadNotification); + } + + [TestMethod] + public void BasicDigitalTwin_JsonParse_Ok() + { + // arrange - act + var basicDigitalTwin = new BasicDigitalTwin + { + Id = "twinId1234", + Metadata = new DigitalTwinMetadata + { + ModelId = "modelId1234" + } + }; + + string basicDigitalTwinSerialized = JsonConvert.SerializeObject(basicDigitalTwin); + BasicDigitalTwin bdt = JsonConvert.DeserializeObject(basicDigitalTwinSerialized); + + // assert + basicDigitalTwin.Should().BeEquivalentTo(bdt); + } + + [TestMethod] + public void BasicDigitalTwin_WithCustomProperties_JsonParse_Ok() + { + // arrange - act + var basicDigitalTwin = new BasicDigitalTwin + { + Id = "twinId1234", + Metadata = new DigitalTwinMetadata + { + ModelId = "modelId1234", + WritableProperties = + { + { "additionalKey", "value" } + } + }, + CustomProperties = + { + { "desiredValue", "sampleValue" }, + { "desiredVersion", 1 }, + { "ackVersion", 1 }, + { "ackCode", 200 }, + { "ackDescription", "Ack Description" } + } + }; + + string basicDigitalTwinSerialized = JsonConvert.SerializeObject(basicDigitalTwin); + BasicDigitalTwin bdt = JsonConvert.DeserializeObject(basicDigitalTwinSerialized); + + // assert + basicDigitalTwin.Should().BeEquivalentTo(bdt); + } + + [TestMethod] + public void WritableProperty_JsonParse_Ok() + { + // arrange - act + var writableProperty = new WritableProperty + { + DesiredValue = "sampleValue", + DesiredVersion = 1, + AckVersion = 1, + AckCode = 200, + AckDescription = "Ack Description", + LastUpdatedOnUtc = new DateTimeOffset(2023, 1, 20, 8, 6, 32, new TimeSpan(1, 0, 0)) + }; + + string writablePropertySerialized = JsonConvert.SerializeObject(writableProperty); + WritableProperty wp = JsonConvert.DeserializeObject(writablePropertySerialized); + + // assert + writableProperty.Should().BeEquivalentTo(wp); + } + + [TestMethod] + public void ComponentMetadata_JsonParse_Ok() + { + // arrange - act + var componentMetadata = new ComponentMetadata + { + WritableProperties = + { + { "key1", "sampleValue" }, + { "key2", 1 }, + } + }; + string componentMetadataSerialized = JsonConvert.SerializeObject(componentMetadata); + ComponentMetadata metaData = JsonConvert.DeserializeObject(componentMetadataSerialized); + + // assert + metaData.Should().BeEquivalentTo(componentMetadata); + } + + [TestMethod] + public void CloudToDeviceMethodScheduledJob_JsonParse_Ok() + { + // arrange - act + var cloudToDeviceMethodScheduledJob = new CloudToDeviceMethodScheduledJob( + new DirectMethodServiceRequest("testMethod") + { + Payload = "testPayload" + } + ); + + string cloudToDeviceMethodScheduledJobSerialized = JsonConvert.SerializeObject(cloudToDeviceMethodScheduledJob); + CloudToDeviceMethodScheduledJob job = JsonConvert.DeserializeObject(cloudToDeviceMethodScheduledJobSerialized); + + // assert + job.Should().BeEquivalentTo(cloudToDeviceMethodScheduledJob); + } + + [TestMethod] + public void DeviceJobStatistics_JsonParse_Ok() + { + // arrange - act + var deviceJobStatistics = new DeviceJobStatistics + { + DeviceCount = 100, + FailedCount = 50, + SucceededCount = 0, + RunningCount = 20, + PendingCount = 30 + }; + + string deviceJobStatisticsSerialized = JsonConvert.SerializeObject(deviceJobStatistics); + + DeviceJobStatistics statistics = JsonConvert.DeserializeObject(deviceJobStatisticsSerialized); + + // assert + statistics.Should().BeEquivalentTo(deviceJobStatistics); } } }