mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2026-04-07 10:11:10 +00:00
Updated API credential logic, exchange implementation are expected to provide their own credentials implementation with ApiCredentials as base class Removed ApiCredentials implementation used by most exchanges Removed ApiCredentialsType Enum Added CredentialSet base class and implementations for defining different API credentials Added optional type param to AuthenticationProvider for the specific API credential type to improve type safety Moved AuthenticationProvider/ApiCredentials from BaseApiClient to RestApiClient/SocketApiClient base classes Added optional type params to RestApiClient/SocketApiClient base class to specify the AuthenticationProvider type and credentials type to improve type safety Moved SetOptions/SetApiCredentials from BaseApiClient to RestApiClient/SocketApiClient Extracted LibraryOptions<TRestOptions, TSocketOptions, TEnvironment> without TApiCredentials for libraries without API credentials Removed ApiCredentials from ApiOptions, credentials can only be configured at library, rest or socket level Added EnvironmentName to RestApiClient/SocketApiClient property Added Unknown enum value to Shared interfaces SharedOrderStatus, SharedTransferStatus and SharedTriggerOrderStatus enums Updated Enum converter to map value to an undefined Enum value instead of the first Enum value Added support for checking for missing fields on RestIntegrationTest Added BytesToHexString and HexToBytesString to ExchangeHelpers static class Fixed bug where WebSocket connections are not reconnected when configuring Proxy with SetUpdates Removed legacy CryptoBaseClient, CryptoRestClient and CryptoSocketClient
158 lines
5.8 KiB
C#
158 lines
5.8 KiB
C#
using CryptoExchange.Net.Interfaces;
|
|
using CryptoExchange.Net.Objects;
|
|
using CryptoExchange.Net.Testing.Comparers;
|
|
using Microsoft.Extensions.Logging;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq.Expressions;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace CryptoExchange.Net.Testing
|
|
{
|
|
/// <summary>
|
|
/// Base class for executing REST API integration tests
|
|
/// </summary>
|
|
/// <typeparam name="TClient">Client type</typeparam>
|
|
public abstract class RestIntegrationTest<TClient>
|
|
{
|
|
/// <summary>
|
|
/// Get a client instance
|
|
/// </summary>
|
|
public abstract TClient GetClient(ILoggerFactory loggerFactory);
|
|
|
|
/// <summary>
|
|
/// Whether the test should be run. By default integration tests aren't executed, can be set to true to force execution.
|
|
/// </summary>
|
|
public virtual bool Run { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether API credentials are provided and thus authenticated calls can be executed. Should be set in the GetClient implementation.
|
|
/// </summary>
|
|
public bool Authenticated { get; set; }
|
|
|
|
/// <summary>
|
|
/// Create a client
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
protected TClient CreateClient()
|
|
{
|
|
var fact = new LoggerFactory();
|
|
fact.AddProvider(new TraceLoggerProvider());
|
|
return GetClient(fact);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if integration tests should be executed
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
protected bool ShouldRun()
|
|
{
|
|
var integrationTests = Environment.GetEnvironmentVariable("INTEGRATION");
|
|
if (!Run && integrationTests != "1")
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Execute a REST endpoint call and check for any errors or warnings.
|
|
/// </summary>
|
|
/// <typeparam name="T">Type of response</typeparam>
|
|
/// <param name="expression">The call expression</param>
|
|
/// <param name="authRequest">Whether this is an authenticated request</param>
|
|
/// <param name="checkMissingFields">Whether to check the response for missing fields</param>
|
|
/// <param name="compareNestedProperty">Nested property to use for comparing when checking for missing fields</param>
|
|
/// <param name="ignoreProperties">Properties to ignore when checking for missing fields</param>
|
|
/// <param name="useSingleArrayItem">Whether to use the single array item as compare when checking for missing fields</param>
|
|
public async Task RunAndCheckResult<T>(
|
|
Expression<Func<TClient, Task<WebCallResult<T>>>> expression,
|
|
bool authRequest,
|
|
bool checkMissingFields = false,
|
|
string? compareNestedProperty = null,
|
|
List<string>? ignoreProperties = null,
|
|
bool? useSingleArrayItem = null)
|
|
{
|
|
if (!ShouldRun())
|
|
return;
|
|
|
|
var client = CreateClient();
|
|
|
|
var expressionBody = (MethodCallExpression)expression.Body;
|
|
if (authRequest && !Authenticated)
|
|
{
|
|
Debug.WriteLine($"Skipping {expressionBody.Method.Name}, not authenticated");
|
|
return;
|
|
}
|
|
|
|
var listener = new EnumValueTraceListener();
|
|
Trace.Listeners.Add(listener);
|
|
|
|
WebCallResult<T> result;
|
|
try
|
|
{
|
|
result = await expression.Compile().Invoke(client).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new Exception($"Method {expressionBody.Method.Name} threw an exception: " + ex.ToLogString());
|
|
}
|
|
finally
|
|
{
|
|
Trace.Listeners.Remove(listener);
|
|
}
|
|
|
|
if (!result.Success)
|
|
throw new Exception($"Method {expressionBody.Method.Name} returned error: " + result.Error);
|
|
|
|
if (checkMissingFields)
|
|
{
|
|
var data = result.Data;
|
|
var originalData = result.OriginalData;
|
|
if (originalData == null)
|
|
throw new Exception($"Original data needs to be enabled in the client options to check for missing fields");
|
|
|
|
try {
|
|
SystemTextJsonComparer.CompareData(expressionBody.Method.Name, data, originalData, compareNestedProperty, ignoreProperties, useSingleArrayItem ?? false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new Exception($"Compare failed: {ex.Message}; original data: {originalData}", ex);
|
|
}
|
|
}
|
|
|
|
Debug.WriteLine($"{expressionBody.Method.Name} {result}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start an order book implementation and expect it to sync and produce an update
|
|
/// </summary>
|
|
public async Task TestOrderBook(ISymbolOrderBook book)
|
|
{
|
|
if (!ShouldRun())
|
|
return;
|
|
|
|
var bookHasChanged = false;
|
|
book.OnStatusChange += (_, news) =>
|
|
{
|
|
if (news == OrderBookStatus.Reconnecting)
|
|
throw new Exception($"Book reconnecting");
|
|
};
|
|
book.OnOrderBookUpdate += (change) =>
|
|
{
|
|
bookHasChanged = true;
|
|
};
|
|
|
|
var result = await book.StartAsync().ConfigureAwait(false);
|
|
if (!result)
|
|
throw new Exception($"Book failed to start: " + result.Error);
|
|
|
|
await Task.Delay(5000).ConfigureAwait(false);
|
|
await book.StopAsync().ConfigureAwait(false);
|
|
|
|
if (!bookHasChanged)
|
|
throw new Exception($"Expected book to have changed at least once");
|
|
}
|
|
}
|
|
}
|