diff --git a/CryptoExchange.Net.UnitTests/CallResultTests.cs b/CryptoExchange.Net.UnitTests/CallResultTests.cs new file mode 100644 index 0000000..fc1ff8e --- /dev/null +++ b/CryptoExchange.Net.UnitTests/CallResultTests.cs @@ -0,0 +1,176 @@ +using CryptoExchange.Net.Objects; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.UnitTests +{ + [TestFixture()] + internal class CallResultTests + { + [Test] + public void TestBasicErrorCallResult() + { + var result = new CallResult(new ServerError("TestError")); + + Assert.AreEqual(result.Error.Message, "TestError"); + Assert.IsFalse(result); + Assert.IsFalse(result.Success); + } + + [Test] + public void TestBasicSuccessCallResult() + { + var result = new CallResult(null); + + Assert.IsNull(result.Error); + Assert.IsTrue(result); + Assert.IsTrue(result.Success); + } + + [Test] + public void TestCallResultError() + { + var result = new CallResult(new ServerError("TestError")); + + Assert.AreEqual(result.Error.Message, "TestError"); + Assert.IsNull(result.Data); + Assert.IsFalse(result); + Assert.IsFalse(result.Success); + } + + [Test] + public void TestCallResultSuccess() + { + var result = new CallResult(new object()); + + Assert.IsNull(result.Error); + Assert.IsNotNull(result.Data); + Assert.IsTrue(result); + Assert.IsTrue(result.Success); + } + + [Test] + public void TestCallResultSuccessAs() + { + var result = new CallResult(new TestObjectResult()); + var asResult = result.As(result.Data.InnerData); + + Assert.IsNull(asResult.Error); + Assert.IsNotNull(asResult.Data); + Assert.IsTrue(asResult.Data is TestObject2); + Assert.IsTrue(asResult); + Assert.IsTrue(asResult.Success); + } + + [Test] + public void TestCallResultErrorAs() + { + var result = new CallResult(new ServerError("TestError")); + var asResult = result.As(default); + + Assert.IsNotNull(asResult.Error); + Assert.AreEqual(asResult.Error.Message, "TestError"); + Assert.IsNull(asResult.Data); + Assert.IsFalse(asResult); + Assert.IsFalse(asResult.Success); + } + + [Test] + public void TestCallResultErrorAsError() + { + var result = new CallResult(new ServerError("TestError")); + var asResult = result.AsError(new ServerError("TestError2")); + + Assert.IsNotNull(asResult.Error); + Assert.AreEqual(asResult.Error.Message, "TestError2"); + Assert.IsNull(asResult.Data); + Assert.IsFalse(asResult); + Assert.IsFalse(asResult.Success); + } + + [Test] + public void TestWebCallResultErrorAsError() + { + var result = new WebCallResult(new ServerError("TestError")); + var asResult = result.AsError(new ServerError("TestError2")); + + Assert.IsNotNull(asResult.Error); + Assert.AreEqual(asResult.Error.Message, "TestError2"); + Assert.IsNull(asResult.Data); + Assert.IsFalse(asResult); + Assert.IsFalse(asResult.Success); + } + + [Test] + public void TestWebCallResultSuccessAsError() + { + var result = new WebCallResult( + System.Net.HttpStatusCode.OK, + new List>>(), + TimeSpan.FromSeconds(1), + "{}", + "https://test.com/api", + null, + HttpMethod.Get, + new List>>(), + new TestObjectResult(), + null); + var asResult = result.AsError(new ServerError("TestError2")); + + Assert.IsNotNull(asResult.Error); + Assert.AreEqual(asResult.Error.Message, "TestError2"); + Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK); + Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1)); + Assert.AreEqual(asResult.RequestUrl, "https://test.com/api"); + Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get); + Assert.IsNull(asResult.Data); + Assert.IsFalse(asResult); + Assert.IsFalse(asResult.Success); + } + + [Test] + public void TestWebCallResultSuccessAsSuccess() + { + var result = new WebCallResult( + System.Net.HttpStatusCode.OK, + new List>>(), + TimeSpan.FromSeconds(1), + "{}", + "https://test.com/api", + null, + HttpMethod.Get, + new List>>(), + new TestObjectResult(), + null); + var asResult = result.As(result.Data.InnerData); + + Assert.IsNull(asResult.Error); + Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK); + Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1)); + Assert.AreEqual(asResult.RequestUrl, "https://test.com/api"); + Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get); + Assert.IsNotNull(asResult.Data); + Assert.IsTrue(asResult); + Assert.IsTrue(asResult.Success); + } + } + + public class TestObjectResult + { + public TestObject2 InnerData; + + public TestObjectResult() + { + InnerData = new TestObject2(); + } + } + + public class TestObject2 + { + } +} diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs index e425830..6920905 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs @@ -72,6 +72,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations typeof(HttpRequestException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, message); var request = new Mock(); + request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); request.Setup(c => c.GetHeaders()).Returns(new Dictionary>()); request.Setup(c => c.GetResponseAsync(It.IsAny())).Throws(we); diff --git a/CryptoExchange.Net/Clients/BaseRestClient.cs b/CryptoExchange.Net/Clients/BaseRestClient.cs index 7449c27..55da9ec 100644 --- a/CryptoExchange.Net/Clients/BaseRestClient.cs +++ b/CryptoExchange.Net/Clients/BaseRestClient.cs @@ -196,16 +196,16 @@ namespace CryptoExchange.Net // Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example var parseResult = ValidateJson(data); if (!parseResult.Success) - return new WebCallResult(response.StatusCode, response.ResponseHeaders, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!); + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!); // Let the library implementation see if it is an error response, and if so parse the error var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false); if (error != null) - return new WebCallResult(response.StatusCode, response.ResponseHeaders, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!); + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!); // Not an error, so continue deserializing var deserializeResult = Deserialize(parseResult.Data, deserializer, request.RequestId); - return new WebCallResult(response.StatusCode, response.ResponseHeaders, ClientOptions.OutputOriginalData ? data: null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error); + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data: null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error); } else { @@ -214,7 +214,7 @@ namespace CryptoExchange.Net responseStream.Close(); response.Close(); - return new WebCallResult(statusCode, headers, ClientOptions.OutputOriginalData ? desResult.OriginalData : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), desResult.Data, desResult.Error); + return new WebCallResult(statusCode, headers, sw.Elapsed, ClientOptions.OutputOriginalData ? desResult.OriginalData : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), desResult.Data, desResult.Error); } } else @@ -229,7 +229,7 @@ namespace CryptoExchange.Net var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : parseResult.Error!; if(error.Code == null || error.Code == 0) error.Code = (int)response.StatusCode; - return new WebCallResult(statusCode, headers, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error); + return new WebCallResult(statusCode, headers, sw.Elapsed, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error); } } catch (HttpRequestException requestException) @@ -237,7 +237,7 @@ namespace CryptoExchange.Net // Request exception, can't reach server for instance var exceptionInfo = requestException.ToLogString(); log.Write(LogLevel.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo); - return new WebCallResult(null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo)); + return new WebCallResult(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo)); } catch (OperationCanceledException canceledException) { @@ -245,13 +245,13 @@ namespace CryptoExchange.Net { // Cancellation token canceled by caller log.Write(LogLevel.Warning, $"[{request.RequestId}] Request canceled by cancellation token"); - return new WebCallResult(null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError()); + return new WebCallResult(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError()); } else { // Request timed out log.Write(LogLevel.Warning, $"[{request.RequestId}] Request timed out: " + canceledException.ToLogString()); - return new WebCallResult(null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"[{request.RequestId}] Request timed out")); + return new WebCallResult(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"[{request.RequestId}] Request timed out")); } } } diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 3f3f2b2..1d661bf 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -68,7 +68,7 @@ namespace CryptoExchange.Net if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < TimeSpan.FromHours(1))) { timeSyncParams.TimeSyncState.Semaphore.Release(); - return new WebCallResult(null, null, null, null, null, null, null, true, null); + return new WebCallResult(null, null, null, null, null, null, null, null, true, null); } var localTime = DateTime.UtcNow; @@ -106,7 +106,7 @@ namespace CryptoExchange.Net } } - return new WebCallResult(null, null, null, null, null, null, null, true, null); + return new WebCallResult(null, null, null, null, null, null, null, null, true, null); } } } diff --git a/CryptoExchange.Net/Objects/CallResult.cs b/CryptoExchange.Net/Objects/CallResult.cs index 91b46f9..7fff976 100644 --- a/CryptoExchange.Net/Objects/CallResult.cs +++ b/CryptoExchange.Net/Objects/CallResult.cs @@ -145,6 +145,26 @@ namespace CryptoExchange.Net.Objects /// public class WebCallResult : CallResult { + /// + /// The request http method + /// + public HttpMethod? RequestMethod { get; set; } + + /// + /// The headers sent with the request + /// + public IEnumerable>>? RequestHeaders { get; set; } + + /// + /// The url which was requested + /// + public string? RequestUrl { get; set; } + + /// + /// The body of the request + /// + public string? RequestBody { get; set; } + /// /// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this. /// @@ -155,21 +175,48 @@ namespace CryptoExchange.Net.Objects /// public IEnumerable>>? ResponseHeaders { get; set; } + /// + /// The time between sending the request and receiving the response + /// + public TimeSpan? ResponseTime { get; set; } + /// /// ctor /// - /// Status code - /// Response headers - /// Error - private WebCallResult( + /// + /// + /// + /// + /// + /// + /// + /// + public WebCallResult( HttpStatusCode? code, - IEnumerable>>? responseHeaders, + IEnumerable>>? responseHeaders, + TimeSpan? responseTime, + string? requestUrl, + string? requestBody, + HttpMethod? requestMethod, + IEnumerable>>? requestHeaders, Error? error) : base(error) { - ResponseHeaders = responseHeaders; ResponseStatusCode = code; + ResponseHeaders = responseHeaders; + ResponseTime = responseTime; + + RequestUrl = requestUrl; + RequestBody = requestBody; + RequestHeaders = requestHeaders; + RequestMethod = requestMethod; } + /// + /// ctor + /// + /// + public WebCallResult(Error error): base(error) { } + /// /// Return the result as an error result /// @@ -177,7 +224,7 @@ namespace CryptoExchange.Net.Objects /// public WebCallResult AsError(Error error) { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, error); + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); } } @@ -227,6 +274,7 @@ namespace CryptoExchange.Net.Objects /// /// /// + /// /// /// /// @@ -237,6 +285,7 @@ namespace CryptoExchange.Net.Objects public WebCallResult( HttpStatusCode? code, IEnumerable>>? responseHeaders, + TimeSpan? responseTime, string? originalData, string? requestUrl, string? requestBody, @@ -247,6 +296,8 @@ namespace CryptoExchange.Net.Objects { ResponseStatusCode = code; ResponseHeaders = responseHeaders; + ResponseTime = responseTime; + RequestUrl = requestUrl; RequestBody = requestBody; RequestHeaders = requestHeaders; @@ -257,7 +308,7 @@ namespace CryptoExchange.Net.Objects /// Create a new error result /// /// The error - public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, default, error) { } + public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, default, error) { } /// /// Copy the WebCallResult to a new data type @@ -267,7 +318,25 @@ namespace CryptoExchange.Net.Objects /// public new WebCallResult As([AllowNull] K data) { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error); + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error); + } + + /// + /// Copy as a dataless result + /// + /// + public WebCallResult AsDataless() + { + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error); + } + + /// + /// Copy as a dataless result + /// + /// + public WebCallResult AsDatalessError(Error error) + { + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); } /// @@ -278,7 +347,7 @@ namespace CryptoExchange.Net.Objects /// public new WebCallResult AsError(Error error) { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error); + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error); } } }