diff --git a/e2e/test/Helpers/TestDeviceCallbackHandler.cs b/e2e/test/Helpers/TestDeviceCallbackHandler.cs index e2b075f91b..1337ce0819 100644 --- a/e2e/test/Helpers/TestDeviceCallbackHandler.cs +++ b/e2e/test/Helpers/TestDeviceCallbackHandler.cs @@ -29,6 +29,10 @@ public class TestDeviceCallbackHandler : IDisposable private ExceptionDispatchInfo _receiveMessageExceptionDispatch; private Message _expectedMessageSentByService = null; + private readonly SemaphoreSlim _clientPropertyCallbackSemaphore = new SemaphoreSlim(0, 1); + private ExceptionDispatchInfo _clientPropertyExceptionDispatch; + private object _expectedClientPropertyValue = null; + public TestDeviceCallbackHandler(DeviceClient deviceClient, TestDevice testDevice, MsTestLogger logger) { _deviceClient = deviceClient; @@ -48,6 +52,12 @@ public Message ExpectedMessageSentByService set => Volatile.Write(ref _expectedMessageSentByService, value); } + public object ExpectedClientPropertyValue + { + get => Volatile.Read(ref _expectedClientPropertyValue); + set => Volatile.Write(ref _expectedClientPropertyValue, value); + } + public async Task SetDeviceReceiveMethodAsync(string methodName, string deviceResponseJson, string expectedServiceRequestJson) { await _deviceClient.SetMethodHandlerAsync(methodName, @@ -158,6 +168,42 @@ public async Task WaitForReceiveMessageCallbackAsync(CancellationToken ct) _receiveMessageExceptionDispatch?.Throw(); } + public async Task SetClientPropertyUpdateCallbackHandlerAsync(string expectedPropName) + { + string userContext = "myContext"; + + await _deviceClient.SubscribeToWritablePropertiesEventAsync( + (patch, context) => + { + _logger.Trace($"{nameof(SetClientPropertyUpdateCallbackHandlerAsync)}: DeviceClient {_testDevice.Id} callback property: WritableProperty: {patch}, {context}"); + + try + { + bool isPropertyPresent = patch.TryGetValue(expectedPropName, out T propertyFromCollection); + isPropertyPresent.Should().BeTrue(); + propertyFromCollection.Should().BeEquivalentTo((T)ExpectedClientPropertyValue); + context.Should().Be(userContext); + } + catch (Exception ex) + { + _clientPropertyExceptionDispatch = ExceptionDispatchInfo.Capture(ex); + } + finally + { + // Always notify that we got the callback. + _clientPropertyCallbackSemaphore.Release(); + } + + return Task.FromResult(true); + }, userContext).ConfigureAwait(false); + } + + public async Task WaitForClientPropertyUpdateCallbcakAsync(CancellationToken ct) + { + await _clientPropertyCallbackSemaphore.WaitAsync(ct).ConfigureAwait(false); + _clientPropertyExceptionDispatch?.Throw(); + } + public void Dispose() { _methodCallbackSemaphore?.Dispose(); diff --git a/e2e/test/iothub/properties/PropertiesE2ETests.cs b/e2e/test/iothub/properties/PropertiesE2ETests.cs index f60d38d4c9..d1c6c4d1a1 100644 --- a/e2e/test/iothub/properties/PropertiesE2ETests.cs +++ b/e2e/test/iothub/properties/PropertiesE2ETests.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; +using FluentAssertions; using Microsoft.Azure.Devices.Client; using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Azure.Devices.E2ETests.Helpers; @@ -21,17 +23,13 @@ public class PropertiesE2ETests : E2EMsTestBase private readonly string _devicePrefix = $"E2E_{nameof(PropertiesE2ETests)}_"; private static readonly RegistryManager s_registryManager = RegistryManager.CreateFromConnectionString(Configuration.IoTHub.ConnectionString); + private static readonly TimeSpan s_maxWaitTimeForCallback = TimeSpan.FromSeconds(30); - private static readonly List s_listOfPropertyValues = new List + private static readonly Dictionary s_mapOfPropertyValues = new Dictionary { - 1, - "someString", - false, - new CustomClientProperty - { - Id = 123, - Name = "someName" - } + { "key1", 123 }, + { "key2", "someString" }, + { "key3", true } }; [LoggedTestMethod] @@ -51,17 +49,17 @@ await Properties_DeviceSetsPropertyAndGetsItBackSingleDeviceAsync( } [LoggedTestMethod] - public async Task Properties_DeviceSetsPropertyArrayAndGetsItBack_Mqtt() + public async Task Properties_DeviceSetsPropertyMapAndGetsItBack_Mqtt() { - await Properties_DeviceSetsPropertyArrayAndGetsItBackSingleDeviceAsync( + await Properties_DeviceSetsPropertyMapAndGetsItBackSingleDeviceAsync( Client.TransportType.Mqtt_Tcp_Only) .ConfigureAwait(false); } [LoggedTestMethod] - public async Task Properties_DeviceSetsPropertyArrayAndGetsItBack_MqttWs() + public async Task Properties_DeviceSetsPropertyMapAndGetsItBack_MqttWs() { - await Properties_DeviceSetsPropertyArrayAndGetsItBackSingleDeviceAsync( + await Properties_DeviceSetsPropertyMapAndGetsItBackSingleDeviceAsync( Client.TransportType.Mqtt_WebSocket_Only) .ConfigureAwait(false); } @@ -89,7 +87,6 @@ public async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesEvent_M { await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( Client.TransportType.Mqtt_Tcp_Only, - SetClientPropertyUpdateCallbackHandlerAsync, Guid.NewGuid().ToString()) .ConfigureAwait(false); } @@ -99,28 +96,25 @@ public async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesEvent_M { await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( Client.TransportType.Mqtt_WebSocket_Only, - SetClientPropertyUpdateCallbackHandlerAsync, Guid.NewGuid().ToString()) .ConfigureAwait(false); } [LoggedTestMethod] - public async Task Properties_ServiceSetsWritablePropertyArrayAndDeviceReceivesEvent_Mqtt() + public async Task Properties_ServiceSetsWritablePropertyMapAndDeviceReceivesEvent_Mqtt() { await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( Client.TransportType.Mqtt_Tcp_Only, - SetClientPropertyUpdateCallbackHandlerAsync, - s_listOfPropertyValues) + s_mapOfPropertyValues) .ConfigureAwait(false); } [LoggedTestMethod] - public async Task Properties_ServiceSetsWritablePropertyArrayAndDeviceReceivesEvent_MqttWs() + public async Task Properties_ServiceSetsWritablePropertyMapAndDeviceReceivesEvent_MqttWs() { await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( Client.TransportType.Mqtt_WebSocket_Only, - SetClientPropertyUpdateCallbackHandlerAsync, - s_listOfPropertyValues) + s_mapOfPropertyValues) .ConfigureAwait(false); } @@ -196,12 +190,12 @@ private async Task Properties_DeviceSetsPropertyAndGetsItBackSingleDeviceAsync(C await Properties_DeviceSetsPropertyAndGetsItBackAsync(deviceClient, testDevice.Id, Guid.NewGuid().ToString(), Logger).ConfigureAwait(false); } - private async Task Properties_DeviceSetsPropertyArrayAndGetsItBackSingleDeviceAsync(Client.TransportType transport) + private async Task Properties_DeviceSetsPropertyMapAndGetsItBackSingleDeviceAsync(Client.TransportType transport) { TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); using var deviceClient = DeviceClient.CreateFromConnectionString(testDevice.ConnectionString, transport); - await Properties_DeviceSetsPropertyAndGetsItBackAsync(deviceClient, testDevice.Id, s_listOfPropertyValues, Logger).ConfigureAwait(false); + await Properties_DeviceSetsPropertyAndGetsItBackAsync(deviceClient, testDevice.Id, s_mapOfPropertyValues, Logger).ConfigureAwait(false); } public static async Task Properties_DeviceSetsPropertyAndGetsItBackAsync(DeviceClient deviceClient, string deviceId, T propValue, MsTestLogger logger) @@ -216,59 +210,17 @@ public static async Task Properties_DeviceSetsPropertyAndGetsItBackAsync(Devi // Validate the updated twin from the device-client ClientProperties clientProperties = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); - if (clientProperties.TryGetValue(propName, out T propFromCollection)) - { - Assert.AreEqual(propFromCollection, propValue); - } - else - { - Assert.Fail($"The property {propName} was not found in the collection"); - } + bool isPropertyPresent = clientProperties.TryGetValue(propName, out T propFromCollection); + isPropertyPresent.Should().BeTrue(); + propFromCollection.Should().BeEquivalentTo(propValue); // Validate the updated twin from the service-client Twin completeTwin = await s_registryManager.GetTwinAsync(deviceId).ConfigureAwait(false); dynamic actualProp = completeTwin.Properties.Reported[propName]; - Assert.AreEqual(actualProp, propValue); - } - - public static async Task SetClientPropertyUpdateCallbackHandlerAsync(DeviceClient deviceClient, string expectedPropName, T expectedPropValue, MsTestLogger logger) - { - var propertyUpdateReceived = new TaskCompletionSource(); - string userContext = "myContext"; - - await deviceClient - .SubscribeToWritablePropertiesEventAsync( - (patch, context) => - { - logger.Trace($"{nameof(SetClientPropertyUpdateCallbackHandlerAsync)}: WritableProperty: {patch}, {context}"); - - try - { - if (patch.TryGetValue(expectedPropName, out var propertyFromCollection)) - { - Assert.AreEqual(JsonConvert.SerializeObject(expectedPropValue), JsonConvert.SerializeObject(propertyFromCollection)); - } - else - { - Assert.Fail("Property was not found in the collection."); - } - Assert.AreEqual(userContext, context, "Context"); - } - catch (Exception e) - { - propertyUpdateReceived.SetException(e); - } - finally - { - propertyUpdateReceived.SetResult(true); - } - - return Task.FromResult(true); - }, - userContext) - .ConfigureAwait(false); - return propertyUpdateReceived.Task; + // The value will be retrieved as a TwinCollection, so we'll serialize the value and then compare. + string serializedActualPropertyValue = JsonConvert.SerializeObject(actualProp); + serializedActualPropertyValue.Should().Be(JsonConvert.SerializeObject(propValue)); } public static async Task RegistryManagerUpdateWritablePropertyAsync(string deviceId, string propName, T propValue) @@ -276,14 +228,7 @@ public static async Task RegistryManagerUpdateWritablePropertyAsync(string de using var registryManager = RegistryManager.CreateFromConnectionString(Configuration.IoTHub.ConnectionString); var twinPatch = new Twin(); - if (propValue is List) - { - twinPatch.Properties.Desired[propName] = (Newtonsoft.Json.Linq.JToken)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(propValue)); - } - else - { - twinPatch.Properties.Desired[propName] = propValue; - } + twinPatch.Properties.Desired[propName] = propValue; await registryManager.UpdateTwinAsync(deviceId, twinPatch, "*").ConfigureAwait(false); await registryManager.CloseAsync().ConfigureAwait(false); @@ -291,7 +236,7 @@ public static async Task RegistryManagerUpdateWritablePropertyAsync(string de private async Task Properties_ServiceSetsWritablePropertyAndDeviceUnsubscribes(Client.TransportType transport, object propValue) { - var propName = Guid.NewGuid().ToString(); + string propName = Guid.NewGuid().ToString(); Logger.Trace($"{nameof(Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync)}: name={propName}, value={propValue}"); @@ -303,12 +248,10 @@ await deviceClient. SubscribeToWritablePropertiesEventAsync( (patch, context) => { - Logger.Trace($"{nameof(SetClientPropertyUpdateCallbackHandlerAsync)}: WritableProperty: {patch}, {context}"); + Assert.Fail("After having unsubscribed from receiving client property update notifications " + + "this callback should not have been invoked."); - // After unsubscribing it should never reach here - Assert.IsNull(patch); - - return Task.FromResult(true); + return Task.FromResult(true); }, null) .ConfigureAwait(false); @@ -324,36 +267,37 @@ await RegistryManagerUpdateWritablePropertyAsync(testDevice.Id, propName, propVa await deviceClient.CloseAsync().ConfigureAwait(false); } - private async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync(Client.TransportType transport, Func> setTwinPropertyUpdateCallbackAsync, T propValue) + private async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync(Client.TransportType transport, T propValue) { - var propName = Guid.NewGuid().ToString(); + using var cts = new CancellationTokenSource(s_maxWaitTimeForCallback); + string propName = Guid.NewGuid().ToString(); Logger.Trace($"{nameof(Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync)}: name={propName}, value={propValue}"); TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); using var deviceClient = DeviceClient.CreateFromConnectionString(testDevice.ConnectionString, transport); + using var testDeviceCallbackHandler = new TestDeviceCallbackHandler(deviceClient, testDevice, Logger); - Task updateReceivedTask = await setTwinPropertyUpdateCallbackAsync(deviceClient, propName, propValue, Logger).ConfigureAwait(false); + await testDeviceCallbackHandler.SetClientPropertyUpdateCallbackHandlerAsync(propName).ConfigureAwait(false); + testDeviceCallbackHandler.ExpectedClientPropertyValue = propValue; await Task.WhenAll( RegistryManagerUpdateWritablePropertyAsync(testDevice.Id, propName, propValue), - updateReceivedTask).ConfigureAwait(false); + testDeviceCallbackHandler.WaitForClientPropertyUpdateCallbcakAsync(cts.Token)).ConfigureAwait(false); // Validate the updated twin from the device-client - ClientProperties deviceTwin = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); - if (deviceTwin.Writable.TryGetValue(propName, out var propFromCollection)) - { - Assert.AreEqual(JsonConvert.SerializeObject(propFromCollection), JsonConvert.SerializeObject(propValue)); - } - else - { - Assert.Fail($"The property {propName} was not found in the Writable collection"); - } + ClientProperties clientProperties = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); + bool isPropertyPresent = clientProperties.Writable.TryGetValue(propName, out T propFromCollection); + isPropertyPresent.Should().BeTrue(); + propFromCollection.Should().BeEquivalentTo(propValue); // Validate the updated twin from the service-client Twin completeTwin = await s_registryManager.GetTwinAsync(testDevice.Id).ConfigureAwait(false); - var actualProp = completeTwin.Properties.Desired[propName]; - Assert.AreEqual(JsonConvert.SerializeObject(actualProp), JsonConvert.SerializeObject(propValue)); + dynamic actualProp = completeTwin.Properties.Desired[propName]; + + // The value will be retrieved as a TwinCollection, so we'll serialize the value and then compare. + string serializedActualPropertyValue = JsonConvert.SerializeObject(actualProp); + serializedActualPropertyValue.Should().Be(JsonConvert.SerializeObject(propValue)); await deviceClient.SubscribeToWritablePropertiesEventAsync(null, null).ConfigureAwait(false); await deviceClient.CloseAsync().ConfigureAwait(false); @@ -361,8 +305,8 @@ await Task.WhenAll( private async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGetAsync(Client.TransportType transport) { - var propName = Guid.NewGuid().ToString(); - var propValue = Guid.NewGuid().ToString(); + string propName = Guid.NewGuid().ToString(); + string propValue = Guid.NewGuid().ToString(); TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); using var registryManager = RegistryManager.CreateFromConnectionString(Configuration.IoTHub.ConnectionString); @@ -372,37 +316,35 @@ private async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesItOnNe twinPatch.Properties.Desired[propName] = propValue; await registryManager.UpdateTwinAsync(testDevice.Id, twinPatch, "*").ConfigureAwait(false); - ClientProperties deviceTwin = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); - if (deviceTwin.Writable.TryGetValue(propName, out string propFromCollection)) - { - Assert.AreEqual(propFromCollection, propValue); - } - else - { - Assert.Fail("Property not found in ClientProperties"); - } + ClientProperties clientProperties = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); + bool isPropertyPresent = clientProperties.Writable.TryGetValue(propName, out string propFromCollection); + isPropertyPresent.Should().BeTrue(); + propFromCollection.Should().Be(propValue); + await deviceClient.CloseAsync().ConfigureAwait(false); await registryManager.CloseAsync().ConfigureAwait(false); } private async Task Properties_DeviceSetsPropertyAndServiceReceivesItAsync(Client.TransportType transport) { - var propName = Guid.NewGuid().ToString(); - var propValue = Guid.NewGuid().ToString(); + string propName = Guid.NewGuid().ToString(); + string propValue = Guid.NewGuid().ToString(); TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); using var registryManager = RegistryManager.CreateFromConnectionString(Configuration.IoTHub.ConnectionString); using var deviceClient = DeviceClient.CreateFromConnectionString(testDevice.ConnectionString, transport); var patch = new ClientPropertyCollection(); - patch[propName] = propValue; + patch.AddRootProperty(propName, propValue); await deviceClient.UpdateClientPropertiesAsync(patch).ConfigureAwait(false); await deviceClient.CloseAsync().ConfigureAwait(false); Twin serviceTwin = await registryManager.GetTwinAsync(testDevice.Id).ConfigureAwait(false); - Assert.AreEqual(serviceTwin.Properties.Reported[propName].ToString(), propValue); + dynamic actualProp = serviceTwin.Properties.Reported[propName]; - Logger.Trace("verified " + serviceTwin.Properties.Reported[propName].ToString() + "=" + propValue); + // The value will be retrieved as a TwinCollection, so we'll serialize the value and then compare. + string serializedActualPropertyValue = JsonConvert.SerializeObject(actualProp); + serializedActualPropertyValue.Should().Be(JsonConvert.SerializeObject(propValue)); } private async Task Properties_ServiceDoesNotCreateNullPropertyInCollectionAsync(Client.TransportType transport)