From 5ae087d68ad759b2aade9e261c4cd97d944b4f3c Mon Sep 17 00:00:00 2001 From: Pericles Alves Date: Tue, 19 Jan 2021 16:23:24 -0800 Subject: [PATCH] Connection manager tests --- .gitignore | 2 + DeviceBridge/DeviceBridge.xml | 2 +- DeviceBridge/Services/ConnectionManager.cs | 2 +- DeviceBridgeTests/DeviceBridgeTests.csproj | 13 + .../Microsoft.Azure.Devices.Client.fakes | Bin 0 -> 278 bytes ...ft.Azure.Devices.Provisioning.Client.fakes | Bin 0 -> 302 bytes DeviceBridgeTests/Fakes/System.Runtime.fakes | Bin 0 -> 244 bytes .../Fakes/System.Threading.fakes | Bin 0 -> 248 bytes .../Services/ConnectionManagerServiceTests.cs | 55 -- .../Services/ConnectionManagerTests.cs | 563 ++++++++++++++++++ 10 files changed, 580 insertions(+), 57 deletions(-) create mode 100644 DeviceBridgeTests/Fakes/Microsoft.Azure.Devices.Client.fakes create mode 100644 DeviceBridgeTests/Fakes/Microsoft.Azure.Devices.Provisioning.Client.fakes create mode 100644 DeviceBridgeTests/Fakes/System.Runtime.fakes create mode 100644 DeviceBridgeTests/Fakes/System.Threading.fakes delete mode 100644 DeviceBridgeTests/Services/ConnectionManagerServiceTests.cs create mode 100644 DeviceBridgeTests/Services/ConnectionManagerTests.cs diff --git a/.gitignore b/.gitignore index 42105c2..2322f77 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +#Junk Files *.DS_Store [Tt]humbs.db @@ -34,6 +35,7 @@ ipch/ *.bak.* *.bak .vs/ +FakesAssemblies/ #Tools _ReSharper.* diff --git a/DeviceBridge/DeviceBridge.xml b/DeviceBridge/DeviceBridge.xml index 4971ab0..86f75d1 100644 --- a/DeviceBridge/DeviceBridge.xml +++ b/DeviceBridge/DeviceBridge.xml @@ -418,7 +418,7 @@ See ConnectionStatus documentation - for a detailed description of each status and reaosn. + for a detailed description of each status and reason. Id of the device to get the status for. The last known connection status of the device or null if the device has never connected. diff --git a/DeviceBridge/Services/ConnectionManager.cs b/DeviceBridge/Services/ConnectionManager.cs index 0c93413..c85ea93 100644 --- a/DeviceBridge/Services/ConnectionManager.cs +++ b/DeviceBridge/Services/ConnectionManager.cs @@ -119,7 +119,7 @@ namespace DeviceBridge.Services /// /// See ConnectionStatus documentation - /// for a detailed description of each status and reaosn. + /// for a detailed description of each status and reason. /// /// Id of the device to get the status for. /// The last known connection status of the device or null if the device has never connected. diff --git a/DeviceBridgeTests/DeviceBridgeTests.csproj b/DeviceBridgeTests/DeviceBridgeTests.csproj index 2a118e1..598f411 100644 --- a/DeviceBridgeTests/DeviceBridgeTests.csproj +++ b/DeviceBridgeTests/DeviceBridgeTests.csproj @@ -8,6 +8,15 @@ + + + + + + + + + @@ -26,4 +35,8 @@ + + + + diff --git a/DeviceBridgeTests/Fakes/Microsoft.Azure.Devices.Client.fakes b/DeviceBridgeTests/Fakes/Microsoft.Azure.Devices.Client.fakes new file mode 100644 index 0000000000000000000000000000000000000000..416a8c1fdd0fcb3edaff914697a5ac4d1fa9c055 GIT binary patch literal 278 zcmZvY%?`mp6otRF#5+hIVCrWlB@trB(jKX3Xe)}~;qj@6jof7B-ZST(b286I!jvUD zQYu!#%g3@g*hU%7WPM9G7}ZGVnP literal 0 HcmV?d00001 diff --git a/DeviceBridgeTests/Fakes/Microsoft.Azure.Devices.Provisioning.Client.fakes b/DeviceBridgeTests/Fakes/Microsoft.Azure.Devices.Provisioning.Client.fakes new file mode 100644 index 0000000000000000000000000000000000000000..a1741028fa18e5ac347e21587d933d916990dc0a GIT binary patch literal 302 zcmZvYK@Y(|6olt2@gJK0fNtyHASG#t8*y?kDH__9E`lGAuPeC7%WL18ot>HX`A9WY zuDuF51-h!zK{fMGp>{e^M-5~sr!`0cBWI3ymmrNSo7Dzp#BRY2+eg!=r&tMPP#meN z4nBcWW!!UKZ-wU#Y|gC;r1hTtQO|VO8OH*r3D1k?w8|{B$b91)XP^#G2$}Zd%yX9Wmw1|Tinx1VQVLc-VzI%W(Rm@^_} zp|0ut_GZ4sR;sQdZT3c_(X-8qS@ZB!cN5$DNbc5N%P34NJH=Gqq(6zA(5IzN|37AY E0aYs~xc~qF literal 0 HcmV?d00001 diff --git a/DeviceBridgeTests/Fakes/System.Threading.fakes b/DeviceBridgeTests/Fakes/System.Threading.fakes new file mode 100644 index 0000000000000000000000000000000000000000..f4705dcee08ed28b07b93378b94a9f80ac195430 GIT binary patch literal 248 zcmYL^y9&Zk5Cx|e{D$ literal 0 HcmV?d00001 diff --git a/DeviceBridgeTests/Services/ConnectionManagerServiceTests.cs b/DeviceBridgeTests/Services/ConnectionManagerServiceTests.cs deleted file mode 100644 index 374ad29..0000000 --- a/DeviceBridgeTests/Services/ConnectionManagerServiceTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. - -using System.Threading.Tasks; -using NUnit.Framework; - -namespace DeviceBridge.Services.Tests -{ - [TestFixture] - public class ConnectionManagerServiceTests - { - [Test] - public async Task TryGetDeviceClient() - { - // TODO - // Returns device client if one exists - } - - [Test] - public async Task InitializeDeviceClientAsync() - { - // TODO - // Multiple calls to InitializeDeviceClientAsync do not run in parallel (test mutual exclusion) - // Calls to InitializeDeviceClientAsync and TearDownDeviceClientAsync do not run in parallel (test mutual exclusion) - // Returns existing client if one already exists - // Tries to connect to cached device hub, if one exists, before attempting other known hubs - // Tries to connect to all known hubs before trying DPS registration - // Fails right away if OpenAsync throws an exception not in the list of expected errors - // Tries DPS registration with correct key if attempts to connect to all known hubs fail - // Adds new hub to local device -> hub cache - // Tries to add new hub to local cache of known hubs - // Tries to store new hub in Key Vault if it was not yet stored - // Sets pooling and correct pool size when building a client - // Sets custom retry policy when building a client - // Disposes temporary client if an error happens during client build - } - - [Test] - public async Task TearDownDeviceClientAsync() - { - // TODO - // Multiple calls to TearDownDeviceClientAsync do not run in parallel (test mutual exclusion) - // Calls to InitializeDeviceClientAsync and TearDownDeviceClientAsync do not run in parallel (test mutual exclusion) - // Removes client from list before closing - // Calls CloseAsync before disposing - // Disposes client - } - - [Test] - public async Task ConnectionStatusChange() - { - // TODO - // SDK connection status changes update device status - } - } -} \ No newline at end of file diff --git a/DeviceBridgeTests/Services/ConnectionManagerTests.cs b/DeviceBridgeTests/Services/ConnectionManagerTests.cs new file mode 100644 index 0000000..acc091e --- /dev/null +++ b/DeviceBridgeTests/Services/ConnectionManagerTests.cs @@ -0,0 +1,563 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using DeviceBridge.Common.Exceptions; +using DeviceBridge.Models; +using DeviceBridge.Providers; +using Microsoft.Azure.Devices.Client; +using Microsoft.Azure.Devices.Client.Exceptions; +using Microsoft.Azure.Devices.Provisioning.Client; +using Microsoft.QualityTools.Testing.Fakes; +using Moq; +using NLog; +using NUnit.Framework; + +namespace DeviceBridge.Services.Tests +{ + [TestFixture] + public class ConnectionManagerTests + { + private Mock _storageProviderMock = new Mock(); + + [SetUp] + public async Task Setup() + { + } + + [Test] + public async Task AssertDeviceConnectionOpenAsyncMutualExclusion() + { + using (ShimsContext.Create()) + { + var connectionManager = CreateConnectionManager(); + + // Check that client open and close operations for the same device block on the same mutex. + SemaphoreSlim openSemaphore = null, closeSemaphore = null; + CaptureSemaphoreOnWait((semaphore) => openSemaphore = semaphore); + ShimDps("test-hub.azure.devices.net"); + ShimDeviceClient(); + await connectionManager.AssertDeviceConnectionOpenAsync("test-device-id"); + CaptureSemaphoreOnWait((semaphore) => closeSemaphore = semaphore); + await connectionManager.AssertDeviceConnectionClosedAsync("test-device-id"); + Assert.IsNotNull(openSemaphore); + Assert.AreEqual(openSemaphore, closeSemaphore); + + // Check that client open operations for different devices block on different mutexes. + SemaphoreSlim anotherDeviceOpenSemaphore = null; + CaptureSemaphoreOnWait((semaphore) => anotherDeviceOpenSemaphore = semaphore); + await connectionManager.AssertDeviceConnectionOpenAsync("another-test-device-id"); + Assert.IsNotNull(anotherDeviceOpenSemaphore); + Assert.AreNotEqual(openSemaphore, anotherDeviceOpenSemaphore); + + // Check that the mutex is unlocked on failure + ShimDeviceClientToFail(); + SemaphoreSlim openFailSemaphore = null; + CaptureSemaphoreOnWait((semaphore) => openFailSemaphore = semaphore); + await ExpectToThrow(() => connectionManager.AssertDeviceConnectionOpenAsync("device-to-fail-id")); + Assert.AreEqual(openFailSemaphore.CurrentCount, 1); + + // Check that a device connection attempt time is registered before it enters the critical section. + var startTime = DateTime.Now; + SemaphoreSlim connectionTimeSemaphore = null; + ShimDeviceClient(); + CaptureSemaphoreOnWait((semaphore) => + { + connectionTimeSemaphore = semaphore; + Assert.IsNotNull(connectionManager.GetDevicesThatConnectedSince(startTime).Find(id => id == "connection-time-test-id")); + }); + await connectionManager.AssertDeviceConnectionOpenAsync("connection-time-test-id"); + Assert.NotNull(connectionTimeSemaphore); + } + } + + [Test] + public async Task AssertDeviceConnectionOpenAsyncTemporaryVsPermanent() + { + using (ShimsContext.Create()) + { + var connectionManager = CreateConnectionManager(); + int closeCount = 0; + + // If temporary is set to false (default), creates a permanent connection without creating or renewing a temporary connection. + ShimDps("test-hub.azure.devices.net"); + ShimDeviceClientAndCaptureClose(() => closeCount++); + await connectionManager.AssertDeviceConnectionOpenAsync("permanent-device-id"); + await connectionManager.AssertDeviceConnectionClosedAsync("permanent-device-id", true); + Assert.AreEqual(closeCount, 0, "Closing a temporary connection should not have closed a permanent connection"); + await connectionManager.AssertDeviceConnectionClosedAsync("permanent-device-id"); + Assert.AreEqual(closeCount, 1); + + // If temporary is set to true, creates a temporary connection if one doesn't exist, without creating a permanent connection. + closeCount = 0; + await connectionManager.AssertDeviceConnectionOpenAsync("temporary-device-id", true); + ShimUtcNowAhead(20); // Move the clock so the temporary connection will expire. + await connectionManager.AssertDeviceConnectionClosedAsync("temporary-device-id"); + Assert.AreEqual(closeCount, 0, "Closing a permanent connection should not have closed a temporary connection"); + await connectionManager.AssertDeviceConnectionClosedAsync("temporary-device-id", true); + Assert.AreEqual(closeCount, 1); + + // If temporary is set to true, renews a temporary connection if one already exists, without creating a permanent connection. + closeCount = 0; + UnshimUtcNow(); + await connectionManager.AssertDeviceConnectionOpenAsync("renew-device-id", true); // Create initial ~10min connection. + ShimUtcNowAhead(5); + await connectionManager.AssertDeviceConnectionOpenAsync("renew-device-id", true); // Move the clock 5min and renew connection for another ~10min, so total connection duration is ~15min. + ShimUtcNowAhead(12); + await connectionManager.AssertDeviceConnectionClosedAsync("renew-device-id", true); + Assert.AreEqual(closeCount, 0, "Temporary connection should not have been closed after 12min, as it was renewed for ~15min"); + ShimUtcNowAhead(18); + await connectionManager.AssertDeviceConnectionClosedAsync("renew-device-id", true); + Assert.AreEqual(closeCount, 1, "Temporary connection should have been closed after 18min."); + } + } + + [Test] + public async Task AssertDeviceConnectionOpenAsyncRecreateFailedClient() + { + using (ShimsContext.Create()) + { + var connectionManager = CreateConnectionManager(); + int closeCount = 0; + + // Create a client that instantly goes to a failure state. + ShimDps("test-hub.azure.devices.net"); + ShimDeviceClientAndEmitStatus(ConnectionStatus.Disconnected, ConnectionStatusChangeReason.Retry_Expired); + await connectionManager.AssertDeviceConnectionOpenAsync("recreate-failed-device-id"); + + // If recreateFailedClient is set to false (default), don't try to recreate a client in a permanent failure state + ShimDeviceClientAndCaptureClose(() => closeCount++); + await connectionManager.AssertDeviceConnectionOpenAsync("recreate-failed-device-id"); + Assert.AreEqual(closeCount, 0); + + // If recreateFailedClient is set to true, tries to recreate a client in a permanent failure state + ShimDeviceClientAndCaptureClose(() => closeCount++); + await connectionManager.AssertDeviceConnectionOpenAsync("recreate-failed-device-id", false, true); + Assert.AreEqual(closeCount, 1); + } + } + + [Test] + public async Task AssertDeviceConnectionOpenAsyncTriesCachedHub() + { + using (ShimsContext.Create()) + { + var hubCache = new List() + { + new HubCacheEntry() + { + DeviceId = "test-device-id", + Hub = "known-hub.azure.devices.net", + }, + }; + var connectionManager = CreateConnectionManager(hubCache); + + // Check that it Attempts to connect to the cached device hub first, if one exists. + string connStr = null; + ShimDeviceClientAndCaptureConnectionString(capturedConnStr => connStr = capturedConnStr); + await connectionManager.AssertDeviceConnectionOpenAsync("test-device-id"); + StringAssert.Contains("known-hub.azure.devices.net", connStr); + + // Check that the device client is cached and not reopened in subsequent calls. + bool openAttempted = false; + ShimDeviceClientAndCaptureOpen(() => openAttempted = true); + await connectionManager.AssertDeviceConnectionOpenAsync("test-device-id"); + Assert.False(openAttempted); + + // Check that DPS registration is eventually attempted if connection error indicates that the device doesn't exist in the target hub. + connectionManager = CreateConnectionManager(hubCache); + ShimDeviceClientToFail(new DeviceNotFoundException()); + var registrationAttempted = false; + ShimDpsAndCaptureRegistration("test-hub.azure.devices.net", () => registrationAttempted = true); + await ExpectToThrow(() => connectionManager.AssertDeviceConnectionOpenAsync("test-device-id")); + Assert.True(registrationAttempted); + + // Check that DPS registration is not attempted if connection attempt fails with unknown error. + registrationAttempted = false; + ShimDeviceClientToFail(new Exception()); + await ExpectToThrow(() => connectionManager.AssertDeviceConnectionOpenAsync("test-device-id")); + Assert.False(registrationAttempted); + } + } + + [Test] + public async Task AssertDeviceConnectionOpenAsyncTriesAllKnownHubs() + { + using (ShimsContext.Create()) + { + var hubCache = new List() + { + new HubCacheEntry() + { + DeviceId = "another-device-id-1", + Hub = "known-hub-1.azure.devices.net", + }, + new HubCacheEntry() + { + DeviceId = "another-device-id-2", + Hub = "known-hub-2.azure.devices.net", + }, + }; + var connectionManager = CreateConnectionManager(hubCache); + + // Check that it Attempts to connect to a known hub, even if it the device Id doesn't match. + string connStr = null; + ShimDeviceClientAndCaptureConnectionString(capturedConnStr => connStr = capturedConnStr); + await connectionManager.AssertDeviceConnectionOpenAsync("test-device-id"); + Assert.True(connStr.Contains("known-hub-1.azure.devices.net") || connStr.Contains("known-hub-2.azure.devices.net")); + + // Check that hub was cached in the DB. + _storageProviderMock.Verify(p => p.AddOrUpdateHubCacheEntry(It.IsAny(), "test-device-id", It.IsIn(new string[] { "known-hub-1.azure.devices.net", "known-hub-2.azure.devices.net" })), Times.Once); + + // Checks that failure to save hub in DB cache doesn't fail the open operation. + connectionManager = CreateConnectionManager(hubCache); + _storageProviderMock.Setup(p => p.AddOrUpdateHubCacheEntry(It.IsAny(), It.IsAny(), It.IsAny())).Throws(new Exception()); + await connectionManager.AssertDeviceConnectionOpenAsync("test-device-id"); + _storageProviderMock.Setup(p => p.AddOrUpdateHubCacheEntry(It.IsAny(), It.IsAny(), It.IsAny())).Verifiable(); + + // Check that the device client is cached and not reopened in subsequent calls. + bool openAttempted = false; + ShimDeviceClientAndCaptureOpen(() => openAttempted = true); + await connectionManager.AssertDeviceConnectionOpenAsync("test-device-id"); + Assert.False(openAttempted); + + // Check that all hubs are tried and DPS registration is eventually attempted if connection + // error indicates that the device doesn't exist in the target hub. + connectionManager = CreateConnectionManager(hubCache); + var connStrs = new List(); + ShimDeviceClientToFailAndCaptureConnectionString(capturedConnStr => connStrs.Add(capturedConnStr), new DeviceNotFoundException()); + var registrationAttempted = false; + ShimDpsAndCaptureRegistration("test-hub.azure.devices.net", () => registrationAttempted = true); + await ExpectToThrow(() => connectionManager.AssertDeviceConnectionOpenAsync("test-device-id")); + Assert.True((connStrs.Find(s => s.Contains("known-hub-1.azure.devices.net")) != null) && (connStrs.Find(s => s.Contains("known-hub-2.azure.devices.net")) != null)); + Assert.True(registrationAttempted); + + // Check that DPS registration is not attempted if connection attempt fails with unknown error. + registrationAttempted = false; + ShimDeviceClientToFail(new Exception()); + await ExpectToThrow(() => connectionManager.AssertDeviceConnectionOpenAsync("test-device-id")); + Assert.False(registrationAttempted); + } + } + + [Test] + public async Task AssertDeviceConnectionOpenAsyncDps() + { + using (ShimsContext.Create()) + { + var connectionManager = CreateConnectionManager(); + + // Checks that it attempts to connect to the hub returned by DPS. + string connStr = null; + ShimDps("test-hub.azure.devices.net"); + ShimDeviceClientAndCaptureConnectionString(capturedConnStr => connStr = capturedConnStr); + await connectionManager.AssertDeviceConnectionOpenAsync("dps-test-device-id"); + Assert.True(connStr.Contains("test-hub.azure.devices.net")); + + // Check that the hub returned by DPS was cached in the DB. + _storageProviderMock.Verify(p => p.AddOrUpdateHubCacheEntry(It.IsAny(), "dps-test-device-id", "test-hub.azure.devices.net"), Times.Once); + + // Checks that failure to save hub in DB cache doesn't fail the open operation. + connectionManager = CreateConnectionManager(); + _storageProviderMock.Setup(p => p.AddOrUpdateHubCacheEntry(It.IsAny(), It.IsAny(), It.IsAny())).Throws(new Exception()); + await connectionManager.AssertDeviceConnectionOpenAsync("dps-test-device-id"); + _storageProviderMock.Setup(p => p.AddOrUpdateHubCacheEntry(It.IsAny(), It.IsAny(), It.IsAny())).Verifiable(); + + // Check that the device client is cached and not reopened in subsequent calls. + bool openAttempted = false; + ShimDeviceClientAndCaptureOpen(() => openAttempted = true); + await connectionManager.AssertDeviceConnectionOpenAsync("dps-test-device-id"); + Assert.False(openAttempted); + + // Operation fails if DPS registration fails. + connectionManager = CreateConnectionManager(); + ShimDpsToFail(); + await ExpectToThrow(() => connectionManager.AssertDeviceConnectionOpenAsync("dps-test-device-id")); + + // Fails with DpsRegistrationFailedWithUnknownStatusException if DPS returns unknown response. + ShimDps("test-hub.azure.devices.net", ProvisioningRegistrationStatusType.Failed); + await ExpectToThrow(() => connectionManager.AssertDeviceConnectionOpenAsync("dps-test-device-id"), e => e is DpsRegistrationFailedWithUnknownStatusException); + } + } + + [Test] + public async Task AssertDeviceConnectionOpenAsyncClient() + { + using (ShimsContext.Create()) + { + // Uses correct connection string. + var connectionManager = CreateConnectionManager(); + string connStr = null; + ShimDps("test-hub.azure.devices.net"); + ShimDeviceClientAndCaptureConnectionString(capturedConnStr => connStr = capturedConnStr); + await connectionManager.AssertDeviceConnectionOpenAsync("test-device-id"); + using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes("test-sas-key"))) + { + var derivedKey = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes("test-device-id"))); + Assert.AreEqual(connStr, $"HostName=test-hub.azure.devices.net;DeviceId=test-device-id;SharedAccessKey={derivedKey}"); + } + + // Sets connection status change handler that updates local device connection status and calls user-defined + // callback if one exists and we're not in hub-probing phase. + connectionManager = CreateConnectionManager(); + var statusCallbackCalled = false; + connectionManager.SetConnectionStatusCallback("test-device-id", (_, __) => Task.FromResult(statusCallbackCalled = true)); + ShimDeviceClientAndEmitStatus(ConnectionStatus.Connected, ConnectionStatusChangeReason.Connection_Ok); + await connectionManager.AssertDeviceConnectionOpenAsync("test-device-id"); + Assert.True(statusCallbackCalled); + var status = connectionManager.GetDeviceStatus("test-device-id"); + Assert.AreEqual(status?.status, ConnectionStatus.Connected); + Assert.AreEqual(status?.reason, ConnectionStatusChangeReason.Connection_Ok); + + // Correctly sets desired property update, methods, and C2D message callbacks if they exist. + connectionManager = CreateConnectionManager(); + bool desiredPropertyCallbackCalled = false, methodCallbackCalled = false, c2dCallbackCalled = false; + await connectionManager.SetMethodCallbackAsync("test-device-id", "", (_, __) => { + methodCallbackCalled = true; + return Task.FromResult(new MethodResponse(200)); + }); + await connectionManager.SetMessageCallbackAsync("test-device-id", "", (_) => + { + c2dCallbackCalled = true; + return Task.FromResult(ReceiveMessageCallbackStatus.Accept); + }); + await connectionManager.SetDesiredPropertyUpdateCallbackAsync("test-device-id", "", (_, __) => Task.FromResult(desiredPropertyCallbackCalled = true)); + MethodCallback capturedMethodCallback = null; + ReceiveMessageCallback capturedMessageCallback = null; + DesiredPropertyUpdateCallback capturedPropertyUpdateCallback = null; + ShimDeviceClientAndCaptureAllHandlers(handler => capturedMethodCallback = handler, handler => capturedMessageCallback = handler, handler => capturedPropertyUpdateCallback = handler); + await connectionManager.AssertDeviceConnectionOpenAsync("test-device-id"); + await capturedMethodCallback(null, null); + await capturedMessageCallback(null, null); + await capturedPropertyUpdateCallback(null, null); + Assert.True(desiredPropertyCallbackCalled); + Assert.True(methodCallbackCalled); + Assert.True(c2dCallbackCalled); + } + } + + private ConnectionManager CreateConnectionManager(List hubCache = null) + { + _storageProviderMock.Setup(p => p.ListHubCacheEntries(It.IsAny())).Returns(Task.FromResult(hubCache ?? new List())); + return new ConnectionManager(LogManager.GetCurrentClassLogger(), "test-id-scope", Convert.ToBase64String(Encoding.UTF8.GetBytes("test-sas-key")), 50, _storageProviderMock.Object); + } + + /// + /// Shims SemaphoreSlime to capture the target semaphore of WaitAsync. + /// + /// Must be used within a ShimsContext. + /// Delegate called when semaphore is captured. + private static void CaptureSemaphoreOnWait(Action onCapture) + { + System.Threading.Fakes.ShimSemaphoreSlim.AllInstances.WaitAsync = (@this) => + { + onCapture(@this); + return ShimsContext.ExecuteWithoutShims(() => @this.WaitAsync()); + }; + } + + /// + /// Shims DPS registration to return a successful assignment. + /// + /// Must be used within a ShimsContext. + /// Hub to be returned in the assignment. + private static void ShimDps(string hubToAssign, ProvisioningRegistrationStatusType? status = null) + { + Microsoft.Azure.Devices.Provisioning.Client.Fakes.ShimProvisioningDeviceClient.AllInstances.RegisterAsync = (@this) => + Task.FromResult(new DeviceRegistrationResult("some-id", DateTime.Now, hubToAssign, "some-id", status ?? ProvisioningRegistrationStatusType.Assigned, "", DateTime.Now, 0, "", "")); + } + + /// + /// Shims DPS registration to fail. + /// + /// Must be used within a ShimsContext. + private static void ShimDpsToFail() + { + Microsoft.Azure.Devices.Provisioning.Client.Fakes.ShimProvisioningDeviceClient.AllInstances.RegisterAsync = (@this) => throw new Exception(); + } + + /// + /// Shims DPS registration to return a successful assignment and captures the registration call. + /// + /// Must be used within a ShimsContext. + /// Hub to be returned in the assignment. + /// Action to execute on registration. + private static void ShimDpsAndCaptureRegistration(string hubToAssign, Action onRegister) + { + Microsoft.Azure.Devices.Provisioning.Client.Fakes.ShimProvisioningDeviceClient.AllInstances.RegisterAsync = (@this) => + { + onRegister(); + return Task.FromResult(new DeviceRegistrationResult("some-id", DateTime.Now, hubToAssign, "some-id", ProvisioningRegistrationStatusType.Assigned, "", DateTime.Now, 0, "", "")); + }; + } + + /// + /// Shims the DeviceClient to return success in all calls. + /// + /// Must be used within a ShimsContext. + private static void ShimDeviceClient() + { + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.CreateFromConnectionStringStringITransportSettingsArrayClientOptions = (string connStr, ITransportSettings[] settings, ClientOptions _) => ShimsContext.ExecuteWithoutShims(() => DeviceClient.CreateFromConnectionString(connStr, settings)); + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.AllInstances.OpenAsync = (@this) => Task.CompletedTask; + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.AllInstances.CloseAsync = (@this) => Task.CompletedTask; + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.AllInstances.CompleteAsyncMessage = (@this, message) => Task.CompletedTask; + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.AllInstances.RejectAsyncMessage = (@this, message) => Task.CompletedTask; + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.AllInstances.AbandonAsyncMessage = (@this, message) => Task.CompletedTask; + } + + /// + /// Shims the DeviceClient to return success in all calls and captures open call. + /// + /// Must be used within a ShimsContext. + /// Delegate to be called when OpenAsync is called. + private static void ShimDeviceClientAndCaptureOpen(Action onOpen) + { + ShimDeviceClient(); + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.AllInstances.OpenAsync = (@this) => + { + onOpen(); + return Task.CompletedTask; + }; + } + + /// + /// Shims the DeviceClient to return success in all calls and captures close call. + /// + /// Must be used within a ShimsContext. + /// Delegate to be called when CloseAsync is called. + private static void ShimDeviceClientAndCaptureClose(Action onClose) + { + ShimDeviceClient(); + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.AllInstances.CloseAsync = (@this) => + { + onClose(); + return Task.CompletedTask; + }; + } + + /// + /// Shims the DeviceClient to emit a specific status when the status change handler is registered. + /// + /// Must be used within a ShimsContext. + /// status. + /// status reason. + private static void ShimDeviceClientAndEmitStatus(ConnectionStatus status, ConnectionStatusChangeReason reason) + { + ShimDeviceClient(); + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.AllInstances.SetConnectionStatusChangesHandlerConnectionStatusChangesHandler = (@this, handler) => + { + if (handler != null) + { + handler(status, reason); + } + }; + } + + private static void ShimDeviceClientAndCaptureAllHandlers(Action onMethodHandlerCaptured, Action onMessageHandlerCaptured, Action onDesiredPropertyHandlerCaptured) + { + ShimDeviceClient(); + + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.AllInstances.SetMethodDefaultHandlerAsyncMethodCallbackObject = (@this, handler, context) => + { + onMethodHandlerCaptured(handler); + return Task.CompletedTask; + }; + + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.AllInstances.SetReceiveMessageHandlerAsyncReceiveMessageCallbackObjectCancellationToken = (@this, handler, context, token) => + { + onMessageHandlerCaptured(handler); + return Task.CompletedTask; + }; + + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.AllInstances.SetDesiredPropertyUpdateCallbackAsyncDesiredPropertyUpdateCallbackObject = (@this, handler, context) => + { + onDesiredPropertyHandlerCaptured(handler); + return Task.CompletedTask; + }; + } + + /// + /// Shims the device client and capture the connection string used to create it. + /// + /// /// Must be used within a ShimsContext. + /// Action to execute when connection string is captured. + private static void ShimDeviceClientAndCaptureConnectionString(Action onCreate) + { + ShimDeviceClient(); + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.CreateFromConnectionStringStringITransportSettingsArrayClientOptions = (string connStr, ITransportSettings[] settings, ClientOptions _) => { + onCreate(connStr); + return ShimsContext.ExecuteWithoutShims(() => DeviceClient.CreateFromConnectionString(connStr, settings)); + }; + } + + /// + /// Shims the device client to fail and capture the connection string used to create it. + /// + /// /// Must be used within a ShimsContext. + /// Action to execute when connection string is captured. + /// /// Exception to throw. + private static void ShimDeviceClientToFailAndCaptureConnectionString(Action onCreate, Exception exception = null) + { + ShimDeviceClientToFail(); + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.CreateFromConnectionStringStringITransportSettingsArrayClientOptions = (string connStr, ITransportSettings[] settings, ClientOptions _) => { + onCreate(connStr); + throw exception ?? new Exception(); + }; + } + + /// + /// Shims the DeviceClient to fail in all calls. + /// + /// Must be used within a ShimsContext. + /// Exception to throw. + private static void ShimDeviceClientToFail(Exception exception = null) + { + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.CreateFromConnectionStringStringITransportSettingsArrayClientOptions = (string connStr, ITransportSettings[] settings, ClientOptions _) => throw exception ?? new Exception(); + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.AllInstances.OpenAsync = (@this) => throw exception ?? new Exception(); + Microsoft.Azure.Devices.Client.Fakes.ShimDeviceClient.AllInstances.CloseAsync = (@this) => throw exception ?? new Exception(); + } + + /// + /// Asserts that an async function throws. + /// + /// The async function to await. + private static async Task ExpectToThrow(Func fn, Func exceptionTest = null) + { + try + { + await fn(); + Assert.Fail("Expected function to throw"); + } + catch (Exception e) + { + if (exceptionTest != null && !exceptionTest(e)) + { + Assert.Fail("Exception didn't match test"); + } + } + } + + /// + /// Shims UtcNow to return a specific number of minutes into the future. + /// + /// Must be used within a ShimsContext. + /// How much to move the original time ahead. + private static void ShimUtcNowAhead(int minutes) + { + System.Fakes.ShimDateTimeOffset.UtcNowGet = () => ShimsContext.ExecuteWithoutShims(() => DateTimeOffset.UtcNow).AddMinutes(minutes); + } + + /// + /// Reverts UtcNow to its original behavior. + /// + /// Must be used within a ShimsContext. + private static void UnshimUtcNow() + { + System.Fakes.ShimDateTimeOffset.UtcNowGet = () => ShimsContext.ExecuteWithoutShims(() => DateTimeOffset.UtcNow); + } + } +} \ No newline at end of file