From a8dded2e30257749705c1926b94378507abcc09a Mon Sep 17 00:00:00 2001 From: Abhipsa Misra Date: Thu, 3 Jun 2021 21:40:51 -0700 Subject: [PATCH] component property e2e tests --- e2e/test/Helpers/TestDeviceCallbackHandler.cs | 203 ++++++------ .../iothub/properties/PropertiesE2ETests.cs | 5 +- .../PropertiesWithComponentsE2ETests.cs | 298 +++++++++--------- iothub/device/src/ClientPropertyCollection.cs | 65 ++-- 4 files changed, 289 insertions(+), 282 deletions(-) diff --git a/e2e/test/Helpers/TestDeviceCallbackHandler.cs b/e2e/test/Helpers/TestDeviceCallbackHandler.cs index 1337ce0819..6f2025de76 100644 --- a/e2e/test/Helpers/TestDeviceCallbackHandler.cs +++ b/e2e/test/Helpers/TestDeviceCallbackHandler.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Linq; using System.Runtime.ExceptionServices; using System.Text; using System.Threading; @@ -60,31 +59,34 @@ public object ExpectedClientPropertyValue public async Task SetDeviceReceiveMethodAsync(string methodName, string deviceResponseJson, string expectedServiceRequestJson) { - await _deviceClient.SetMethodHandlerAsync(methodName, - (request, context) => - { - try + await _deviceClient + .SetMethodHandlerAsync( + methodName, + (request, context) => { - _logger.Trace($"{nameof(SetDeviceReceiveMethodAsync)}: DeviceClient {_testDevice.Id} callback method: {request.Name} {request.ResponseTimeout}."); - request.Name.Should().Be(methodName, "The expected method name should match what was sent from service"); - request.DataAsJson.Should().Be(expectedServiceRequestJson, "The expected method data should match what was sent from service"); - - return Task.FromResult(new MethodResponse(Encoding.UTF8.GetBytes(deviceResponseJson), 200)); - } - catch (Exception ex) - { - _logger.Trace($"{nameof(SetDeviceReceiveMethodAsync)}: Error during DeviceClient callback method: {ex}."); - - _methodExceptionDispatch = ExceptionDispatchInfo.Capture(ex); - return Task.FromResult(new MethodResponse(500)); - } - finally - { - // Always notify that we got the callback. - _methodCallbackSemaphore.Release(); - } - }, - null).ConfigureAwait(false); + try + { + _logger.Trace($"{nameof(SetDeviceReceiveMethodAsync)}: DeviceClient {_testDevice.Id} callback method: {request.Name} {request.ResponseTimeout}."); + request.Name.Should().Be(methodName, "The expected method name should match what was sent from service"); + request.DataAsJson.Should().Be(expectedServiceRequestJson, "The expected method data should match what was sent from service"); + + return Task.FromResult(new MethodResponse(Encoding.UTF8.GetBytes(deviceResponseJson), 200)); + } + catch (Exception ex) + { + _logger.Trace($"{nameof(SetDeviceReceiveMethodAsync)}: Error during DeviceClient callback method: {ex}."); + + _methodExceptionDispatch = ExceptionDispatchInfo.Capture(ex); + return Task.FromResult(new MethodResponse(500)); + } + finally + { + // Always notify that we got the callback. + _methodCallbackSemaphore.Release(); + } + }, + null) + .ConfigureAwait(false); } public async Task WaitForMethodCallbackAsync(CancellationToken ct) @@ -97,29 +99,32 @@ public async Task SetTwinPropertyUpdateCallbackHandlerAsync(string expectedPropN { string userContext = "myContext"; - await _deviceClient.SetDesiredPropertyUpdateCallbackAsync( - (patch, context) => - { - _logger.Trace($"{nameof(SetTwinPropertyUpdateCallbackHandlerAsync)}: DeviceClient {_testDevice.Id} callback twin: DesiredProperty: {patch}, {context}"); - - try - { - string propertyValue = patch[expectedPropName]; - propertyValue.Should().Be(ExpectedTwinPropertyValue, "The property value should match what was set by service"); - context.Should().Be(userContext, "The context should match what was set by service"); - } - catch (Exception ex) + await _deviceClient + .SetDesiredPropertyUpdateCallbackAsync( + (patch, context) => { - _twinExceptionDispatch = ExceptionDispatchInfo.Capture(ex); - } - finally - { - // Always notify that we got the callback. - _twinCallbackSemaphore.Release(); - } - - return Task.FromResult(true); - }, userContext).ConfigureAwait(false); + _logger.Trace($"{nameof(SetTwinPropertyUpdateCallbackHandlerAsync)}: DeviceClient {_testDevice.Id} callback twin: DesiredProperty: {patch}, {context}"); + + try + { + string propertyValue = patch[expectedPropName]; + propertyValue.Should().Be(ExpectedTwinPropertyValue, "The property value should match what was set by service"); + context.Should().Be(userContext, "The context should match what was set by service"); + } + catch (Exception ex) + { + _twinExceptionDispatch = ExceptionDispatchInfo.Capture(ex); + } + finally + { + // Always notify that we got the callback. + _twinCallbackSemaphore.Release(); + } + + return Task.FromResult(true); + }, + userContext) + .ConfigureAwait(false); } public async Task WaitForTwinCallbackAsync(CancellationToken ct) @@ -130,31 +135,33 @@ public async Task WaitForTwinCallbackAsync(CancellationToken ct) public async Task SetMessageReceiveCallbackHandlerAsync() { - await _deviceClient.SetReceiveMessageHandlerAsync( - async (receivedMessage, context) => - { - _logger.Trace($"{nameof(SetMessageReceiveCallbackHandlerAsync)}: DeviceClient {_testDevice.Id} received message with Id: {receivedMessage.MessageId}."); - - try - { - receivedMessage.MessageId.Should().Be(ExpectedMessageSentByService.MessageId, "Received message Id should match what was sent by service"); - receivedMessage.UserId.Should().Be(ExpectedMessageSentByService.UserId, "Received user Id should match what was sent by service"); - - await CompleteMessageAsync(receivedMessage).ConfigureAwait(false); - _logger.Trace($"{nameof(SetMessageReceiveCallbackHandlerAsync)}: DeviceClient completed message with Id: {receivedMessage.MessageId}."); - } - catch (Exception ex) - { - _logger.Trace($"{nameof(SetMessageReceiveCallbackHandlerAsync)}: Error during DeviceClient receive message callback: {ex}."); - _receiveMessageExceptionDispatch = ExceptionDispatchInfo.Capture(ex); - } - finally + await _deviceClient + .SetReceiveMessageHandlerAsync( + async (receivedMessage, context) => { - // Always notify that we got the callback. - _receivedMessageCallbackSemaphore.Release(); - } - }, - null).ConfigureAwait(false); + _logger.Trace($"{nameof(SetMessageReceiveCallbackHandlerAsync)}: DeviceClient {_testDevice.Id} received message with Id: {receivedMessage.MessageId}."); + + try + { + receivedMessage.MessageId.Should().Be(ExpectedMessageSentByService.MessageId, "Received message Id should match what was sent by service"); + receivedMessage.UserId.Should().Be(ExpectedMessageSentByService.UserId, "Received user Id should match what was sent by service"); + + await CompleteMessageAsync(receivedMessage).ConfigureAwait(false); + _logger.Trace($"{nameof(SetMessageReceiveCallbackHandlerAsync)}: DeviceClient completed message with Id: {receivedMessage.MessageId}."); + } + catch (Exception ex) + { + _logger.Trace($"{nameof(SetMessageReceiveCallbackHandlerAsync)}: Error during DeviceClient receive message callback: {ex}."); + _receiveMessageExceptionDispatch = ExceptionDispatchInfo.Capture(ex); + } + finally + { + // Always notify that we got the callback. + _receivedMessageCallbackSemaphore.Release(); + } + }, + null) + .ConfigureAwait(false); } private async Task CompleteMessageAsync(Client.Message message) @@ -168,34 +175,40 @@ public async Task WaitForReceiveMessageCallbackAsync(CancellationToken ct) _receiveMessageExceptionDispatch?.Throw(); } - public async Task SetClientPropertyUpdateCallbackHandlerAsync(string expectedPropName) + public async Task SetClientPropertyUpdateCallbackHandlerAsync(string expectedPropName, string componentName = default) { 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) + await _deviceClient + .SubscribeToWritablePropertiesEventAsync( + (patch, context) => { - _clientPropertyExceptionDispatch = ExceptionDispatchInfo.Capture(ex); - } - finally - { - // Always notify that we got the callback. - _clientPropertyCallbackSemaphore.Release(); - } - - return Task.FromResult(true); - }, userContext).ConfigureAwait(false); + _logger.Trace($"{nameof(SetClientPropertyUpdateCallbackHandlerAsync)}: DeviceClient {_testDevice.Id} callback property: WritableProperty: {patch}, {context}"); + + try + { + bool isPropertyPresent = componentName == null + ? patch.TryGetValue(expectedPropName, out T propertyFromCollection) + : patch.TryGetValue(componentName, expectedPropName, out 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) diff --git a/e2e/test/iothub/properties/PropertiesE2ETests.cs b/e2e/test/iothub/properties/PropertiesE2ETests.cs index cfd0adebd8..75129b04fc 100644 --- a/e2e/test/iothub/properties/PropertiesE2ETests.cs +++ b/e2e/test/iothub/properties/PropertiesE2ETests.cs @@ -379,7 +379,7 @@ await deviceClient prop2Value.Should().Be(propValue); // Sending a null value for a property will result in service removing the property from the client's twin representation. - // As a result, for the property patch sent here will result in propName2 being removed. + // For the property patch sent here will result in propName2 being removed. await deviceClient .UpdateClientPropertiesAsync( new ClientPropertyCollection @@ -396,8 +396,7 @@ await deviceClient string serializedActualProperty = JsonConvert.SerializeObject(serviceTwin.Properties.Reported[propName1]); serializedActualProperty.Should().Be(propEmptyValue); - // Sending a null value for a property will result in service removing the property from the client's twin representation. - // As a result, for the property patch sent here will result in propName1 being removed. + // For the property patch sent here will result in propName1 being removed. await deviceClient .UpdateClientPropertiesAsync( new ClientPropertyCollection diff --git a/e2e/test/iothub/properties/PropertiesWithComponentsE2ETests.cs b/e2e/test/iothub/properties/PropertiesWithComponentsE2ETests.cs index f3c43c1a37..0403b51fa7 100644 --- a/e2e/test/iothub/properties/PropertiesWithComponentsE2ETests.cs +++ b/e2e/test/iothub/properties/PropertiesWithComponentsE2ETests.cs @@ -5,6 +5,7 @@ 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; @@ -19,22 +20,18 @@ namespace Microsoft.Azure.Devices.E2ETests.Properties [TestCategory("IoTHub")] public class PropertiesWithComponentsE2ETests : E2EMsTestBase { - public static string ComponentName = "testableComponent"; + public const string ComponentName = "testableComponent"; private readonly string _devicePrefix = $"E2E_{nameof(PropertiesWithComponentsE2ETests)}_"; - private static readonly RegistryManager _registryManager = RegistryManager.CreateFromConnectionString(Configuration.IoTHub.ConnectionString); + 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 CustomClientPropertyWithComponent - { - Id = 123, - Name = "someName" - } + { "key1", 123 }, + { "key2", "someString" }, + { "key3", true } }; [LoggedTestMethod] @@ -54,17 +51,17 @@ await PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackSingleDeviceAsync( } [LoggedTestMethod] - public async Task PropertiesWithComponents_DeviceSetsPropertyArrayAndGetsItBack_Mqtt() + public async Task PropertiesWithComponents_DeviceSetsPropertyMapAndGetsItBack_Mqtt() { - await PropertiesWithComponents_DeviceSetsPropertyArrayAndGetsItBackSingleDeviceAsync( + await PropertiesWithComponents_DeviceSetsPropertyMapAndGetsItBackSingleDeviceAsync( Client.TransportType.Mqtt_Tcp_Only) .ConfigureAwait(false); } [LoggedTestMethod] - public async Task PropertiesWithComponents_DeviceSetsPropertyArrayAndGetsItBack_MqttWs() + public async Task PropertiesWithComponents_DeviceSetsPropertyMapAndGetsItBack_MqttWs() { - await PropertiesWithComponents_DeviceSetsPropertyArrayAndGetsItBackSingleDeviceAsync( + await PropertiesWithComponents_DeviceSetsPropertyMapAndGetsItBackSingleDeviceAsync( Client.TransportType.Mqtt_WebSocket_Only) .ConfigureAwait(false); } @@ -92,7 +89,6 @@ public async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceR { await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( Client.TransportType.Mqtt_Tcp_Only, - SetClientPropertyUpdateCallbackHandlerAsync, Guid.NewGuid()) .ConfigureAwait(false); } @@ -102,28 +98,25 @@ public async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceR { await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( Client.TransportType.Mqtt_WebSocket_Only, - SetClientPropertyUpdateCallbackHandlerAsync, Guid.NewGuid()) .ConfigureAwait(false); } [LoggedTestMethod] - public async Task PropertiesWithComponents_ServiceSetsWritablePropertyArrayAndDeviceReceivesEvent_Mqtt() + public async Task PropertiesWithComponents_ServiceSetsWritablePropertyMapAndDeviceReceivesEvent_Mqtt() { await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( Client.TransportType.Mqtt_Tcp_Only, - SetClientPropertyUpdateCallbackHandlerAsync, - s_listOfPropertyValues) + s_mapOfPropertyValues) .ConfigureAwait(false); } [LoggedTestMethod] - public async Task PropertiesWithComponents_ServiceSetsWritablePropertyArrayAndDeviceReceivesEvent_MqttWs() + public async Task PropertiesWithComponents_ServiceSetsWritablePropertyMapAndDeviceReceivesEvent_MqttWs() { await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( Client.TransportType.Mqtt_WebSocket_Only, - SetClientPropertyUpdateCallbackHandlerAsync, - s_listOfPropertyValues) + s_mapOfPropertyValues) .ConfigureAwait(false); } @@ -160,17 +153,17 @@ await PropertiesWithComponents_DeviceSetsPropertyAndServiceReceivesItAsync( } [LoggedTestMethod] - public async Task PropertiesWithComponents_ServiceDoesNotCreateNullPropertyInCollection_Mqtt() + public async Task Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingItAsync_Mqtt() { - await PropertiesWithComponents_ServiceDoesNotCreateNullPropertyInCollectionAsync( + await Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingItAsync( Client.TransportType.Mqtt_Tcp_Only) .ConfigureAwait(false); } [LoggedTestMethod] - public async Task PropertiesWithComponents_ServiceDoesNotCreateNullPropertyInCollection_MqttWs() + public async Task Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingItAsync_MqttWs() { - await PropertiesWithComponents_ServiceDoesNotCreateNullPropertyInCollectionAsync( + await Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingItAsync( Client.TransportType.Mqtt_WebSocket_Only) .ConfigureAwait(false); } @@ -199,17 +192,17 @@ private async Task PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackSingl await PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackAsync(deviceClient, testDevice.Id, Guid.NewGuid().ToString(), Logger).ConfigureAwait(false); } - private async Task PropertiesWithComponents_DeviceSetsPropertyArrayAndGetsItBackSingleDeviceAsync(Client.TransportType transport) + private async Task PropertiesWithComponents_DeviceSetsPropertyMapAndGetsItBackSingleDeviceAsync(Client.TransportType transport) { TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); using var deviceClient = DeviceClient.CreateFromConnectionString(testDevice.ConnectionString, transport); - await PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackAsync(deviceClient, testDevice.Id, s_listOfPropertyValues, Logger).ConfigureAwait(false); + await PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackAsync(deviceClient, testDevice.Id, s_mapOfPropertyValues, Logger).ConfigureAwait(false); } public static async Task PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackAsync(DeviceClient deviceClient, string deviceId, T propValue, MsTestLogger logger) { - var propName = Guid.NewGuid().ToString(); + string propName = Guid.NewGuid().ToString(); logger.Trace($"{nameof(PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackAsync)}: name={propName}, value={propValue}"); @@ -217,61 +210,19 @@ public static async Task PropertiesWithComponents_DeviceSetsPropertyAndGetsItBac props.AddComponentProperty(ComponentName, propName, propValue); await deviceClient.UpdateClientPropertiesAsync(props).ConfigureAwait(false); - // Validate the updated twin from the device-client - ClientProperties deviceTwin = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); - if (deviceTwin.TryGetValue(ComponentName, propName, out var propFromCollection)) - { - Assert.AreEqual(JsonConvert.SerializeObject(propFromCollection), JsonConvert.SerializeObject(propValue)); - } - else - { - Assert.Fail($"The property {propName} was not found in the collection"); - } - // Validate the updated twin from the service-client - Twin completeTwin = await _registryManager.GetTwinAsync(deviceId).ConfigureAwait(false); - var actualProp = completeTwin.Properties.Reported[ComponentName][propName]; - Assert.AreEqual(JsonConvert.SerializeObject(actualProp), JsonConvert.SerializeObject(propValue)); - } - - public static async Task SetClientPropertyUpdateCallbackHandlerAsync(DeviceClient deviceClient, string expectedComponentName, 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(expectedComponentName, expectedPropName, out var propertyFromCollection)) - { - //Assert.AreEqual(JsonConvert.SerializeObject(JsonConvert.DeserializeObject(propertyFromCollection.Replace("\\\"", ""))), JsonConvert.SerializeObject(expectedPropValue)); - 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); - } + // Validate the updated properties from the device-client + ClientProperties clientProperties = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); + bool isPropertyPresent = clientProperties.TryGetValue(ComponentName, propName, out T propFromCollection); + isPropertyPresent.Should().BeTrue(); + propFromCollection.Should().BeEquivalentTo(propValue); - return Task.FromResult(true); - }, - userContext) - .ConfigureAwait(false); + // Validate the updated twin from the service-client + Twin completeTwin = await s_registryManager.GetTwinAsync(deviceId).ConfigureAwait(false); + dynamic actualProp = completeTwin.Properties.Reported[ComponentName][propName]; - 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 componentName, string propName, T propValue) @@ -279,18 +230,12 @@ public static async Task RegistryManagerUpdateWritablePropertyAsync(string de using var registryManager = RegistryManager.CreateFromConnectionString(Configuration.IoTHub.ConnectionString); var twinPatch = new Twin(); - twinPatch.Properties.Desired[componentName] = new + var componentProperties = new TwinCollection { - __t = "c", + [ConventionBasedConstants.ComponentIdentifierKey] = ConventionBasedConstants.ComponentIdentifierValue, + [propName] = propValue }; - if (propValue is List) - { - twinPatch.Properties.Desired[componentName][propName] = (Newtonsoft.Json.Linq.JToken)(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(propValue))); - } - else - { - twinPatch.Properties.Desired[componentName][propName] = propValue; - } + twinPatch.Properties.Desired[componentName] = componentProperties; await registryManager.UpdateTwinAsync(deviceId, twinPatch, "*").ConfigureAwait(false); await registryManager.CloseAsync().ConfigureAwait(false); @@ -298,7 +243,7 @@ public static async Task RegistryManagerUpdateWritablePropertyAsync(string de private async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceUnsubscribes(Client.TransportType transport, T propValue) { - var propName = Guid.NewGuid().ToString(); + string propName = Guid.NewGuid().ToString(); Logger.Trace($"{nameof(PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync)}: name={propName}, value={propValue}"); @@ -310,12 +255,10 @@ await deviceClient. SubscribeToWritablePropertiesEventAsync( (patch, context) => { - Logger.Trace($"{nameof(SetClientPropertyUpdateCallbackHandlerAsync)}: WritableProperty: {patch}, {context}"); - - // After unsubscribing it should never reach here - Assert.IsNull(patch); + Assert.Fail("After having unsubscribed from receiving client property update notifications " + + "this callback should not have been invoked."); - return Task.FromResult(true); + return Task.FromResult(true); }, null) .ConfigureAwait(false); @@ -331,32 +274,37 @@ await RegistryManagerUpdateWritablePropertyAsync(testDevice.Id, ComponentName, p await deviceClient.CloseAsync().ConfigureAwait(false); } - private async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync(Client.TransportType transport, Func> setTwinPropertyUpdateCallbackAsync, T propValue) + private async Task PropertiesWithComponents_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(PropertiesWithComponents_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, ComponentName, propName, propValue, Logger).ConfigureAwait(false); + await testDeviceCallbackHandler.SetClientPropertyUpdateCallbackHandlerAsync(propName, ComponentName).ConfigureAwait(false); + testDeviceCallbackHandler.ExpectedClientPropertyValue = propValue; await Task.WhenAll( RegistryManagerUpdateWritablePropertyAsync(testDevice.Id, ComponentName, 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(ComponentName, propName, out var propFromCollection)) - { - Assert.AreEqual(JsonConvert.SerializeObject(propValue), JsonConvert.SerializeObject(propFromCollection)); - } + // Validate the updated properties from the device-client + ClientProperties clientProperties = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); + bool isPropertyPresent = clientProperties.Writable.TryGetValue(ComponentName, propName, out T propFromCollection); + isPropertyPresent.Should().BeTrue(); + propFromCollection.Should().BeEquivalentTo(propValue); // Validate the updated twin from the service-client - Twin completeTwin = await _registryManager.GetTwinAsync(testDevice.Id).ConfigureAwait(false); - var actualProp = completeTwin.Properties.Desired[ComponentName][propName]; - Assert.AreEqual(JsonConvert.SerializeObject(actualProp), JsonConvert.SerializeObject(propValue)); + Twin completeTwin = await s_registryManager.GetTwinAsync(testDevice.Id).ConfigureAwait(false); + dynamic actualProp = completeTwin.Properties.Desired[ComponentName][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); @@ -364,34 +312,35 @@ await Task.WhenAll( private async Task PropertiesWithComponents_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); using var deviceClient = DeviceClient.CreateFromConnectionString(testDevice.ConnectionString, transport); var twinPatch = new Twin(); - twinPatch.Properties.Desired[propName] = propValue; + var componentProperties = new TwinCollection + { + [ConventionBasedConstants.ComponentIdentifierKey] = ConventionBasedConstants.ComponentIdentifierValue, + [propName] = propValue + }; + twinPatch.Properties.Desired[ComponentName] = componentProperties; 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(ComponentName, propName, out string propFromCollection); + isPropertyPresent.Should().BeTrue(); + propFromCollection.Should().Be(propValue); + await deviceClient.CloseAsync().ConfigureAwait(false); await registryManager.CloseAsync().ConfigureAwait(false); } private async Task PropertiesWithComponents_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); @@ -403,58 +352,100 @@ private async Task PropertiesWithComponents_DeviceSetsPropertyAndServiceReceives await deviceClient.CloseAsync().ConfigureAwait(false); Twin serviceTwin = await registryManager.GetTwinAsync(testDevice.Id).ConfigureAwait(false); - Assert.AreEqual(serviceTwin.Properties.Reported[ComponentName][propName].ToString(), propValue); + dynamic actualProp = serviceTwin.Properties.Reported[ComponentName][propName]; - Logger.Trace("verified " + serviceTwin.Properties.Reported[ComponentName][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 PropertiesWithComponents_ServiceDoesNotCreateNullPropertyInCollectionAsync(Client.TransportType transport) + private async Task Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingItAsync(Client.TransportType transport) { - var propName1 = Guid.NewGuid().ToString(); - var propName2 = Guid.NewGuid().ToString(); - var propEmptyValue = "{}"; + string propName1 = Guid.NewGuid().ToString(); + string propName2 = Guid.NewGuid().ToString(); + string propValue = Guid.NewGuid().ToString(); + string propEmptyValue = "{}"; 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 prop1 = new ClientPropertyCollection(); - prop1.AddComponentProperty(ComponentName, propName1, null); - await deviceClient.UpdateClientPropertiesAsync(prop1).ConfigureAwait(false); + // First send a property patch with valid values for both prop1 and prop2. + var propertyPatch1 = new ClientPropertyCollection(); + propertyPatch1.AddComponentProperty( + ComponentName, + propName1, + new Dictionary + { + [propName2] = propValue + }); + await deviceClient.UpdateClientPropertiesAsync(propertyPatch1).ConfigureAwait(false); Twin serviceTwin = await registryManager.GetTwinAsync(testDevice.Id).ConfigureAwait(false); - Assert.IsTrue(serviceTwin.Properties.Reported.Contains(ComponentName)); - Assert.IsFalse(serviceTwin.Properties.Reported[ComponentName].Contains(propName1)); + serviceTwin.Properties.Reported.Contains(ComponentName).Should().BeTrue(); + + TwinCollection componentPatch1 = serviceTwin.Properties.Reported[ComponentName]; + componentPatch1.Contains(propName1).Should().BeTrue(); + + TwinCollection property1Value = componentPatch1[propName1]; + property1Value.Contains(propName2).Should().BeTrue(); - var prop2 = new ClientPropertyCollection(); - prop2.AddComponentProperty( + string property2Value = property1Value[propName2]; + property2Value.Should().Be(propValue); + + // Sending a null value for a property will result in service removing the property from the client's twin representation. + // For the property patch sent here will result in propName2 being removed. + var propertyPatch2 = new ClientPropertyCollection(); + propertyPatch2.AddComponentProperty( ComponentName, propName1, new Dictionary { [propName2] = null }); - await deviceClient.UpdateClientPropertiesAsync(prop2).ConfigureAwait(false); + await deviceClient.UpdateClientPropertiesAsync(propertyPatch2).ConfigureAwait(false); + + serviceTwin = await registryManager.GetTwinAsync(testDevice.Id).ConfigureAwait(false); + serviceTwin.Properties.Reported.Contains(ComponentName).Should().BeTrue(); + + TwinCollection componentPatch2 = serviceTwin.Properties.Reported[ComponentName]; + componentPatch2.Contains(propName1).Should().BeTrue(); + + string serializedActualProperty = JsonConvert.SerializeObject(componentPatch2[propName1]); + serializedActualProperty.Should().Be(propEmptyValue); + + // For the property patch sent here will result in propName1 being removed. + var propertyPatch3 = new ClientPropertyCollection(); + propertyPatch3.AddComponentProperty(ComponentName, propName1, null); + await deviceClient.UpdateClientPropertiesAsync(propertyPatch3).ConfigureAwait(false); serviceTwin = await registryManager.GetTwinAsync(testDevice.Id).ConfigureAwait(false); - Assert.IsTrue(serviceTwin.Properties.Reported.Contains(ComponentName)); - Assert.IsTrue(serviceTwin.Properties.Reported[ComponentName].Contains(propName1)); - string value1 = serviceTwin.Properties.Reported[ComponentName][propName1].ToString(); + serviceTwin.Properties.Reported.Contains(ComponentName).Should().BeTrue(); - Assert.AreEqual(value1, propEmptyValue); + // The only elements within the component should be the component identifiers. + TwinCollection componentPatch3 = serviceTwin.Properties.Reported[ComponentName]; + componentPatch3.Count.Should().Be(1); + componentPatch3.Contains(ConventionBasedConstants.ComponentIdentifierKey).Should().BeTrue(); + + // For the property patch sent here will result in the component being removed. + var propertyPatch4 = new ClientPropertyCollection(); + propertyPatch4.AddComponentProperties(ComponentName, null); + await deviceClient.UpdateClientPropertiesAsync(propertyPatch4).ConfigureAwait(false); + + serviceTwin = await registryManager.GetTwinAsync(testDevice.Id).ConfigureAwait(false); + serviceTwin.Properties.Reported.Contains(ComponentName).Should().BeFalse(); } private async Task PropertiesWithComponents_ClientHandlesRejectionInvalidPropertyNameAsync(Client.TransportType transport) { - var propName1 = "$" + Guid.NewGuid().ToString(); - var propName2 = Guid.NewGuid().ToString(); + string propName1 = "$" + Guid.NewGuid().ToString(); + string propName2 = 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 exceptionThrown = false; - try + Func func = async () => { await deviceClient .UpdateClientPropertiesAsync( @@ -469,16 +460,11 @@ await deviceClient } }) .ConfigureAwait(false); - } - catch (IotHubException) - { - exceptionThrown = true; - } - - Assert.IsTrue(exceptionThrown, "IotHubException was expected for updating reported property with an invalid property name, but was not thrown."); + }; + await func.Should().ThrowAsync(); Twin serviceTwin = await registryManager.GetTwinAsync(testDevice.Id).ConfigureAwait(false); - Assert.IsFalse(serviceTwin.Properties.Reported.Contains(propName1)); + serviceTwin.Properties.Reported.Contains(ComponentName).Should().BeFalse(); } } diff --git a/iothub/device/src/ClientPropertyCollection.cs b/iothub/device/src/ClientPropertyCollection.cs index 45aef54502..36eb7267c4 100644 --- a/iothub/device/src/ClientPropertyCollection.cs +++ b/iothub/device/src/ClientPropertyCollection.cs @@ -48,7 +48,6 @@ public void AddComponentProperty(string componentName, string propertyName, obje /// /// - /// /// /// /// Adds the value to the collection. @@ -59,7 +58,6 @@ public void AddComponentProperties(string componentName, IDictionary AddInternal(properties, componentName, true); /// - /// /// /// /// Adds the values to the collection. @@ -79,6 +77,7 @@ public void AddComponentProperties(string componentName, IDictionary /// /// A collection of properties to add. + /// is null. public void Add(IDictionary properties) { if (properties == null) @@ -112,7 +111,6 @@ public void AddOrUpdateComponentProperty(string componentName, string propertyNa /// /// - /// /// /// If the collection has a key that matches this will overwrite the current value. Otherwise it will attempt to add this to the collection. /// @@ -127,8 +125,9 @@ public void AddOrUpdateComponentProperties(string componentName, IDictionary AddInternal(properties, componentName, true); /// - /// /// + /// + /// /// /// If the collection has a key that matches this will overwrite the current value. Otherwise it will attempt to add this to the collection. /// @@ -297,18 +296,20 @@ internal static ClientPropertyCollection FromClientTwinDictionary(IDictionaryThe component with the properties to add or update. /// Forces the collection to use the Add or Update behavior. /// Setting to true will simply overwrite the value. Setting to false will use - /// is null. + /// is null for a root-level property operation. private void AddInternal(IDictionary properties, string componentName = default, bool forceUpdate = false) { - if (properties == null) - { - throw new ArgumentNullException(nameof(properties)); - } - // If the componentName is null then simply add the key-value pair to Collection dictionary. // This will either insert a property or overwrite it if it already exists. if (componentName == null) { + // If both the component name and properties collection are null then throw a ArgumentNullException. + // This is not a valid use-case. + if (properties == null) + { + throw new ArgumentNullException(nameof(properties)); + } + foreach (KeyValuePair entry in properties) { if (forceUpdate) @@ -323,30 +324,38 @@ private void AddInternal(IDictionary properties, string componen } else { - // If the component name already exists within the dictionary, then the value is a dictionary containing the component level property key and values. - // Append this property dictionary to the existing property value dictionary (overwrite entries if they already exist, if forceUpdate is true). - // Otherwise, if the component name does not exist in the dictionary, then add this as a new entry. - var componentProperties = new Dictionary(); - if (Collection.ContainsKey(componentName)) - { - componentProperties = (Dictionary)Collection[componentName]; - } - foreach (KeyValuePair entry in properties) + Dictionary componentProperties = null; + + // If the supplied properties are non-null, then add or update the supplied property dictionary to the collection. + // If the supplied properties are null, then this operation is to remove a component from the client's twin representation. + // It is added to the collection as-is. + if (properties != null) { - if (forceUpdate) + // If the component name already exists within the dictionary, then the value is a dictionary containing the component level property key and values. + // Otherwise, it is added as a new entry. + componentProperties = new Dictionary(); + if (Collection.ContainsKey(componentName)) { - componentProperties[entry.Key] = entry.Value; + componentProperties = (Dictionary)Collection[componentName]; } - else + + foreach (KeyValuePair entry in properties) { - componentProperties.Add(entry.Key, entry.Value); + if (forceUpdate) + { + componentProperties[entry.Key] = entry.Value; + } + else + { + componentProperties.Add(entry.Key, entry.Value); + } } - } - // For a component level property, the property patch needs to contain the {"__t": "c"} component identifier. - if (!componentProperties.ContainsKey(ConventionBasedConstants.ComponentIdentifierKey)) - { - componentProperties[ConventionBasedConstants.ComponentIdentifierKey] = ConventionBasedConstants.ComponentIdentifierValue; + // For a component level property, the property patch needs to contain the {"__t": "c"} component identifier. + if (!componentProperties.ContainsKey(ConventionBasedConstants.ComponentIdentifierKey)) + { + componentProperties[ConventionBasedConstants.ComponentIdentifierKey] = ConventionBasedConstants.ComponentIdentifierValue; + } } Collection[componentName] = componentProperties;