mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2026-04-13 00:22:22 +00:00
Updated WebSocket message routing improving performance for scenarios with multiple different subscriptions and topics Added AddCommaSeparated helper for Enum value arrays to ParameterCollection Improved EnumConverter performance and removed string allocation for happy path Fixed CreateParamString extension method for ArrayParametersSerialization.Json Fixed Shared GetOrderBookOptions and GetRecentTradeOptions base validations not being called
378 lines
20 KiB
C#
378 lines
20 KiB
C#
using CryptoExchange.Net.Objects;
|
|
using NUnit.Framework;
|
|
using System;
|
|
using System.Net.Http;
|
|
using System.Threading.Tasks;
|
|
using System.Threading;
|
|
using NUnit.Framework.Legacy;
|
|
using CryptoExchange.Net.RateLimiting;
|
|
using CryptoExchange.Net.RateLimiting.Guards;
|
|
using CryptoExchange.Net.RateLimiting.Filters;
|
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
|
using System.Text.Json;
|
|
using CryptoExchange.Net.UnitTests.Implementations;
|
|
using CryptoExchange.Net.Testing;
|
|
|
|
namespace CryptoExchange.Net.UnitTests.ClientTests
|
|
{
|
|
[TestFixture()]
|
|
public class RestClientTests
|
|
{
|
|
[TestCase]
|
|
public async Task RequestingData_Should_ResultInData()
|
|
{
|
|
// arrange
|
|
var client = new TestRestClient();
|
|
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
|
|
var strData = JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() });
|
|
client.ApiClient1.SetNextResponse(strData, System.Net.HttpStatusCode.OK);
|
|
|
|
// act
|
|
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
|
|
|
|
// assert
|
|
Assert.That(result.Success);
|
|
Assert.That(TestHelpers.AreEqual(expected, result.Data));
|
|
}
|
|
|
|
[TestCase]
|
|
public async Task ReceivingInvalidData_Should_ResultInError()
|
|
{
|
|
// arrange
|
|
var client = new TestRestClient();
|
|
client.ApiClient1.SetNextResponse("{\"property\": 123", System.Net.HttpStatusCode.OK);
|
|
|
|
// act
|
|
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
|
|
|
|
// assert
|
|
ClassicAssert.IsFalse(result.Success);
|
|
Assert.That(result.Error != null);
|
|
}
|
|
|
|
[TestCase]
|
|
public async Task ReceivingErrorCode_Should_ResultInError()
|
|
{
|
|
// arrange
|
|
var client = new TestRestClient();
|
|
client.ApiClient1.SetNextResponse("Invalid request", System.Net.HttpStatusCode.BadRequest);
|
|
|
|
// act
|
|
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
|
|
|
|
// assert
|
|
ClassicAssert.IsFalse(result.Success);
|
|
Assert.That(result.Error != null);
|
|
}
|
|
|
|
[TestCase]
|
|
public async Task ReceivingErrorAndNotParsingError_Should_ResultInFlatError()
|
|
{
|
|
// arrange
|
|
var client = new TestRestClient();
|
|
client.ApiClient1.SetNextResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
|
|
|
// act
|
|
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
|
|
|
|
// assert
|
|
ClassicAssert.IsFalse(result.Success);
|
|
Assert.That(result.Error != null);
|
|
Assert.That(result.Error is ServerError);
|
|
}
|
|
|
|
[TestCase]
|
|
public async Task ReceivingErrorAndNotParsingErrorAndInvalidJson_Should_ContainData()
|
|
{
|
|
// arrange
|
|
var client = new TestRestClient();
|
|
var response = "<html>...</html>";
|
|
client.ApiClient1.SetNextResponse(response, System.Net.HttpStatusCode.BadRequest);
|
|
|
|
// act
|
|
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
|
|
|
|
// assert
|
|
ClassicAssert.IsFalse(result.Success);
|
|
Assert.That(result.Error != null);
|
|
Assert.That(result.Error is DeserializeError);
|
|
Assert.That(result.Error!.Message!.Contains(response));
|
|
}
|
|
|
|
[TestCase]
|
|
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
|
|
{
|
|
// arrange
|
|
var client = new TestRestClient();
|
|
client.ApiClient1.SetNextResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
|
|
|
// act
|
|
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
|
|
|
|
// assert
|
|
ClassicAssert.IsFalse(result.Success);
|
|
Assert.That(result.Error != null);
|
|
Assert.That(result.Error is ServerError);
|
|
Assert.That(result.Error!.ErrorCode == "123");
|
|
Assert.That(result.Error.Message == "Invalid request");
|
|
}
|
|
|
|
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
|
|
[TestCase("POST", HttpMethodParameterPosition.InBody)]
|
|
[TestCase("POST", HttpMethodParameterPosition.InUri)]
|
|
[TestCase("DELETE", HttpMethodParameterPosition.InBody)]
|
|
[TestCase("DELETE", HttpMethodParameterPosition.InUri)]
|
|
[TestCase("PUT", HttpMethodParameterPosition.InUri)]
|
|
[TestCase("PUT", HttpMethodParameterPosition.InBody)]
|
|
public async Task Setting_Should_ResultInOptionsSet(string method, HttpMethodParameterPosition pos)
|
|
{
|
|
// arrange
|
|
// act
|
|
var client = new TestRestClient();
|
|
|
|
var httpMethod = new HttpMethod(method);
|
|
client.ApiClient1.SetParameterPosition(httpMethod, pos);
|
|
client.ApiClient1.SetNextResponse("{}", System.Net.HttpStatusCode.OK);
|
|
|
|
var result = await client.ApiClient1.GetResponseAsync<TestObject>(httpMethod, new ParameterCollection
|
|
{
|
|
{ "TestParam1", "Value1" },
|
|
{ "TestParam2", 2 },
|
|
});
|
|
|
|
// assert
|
|
Assert.That(result.RequestMethod == new HttpMethod(method));
|
|
Assert.That(result.RequestBody?.Contains("TestParam1") == true == (pos == HttpMethodParameterPosition.InBody));
|
|
Assert.That((result.RequestUrl?.ToString().Contains("TestParam1")) == (pos == HttpMethodParameterPosition.InUri));
|
|
Assert.That(result.RequestBody?.Contains("TestParam2") == true == (pos == HttpMethodParameterPosition.InBody));
|
|
Assert.That((result.RequestUrl?.ToString().Contains("TestParam2")) == (pos == HttpMethodParameterPosition.InUri));
|
|
}
|
|
|
|
|
|
[TestCase(1, 0.1)]
|
|
[TestCase(2, 0.1)]
|
|
[TestCase(5, 1)]
|
|
[TestCase(1, 2)]
|
|
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
|
|
{
|
|
var rateLimiter = new RateLimitGate("Test");
|
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
|
|
|
|
var triggered = false;
|
|
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
|
|
var requestDefinition = new RequestDefinition("/sapi/v1/system/status", HttpMethod.Get);
|
|
|
|
for (var i = 0; i < requests + 1; i++)
|
|
{
|
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
Assert.That(i == requests ? triggered : !triggered);
|
|
}
|
|
triggered = false;
|
|
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
Assert.That(!triggered);
|
|
}
|
|
|
|
[TestCase("/sapi/test1", true)]
|
|
[TestCase("/sapi/test2", true)]
|
|
[TestCase("/api/test1", false)]
|
|
[TestCase("sapi/test1", true)]
|
|
[TestCase("/sapi/", true)]
|
|
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
|
{
|
|
var rateLimiter = new RateLimitGate("Test");
|
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
|
|
|
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
|
|
|
RateLimitEvent? evnt = null;
|
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
|
for (var i = 0; i < 2; i++)
|
|
{
|
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
bool expected = i == 1 ? expectLimiting ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
|
|
Assert.That(expected);
|
|
}
|
|
}
|
|
|
|
[TestCase("/sapi/", "/sapi/", true)]
|
|
[TestCase("/sapi/test", "/sapi/test", true)]
|
|
[TestCase("/sapi/test", "/sapi/test123", false)]
|
|
[TestCase("/sapi/test", "/sapi/", false)]
|
|
public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting)
|
|
{
|
|
var rateLimiter = new RateLimitGate("Test");
|
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
|
|
|
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
|
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get);
|
|
|
|
RateLimitEvent? evnt = null;
|
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
|
|
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
Assert.That(evnt == null);
|
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
Assert.That(expectLimiting ? evnt != null : evnt == null);
|
|
}
|
|
|
|
[TestCase(1, 0.1)]
|
|
[TestCase(2, 0.1)]
|
|
[TestCase(5, 1)]
|
|
[TestCase(1, 2)]
|
|
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
|
|
{
|
|
var rateLimiter = new RateLimitGate("Test");
|
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/test"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
|
|
|
|
bool triggered = false;
|
|
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
|
|
var requestDefinition = new RequestDefinition("/sapi/test", HttpMethod.Get);
|
|
|
|
for (var i = 0; i < requests + 1; i++)
|
|
{
|
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
Assert.That(i == requests ? triggered : !triggered);
|
|
}
|
|
triggered = false;
|
|
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
Assert.That(!triggered);
|
|
}
|
|
|
|
[TestCase("/", false)]
|
|
[TestCase("/sapi/test", true)]
|
|
[TestCase("/sapi/test/123", false)]
|
|
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
|
|
{
|
|
var rateLimiter = new RateLimitGate("Test");
|
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
|
|
|
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
|
|
|
RateLimitEvent? evnt = null;
|
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
|
for (var i = 0; i < 2; i++)
|
|
{
|
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
|
|
Assert.That(expected);
|
|
}
|
|
}
|
|
|
|
[TestCase("/", false)]
|
|
[TestCase("/sapi/test", true)]
|
|
[TestCase("/sapi/test2", true)]
|
|
[TestCase("/sapi/test23", false)]
|
|
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
|
|
{
|
|
var rateLimiter = new RateLimitGate("Test");
|
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathsFilter(new[] { "/sapi/test", "/sapi/test2" }), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
|
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
|
|
|
RateLimitEvent? evnt = null;
|
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
|
for (var i = 0; i < 2; i++)
|
|
{
|
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
|
|
Assert.That(expected);
|
|
}
|
|
}
|
|
|
|
[TestCase("123", "123", "/sapi/test", "/sapi/test", true)]
|
|
[TestCase("123", "456", "/sapi/test", "/sapi/test", false)]
|
|
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true)]
|
|
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true)]
|
|
[TestCase(null, "123", "/sapi/test", "/sapi/test", false)]
|
|
[TestCase("123", null, "/sapi/test", "/sapi/test", false)]
|
|
[TestCase(null, null, "/sapi/test", "/sapi/test", false)]
|
|
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool expectLimited)
|
|
{
|
|
var rateLimiter = new RateLimitGate("Test");
|
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding));
|
|
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
|
|
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
|
|
|
|
RateLimitEvent? evnt = null;
|
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
|
|
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default);
|
|
Assert.That(evnt == null);
|
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, null, default);
|
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
|
}
|
|
|
|
[TestCase("/sapi/test", "/sapi/test", true)]
|
|
[TestCase("/sapi/test1", "/api/test2", true)]
|
|
[TestCase("/", "/sapi/test2", true)]
|
|
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
|
|
{
|
|
var rateLimiter = new RateLimitGate("Test");
|
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, Array.Empty<IGuardFilter>(), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
|
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
|
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
|
|
|
RateLimitEvent? evnt = null;
|
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
|
|
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
Assert.That(evnt == null);
|
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, null, default);
|
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
|
}
|
|
|
|
[TestCase("https://test.com", "/sapi/test", "https://test.com", "/sapi/test", true)]
|
|
[TestCase("https://test2.com", "/sapi/test", "https://test.com", "/sapi/test", false)]
|
|
[TestCase("https://test.com", "/sapi/test", "https://test2.com", "/sapi/test", false)]
|
|
[TestCase("https://test.com", "/sapi/test", "https://test.com", "/sapi/test2", true)]
|
|
public async Task HostRateLimiterBasics(string host1, string endpoint1, string host2, string endpoint2, bool expectLimited)
|
|
{
|
|
var rateLimiter = new RateLimitGate("Test");
|
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new HostFilter("https://test.com"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
|
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
|
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
|
|
|
RateLimitEvent? evnt = null;
|
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
|
|
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
Assert.That(evnt == null);
|
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
|
}
|
|
|
|
[TestCase("https://test.com", "https://test.com", true)]
|
|
[TestCase("https://test2.com", "https://test.com", false)]
|
|
[TestCase("https://test.com", "https://test2.com", false)]
|
|
public async Task ConnectionRateLimiterBasics(string host1, string host2, bool expectLimited)
|
|
{
|
|
var rateLimiter = new RateLimitGate("Test");
|
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
|
|
|
RateLimitEvent? evnt = null;
|
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
|
|
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
Assert.That(evnt == null);
|
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
|
}
|
|
|
|
[Test]
|
|
public async Task ConnectionRateLimiterCancel()
|
|
{
|
|
var rateLimiter = new RateLimitGate("Test");
|
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
|
|
|
|
RateLimitEvent? evnt = null;
|
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
|
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
|
|
|
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
|
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
|
|
}
|
|
}
|
|
}
|