Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support ad-hoc calls #129

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 207 additions & 3 deletions MetasysServices.Tests/MetasysClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Linq;
using Flurl.Http.Testing;
using NUnit.Framework;
using Newtonsoft.Json.Linq;
using JohnsonControls.Metasys.BasicServices;
using Nito.AsyncEx;
using System.Threading.Tasks;
using System.Globalization;
using JohnsonControls.Metasys.BasicServices.Enums;
using Flurl.Http;
using System.IO;
using System.Text;
using System.Net;
using System.Net.Http.Json;

namespace MetasysServices.Tests
{
Expand Down Expand Up @@ -1547,7 +1550,7 @@ public void TestGetNetworkDevicesWithClassification()
"\"items\": [", device, "],",
"\"self\": \"https://hostname/api/v2/networkDevices?page=1&pageSize=200&sort=name\"}"));

var devices = client.NetworkDevices.GetAsync(NetworkDeviceClassificationEnum.Controller).GetAwaiter().GetResult();
var devices = client.NetworkDevices.GetAsync(NetworkDeviceClassificationEnum.Controller).GetAwaiter().GetResult();

httpTest.ShouldHaveCalled($"https://hostname/api/v2/networkDevices")
.WithVerb(HttpMethod.Get)
Expand Down Expand Up @@ -2357,5 +2360,206 @@ public void TestMiscNullTokenValue()
}

#endregion

#region Ad-Hoc Calls
[TestCase("v5/networkDevices", "https://hostname/api/v5/networkDevices")]
[TestCase("v5/networkDevices?sort=itemReference", "https://hostname/api/v5/networkDevices?sort=itemReference")]
[TestCase("v5/networkDevices?sort=itemReference#fragment1", "https://hostname/api/v5/networkDevices?sort=itemReference#fragment1")]
[TestCase("/v5/networkDevices", "https://hostname/api/v5/networkDevices")]
[TestCase("/v5/networkDevices?sort=itemReference", "https://hostname/api/v5/networkDevices?sort=itemReference")]
[TestCase("/v5/networkDevices?sort=itemReference#fragment1", "https://hostname/api/v5/networkDevices?sort=itemReference#fragment1")]
[TestCase("https://hostname/api/v5/networkDevices", "https://hostname/api/v5/networkDevices")]
[TestCase("https://hostname/api/v5/networkDevices?sort=itemReference", "https://hostname/api/v5/networkDevices?sort=itemReference")]
[TestCase("https://hostname/api/v5/networkDevices?sort=itemReference#fragment1", "https://hostname/api/v5/networkDevices?sort=itemReference#fragment1")]
public async Task TestSendAsync(string requestUrl, string expectedUrl)
{
var httpRequest = new HttpRequestMessage(HttpMethod.Get, requestUrl);
await client.SendAsync(httpRequest);

httpTest.ShouldHaveCalled(expectedUrl)
.WithVerb(HttpMethod.Get)
.Times(1);
}

[Test]
public async Task TestSendAsyncCheckQueryStringAndFragmentsAreUsed()
{
var requestUrl = "/v5/networkDevices?sort=itemReference#fragment1";
var httpRequest = new HttpRequestMessage(HttpMethod.Get, requestUrl);
await client.SendAsync(httpRequest);

httpTest.ShouldHaveCalled("https://hostname/api/v5/networkDevices?sort=itemReference#fragment1")
.WithVerb(HttpMethod.Get)
.WithQueryParamValue("sort", "itemReference")
.With(call => call.Request.RequestUri.Fragment.Equals("#fragment1"))
.Times(1);
}

[Test]
public async Task TestSendAsyncCheckRequestHeadersAreUsed()
{
var requestUrl = "/v5/networkDevices?sort=itemReference";
var httpRequest = new HttpRequestMessage(HttpMethod.Get, requestUrl);
httpRequest.Headers.Add("request-header", "header_value");
await client.SendAsync(httpRequest);

httpTest.ShouldHaveCalled("https://hostname/api/v5/networkDevices?sort=itemReference")
.WithVerb(HttpMethod.Get)
.WithHeader("request-header", "header_value")
.Times(1);
}

[Test]
public void TestSendAsyncWithInvalidAbsoluteUrlThrowsException()
{
var requestUrl = "https://different-hostname/api/v5/networkDevices";
var httpRequest = new HttpRequestMessage(HttpMethod.Get, requestUrl);
var e = Assert.Throws<UriFormatException>(() =>
client.SendAsync(httpRequest).GetAwaiter().GetResult());

PrintMessage($"TestSendAsyncWithInvalidAbsoluteUrlThrowsException: {e.Message}", true);
}

[TestCase("v5/networkDevices", "v5/networkDevices")]
[TestCase("/v5/networkDevices", "v5/networkDevices")]
public async Task TestSendAsyncWithCustomPort(string requestUrl, string expectedRelativeUrl)
{
var originalHostname = client.Hostname;
var expectedUrl = $"https://{originalHostname}:8080/api/{expectedRelativeUrl}";

client.Hostname = $"{originalHostname}:8080";

var httpRequest = new HttpRequestMessage(HttpMethod.Get, requestUrl);
await client.SendAsync(httpRequest);

httpTest.ShouldHaveCalled(expectedUrl)
.WithVerb(HttpMethod.Get)
.Times(1);

client.Hostname = originalHostname;
}

[Test]
public async Task TestSendAsyncCanGetJson()
{
httpTest.RespondWithJson(new TestData { id = 1, name = "Metasys" });

var httpRequest = new HttpRequestMessage(HttpMethod.Get, "https://hostname/api/v5/getJson");
var data = await client.SendAsync(httpRequest).ReceiveJson<TestData>();

Assert.AreEqual(1, data.id);
Assert.AreEqual("Metasys", data.name);
}

[Test]
public async Task TestSendAsyncCanGetJsonDynamic()
{
httpTest.RespondWithJson(new { id = 1, name = "Metasys" });

var httpRequest = new HttpRequestMessage(HttpMethod.Get, "https://hostname/api/v5/getJsonDynamic");
var data = await client.SendAsync(httpRequest).ReceiveJson();

Assert.AreEqual(1, data.id);
Assert.AreEqual("Metasys", data.name);
}

[Test]
public async Task TestSendAsyncCanGetJsonDynamicList()
{
httpTest.RespondWithJson(
new[] {
new { id = 1, name = "Metasys" },
new { id = 2, name = "Client" }
}
);

var httpRequest = new HttpRequestMessage(HttpMethod.Get, "https://hostname/api/v5/getJsonDynamicList");
var data = await client.SendAsync(httpRequest).ReceiveJsonList();

Assert.AreEqual(1, data[0].id);
Assert.AreEqual("Metasys", data[0].name);
Assert.AreEqual(2, data[1].id);
Assert.AreEqual("Client", data[1].name);
}

[Test]
public async Task TestSendAsyncCanGetString()
{
httpTest.RespondWith("Metasys Client");

var httpRequest = new HttpRequestMessage(HttpMethod.Get, "https://hostname/api/v5/getString");
var data = await client.SendAsync(httpRequest).ReceiveString();

Assert.AreEqual("Metasys Client", data);
}

[Test]
public async Task TestSendAsyncCanGetStream()
{
httpTest.RespondWith("Metasys Client");

var httpRequest = new HttpRequestMessage(HttpMethod.Get, "https://hostname/api/v5/getStream");
var data = await client.SendAsync(httpRequest).ReceiveStream();

Assert.AreEqual(new MemoryStream(Encoding.UTF8.GetBytes("Metasys Client")), data);
}

[Test]
public async Task TestSendAsyncCanGetBytes()
{
httpTest.RespondWith("Metasys Client");

var httpRequest = new HttpRequestMessage(HttpMethod.Get, "https://hostname/api/v5/getBytes");
var data = await client.SendAsync(httpRequest).ReceiveBytes();

Assert.AreEqual(Encoding.UTF8.GetBytes("Metasys Client"), data);
}

[TestCase("PUT", "https://hostname/api/v5/put")]
[TestCase("POST", "https://hostname/api/v5/post")]
[TestCase("PATCH", "https://hostname/api/v5/patch")]
public async Task TestSendAsyncCanMakeRequestWithContent(string httpVerb, string requestUrl)
{
var content = JsonContent.Create(new { id = 1, name = "Metasys" });
var httpMethod = new HttpMethod(httpVerb);
var httpRequest = new HttpRequestMessage(httpMethod, requestUrl)
{
Content = content
};

var response = await client.SendAsync(httpRequest);

httpTest.ShouldHaveCalled(requestUrl)
.WithVerb(httpMethod)
.WithContentType("application/json")
.With(call => call.Request.Content.Equals(content))
.Times(1);

Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}

[TestCase("DELETE", "https://hostname/api/v5/delete")]
[TestCase("HEAD", "https://hostname/api/v5/head")]
[TestCase("TRACE", "https://hostname/api/v5/trace")]
[TestCase("OPTIONS", "https://hostname/api/v5/options")]
public async Task TestSendAsyncCanSupportsDeleteHeadTraceOptionsRequest(string httpVerb, string requestUrl)
{
var httpMethod = new HttpMethod(httpVerb);
var httpRequest = new HttpRequestMessage(httpMethod, requestUrl);
var response = await client.SendAsync(httpRequest);

httpTest.ShouldHaveCalled(requestUrl)
.WithVerb(httpMethod)
.Times(1);

Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}

private class TestData
{
public int id { get; set; }
public string name { get; set; }
}
#endregion
}
}
14 changes: 14 additions & 0 deletions MetasysServices/Interfaces/IMetasysClient.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using JohnsonControls.Metasys.BasicServices;

Expand Down Expand Up @@ -404,5 +406,17 @@ public interface IMetasysClient :IBasicService
/// <inheritdoc cref="IMetasysClient.GetServerTime()"/>
Task<DateTime> GetServerTimeAsync();

/// <summary>
/// Send an HTTP request as an asynchronous operation.
///
/// <para>
/// This method currently only supports 1 value per header rather than multiple. In a future revision, this is planned to be addressed.
/// </para>
/// </summary>
/// <param name="request">The HTTP request message to send.</param>
/// <param name="completionOption"> When the operation should complete (as soon as a response is available or after reading the whole response content).</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default);
}
}
44 changes: 44 additions & 0 deletions MetasysServices/MetasysClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
using System.Dynamic;
using System.IdentityModel.Tokens.Jwt;
using System.Timers;
using System.Net.Http.Headers;
using System.Threading;

namespace JohnsonControls.Metasys.BasicServices
{
Expand Down Expand Up @@ -1207,7 +1209,49 @@ private async Task SendCommandRequestAsync(Guid id, string command, IEnumerable<
}
}

#region "ad-hoc calls" // =========================================================================================================
// Support Ad-Hoc calls -----------------------------------------------------------------------------------------------------------
///<inheritdoc/>
public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage requestMessage, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default)
{
var response = new HttpResponseMessage();
try
{
var flurlRequest = Client.Request();
flurlRequest.Url = GetUrlFromHttpRequest(requestMessage);

// Flurl.Http 2.4.2 can only work with 1 value per header
// Once upgraded to Flurl 3.0.1, then multiple values can be supported
var headers = requestMessage.Headers.ToDictionary((kvp) => kvp.Key, (kvp) => kvp.Value.First());
flurlRequest.WithHeaders(headers);

response = await flurlRequest.SendAsync(requestMessage.Method, requestMessage.Content, cancellationToken, completionOption).ConfigureAwait(false);
}
catch (FlurlHttpException e)
{
ThrowHttpException(e);
}
return response;
}

private Url GetUrlFromHttpRequest(HttpRequestMessage requestMessage)
{
var baseUri = new Uri(Client.BaseUrl);
var requestUri = requestMessage.RequestUri.ToString();
if (Uri.IsWellFormedUriString(requestUri, UriKind.Absolute))
{
if (Uri.Compare(baseUri, requestMessage.RequestUri, UriComponents.SchemeAndServer, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase) != 0)
{
throw new UriFormatException("HTTP request can not be made. Scheme or Host is invalid.");
}
return new Url(requestUri);
}
else
{
return new Url(Url.Combine(baseUri.GetLeftPart(UriPartial.Authority), "/api/", requestUri));
}
}
#endregion
}

}
Expand Down
49 changes: 49 additions & 0 deletions MetasysServicesExampleApp/FeaturesDemo/AdHocCallsDemo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using JohnsonControls.Metasys.BasicServices;
using System;
using System.Net.Http;

namespace MetasysServicesExampleApp.FeaturesDemo
{
public class AdHocCallsDemo
{
private MetasysClient client;
private LogInitializer log;

public AdHocCallsDemo(MetasysClient client)
{
this.client = client;
log = new LogInitializer(typeof(AdHocCallsDemo));
}

public void Run()
{
try
{
Console.WriteLine("\nEnter the endpoint you want to make the request to (Example: \"https://hostname/api/v5/networkDevices?sort=itemReference\"): .");
string endpoint = Console.ReadLine();
Console.WriteLine($"\nMaking the call to {endpoint}...");
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, endpoint);

var response = client.SendAsync(httpRequest).Result;
var statusCode = ((int)response.StatusCode).ToString();
if (statusCode.StartsWith("2") || statusCode.StartsWith("3"))
{
Console.WriteLine($"\n \nStatus - {response.StatusCode}");
Console.WriteLine("\nResponse content - ");
var content = response.Content.ReadAsStringAsync().Result;
Console.WriteLine("\n \n" + content);
}
else
{
Console.WriteLine(string.Format("\n \nRequest failed with - {0}", response.StatusCode));
}
}
catch (Exception exception)
{
log.Logger.Error(string.Format("An error occured while making the request - {0}", exception.Message));
Console.WriteLine("\n \nAn Error occurred. Press Enter to return to Main Menu");
}
Console.ReadLine();
}
}
}
Loading