diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..eba1110 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3d05591 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: csharp +mono: none +solution: CryptoExchange.Net.sln +dotnet: 2.0.0 +dist: trusty +script: + - dotnet build CryptoExchange.Net/CryptoExchange.Net.csproj --framework "netstandard2.0" + - dotnet test CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj \ No newline at end of file diff --git a/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj b/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj new file mode 100644 index 0000000..28c7213 --- /dev/null +++ b/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + + diff --git a/CryptoExchange.Net.UnitTests/ExchangeClientTests.cs b/CryptoExchange.Net.UnitTests/ExchangeClientTests.cs new file mode 100644 index 0000000..b9be9e6 --- /dev/null +++ b/CryptoExchange.Net.UnitTests/ExchangeClientTests.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Logging; +using CryptoExchange.Net.RateLimiter; +using Moq; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace CryptoExchange.Net.UnitTests +{ + [TestFixture()] + public class ExchangeClientTests + { + [TestCase(null, null)] + [TestCase("", "")] + [TestCase("test", null)] + [TestCase("test", "")] + [TestCase(null, "test")] + [TestCase("", "test")] + public void SettingEmptyValuesForAPICredentials_Should_ThrowException(string key, string secret) + { + // arrange + var client = PrepareClient(""); + + // act + // assert + Assert.Throws(typeof(ArgumentException), () => client.SetApiCredentails(key, secret)); + } + + [TestCase()] + public void SettingLogOutput_Should_RedirectLogOutput() + { + // arrange + var stringBuilder = new StringBuilder(); + var client = PrepareClient("{}", true, LogVerbosity.Debug, new StringWriter(stringBuilder)); + + // act + client.TestCall(); + + // assert + Assert.IsFalse(string.IsNullOrEmpty(stringBuilder.ToString())); + } + + [TestCase()] + public void ObjectDeserializationFail_Should_GiveFailedResult() + { + // arrange + var errorMessage = "TestErrorMessage"; + var client = PrepareClient(JsonConvert.SerializeObject(errorMessage)); + + // act + var result = client.TestCall(); + + // assert + Assert.IsFalse(result.Success); + Assert.AreNotEqual(0, result.Error.Code); + Assert.IsTrue(result.Error.Message.Contains(errorMessage)); + } + + [TestCase()] + public void InvalidJson_Should_GiveFailedResult() + { + // arrange + var errorMessage = "TestErrorMessage"; + var client = PrepareClient(JsonConvert.SerializeObject(errorMessage)); + + // act + var result = client.TestCall(); + + // assert + Assert.IsFalse(result.Success); + Assert.AreNotEqual(0, result.Error.Code); + Assert.IsTrue(result.Error.Message.Contains(errorMessage)); + } + + [TestCase()] + public void WhenUsingRateLimiterTotalRequests_Should_BeDelayed() + { + // arrange + var client = PrepareClient(JsonConvert.SerializeObject(new TestObject())); + client.AddRateLimiter(new RateLimiterTotal(1, TimeSpan.FromSeconds(5))); + + // act + var sw = Stopwatch.StartNew(); + client.TestCall(); + client.TestCall(); + client.TestCall(); + sw.Stop(); + + // assert + Assert.IsTrue(sw.ElapsedMilliseconds > 9000); + } + + [TestCase()] + public void WhenUsingRateLimiterPerEndpointRequests_Should_BeDelayed() + { + // arrange + var client = PrepareClient(JsonConvert.SerializeObject(new TestObject())); + client.AddRateLimiter(new RateLimiterTotal(1, TimeSpan.FromSeconds(5))); + + // act + var sw = Stopwatch.StartNew(); + client.TestCall(); + client.TestCall(); + client.TestCall(); + sw.Stop(); + + // assert + Assert.IsTrue(sw.ElapsedMilliseconds > 9000); + } + + [TestCase()] + public void WhenRemovingRateLimiterRequest_Should_NoLongerBeDelayed() + { + // arrange + var client = PrepareClient(JsonConvert.SerializeObject(new TestObject())); + client.AddRateLimiter(new RateLimiterTotal(1, TimeSpan.FromSeconds(5))); + client.RemoveRateLimiters(); + + // act + var sw = Stopwatch.StartNew(); + client.TestCall(); + client.TestCall(); + client.TestCall(); + sw.Stop(); + + // assert + Assert.IsTrue(sw.ElapsedMilliseconds < 5000); + } + + [TestCase()] + public void ReceivingErrorStatusCode_Should_NotSuccess() + { + // arrange + var client = PrepareExceptionClient(JsonConvert.SerializeObject(new TestObject()), "InvalidStatusCodeResponse", 203); + + // act + var result = client.TestCall(); + + // assert + Assert.IsFalse(result.Success); + Assert.IsNotNull(result.Error); + Assert.IsTrue(result.Error.Message.Contains("InvalidStatusCodeResponse")); + } + + private TestImplementation PrepareClient(string responseData, bool withOptions = true, LogVerbosity verbosity = LogVerbosity.Warning, TextWriter tw = null) + { + var expectedBytes = Encoding.UTF8.GetBytes(responseData); + var responseStream = new MemoryStream(); + responseStream.Write(expectedBytes, 0, expectedBytes.Length); + responseStream.Seek(0, SeekOrigin.Begin); + + var response = new Mock(); + response.Setup(c => c.GetResponseStream()).Returns(responseStream); + + var request = new Mock(); + request.Setup(c => c.Headers).Returns(new WebHeaderCollection()); + request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); + request.Setup(c => c.GetResponse()).Returns(Task.FromResult(response.Object)); + + var factory = new Mock(); + factory.Setup(c => c.Create(It.IsAny())) + .Returns(request.Object); + TestImplementation client; + if (withOptions) + { + client = new TestImplementation(new ExchangeOptions() + { + ApiCredentials = new ApiCredentials("Test", "Test2"), + LogVerbosity = verbosity, + LogWriter = tw + }); + } + else + { + client = new TestImplementation(); + } + client.RequestFactory = factory.Object; + return client; + } + + private TestImplementation PrepareExceptionClient(string responseData, string exceptionMessage, int statusCode, bool credentials = true) + { + var expectedBytes = Encoding.UTF8.GetBytes(responseData); + var responseStream = new MemoryStream(); + responseStream.Write(expectedBytes, 0, expectedBytes.Length); + responseStream.Seek(0, SeekOrigin.Begin); + + var we = new WebException(); + var r = new HttpWebResponse(); + var re = new HttpResponseMessage(); + + typeof(HttpResponseMessage).GetField("_statusCode", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(re, (HttpStatusCode)statusCode); + typeof(HttpWebResponse).GetField("_httpResponseMessage", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(r, re); + typeof(WebException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, exceptionMessage); + typeof(WebException).GetField("_response", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, r); + + var response = new Mock(); + response.Setup(c => c.GetResponseStream()).Throws(we); + + var request = new Mock(); + request.Setup(c => c.Headers).Returns(new WebHeaderCollection()); + request.Setup(c => c.GetResponse()).Returns(Task.FromResult(response.Object)); + + var factory = new Mock(); + factory.Setup(c => c.Create(It.IsAny())) + .Returns(request.Object); + + TestImplementation client = credentials ? new TestImplementation(new ExchangeOptions() { ApiCredentials = new ApiCredentials("Test", "Test2") }) : new TestImplementation(); + client.RequestFactory = factory.Object; + return client; + } + } +} diff --git a/CryptoExchange.Net.UnitTests/TestImplementation.cs b/CryptoExchange.Net.UnitTests/TestImplementation.cs new file mode 100644 index 0000000..b4f75eb --- /dev/null +++ b/CryptoExchange.Net.UnitTests/TestImplementation.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Text; +using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Interfaces; + +namespace CryptoExchange.Net.UnitTests +{ + public class TestImplementation: ExchangeClient + { + public TestImplementation(): base(new ExchangeOptions(), null) { } + + public TestImplementation(ExchangeOptions exchangeOptions) : base(exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials)) + { + } + + public void SetApiCredentails(string key, string secret) + { + SetAuthenticationProvider(new TestAuthProvider(new ApiCredentials(key, secret))); + } + + public CallResult TestCall() + { + return ExecuteRequest(new Uri("http://www.test.com")).Result; + } + } + + public class TestAuthProvider : AuthenticationProvider + { + public TestAuthProvider(ApiCredentials credentials) : base(credentials) + { + } + + public override string AddAuthenticationToUriString(string uri, bool signed) + { + return uri; + } + + public override IRequest AddAuthenticationToRequest(IRequest request, bool signed) + { + return request; + } + + public override string Sign(string toSign) + { + return toSign; + } + } + + public class TestObject + { + public int Id { get; set; } + public List Data { get; set; } + } +} diff --git a/CryptoExchange.Net.sln b/CryptoExchange.Net.sln new file mode 100644 index 0000000..0f431bb --- /dev/null +++ b/CryptoExchange.Net.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27004.2008 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoExchange.Net", "CryptoExchange.Net\CryptoExchange.Net.csproj", "{3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoExchange.Net.UnitTests", "CryptoExchange.Net.UnitTests\CryptoExchange.Net.UnitTests.csproj", "{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}.Release|Any CPU.Build.0 = Release|Any CPU + {FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0D1B9CE9-E0B7-4B8B-88BF-6EA2CC8CA3D7} + EndGlobalSection +EndGlobal diff --git a/ApiProxy.cs b/CryptoExchange.Net/ApiProxy.cs similarity index 100% rename from ApiProxy.cs rename to CryptoExchange.Net/ApiProxy.cs diff --git a/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs similarity index 100% rename from Authentication/ApiCredentials.cs rename to CryptoExchange.Net/Authentication/ApiCredentials.cs diff --git a/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs similarity index 100% rename from Authentication/AuthenticationProvider.cs rename to CryptoExchange.Net/Authentication/AuthenticationProvider.cs diff --git a/BaseConverter.cs b/CryptoExchange.Net/BaseConverter.cs similarity index 100% rename from BaseConverter.cs rename to CryptoExchange.Net/BaseConverter.cs diff --git a/CallResult.cs b/CryptoExchange.Net/CallResult.cs similarity index 100% rename from CallResult.cs rename to CryptoExchange.Net/CallResult.cs diff --git a/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj similarity index 100% rename from CryptoExchange.Net.csproj rename to CryptoExchange.Net/CryptoExchange.Net.csproj diff --git a/Error.cs b/CryptoExchange.Net/Error.cs similarity index 100% rename from Error.cs rename to CryptoExchange.Net/Error.cs diff --git a/ExchangeClient.cs b/CryptoExchange.Net/ExchangeClient.cs similarity index 100% rename from ExchangeClient.cs rename to CryptoExchange.Net/ExchangeClient.cs diff --git a/ExchangeOptions.cs b/CryptoExchange.Net/ExchangeOptions.cs similarity index 100% rename from ExchangeOptions.cs rename to CryptoExchange.Net/ExchangeOptions.cs diff --git a/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs similarity index 100% rename from ExtensionMethods.cs rename to CryptoExchange.Net/ExtensionMethods.cs diff --git a/Interfaces/IExchangeClient.cs b/CryptoExchange.Net/Interfaces/IExchangeClient.cs similarity index 100% rename from Interfaces/IExchangeClient.cs rename to CryptoExchange.Net/Interfaces/IExchangeClient.cs diff --git a/Interfaces/IRequest.cs b/CryptoExchange.Net/Interfaces/IRequest.cs similarity index 100% rename from Interfaces/IRequest.cs rename to CryptoExchange.Net/Interfaces/IRequest.cs diff --git a/Interfaces/IRequestFactory.cs b/CryptoExchange.Net/Interfaces/IRequestFactory.cs similarity index 100% rename from Interfaces/IRequestFactory.cs rename to CryptoExchange.Net/Interfaces/IRequestFactory.cs diff --git a/Interfaces/IResponse.cs b/CryptoExchange.Net/Interfaces/IResponse.cs similarity index 100% rename from Interfaces/IResponse.cs rename to CryptoExchange.Net/Interfaces/IResponse.cs diff --git a/Logging/DebugTextWriter.cs b/CryptoExchange.Net/Logging/DebugTextWriter.cs similarity index 100% rename from Logging/DebugTextWriter.cs rename to CryptoExchange.Net/Logging/DebugTextWriter.cs diff --git a/Logging/Log.cs b/CryptoExchange.Net/Logging/Log.cs similarity index 100% rename from Logging/Log.cs rename to CryptoExchange.Net/Logging/Log.cs diff --git a/RateLimiter/IRateLimiter.cs b/CryptoExchange.Net/RateLimiter/IRateLimiter.cs similarity index 100% rename from RateLimiter/IRateLimiter.cs rename to CryptoExchange.Net/RateLimiter/IRateLimiter.cs diff --git a/RateLimiter/RateLimitObject.cs b/CryptoExchange.Net/RateLimiter/RateLimitObject.cs similarity index 100% rename from RateLimiter/RateLimitObject.cs rename to CryptoExchange.Net/RateLimiter/RateLimitObject.cs diff --git a/RateLimiter/RateLimiterPerEndpoint.cs b/CryptoExchange.Net/RateLimiter/RateLimiterPerEndpoint.cs similarity index 100% rename from RateLimiter/RateLimiterPerEndpoint.cs rename to CryptoExchange.Net/RateLimiter/RateLimiterPerEndpoint.cs diff --git a/RateLimiter/RateLimiterTotal.cs b/CryptoExchange.Net/RateLimiter/RateLimiterTotal.cs similarity index 100% rename from RateLimiter/RateLimiterTotal.cs rename to CryptoExchange.Net/RateLimiter/RateLimiterTotal.cs diff --git a/Requests/Request.cs b/CryptoExchange.Net/Requests/Request.cs similarity index 100% rename from Requests/Request.cs rename to CryptoExchange.Net/Requests/Request.cs diff --git a/Requests/RequestFactory.cs b/CryptoExchange.Net/Requests/RequestFactory.cs similarity index 100% rename from Requests/RequestFactory.cs rename to CryptoExchange.Net/Requests/RequestFactory.cs diff --git a/Requests/Response.cs b/CryptoExchange.Net/Requests/Response.cs similarity index 100% rename from Requests/Response.cs rename to CryptoExchange.Net/Requests/Response.cs