1
0
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:
Jan Korf 2024-05-01 19:24:53 +02:00 committed by GitHub
parent 96c9a55c48
commit 050286ecd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1740 additions and 221 deletions

View File

@ -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();

View File

@ -1,5 +1,6 @@
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
@ -15,6 +16,8 @@ namespace CryptoExchange.Net.Authentication
/// </summary>
public abstract class AuthenticationProvider : IDisposable
{
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
/// <summary>
/// Provided credentials
/// </summary>
@ -44,7 +47,6 @@ namespace CryptoExchange.Net.Authentication
/// <param name="apiClient">The Api client sending the request</param>
/// <param name="uri">The uri for 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="arraySerialization">Array serialization type</param>
/// <param name="parameterPosition">The position where the providedParameters should go</param>
@ -56,14 +58,13 @@ namespace CryptoExchange.Net.Authentication
RestApiClient apiClient,
Uri uri,
HttpMethod method,
Dictionary<string, object> providedParameters,
IDictionary<string, object> uriParameters,
IDictionary<string, object> bodyParameters,
Dictionary<string, string> headers,
bool auth,
ArrayParametersSerialization arraySerialization,
HttpMethodParameterPosition parameterPosition,
RequestBodyFormat requestBodyFormat,
out SortedDictionary<string, object> uriParameters,
out SortedDictionary<string, object> bodyParameters,
out Dictionary<string, string> headers
RequestBodyFormat requestBodyFormat
);
/// <summary>
@ -418,9 +419,9 @@ namespace CryptoExchange.Net.Authentication
/// </summary>
/// <param name="apiClient"></param>
/// <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>
@ -428,7 +429,7 @@ namespace CryptoExchange.Net.Authentication
/// </summary>
/// <param name="apiClient"></param>
/// <returns></returns>
protected static string GetMillisecondTimestamp(RestApiClient apiClient)
protected string GetMillisecondTimestamp(RestApiClient apiClient)
{
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
}

View File

@ -40,23 +40,33 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Request body content type
/// </summary>
protected RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
/// <summary>
/// How to serialize array parameters when making requests
/// </summary>
protected ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
/// <summary>
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
/// </summary>
protected string RequestBodyEmptyContent = "{}";
protected internal string RequestBodyEmptyContent = "{}";
/// <summary>
/// Request headers to be sent with each request
/// </summary>
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>
/// Where to put the parameters for requests with different Http methods
/// </summary>
@ -269,22 +279,9 @@ namespace CryptoExchange.Net.Clients
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
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 uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? 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 ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
if (AuthenticationProvider != null)
{
try
@ -293,14 +290,13 @@ namespace CryptoExchange.Net.Clients
this,
uri,
definition.Method,
parameters,
uriParameters,
bodyParameters,
headers,
definition.Authenticated,
arraySerialization,
parameterPosition,
bodyFormat,
out uriParameters,
out bodyParameters,
out headers);
bodyFormat);
}
catch (Exception ex)
{
@ -346,7 +342,7 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InBody)
{
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParameters.Any())
if (bodyParameters.Count != 0)
WriteParamBody(request, bodyParameters, contentType);
else
request.SetContent(RequestBodyEmptyContent, contentType);
@ -603,7 +599,7 @@ namespace CryptoExchange.Net.Clients
if (!valid)
{
// 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);
}
@ -726,8 +722,8 @@ namespace CryptoExchange.Net.Clients
}
var headers = new Dictionary<string, string>();
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? 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 ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
if (AuthenticationProvider != null)
{
try
@ -736,14 +732,13 @@ namespace CryptoExchange.Net.Clients
this,
uri,
method,
parameters,
uriParameters,
bodyParameters,
headers,
signed,
arraySerialization,
parameterPosition,
bodyFormat,
out uriParameters,
out bodyParameters,
out headers);
bodyFormat);
}
catch (Exception ex)
{
@ -804,7 +799,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="request">The request to set the parameters on</param>
/// <param name="parameters">The parameters to set</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)
{
@ -859,6 +854,19 @@ namespace CryptoExchange.Net.Clients
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>
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
/// </summary>

View File

@ -1,4 +1,5 @@
using Newtonsoft.Json;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@ -38,14 +39,8 @@ namespace CryptoExchange.Net.Converters.JsonNet
var longValue = (long)reader.Value;
if (longValue == 0 || longValue == -1)
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)
{
@ -68,6 +63,43 @@ namespace CryptoExchange.Net.Converters.JsonNet
return objectType == typeof(DateTime) ? default(DateTime) : null;
}
return ParseFromString(stringValue);
}
else if(reader.TokenType == JsonToken.Date)
{
return (DateTime)reader.Value;
}
else
{
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
return default;
}
}
/// <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
@ -77,7 +109,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|| !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);
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);
@ -90,7 +122,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|| !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);
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);
@ -103,7 +135,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|| !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);
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);
@ -122,15 +154,15 @@ namespace CryptoExchange.Net.Converters.JsonNet
return ConvertFromNanoseconds((long)doubleValue);
}
if(stringValue.Length == 10)
if (stringValue.Length == 10)
{
// Parse 2021-11-03 format
var values = stringValue.Split('-');
if(!int.TryParse(values[0], out var year)
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);
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
@ -139,16 +171,6 @@ namespace CryptoExchange.Net.Converters.JsonNet
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
}
else if(reader.TokenType == JsonToken.Date)
{
return (DateTime)reader.Value;
}
else
{
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
return default;
}
}
/// <summary>
/// Convert a seconds since epoch (01-01-1970) value to DateTime

View File

@ -224,7 +224,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <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)
{
@ -252,14 +252,15 @@ namespace CryptoExchange.Net.Converters.JsonNet
{
_token = await JToken.LoadAsync(jsonTextReader).ConfigureAwait(false);
IsJson = true;
return new CallResult(null);
}
catch (Exception)
catch (Exception ex)
{
// Not a json message
IsJson = false;
return new CallResult(new ServerError("JsonError: " + ex.Message));
}
return IsJson;
}
/// <inheritdoc />
public override string GetOriginalString()
@ -290,7 +291,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
private ReadOnlyMemory<byte> _bytes;
/// <inheritdoc />
public bool Read(ReadOnlyMemory<byte> data)
public CallResult Read(ReadOnlyMemory<byte> data)
{
_bytes = data;
@ -305,14 +306,14 @@ namespace CryptoExchange.Net.Converters.JsonNet
{
_token = JToken.Load(jsonTextReader);
IsJson = true;
return new CallResult(null);
}
catch (Exception)
catch (Exception ex)
{
// Not a json message
IsJson = false;
return new CallResult(new ServerError("JsonError: " + ex.Message));
}
return IsJson;
}
/// <inheritdoc />

View File

@ -1,4 +1,5 @@
using System;
using Microsoft.Extensions.Primitives;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@ -49,14 +50,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var longValue = reader.GetDouble();
if (longValue == 0 || longValue == -1)
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)
{
@ -68,6 +63,53 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return default;
}
return ParseFromString(stringValue!);
}
else
{
return reader.GetDateTime();
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value == null)
writer.WriteNullValue();
else
{
var dtValue = (DateTime)(object)value;
if (dtValue == default)
writer.WriteStringValue(default(DateTime));
else
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
}
}
}
/// <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
@ -139,26 +181,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
}
else
{
return reader.GetDateTime();
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value == null)
writer.WriteNullValue();
else
{
var dtValue = (DateTime)(object)value;
if (dtValue == default)
writer.WriteStringValue(default(DateTime));
else
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
}
}
}
/// <summary>
/// Convert a seconds since epoch (01-01-1970) value to DateTime

View File

@ -188,7 +188,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <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)
{
@ -211,15 +211,16 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
IsJson = true;
return new CallResult(null);
}
catch (Exception)
catch (Exception ex)
{
// Not a json message
IsJson = false;
return new CallResult(new ServerError("JsonError: " + ex.Message));
}
}
return IsJson;
}
/// <inheritdoc />
public override string GetOriginalString()
{
@ -249,7 +250,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private ReadOnlyMemory<byte> _bytes;
/// <inheritdoc />
public bool Read(ReadOnlyMemory<byte> data)
public CallResult Read(ReadOnlyMemory<byte> data)
{
_bytes = data;
@ -257,14 +258,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
_document = JsonDocument.Parse(data);
IsJson = true;
return new CallResult(null);
}
catch (Exception)
catch (Exception ex)
{
// Not a json message
IsJson = false;
return new CallResult(new ServerError("JsonError: " + ex.Message));
}
return IsJson;
}
/// <inheritdoc />

View File

@ -342,7 +342,7 @@ namespace CryptoExchange.Net
/// <param name="baseUri"></param>
/// <param name="arraySerialization"></param>
/// <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();
uriBuilder.Scheme = baseUri.Scheme;

View 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();
}
}

View File

@ -84,7 +84,7 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
/// <param name="stream"></param>
/// <param name="bufferStream"></param>
Task<bool> Read(Stream stream, bool bufferStream);
Task<CallResult> Read(Stream stream, bool bufferStream);
}
/// <summary>
@ -96,6 +96,6 @@ namespace CryptoExchange.Net.Interfaces
/// Load a data message
/// </summary>
/// <param name="data"></param>
bool Read(ReadOnlyMemory<byte> data);
CallResult Read(ReadOnlyMemory<byte> data);
}
}

View File

@ -0,0 +1,10 @@
using CryptoExchange.Net.Interfaces;
using System;
namespace CryptoExchange.Net.Objects
{
internal class AuthTimeProvider : IAuthTimeProvider
{
public DateTime GetTime() => DateTime.UtcNow;
}
}

View 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);
}
}
}

View 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}");
}
}
}
}

View 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}");
}
}
}
}

View 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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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;
}
}
}

View File

@ -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;
}
}
}

View 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);
}
}

View 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() { }
}
}

View File

@ -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;
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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");
}
}
}
}