Browse Source

Change From Either Types to Exceptions (#4)

master
Taylor Bockman 6 years ago committed by GitHub
parent
commit
de32c1049c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 85
      README.md
  2. 1
      build.gradle
  3. 2
      src/main/java/com/sigmaflare/binancej/BaseBinanceApi.java
  4. 64
      src/main/java/com/sigmaflare/binancej/GeneralUtilities.java
  5. 61
      src/main/java/com/sigmaflare/binancej/HttpRequests.java
  6. 178
      src/main/java/com/sigmaflare/binancej/MarketData.java
  7. 4
      src/main/java/com/sigmaflare/binancej/entities/ServiceError.java
  8. 4
      src/main/java/com/sigmaflare/binancej/exceptions/BinanceServiceUnreachableException.java
  9. 11
      src/main/java/com/sigmaflare/binancej/exceptions/InternalServiceErrorException.java
  10. 10
      src/main/java/com/sigmaflare/binancej/exceptions/IpBannedException.java
  11. 11
      src/main/java/com/sigmaflare/binancej/exceptions/MalformedRequestException.java
  12. 10
      src/main/java/com/sigmaflare/binancej/exceptions/RateLimitExceededException.java
  13. 7
      src/main/java/com/sigmaflare/binancej/exceptions/UnexpectedErrorException.java
  14. 145
      src/test/java/com/sigmaflare/binancej/CandlestickMethodTests.java
  15. 32
      src/test/java/com/sigmaflare/binancej/ExchangeInfoMethodTests.java
  16. 42
      src/test/java/com/sigmaflare/binancej/HttpErrorThrowerTests.java
  17. 61
      src/test/java/com/sigmaflare/binancej/OrderBookDepthTests.java
  18. 32
      src/test/java/com/sigmaflare/binancej/PingMethodTests.java
  19. 59
      src/test/java/com/sigmaflare/binancej/TickerPriceTests.java
  20. 31
      src/test/java/com/sigmaflare/binancej/TimeMethodTests.java

85
README.md

@ -11,9 +11,9 @@ BinanceJ is released under the MIT license.
## Rate Limiting ## Rate Limiting
BinanceJ does not perform rate limiting, so you are responsible for limiting your requests. Pay attention to the errors BinanceJ will throw a `RateLimitExceededException` when a HTTP 429 error code comes back. This is an important
you receive from Binance and check to see if they are a 429. If they are, you need to back off or face a temporary exception to catch and handle because if you do not back off they will issue a temporary ban. If you don't listen,
ban. you'll be receiving `IpBannedException`s.
## API Coverage ## API Coverage
@ -26,110 +26,77 @@ The following endpoints are currently covered:
5. `GET /api/v1/klines` 5. `GET /api/v1/klines`
6. `GET /api/v3/ticker/price` 6. `GET /api/v3/ticker/price`
More will be added in future PRs as they become necessary to me or the people using the library. More will be added in future PRs as they become necessary to me or the people using the library.
## Return Types ## HTTP Exceptions
All functions return an `Either` type. For those of you not in the know - an Either type represents a disjoint union. ### Checked Exceptions
Typically this means a "success" and "failure" case that must be handled uniquely. They work well with stream
processing and are a very natural way to delineate success and failure cleanly. Additionally it eliminates the pattern
of throwing exceptions on errors, and allows exceptions to be reserved for truly exceptional behavior as intended.
By convention I adapted the Haskell Either type style to this code. What this means is that the Either type's Right Any function in the API can throw a handful of exceptions related to HTTP:
value is the correct one (mnemonic: "right" as in correct) and the Left value is the `ServiceError`.
### How do I extract the raw value from the Either type? 1. `IpBannedException`: Your IP has been banned after ignoring rate limit exceeded messages
2. `RateLimitExceededException`: Your IP has exceeded the rate limit and needs to slow down
3. `InternalServiceErrorException`: An error occurred on Binance's server side (see note below)
4. `MalformedRequestException`: A malformed request was sent, the error exists on the sender's side (check error code and message)
There are a few ways to do this using Ambivalent, but the most prevalent way in BinanceJ's tests is: **Note**: A 504 error in general should not be treated as an error (though it is "exceptional" behavior). In the case of `InternalServiceErrorException`s it is critical to check the `code` member of the exception.
```java The "more general" HTTP exceptions (`MalformedRequestException` and `InternalServiceErrorException`) have a code
Either<TypeA, TypeB> val = clazz.getThing(); component so you can check the specifics against the [Binance Error Documentation](https://github.com/binance-exchange/binance-official-api-docs/blob/master/errors.md).
// Good path case
if(val.isRight()) {
TypeB thing = val.right().join(Function.identity(), Function.identity());
}
``` ## Standard Exceptions
Alternatively you are welcome to use `Helpers.extractEitherValueSafely` method to make your code cleaner. ### Runtime Exceptions
In the event an unknown error occurs, the code will throw an `UnexpectedErrorException`. These are generally complete showstoppers.
## Examples ## Examples
### Server alive check with ping ### Server alive check with ping
If the ping method does not throw an exception, the ping was successful.
```java ```java
GeneralUtilities generalUtilities = GeneralUtilities.builder().apiKey("KEY").secretKey("KEY").build(); GeneralUtilities generalUtilities = GeneralUtilities.builder().apiKey("KEY").secretKey("KEY").build();
Either<ServiceError, Ping> res = generalUtilities.ping(); generalUtilities.ping();
if(res.isRight()) {
// Successful ping
}
``` ```
### Getting current server time ### Getting current server time
```java ```java
GeneralUtilities generalUtilities = GeneralUtilities.builder().apiKey("KEY").secretKey("KEY").build(); GeneralUtilities generalUtilities = GeneralUtilities.builder().apiKey("KEY").secretKey("KEY").build();
Either<ServiceError, ServerTime> res = generalUtilities.getServerTime(); ServerTime = generalUtilities.getServerTime();
if(res.isRight()) {
ServerTime response = Helpers.extractEitherValueSafely(res.right());
//...
}
``` ```
### Getting Exchange Information ### Getting Exchange Information
```java ```java
GeneralUtilities generalUtilities = GeneralUtilities.builder().apiKey("KEY").secretKey("KEY").build(); GeneralUtilities generalUtilities = GeneralUtilities.builder().apiKey("KEY").secretKey("KEY").build();
Either<ServiceError, ExchangeInfo> res = generalUtilities.getExchangeInfo(); ExchangeInfo res = generalUtilities.getExchangeInfo();
if(res.isRight()) {
ExchangeInfo response = Helpers.extractEitherValueSafely(res.right());
//...
}
``` ```
### Getting Candlestick data ### Getting Candlestick data
```java ```java
MarketData marketData = MarketData.builder().apiKey("KEY").secretKey("KEY").build(); MarketData marketData = MarketData.builder().apiKey("KEY").secretKey("KEY").build();
Either<ServiceError, List<Candlestick>> res = marketData.getCandlestickData("ETHBTC", Interval.ONE_MINUTE); List<Candlestick> res = marketData.getCandlestickData("ETHBTC", Interval.ONE_MINUTE);
if(res.isRight()) {
List<Candlestick> data = Helpers.extractEitherValueSafely(res.right());
//...
}
``` ```
### Getting market depth ### Getting market depth
```java ```java
MarketData marketData = MarketData.builder().apiKey("KEY").secretKey("KEY").build(); MarketData marketData = MarketData.builder().apiKey("KEY").secretKey("KEY").build();
Either<ServiceError, OrderBookDepth> res = marketData.getOrderBookDepth("ETHBTC", 1000); OrderBookDepth res = marketData.getOrderBookDepth("ETHBTC", 1000);
if(res.isRight()) {
OrderBookDepth orderBookDepth = Helpers.extractEitherValueSafely(res.right());
//...
}
``` ```
### Getting ticker price for an instrument ### Getting ticker price for an instrument
```java ```java
MarketData marketData = MarketData.builder().apiKey("KEY").secretKey("KEY").build(); MarketData marketData = MarketData.builder().apiKey("KEY").secretKey("KEY").build();
Either<ServiceError, TickerPrice> res = marketData.getTickerPriceForSymbol("ETHBTC"); TickerPrice res = marketData.getTickerPriceForSymbol("ETHBTC");
if(res.isRight()) {
TickerPrice tickerPrice = Helpers.extractEitherValueSafely(res.right());
//...
}
``` ```
## Contributing ## Contributing
Head over to our [CONTRIBUTING.md](CONTRIBUTING.md) to get started. All features are welcome as long as they are Head over to our [CONTRIBUTING.md](CONTRIBUTING.md) to get started. All features are welcome as long as they are
in scope of the API and following the contributing guide. in scope of the API and following the contributing guide.

1
build.gradle

@ -10,7 +10,6 @@ sourceCompatibility = 1.8
dependencies { dependencies {
compile group: 'org.projectlombok', name: 'lombok', version: '1.16.20' compile group: 'org.projectlombok', name: 'lombok', version: '1.16.20'
compile group: 'com.codepoetics', name: 'ambivalence', version: '0.2'
compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.9.5' compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.9.5'
compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.5' compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.5'

2
src/main/java/com/sigmaflare/binancej/BaseBinanceApi.java

@ -12,7 +12,7 @@ public abstract class BaseBinanceApi {
protected final String secretKey; protected final String secretKey;
protected final CloseableHttpClient closeableHttpClient; protected final CloseableHttpClient closeableHttpClient;
protected static final ObjectMapper mapper = Helpers.objectMapperBuilder(); protected static final ObjectMapper mapper = HttpRequests.objectMapperBuilder();
public BaseBinanceApi(String apiKey, String secretKey) { public BaseBinanceApi(String apiKey, String secretKey) {
this.apiKey = apiKey; this.apiKey = apiKey;

64
src/main/java/com/sigmaflare/binancej/GeneralUtilities.java

@ -1,11 +1,12 @@
package com.sigmaflare.binancej; package com.sigmaflare.binancej;
import com.codepoetics.ambivalence.Either;
import com.sigmaflare.binancej.entities.ExchangeInfo; import com.sigmaflare.binancej.entities.ExchangeInfo;
import com.sigmaflare.binancej.entities.Ping; import com.sigmaflare.binancej.entities.Ping;
import com.sigmaflare.binancej.entities.ServiceError; import com.sigmaflare.binancej.entities.ServiceError;
import com.sigmaflare.binancej.entities.Time; import com.sigmaflare.binancej.entities.Time;
import com.sigmaflare.binancej.exceptions.BinanceServiceException;
import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException; import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException;
import com.sigmaflare.binancej.exceptions.UnexpectedErrorException;
import lombok.Builder; import lombok.Builder;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity; import org.apache.http.HttpEntity;
@ -19,7 +20,7 @@ import java.io.IOException;
import static com.sigmaflare.binancej.Constant.NO_RESPONSE_TEXT; import static com.sigmaflare.binancej.Constant.NO_RESPONSE_TEXT;
import static com.sigmaflare.binancej.Constant.NO_RESPONSE_TEXT_FORMATTED; import static com.sigmaflare.binancej.Constant.NO_RESPONSE_TEXT_FORMATTED;
import static com.sigmaflare.binancej.Helpers.buildGetRequestFromEndpoint; import static com.sigmaflare.binancej.HttpRequests.buildGetRequestFromEndpoint;
/** /**
* GeneralUtilities is a container class for methods that interact with the infrastructure * GeneralUtilities is a container class for methods that interact with the infrastructure
@ -44,9 +45,15 @@ public class GeneralUtilities extends BaseBinanceApi {
/** /**
* Hits the ping endpoint to check if the service is alive. * Hits the ping endpoint to check if the service is alive.
* *
* @return empty Ping object if it returned 200, otherwise ServiceError * @return Empty Ping object
* @throws com.sigmaflare.binancej.exceptions.IpBannedException If the IP is banned
* @throws com.sigmaflare.binancej.exceptions.RateLimitExceededException If the rate limit is exceeded
* @throws com.sigmaflare.binancej.exceptions.MalformedRequestException If the user's request is malformed
* @throws com.sigmaflare.binancej.exceptions.InternalServiceErrorException If the error occurs on Binance's side
* @throws com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException If the service is unreachable
* @throws UnexpectedErrorException If an unexpected exception occurs
*/ */
public Either<ServiceError, Ping> ping() throws BinanceServiceUnreachableException { public Ping ping() throws BinanceServiceException {
final HttpGet request = buildGetRequestFromEndpoint(PING_URL, apiKey); final HttpGet request = buildGetRequestFromEndpoint(PING_URL, apiKey);
try { try {
@ -63,24 +70,30 @@ public class GeneralUtilities extends BaseBinanceApi {
String response = EntityUtils.toString(httpEntity); String response = EntityUtils.toString(httpEntity);
if (!Helpers.statusCodeIsOk(sl.getStatusCode())) { if (!HttpRequests.statusCodeIsOk(sl.getStatusCode())) {
return Either.ofLeft(mapper.readValue(response, ServiceError.class)); ServiceError error = mapper.readValue(response, ServiceError.class);
throw HttpRequests.buildHttpException(sl.getStatusCode(), error);
} }
return Either.ofRight(new Ping()); return new Ping();
} }
} catch (IOException e) { } catch (IOException e) {
throw new BinanceServiceUnreachableException(e.getMessage(), e.getCause()); throw new UnexpectedErrorException(e.getMessage(), e.getCause());
} }
} }
/** /**
* Gets the current server time on Binance's servers * Gets the current server time on Binance's servers
* *
* @return A Time object if successful, otherwise an ServiceError object * @return A Time object populated with the current server time
* @throws BinanceServiceUnreachableException Throws when the request fails * @throws com.sigmaflare.binancej.exceptions.IpBannedException If the IP is banned
* @throws com.sigmaflare.binancej.exceptions.RateLimitExceededException If the rate limit is exceeded
* @throws com.sigmaflare.binancej.exceptions.MalformedRequestException If the user's request is malformed
* @throws com.sigmaflare.binancej.exceptions.InternalServiceErrorException If the error occurs on Binance's side
* @throws com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException If the service is unreachable
* @throws UnexpectedErrorException If an unexpected exception occurs
*/ */
public Either<ServiceError, Time> getServerTime() throws BinanceServiceUnreachableException { public Time getServerTime() throws BinanceServiceException {
final HttpGet request = buildGetRequestFromEndpoint(TIME_URL, apiKey); final HttpGet request = buildGetRequestFromEndpoint(TIME_URL, apiKey);
try { try {
@ -97,24 +110,30 @@ public class GeneralUtilities extends BaseBinanceApi {
String response = EntityUtils.toString(httpEntity); String response = EntityUtils.toString(httpEntity);
if (!Helpers.statusCodeIsOk(sl.getStatusCode())) { if (!HttpRequests.statusCodeIsOk(sl.getStatusCode())) {
return Either.ofLeft(mapper.readValue(response, ServiceError.class)); ServiceError error = mapper.readValue(response, ServiceError.class);
throw HttpRequests.buildHttpException(sl.getStatusCode(), error);
} }
return Either.ofRight(mapper.readValue(response, Time.class)); return mapper.readValue(response, Time.class);
} }
} catch (IOException e) { } catch (IOException e) {
throw new BinanceServiceUnreachableException(e.getMessage(), e.getCause()); throw new UnexpectedErrorException(e.getMessage(), e.getCause());
} }
} }
/** /**
* Retrieves exchange information * Retrieves exchange information
* *
* @return An ExchangeInfo when successful, otherwise an ServiceError * @return An ExchangeInfo populated with exchange information
* @throws BinanceServiceUnreachableException If the service cannot be reached * @throws com.sigmaflare.binancej.exceptions.IpBannedException If the IP is banned
* @throws com.sigmaflare.binancej.exceptions.RateLimitExceededException If the rate limit is exceeded
* @throws com.sigmaflare.binancej.exceptions.MalformedRequestException If the user's request is malformed
* @throws com.sigmaflare.binancej.exceptions.InternalServiceErrorException If the error occurs on Binance's side
* @throws com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException If the service is unreachable
* @throws UnexpectedErrorException If an unexpected exception occurs
*/ */
public Either<ServiceError, ExchangeInfo> getExchangeInfo() throws BinanceServiceUnreachableException { public ExchangeInfo getExchangeInfo() throws BinanceServiceException {
final HttpGet request = buildGetRequestFromEndpoint(EXCHANGE_INFO_URL, apiKey); final HttpGet request = buildGetRequestFromEndpoint(EXCHANGE_INFO_URL, apiKey);
try { try {
@ -131,14 +150,15 @@ public class GeneralUtilities extends BaseBinanceApi {
String response = EntityUtils.toString(httpEntity); String response = EntityUtils.toString(httpEntity);
if (!Helpers.statusCodeIsOk(sl.getStatusCode())) { if (!HttpRequests.statusCodeIsOk(sl.getStatusCode())) {
return Either.ofLeft(mapper.readValue(response, ServiceError.class)); ServiceError error = mapper.readValue(response, ServiceError.class);
throw HttpRequests.buildHttpException(sl.getStatusCode(), error);
} }
return Either.ofRight(mapper.readValue(response, ExchangeInfo.class)); return mapper.readValue(response, ExchangeInfo.class);
} }
} catch (IOException e) { } catch (IOException e) {
throw new BinanceServiceUnreachableException(e.getMessage(), e.getCause()); throw new UnexpectedErrorException(e.getMessage(), e.getCause());
} }
} }

61
src/main/java/com/sigmaflare/binancej/Helpers.java → src/main/java/com/sigmaflare/binancej/HttpRequests.java

@ -1,18 +1,21 @@
package com.sigmaflare.binancej; package com.sigmaflare.binancej;
import com.codepoetics.ambivalence.LeftProjection;
import com.codepoetics.ambivalence.RightProjection;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.sigmaflare.binancej.entities.ServiceError;
import com.sigmaflare.binancej.exceptions.BinanceServiceException;
import com.sigmaflare.binancej.exceptions.InternalServiceErrorException;
import com.sigmaflare.binancej.exceptions.IpBannedException;
import com.sigmaflare.binancej.exceptions.MalformedRequestException;
import com.sigmaflare.binancej.exceptions.RateLimitExceededException;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
import java.util.function.Function;
import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT; import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT;
public final class Helpers { public final class HttpRequests {
private Helpers() { private HttpRequests() {
} }
/** /**
@ -28,24 +31,26 @@ public final class Helpers {
/** /**
* Utilizes getBuilder to build a fully functional HttpGet request * Utilizes getBuilder to build a fully functional HttpGet request
*
* @param endpoint The endpoint AFTER the base endpoint (e.g. /api/v1/xyz) * @param endpoint The endpoint AFTER the base endpoint (e.g. /api/v1/xyz)
* @param apiKey The API key to use * @param apiKey The API key to use
* @return A configured HttpGet object * @return A configured HttpGet object
*/ */
public static HttpGet buildGetRequestFromEndpoint(String endpoint, String apiKey) { public static HttpGet buildGetRequestFromEndpoint(String endpoint, String apiKey) {
final String url = String.format("%s%s", BASE_ENDPOINT, endpoint); final String url = String.format("%s%s", BASE_ENDPOINT, endpoint);
return Helpers.getBuilder(url, apiKey); return HttpRequests.getBuilder(url, apiKey);
} }
/** /**
* Utilizes postBuilder to build a fully functional HttpPost request * Utilizes postBuilder to build a fully functional HttpPost request
*
* @param endpoint The endpoint AFTER the base endpoint (e.g. /api/v1/xyz) * @param endpoint The endpoint AFTER the base endpoint (e.g. /api/v1/xyz)
* @param apiKey The API key to use * @param apiKey The API key to use
* @return A configured HttpPost object * @return A configured HttpPost object
*/ */
public static HttpPost buildPostRequestFromEndpoint(String endpoint, String apiKey) { public static HttpPost buildPostRequestFromEndpoint(String endpoint, String apiKey) {
final String url = String.format("%s%s", BASE_ENDPOINT, endpoint); final String url = String.format("%s%s", BASE_ENDPOINT, endpoint);
return Helpers.postBuilder(url, apiKey); return HttpRequests.postBuilder(url, apiKey);
} }
/** /**
@ -84,28 +89,24 @@ public final class Helpers {
} }
/** /**
* Safely extracts the value from a LeftProjection of an Either * A simple function to consolidate the creation of common HTTP exceptions
*
* @param val The LeftProjection to perform the extraction on
* @param <L> The Left type
* @param <R> The Right type
* @return The unwrapped L type object
*/ */
@SuppressWarnings("unchecked") public static BinanceServiceException buildHttpException(int statusCode, ServiceError error) {
public static <L, R> L extractEitherValueSafely(LeftProjection<L, R> val) { if(statusCode < 400) {
return (L) val.join(Function.identity(), Function.identity()); throw new IllegalArgumentException(
} String.format("Status codes below 400 (statusCode = %d) do not have an exception", statusCode));
}
/** if (statusCode == 418) {
* Safely extracts the value from a RightProjection of an Either return new IpBannedException(error.getMessage());
* } else if (statusCode == 429) {
* @param val The RightProjection to perform the extraction on return new RateLimitExceededException(error.getMessage());
* @param <L> The Left type } else if (statusCode == 504) {
* @param <R> The Right type return new InternalServiceErrorException(error.getCode(), error.getMessage());
* @return The unwrapped R type object } else if (statusCode < 500) {
*/ return new MalformedRequestException(error.getCode(), error.getMessage());
@SuppressWarnings("unchecked") } else {
public static <L, R> R extractEitherValueSafely(RightProjection<L, R> val) { return new InternalServiceErrorException(error.getCode(), error.getMessage());
return (R) val.join(Function.identity(), Function.identity()); }
} }
} }

178
src/main/java/com/sigmaflare/binancej/MarketData.java

@ -1,13 +1,14 @@
package com.sigmaflare.binancej; package com.sigmaflare.binancej;
import com.codepoetics.ambivalence.Either;
import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JavaType;
import com.sigmaflare.binancej.entities.Candlestick; import com.sigmaflare.binancej.entities.Candlestick;
import com.sigmaflare.binancej.entities.Interval; import com.sigmaflare.binancej.entities.Interval;
import com.sigmaflare.binancej.entities.OrderBookDepth; import com.sigmaflare.binancej.entities.OrderBookDepth;
import com.sigmaflare.binancej.entities.ServiceError; import com.sigmaflare.binancej.entities.ServiceError;
import com.sigmaflare.binancej.entities.TickerPrice; import com.sigmaflare.binancej.entities.TickerPrice;
import com.sigmaflare.binancej.exceptions.BinanceServiceException;
import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException; import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException;
import com.sigmaflare.binancej.exceptions.UnexpectedErrorException;
import lombok.Builder; import lombok.Builder;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity; import org.apache.http.HttpEntity;
@ -18,12 +19,14 @@ import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils; import org.apache.http.util.EntityUtils;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import static com.sigmaflare.binancej.Constant.NO_RESPONSE_TEXT; import static com.sigmaflare.binancej.Constant.NO_RESPONSE_TEXT;
import static com.sigmaflare.binancej.Constant.NO_RESPONSE_TEXT_FORMATTED; import static com.sigmaflare.binancej.Constant.NO_RESPONSE_TEXT_FORMATTED;
import static com.sigmaflare.binancej.Constant.SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED; import static com.sigmaflare.binancej.Constant.SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED;
import static com.sigmaflare.binancej.Helpers.buildGetRequestFromEndpoint; import static com.sigmaflare.binancej.HttpRequests.buildGetRequestFromEndpoint;
@Slf4j @Slf4j
public class MarketData extends BaseBinanceApi { public class MarketData extends BaseBinanceApi {
@ -31,6 +34,9 @@ public class MarketData extends BaseBinanceApi {
private static final String CANDLESTICK_URL = "/api/v1/klines"; private static final String CANDLESTICK_URL = "/api/v1/klines";
private static final String TICKER_PRICE_URL = "/api/v3/ticker/price"; private static final String TICKER_PRICE_URL = "/api/v3/ticker/price";
private static final ArrayList<Integer> LIMITS = new ArrayList<>(Arrays.asList(5, 10, 20, 50, 100, 500, 1000));
private static final String LIMIT_EXCEPTION_MESSAGE = "Limit must be in [5, 10, 20, 50, 100, 500, 1000]";
@Builder @Builder
MarketData(String apiKey, String secretKey) { MarketData(String apiKey, String secretKey) {
super(apiKey, secretKey); super(apiKey, secretKey);
@ -40,15 +46,24 @@ public class MarketData extends BaseBinanceApi {
super(apiKey, secretKey, closeableHttpClient); super(apiKey, secretKey, closeableHttpClient);
} }
private void checkCandlestickLimit(int limit) {
if(limit < 0 || limit > 500) {
throw new IllegalArgumentException("Limit must be greater than 0 and less than or equal to 500");
}
}
/** /**
* Overloaded version of getOrderBookDepth that uses the default limit of 100 * Overloaded version of getOrderBookDepth that uses the default limit of 100
* *
* @param symbol The symbol * @param symbol The symbol
* @return A populated OrderBookDepth if successful, otherwise an ServiceError * @return A populated OrderBookDepth if successful, otherwise an ServiceError
* @throws BinanceServiceUnreachableException In the case the service is unreachable * @throws com.sigmaflare.binancej.exceptions.IpBannedException If the IP is banned
* @throws com.sigmaflare.binancej.exceptions.RateLimitExceededException If the rate limit is exceeded
* @throws com.sigmaflare.binancej.exceptions.MalformedRequestException If the user's request is malformed
* @throws com.sigmaflare.binancej.exceptions.InternalServiceErrorException If the error occurs on Binance's side
* @throws com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException If the service is unreachable
*/ */
public Either<ServiceError, OrderBookDepth> getOrderBookDepth(String symbol) public OrderBookDepth getOrderBookDepth(String symbol) throws BinanceServiceException {
throws BinanceServiceUnreachableException {
return getOrderBookDepth(symbol, 100); return getOrderBookDepth(symbol, 100);
} }
@ -57,11 +72,19 @@ public class MarketData extends BaseBinanceApi {
* *
* @param symbol The symbol * @param symbol The symbol
* @param limit The record limit (default: 100, maximum 1000) * @param limit The record limit (default: 100, maximum 1000)
* @return A populated OrderBookDepth if successful, otherwise an ServiceError * @return A populated OrderBookDepth
* @throws BinanceServiceUnreachableException In the case the service is unreachable * @throws com.sigmaflare.binancej.exceptions.IpBannedException If the IP is banned
* @throws com.sigmaflare.binancej.exceptions.RateLimitExceededException If the rate limit is exceeded
* @throws com.sigmaflare.binancej.exceptions.MalformedRequestException If the user's request is malformed
* @throws com.sigmaflare.binancej.exceptions.InternalServiceErrorException If the error occurs on Binance's side
* @throws com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException If the service is unreachable
* @throws UnexpectedErrorException If an unexpected exception occurs
*/ */
public Either<ServiceError, OrderBookDepth> getOrderBookDepth(String symbol, int limit) public OrderBookDepth getOrderBookDepth(String symbol, int limit) throws BinanceServiceException {
throws BinanceServiceUnreachableException { if(!LIMITS.contains(limit)) {
throw new IllegalArgumentException(LIMIT_EXCEPTION_MESSAGE);
}
String urlWithParams = String.format("%s?symbol=%s&limit=%d", ORDER_BOOK_URL, symbol, limit); String urlWithParams = String.format("%s?symbol=%s&limit=%d", ORDER_BOOK_URL, symbol, limit);
final HttpGet request = buildGetRequestFromEndpoint(urlWithParams, apiKey); final HttpGet request = buildGetRequestFromEndpoint(urlWithParams, apiKey);
@ -79,14 +102,15 @@ public class MarketData extends BaseBinanceApi {
String response = EntityUtils.toString(httpEntity); String response = EntityUtils.toString(httpEntity);
if (!Helpers.statusCodeIsOk(sl.getStatusCode())) { if (!HttpRequests.statusCodeIsOk(sl.getStatusCode())) {
return Either.ofLeft(mapper.readValue(response, ServiceError.class)); ServiceError error = mapper.readValue(response, ServiceError.class);
throw HttpRequests.buildHttpException(sl.getStatusCode(), error);
} }
return Either.ofRight(mapper.readValue(response, OrderBookDepth.class)); return mapper.readValue(response, OrderBookDepth.class);
} }
} catch (IOException e) { } catch (IOException e) {
throw new BinanceServiceUnreachableException(e.getMessage(), e.getCause()); throw new UnexpectedErrorException(e.getMessage(), e.getCause());
} }
} }
@ -95,12 +119,16 @@ public class MarketData extends BaseBinanceApi {
* *
* @param symbol The symbol * @param symbol The symbol
* @param interval The interval * @param interval The interval
* @return A list of candlesticks if successful, otherwise an ServiceError * @return A list of candlesticks
* @throws BinanceServiceUnreachableException If the service is unreachable * @throws com.sigmaflare.binancej.exceptions.IpBannedException If the IP is banned
* @throws com.sigmaflare.binancej.exceptions.RateLimitExceededException If the rate limit is exceeded
* @throws com.sigmaflare.binancej.exceptions.MalformedRequestException If the user's request is malformed
* @throws com.sigmaflare.binancej.exceptions.InternalServiceErrorException If the error occurs on Binance's side
* @throws com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException If the service is unreachable
* @throws IllegalArgumentException If the required arguments symbol and interval are not supplied * @throws IllegalArgumentException If the required arguments symbol and interval are not supplied
*/ */
public Either<ServiceError, List<Candlestick>> getCandleStickData(String symbol, Interval interval) public List<Candlestick> getCandleStickData(String symbol, Interval interval)
throws BinanceServiceUnreachableException { throws BinanceServiceException {
if (symbol == null || interval == null) { if (symbol == null || interval == null) {
throw new IllegalArgumentException(SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED); throw new IllegalArgumentException(SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED);
@ -122,11 +150,17 @@ public class MarketData extends BaseBinanceApi {
* @param interval The interval * @param interval The interval
* @param limit The output limit * @param limit The output limit
* @return A list of candlesticks if successful, otherwise an ServiceError * @return A list of candlesticks if successful, otherwise an ServiceError
* @throws BinanceServiceUnreachableException If the service is unreachable * @throws com.sigmaflare.binancej.exceptions.IpBannedException If the IP is banned
* @throws com.sigmaflare.binancej.exceptions.RateLimitExceededException If the rate limit is exceeded
* @throws com.sigmaflare.binancej.exceptions.MalformedRequestException If the user's request is malformed
* @throws com.sigmaflare.binancej.exceptions.InternalServiceErrorException If the error occurs on Binance's side
* @throws com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException If the service is unreachable
* @throws IllegalArgumentException If the required arguments symbol and interval are not supplied * @throws IllegalArgumentException If the required arguments symbol and interval are not supplied
*/ */
public Either<ServiceError, List<Candlestick>> getCandleStickData(String symbol, Interval interval, int limit) public List<Candlestick> getCandleStickData(String symbol, Interval interval, int limit)
throws BinanceServiceUnreachableException { throws BinanceServiceException {
checkCandlestickLimit(limit);
if (symbol == null || interval == null) { if (symbol == null || interval == null) {
throw new IllegalArgumentException(SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED); throw new IllegalArgumentException(SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED);
@ -149,13 +183,17 @@ public class MarketData extends BaseBinanceApi {
* @param interval The interval * @param interval The interval
* @param time The start/end time * @param time The start/end time
* @param isStartTime Indicates whether the time is a start or end time * @param isStartTime Indicates whether the time is a start or end time
* @return A list of candlesticks if successful, otherwise an ServiceError * @return A list of candlesticks
* @throws BinanceServiceUnreachableException If the service is unreachable * @throws com.sigmaflare.binancej.exceptions.IpBannedException If the IP is banned
* @throws com.sigmaflare.binancej.exceptions.RateLimitExceededException If the rate limit is exceeded
* @throws com.sigmaflare.binancej.exceptions.MalformedRequestException If the user's request is malformed
* @throws com.sigmaflare.binancej.exceptions.InternalServiceErrorException If the error occurs on Binance's side
* @throws com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException If the service is unreachable
* @throws IllegalArgumentException If the required arguments symbol and interval are not supplied * @throws IllegalArgumentException If the required arguments symbol and interval are not supplied
*/ */
public Either<ServiceError, List<Candlestick>> public List<Candlestick>
getCandleStickData(String symbol, Interval interval, long time, boolean isStartTime) getCandleStickData(String symbol, Interval interval, long time, boolean isStartTime)
throws BinanceServiceUnreachableException { throws BinanceServiceException {
if (symbol == null || interval == null) { if (symbol == null || interval == null) {
throw new IllegalArgumentException(SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED); throw new IllegalArgumentException(SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED);
@ -190,13 +228,19 @@ public class MarketData extends BaseBinanceApi {
* @param limit The output limit * @param limit The output limit
* @param time The timeframe * @param time The timeframe
* @param isStartTime indicates whether time is a startTime (true) or endTime (false) * @param isStartTime indicates whether time is a startTime (true) or endTime (false)
* @return A list of candlesticks if successful, otherwise an ServiceError * @return A list of candlesticks
* @throws BinanceServiceUnreachableException If the service is unreachable * @throws com.sigmaflare.binancej.exceptions.IpBannedException If the IP is banned
* @throws com.sigmaflare.binancej.exceptions.RateLimitExceededException If the rate limit is exceeded
* @throws com.sigmaflare.binancej.exceptions.MalformedRequestException If the user's request is malformed
* @throws com.sigmaflare.binancej.exceptions.InternalServiceErrorException If the error occurs on Binance's side
* @throws com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException If the service is unreachable
* @throws IllegalArgumentException If the required arguments symbol and interval are not supplied * @throws IllegalArgumentException If the required arguments symbol and interval are not supplied
*/ */
public Either<ServiceError, List<Candlestick>> public List<Candlestick>
getCandleStickData(String symbol, Interval interval, int limit, long time, boolean isStartTime) getCandleStickData(String symbol, Interval interval, int limit, long time, boolean isStartTime)
throws BinanceServiceUnreachableException { throws BinanceServiceException {
checkCandlestickLimit(limit);
if (symbol == null || interval == null) { if (symbol == null || interval == null) {
throw new IllegalArgumentException(SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED); throw new IllegalArgumentException(SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED);
@ -233,13 +277,19 @@ public class MarketData extends BaseBinanceApi {
* @param limit The output limit * @param limit The output limit
* @param startTime The start timeframe * @param startTime The start timeframe
* @param endTime The end timeframe * @param endTime The end timeframe
* @return A list of candlesticks if successful, otherwise an ServiceError * @return A list of candlesticks
* @throws BinanceServiceUnreachableException If the service is unreachable * @throws com.sigmaflare.binancej.exceptions.IpBannedException If the IP is banned
* @throws com.sigmaflare.binancej.exceptions.RateLimitExceededException If the rate limit is exceeded
* @throws com.sigmaflare.binancej.exceptions.MalformedRequestException If the user's request is malformed
* @throws com.sigmaflare.binancej.exceptions.InternalServiceErrorException If the error occurs on Binance's side
* @throws com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException If the service is unreachable
* @throws IllegalArgumentException If the required arguments symbol and interval are not supplied * @throws IllegalArgumentException If the required arguments symbol and interval are not supplied
*/ */
public Either<ServiceError, List<Candlestick>> public List<Candlestick>
getCandleStickData(String symbol, Interval interval, int limit, long startTime, long endTime) getCandleStickData(String symbol, Interval interval, int limit, long startTime, long endTime)
throws BinanceServiceUnreachableException { throws BinanceServiceException {
checkCandlestickLimit(limit);
if (symbol == null || interval == null) { if (symbol == null || interval == null) {
throw new IllegalArgumentException(SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED); throw new IllegalArgumentException(SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED);
@ -263,11 +313,16 @@ public class MarketData extends BaseBinanceApi {
* Retrieves the current ticker price for the specified symbol * Retrieves the current ticker price for the specified symbol
* *
* @param symbol The symbol * @param symbol The symbol
* @return A TickerPrice if successful, otherwise an ServiceError * @return A populated list of TickerPrice objects
* @throws BinanceServiceUnreachableException If the service is unreachable * @throws com.sigmaflare.binancej.exceptions.IpBannedException If the IP is banned
* @throws com.sigmaflare.binancej.exceptions.RateLimitExceededException If the rate limit is exceeded
* @throws com.sigmaflare.binancej.exceptions.MalformedRequestException If the user's request is malformed
* @throws com.sigmaflare.binancej.exceptions.InternalServiceErrorException If the error occurs on Binance's side
* @throws com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException If the service is unreachable
* @throws UnexpectedErrorException If an unexpected exception occurs
*/ */
public Either<ServiceError, TickerPrice> getTickerPriceForSymbol(String symbol) public TickerPrice getTickerPriceForSymbol(String symbol)
throws BinanceServiceUnreachableException { throws BinanceServiceException {
if (symbol == null) { if (symbol == null) {
throw new IllegalArgumentException("Symbol must not be null"); throw new IllegalArgumentException("Symbol must not be null");
} }
@ -289,14 +344,15 @@ public class MarketData extends BaseBinanceApi {
String response = EntityUtils.toString(httpEntity); String response = EntityUtils.toString(httpEntity);
if (!Helpers.statusCodeIsOk(sl.getStatusCode())) { if (!HttpRequests.statusCodeIsOk(sl.getStatusCode())) {
return Either.ofLeft(mapper.readValue(response, ServiceError.class)); ServiceError error = mapper.readValue(response, ServiceError.class);
throw HttpRequests.buildHttpException(sl.getStatusCode(), error);
} }
return Either.ofRight(mapper.readValue(response, TickerPrice.class)); return mapper.readValue(response, TickerPrice.class);
} }
} catch (IOException e) { } catch (IOException e) {
throw new BinanceServiceUnreachableException(e.getMessage(), e.getCause()); throw new UnexpectedErrorException(e.getMessage(), e.getCause());
} }
} }
@ -304,10 +360,15 @@ public class MarketData extends BaseBinanceApi {
* Retrieves ticker prices for all supported Binance symbols. This is good to use in terms of cost if you need * Retrieves ticker prices for all supported Binance symbols. This is good to use in terms of cost if you need
* more than one symbol's current ticker price. * more than one symbol's current ticker price.
* *
* @return Either a list of TickerPrice objects if successful, or an ServiceError * @return A populated list of TickerPrice objects
* @throws BinanceServiceUnreachableException If the service is unreachable * @throws com.sigmaflare.binancej.exceptions.IpBannedException If the IP is banned
* @throws com.sigmaflare.binancej.exceptions.RateLimitExceededException If the rate limit is exceeded
* @throws com.sigmaflare.binancej.exceptions.MalformedRequestException If the user's request is malformed
* @throws com.sigmaflare.binancej.exceptions.InternalServiceErrorException If the error occurs on Binance's side
* @throws com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException If the service is unreachable
* @throws UnexpectedErrorException If an unexpected exception occurs
*/ */
public Either<ServiceError, List<TickerPrice>> getTickerPrices() throws BinanceServiceUnreachableException { public List<TickerPrice> getTickerPrices() throws BinanceServiceException {
final HttpGet request = buildGetRequestFromEndpoint(TICKER_PRICE_URL, apiKey); final HttpGet request = buildGetRequestFromEndpoint(TICKER_PRICE_URL, apiKey);
try { try {
@ -324,15 +385,16 @@ public class MarketData extends BaseBinanceApi {
String response = EntityUtils.toString(httpEntity); String response = EntityUtils.toString(httpEntity);
if (!Helpers.statusCodeIsOk(sl.getStatusCode())) { if (!HttpRequests.statusCodeIsOk(sl.getStatusCode())) {
return Either.ofLeft(mapper.readValue(response, ServiceError.class)); ServiceError error = mapper.readValue(response, ServiceError.class);
throw HttpRequests.buildHttpException(sl.getStatusCode(), error);
} }
JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, TickerPrice.class); JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, TickerPrice.class);
return Either.ofRight(mapper.readValue(response, type)); return mapper.readValue(response, type);
} }
} catch (IOException e) { } catch (IOException e) {
throw new BinanceServiceUnreachableException(e.getMessage(), e.getCause()); throw new UnexpectedErrorException(e.getMessage(), e.getCause());
} }
} }
@ -341,11 +403,16 @@ public class MarketData extends BaseBinanceApi {
* as glorified URL builders. * as glorified URL builders.
* *
* @param endpoint The endpoint to use that has the associated parameters populated * @param endpoint The endpoint to use that has the associated parameters populated
* @return A list of candlesticks if successful, otherwise an ServiceError * @return A list of candlesticks
* @throws BinanceServiceUnreachableException If the service is unreachable * @throws com.sigmaflare.binancej.exceptions.IpBannedException If the IP is banned
* @throws com.sigmaflare.binancej.exceptions.RateLimitExceededException If the rate limit is exceeded
* @throws com.sigmaflare.binancej.exceptions.MalformedRequestException If the user's request is malformed
* @throws com.sigmaflare.binancej.exceptions.InternalServiceErrorException If the error occurs on Binance's side
* @throws com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException If the service is unreachable
* @throws UnexpectedErrorException If an unexpected exception occurs
*/ */
private Either<ServiceError, List<Candlestick>> private List<Candlestick>
getCandleStickDataFromUrl(String endpoint) throws BinanceServiceUnreachableException { getCandleStickDataFromUrl(String endpoint) throws BinanceServiceException {
final HttpGet request = buildGetRequestFromEndpoint(endpoint, apiKey); final HttpGet request = buildGetRequestFromEndpoint(endpoint, apiKey);
try { try {
@ -362,15 +429,16 @@ public class MarketData extends BaseBinanceApi {
String response = EntityUtils.toString(httpEntity); String response = EntityUtils.toString(httpEntity);
if (!Helpers.statusCodeIsOk(sl.getStatusCode())) { if (!HttpRequests.statusCodeIsOk(sl.getStatusCode())) {
return Either.ofLeft(mapper.readValue(response, ServiceError.class)); ServiceError error = mapper.readValue(response, ServiceError.class);
throw HttpRequests.buildHttpException(sl.getStatusCode(), error);
} }
JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, Candlestick.class); JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, Candlestick.class);
return Either.ofRight(mapper.readValue(response, type)); return mapper.readValue(response, type);
} }
} catch (IOException e) { } catch (IOException e) {
throw new BinanceServiceUnreachableException(e.getMessage(), e.getCause()); throw new UnexpectedErrorException(e.getMessage(), e.getCause());
} }
} }
} }

4
src/main/java/com/sigmaflare/binancej/entities/ServiceError.java

@ -13,9 +13,9 @@ import lombok.NonNull;
@NoArgsConstructor @NoArgsConstructor
public class ServiceError { public class ServiceError {
@JsonProperty("code") @JsonProperty("code")
public int code; private int code;
@NonNull @NonNull
@JsonProperty("msg") @JsonProperty("msg")
public String message; private String message;
} }

4
src/main/java/com/sigmaflare/binancej/exceptions/BinanceServiceUnreachableException.java

@ -6,6 +6,6 @@ import lombok.Getter;
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public class BinanceServiceUnreachableException extends BinanceServiceException { public class BinanceServiceUnreachableException extends BinanceServiceException {
public String message; private final String message;
public Throwable cause; private final Throwable cause;
} }

11
src/main/java/com/sigmaflare/binancej/exceptions/InternalServiceErrorException.java

@ -0,0 +1,11 @@
package com.sigmaflare.binancej.exceptions;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class InternalServiceErrorException extends BinanceServiceException {
private final int code;
private final String message;
}

10
src/main/java/com/sigmaflare/binancej/exceptions/IpBannedException.java

@ -0,0 +1,10 @@
package com.sigmaflare.binancej.exceptions;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class IpBannedException extends BinanceServiceException {
private final String message;
}

11
src/main/java/com/sigmaflare/binancej/exceptions/MalformedRequestException.java

@ -0,0 +1,11 @@
package com.sigmaflare.binancej.exceptions;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class MalformedRequestException extends BinanceServiceException {
private final int code;
private final String message;
}

10
src/main/java/com/sigmaflare/binancej/exceptions/RateLimitExceededException.java

@ -0,0 +1,10 @@
package com.sigmaflare.binancej.exceptions;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class RateLimitExceededException extends BinanceServiceException {
private final String message;
}

7
src/main/java/com/sigmaflare/binancej/exceptions/UnexpectedErrorException.java

@ -0,0 +1,7 @@
package com.sigmaflare.binancej.exceptions;
public class UnexpectedErrorException extends RuntimeException {
public UnexpectedErrorException(String message, Throwable cause) {
super(message, cause);
}
}

145
src/test/java/com/sigmaflare/binancej/CandlestickMethodTests.java

@ -1,12 +1,13 @@
package com.sigmaflare.binancej; package com.sigmaflare.binancej;
import com.codepoetics.ambivalence.Either;
import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.sigmaflare.binancej.entities.Candlestick; import com.sigmaflare.binancej.entities.Candlestick;
import com.sigmaflare.binancej.entities.ServiceError; import com.sigmaflare.binancej.entities.ServiceError;
import com.sigmaflare.binancej.entities.Interval; import com.sigmaflare.binancej.entities.Interval;
import com.sigmaflare.binancej.exceptions.BinanceServiceException;
import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException; import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException;
import com.sigmaflare.binancej.exceptions.MalformedRequestException;
import com.sigmaflare.binancej.matchers.GetMatcher; import com.sigmaflare.binancej.matchers.GetMatcher;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.http.ProtocolVersion; import org.apache.http.ProtocolVersion;
@ -26,15 +27,15 @@ import java.util.List;
import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT; import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class CandlestickMethodTests { public class CandlestickMethodTests {
private static ObjectMapper mapper = Helpers.objectMapperBuilder(); private static ObjectMapper mapper = HttpRequests.objectMapperBuilder();
private final String url = String.format("%s%s", BASE_ENDPOINT, "/api/v1/klines?symbol=TEST&interval=8h"); private final String url = String.format("%s%s", BASE_ENDPOINT, "/api/v1/klines?symbol=TEST&interval=8h");
private final String urlWithLimit = String.format("%s%s", url, "&limit=1000"); private final String urlWithLimit = String.format("%s%s", url, "&limit=250");
private final String urlWithStartTime = String.format("%s%s", url, "&startTime=1234"); private final String urlWithStartTime = String.format("%s%s", url, "&startTime=1234");
private final String urlWithEndTime = String.format("%s%s", url, "&endTime=1234"); private final String urlWithEndTime = String.format("%s%s", url, "&endTime=1234");
private final String urlWithAllParameters = String.format("%s%s", urlWithLimit, "&startTime=1234&endTime=1234"); private final String urlWithAllParameters = String.format("%s%s", urlWithLimit, "&startTime=1234&endTime=1234");
@ -82,7 +83,7 @@ public class CandlestickMethodTests {
} }
@Test @Test
public void testCandlestickServiceReturnsSuccessfully() throws IOException, BinanceServiceUnreachableException { public void testCandlestickServiceReturnsSuccessfully() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -100,16 +101,38 @@ public class CandlestickMethodTests {
MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, List<Candlestick>> res = List<Candlestick> res = marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS);
marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS);
assertTrue(res.isRight()); assertEquals(candlesticks, res);
assertEquals(candlesticks, Helpers.extractEitherValueSafely(res.right()));
} }
@Test @Test
public void testCandlestickServiceWithLimitReturnsSuccessfully() public void testCandlestickServiceWithLimitReturnsSuccessfully() throws Exception {
throws IOException, BinanceServiceUnreachableException { CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
when(mockedCloseableHttpResponse.getStatusLine())
.thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0),
200, "test"));
when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(urlWithLimit, "1234"))))
.thenReturn(mockedCloseableHttpResponse);
BasicHttpEntity httpEntity = new BasicHttpEntity();
httpEntity.setContent(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)));
when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity);
MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient);
List<Candlestick> res =
marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 250);
assertEquals(candlesticks, res);
}
@Test(expected = IllegalArgumentException.class)
public void testCandlestickServiceWithLimitOutOfBoundsThrows() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -127,16 +150,14 @@ public class CandlestickMethodTests {
MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, List<Candlestick>> res = List<Candlestick> res =
marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 1000); marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 60000);
assertTrue(res.isRight()); assertEquals(candlesticks, res);
assertEquals(candlesticks, Helpers.extractEitherValueSafely(res.right()));
} }
@Test @Test
public void testCandlestickServiceWithLimitAndStartTimeReturnsSuccessfully() public void testCandlestickServiceWithLimitAndStartTimeReturnsSuccessfully() throws Exception {
throws IOException, BinanceServiceUnreachableException {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -154,16 +175,39 @@ public class CandlestickMethodTests {
MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, List<Candlestick>> res = List<Candlestick> res =
marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 1000, 1234L,true); marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 250, 1234L, true);
assertTrue(res.isRight()); assertEquals(candlesticks, res);
assertEquals(candlesticks, Helpers.extractEitherValueSafely(res.right())); }
@Test(expected = IllegalArgumentException.class)
public void testCandlestickServiceWithLimitOutOfBoundsAndStartTimeThrows() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
when(mockedCloseableHttpResponse.getStatusLine())
.thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0),
200, "test"));
when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(urlWithLimitAndStartTime, "1234"))))
.thenReturn(mockedCloseableHttpResponse);
BasicHttpEntity httpEntity = new BasicHttpEntity();
httpEntity.setContent(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)));
when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity);
MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient);
List<Candlestick> res =
marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 750, 1234L, true);
assertEquals(candlesticks, res);
} }
@Test @Test
public void testCandlestickServiceWithLimitAndEndTimeReturnsSuccessfully() public void testCandlestickServiceWithLimitAndEndTimeReturnsSuccessfully() throws Exception {
throws IOException, BinanceServiceUnreachableException {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -181,16 +225,14 @@ public class CandlestickMethodTests {
MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, List<Candlestick>> res = List<Candlestick> res =
marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 1000, 1234L,false); marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 250, 1234L, false);
assertTrue(res.isRight()); assertEquals(candlesticks, res);
assertEquals(candlesticks, Helpers.extractEitherValueSafely(res.right()));
} }
@Test @Test
public void testCandlestickServiceWithAllParametersReturnsSuccessfully() public void testCandlestickServiceWithAllParametersReturnsSuccessfully() throws Exception {
throws IOException, BinanceServiceUnreachableException {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -208,17 +250,15 @@ public class CandlestickMethodTests {
MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, List<Candlestick>> res = List<Candlestick> res =
marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS,
1000, 1234L,1234L); 250, 1234L, 1234L);
assertTrue(res.isRight()); assertEquals(candlesticks, res);
assertEquals(candlesticks, Helpers.extractEitherValueSafely(res.right()));
} }
@Test @Test
public void testCandlestickServiceWithStartTimeReturnsSuccessfully() public void testCandlestickServiceWithStartTimeReturnsSuccessfully() throws Exception {
throws IOException, BinanceServiceUnreachableException {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -236,16 +276,14 @@ public class CandlestickMethodTests {
MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, List<Candlestick>> res = List<Candlestick> res =
marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 1234L,true); marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 1234L, true);
assertTrue(res.isRight()); assertEquals(candlesticks, res);
assertEquals(candlesticks, Helpers.extractEitherValueSafely(res.right()));
} }
@Test @Test
public void testCandlestickServiceWithEndTimeReturnsSuccessfully() public void testCandlestickServiceWithEndTimeReturnsSuccessfully() throws Exception {
throws IOException, BinanceServiceUnreachableException {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -263,15 +301,14 @@ public class CandlestickMethodTests {
MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, List<Candlestick>> res = List<Candlestick> res =
marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 1234L,false); marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 1234L, false);
assertTrue(res.isRight()); assertEquals(candlesticks, res);
assertEquals(candlesticks, Helpers.extractEitherValueSafely(res.right()));
} }
@Test(expected = BinanceServiceUnreachableException.class) @Test(expected = BinanceServiceUnreachableException.class)
public void testCandlestickServiceWithoutHttpEntityThrows() throws IOException, BinanceServiceUnreachableException { public void testCandlestickServiceWithoutHttpEntityThrows() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -288,7 +325,7 @@ public class CandlestickMethodTests {
} }
@Test @Test
public void testCandlestickServiceBadApiKeyReturns400() throws IOException, BinanceServiceUnreachableException { public void testCandlestickServiceBadApiKeyReturns400() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -309,10 +346,18 @@ public class CandlestickMethodTests {
MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, List<Candlestick>> res =
try {
marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS); marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS);
fail();
} catch (BinanceServiceException e) {
if (!(e instanceof MalformedRequestException)) {
fail();
}
assertEquals(400, ((MalformedRequestException) e).getCode());
assertEquals("Bad API Key", e.getMessage());
}
assertTrue(res.isLeft());
assertEquals(serviceError, Helpers.extractEitherValueSafely(res.left()));
} }
} }

32
src/test/java/com/sigmaflare/binancej/ExchangeInfoMethodTests.java

@ -1,11 +1,12 @@
package com.sigmaflare.binancej; package com.sigmaflare.binancej;
import com.codepoetics.ambivalence.Either;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.sigmaflare.binancej.entities.ExchangeInfo; import com.sigmaflare.binancej.entities.ExchangeInfo;
import com.sigmaflare.binancej.entities.ServiceError; import com.sigmaflare.binancej.entities.ServiceError;
import com.sigmaflare.binancej.exceptions.BinanceServiceException;
import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException; import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException;
import com.sigmaflare.binancej.exceptions.MalformedRequestException;
import com.sigmaflare.binancej.matchers.GetMatcher; import com.sigmaflare.binancej.matchers.GetMatcher;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.http.ProtocolVersion; import org.apache.http.ProtocolVersion;
@ -23,13 +24,13 @@ import java.nio.charset.StandardCharsets;
import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT; import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class ExchangeInfoMethodTests { public class ExchangeInfoMethodTests {
private static ObjectMapper mapper = Helpers.objectMapperBuilder(); private static ObjectMapper mapper = HttpRequests.objectMapperBuilder();
private String url = String.format("%s%s", BASE_ENDPOINT, "/api/v1/exchangeInfo"); private String url = String.format("%s%s", BASE_ENDPOINT, "/api/v1/exchangeInfo");
private ExchangeInfo exchangeInfo; private ExchangeInfo exchangeInfo;
@ -44,7 +45,7 @@ public class ExchangeInfoMethodTests {
} }
@Test @Test
public void testExchangeInfoServiceReturnsSuccessfully() throws IOException, BinanceServiceUnreachableException { public void testExchangeInfoServiceReturnsSuccessfully() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -65,14 +66,13 @@ public class ExchangeInfoMethodTests {
GeneralUtilities generalUtilities = GeneralUtilities generalUtilities =
new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient); new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, ExchangeInfo> res = generalUtilities.getExchangeInfo(); ExchangeInfo res = generalUtilities.getExchangeInfo();
assertTrue(res.isRight()); assertEquals(exchangeInfo, res);
assertEquals(exchangeInfo, Helpers.extractEitherValueSafely(res.right()));
} }
@Test(expected = BinanceServiceUnreachableException.class) @Test(expected = BinanceServiceUnreachableException.class)
public void testExchangeInfoServiceWithNoHttpEntityFails() throws IOException, BinanceServiceUnreachableException { public void testExchangeInfoServiceWithNoHttpEntityFails() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -90,7 +90,7 @@ public class ExchangeInfoMethodTests {
} }
@Test @Test
public void testExchangeInfoServiceBadApiKeyReturns400() throws IOException, BinanceServiceUnreachableException { public void testExchangeInfoServiceBadApiKeyReturns400() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -112,9 +112,17 @@ public class ExchangeInfoMethodTests {
GeneralUtilities generalUtilities = GeneralUtilities generalUtilities =
new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient); new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, ExchangeInfo> res = generalUtilities.getExchangeInfo();
assertTrue(res.isLeft()); try {
assertEquals(serviceError, Helpers.extractEitherValueSafely(res.left())); generalUtilities.getExchangeInfo();
fail();
} catch (BinanceServiceException e) {
if (!(e instanceof MalformedRequestException)) {
fail();
}
assertEquals(400, ((MalformedRequestException) e).getCode());
assertEquals("Bad API Key", e.getMessage());
}
} }
} }

42
src/test/java/com/sigmaflare/binancej/HttpErrorThrowerTests.java

@ -0,0 +1,42 @@
package com.sigmaflare.binancej;
import com.sigmaflare.binancej.entities.ServiceError;
import com.sigmaflare.binancej.exceptions.InternalServiceErrorException;
import com.sigmaflare.binancej.exceptions.IpBannedException;
import com.sigmaflare.binancej.exceptions.MalformedRequestException;
import com.sigmaflare.binancej.exceptions.RateLimitExceededException;
import org.junit.Test;
public class HttpErrorThrowerTests {
@Test(expected = IpBannedException.class)
public void testIpBannedOnHttpCode418() throws Exception {
throw HttpRequests.buildHttpException(418, ServiceError.builder().message("TEST").code(418).build());
}
@Test(expected = RateLimitExceededException.class)
public void testRateLimitOnHttpCode421() throws Exception {
throw HttpRequests.buildHttpException(429, ServiceError.builder().message("TEST").code(429).build());
}
@Test(expected = MalformedRequestException.class)
public void testMalformedRequestOnHttpCode400() throws Exception {
throw HttpRequests.buildHttpException(400, ServiceError.builder().message("TEST").code(400).build());
}
@Test(expected = InternalServiceErrorException.class)
public void testInternalServiceErrorOnHttpCode500() throws Exception {
throw HttpRequests.buildHttpException(500, ServiceError.builder().message("TEST").code(500).build());
}
@Test(expected = InternalServiceErrorException.class)
public void testInternalServiceErrorOnHttpCode504() throws Exception {
throw HttpRequests.buildHttpException(504, ServiceError.builder().message("TEST").code(504).build());
}
@Test(expected = IllegalArgumentException.class)
public void testInternalServiceThrowsOn200Ok() throws Exception {
throw HttpRequests.buildHttpException(200, ServiceError.builder().message("TEST").code(200).build());
}
}

61
src/test/java/com/sigmaflare/binancej/OrderBookDepthTests.java

@ -1,11 +1,12 @@
package com.sigmaflare.binancej; package com.sigmaflare.binancej;
import com.codepoetics.ambivalence.Either;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.sigmaflare.binancej.entities.OrderBookDepth; import com.sigmaflare.binancej.entities.OrderBookDepth;
import com.sigmaflare.binancej.entities.OrderBookPricing; import com.sigmaflare.binancej.entities.OrderBookPricing;
import com.sigmaflare.binancej.entities.ServiceError; import com.sigmaflare.binancej.entities.ServiceError;
import com.sigmaflare.binancej.exceptions.BinanceServiceException;
import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException; import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException;
import com.sigmaflare.binancej.exceptions.MalformedRequestException;
import com.sigmaflare.binancej.matchers.GetMatcher; import com.sigmaflare.binancej.matchers.GetMatcher;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.http.ProtocolVersion; import org.apache.http.ProtocolVersion;
@ -24,13 +25,13 @@ import java.nio.charset.StandardCharsets;
import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT; import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class OrderBookDepthTests { public class OrderBookDepthTests {
private static ObjectMapper mapper = Helpers.objectMapperBuilder(); private static ObjectMapper mapper = HttpRequests.objectMapperBuilder();
private final String url = String.format("%s%s", BASE_ENDPOINT, "/api/v1/depth?symbol=ETHBTC&limit=100"); private final String url = String.format("%s%s", BASE_ENDPOINT, "/api/v1/depth?symbol=ETHBTC&limit=100");
private final String urlWithLimit = String.format("%s%s", BASE_ENDPOINT, "/api/v1/depth?symbol=ETHBTC&limit=1000"); private final String urlWithLimit = String.format("%s%s", BASE_ENDPOINT, "/api/v1/depth?symbol=ETHBTC&limit=1000");
@ -70,7 +71,7 @@ public class OrderBookDepthTests {
@Test @Test
public void testOrderBookDepthReturnsSuccessfully() throws IOException, BinanceServiceUnreachableException { public void testOrderBookDepthReturnsSuccessfully() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -92,15 +93,43 @@ public class OrderBookDepthTests {
MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, OrderBookDepth> res = marketData.getOrderBookDepth("ETHBTC"); OrderBookDepth res = marketData.getOrderBookDepth("ETHBTC");
assertTrue(res.isRight()); assertEquals(orderBookDepth, res);
assertEquals(orderBookDepth, Helpers.extractEitherValueSafely(res.right()));
}
@Test(expected = IllegalArgumentException.class)
public void testOrderBookDepthWithLimitOutOfBounds() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
when(mockedCloseableHttpResponse.getStatusLine())
.thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0),
200, "test"));
when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234"))))
.thenReturn(mockedCloseableHttpResponse);
BasicHttpEntity httpEntity = new BasicHttpEntity();
// This changes from the usual way we do this because we do post-processing in the deserializer and the JSON
// will no longer match the stringified OrderBookDepth.
httpEntity.setContent(new ByteArrayInputStream(orderBookDepthJson.getBytes()));
when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity);
MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient);
OrderBookDepth res = marketData.getOrderBookDepth("ETHBTC", 1500);
assertEquals(orderBookDepth, res);
} }
@Test(expected = BinanceServiceUnreachableException.class) @Test(expected = BinanceServiceUnreachableException.class)
public void testOrderBookDepthWithNoHttpEntityFails() throws IOException, BinanceServiceUnreachableException { public void testOrderBookDepthWithNoHttpEntityFails() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -117,7 +146,7 @@ public class OrderBookDepthTests {
} }
@Test @Test
public void testOrderBookDepthWithBadApiKeyReturns400() throws IOException, BinanceServiceUnreachableException { public void testOrderBookDepthWithBadApiKeyReturns400() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -139,9 +168,17 @@ public class OrderBookDepthTests {
when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity);
Either<ServiceError, OrderBookDepth> res = marketData.getOrderBookDepth("ETHBTC", 1000); try {
marketData.getOrderBookDepth("ETHBTC", 1000);
fail();
} catch (BinanceServiceException e) {
if (!(e instanceof MalformedRequestException)) {
fail();
}
assertEquals(400, ((MalformedRequestException) e).getCode());
assertEquals("Bad API Key", e.getMessage());
}
assertTrue(res.isLeft());
assertEquals(serviceError, Helpers.extractEitherValueSafely(res.left()));
} }
} }

32
src/test/java/com/sigmaflare/binancej/PingMethodTests.java

@ -1,11 +1,11 @@
package com.sigmaflare.binancej; package com.sigmaflare.binancej;
import com.codepoetics.ambivalence.Either;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.sigmaflare.binancej.entities.ServiceError; import com.sigmaflare.binancej.entities.ServiceError;
import com.sigmaflare.binancej.entities.Ping; import com.sigmaflare.binancej.exceptions.BinanceServiceException;
import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException; import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException;
import com.sigmaflare.binancej.exceptions.MalformedRequestException;
import com.sigmaflare.binancej.matchers.GetMatcher; import com.sigmaflare.binancej.matchers.GetMatcher;
import org.apache.http.ProtocolVersion; import org.apache.http.ProtocolVersion;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
@ -20,7 +20,7 @@ import java.nio.charset.StandardCharsets;
import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT; import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -30,7 +30,7 @@ public class PingMethodTests {
private final String url = String.format("%s%s", BASE_ENDPOINT, "/api/v1/ping"); private final String url = String.format("%s%s", BASE_ENDPOINT, "/api/v1/ping");
@Test @Test
public void testServiceAliveReturnsSuccess() throws IOException, BinanceServiceUnreachableException { public void testServiceAliveReturnsSuccess() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -50,14 +50,12 @@ public class PingMethodTests {
GeneralUtilities generalUtilities = GeneralUtilities generalUtilities =
new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient); new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, Ping> res = generalUtilities.ping(); // Ping returns an object that is an empty Ping - all we care is that is doesn't throw
generalUtilities.ping();
assertTrue(res.isRight());
} }
@Test(expected = BinanceServiceUnreachableException.class) @Test(expected = BinanceServiceUnreachableException.class)
public void testServiceFailsToReturnHttpEntity() throws IOException, BinanceServiceUnreachableException { public void testServiceFailsToReturnHttpEntity() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -75,7 +73,7 @@ public class PingMethodTests {
} }
@Test @Test
public void testServiceBadApiKeyReturns400() throws IOException, BinanceServiceUnreachableException { public void testServiceBadApiKeyReturns400() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -97,9 +95,15 @@ public class PingMethodTests {
GeneralUtilities generalUtilities GeneralUtilities generalUtilities
= new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient); = new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, Ping> res = generalUtilities.ping(); try {
generalUtilities.ping();
assertTrue(res.isLeft()); fail();
assertEquals(serviceError, Helpers.extractEitherValueSafely(res.left())); } catch (BinanceServiceException e) {
if (!(e instanceof MalformedRequestException)) {
fail();
}
assertEquals(400, ((MalformedRequestException) e).getCode());
assertEquals("Bad API Key", e.getMessage());
}
} }
} }

59
src/test/java/com/sigmaflare/binancej/TickerPriceTests.java

@ -1,11 +1,12 @@
package com.sigmaflare.binancej; package com.sigmaflare.binancej;
import com.codepoetics.ambivalence.Either;
import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.sigmaflare.binancej.entities.ServiceError; import com.sigmaflare.binancej.entities.ServiceError;
import com.sigmaflare.binancej.entities.TickerPrice; import com.sigmaflare.binancej.entities.TickerPrice;
import com.sigmaflare.binancej.exceptions.BinanceServiceException;
import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException; import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException;
import com.sigmaflare.binancej.exceptions.MalformedRequestException;
import com.sigmaflare.binancej.matchers.GetMatcher; import com.sigmaflare.binancej.matchers.GetMatcher;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.http.ProtocolVersion; import org.apache.http.ProtocolVersion;
@ -24,13 +25,13 @@ import java.util.List;
import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT; import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class TickerPriceTests { public class TickerPriceTests {
private static final ObjectMapper mapper = Helpers.objectMapperBuilder(); private static final ObjectMapper mapper = HttpRequests.objectMapperBuilder();
private final String url = String.format("%s%s", BASE_ENDPOINT, "/api/v3/ticker/price"); private final String url = String.format("%s%s", BASE_ENDPOINT, "/api/v3/ticker/price");
private final String specificUrl = String.format("%s%s", url, "?symbol=TEST"); private final String specificUrl = String.format("%s%s", url, "?symbol=TEST");
@ -61,7 +62,7 @@ public class TickerPriceTests {
} }
@Test @Test
public void testTickerPriceForSymbolReturnsSuccessfully() throws IOException, BinanceServiceUnreachableException { public void testTickerPriceForSymbolReturnsSuccessfully() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -80,14 +81,13 @@ public class TickerPriceTests {
MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, TickerPrice> res = marketData.getTickerPriceForSymbol("TEST"); TickerPrice res = marketData.getTickerPriceForSymbol("TEST");
assertTrue(res.isRight()); assertEquals(tickerPrice, res);
assertEquals(tickerPrice, Helpers.extractEitherValueSafely(res.right()));
} }
@Test @Test
public void testTickerPricesSuccessfully() throws IOException, BinanceServiceUnreachableException { public void testTickerPricesSuccessfully() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -106,15 +106,13 @@ public class TickerPriceTests {
MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, List<TickerPrice>> res = marketData.getTickerPrices(); List<TickerPrice> res = marketData.getTickerPrices();
assertTrue(res.isRight()); assertEquals(tickerPrices, res);
assertEquals(tickerPrices, Helpers.extractEitherValueSafely(res.right()));
} }
@Test(expected = BinanceServiceUnreachableException.class) @Test(expected = BinanceServiceUnreachableException.class)
public void testTickerPriceForSymbolWithoutHttpEntityThrows() public void testTickerPriceForSymbolWithoutHttpEntityThrows() throws Exception {
throws IOException, BinanceServiceUnreachableException {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -131,8 +129,7 @@ public class TickerPriceTests {
} }
@Test(expected = BinanceServiceUnreachableException.class) @Test(expected = BinanceServiceUnreachableException.class)
public void testTickerPricesWithoutHttpEntityThrows() public void testTickerPricesWithoutHttpEntityThrows() throws Exception {
throws IOException, BinanceServiceUnreachableException {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -149,8 +146,7 @@ public class TickerPriceTests {
} }
@Test @Test
public void testTickerPriceForSymbolWithBadApiKeyReturns400() public void testTickerPriceForSymbolWithBadApiKeyReturns400() throws Exception {
throws IOException, BinanceServiceUnreachableException {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -171,14 +167,21 @@ public class TickerPriceTests {
MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, TickerPrice> res = marketData.getTickerPriceForSymbol("TEST"); try {
marketData.getTickerPriceForSymbol("TEST");
assertTrue(res.isLeft()); fail();
assertEquals(serviceError, Helpers.extractEitherValueSafely(res.left())); } catch (BinanceServiceException e) {
if (!(e instanceof MalformedRequestException)) {
fail();
}
assertEquals(400, ((MalformedRequestException) e).getCode());
assertEquals("Bad API Key", e.getMessage());
}
} }
@Test @Test
public void testTickerPricesBadApiKeyReturns400() throws IOException, BinanceServiceUnreachableException { public void testTickerPricesBadApiKeyReturns400() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -199,9 +202,15 @@ public class TickerPriceTests {
MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, List<TickerPrice>> res = marketData.getTickerPrices(); try {
marketData.getTickerPrices();
} catch (BinanceServiceException e) {
if (!(e instanceof MalformedRequestException)) {
fail();
}
assertTrue(res.isLeft()); assertEquals(400, ((MalformedRequestException) e).getCode());
assertEquals(serviceError, Helpers.extractEitherValueSafely(res.left())); assertEquals("Bad API Key", e.getMessage());
}
} }
} }

31
src/test/java/com/sigmaflare/binancej/TimeMethodTests.java

@ -1,10 +1,11 @@
package com.sigmaflare.binancej; package com.sigmaflare.binancej;
import com.codepoetics.ambivalence.Either;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.sigmaflare.binancej.entities.ServiceError; import com.sigmaflare.binancej.entities.ServiceError;
import com.sigmaflare.binancej.entities.Time; import com.sigmaflare.binancej.entities.Time;
import com.sigmaflare.binancej.exceptions.BinanceServiceException;
import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException; import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException;
import com.sigmaflare.binancej.exceptions.MalformedRequestException;
import com.sigmaflare.binancej.matchers.GetMatcher; import com.sigmaflare.binancej.matchers.GetMatcher;
import org.apache.http.ProtocolVersion; import org.apache.http.ProtocolVersion;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
@ -19,7 +20,7 @@ import java.nio.charset.StandardCharsets;
import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT; import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -29,7 +30,7 @@ public class TimeMethodTests {
private final String url = String.format("%s%s", BASE_ENDPOINT, "/api/v1/time"); private final String url = String.format("%s%s", BASE_ENDPOINT, "/api/v1/time");
@Test @Test
public void testTimeReturnsSuccessfully() throws IOException, BinanceServiceUnreachableException { public void testTimeReturnsSuccessfully() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -52,14 +53,13 @@ public class TimeMethodTests {
GeneralUtilities generalUtilities = GeneralUtilities generalUtilities =
new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient); new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, Time> res = generalUtilities.getServerTime(); Time res = generalUtilities.getServerTime();
assertTrue(res.isRight()); assertEquals(time, res);
assertEquals(time, Helpers.extractEitherValueSafely(res.right()));
} }
@Test(expected = BinanceServiceUnreachableException.class) @Test(expected = BinanceServiceUnreachableException.class)
public void testTimeNoEntityReturnedFails() throws IOException, BinanceServiceUnreachableException { public void testTimeNoEntityReturnedFails() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -77,7 +77,7 @@ public class TimeMethodTests {
} }
@Test @Test
public void testTimeBadApiKeyReturns400() throws IOException, BinanceServiceUnreachableException { public void testTimeBadApiKeyReturns400() throws Exception {
CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class);
CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class);
@ -100,9 +100,16 @@ public class TimeMethodTests {
GeneralUtilities generalUtilities = GeneralUtilities generalUtilities =
new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient); new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient);
Either<ServiceError, Time> res = generalUtilities.getServerTime(); try {
generalUtilities.getServerTime();
assertTrue(res.isLeft()); fail();
assertEquals(serviceError, Helpers.extractEitherValueSafely(res.left())); } catch(BinanceServiceException e) {
if(!(e instanceof MalformedRequestException)) {
fail();
}
assertEquals(400, ((MalformedRequestException) e).getCode());
assertEquals("Bad API Key", e.getMessage());
}
} }
} }

Loading…
Cancel
Save