mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-06-07 16:06:15 +00:00
Unit testing update (#199)
This commit is contained in:
parent
96c9a55c48
commit
050286ecd1
@ -68,11 +68,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary<string, object> providedParameters, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat, out SortedDictionary<string, object> uriParameters, out SortedDictionary<string, object> bodyParameters, out Dictionary<string, string> headers)
|
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, IDictionary<string, object> uriParams, IDictionary<string, object> bodyParams, Dictionary<string, string> headers, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat)
|
||||||
{
|
{
|
||||||
bodyParameters = new SortedDictionary<string, object>();
|
|
||||||
uriParameters = new SortedDictionary<string, object>();
|
|
||||||
headers = new Dictionary<string, string>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetKey() => _credentials.Key.GetString();
|
public string GetKey() => _credentials.Key.GetString();
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using CryptoExchange.Net.Clients;
|
using CryptoExchange.Net.Clients;
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -15,6 +16,8 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class AuthenticationProvider : IDisposable
|
public abstract class AuthenticationProvider : IDisposable
|
||||||
{
|
{
|
||||||
|
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provided credentials
|
/// Provided credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -44,7 +47,6 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <param name="apiClient">The Api client sending the request</param>
|
/// <param name="apiClient">The Api client sending the request</param>
|
||||||
/// <param name="uri">The uri for the request</param>
|
/// <param name="uri">The uri for the request</param>
|
||||||
/// <param name="method">The method of the request</param>
|
/// <param name="method">The method of the request</param>
|
||||||
/// <param name="providedParameters">The request parameters</param>
|
|
||||||
/// <param name="auth">If the requests should be authenticated</param>
|
/// <param name="auth">If the requests should be authenticated</param>
|
||||||
/// <param name="arraySerialization">Array serialization type</param>
|
/// <param name="arraySerialization">Array serialization type</param>
|
||||||
/// <param name="parameterPosition">The position where the providedParameters should go</param>
|
/// <param name="parameterPosition">The position where the providedParameters should go</param>
|
||||||
@ -56,14 +58,13 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
RestApiClient apiClient,
|
RestApiClient apiClient,
|
||||||
Uri uri,
|
Uri uri,
|
||||||
HttpMethod method,
|
HttpMethod method,
|
||||||
Dictionary<string, object> providedParameters,
|
IDictionary<string, object> uriParameters,
|
||||||
|
IDictionary<string, object> bodyParameters,
|
||||||
|
Dictionary<string, string> headers,
|
||||||
bool auth,
|
bool auth,
|
||||||
ArrayParametersSerialization arraySerialization,
|
ArrayParametersSerialization arraySerialization,
|
||||||
HttpMethodParameterPosition parameterPosition,
|
HttpMethodParameterPosition parameterPosition,
|
||||||
RequestBodyFormat requestBodyFormat,
|
RequestBodyFormat requestBodyFormat
|
||||||
out SortedDictionary<string, object> uriParameters,
|
|
||||||
out SortedDictionary<string, object> bodyParameters,
|
|
||||||
out Dictionary<string, string> headers
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -418,9 +419,9 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient"></param>
|
/// <param name="apiClient"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected static DateTime GetTimestamp(RestApiClient apiClient)
|
protected DateTime GetTimestamp(RestApiClient apiClient)
|
||||||
{
|
{
|
||||||
return DateTime.UtcNow.Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
|
return TimeProvider.GetTime().Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -428,7 +429,7 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient"></param>
|
/// <param name="apiClient"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected static string GetMillisecondTimestamp(RestApiClient apiClient)
|
protected string GetMillisecondTimestamp(RestApiClient apiClient)
|
||||||
{
|
{
|
||||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
@ -40,23 +40,33 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request body content type
|
/// Request body content type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
|
protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How to serialize array parameters when making requests
|
/// How to serialize array parameters when making requests
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
|
protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
|
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected string RequestBodyEmptyContent = "{}";
|
protected internal string RequestBodyEmptyContent = "{}";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request headers to be sent with each request
|
/// Request headers to be sent with each request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether parameters need to be ordered
|
||||||
|
/// </summary>
|
||||||
|
protected internal bool OrderParameters { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parameter order comparer
|
||||||
|
/// </summary>
|
||||||
|
protected IComparer<string> ParameterOrderComparer { get; } = new OrderedStringComparer();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Where to put the parameters for requests with different Http methods
|
/// Where to put the parameters for requests with different Http methods
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -269,22 +279,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
|
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
|
||||||
var requestId = ExchangeHelpers.NextId();
|
var requestId = ExchangeHelpers.NextId();
|
||||||
|
|
||||||
for (var i = 0; i < parameters.Count; i++)
|
|
||||||
{
|
|
||||||
var kvp = parameters.ElementAt(i);
|
|
||||||
if (kvp.Value is Func<object> delegateValue)
|
|
||||||
parameters[kvp.Key] = delegateValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameterPosition == HttpMethodParameterPosition.InUri)
|
|
||||||
{
|
|
||||||
foreach (var parameter in parameters)
|
|
||||||
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
var headers = new Dictionary<string, string>();
|
var headers = new Dictionary<string, string>();
|
||||||
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||||
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||||
if (AuthenticationProvider != null)
|
if (AuthenticationProvider != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -293,14 +290,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
this,
|
this,
|
||||||
uri,
|
uri,
|
||||||
definition.Method,
|
definition.Method,
|
||||||
parameters,
|
uriParameters,
|
||||||
|
bodyParameters,
|
||||||
|
headers,
|
||||||
definition.Authenticated,
|
definition.Authenticated,
|
||||||
arraySerialization,
|
arraySerialization,
|
||||||
parameterPosition,
|
parameterPosition,
|
||||||
bodyFormat,
|
bodyFormat);
|
||||||
out uriParameters,
|
|
||||||
out bodyParameters,
|
|
||||||
out headers);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -346,7 +342,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
||||||
{
|
{
|
||||||
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||||
if (bodyParameters.Any())
|
if (bodyParameters.Count != 0)
|
||||||
WriteParamBody(request, bodyParameters, contentType);
|
WriteParamBody(request, bodyParameters, contentType);
|
||||||
else
|
else
|
||||||
request.SetContent(RequestBodyEmptyContent, contentType);
|
request.SetContent(RequestBodyEmptyContent, contentType);
|
||||||
@ -603,7 +599,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (!valid)
|
if (!valid)
|
||||||
{
|
{
|
||||||
// Invalid json
|
// Invalid json
|
||||||
var error = new ServerError("Failed to parse response", accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
|
var error = new ServerError("Failed to parse response: " + valid.Error!.Message, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -726,8 +722,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
|
|
||||||
var headers = new Dictionary<string, string>();
|
var headers = new Dictionary<string, string>();
|
||||||
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||||
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||||
if (AuthenticationProvider != null)
|
if (AuthenticationProvider != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -736,14 +732,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
this,
|
this,
|
||||||
uri,
|
uri,
|
||||||
method,
|
method,
|
||||||
parameters,
|
uriParameters,
|
||||||
|
bodyParameters,
|
||||||
|
headers,
|
||||||
signed,
|
signed,
|
||||||
arraySerialization,
|
arraySerialization,
|
||||||
parameterPosition,
|
parameterPosition,
|
||||||
bodyFormat,
|
bodyFormat);
|
||||||
out uriParameters,
|
|
||||||
out bodyParameters,
|
|
||||||
out headers);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -804,7 +799,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="request">The request to set the parameters on</param>
|
/// <param name="request">The request to set the parameters on</param>
|
||||||
/// <param name="parameters">The parameters to set</param>
|
/// <param name="parameters">The parameters to set</param>
|
||||||
/// <param name="contentType">The content type of the data</param>
|
/// <param name="contentType">The content type of the data</param>
|
||||||
protected virtual void WriteParamBody(IRequest request, SortedDictionary<string, object> parameters, string contentType)
|
protected virtual void WriteParamBody(IRequest request, IDictionary<string, object> parameters, string contentType)
|
||||||
{
|
{
|
||||||
if (contentType == Constants.JsonContentHeader)
|
if (contentType == Constants.JsonContentHeader)
|
||||||
{
|
{
|
||||||
@ -859,6 +854,19 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new ServerRateLimitError(message);
|
return new ServerRateLimitError(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create the parameter IDictionary
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameters"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected internal IDictionary<string, object> CreateParameterDictionary(IDictionary<string, object> parameters)
|
||||||
|
{
|
||||||
|
if (!OrderParameters)
|
||||||
|
return parameters;
|
||||||
|
|
||||||
|
return new SortedDictionary<string, object>(parameters, ParameterOrderComparer);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Newtonsoft.Json;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
@ -38,14 +39,8 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
var longValue = (long)reader.Value;
|
var longValue = (long)reader.Value;
|
||||||
if (longValue == 0 || longValue == -1)
|
if (longValue == 0 || longValue == -1)
|
||||||
return objectType == typeof(DateTime) ? default(DateTime): null;
|
return objectType == typeof(DateTime) ? default(DateTime): null;
|
||||||
if (longValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(longValue);
|
|
||||||
if (longValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds(longValue);
|
|
||||||
if (longValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds(longValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds(longValue);
|
return ParseFromLong(longValue);
|
||||||
}
|
}
|
||||||
else if (reader.TokenType is JsonToken.Float)
|
else if (reader.TokenType is JsonToken.Float)
|
||||||
{
|
{
|
||||||
@ -68,76 +63,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stringValue.Length == 12 && stringValue.StartsWith("202"))
|
return ParseFromString(stringValue);
|
||||||
{
|
|
||||||
// Parse 202303261200 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
|
||||||
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
|
||||||
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 8)
|
|
||||||
{
|
|
||||||
// Parse 20211103 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 6)
|
|
||||||
{
|
|
||||||
// Parse 211103 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
|
||||||
{
|
|
||||||
// Parse 1637745563.000 format
|
|
||||||
if (doubleValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(doubleValue);
|
|
||||||
if (doubleValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds((long)doubleValue);
|
|
||||||
if (doubleValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds((long)doubleValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds((long)doubleValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(stringValue.Length == 10)
|
|
||||||
{
|
|
||||||
// Parse 2021-11-03 format
|
|
||||||
var values = stringValue.Split('-');
|
|
||||||
if(!int.TryParse(values[0], out var year)
|
|
||||||
|| !int.TryParse(values[1], out var month)
|
|
||||||
|| !int.TryParse(values[2], out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
|
||||||
}
|
}
|
||||||
else if(reader.TokenType == JsonToken.Date)
|
else if(reader.TokenType == JsonToken.Date)
|
||||||
{
|
{
|
||||||
@ -150,6 +76,102 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a long value to datetime
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="longValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static DateTime ParseFromLong(long longValue)
|
||||||
|
{
|
||||||
|
if (longValue < 19999999999)
|
||||||
|
return ConvertFromSeconds(longValue);
|
||||||
|
if (longValue < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds(longValue);
|
||||||
|
if (longValue < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds(longValue);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds(longValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a string value to datetime
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stringValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static DateTime ParseFromString(string stringValue)
|
||||||
|
{
|
||||||
|
if (stringValue.Length == 12 && stringValue.StartsWith("202"))
|
||||||
|
{
|
||||||
|
// Parse 202303261200 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
||||||
|
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
||||||
|
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 8)
|
||||||
|
{
|
||||||
|
// Parse 20211103 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 6)
|
||||||
|
{
|
||||||
|
// Parse 211103 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
||||||
|
{
|
||||||
|
// Parse 1637745563.000 format
|
||||||
|
if (doubleValue < 19999999999)
|
||||||
|
return ConvertFromSeconds(doubleValue);
|
||||||
|
if (doubleValue < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds((long)doubleValue);
|
||||||
|
if (doubleValue < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds((long)doubleValue);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds((long)doubleValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 10)
|
||||||
|
{
|
||||||
|
// Parse 2021-11-03 format
|
||||||
|
var values = stringValue.Split('-');
|
||||||
|
if (!int.TryParse(values[0], out var year)
|
||||||
|
|| !int.TryParse(values[1], out var month)
|
||||||
|
|| !int.TryParse(values[2], out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -224,7 +224,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> Read(Stream stream, bool bufferStream)
|
public async Task<CallResult> Read(Stream stream, bool bufferStream)
|
||||||
{
|
{
|
||||||
if (bufferStream && stream is not MemoryStream)
|
if (bufferStream && stream is not MemoryStream)
|
||||||
{
|
{
|
||||||
@ -252,14 +252,15 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
{
|
{
|
||||||
_token = await JToken.LoadAsync(jsonTextReader).ConfigureAwait(false);
|
_token = await JToken.LoadAsync(jsonTextReader).ConfigureAwait(false);
|
||||||
IsJson = true;
|
IsJson = true;
|
||||||
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsJson = false;
|
||||||
|
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsJson;
|
|
||||||
}
|
}
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string GetOriginalString()
|
public override string GetOriginalString()
|
||||||
@ -290,7 +291,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
private ReadOnlyMemory<byte> _bytes;
|
private ReadOnlyMemory<byte> _bytes;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Read(ReadOnlyMemory<byte> data)
|
public CallResult Read(ReadOnlyMemory<byte> data)
|
||||||
{
|
{
|
||||||
_bytes = data;
|
_bytes = data;
|
||||||
|
|
||||||
@ -305,14 +306,14 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
{
|
{
|
||||||
_token = JToken.Load(jsonTextReader);
|
_token = JToken.Load(jsonTextReader);
|
||||||
IsJson = true;
|
IsJson = true;
|
||||||
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsJson = false;
|
||||||
|
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsJson;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@ -49,14 +50,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
var longValue = reader.GetDouble();
|
var longValue = reader.GetDouble();
|
||||||
if (longValue == 0 || longValue == -1)
|
if (longValue == 0 || longValue == -1)
|
||||||
return default;
|
return default;
|
||||||
if (longValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(longValue);
|
|
||||||
if (longValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds(longValue);
|
|
||||||
if (longValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds(longValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds(longValue);
|
return ParseFromDouble(longValue);
|
||||||
}
|
}
|
||||||
else if (reader.TokenType is JsonTokenType.String)
|
else if (reader.TokenType is JsonTokenType.String)
|
||||||
{
|
{
|
||||||
@ -68,76 +63,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stringValue!.Length == 12 && stringValue.StartsWith("202"))
|
return ParseFromString(stringValue!);
|
||||||
{
|
|
||||||
// Parse 202303261200 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
|
||||||
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
|
||||||
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 8)
|
|
||||||
{
|
|
||||||
// Parse 20211103 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 6)
|
|
||||||
{
|
|
||||||
// Parse 211103 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
|
||||||
{
|
|
||||||
// Parse 1637745563.000 format
|
|
||||||
if (doubleValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(doubleValue);
|
|
||||||
if (doubleValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds((long)doubleValue);
|
|
||||||
if (doubleValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds((long)doubleValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds((long)doubleValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 10)
|
|
||||||
{
|
|
||||||
// Parse 2021-11-03 format
|
|
||||||
var values = stringValue.Split('-');
|
|
||||||
if (!int.TryParse(values[0], out var year)
|
|
||||||
|| !int.TryParse(values[1], out var month)
|
|
||||||
|| !int.TryParse(values[2], out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -160,6 +86,102 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a long value to datetime
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="longValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static DateTime ParseFromDouble(double longValue)
|
||||||
|
{
|
||||||
|
if (longValue < 19999999999)
|
||||||
|
return ConvertFromSeconds(longValue);
|
||||||
|
if (longValue < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds(longValue);
|
||||||
|
if (longValue < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds(longValue);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds(longValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a string value to datetime
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stringValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static DateTime ParseFromString(string stringValue)
|
||||||
|
{
|
||||||
|
if (stringValue!.Length == 12 && stringValue.StartsWith("202"))
|
||||||
|
{
|
||||||
|
// Parse 202303261200 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
||||||
|
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
||||||
|
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 8)
|
||||||
|
{
|
||||||
|
// Parse 20211103 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 6)
|
||||||
|
{
|
||||||
|
// Parse 211103 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
||||||
|
{
|
||||||
|
// Parse 1637745563.000 format
|
||||||
|
if (doubleValue < 19999999999)
|
||||||
|
return ConvertFromSeconds(doubleValue);
|
||||||
|
if (doubleValue < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds((long)doubleValue);
|
||||||
|
if (doubleValue < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds((long)doubleValue);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds((long)doubleValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 10)
|
||||||
|
{
|
||||||
|
// Parse 2021-11-03 format
|
||||||
|
var values = stringValue.Split('-');
|
||||||
|
if (!int.TryParse(values[0], out var year)
|
||||||
|
|| !int.TryParse(values[1], out var month)
|
||||||
|
|| !int.TryParse(values[2], out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -188,7 +188,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> Read(Stream stream, bool bufferStream)
|
public async Task<CallResult> Read(Stream stream, bool bufferStream)
|
||||||
{
|
{
|
||||||
if (bufferStream && stream is not MemoryStream)
|
if (bufferStream && stream is not MemoryStream)
|
||||||
{
|
{
|
||||||
@ -211,15 +211,16 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
||||||
IsJson = true;
|
IsJson = true;
|
||||||
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsJson = false;
|
||||||
|
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsJson;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string GetOriginalString()
|
public override string GetOriginalString()
|
||||||
{
|
{
|
||||||
@ -249,7 +250,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
private ReadOnlyMemory<byte> _bytes;
|
private ReadOnlyMemory<byte> _bytes;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Read(ReadOnlyMemory<byte> data)
|
public CallResult Read(ReadOnlyMemory<byte> data)
|
||||||
{
|
{
|
||||||
_bytes = data;
|
_bytes = data;
|
||||||
|
|
||||||
@ -257,14 +258,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
_document = JsonDocument.Parse(data);
|
_document = JsonDocument.Parse(data);
|
||||||
IsJson = true;
|
IsJson = true;
|
||||||
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsJson = false;
|
||||||
|
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsJson;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -342,7 +342,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="baseUri"></param>
|
/// <param name="baseUri"></param>
|
||||||
/// <param name="arraySerialization"></param>
|
/// <param name="arraySerialization"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static Uri SetParameters(this Uri baseUri, SortedDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
|
public static Uri SetParameters(this Uri baseUri, IDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
|
||||||
{
|
{
|
||||||
var uriBuilder = new UriBuilder();
|
var uriBuilder = new UriBuilder();
|
||||||
uriBuilder.Scheme = baseUri.Scheme;
|
uriBuilder.Scheme = baseUri.Scheme;
|
||||||
|
16
CryptoExchange.Net/Interfaces/IAuthTimeProvider.cs
Normal file
16
CryptoExchange.Net/Interfaces/IAuthTimeProvider.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Time provider
|
||||||
|
/// </summary>
|
||||||
|
internal interface IAuthTimeProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get current time
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
DateTime GetTime();
|
||||||
|
}
|
||||||
|
}
|
@ -84,7 +84,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stream"></param>
|
/// <param name="stream"></param>
|
||||||
/// <param name="bufferStream"></param>
|
/// <param name="bufferStream"></param>
|
||||||
Task<bool> Read(Stream stream, bool bufferStream);
|
Task<CallResult> Read(Stream stream, bool bufferStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -96,6 +96,6 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// Load a data message
|
/// Load a data message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
bool Read(ReadOnlyMemory<byte> data);
|
CallResult Read(ReadOnlyMemory<byte> data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
CryptoExchange.Net/Objects/AuthTimeProvider.cs
Normal file
10
CryptoExchange.Net/Objects/AuthTimeProvider.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
|
{
|
||||||
|
internal class AuthTimeProvider : IAuthTimeProvider
|
||||||
|
{
|
||||||
|
public DateTime GetTime() => DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
29
CryptoExchange.Net/Objects/OrderedStringComparer.cs
Normal file
29
CryptoExchange.Net/Objects/OrderedStringComparer.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Order string comparer, sorts by alphabetical order
|
||||||
|
/// </summary>
|
||||||
|
public class OrderedStringComparer : IComparer<string>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Compare function
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="x"></param>
|
||||||
|
/// <param name="y"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public int Compare(string x, string y)
|
||||||
|
{
|
||||||
|
// Shortcuts: If both are null, they are the same.
|
||||||
|
if (x == null && y == null) return 0;
|
||||||
|
|
||||||
|
// If one is null and the other isn't, then the
|
||||||
|
// one that is null is "lesser".
|
||||||
|
if (x == null) return -1;
|
||||||
|
if (y == null) return 1;
|
||||||
|
|
||||||
|
return x.CompareTo(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
309
CryptoExchange.Net/Testing/Comparers/JsonNetComparer.cs
Normal file
309
CryptoExchange.Net/Testing/Comparers/JsonNetComparer.cs
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using CryptoExchange.Net.Converters;
|
||||||
|
using CryptoExchange.Net.Converters.JsonNet;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Comparers
|
||||||
|
{
|
||||||
|
internal class JsonNetComparer
|
||||||
|
{
|
||||||
|
internal static void CompareData(
|
||||||
|
string method,
|
||||||
|
object resultData,
|
||||||
|
string json,
|
||||||
|
string? nestedJsonProperty,
|
||||||
|
List<string>? ignoreProperties = null,
|
||||||
|
bool userSingleArrayItem = false)
|
||||||
|
{
|
||||||
|
var resultProperties = resultData.GetType().GetProperties().Select(p => (p, (JsonPropertyAttribute?)p.GetCustomAttributes(typeof(JsonPropertyAttribute), true).SingleOrDefault()));
|
||||||
|
var jsonObject = JToken.Parse(json);
|
||||||
|
if (nestedJsonProperty != null)
|
||||||
|
jsonObject = jsonObject[nestedJsonProperty];
|
||||||
|
|
||||||
|
if (userSingleArrayItem)
|
||||||
|
jsonObject = ((JArray)jsonObject!)[0];
|
||||||
|
|
||||||
|
if (resultData.GetType().GetInterfaces().Contains(typeof(IDictionary)))
|
||||||
|
{
|
||||||
|
var dict = (IDictionary)resultData;
|
||||||
|
var jObj = (JObject)jsonObject!;
|
||||||
|
var properties = jObj.Properties();
|
||||||
|
foreach (var dictProp in properties)
|
||||||
|
{
|
||||||
|
if (!dict.Contains(dictProp.Name))
|
||||||
|
throw new Exception($"{method}: Dictionary has no value for {dictProp.Name} while input json `{dictProp.Name}` has value {dictProp.Value}");
|
||||||
|
|
||||||
|
if (dictProp.Value.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
// TODO Some additional checking for objects
|
||||||
|
foreach (var prop in ((JObject)dictProp.Value).Properties())
|
||||||
|
CheckObject(method, prop, dict[dictProp.Name]!, ignoreProperties!);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null)
|
||||||
|
// Property value not correct
|
||||||
|
throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {dictProp.Value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonObject!.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var jObjs = (JArray)jsonObject;
|
||||||
|
if (resultData is IEnumerable list)
|
||||||
|
{
|
||||||
|
var enumerator = list.GetEnumerator();
|
||||||
|
foreach (var jObj in jObjs)
|
||||||
|
{
|
||||||
|
enumerator.MoveNext();
|
||||||
|
if (jObj.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var subProp in ((JObject)jObj).Properties())
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||||
|
continue;
|
||||||
|
CheckObject(method, subProp, enumerator.Current, ignoreProperties!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jObj.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var resultObj = enumerator.Current;
|
||||||
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
if (jsonConverter != typeof(ArrayConverter))
|
||||||
|
// Not array converter?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObj.Values())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = enumerator.Current;
|
||||||
|
if (value == default && ((JValue)jObj).Type != JTokenType.Null)
|
||||||
|
throw new Exception($"{method}: Array has no value while input json array has value {jObj}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var resultProps = resultData.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObjs.Values())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultData), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var item in jsonObject)
|
||||||
|
{
|
||||||
|
if (item is JProperty prop)
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(prop.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, prop, resultData, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"Successfully validated {method}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckObject(string method, JProperty prop, object obj, List<string>? ignoreProperties)
|
||||||
|
{
|
||||||
|
var resultProperties = obj.GetType().GetProperties().Select(p => (p, ((JsonPropertyAttribute?)p.GetCustomAttributes(typeof(JsonPropertyAttribute), true).SingleOrDefault())?.PropertyName));
|
||||||
|
|
||||||
|
// Property has a value
|
||||||
|
var property = resultProperties.SingleOrDefault(p => p.PropertyName == prop.Name).p;
|
||||||
|
property ??= resultProperties.SingleOrDefault(p => p.p.Name == prop.Name).p;
|
||||||
|
property ??= resultProperties.SingleOrDefault(p => p.p.Name.Equals(prop.Name, StringComparison.InvariantCultureIgnoreCase)).p;
|
||||||
|
|
||||||
|
if (property is null)
|
||||||
|
// Property not found
|
||||||
|
throw new Exception($"{method}: Missing property `{prop.Name}` on `{obj.GetType().Name}`");
|
||||||
|
|
||||||
|
var propertyValue = property.GetValue(obj);
|
||||||
|
if (property.GetCustomAttribute<JsonPropertyAttribute>(true)?.ItemConverterType == null)
|
||||||
|
CheckPropertyValue(method, prop.Value, propertyValue, property.PropertyType, property.Name, prop.Name, ignoreProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckPropertyValue(string method, JToken propValue, object? propertyValue, Type propertyType, string? propertyName = null, string? propName = null, List<string>? ignoreProperties = null)
|
||||||
|
{
|
||||||
|
if (propertyValue == default && propValue.Type != JTokenType.Null && !string.IsNullOrEmpty(propValue.ToString()))
|
||||||
|
{
|
||||||
|
if (propertyType == typeof(DateTime?) && (propValue.ToString() == "" || propValue.ToString() == "0" || propValue.ToString() == "-1"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Property value not correct
|
||||||
|
if (propValue.ToString() != "0")
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propertyValue == default && (propValue.Type == JTokenType.Null || string.IsNullOrEmpty(propValue.ToString())) || propValue.ToString() == "0")
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (propertyValue!.GetType().GetInterfaces().Contains(typeof(IDictionary)))
|
||||||
|
{
|
||||||
|
var dict = (IDictionary)propertyValue;
|
||||||
|
var jObj = (JObject)propValue;
|
||||||
|
var properties = jObj.Properties();
|
||||||
|
foreach (var dictProp in properties)
|
||||||
|
{
|
||||||
|
if (!dict.Contains(dictProp.Name))
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}");
|
||||||
|
|
||||||
|
if (dictProp.Value.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
CheckObject(method, dictProp, dict[dictProp.Name]!, ignoreProperties);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null)
|
||||||
|
// Property value not correct
|
||||||
|
throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {propValue} for");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable))
|
||||||
|
&& propertyValue.GetType() != typeof(string))
|
||||||
|
{
|
||||||
|
var jObjs = (JArray)propValue;
|
||||||
|
var list = (IEnumerable)propertyValue;
|
||||||
|
var enumerator = list.GetEnumerator();
|
||||||
|
foreach (JToken jtoken in jObjs)
|
||||||
|
{
|
||||||
|
enumerator.MoveNext();
|
||||||
|
var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true);
|
||||||
|
if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter))
|
||||||
|
// Custom converter for the type, skip
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (jtoken.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var subProp in ((JObject)jtoken).Properties())
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, subProp, enumerator.Current, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jtoken.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var resultObj = enumerator.Current;
|
||||||
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
if (jsonConverter != typeof(ArrayConverter))
|
||||||
|
// Not array converter?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jtoken.Values())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), propertyType, arrayProp.Name, "Array index " + i, ignoreProperties);
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = enumerator.Current;
|
||||||
|
if (value == default && ((JValue)jtoken).Type != JTokenType.Null)
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {jtoken}");
|
||||||
|
|
||||||
|
CheckValues(method, propertyName!, propertyType, (JValue)jtoken, value!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (propValue.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var item in propValue)
|
||||||
|
{
|
||||||
|
if (item is JProperty prop)
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(prop.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, prop, propertyValue, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CheckValues(method, propertyName!, propertyType, (JValue)propValue, propertyValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckValues(string method, string property, Type propertyType, JValue jsonValue, object objectValue)
|
||||||
|
{
|
||||||
|
if (jsonValue.Type == JTokenType.String)
|
||||||
|
{
|
||||||
|
if (objectValue is decimal dec)
|
||||||
|
{
|
||||||
|
if (jsonValue.Value<decimal>() != dec)
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {dec}");
|
||||||
|
}
|
||||||
|
else if (objectValue is DateTime time)
|
||||||
|
{
|
||||||
|
if (time != DateTimeConverter.ParseFromString(jsonValue.Value<string>()!))
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {time}");
|
||||||
|
}
|
||||||
|
else if (propertyType.IsEnum)
|
||||||
|
{
|
||||||
|
// TODO enum comparing
|
||||||
|
}
|
||||||
|
else if (!jsonValue.Value<string>()!.Equals(Convert.ToString(objectValue, CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {objectValue}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonValue.Type == JTokenType.Integer)
|
||||||
|
{
|
||||||
|
if (objectValue is DateTime time)
|
||||||
|
{
|
||||||
|
if (time != DateTimeConverter.ParseFromLong(jsonValue.Value<long>()!))
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {time}");
|
||||||
|
}
|
||||||
|
else if (propertyType.IsEnum)
|
||||||
|
{
|
||||||
|
// TODO enum comparing
|
||||||
|
}
|
||||||
|
else if (jsonValue.Value<long>() != Convert.ToInt64(objectValue))
|
||||||
|
{
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<long>()} vs {Convert.ToInt64(objectValue)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonValue.Type == JTokenType.Boolean)
|
||||||
|
{
|
||||||
|
if (jsonValue.Value<bool>() != (bool)objectValue)
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
289
CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs
Normal file
289
CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using CryptoExchange.Net.Converters;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Comparers
|
||||||
|
{
|
||||||
|
internal class SystemTextJsonComparer
|
||||||
|
{
|
||||||
|
internal static void CompareData(
|
||||||
|
string method,
|
||||||
|
object resultData,
|
||||||
|
string json,
|
||||||
|
string? nestedJsonProperty,
|
||||||
|
List<string>? ignoreProperties = null,
|
||||||
|
bool userSingleArrayItem = false)
|
||||||
|
{
|
||||||
|
var resultProperties = resultData.GetType().GetProperties().Select(p => (p, (JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault()));
|
||||||
|
var jsonObject = JToken.Parse(json);
|
||||||
|
if (nestedJsonProperty != null)
|
||||||
|
jsonObject = jsonObject[nestedJsonProperty];
|
||||||
|
|
||||||
|
if (userSingleArrayItem)
|
||||||
|
jsonObject = ((JArray)jsonObject!)[0];
|
||||||
|
|
||||||
|
if (resultData.GetType().GetInterfaces().Contains(typeof(IDictionary)))
|
||||||
|
{
|
||||||
|
var dict = (IDictionary)resultData;
|
||||||
|
var jObj = (JObject)jsonObject!;
|
||||||
|
var properties = jObj.Properties();
|
||||||
|
foreach (var dictProp in properties)
|
||||||
|
{
|
||||||
|
if (!dict.Contains(dictProp.Name))
|
||||||
|
throw new Exception($"{method}: Dictionary has no value for {dictProp.Name} while input json `{dictProp.Name}` has value {dictProp.Value}");
|
||||||
|
|
||||||
|
if (dictProp.Value.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
// TODO Some additional checking for objects
|
||||||
|
foreach (var prop in ((JObject)dictProp.Value).Properties())
|
||||||
|
CheckObject(method, prop, dict[dictProp.Name]!, ignoreProperties!);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null)
|
||||||
|
// Property value not correct
|
||||||
|
throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {dictProp.Value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonObject!.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var jObjs = (JArray)jsonObject;
|
||||||
|
var list = (IEnumerable)resultData;
|
||||||
|
var enumerator = list.GetEnumerator();
|
||||||
|
foreach (var jObj in jObjs)
|
||||||
|
{
|
||||||
|
enumerator.MoveNext();
|
||||||
|
if (jObj.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var subProp in ((JObject)jObj).Properties())
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||||
|
continue;
|
||||||
|
CheckObject(method, subProp, enumerator.Current, ignoreProperties!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jObj.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var resultObj = enumerator.Current;
|
||||||
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
if (jsonConverter != typeof(ArrayConverter))
|
||||||
|
// Not array converter?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObj.Values())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = enumerator.Current;
|
||||||
|
if (value == default && ((JValue)jObj).Type != JTokenType.Null)
|
||||||
|
throw new Exception($"{method}: Array has no value while input json array has value {jObj}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var item in jsonObject)
|
||||||
|
{
|
||||||
|
if (item is JProperty prop)
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(prop.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, prop, resultData, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"Successfully validated {method}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckObject(string method, JProperty prop, object obj, List<string>? ignoreProperties)
|
||||||
|
{
|
||||||
|
var resultProperties = obj.GetType().GetProperties().Select(p => (p, ((JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault())?.Name));
|
||||||
|
|
||||||
|
// Property has a value
|
||||||
|
var property = resultProperties.SingleOrDefault(p => p.Name == prop.Name).p;
|
||||||
|
property ??= resultProperties.SingleOrDefault(p => p.p.Name == prop.Name).p;
|
||||||
|
property ??= resultProperties.SingleOrDefault(p => p.p.Name.Equals(prop.Name, StringComparison.InvariantCultureIgnoreCase)).p;
|
||||||
|
|
||||||
|
if (property is null)
|
||||||
|
// Property not found
|
||||||
|
throw new Exception($"{method}: Missing property `{prop.Name}` on `{obj.GetType().Name}`");
|
||||||
|
|
||||||
|
var propertyValue = property.GetValue(obj);
|
||||||
|
CheckPropertyValue(method, prop.Value, propertyValue, property.PropertyType, property.Name, prop.Name, ignoreProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckPropertyValue(string method, JToken propValue, object? propertyValue, Type propertyType, string? propertyName = null, string? propName = null, List<string>? ignoreProperties = null)
|
||||||
|
{
|
||||||
|
if (propertyValue == default && propValue.Type != JTokenType.Null && !string.IsNullOrEmpty(propValue.ToString()))
|
||||||
|
{
|
||||||
|
if (propertyType == typeof(DateTime?) && (propValue.ToString() == "" || propValue.ToString() == "0" || propValue.ToString() == "-1"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Property value not correct
|
||||||
|
if (propValue.ToString() != "0")
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propertyValue == default && (propValue.Type == JTokenType.Null || string.IsNullOrEmpty(propValue.ToString())) || propValue.ToString() == "0")
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (propertyValue!.GetType().GetInterfaces().Contains(typeof(IDictionary)))
|
||||||
|
{
|
||||||
|
var dict = (IDictionary)propertyValue;
|
||||||
|
var jObj = (JObject)propValue;
|
||||||
|
var properties = jObj.Properties();
|
||||||
|
foreach (var dictProp in properties)
|
||||||
|
{
|
||||||
|
if (!dict.Contains(dictProp.Name))
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}");
|
||||||
|
|
||||||
|
if (dictProp.Value.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
CheckObject(method, dictProp, dict[dictProp.Name]!, ignoreProperties);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null)
|
||||||
|
// Property value not correct
|
||||||
|
throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {propValue} for");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable))
|
||||||
|
&& propertyValue.GetType() != typeof(string))
|
||||||
|
{
|
||||||
|
var jObjs = (JArray)propValue;
|
||||||
|
var list = (IEnumerable)propertyValue;
|
||||||
|
var enumerator = list.GetEnumerator();
|
||||||
|
foreach (JToken jtoken in jObjs)
|
||||||
|
{
|
||||||
|
enumerator.MoveNext();
|
||||||
|
var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true);
|
||||||
|
if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter))
|
||||||
|
// Custom converter for the type, skip
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (jtoken.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var subProp in ((JObject)jtoken).Properties())
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, subProp, enumerator.Current, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jtoken.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var resultObj = enumerator.Current;
|
||||||
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
if (jsonConverter != typeof(ArrayConverter))
|
||||||
|
// Not array converter?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jtoken.Values())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), propertyType, arrayProp.Name, "Array index " + i, ignoreProperties);
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = enumerator.Current;
|
||||||
|
if (value == default && ((JValue)jtoken).Type != JTokenType.Null)
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {jtoken}");
|
||||||
|
|
||||||
|
CheckValues(method, propertyName!, propertyType, (JValue)jtoken, value!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (propValue.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var item in propValue)
|
||||||
|
{
|
||||||
|
if (item is JProperty prop)
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(prop.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, prop, propertyValue, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CheckValues(method, propertyName!, propertyType, (JValue)propValue, propertyValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckValues(string method, string property, Type propertyType, JValue jsonValue, object objectValue)
|
||||||
|
{
|
||||||
|
if (jsonValue.Type == JTokenType.String)
|
||||||
|
{
|
||||||
|
if (objectValue is decimal dec)
|
||||||
|
{
|
||||||
|
if (jsonValue.Value<decimal>() != dec)
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {dec}");
|
||||||
|
}
|
||||||
|
else if (objectValue is DateTime time)
|
||||||
|
{
|
||||||
|
if (time != DateTimeConverter.ParseFromString(jsonValue.Value<string>()!))
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {time}");
|
||||||
|
}
|
||||||
|
else if (propertyType.IsEnum)
|
||||||
|
{
|
||||||
|
// TODO enum comparing
|
||||||
|
}
|
||||||
|
else if (!jsonValue.Value<string>()!.Equals(Convert.ToString(objectValue, CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {objectValue}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonValue.Type == JTokenType.Integer)
|
||||||
|
{
|
||||||
|
if (objectValue is DateTime time)
|
||||||
|
{
|
||||||
|
if (time != DateTimeConverter.ParseFromDouble(jsonValue.Value<long>()!))
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {time}");
|
||||||
|
}
|
||||||
|
else if (jsonValue.Value<long>() != Convert.ToInt64(objectValue))
|
||||||
|
{
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<long>()} vs {Convert.ToInt64(objectValue)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonValue.Type == JTokenType.Boolean)
|
||||||
|
{
|
||||||
|
if (jsonValue.Value<bool>() != (bool)objectValue)
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
CryptoExchange.Net/Testing/EnumValueTraceListener.cs
Normal file
20
CryptoExchange.Net/Testing/EnumValueTraceListener.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing
|
||||||
|
{
|
||||||
|
internal class EnumValueTraceListener : TraceListener
|
||||||
|
{
|
||||||
|
public override void Write(string message)
|
||||||
|
{
|
||||||
|
if (message.Contains("Cannot map"))
|
||||||
|
throw new Exception("Enum value error: " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WriteLine(string message)
|
||||||
|
{
|
||||||
|
if (message.Contains("Cannot map"))
|
||||||
|
throw new Exception("Enum value error: " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestAuthTimeProvider : IAuthTimeProvider
|
||||||
|
{
|
||||||
|
private readonly DateTime _timestamp;
|
||||||
|
|
||||||
|
public TestAuthTimeProvider(DateTime timestamp)
|
||||||
|
{
|
||||||
|
_timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime GetTime() => _timestamp;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Test implementation for nonce provider, returning a prespecified nonce
|
||||||
|
/// </summary>
|
||||||
|
public class TestNonceProvider : INonceProvider
|
||||||
|
{
|
||||||
|
private readonly long _nonce;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public TestNonceProvider(long nonce)
|
||||||
|
{
|
||||||
|
_nonce = nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public long GetNonce() => _nonce;
|
||||||
|
}
|
||||||
|
}
|
50
CryptoExchange.Net/Testing/Implementations/TestRequest.cs
Normal file
50
CryptoExchange.Net/Testing/Implementations/TestRequest.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestRequest : IRequest
|
||||||
|
{
|
||||||
|
private readonly TestResponse _response;
|
||||||
|
|
||||||
|
public string Accept { set { } }
|
||||||
|
|
||||||
|
public string? Content { get; private set; }
|
||||||
|
|
||||||
|
public HttpMethod Method { get; set; }
|
||||||
|
|
||||||
|
public Uri Uri { get; set; }
|
||||||
|
|
||||||
|
public int RequestId { get; set; }
|
||||||
|
|
||||||
|
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
|
public TestRequest(TestResponse response)
|
||||||
|
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
|
{
|
||||||
|
_response = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddHeader(string key, string value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, IEnumerable<string>> GetHeaders() => new();
|
||||||
|
|
||||||
|
public Task<IResponse> GetResponseAsync(CancellationToken cancellationToken) => Task.FromResult<IResponse>(_response);
|
||||||
|
|
||||||
|
public void SetContent(byte[] data)
|
||||||
|
{
|
||||||
|
Content = Encoding.UTF8.GetString(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetContent(string data, string contentType)
|
||||||
|
{
|
||||||
|
Content = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestRequestFactory : IRequestFactory
|
||||||
|
{
|
||||||
|
private readonly TestRequest _request;
|
||||||
|
|
||||||
|
public TestRequestFactory(TestRequest request)
|
||||||
|
{
|
||||||
|
_request = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public IRequest Create(HttpMethod method, Uri uri, int requestId)
|
||||||
|
{
|
||||||
|
_request.Method = method;
|
||||||
|
_request.Uri = uri;
|
||||||
|
_request.RequestId = requestId;
|
||||||
|
return _request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
CryptoExchange.Net/Testing/Implementations/TestResponse.cs
Normal file
34
CryptoExchange.Net/Testing/Implementations/TestResponse.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestResponse : IResponse
|
||||||
|
{
|
||||||
|
private readonly Stream _response;
|
||||||
|
|
||||||
|
public HttpStatusCode StatusCode { get; }
|
||||||
|
|
||||||
|
public bool IsSuccessStatusCode { get; }
|
||||||
|
|
||||||
|
public long? ContentLength { get; }
|
||||||
|
|
||||||
|
public IEnumerable<KeyValuePair<string, IEnumerable<string>>> ResponseHeaders { get; } = new Dictionary<string, IEnumerable<string>>();
|
||||||
|
|
||||||
|
public TestResponse(HttpStatusCode code, Stream response)
|
||||||
|
{
|
||||||
|
StatusCode = code;
|
||||||
|
IsSuccessStatusCode = code == HttpStatusCode.OK;
|
||||||
|
_response = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Stream> GetResponseStreamAsync() => Task.FromResult(_response);
|
||||||
|
}
|
||||||
|
}
|
81
CryptoExchange.Net/Testing/Implementations/TestSocket.cs
Normal file
81
CryptoExchange.Net/Testing/Implementations/TestSocket.cs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestSocket : IWebsocket
|
||||||
|
{
|
||||||
|
public event Action<string>? OnMessageSend;
|
||||||
|
|
||||||
|
public bool CanConnect { get; set; } = true;
|
||||||
|
public bool Connected { get; set; }
|
||||||
|
|
||||||
|
public event Func<Task>? OnClose;
|
||||||
|
#pragma warning disable 0067
|
||||||
|
public event Func<Task>? OnReconnected;
|
||||||
|
public event Func<Task>? OnReconnecting;
|
||||||
|
public event Func<int, Task>? OnRequestRateLimited;
|
||||||
|
public event Func<Exception, Task>? OnError;
|
||||||
|
#pragma warning restore 0067
|
||||||
|
public event Func<int, Task>? OnRequestSent;
|
||||||
|
public event Action<WebSocketMessageType, ReadOnlyMemory<byte>>? OnStreamMessage;
|
||||||
|
public event Func<Task>? OnOpen;
|
||||||
|
|
||||||
|
public int Id { get; }
|
||||||
|
public bool IsClosed => !Connected;
|
||||||
|
public bool IsOpen => Connected;
|
||||||
|
public double IncomingKbps => 0;
|
||||||
|
public Uri Uri => new("wss://test.com/ws");
|
||||||
|
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||||
|
|
||||||
|
public Task<CallResult> ConnectAsync()
|
||||||
|
{
|
||||||
|
Connected = CanConnect;
|
||||||
|
return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Send(int requestId, string data, int weight)
|
||||||
|
{
|
||||||
|
if (!Connected)
|
||||||
|
throw new Exception("Socket not connected");
|
||||||
|
|
||||||
|
OnRequestSent?.Invoke(requestId);
|
||||||
|
OnMessageSend?.Invoke(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CloseAsync()
|
||||||
|
{
|
||||||
|
Connected = false;
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvokeClose()
|
||||||
|
{
|
||||||
|
Connected = false;
|
||||||
|
OnClose?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvokeOpen()
|
||||||
|
{
|
||||||
|
OnOpen?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvokeMessage(string data)
|
||||||
|
{
|
||||||
|
OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvokeMessage<T>(T data)
|
||||||
|
{
|
||||||
|
OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(data))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ReconnectAsync() => throw new NotImplementedException();
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestWebsocketFactory : IWebsocketFactory
|
||||||
|
{
|
||||||
|
private readonly TestSocket _socket;
|
||||||
|
public TestWebsocketFactory(TestSocket socket)
|
||||||
|
{
|
||||||
|
_socket = socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters) => _socket;
|
||||||
|
}
|
||||||
|
}
|
190
CryptoExchange.Net/Testing/RestRequestValidator.cs
Normal file
190
CryptoExchange.Net/Testing/RestRequestValidator.cs
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
using CryptoExchange.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Testing.Comparers;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validator for REST requests, comparing path, http method, authentication and response parsing
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TClient">The Rest client</typeparam>
|
||||||
|
public class RestRequestValidator<TClient> where TClient : BaseRestClient
|
||||||
|
{
|
||||||
|
private readonly TClient _client;
|
||||||
|
private readonly Func<WebCallResult, bool> _isAuthenticated;
|
||||||
|
private readonly string _folder;
|
||||||
|
private readonly string _baseAddress;
|
||||||
|
private readonly string? _nestedPropertyForCompare;
|
||||||
|
private readonly bool _stjCompare;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">Client to test</param>
|
||||||
|
/// <param name="folder">Folder for json test values</param>
|
||||||
|
/// <param name="baseAddress">The base address that is expected</param>
|
||||||
|
/// <param name="isAuthenticated">Func for checking if the request is authenticated</param>
|
||||||
|
/// <param name="nestedPropertyForCompare">Property to use for compare</param>
|
||||||
|
/// <param name="stjCompare">Use System.Text.Json for comparing</param>
|
||||||
|
public RestRequestValidator(TClient client, string folder, string baseAddress, Func<WebCallResult, bool> isAuthenticated, string? nestedPropertyForCompare = null, bool stjCompare = true)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_folder = folder;
|
||||||
|
_baseAddress = baseAddress;
|
||||||
|
_nestedPropertyForCompare = nestedPropertyForCompare;
|
||||||
|
_isAuthenticated = isAuthenticated;
|
||||||
|
_stjCompare = stjCompare;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate a request
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResponse">Expected response type</typeparam>
|
||||||
|
/// <param name="methodInvoke">Method invocation</param>
|
||||||
|
/// <param name="name">Method name for looking up json test values</param>
|
||||||
|
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
||||||
|
/// <param name="ignoreProperties">Ignore certain properties</param>
|
||||||
|
/// <param name="useSingleArrayItem">Use the first item of an json array response</param>
|
||||||
|
/// <param name="skipResponseValidation">Whether to skip the response model validation</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public Task ValidateAsync<TResponse>(
|
||||||
|
Func<TClient, Task<WebCallResult<TResponse>>> methodInvoke,
|
||||||
|
string name,
|
||||||
|
string? nestedJsonProperty = null,
|
||||||
|
List<string>? ignoreProperties = null,
|
||||||
|
bool useSingleArrayItem = false,
|
||||||
|
bool skipResponseValidation = false)
|
||||||
|
=> ValidateAsync<TResponse, TResponse>(methodInvoke, name, nestedJsonProperty, ignoreProperties, useSingleArrayItem, skipResponseValidation);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate a request
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResponse">Expected response type</typeparam>
|
||||||
|
/// <typeparam name="TActualResponse">The concrete response type</typeparam>
|
||||||
|
/// <param name="methodInvoke">Method invocation</param>
|
||||||
|
/// <param name="name">Method name for looking up json test values</param>
|
||||||
|
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
||||||
|
/// <param name="ignoreProperties">Ignore certain properties</param>
|
||||||
|
/// <param name="useSingleArrayItem">Use the first item of an json array response</param>
|
||||||
|
/// <param name="skipResponseValidation">Whether to skip the response model validation</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public async Task ValidateAsync<TResponse, TActualResponse>(
|
||||||
|
Func<TClient, Task<WebCallResult<TResponse>>> methodInvoke,
|
||||||
|
string name,
|
||||||
|
string? nestedJsonProperty = null,
|
||||||
|
List<string>? ignoreProperties = null,
|
||||||
|
bool useSingleArrayItem = false,
|
||||||
|
bool skipResponseValidation = false) where TActualResponse : TResponse
|
||||||
|
{
|
||||||
|
var listener = new EnumValueTraceListener();
|
||||||
|
Trace.Listeners.Add(listener);
|
||||||
|
|
||||||
|
var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName;
|
||||||
|
FileStream file;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt"));
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
throw new Exception($"Response file not found for {name}: {path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = new byte[file.Length];
|
||||||
|
await file.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||||
|
file.Close();
|
||||||
|
|
||||||
|
var data = Encoding.UTF8.GetString(buffer);
|
||||||
|
using var reader = new StringReader(data);
|
||||||
|
var expectedMethod = reader.ReadLine();
|
||||||
|
var expectedPath = reader.ReadLine();
|
||||||
|
var expectedAuth = bool.Parse(reader.ReadLine()!);
|
||||||
|
var response = reader.ReadToEnd();
|
||||||
|
|
||||||
|
TestHelpers.ConfigureRestClient(_client, response, System.Net.HttpStatusCode.OK);
|
||||||
|
var result = await methodInvoke(_client).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Check request/response properties
|
||||||
|
if (result.Error != null)
|
||||||
|
throw new Exception(name + " returned error " + result.Error);
|
||||||
|
if (_isAuthenticated(result.AsDataless()) != expectedAuth)
|
||||||
|
throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result.AsDataless())}");
|
||||||
|
if (result.RequestMethod != new HttpMethod(expectedMethod!))
|
||||||
|
throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}");
|
||||||
|
if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0])
|
||||||
|
throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}");
|
||||||
|
|
||||||
|
if (!skipResponseValidation)
|
||||||
|
{
|
||||||
|
// Check response data
|
||||||
|
object responseData = (TActualResponse)result.Data!;
|
||||||
|
if (_stjCompare == true)
|
||||||
|
SystemTextJsonComparer.CompareData(name, responseData, response, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useSingleArrayItem);
|
||||||
|
else
|
||||||
|
JsonNetComparer.CompareData(name, responseData, response, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useSingleArrayItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
Trace.Listeners.Remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate a request
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="methodInvoke">Method invocation</param>
|
||||||
|
/// <param name="name">Method name for looking up json test values</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public async Task ValidateAsync(
|
||||||
|
Func<TClient, Task<WebCallResult>> methodInvoke,
|
||||||
|
string name)
|
||||||
|
{
|
||||||
|
var listener = new EnumValueTraceListener();
|
||||||
|
Trace.Listeners.Add(listener);
|
||||||
|
|
||||||
|
var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName;
|
||||||
|
FileStream file;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt"));
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
throw new Exception($"Response file not found for {name}: {path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = new byte[file.Length];
|
||||||
|
await file.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||||
|
file.Close();
|
||||||
|
|
||||||
|
var data = Encoding.UTF8.GetString(buffer);
|
||||||
|
using var reader = new StringReader(data);
|
||||||
|
var expectedMethod = reader.ReadLine();
|
||||||
|
var expectedPath = reader.ReadLine();
|
||||||
|
var expectedAuth = bool.Parse(reader.ReadLine()!);
|
||||||
|
|
||||||
|
TestHelpers.ConfigureRestClient(_client, "", System.Net.HttpStatusCode.OK);
|
||||||
|
var result = await methodInvoke(_client).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Check request/response properties
|
||||||
|
if (result.Error != null)
|
||||||
|
throw new Exception(name + " returned error " + result.Error);
|
||||||
|
if (_isAuthenticated(result) != expectedAuth)
|
||||||
|
throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result)}");
|
||||||
|
if (result.RequestMethod != new HttpMethod(expectedMethod!))
|
||||||
|
throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}");
|
||||||
|
if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0])
|
||||||
|
throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}");
|
||||||
|
|
||||||
|
Trace.Listeners.Remove(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
163
CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs
Normal file
163
CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
using CryptoExchange.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.Testing.Comparers;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validator for websocket subscriptions, checking expected requests and responses and comparing update models
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TClient"></typeparam>
|
||||||
|
public class SocketSubscriptionValidator<TClient> where TClient : BaseSocketClient
|
||||||
|
{
|
||||||
|
private readonly TClient _client;
|
||||||
|
private readonly string _folder;
|
||||||
|
private readonly string _baseAddress;
|
||||||
|
private readonly string? _nestedPropertyForCompare;
|
||||||
|
private readonly bool _stjCompare;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">Client to test</param>
|
||||||
|
/// <param name="folder">Folder for json test values</param>
|
||||||
|
/// <param name="baseAddress">The base address that is expected</param>
|
||||||
|
/// <param name="nestedPropertyForCompare">Property to use for compare</param>
|
||||||
|
/// <param name="stjCompare">Use System.Text.Json for comparing</param>
|
||||||
|
public SocketSubscriptionValidator(TClient client, string folder, string baseAddress, string? nestedPropertyForCompare = null, bool stjCompare = true)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_folder = folder;
|
||||||
|
_baseAddress = baseAddress;
|
||||||
|
_nestedPropertyForCompare = nestedPropertyForCompare;
|
||||||
|
_stjCompare = stjCompare;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate a subscription
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TUpdate">The expected update type</typeparam>
|
||||||
|
/// <param name="methodInvoke">Subscription method invocation</param>
|
||||||
|
/// <param name="name">Method name for looking up json test values</param>
|
||||||
|
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
||||||
|
/// <param name="ignoreProperties">Ignore certain properties</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public async Task ValidateAsync<TUpdate>(
|
||||||
|
Func<TClient, Action<DataEvent<TUpdate>>, Task<CallResult<UpdateSubscription>>> methodInvoke,
|
||||||
|
string name,
|
||||||
|
string? nestedJsonProperty = null,
|
||||||
|
List<string>? ignoreProperties = null)
|
||||||
|
{
|
||||||
|
var listener = new EnumValueTraceListener();
|
||||||
|
Trace.Listeners.Add(listener);
|
||||||
|
|
||||||
|
var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName;
|
||||||
|
FileStream file ;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt"));
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
throw new Exception("Response file not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = new byte[file.Length];
|
||||||
|
await file.ReadAsync(buffer, 0, (int)file.Length).ConfigureAwait(false);
|
||||||
|
file.Close();
|
||||||
|
|
||||||
|
var data = Encoding.UTF8.GetString(buffer);
|
||||||
|
using var reader = new StringReader(data);
|
||||||
|
|
||||||
|
var socket = TestHelpers.ConfigureSocketClient(_client);
|
||||||
|
|
||||||
|
var waiter = new AutoResetEvent(false);
|
||||||
|
string? lastMessage = null;
|
||||||
|
socket.OnMessageSend += (x) =>
|
||||||
|
{
|
||||||
|
lastMessage = x;
|
||||||
|
waiter.Set();
|
||||||
|
};
|
||||||
|
|
||||||
|
TUpdate? update = default;
|
||||||
|
// Invoke subscription method
|
||||||
|
var task = methodInvoke(_client, x => { update = x.Data; });
|
||||||
|
|
||||||
|
string? overrideKey = null;
|
||||||
|
string? overrideValue = null;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var line = reader.ReadLine();
|
||||||
|
if (line == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (line.StartsWith("> "))
|
||||||
|
{
|
||||||
|
// Expect a message from client to server
|
||||||
|
waiter.WaitOne(TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
if (lastMessage == null)
|
||||||
|
throw new Exception($"{name} expected to {line} to be send to server but did not receive anything");
|
||||||
|
|
||||||
|
var lastMessageJson = JToken.Parse(lastMessage);
|
||||||
|
var expectedJson = JToken.Parse(line.Substring(2));
|
||||||
|
foreach(var item in expectedJson)
|
||||||
|
{
|
||||||
|
if (item is JProperty prop && prop.Value is JValue val)
|
||||||
|
{
|
||||||
|
if (val.ToString().StartsWith("|") && val.ToString().EndsWith("|"))
|
||||||
|
{
|
||||||
|
// |x| values are used to replace parts or response messages
|
||||||
|
overrideKey = val.ToString();
|
||||||
|
overrideValue = lastMessageJson[prop.Name]?.Value<string>();
|
||||||
|
}
|
||||||
|
else if (lastMessageJson[prop.Name]?.Value<string>() != val.ToString() && ignoreProperties?.Contains(prop.Name) != true)
|
||||||
|
throw new Exception($"{name} Expected {prop.Name} to be {val}, but was {lastMessageJson[prop.Name]?.Value<string>()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO check objects and arrays
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (line.StartsWith("< "))
|
||||||
|
{
|
||||||
|
// Expect a message from server to client
|
||||||
|
if (overrideKey != null)
|
||||||
|
{
|
||||||
|
line = line.Replace(overrideKey, overrideValue);
|
||||||
|
overrideKey = null;
|
||||||
|
overrideValue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.InvokeMessage(line.Substring(2));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// A update message from server to client
|
||||||
|
var compareData = reader.ReadToEnd();
|
||||||
|
socket.InvokeMessage(compareData);
|
||||||
|
|
||||||
|
if (update == null)
|
||||||
|
throw new Exception($"{name} Update send to client did not trigger in update handler");
|
||||||
|
|
||||||
|
if (_stjCompare == true)
|
||||||
|
SystemTextJsonComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties);
|
||||||
|
else
|
||||||
|
JsonNetComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _client.UnsubscribeAllAsync().ConfigureAwait(false);
|
||||||
|
Trace.Listeners.Remove(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
190
CryptoExchange.Net/Testing/TestHelpers.cs
Normal file
190
CryptoExchange.Net/Testing/TestHelpers.cs
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.Testing.Implementations;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Testing helpers
|
||||||
|
/// </summary>
|
||||||
|
public class TestHelpers
|
||||||
|
{
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
internal static bool AreEqual<T>(T? self, T? to, params string[] ignore) where T : class
|
||||||
|
{
|
||||||
|
if (self != null && to != null)
|
||||||
|
{
|
||||||
|
var type = self.GetType();
|
||||||
|
var ignoreList = new List<string>(ignore);
|
||||||
|
foreach (var pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||||
|
{
|
||||||
|
if (ignoreList.Contains(pi.Name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var selfValue = type.GetProperty(pi.Name)!.GetValue(self, null);
|
||||||
|
var toValue = type.GetProperty(pi.Name)!.GetValue(to, null);
|
||||||
|
|
||||||
|
if (pi.PropertyType.IsClass && !pi.PropertyType.Module.ScopeName.Equals("System.Private.CoreLib.dll"))
|
||||||
|
{
|
||||||
|
// Check of "CommonLanguageRuntimeLibrary" is needed because string is also a class
|
||||||
|
if (AreEqual(selfValue, toValue, ignore))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue)))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self == to;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static TestSocket ConfigureSocketClient<T>(T client) where T : BaseSocketClient
|
||||||
|
{
|
||||||
|
var socket = new TestSocket();
|
||||||
|
foreach (var apiClient in client.ApiClients.OfType<SocketApiClient>())
|
||||||
|
{
|
||||||
|
apiClient.SocketFactory = new TestWebsocketFactory(socket);
|
||||||
|
}
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ConfigureRestClient<T>(T client, string data, HttpStatusCode code) where T : BaseRestClient
|
||||||
|
{
|
||||||
|
foreach (var apiClient in client.ApiClients.OfType<RestApiClient>())
|
||||||
|
{
|
||||||
|
var expectedBytes = Encoding.UTF8.GetBytes(data);
|
||||||
|
var responseStream = new MemoryStream();
|
||||||
|
responseStream.Write(expectedBytes, 0, expectedBytes.Length);
|
||||||
|
responseStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
var response = new TestResponse(code, responseStream);
|
||||||
|
var request = new TestRequest(response);
|
||||||
|
|
||||||
|
var factory = new TestRequestFactory(request);
|
||||||
|
apiClient.RequestFactory = factory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check a signature matches the expected signature
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client"></param>
|
||||||
|
/// <param name="authProvider"></param>
|
||||||
|
/// <param name="method"></param>
|
||||||
|
/// <param name="path"></param>
|
||||||
|
/// <param name="getSignature"></param>
|
||||||
|
/// <param name="expectedSignature"></param>
|
||||||
|
/// <param name="parameters"></param>
|
||||||
|
/// <param name="time"></param>
|
||||||
|
/// <param name="disableOrdering"></param>
|
||||||
|
/// <param name="compareCase"></param>
|
||||||
|
/// <param name="host"></param>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public static void CheckSignature(
|
||||||
|
RestApiClient client,
|
||||||
|
AuthenticationProvider authProvider,
|
||||||
|
HttpMethod method,
|
||||||
|
string path,
|
||||||
|
Func<IDictionary<string, object>?, IDictionary<string, object>?, IDictionary<string, string>?, string> getSignature,
|
||||||
|
string expectedSignature,
|
||||||
|
Dictionary<string, object>? parameters = null,
|
||||||
|
DateTime? time = null,
|
||||||
|
bool disableOrdering = false,
|
||||||
|
bool compareCase = true,
|
||||||
|
string host = "https://test.test-api.com")
|
||||||
|
{
|
||||||
|
parameters ??= new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "test", 123 },
|
||||||
|
{ "test2", "abc" }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (disableOrdering)
|
||||||
|
client.OrderParameters = false;
|
||||||
|
|
||||||
|
var uriParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InUri ? client.CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||||
|
var bodyParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InBody ? client.CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||||
|
|
||||||
|
var headers = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
authProvider.TimeProvider = new TestAuthTimeProvider(time ?? new DateTime(2024, 01, 01, 0, 0, 0, DateTimeKind.Utc));
|
||||||
|
authProvider.AuthenticateRequest(
|
||||||
|
client,
|
||||||
|
new Uri(host.AppendPath(path)),
|
||||||
|
method,
|
||||||
|
uriParams,
|
||||||
|
bodyParams,
|
||||||
|
headers,
|
||||||
|
true,
|
||||||
|
client.ArraySerialization,
|
||||||
|
client.ParameterPositions[method],
|
||||||
|
client.RequestBodyFormat);
|
||||||
|
|
||||||
|
var signature = getSignature(uriParams, bodyParams, headers);
|
||||||
|
|
||||||
|
if (!string.Equals(signature, expectedSignature, compareCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new Exception($"Signatures do not match. Expected: {expectedSignature}, Actual: {signature}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scan the TClient rest client type for missing interface methods
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TClient"></typeparam>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public static void CheckForMissingRestInterfaces<TClient>()
|
||||||
|
{
|
||||||
|
CheckForMissingInterfaces(typeof(TClient), typeof(Task));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scan the TClient socket client type for missing interface methods
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TClient"></typeparam>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public static void CheckForMissingSocketInterfaces<TClient>()
|
||||||
|
{
|
||||||
|
CheckForMissingInterfaces(typeof(TClient), typeof(Task<CallResult<UpdateSubscription>>));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckForMissingInterfaces(Type clientType, Type implementationTypes)
|
||||||
|
{
|
||||||
|
var assembly = Assembly.GetAssembly(clientType);
|
||||||
|
var interfaceType = clientType.GetInterface("I" + clientType.Name);
|
||||||
|
var clientInterfaces = assembly.GetTypes().Where(t => t.Name.StartsWith("I" + clientType.Name));
|
||||||
|
|
||||||
|
foreach (var clientInterface in clientInterfaces)
|
||||||
|
{
|
||||||
|
var implementation = assembly.GetTypes().Single(t => clientInterface.IsAssignableFrom(t) && t != clientInterface);
|
||||||
|
int methods = 0;
|
||||||
|
foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType)))
|
||||||
|
{
|
||||||
|
var interfaceMethod = clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray());
|
||||||
|
if (interfaceMethod == null)
|
||||||
|
throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}");
|
||||||
|
methods++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"{clientInterface.Name} {methods} methods validated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user