Skip to content

Commit

Permalink
JSON cleanup of IoT Hub service direct method + unit tests (#3011)
Browse files Browse the repository at this point in the history
* JSON cleanup of IoT Hub service direct method + unit tests

* Fix tests
  • Loading branch information
David R. Williamson authored Dec 8, 2022
1 parent 133034d commit 4dbe579
Show file tree
Hide file tree
Showing 13 changed files with 292 additions and 57 deletions.
2 changes: 2 additions & 0 deletions SDK v2 migration guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ What was a loose affiliation of separate clients is now a consolidated client wi

- The library now includes `IIotHubServiceRetryPolicy` implementations: `IotHubServiceExponentialBackoffRetryPolicy`, `IotHubServiceFixedDelayRetryPolicy`, `IotHubServiceIncrementalDelayRetryPolicy` and `IotHubServiceNoRetry`,
which can be set via `IotHubServiceClientOptions.RetryPolicy`.
- `DirectMethodClientResponse` now has a method `TryGetValue<T>` to deserialize the payload to a type of your choice.

#### API mapping

Expand All @@ -291,6 +292,7 @@ What was a loose affiliation of separate clients is now a consolidated client wi
| `ServiceClient.InvokeDeviceMethodAsync(...)` | `IotHubServiceClient.DirectMethods.InvokeAsync(...)` | |
| `CloudToDeviceMethod` | `DirectMethodServiceRequest` | Disambiguate from types in the device client.² |
| `CloudToDeviceMethodResult` | `DirectMethodClientResponse` | See² |
| `CloudToDeviceMethodResult.GetPayloadAsJson()` | `DirectMethodClientResponse.PayloadAsString` | |
| `ServiceClient.GetFeedbackReceiver(...)` | `IotHubServiceClient.MessageFeedback.MessageFeedbackProcessor` | |
| `ServiceClient.GetFileNotificationReceiver()` | `IotHubServiceClient.FileUploadNotifications.FileUploadNotificationProcessor` | |
| `IotHubException` | `IotHubServiceException` | Specify the exception is for Hub service client only. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ async Task TestOperationAsync(IotHubDeviceClient deviceClient, TestDevice testDe

// D2C Operation
VerboseTestLogger.WriteLine($"{nameof(CombinedClientOperationsPoolAmqpTests)}: Operation 1: Send D2C for device={testDevice.Id}");
var message = TelemetryE2ETests.ComposeD2cTestMessage(out string _, out string _);
TelemetryMessage message = TelemetryE2ETests.ComposeD2cTestMessage(out string _, out string _);
Task sendD2cMessage = deviceClient.SendTelemetryAsync(message);
clientOperations.Add(sendD2cMessage);

Expand Down
10 changes: 5 additions & 5 deletions e2e/test/iothub/device/MethodE2ECustomPayloadTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,21 +128,20 @@ private async Task SendMethodAndRespondAsync(
await Task.WhenAll(serviceSendTask, methodReceivedTask).ConfigureAwait(false);
}

public static async Task ServiceSendMethodAndVerifyResponseAsync(
public static async Task ServiceSendMethodAndVerifyResponseAsync<T>(
string deviceId,
string methodName,
object response,
object request,
T request,
TimeSpan responseTimeout = default,
IotHubServiceClientOptions serviceClientTransportSettings = default)
{
using var serviceClient = new IotHubServiceClient(TestConfiguration.IotHub.ConnectionString);
TimeSpan methodTimeout = responseTimeout == default ? s_defaultMethodTimeoutMinutes : responseTimeout;
VerboseTestLogger.WriteLine($"{nameof(ServiceSendMethodAndVerifyResponseAsync)}: Invoke method {methodName}.");

var directMethodRequest = new DirectMethodServiceRequest
var directMethodRequest = new DirectMethodServiceRequest(methodName)
{
MethodName = methodName,
ResponseTimeout = methodTimeout,
Payload = request,
};
Expand All @@ -153,7 +152,8 @@ public static async Task ServiceSendMethodAndVerifyResponseAsync(

VerboseTestLogger.WriteLine($"{nameof(ServiceSendMethodAndVerifyResponseAsync)}: Method status: {methodResponse.Status}.");
methodResponse.Status.Should().Be(200);
JsonConvert.SerializeObject(methodResponse.Payload).Should().BeEquivalentTo(JsonConvert.SerializeObject(response));
methodResponse.TryGetPayload(out T actual).Should().BeTrue();
JsonConvert.SerializeObject(actual).Should().BeEquivalentTo(JsonConvert.SerializeObject(response));
}

public static async Task<Task> SetDeviceReceiveMethod_booleanPayloadAsync(IotHubDeviceClient deviceClient, string methodName)
Expand Down
36 changes: 16 additions & 20 deletions e2e/test/iothub/device/MethodE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,8 @@ public async Task Method_ServiceInvokeDeviceMethodWithUnknownDeviceThrows()
{
// setup
using var serviceClient = new IotHubServiceClient(TestConfiguration.IotHub.ConnectionString);
var methodInvocation = new DirectMethodServiceRequest
var methodInvocation = new DirectMethodServiceRequest("SetTelemetryInterval")
{
MethodName = "SetTelemetryInterval",
Payload = "10"
};

Expand Down Expand Up @@ -175,9 +174,8 @@ public async Task Method_ServiceInvokeDeviceMethodWithUnknownModuleThrows()
// setup
using TestDevice testDevice = await TestDevice.GetTestDeviceAsync("ModuleNotFoundTest").ConfigureAwait(false);
using var serviceClient = new IotHubServiceClient(TestConfiguration.IotHub.ConnectionString);
var directMethodRequest = new DirectMethodServiceRequest
var directMethodRequest = new DirectMethodServiceRequest("SetTelemetryInterval")
{
MethodName = "SetTelemetryInterval",
Payload = "10",
};

Expand All @@ -199,7 +197,7 @@ public async Task Method_ServiceInvokeDeviceMethodWithNullPayload_DoesNotThrow()
{
// arrange

const string commandName = "Reboot";
const string methodName = "Reboot";
bool deviceMethodCalledSuccessfully = false;
TestDevice testDevice = await TestDevice.GetTestDeviceAsync("NullMethodPayloadTest").ConfigureAwait(false);
await using IotHubDeviceClient deviceClient = testDevice.CreateDeviceClient(new IotHubClientOptions(new IotHubClientMqttSettings()));
Expand All @@ -210,7 +208,7 @@ await deviceClient
.SetDirectMethodCallbackAsync(
(methodRequest) =>
{
methodRequest.MethodName.Should().Be(commandName);
methodRequest.MethodName.Should().Be(methodName);
deviceMethodCalledSuccessfully = true;
var response = new Client.DirectMethodResponse(200);

Expand All @@ -219,9 +217,8 @@ await deviceClient
.ConfigureAwait(false);

using var serviceClient = new IotHubServiceClient(TestConfiguration.IotHub.ConnectionString);
var directMethodRequest = new DirectMethodServiceRequest
var directMethodRequest = new DirectMethodServiceRequest(methodName)
{
MethodName = commandName,
ConnectionTimeout = TimeSpan.FromMinutes(1),
ResponseTimeout = TimeSpan.FromMinutes(1),
};
Expand Down Expand Up @@ -270,9 +267,8 @@ public static async Task ServiceSendMethodAndVerifyNotReceivedAsync(
TimeSpan methodTimeout = responseTimeout == default ? s_defaultMethodTimeoutMinutes : responseTimeout;
VerboseTestLogger.WriteLine($"{nameof(ServiceSendMethodAndVerifyResponseAsync)}: Invoke method {methodName}.");

var directMethodRequest = new DirectMethodServiceRequest
var directMethodRequest = new DirectMethodServiceRequest(methodName)
{
MethodName = methodName,
ResponseTimeout = methodTimeout,
};

Expand All @@ -289,10 +285,10 @@ public static async Task ServiceSendMethodAndVerifyNotReceivedAsync(
error.And.IsTransient.Should().BeTrue();
}

public static async Task ServiceSendMethodAndVerifyResponseAsync(
public static async Task ServiceSendMethodAndVerifyResponseAsync<T>(
string deviceId,
string methodName,
object respJson,
T respJson,
object reqJson,
TimeSpan responseTimeout = default,
IotHubServiceClientOptions serviceClientTransportSettings = default)
Expand All @@ -301,9 +297,8 @@ public static async Task ServiceSendMethodAndVerifyResponseAsync(
TimeSpan methodTimeout = responseTimeout == default ? s_defaultMethodTimeoutMinutes : responseTimeout;
VerboseTestLogger.WriteLine($"{nameof(ServiceSendMethodAndVerifyResponseAsync)}: Invoke method {methodName}.");

var directMethodRequest = new DirectMethodServiceRequest
var directMethodRequest = new DirectMethodServiceRequest(methodName)
{
MethodName = methodName,
ResponseTimeout = methodTimeout,
Payload = reqJson,
};
Expand All @@ -314,14 +309,15 @@ public static async Task ServiceSendMethodAndVerifyResponseAsync(

VerboseTestLogger.WriteLine($"{nameof(ServiceSendMethodAndVerifyResponseAsync)}: Method status: {response.Status}.");
response.Status.Should().Be(200);
JsonConvert.SerializeObject(response.Payload).Should().Be(JsonConvert.SerializeObject(respJson));
response.TryGetPayload(out T actual).Should().BeTrue();
JsonConvert.SerializeObject(actual).Should().Be(JsonConvert.SerializeObject(respJson));
}

public static async Task ServiceSendMethodAndVerifyResponseAsync(
public static async Task ServiceSendMethodAndVerifyResponseAsync<T>(
string deviceId,
string moduleId,
string methodName,
object respJson,
T respJson,
object reqJson,
TimeSpan responseTimeout = default,
IotHubServiceClientOptions serviceClientTransportSettings = default)
Expand All @@ -330,9 +326,8 @@ public static async Task ServiceSendMethodAndVerifyResponseAsync(

TimeSpan methodTimeout = responseTimeout == default ? s_defaultMethodTimeoutMinutes : responseTimeout;

var directMethodRequest = new DirectMethodServiceRequest
var directMethodRequest = new DirectMethodServiceRequest(methodName)
{
MethodName = methodName,
ResponseTimeout = methodTimeout,
Payload = reqJson,
};
Expand All @@ -344,7 +339,8 @@ public static async Task ServiceSendMethodAndVerifyResponseAsync(

VerboseTestLogger.WriteLine($"{nameof(ServiceSendMethodAndVerifyResponseAsync)}: Method status: {response.Status}.");
response.Status.Should().Be(200);
JsonConvert.SerializeObject(response.Payload).Should().Be(JsonConvert.SerializeObject(respJson));
response.TryGetPayload(out T actual).Should().BeTrue();
JsonConvert.SerializeObject(actual).Should().Be(JsonConvert.SerializeObject(respJson));
}

public static async Task<Task> SubscribeAndUnsubscribeMethodAsync(IotHubDeviceClient deviceClient, string methodName)
Expand Down
8 changes: 4 additions & 4 deletions e2e/test/iothub/device/MethodFaultInjectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ await SendMethodAndRespondRecoveryAsync(
.ConfigureAwait(false);
}

private async Task ServiceSendMethodAndVerifyResponseAsync(string deviceName, string methodName, object deviceResponsePayload, object serviceRequestPayload)
private async Task ServiceSendMethodAndVerifyResponseAsync<T>(string deviceName, string methodName, T deviceResponsePayload, object serviceRequestPayload)
{
var sw = Stopwatch.StartNew();
bool done = false;
Expand All @@ -224,9 +224,8 @@ private async Task ServiceSendMethodAndVerifyResponseAsync(string deviceName, st
{
using var serviceClient = new IotHubServiceClient(TestConfiguration.IotHub.ConnectionString);

var directMethodRequest = new DirectMethodServiceRequest
var directMethodRequest = new DirectMethodServiceRequest(methodName)
{
MethodName = methodName,
Payload = serviceRequestPayload,
ResponseTimeout = TimeSpan.FromMinutes(5),
};
Expand All @@ -239,7 +238,8 @@ private async Task ServiceSendMethodAndVerifyResponseAsync(string deviceName, st
VerboseTestLogger.WriteLine($"{nameof(ServiceSendMethodAndVerifyResponseAsync)}: Method status: {response.Status}.");

response.Status.Should().Be(200);
JsonConvert.SerializeObject(response.Payload).Should().Be(JsonConvert.SerializeObject(deviceResponsePayload));
response.TryGetPayload<T>(out T actual).Should().BeTrue();
JsonConvert.SerializeObject(actual).Should().Be(JsonConvert.SerializeObject(deviceResponsePayload));

done = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,18 @@ private static async Task Main(string[] args)
// Invoke the direct method on the device, passing the payload.
private static async Task InvokeMethodAsync(IotHubServiceClient serviceClient, string deviceId)
{
var methodInvocation = new DirectMethodServiceRequest
var methodInvocation = new DirectMethodServiceRequest("SetTelemetryInterval")
{
MethodName = "SetTelemetryInterval",
ResponseTimeout = TimeSpan.FromSeconds(30),
Payload = "10",
ResponseTimeout = TimeSpan.FromSeconds(30),
};

Console.WriteLine($"Invoking direct method for device: {deviceId}");

// Invoke the direct method asynchronously and get the response from the simulated device.
DirectMethodClientResponse response = await serviceClient.DirectMethods.InvokeAsync(deviceId, methodInvocation);

Console.WriteLine($"Response status: {response.Status}, payload:\n\t{JsonConvert.SerializeObject(response.Payload)}");
Console.WriteLine($"Response status: {response.Status}, payload:\n\t{JsonConvert.SerializeObject(response.PayloadAsString)}");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,9 @@ private async Task InvokeGetMaxMinReportCommandAsync()
const string getMaxMinReportCommandName = "getMaxMinReport";

// Create command name to invoke for component. The command is formatted as <component name>*<command name>
string commandToInvoke = $"{Thermostat1Component}*{getMaxMinReportCommandName}";
var commandInvocation = new DirectMethodServiceRequest
string commandName = $"{Thermostat1Component}*{getMaxMinReportCommandName}";
var commandInvocation = new DirectMethodServiceRequest(commandName)
{
MethodName = commandToInvoke,
ResponseTimeout = TimeSpan.FromSeconds(30),
Payload = DateTimeOffset.Now.Subtract(TimeSpan.FromMinutes(2)),
};
Expand All @@ -73,7 +72,7 @@ private async Task InvokeGetMaxMinReportCommandAsync()
{
DirectMethodClientResponse result = await _serviceClient.DirectMethods.InvokeAsync(_deviceId, commandInvocation);
_logger.LogDebug($"Command {getMaxMinReportCommandName} was invoked on component {Thermostat1Component}." +
$"\nDevice returned status: {result.Status}. \nReport: {result.Payload}");
$"\nDevice returned status: {result.Status}. \nReport: {result.PayloadAsString}");
}
catch (IotHubServiceException ex) when (ex.ErrorCode == IotHubServiceErrorCode.DeviceNotFound)
{
Expand All @@ -85,26 +84,25 @@ private async Task InvokeGetMaxMinReportCommandAsync()
private async Task InvokeRebootCommandAsync()
{
// Create command name to invoke for component
const string commandToInvoke = "reboot";
var commandInvocation = new DirectMethodServiceRequest
const string commandName = "reboot";
var commandInvocation = new DirectMethodServiceRequest(commandName)
{
MethodName = commandToInvoke,
ResponseTimeout = TimeSpan.FromSeconds(30),
Payload = JsonConvert.SerializeObject(3),
};

_logger.LogDebug($"Invoke the {commandToInvoke} command on the {_deviceId} device twin." +
_logger.LogDebug($"Invoke the {commandName} command on the {_deviceId} device twin." +
$"\nThis will set the \"targetTemperature\" on \"Thermostat\" component to 0.");

try
{
DirectMethodClientResponse result = await _serviceClient.DirectMethods.InvokeAsync(_deviceId, commandInvocation);
_logger.LogDebug($"Command {commandToInvoke} was invoked on the {_deviceId} device twin." +
_logger.LogDebug($"Command {commandName} was invoked on the {_deviceId} device twin." +
$"\nDevice returned status: {result.Status}.");
}
catch (IotHubServiceException ex) when (ex.ErrorCode == IotHubServiceErrorCode.DeviceNotFound)
{
_logger.LogWarning($"Unable to execute command {commandToInvoke} on component {Thermostat1Component}." +
_logger.LogWarning($"Unable to execute command {commandName} on component {Thermostat1Component}." +
$"\nMake sure that the device sample TemperatureController located in {DeviceSampleLink} is also running.");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,8 @@ private async Task InvokeGetMaxMinReportCommandAsync()
const string getMaxMinReportCommandName = "getMaxMinReport";

// Create command name to invoke for component
var commandInvocation = new DirectMethodServiceRequest
var commandInvocation = new DirectMethodServiceRequest(getMaxMinReportCommandName)
{
MethodName = getMaxMinReportCommandName,
ResponseTimeout = TimeSpan.FromSeconds(30),
Payload = DateTimeOffset.Now.Subtract(TimeSpan.FromMinutes(2)),
};
Expand All @@ -87,7 +86,7 @@ private async Task InvokeGetMaxMinReportCommandAsync()
DirectMethodClientResponse result = await _serviceClient.DirectMethods.InvokeAsync(_deviceId, commandInvocation);

_logger.LogDebug($"Command {getMaxMinReportCommandName} was invoked on device twin {_deviceId}." +
$"\nDevice returned status: {result.Status}. \nReport: {result.Payload}");
$"\nDevice returned status: {result.Status}. \nReport: {result.PayloadAsString}");
}
catch (IotHubServiceException ex) when (ex.ErrorCode == IotHubServiceErrorCode.DeviceNotFound)
{
Expand Down
3 changes: 1 addition & 2 deletions iothub/service/src/DirectMethod/DirectMethodsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ public class DirectMethodsClient
/// Creates an instance of this class. Provided for unit testing purposes only.
/// </summary>
protected DirectMethodsClient()
{
}
{ }

internal DirectMethodsClient(
string hostName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ protected internal DirectMethodClientResponse()
public int Status { get; protected internal set; }

/// <summary>
/// Get the payload object. May be null or empty.
/// Get the payload as a JSON string.
/// </summary>
/// <remarks>
/// The payload can be null or primitive type (e.g., string, int/array/list/dictionary/custom type)
/// To get the payload as a specified type, use <see cref="TryGetPayload{T}(out T)"/>.
/// </remarks>
[JsonIgnore]
public object Payload => JsonConvert.DeserializeObject((string)JsonPayload.Value);
public string PayloadAsString => JsonPayload.Value<string>();

[JsonProperty("payload")]
internal JRaw JsonPayload { get; set; }
Expand All @@ -56,7 +56,7 @@ public bool TryGetPayload<T>(out T value)

try
{
value = JsonPayload.Value<T>();
value = JsonConvert.DeserializeObject<T>(JsonPayload.Value<string>());
return true;
}
catch (JsonSerializationException)
Expand Down
Loading

0 comments on commit 4dbe579

Please sign in to comment.