ips) throws IpdataException;
@Cacheable
@RequestLine("GET /{ip}?fields={fields}")
- IpdataModel getFields(@Param("ip") String ip, @Param("fields") String fields) throws IpdataException;
+ IpdataModel getFields(@Param(value = "ip", encoded = true) String ip, @Param("fields") String fields) throws IpdataException;
}
diff --git a/src/main/java/io/ipdata/client/service/IpdataInternalSingleFieldClient.java b/src/main/java/io/ipdata/client/service/IpdataInternalSingleFieldClient.java
index 2d39366..5f55f7a 100644
--- a/src/main/java/io/ipdata/client/service/IpdataInternalSingleFieldClient.java
+++ b/src/main/java/io/ipdata/client/service/IpdataInternalSingleFieldClient.java
@@ -3,70 +3,69 @@
import feign.Param;
import feign.RequestLine;
import io.ipdata.client.error.IpdataException;
-import io.ipdata.client.model.Asn;
+import io.ipdata.client.model.AsnModel;
import io.ipdata.client.model.Currency;
-import io.ipdata.client.model.Threat;
+import io.ipdata.client.model.ThreatModel;
import io.ipdata.client.model.TimeZone;
-@SuppressWarnings("RedundantThrows")
interface IpdataInternalSingleFieldClient {
@RequestLine("GET /{ip}/ip")
- String getIp(@Param("ip") String ip) throws IpdataException;
+ String getIp(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
@RequestLine("GET /{ip}/is_eu")
- boolean isEu(@Param("ip") String ip) throws IpdataException;
+ boolean isEu(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
@RequestLine("GET /{ip}/city")
- String getCity(@Param("ip") String ip) throws IpdataException;
+ String getCity(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
@RequestLine("GET /{ip}/country_name")
- String getCountryName(@Param("ip") String ip) throws IpdataException;
+ String getCountryName(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
@RequestLine("GET /{ip}/country_code")
- String getCountryCode(@Param("ip") String ip) throws IpdataException;
+ String getCountryCode(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
@RequestLine("GET /{ip}/continent_code")
- String getContinentCode(@Param("ip") String ip) throws IpdataException;
+ String getContinentCode(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
@RequestLine("GET /{ip}/longitude")
- double getLongitude(@Param("ip") String ip) throws IpdataException;
+ double getLongitude(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
@RequestLine("GET /{ip}/latitude")
- double getLatitude(@Param("ip") String ip) throws IpdataException;
+ double getLatitude(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
@RequestLine("GET /{ip}/organisation")
- String getOrganisation(@Param("ip") String ip) throws IpdataException;
+ String getOrganisation(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
@RequestLine("GET /{ip}/postal")
- String getPostal(@Param("ip") String ip) throws IpdataException;
+ String getPostal(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
- @RequestLine("GET /{ip}/asn")
- String getCallingCode(@Param("ip") String ip) throws IpdataException;
+ @RequestLine("GET /{ip}/calling_code")
+ String getCallingCode(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
@RequestLine("GET /{ip}/flag")
- String getFlag(@Param("ip") String ip) throws IpdataException;
+ String getFlag(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
@RequestLine("GET /{ip}/emoji_flag")
- String getEmojiFlag(@Param("ip") String ip) throws IpdataException;
+ String getEmojiFlag(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
@RequestLine("GET /{ip}/emoji_unicode")
- String getEmojiUnicode(@Param("ip") String ip) throws IpdataException;
+ String getEmojiUnicode(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
@Cacheable
@RequestLine("GET /{ip}/asn")
- Asn asn(@Param("ip") String ip) throws IpdataException;
+ AsnModel asn(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
@Cacheable
@RequestLine("GET /{ip}/time_zone")
- TimeZone timeZone(@Param("ip") String ip) throws IpdataException;
+ TimeZone timeZone(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
@Cacheable
@RequestLine("GET /{ip}/currency")
- Currency currency(@Param("ip") String ip) throws IpdataException;
+ Currency currency(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
@Cacheable
@RequestLine("GET /{ip}/threat")
- Threat threat(@Param("ip") String ip) throws IpdataException;
+ ThreatModel threat(@Param(value = "ip", encoded = true) String ip) throws IpdataException;
}
diff --git a/src/main/java/io/ipdata/client/service/IpdataService.java b/src/main/java/io/ipdata/client/service/IpdataService.java
index 6e13069..d83d522 100644
--- a/src/main/java/io/ipdata/client/service/IpdataService.java
+++ b/src/main/java/io/ipdata/client/service/IpdataService.java
@@ -2,16 +2,51 @@
import io.ipdata.client.error.IpdataException;
import io.ipdata.client.model.IpdataModel;
+
import java.util.List;
-@SuppressWarnings("RedundantThrows")
+/**
+ * Primary interface for accessing the ipdata.co API.
+ *
+ * Provides methods for looking up geolocation, threat intelligence, and other
+ * metadata for IP addresses. Supports single IP lookups, bulk lookups, and
+ * selective field retrieval.
+ *
+ * Also exposes single-field accessors (e.g. {@code getCountryName}, {@code getCity})
+ * inherited from {@link IpdataInternalSingleFieldClient}.
+ *
+ * @see io.ipdata.client.Ipdata#builder()
+ */
public interface IpdataService extends IpdataInternalSingleFieldClient {
+ /**
+ * Retrieves the full IP data model for the given IP address.
+ *
+ * @param ip an IPv4 or IPv6 address
+ * @return the full geolocation and metadata for the IP
+ * @throws IpdataException if the API call fails
+ */
IpdataModel ipdata(String ip) throws IpdataException;
- List bulkIpdata(List ips) throws IpdataException;
-
- IpdataModel[] bulkIpdataAsArray(List ips) throws IpdataException;
+ /**
+ * Retrieves IP data for multiple IP addresses in a single request.
+ *
+ * @param ips list of IPv4 or IPv6 addresses
+ * @return a list of IP data models, one per input address
+ * @throws IpdataException if the API call fails
+ */
+ List bulk(List ips) throws IpdataException;
+ /**
+ * Retrieves only the specified fields for the given IP address.
+ *
+ * Fields are sorted before querying to maximize cache hit rates when caching is enabled.
+ *
+ * @param ip an IPv4 or IPv6 address
+ * @param fields one or more fields to retrieve (e.g. {@code IpdataField.ASN}, {@code IpdataField.CURRENCY})
+ * @return a partial IP data model containing only the requested fields
+ * @throws IpdataException if the API call fails
+ * @throws IllegalArgumentException if no fields are specified
+ */
IpdataModel getFields(String ip, IpdataField>... fields) throws IpdataException;
}
diff --git a/src/main/java/io/ipdata/client/service/IpdataServiceBuilder.java b/src/main/java/io/ipdata/client/service/IpdataServiceBuilder.java
index 65d6cdf..f294cc2 100644
--- a/src/main/java/io/ipdata/client/service/IpdataServiceBuilder.java
+++ b/src/main/java/io/ipdata/client/service/IpdataServiceBuilder.java
@@ -1,7 +1,5 @@
package io.ipdata.client.service;
-import static com.fasterxml.jackson.databind.PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES;
-
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
@@ -11,19 +9,17 @@
import com.google.common.cache.CacheLoader;
import feign.Client;
import feign.Feign;
-import feign.httpclient.ApacheHttpClient;
import feign.jackson.JacksonDecoder;
import feign.jackson.JacksonEncoder;
-import io.ipdata.client.model.Asn;
-import io.ipdata.client.model.Currency;
-import io.ipdata.client.model.IpdataModel;
-import io.ipdata.client.model.Threat;
-import io.ipdata.client.model.TimeZone;
-import java.net.URL;
+import io.ipdata.client.model.*;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.net.URL;
+
+import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SNAKE_CASE;
+
@RequiredArgsConstructor(staticName = "of")
public class IpdataServiceBuilder {
@@ -35,7 +31,7 @@ public class IpdataServiceBuilder {
public IpdataService build() {
final ObjectMapper mapper = new ObjectMapper();
- mapper.setPropertyNamingStrategy(CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
+ mapper.setPropertyNamingStrategy(SNAKE_CASE);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
@@ -45,7 +41,7 @@ public IpdataService build() {
final ApiErrorDecoder apiErrorDecoder = new ApiErrorDecoder(mapper, customLogger);
final IpdataInternalClient client = Feign.builder()
- .client(httpClient == null ? new ApacheHttpClient() : httpClient)
+ .client(new Ipv6SafeClient(httpClient == null ? new Client.Default(null, null) : httpClient))
.decoder(new JacksonDecoder(mapper))
.encoder(new JacksonEncoder(mapper))
.requestInterceptor(keyRequestInterceptor)
@@ -53,7 +49,7 @@ public IpdataService build() {
.target(IpdataInternalClient.class, url.toString());
final IpdataInternalSingleFieldClient singleFieldClient = Feign.builder()
- .client(httpClient == null ? new ApacheHttpClient() : httpClient)
+ .client(new Ipv6SafeClient(httpClient == null ? new Client.Default(null, null) : httpClient))
.decoder(new FieldDecoder(mapper))
.encoder(new JacksonEncoder(mapper))
.requestInterceptor(keyRequestInterceptor)
@@ -91,9 +87,9 @@ public IpdataModel load(HashPair key) throws Exception {
CacheBuilder.newBuilder()
.expireAfterWrite(cacheConfig.timeout(), cacheConfig.unit())
.maximumSize(cacheConfig.maxSize())
- .build(new CacheLoader() {
+ .build(new CacheLoader() {
@Override
- public Asn load(String key) throws Exception {
+ public AsnModel load(String key) throws Exception {
return singleFieldClient.asn(key);
}
})
@@ -124,9 +120,9 @@ public Currency load(String key) throws Exception {
CacheBuilder.newBuilder()
.expireAfterWrite(cacheConfig.timeout(), cacheConfig.unit())
.maximumSize(cacheConfig.maxSize())
- .build(new CacheLoader() {
+ .build(new CacheLoader() {
@Override
- public Threat load(String key) throws Exception {
+ public ThreatModel load(String key) throws Exception {
return singleFieldClient.threat(key);
}
})
diff --git a/src/main/java/io/ipdata/client/service/IpdataServiceSupport.java b/src/main/java/io/ipdata/client/service/IpdataServiceSupport.java
index bac4378..7531ba8 100644
--- a/src/main/java/io/ipdata/client/service/IpdataServiceSupport.java
+++ b/src/main/java/io/ipdata/client/service/IpdataServiceSupport.java
@@ -3,13 +3,14 @@
import com.google.common.base.Joiner;
import io.ipdata.client.error.IpdataException;
import io.ipdata.client.model.IpdataModel;
-import java.util.Arrays;
-import java.util.List;
import lombok.Builder;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Delegate;
import lombok.extern.slf4j.Slf4j;
+import java.util.Arrays;
+import java.util.List;
+
@RequiredArgsConstructor
@Builder
@Slf4j
@@ -29,15 +30,10 @@ private IpdataInternalSingleFieldClient getApi() {
return singleFieldClient;
}
- @Override
- public IpdataModel[] bulkIpdataAsArray(List ips) throws IpdataException {
- return bulkIpdata(ips).toArray(new IpdataModel[0]);
- }
-
@Override
public IpdataModel getFields(String ip, IpdataField>... fields) throws IpdataException {
if (fields.length == 0) {
- return null;
+ throw new IllegalArgumentException("At least one field must be specified");
}
//sorting here to improve the likelihood of a cache hit, otherwise a permutation of the same
//array would result into a different cache key, and thus a cache miss
diff --git a/src/main/java/io/ipdata/client/service/Ipv6SafeClient.java b/src/main/java/io/ipdata/client/service/Ipv6SafeClient.java
new file mode 100644
index 0000000..f8f6f38
--- /dev/null
+++ b/src/main/java/io/ipdata/client/service/Ipv6SafeClient.java
@@ -0,0 +1,44 @@
+package io.ipdata.client.service;
+
+import feign.Client;
+import feign.Request;
+import feign.Response;
+
+import java.io.IOException;
+
+/**
+ * A Feign Client wrapper that prevents percent-encoding of colons in the request path.
+ *
+ * Feign's template engine may percent-encode colons in path parameters (e.g., IPv6 addresses),
+ * converting {@code 2001:4860:4860::8888} to {@code 2001%3A4860%3A4860%3A%3A8888}.
+ * Colons are valid in URI path segments per RFC 3986 section 3.3, so this wrapper
+ * decodes them before forwarding to the underlying HTTP client.
+ *
+ * @see Issue #10
+ */
+class Ipv6SafeClient implements Client {
+
+ private final Client delegate;
+
+ Ipv6SafeClient(Client delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Response execute(Request request, Request.Options options) throws IOException {
+ String url = request.url();
+ int queryIndex = url.indexOf('?');
+ String path = queryIndex >= 0 ? url.substring(0, queryIndex) : url;
+
+ if (path.contains("%3A") || path.contains("%3a")) {
+ String fixedPath = path.replace("%3A", ":").replace("%3a", ":");
+ String query = queryIndex >= 0 ? url.substring(queryIndex) : "";
+ String fixedUrl = fixedPath + query;
+ request = Request.create(
+ request.httpMethod(), fixedUrl, request.headers(),
+ request.body(), request.charset()
+ );
+ }
+ return delegate.execute(request, options);
+ }
+}
diff --git a/src/main/resources/VERSION b/src/main/resources/io/ipdata/client/VERSION
similarity index 100%
rename from src/main/resources/VERSION
rename to src/main/resources/io/ipdata/client/VERSION
diff --git a/src/test/java/io/ipdata/client/AsnTest.java b/src/test/java/io/ipdata/client/AsnTest.java
new file mode 100644
index 0000000..5ae0102
--- /dev/null
+++ b/src/test/java/io/ipdata/client/AsnTest.java
@@ -0,0 +1,61 @@
+package io.ipdata.client;
+
+import feign.httpclient.ApacheHttpClient;
+import io.ipdata.client.error.IpdataException;
+import io.ipdata.client.model.AsnModel;
+import io.ipdata.client.service.IpdataService;
+import lombok.SneakyThrows;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(Parameterized.class)
+public class AsnTest {
+
+ private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MockIpdataServer.getInstance().getUrl());
+
+ @Parameterized.Parameter
+ public TestFixture fixture;
+
+ @Test
+ @SneakyThrows
+ public void testASN() {
+ IpdataService ipdataService = fixture.service();
+ AsnModel asn = ipdataService.asn(fixture.target());
+ assertNotNull(asn.type()); /* See: https://github.com/ipdata/java/issues/2 */
+ String actual = TEST_CONTEXT.mapper().writeValueAsString(asn);
+ String expected = TEST_CONTEXT.get("/"+fixture.target()+"/asn", null);
+ TEST_CONTEXT.assertEqualJson(actual, expected);
+ if (ipdataService == TEST_CONTEXT.cachingIpdataService()) {
+ //value will be returned from cache now
+ asn = ipdataService.asn(fixture.target());
+ assertNotNull(asn.type());
+ actual = TEST_CONTEXT.mapper().writeValueAsString(asn);
+ TEST_CONTEXT.assertEqualJson(actual, expected);
+ }
+ }
+
+ @SneakyThrows
+ @Test(expected = IpdataException.class)
+ public void testAsnError() {
+ IpdataService serviceWithInvalidKey = Ipdata.builder().url(TEST_CONTEXT.url())
+ .key("THIS_IS_AN_INVALID_KEY")
+ .withDefaultCache()
+ .feignClient(new ApacheHttpClient(HttpClientBuilder.create()
+ .setConnectionTimeToLive(10, TimeUnit.SECONDS)
+ .build())
+ ).get();
+ serviceWithInvalidKey.asn(fixture.target());
+ }
+
+ @Parameterized.Parameters
+ public static Iterable data() {
+ return TEST_CONTEXT.fixtures();
+ }
+
+}
diff --git a/src/test/java/io/ipdata/client/BulkTest.java b/src/test/java/io/ipdata/client/BulkTest.java
new file mode 100644
index 0000000..916def4
--- /dev/null
+++ b/src/test/java/io/ipdata/client/BulkTest.java
@@ -0,0 +1,48 @@
+package io.ipdata.client;
+
+import io.ipdata.client.model.IpdataModel;
+import io.ipdata.client.service.IpdataService;
+import lombok.SneakyThrows;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static java.util.Arrays.asList;
+
+@RunWith(Parameterized.class)
+public class BulkTest {
+
+ private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MockIpdataServer.getInstance().getUrl());
+
+ @Parameterized.Parameter
+ public IpdataService ipdataService;
+
+ @SneakyThrows
+ @Test
+ public void testBulkResponse() {
+ List ipdataModels = ipdataService.bulk(Arrays.asList("8.8.8.8", "1.1.1.1"));
+ String actual = TEST_CONTEXT.mapper().writeValueAsString(ipdataModels);
+ String expected = TEST_CONTEXT.post("/bulk", "[\"8.8.8.8\",\"1.1.1.1\"]", null);
+ expected = TEST_CONTEXT.mapper().writeValueAsString(TEST_CONTEXT.mapper().readValue(expected, IpdataModel[].class));
+ TEST_CONTEXT.assertEqualJson(expected, actual, TEST_CONTEXT.configuration().whenIgnoringPaths("[0].time_zone.current_time", "[1].time_zone.current_time", "[0].count", "[1].count"));
+ }
+
+ @SneakyThrows
+ @Test
+ public void testBulkResponseIpv6() {
+ List ipdataModels = ipdataService.bulk(Arrays.asList("2001:4860:4860::8888", "2001:4860:4860::8844"));
+ String actual = TEST_CONTEXT.mapper().writeValueAsString(ipdataModels);
+ String expected = TEST_CONTEXT.post("/bulk", "[\"2001:4860:4860::8888\",\"2001:4860:4860::8844\"]", null);
+ expected = TEST_CONTEXT.mapper().writeValueAsString(TEST_CONTEXT.mapper().readValue(expected, IpdataModel[].class));
+ TEST_CONTEXT.assertEqualJson(expected, actual, TEST_CONTEXT.configuration().whenIgnoringPaths("[0].time_zone.current_time", "[1].time_zone.current_time", "[0].count", "[1].count"));
+ }
+
+ @Parameterized.Parameters
+ public static Iterable data() {
+ return asList(TEST_CONTEXT.ipdataService(), TEST_CONTEXT.cachingIpdataService());
+ }
+
+}
diff --git a/src/test/java/io/ipdata/client/CurrencyTest.java b/src/test/java/io/ipdata/client/CurrencyTest.java
new file mode 100644
index 0000000..78ea1c6
--- /dev/null
+++ b/src/test/java/io/ipdata/client/CurrencyTest.java
@@ -0,0 +1,57 @@
+package io.ipdata.client;
+
+import feign.httpclient.ApacheHttpClient;
+import io.ipdata.client.error.IpdataException;
+import io.ipdata.client.model.Currency;
+import io.ipdata.client.service.IpdataService;
+import lombok.SneakyThrows;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.concurrent.TimeUnit;
+
+@RunWith(Parameterized.class)
+public class CurrencyTest {
+
+ private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MockIpdataServer.getInstance().getUrl());
+
+ @Parameterized.Parameter
+ public TestFixture fixture;
+
+ @Test
+ @SneakyThrows
+ public void testCurrency() {
+ IpdataService ipdataService = fixture.service();
+ Currency currency = ipdataService.currency(fixture.target());
+ String actual = TEST_CONTEXT.mapper().writeValueAsString(currency);
+ String expected = TEST_CONTEXT.get("/"+fixture.target()+"/currency", null);
+ TEST_CONTEXT.assertEqualJson(actual, expected);
+ if (ipdataService == TEST_CONTEXT.cachingIpdataService()) {
+ //value will be returned from cache now
+ currency = ipdataService.currency(fixture.target());
+ actual = TEST_CONTEXT.mapper().writeValueAsString(currency);
+ TEST_CONTEXT.assertEqualJson(actual, expected);
+ }
+ }
+
+ @SneakyThrows
+ @Test(expected = IpdataException.class)
+ public void testCurrencyError() {
+ IpdataService serviceWithInvalidKey = Ipdata.builder().url(TEST_CONTEXT.url())
+ .key("THIS_IS_AN_INVALID_KEY")
+ .withDefaultCache()
+ .feignClient(new ApacheHttpClient(HttpClientBuilder.create()
+ .setConnectionTimeToLive(10, TimeUnit.SECONDS)
+ .build())
+ ).get();
+ serviceWithInvalidKey.currency(fixture.target());
+ }
+
+ @Parameterized.Parameters
+ public static Iterable data() {
+ return TEST_CONTEXT.fixtures();
+ }
+
+}
diff --git a/src/test/java/io/ipdata/client/EdgeCaseTest.java b/src/test/java/io/ipdata/client/EdgeCaseTest.java
new file mode 100644
index 0000000..1874198
--- /dev/null
+++ b/src/test/java/io/ipdata/client/EdgeCaseTest.java
@@ -0,0 +1,73 @@
+package io.ipdata.client;
+
+import io.ipdata.client.error.IpdataException;
+import io.ipdata.client.error.RemoteIpdataException;
+import io.ipdata.client.model.IpdataModel;
+import io.ipdata.client.service.IpdataService;
+import lombok.SneakyThrows;
+import org.junit.Assert;
+import org.junit.Test;
+
+import static io.ipdata.client.service.IpdataField.ASN;
+import static io.ipdata.client.service.IpdataField.CURRENCY;
+
+public class EdgeCaseTest {
+
+ private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MockIpdataServer.getInstance().getUrl());
+
+ @Test(expected = IllegalArgumentException.class)
+ @SneakyThrows
+ public void testGetFieldsWithNoFieldsThrows() {
+ IpdataService service = TEST_CONTEXT.ipdataService();
+ service.getFields("8.8.8.8");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ @SneakyThrows
+ public void testGetFieldsWithNoFieldsThrowsCaching() {
+ IpdataService service = TEST_CONTEXT.cachingIpdataService();
+ service.getFields("8.8.8.8");
+ }
+
+ @Test
+ @SneakyThrows
+ public void testInvalidKeyReturnsRemoteException() {
+ IpdataService service = Ipdata.builder()
+ .url(TEST_CONTEXT.url())
+ .key("INVALID_KEY")
+ .noCache()
+ .get();
+ try {
+ service.ipdata("8.8.8.8");
+ Assert.fail("Expected RemoteIpdataException");
+ } catch (RemoteIpdataException e) {
+ Assert.assertEquals(401, e.getStatus());
+ Assert.assertNotNull(e.getMessage());
+ }
+ }
+
+ @Test
+ @SneakyThrows
+ public void testCachedInvalidKeyUnwrapsException() {
+ IpdataService service = Ipdata.builder()
+ .url(TEST_CONTEXT.url())
+ .key("INVALID_KEY")
+ .withDefaultCache()
+ .get();
+ try {
+ service.ipdata("8.8.8.8");
+ Assert.fail("Expected RemoteIpdataException");
+ } catch (RemoteIpdataException e) {
+ Assert.assertEquals(401, e.getStatus());
+ }
+ }
+
+ @Test
+ @SneakyThrows
+ public void testGetFieldsReturnsSelectedFields() {
+ IpdataService service = TEST_CONTEXT.ipdataService();
+ IpdataModel model = service.getFields("8.8.8.8", ASN, CURRENCY);
+ Assert.assertNotNull(model.asn());
+ Assert.assertNotNull(model.currency());
+ }
+}
diff --git a/src/test/java/io/ipdata/client/FullModelTest.java b/src/test/java/io/ipdata/client/FullModelTest.java
new file mode 100644
index 0000000..55424e8
--- /dev/null
+++ b/src/test/java/io/ipdata/client/FullModelTest.java
@@ -0,0 +1,69 @@
+package io.ipdata.client;
+
+import io.ipdata.client.error.IpdataException;
+import io.ipdata.client.error.RemoteIpdataException;
+import io.ipdata.client.model.IpdataModel;
+import io.ipdata.client.service.IpdataService;
+import lombok.SneakyThrows;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+
+@RunWith(Parameterized.class)
+public class FullModelTest {
+
+ private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MockIpdataServer.getInstance().getUrl());
+
+ @Parameterized.Parameter
+ public TestFixture fixture;
+
+ @Test
+ @SneakyThrows
+ public void testFullResponse() {
+ IpdataService ipdataService = fixture.service();
+ IpdataModel ipdataModel = ipdataService.ipdata(fixture.target());
+ String actual = TEST_CONTEXT.mapper().writeValueAsString(ipdataModel);
+ String expected = TEST_CONTEXT.get("/"+fixture.target(), null);
+ TEST_CONTEXT.assertEqualJson(actual, expected, TEST_CONTEXT.configuration().whenIgnoringPaths("time_zone.current_time"));
+ if (ipdataService == TEST_CONTEXT.cachingIpdataService()) {
+ //value will be returned from cache now
+ ipdataModel = ipdataService.ipdata(fixture.target());
+ actual = TEST_CONTEXT.mapper().writeValueAsString(ipdataModel);
+ TEST_CONTEXT.assertEqualJson(actual, expected, TEST_CONTEXT.configuration().whenIgnoringPaths("time_zone.current_time"));
+ }
+ }
+
+
+ @SneakyThrows
+ @Test
+ public void testSingleFields() {
+ IpdataService ipdataService = fixture.service();
+ String field = ipdataService.getCountryName(fixture.target());
+ String expected = TEST_CONTEXT.get("/" + fixture.target() + "/country_name", null);
+ Assert.assertEquals(expected, field);
+ }
+
+
+ @SneakyThrows
+ @Test
+ public void testError() {
+ IpdataService serviceWithInvalidKey = Ipdata.builder().url(TEST_CONTEXT.url())
+ .key("THIS_IS_AN_INVALID_KEY")
+ .noCache()
+ .get();
+ try {
+ serviceWithInvalidKey.ipdata(fixture.target());
+ Assert.fail("Expected RemoteIpdataException");
+ } catch (RemoteIpdataException e) {
+ Assert.assertEquals(401, e.getStatus());
+ }
+ }
+
+ @Parameterized.Parameters
+ public static Iterable data() {
+ return TEST_CONTEXT.fixtures();
+ }
+
+}
diff --git a/src/test/java/io/ipdata/client/IpdataFunctionalTest.java b/src/test/java/io/ipdata/client/IpdataFunctionalTest.java
deleted file mode 100644
index 94ffc4d..0000000
--- a/src/test/java/io/ipdata/client/IpdataFunctionalTest.java
+++ /dev/null
@@ -1,184 +0,0 @@
-package io.ipdata.client;
-
-import static com.fasterxml.jackson.databind.PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES;
-import static io.ipdata.client.TestUtils.KEY;
-import static org.junit.runners.Parameterized.Parameters;
-import static org.skyscreamer.jsonassert.JSONAssert.assertEquals;
-
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.google.common.collect.ImmutableMap;
-import feign.httpclient.ApacheHttpClient;
-import io.ipdata.client.error.RateLimitException;
-import io.ipdata.client.model.Asn;
-import io.ipdata.client.model.Currency;
-import io.ipdata.client.model.IpdataModel;
-import io.ipdata.client.model.Threat;
-import io.ipdata.client.service.IpdataField;
-import io.ipdata.client.service.IpdataService;
-import java.net.URL;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import lombok.SneakyThrows;
-import org.apache.http.client.HttpClient;
-import org.apache.http.conn.ssl.NoopHostnameVerifier;
-import org.apache.http.impl.client.HttpClientBuilder;
-import org.junit.Assert;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-@RunWith(Parameterized.class)
-public class IpdataFunctionalTest {
-
- private static IpdataService IPDATA_SERVICE;
- private static IpdataService CACHING_IPDATA_SERVICE;
- private static ObjectMapper MAPPER;
- private static HttpClient HTTP_CLIENT;
-
- @Parameterized.Parameter
- public IpdataService ipdataService;
-
- @Test
- @SneakyThrows
- public void testFullresponse() {
- IpdataModel ipdataModel = ipdataService.ipdata("8.8.8.8");
- String serialized = MAPPER.writeValueAsString(ipdataModel);
- String expected = TestUtils.get(HTTP_CLIENT, "/8.8.8.8", null);
- expected = MAPPER.writeValueAsString(MAPPER.readValue(expected, IpdataModel.class));
- assertEquals(serialized, expected, false);
- if (ipdataService == CACHING_IPDATA_SERVICE) {
- //value will be returned from cache now
- ipdataModel = ipdataService.ipdata("8.8.8.8");
- serialized = MAPPER.writeValueAsString(ipdataModel);
- assertEquals(serialized, expected, false);
- }
- }
-
- @Test
- @SneakyThrows
- public void testASN() {
- Asn asn = ipdataService.asn("8.8.8.8");
- String serialized = MAPPER.writeValueAsString(asn);
- String expected = TestUtils.get(HTTP_CLIENT, "/8.8.8.8/asn", null);
- expected = MAPPER.writeValueAsString(MAPPER.readValue(expected, Asn.class));
- assertEquals(serialized, expected, false);
- if (ipdataService == CACHING_IPDATA_SERVICE) {
- //value will be returned from cache now
- asn = ipdataService.asn("8.8.8.8");
- serialized = MAPPER.writeValueAsString(asn);
- assertEquals(serialized, expected, false);
- }
- }
-
- @Test
- @SneakyThrows
- public void testThreat() {
- Threat threat = ipdataService.threat("8.8.8.8");
- String serialized = MAPPER.writeValueAsString(threat);
- String expected = TestUtils.get(HTTP_CLIENT, "/8.8.8.8/threat", null);
- expected = MAPPER.writeValueAsString(MAPPER.readValue(expected, Threat.class));
- assertEquals(serialized, expected, false);
- if (ipdataService == CACHING_IPDATA_SERVICE) {
- //value will be returned from cache now
- threat = ipdataService.threat("8.8.8.8");
- serialized = MAPPER.writeValueAsString(threat);
- assertEquals(serialized, expected, false);
- }
- }
-
- @Test
- @SneakyThrows
- public void testCurrency() {
- Currency currency = ipdataService.currency("8.8.8.8");
- String serialized = MAPPER.writeValueAsString(currency);
- String expected = TestUtils.get(HTTP_CLIENT, "/8.8.8.8/currency", null);
- expected = MAPPER.writeValueAsString(MAPPER.readValue(expected, Currency.class));
- assertEquals(serialized, expected, false);
- if (ipdataService == CACHING_IPDATA_SERVICE) {
- //value will be returned from cache now
- currency = ipdataService.currency("8.8.8.8");
- serialized = MAPPER.writeValueAsString(currency);
- assertEquals(serialized, expected, false);
- }
- }
-
- @Test
- @SneakyThrows
- public void testFieldSelection() {
- IpdataModel ipdataModel = ipdataService.getFields("8.8.8.8", IpdataField.ASN, IpdataField.CURRENCY);
- String serialized = MAPPER.writeValueAsString(ipdataModel);
- String expected = TestUtils.get(HTTP_CLIENT, "/8.8.8.8", ImmutableMap.of("fields", "asn,currency"));
- expected = MAPPER.writeValueAsString(MAPPER.readValue(expected, IpdataModel.class));
- assertEquals(serialized, expected, false);
- Assert.assertNull(ipdataModel.threat());
- if (ipdataService == CACHING_IPDATA_SERVICE) {
- //value will be returned from cache now
- ipdataModel = ipdataService.getFields("8.8.8.8", IpdataField.ASN, IpdataField.CURRENCY);
- serialized = MAPPER.writeValueAsString(ipdataModel);
- assertEquals(serialized, expected, false);
- }
- }
-
- @SneakyThrows
- @Test
- public void testSingleFields() {
- String field = ipdataService.getCountryName("8.8.8.8");
- String expected = TestUtils.get(HTTP_CLIENT, "/8.8.8.8/country_name", null);
- Assert.assertEquals(field, expected);
- }
-
- @SneakyThrows
- @Test
- public void testBulkResponse() {
- List ipdataModels = ipdataService.bulkIpdata(Arrays.asList("8.8.8.8", "1.1.1.1"));
- String serialized = MAPPER.writeValueAsString(ipdataModels);
- String expected = TestUtils.post(HTTP_CLIENT, "/bulk", "[\"8.8.8.8\",\"1.1.1.1\"]", null);
- expected = MAPPER.writeValueAsString(MAPPER.readValue(expected, IpdataModel[].class));
- assertEquals(serialized, expected, false);
- }
-
- @SneakyThrows
- @Test(expected = RateLimitException.class)
- public void testError() {
- URL url = new URL("https://api.ipdata.co");
- IpdataService serviceWithInvalidKey = Ipdata.builder().url(url)
- .key("THIS_IS_AN_INVALID_KEY")
- .feignClient(new ApacheHttpClient(HttpClientBuilder.create()
- .setSSLHostnameVerifier(new NoopHostnameVerifier()).setConnectionTimeToLive(10, TimeUnit.SECONDS)
- .build())).get();
- serviceWithInvalidKey.ipdata("8.8.8.8");
- }
-
- @Parameters
- public static Iterable data() {
- init();
- return Arrays.asList(IPDATA_SERVICE, CACHING_IPDATA_SERVICE);
- }
-
- @SneakyThrows
- public static void init() {
- URL url = new URL("https://api.ipdata.co");
- MAPPER = new ObjectMapper();
- MAPPER.setPropertyNamingStrategy(CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
- MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
- MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
- MAPPER.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
- HTTP_CLIENT = HttpClientBuilder.create().setSSLHostnameVerifier(new NoopHostnameVerifier())
- .build();
- IPDATA_SERVICE = Ipdata.builder().url(url).key(KEY)
- .feignClient(new ApacheHttpClient(HttpClientBuilder.create()
- .setSSLHostnameVerifier(new NoopHostnameVerifier())
- .build())).get();
- CACHING_IPDATA_SERVICE = Ipdata.builder().url(url)
- .feignClient(new ApacheHttpClient(HttpClientBuilder.create()
- .setSSLHostnameVerifier(new NoopHostnameVerifier())
- .build()))
- .withDefaultCache().key(KEY).get();
- }
-
-}
diff --git a/src/test/java/io/ipdata/client/Ipv6EncodingTest.java b/src/test/java/io/ipdata/client/Ipv6EncodingTest.java
new file mode 100644
index 0000000..fddbac7
--- /dev/null
+++ b/src/test/java/io/ipdata/client/Ipv6EncodingTest.java
@@ -0,0 +1,36 @@
+package io.ipdata.client;
+
+import io.ipdata.client.model.IpdataModel;
+import io.ipdata.client.service.IpdataService;
+import lombok.SneakyThrows;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Regression test for https://github.com/ipdata/java/issues/10
+ * Verifies that IPv6 colons are not percent-encoded (%3A) in HTTP requests.
+ */
+public class Ipv6EncodingTest {
+
+ private static final MockIpdataServer MOCK = MockIpdataServer.getInstance();
+ private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MOCK.getUrl());
+
+ @Test
+ @SneakyThrows
+ public void testIpv6ColonsAreNotEncoded() {
+ String ipv6 = "2001:4860:4860::8888";
+ IpdataService service = TEST_CONTEXT.ipdataService();
+ IpdataModel model = service.ipdata(ipv6);
+ Assert.assertNotNull(model);
+
+ String rawPath = MOCK.getLastRawPath();
+ Assert.assertFalse(
+ "IPv6 colons should not be percent-encoded in the request path, but got: " + rawPath,
+ rawPath.contains("%3A")
+ );
+ Assert.assertTrue(
+ "Request path should contain the IPv6 address with literal colons",
+ rawPath.contains(ipv6)
+ );
+ }
+}
diff --git a/src/test/java/io/ipdata/client/MappingTest.java b/src/test/java/io/ipdata/client/MappingTest.java
new file mode 100644
index 0000000..0d5c5fd
--- /dev/null
+++ b/src/test/java/io/ipdata/client/MappingTest.java
@@ -0,0 +1,23 @@
+package io.ipdata.client;
+
+import com.google.common.io.CharStreams;
+import io.ipdata.client.model.IpdataModel;
+import lombok.SneakyThrows;
+import net.javacrumbs.jsonunit.core.Configuration;
+import net.javacrumbs.jsonunit.core.Option;
+import org.junit.Test;
+
+import java.io.InputStreamReader;
+
+public class MappingTest {
+
+ private static final TestContext TEST_CONTEXT = new TestContext("dummy", "https://api.ipdata.co");
+
+ @Test @SneakyThrows
+ public void testMapping(){
+ String actual = TEST_CONTEXT.mapper().writeValueAsString(TEST_CONTEXT.mapper().readValue(getClass().getResourceAsStream("fixture.json"), IpdataModel.class));
+ String expected = CharStreams.toString(new InputStreamReader(getClass().getResourceAsStream("fixture.json")));
+ TEST_CONTEXT.assertEqualJson(actual, expected, Configuration.empty().withOptions(Option.TREATING_NULL_AS_ABSENT).whenIgnoringPaths("languages[0].rtl"));
+ }
+
+}
diff --git a/src/test/java/io/ipdata/client/MockIpdataServer.java b/src/test/java/io/ipdata/client/MockIpdataServer.java
new file mode 100644
index 0000000..527f7c1
--- /dev/null
+++ b/src/test/java/io/ipdata/client/MockIpdataServer.java
@@ -0,0 +1,207 @@
+package io.ipdata.client;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.io.CharStreams;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.InetSocketAddress;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MockIpdataServer {
+
+ public static final String API_KEY = "test-api-key";
+
+ private static MockIpdataServer instance;
+
+ private final HttpServer server;
+ private final String url;
+ private final Map fixtures = new HashMap<>();
+ private final ObjectMapper mapper = new ObjectMapper();
+ private volatile String lastRawPath;
+
+ private MockIpdataServer() {
+ try {
+ server = HttpServer.create(new InetSocketAddress(0), 0);
+ int port = server.getAddress().getPort();
+ url = "http://localhost:" + port;
+ loadFixtures();
+ server.createContext("/", this::handleRequest);
+ server.start();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to start mock server", e);
+ }
+ }
+
+ public static synchronized MockIpdataServer getInstance() {
+ if (instance == null) {
+ instance = new MockIpdataServer();
+ }
+ return instance;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public String getLastRawPath() {
+ return lastRawPath;
+ }
+
+ private void loadFixtures() {
+ String[] ips = {"8.8.8.8", "2001:4860:4860::8888", "1.1.1.1", "2001:4860:4860::8844", "41.128.21.123"};
+ for (String ip : ips) {
+ String resourceName = "fixtures/" + ip.replace(":", "-") + ".json";
+ try (InputStream is = getClass().getResourceAsStream(resourceName)) {
+ if (is != null) {
+ fixtures.put(ip, mapper.readTree(is));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load fixture: " + resourceName, e);
+ }
+ }
+ }
+
+ private void handleRequest(HttpExchange exchange) throws IOException {
+ try {
+ String method = exchange.getRequestMethod();
+ String path = exchange.getRequestURI().getPath();
+ lastRawPath = exchange.getRequestURI().getRawPath();
+ String rawQuery = exchange.getRequestURI().getRawQuery();
+ Map params = parseQuery(rawQuery);
+
+ String apiKey = params.get("api-key");
+ if (!API_KEY.equals(apiKey)) {
+ sendError(exchange, 401, "Invalid API key");
+ return;
+ }
+
+ if ("POST".equals(method) && "/bulk".equals(path)) {
+ handleBulk(exchange);
+ return;
+ }
+
+ if ("GET".equals(method)) {
+ handleGet(exchange, path, params);
+ return;
+ }
+
+ sendError(exchange, 404, "Not found");
+ } catch (Exception e) {
+ sendError(exchange, 500, e.getMessage());
+ }
+ }
+
+ private void handleGet(HttpExchange exchange, String path, Map params) throws IOException {
+ String trimmed = path.startsWith("/") ? path.substring(1) : path;
+
+ String ip = null;
+ String field = null;
+
+ // Match against known IPs (longest first to avoid prefix conflicts)
+ List sortedIps = new ArrayList<>(fixtures.keySet());
+ sortedIps.sort((a, b) -> b.length() - a.length());
+
+ for (String knownIp : sortedIps) {
+ if (trimmed.equals(knownIp)) {
+ ip = knownIp;
+ break;
+ }
+ if (trimmed.startsWith(knownIp + "/")) {
+ ip = knownIp;
+ field = trimmed.substring(knownIp.length() + 1);
+ break;
+ }
+ }
+
+ if (ip == null) {
+ sendError(exchange, 404, "IP not found");
+ return;
+ }
+
+ JsonNode fixture = fixtures.get(ip);
+
+ if (field != null) {
+ // Sub-field request: GET /{ip}/{field}
+ JsonNode fieldNode = fixture.get(field);
+ if (fieldNode == null) {
+ sendError(exchange, 404, "Field not found: " + field);
+ return;
+ }
+ if (fieldNode.isTextual()) {
+ sendResponse(exchange, 200, fieldNode.asText());
+ } else {
+ sendResponse(exchange, 200, mapper.writeValueAsString(fieldNode));
+ }
+ } else if (params.containsKey("fields")) {
+ // Selected fields request: GET /{ip}?fields=a,b
+ String fieldsStr = params.get("fields");
+ String[] fields = fieldsStr.split(",");
+ ObjectNode result = mapper.createObjectNode();
+ for (String f : fields) {
+ JsonNode fieldNode = fixture.get(f.trim());
+ if (fieldNode != null) {
+ result.set(f.trim(), fieldNode);
+ }
+ }
+ sendResponse(exchange, 200, mapper.writeValueAsString(result));
+ } else {
+ // Full model request: GET /{ip}
+ sendResponse(exchange, 200, mapper.writeValueAsString(fixture));
+ }
+ }
+
+ private void handleBulk(HttpExchange exchange) throws IOException {
+ String body = CharStreams.toString(new InputStreamReader(exchange.getRequestBody()));
+ String[] ips = mapper.readValue(body, String[].class);
+ ArrayNode result = mapper.createArrayNode();
+ for (String ip : ips) {
+ JsonNode fixture = fixtures.get(ip);
+ if (fixture != null) {
+ result.add(fixture);
+ }
+ }
+ sendResponse(exchange, 200, mapper.writeValueAsString(result));
+ }
+
+ private void sendResponse(HttpExchange exchange, int status, String body) throws IOException {
+ byte[] bytes = body.getBytes("UTF-8");
+ exchange.getResponseHeaders().set("Content-Type", "application/json");
+ exchange.sendResponseHeaders(status, bytes.length);
+ exchange.getResponseBody().write(bytes);
+ exchange.getResponseBody().close();
+ }
+
+ private void sendError(HttpExchange exchange, int status, String message) throws IOException {
+ String body = "{\"message\":\"" + message.replace("\"", "\\\"") + "\"}";
+ sendResponse(exchange, status, body);
+ }
+
+ private Map parseQuery(String rawQuery) {
+ Map params = new HashMap<>();
+ if (rawQuery != null) {
+ for (String param : rawQuery.split("&")) {
+ String[] pair = param.split("=", 2);
+ if (pair.length == 2) {
+ try {
+ params.put(URLDecoder.decode(pair[0], "UTF-8"), URLDecoder.decode(pair[1], "UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ params.put(pair[0], pair[1]);
+ }
+ }
+ }
+ }
+ return params;
+ }
+}
diff --git a/src/test/java/io/ipdata/client/MultipleFieldsSelectionTest.java b/src/test/java/io/ipdata/client/MultipleFieldsSelectionTest.java
new file mode 100644
index 0000000..41e1381
--- /dev/null
+++ b/src/test/java/io/ipdata/client/MultipleFieldsSelectionTest.java
@@ -0,0 +1,42 @@
+package io.ipdata.client;
+
+import com.google.common.collect.ImmutableMap;
+import io.ipdata.client.model.IpdataModel;
+import io.ipdata.client.service.IpdataService;
+import lombok.SneakyThrows;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static io.ipdata.client.service.IpdataField.*;
+import static java.util.Arrays.asList;
+
+@RunWith(Parameterized.class)
+public class MultipleFieldsSelectionTest {
+
+ private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MockIpdataServer.getInstance().getUrl());
+
+ @Parameterized.Parameter
+ public IpdataService ipdataService;
+
+ @Test
+ @SneakyThrows
+ public void testMultipleFieldsSelection() {
+ IpdataModel ipdataModel = ipdataService.getFields("41.128.21.123", ASN, CURRENCY);
+ String actual = TEST_CONTEXT.mapper().writeValueAsString(ipdataModel);
+ String expected = TEST_CONTEXT.get("/41.128.21.123", ImmutableMap.of("fields", "asn,currency"));
+ TEST_CONTEXT.assertEqualJson(actual, expected, TEST_CONTEXT.configuration());
+ if (ipdataService == TEST_CONTEXT.cachingIpdataService()) {
+ //value will be returned from cache now
+ ipdataModel = ipdataService.getFields("41.128.21.123", ASN, CURRENCY, CARRIER);
+ actual = TEST_CONTEXT.mapper().writeValueAsString(ipdataModel);
+ TEST_CONTEXT.assertEqualJson(actual, expected, TEST_CONTEXT.configuration());
+ }
+ }
+
+ @Parameterized.Parameters
+ public static Iterable data() {
+ return asList(TEST_CONTEXT.ipdataService(), TEST_CONTEXT.cachingIpdataService());
+ }
+
+}
diff --git a/src/test/java/io/ipdata/client/TestContext.java b/src/test/java/io/ipdata/client/TestContext.java
new file mode 100644
index 0000000..c367d3b
--- /dev/null
+++ b/src/test/java/io/ipdata/client/TestContext.java
@@ -0,0 +1,115 @@
+package io.ipdata.client;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.io.CharStreams;
+import io.ipdata.client.service.IpdataService;
+import lombok.Getter;
+import lombok.SneakyThrows;
+import lombok.experimental.Accessors;
+import net.javacrumbs.jsonunit.core.Configuration;
+import net.javacrumbs.jsonunit.core.Option;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.HttpClientBuilder;
+
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.Map;
+
+import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SNAKE_CASE;
+import static java.lang.System.getenv;
+import static java.util.Arrays.asList;
+import static net.javacrumbs.jsonunit.JsonAssert.assertJsonEquals;
+
+@Getter
+@Accessors(fluent = true)
+public class TestContext {
+
+ static final String DEFAULT_KEY_ENV_VAR = "IPDATACO_KEY";
+
+ private final ObjectMapper mapper;
+ private final HttpClient httpClient;
+ private final IpdataService ipdataService;
+ private final IpdataService cachingIpdataService;
+ private final String key;
+ private final URL url;
+ private final Configuration configuration = Configuration.empty().withOptions(Option.IGNORING_EXTRA_FIELDS, Option.TREATING_NULL_AS_ABSENT);
+
+ public TestContext(String url) {
+ this(getenv(DEFAULT_KEY_ENV_VAR), url);
+ }
+
+ @SneakyThrows
+ public TestContext(String key, String url) {
+ this.key = key;
+ this.url = new URL(url);
+ mapper = new ObjectMapper();
+ mapper.setPropertyNamingStrategy(SNAKE_CASE);
+ mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
+ mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
+ httpClient = HttpClientBuilder.create().build();
+ ipdataService = Ipdata.builder().url(this.url).key(this.key)
+ .noCache()
+ .get();
+ cachingIpdataService = Ipdata.builder().url(this.url)
+ .withDefaultCache().key(key).get();
+ }
+
+ void assertEqualJson(String actual, String expected) {
+ assertJsonEquals(expected, actual, configuration);
+ }
+
+ void assertEqualJson(String actual, String expected, Configuration configuration) {
+ assertJsonEquals(expected, actual, configuration);
+ }
+
+ /*
+ Used to get responses of a raw http call, without logical proxying
+ */
+ @SneakyThrows
+ String get(String path, Map params) {
+ URIBuilder builder = new URIBuilder(url.toString() + path);
+ builder.setParameter("api-key", this.key);
+ if (params != null) {
+ for (Map.Entry entry : params.entrySet()) {
+ builder.setParameter(entry.getKey(), entry.getValue());
+ }
+ }
+ HttpGet httpget = new HttpGet(builder.build());
+ HttpResponse response = httpClient.execute(httpget);
+ return CharStreams.toString(new InputStreamReader(response.getEntity().getContent()));
+ }
+
+ @SneakyThrows
+ String post(String path, String content, Map params) {
+ URIBuilder builder = new URIBuilder(url.toString() + path);
+ builder.setParameter("api-key", this.key);
+ if (params != null) {
+ for (Map.Entry entry : params.entrySet()) {
+ builder.setParameter(entry.getKey(), entry.getValue());
+ }
+ }
+ HttpPost httpPost = new HttpPost(builder.build());
+ httpPost.setEntity(new StringEntity(content));
+ HttpResponse response = httpClient.execute(httpPost);
+ return CharStreams.toString(new InputStreamReader(response.getEntity().getContent()));
+ }
+
+
+ public Iterable fixtures() {
+ return asList(
+ new TestFixture("8.8.8.8", ipdataService()),
+ new TestFixture("8.8.8.8", cachingIpdataService()),
+ new TestFixture("2001:4860:4860::8888", ipdataService()),
+ new TestFixture("2001:4860:4860::8888", cachingIpdataService())
+ );
+ }
+
+}
diff --git a/src/test/java/io/ipdata/client/TestFixture.java b/src/test/java/io/ipdata/client/TestFixture.java
new file mode 100644
index 0000000..1ef6d41
--- /dev/null
+++ b/src/test/java/io/ipdata/client/TestFixture.java
@@ -0,0 +1,12 @@
+package io.ipdata.client;
+
+import io.ipdata.client.service.IpdataService;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.Accessors;
+
+@RequiredArgsConstructor @Accessors(fluent = true) @Getter
+public class TestFixture {
+ private final String target;
+ private final IpdataService service;
+}
diff --git a/src/test/java/io/ipdata/client/TestUtils.java b/src/test/java/io/ipdata/client/TestUtils.java
deleted file mode 100644
index ce11457..0000000
--- a/src/test/java/io/ipdata/client/TestUtils.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package io.ipdata.client;
-
-import com.google.common.io.CharStreams;
-import java.io.InputStreamReader;
-import java.util.Map;
-import lombok.SneakyThrows;
-import lombok.experimental.UtilityClass;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.utils.URIBuilder;
-import org.apache.http.entity.StringEntity;
-
-@UtilityClass
-public class TestUtils {
-
- public static final String KEY = System.getenv("IPDATACO_KEY");
-
- /*
- Used to get responses of a raw http call, without client proxying
- */
- @SneakyThrows
- public static String get(HttpClient client, String path, Map params) {
- URIBuilder builder = new URIBuilder("https://api.ipdata.co" + path);
- builder.setParameter("api-key", KEY);
- if (params != null) {
- for (Map.Entry entry : params.entrySet()) {
- builder.setParameter(entry.getKey(), entry.getValue());
- }
- }
- HttpGet httpget = new HttpGet(builder.build());
- HttpResponse response = client.execute(httpget);
- return CharStreams.toString(new InputStreamReader(response.getEntity().getContent()));
- }
-
- @SneakyThrows
- public static String post(HttpClient client, String path, String content, Map params) {
- URIBuilder builder = new URIBuilder("https://api.ipdata.co" + path);
- builder.setParameter("api-key", KEY);
- if (params != null) {
- for (Map.Entry entry : params.entrySet()) {
- builder.setParameter(entry.getKey(), entry.getValue());
- }
- }
- HttpPost httpPost = new HttpPost(builder.build());
- httpPost.setEntity(new StringEntity(content));
- HttpResponse response = client.execute(httpPost);
- return CharStreams.toString(new InputStreamReader(response.getEntity().getContent()));
- }
-
-}
diff --git a/src/test/java/io/ipdata/client/ThreatTest.java b/src/test/java/io/ipdata/client/ThreatTest.java
new file mode 100644
index 0000000..f462abd
--- /dev/null
+++ b/src/test/java/io/ipdata/client/ThreatTest.java
@@ -0,0 +1,56 @@
+package io.ipdata.client;
+
+import feign.httpclient.ApacheHttpClient;
+import io.ipdata.client.error.IpdataException;
+import io.ipdata.client.model.ThreatModel;
+import io.ipdata.client.service.IpdataService;
+import lombok.SneakyThrows;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.concurrent.TimeUnit;
+
+@RunWith(Parameterized.class)
+public class ThreatTest {
+
+ private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MockIpdataServer.getInstance().getUrl());
+
+ @Parameterized.Parameter
+ public TestFixture fixture;
+
+ @Test
+ @SneakyThrows
+ public void testThreat() {
+ IpdataService ipdataService = fixture.service();
+ ThreatModel threat = ipdataService.threat(fixture.target());
+ String actual = TEST_CONTEXT.mapper().writeValueAsString(threat);
+ String expected = TEST_CONTEXT.get("/"+fixture.target()+"/threat", null);
+ TEST_CONTEXT.assertEqualJson(actual, expected);
+ if (ipdataService == TEST_CONTEXT.cachingIpdataService()) {
+ //value will be returned from cache now
+ threat = ipdataService.threat(fixture.target());
+ actual = TEST_CONTEXT.mapper().writeValueAsString(threat);
+ TEST_CONTEXT.assertEqualJson(actual, expected);
+ }
+ }
+
+ @SneakyThrows
+ @Test(expected = IpdataException.class)
+ public void testThreatError() {
+ IpdataService serviceWithInvalidKey = Ipdata.builder().url(TEST_CONTEXT.url())
+ .key("THIS_IS_AN_INVALID_KEY")
+ .withDefaultCache()
+ .feignClient(new ApacheHttpClient(HttpClientBuilder.create()
+ .setConnectionTimeToLive(10, TimeUnit.SECONDS)
+ .build())).get();
+ serviceWithInvalidKey.threat(fixture.target());
+ }
+
+ @Parameterized.Parameters
+ public static Iterable data() {
+ return TEST_CONTEXT.fixtures();
+ }
+
+}
diff --git a/src/test/java/io/ipdata/client/TimeZoneTest.java b/src/test/java/io/ipdata/client/TimeZoneTest.java
new file mode 100644
index 0000000..2ef776f
--- /dev/null
+++ b/src/test/java/io/ipdata/client/TimeZoneTest.java
@@ -0,0 +1,60 @@
+package io.ipdata.client;
+
+import feign.httpclient.ApacheHttpClient;
+import io.ipdata.client.error.IpdataException;
+import io.ipdata.client.model.TimeZone;
+import io.ipdata.client.service.IpdataService;
+import lombok.SneakyThrows;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(Parameterized.class)
+public class TimeZoneTest {
+
+ private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MockIpdataServer.getInstance().getUrl());
+
+ @Parameterized.Parameter
+ public TestFixture fixture;
+
+ @Test
+ @SneakyThrows
+ public void testTimeZone() {
+ IpdataService ipdataService = fixture.service();
+ TimeZone timeZone = fixture.service().timeZone(fixture.target());
+ String expected = TEST_CONTEXT.get("/"+fixture.target()+"/time_zone", null);
+ String actual = TEST_CONTEXT.mapper().writeValueAsString(timeZone);
+ TEST_CONTEXT.assertEqualJson(actual, expected, TEST_CONTEXT.configuration().whenIgnoringPaths("current_time"));
+ assertNotNull(timeZone.currentTime());
+ if (ipdataService == TEST_CONTEXT.cachingIpdataService()) {
+ //value will be returned from cache now
+ timeZone = ipdataService.timeZone(fixture.target());
+ actual = TEST_CONTEXT.mapper().writeValueAsString(timeZone);
+ TEST_CONTEXT.assertEqualJson(actual, expected, TEST_CONTEXT.configuration().whenIgnoringPaths("current_time"));
+ assertNotNull(timeZone.currentTime());
+ }
+ }
+
+ @SneakyThrows
+ @Test(expected = IpdataException.class)
+ public void testTimeZoneError() {
+ IpdataService serviceWithInvalidKey = Ipdata.builder().url(TEST_CONTEXT.url())
+ .key("THIS_IS_AN_INVALID_KEY")
+ .withDefaultCache()
+ .feignClient(new ApacheHttpClient(HttpClientBuilder.create()
+ .setConnectionTimeToLive(10, TimeUnit.SECONDS)
+ .build())).get();
+ serviceWithInvalidKey.timeZone(fixture.target());
+ }
+
+ @Parameterized.Parameters
+ public static Iterable data() {
+ return TEST_CONTEXT.fixtures();
+ }
+
+}
diff --git a/src/test/resources/io/ipdata/client/fixture.json b/src/test/resources/io/ipdata/client/fixture.json
new file mode 100644
index 0000000..3275fb2
--- /dev/null
+++ b/src/test/resources/io/ipdata/client/fixture.json
@@ -0,0 +1,77 @@
+{
+ "ip": "69.78.70.144",
+ "is_eu": false,
+ "city": "test-city",
+ "region": "Illinois",
+ "region_code": "IL",
+ "region_type": "state",
+ "country_name": "United States",
+ "country_code": "US",
+ "continent_name": "North America",
+ "continent_code": "NA",
+ "latitude": 41.8483,
+ "longitude": -87.6517,
+ "postal": "test-postal",
+ "calling_code": "1",
+ "flag": "https://ipdata.co/flags/us.png",
+ "emoji_flag": "\ud83c\uddfa\ud83c\uddf8",
+ "emoji_unicode": "U+1F1FA U+1F1F8",
+ "asn": {
+ "asn": "AS6167",
+ "name": "Cellco Partnership DBA Verizon Wireless",
+ "domain": "verizonwireless.com",
+ "route": "69.78.0.0/16",
+ "type": "business"
+ },
+ "carrier": {
+ "name": "Verizon",
+ "mcc": "310",
+ "mnc": "004"
+ },
+ "company": {
+ "name": "Verizon Wireless",
+ "domain": "verizonwireless.com",
+ "network": "69.78.0.0/16",
+ "type": "business"
+ },
+ "languages": [
+ {
+ "name": "English",
+ "native": "English"
+ }
+ ],
+ "currency": {
+ "name": "US Dollar",
+ "code": "USD",
+ "symbol": "$",
+ "native": "$",
+ "plural": "US dollars"
+ },
+ "time_zone": {
+ "name": "America/Chicago",
+ "abbr": "CDT",
+ "offset": "-0500",
+ "is_dst": true,
+ "current_time": "2020-06-12T06:37:16.595612-05:00"
+ },
+ "threat": {
+ "is_tor": false,
+ "is_vpn": false,
+ "is_proxy": false,
+ "is_anonymous": false,
+ "is_known_attacker": false,
+ "is_known_abuser": false,
+ "is_threat": false,
+ "is_bogon": false,
+ "is_icloud_relay": false,
+ "is_datacenter": false,
+ "blocklists": [],
+ "scores": {
+ "vpn_score": 0,
+ "proxy_score": 0,
+ "threat_score": 0,
+ "trust_score": 100
+ }
+ },
+ "count": "236"
+}
diff --git a/src/test/resources/io/ipdata/client/fixtures/1.1.1.1.json b/src/test/resources/io/ipdata/client/fixtures/1.1.1.1.json
new file mode 100644
index 0000000..11e2002
--- /dev/null
+++ b/src/test/resources/io/ipdata/client/fixtures/1.1.1.1.json
@@ -0,0 +1,77 @@
+{
+ "ip": "1.1.1.1",
+ "is_eu": false,
+ "city": "Los Angeles",
+ "region": "California",
+ "region_code": "CA",
+ "region_type": "state",
+ "country_name": "United States",
+ "country_code": "US",
+ "continent_name": "North America",
+ "continent_code": "NA",
+ "latitude": 34.0522,
+ "longitude": -118.2437,
+ "postal": "90001",
+ "calling_code": "1",
+ "flag": "https://ipdata.co/flags/us.png",
+ "emoji_flag": "\ud83c\uddfa\ud83c\uddf8",
+ "emoji_unicode": "U+1F1FA U+1F1F8",
+ "asn": {
+ "asn": "AS13335",
+ "name": "Cloudflare Inc",
+ "domain": "cloudflare.com",
+ "route": "1.1.1.0/24",
+ "type": "hosting"
+ },
+ "carrier": {
+ "name": "Cloudflare",
+ "mcc": "310",
+ "mnc": "000"
+ },
+ "company": {
+ "name": "Cloudflare Inc",
+ "domain": "cloudflare.com",
+ "network": "1.1.1.0/24",
+ "type": "hosting"
+ },
+ "languages": [
+ {
+ "name": "English",
+ "native": "English"
+ }
+ ],
+ "currency": {
+ "name": "US Dollar",
+ "code": "USD",
+ "symbol": "$",
+ "native": "$",
+ "plural": "US dollars"
+ },
+ "time_zone": {
+ "name": "America/Los_Angeles",
+ "abbr": "PDT",
+ "offset": "-0700",
+ "is_dst": true,
+ "current_time": "2020-06-12T06:37:16.595612-07:00"
+ },
+ "threat": {
+ "is_tor": false,
+ "is_vpn": false,
+ "is_proxy": false,
+ "is_anonymous": false,
+ "is_known_attacker": false,
+ "is_known_abuser": false,
+ "is_threat": false,
+ "is_bogon": false,
+ "is_icloud_relay": false,
+ "is_datacenter": false,
+ "blocklists": [],
+ "scores": {
+ "vpn_score": 0,
+ "proxy_score": 0,
+ "threat_score": 0,
+ "trust_score": 100
+ }
+ },
+ "count": "1500"
+}
diff --git a/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8844.json b/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8844.json
new file mode 100644
index 0000000..62b19c9
--- /dev/null
+++ b/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8844.json
@@ -0,0 +1,77 @@
+{
+ "ip": "2001:4860:4860::8844",
+ "is_eu": false,
+ "city": "Mountain View",
+ "region": "California",
+ "region_code": "CA",
+ "region_type": "state",
+ "country_name": "United States",
+ "country_code": "US",
+ "continent_name": "North America",
+ "continent_code": "NA",
+ "latitude": 37.386,
+ "longitude": -122.0838,
+ "postal": "94035",
+ "calling_code": "1",
+ "flag": "https://ipdata.co/flags/us.png",
+ "emoji_flag": "\ud83c\uddfa\ud83c\uddf8",
+ "emoji_unicode": "U+1F1FA U+1F1F8",
+ "asn": {
+ "asn": "AS15169",
+ "name": "Google LLC",
+ "domain": "google.com",
+ "route": "2001:4860::/32",
+ "type": "hosting"
+ },
+ "carrier": {
+ "name": "Google",
+ "mcc": "310",
+ "mnc": "000"
+ },
+ "company": {
+ "name": "Google LLC",
+ "domain": "google.com",
+ "network": "2001:4860::/32",
+ "type": "hosting"
+ },
+ "languages": [
+ {
+ "name": "English",
+ "native": "English"
+ }
+ ],
+ "currency": {
+ "name": "US Dollar",
+ "code": "USD",
+ "symbol": "$",
+ "native": "$",
+ "plural": "US dollars"
+ },
+ "time_zone": {
+ "name": "America/Los_Angeles",
+ "abbr": "PDT",
+ "offset": "-0700",
+ "is_dst": true,
+ "current_time": "2020-06-12T06:37:16.595612-07:00"
+ },
+ "threat": {
+ "is_tor": false,
+ "is_vpn": false,
+ "is_proxy": false,
+ "is_anonymous": false,
+ "is_known_attacker": false,
+ "is_known_abuser": false,
+ "is_threat": false,
+ "is_bogon": false,
+ "is_icloud_relay": false,
+ "is_datacenter": false,
+ "blocklists": [],
+ "scores": {
+ "vpn_score": 0,
+ "proxy_score": 0,
+ "threat_score": 0,
+ "trust_score": 100
+ }
+ },
+ "count": "1500"
+}
diff --git a/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8888.json b/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8888.json
new file mode 100644
index 0000000..766ef8c
--- /dev/null
+++ b/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8888.json
@@ -0,0 +1,77 @@
+{
+ "ip": "2001:4860:4860::8888",
+ "is_eu": false,
+ "city": "Mountain View",
+ "region": "California",
+ "region_code": "CA",
+ "region_type": "state",
+ "country_name": "United States",
+ "country_code": "US",
+ "continent_name": "North America",
+ "continent_code": "NA",
+ "latitude": 37.386,
+ "longitude": -122.0838,
+ "postal": "94035",
+ "calling_code": "1",
+ "flag": "https://ipdata.co/flags/us.png",
+ "emoji_flag": "\ud83c\uddfa\ud83c\uddf8",
+ "emoji_unicode": "U+1F1FA U+1F1F8",
+ "asn": {
+ "asn": "AS15169",
+ "name": "Google LLC",
+ "domain": "google.com",
+ "route": "2001:4860::/32",
+ "type": "hosting"
+ },
+ "carrier": {
+ "name": "Google",
+ "mcc": "310",
+ "mnc": "000"
+ },
+ "company": {
+ "name": "Google LLC",
+ "domain": "google.com",
+ "network": "2001:4860::/32",
+ "type": "hosting"
+ },
+ "languages": [
+ {
+ "name": "English",
+ "native": "English"
+ }
+ ],
+ "currency": {
+ "name": "US Dollar",
+ "code": "USD",
+ "symbol": "$",
+ "native": "$",
+ "plural": "US dollars"
+ },
+ "time_zone": {
+ "name": "America/Los_Angeles",
+ "abbr": "PDT",
+ "offset": "-0700",
+ "is_dst": true,
+ "current_time": "2020-06-12T06:37:16.595612-07:00"
+ },
+ "threat": {
+ "is_tor": false,
+ "is_vpn": false,
+ "is_proxy": false,
+ "is_anonymous": false,
+ "is_known_attacker": false,
+ "is_known_abuser": false,
+ "is_threat": false,
+ "is_bogon": false,
+ "is_icloud_relay": false,
+ "is_datacenter": false,
+ "blocklists": [],
+ "scores": {
+ "vpn_score": 0,
+ "proxy_score": 0,
+ "threat_score": 0,
+ "trust_score": 100
+ }
+ },
+ "count": "1500"
+}
diff --git a/src/test/resources/io/ipdata/client/fixtures/41.128.21.123.json b/src/test/resources/io/ipdata/client/fixtures/41.128.21.123.json
new file mode 100644
index 0000000..58c661a
--- /dev/null
+++ b/src/test/resources/io/ipdata/client/fixtures/41.128.21.123.json
@@ -0,0 +1,77 @@
+{
+ "ip": "41.128.21.123",
+ "is_eu": false,
+ "city": "Cairo",
+ "region": "Cairo Governorate",
+ "region_code": "C",
+ "region_type": "governorate",
+ "country_name": "Egypt",
+ "country_code": "EG",
+ "continent_name": "Africa",
+ "continent_code": "AF",
+ "latitude": 30.0444,
+ "longitude": 31.2357,
+ "postal": "11511",
+ "calling_code": "20",
+ "flag": "https://ipdata.co/flags/eg.png",
+ "emoji_flag": "\ud83c\uddea\ud83c\uddec",
+ "emoji_unicode": "U+1F1EA U+1F1EC",
+ "asn": {
+ "asn": "AS8452",
+ "name": "TE Data",
+ "domain": "tedata.net",
+ "route": "41.128.0.0/16",
+ "type": "isp"
+ },
+ "carrier": {
+ "name": "TE Data",
+ "mcc": "602",
+ "mnc": "001"
+ },
+ "company": {
+ "name": "TE Data",
+ "domain": "tedata.net",
+ "network": "41.128.0.0/16",
+ "type": "isp"
+ },
+ "languages": [
+ {
+ "name": "Arabic",
+ "native": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629"
+ }
+ ],
+ "currency": {
+ "name": "Egyptian Pound",
+ "code": "EGP",
+ "symbol": "E\u00a3",
+ "native": "\u062c.\u0645.\u200f",
+ "plural": "Egyptian pounds"
+ },
+ "time_zone": {
+ "name": "Africa/Cairo",
+ "abbr": "EET",
+ "offset": "+0200",
+ "is_dst": false,
+ "current_time": "2020-06-12T15:37:16.595612+02:00"
+ },
+ "threat": {
+ "is_tor": false,
+ "is_vpn": false,
+ "is_proxy": false,
+ "is_anonymous": false,
+ "is_known_attacker": false,
+ "is_known_abuser": false,
+ "is_threat": false,
+ "is_bogon": false,
+ "is_icloud_relay": false,
+ "is_datacenter": false,
+ "blocklists": [],
+ "scores": {
+ "vpn_score": 0,
+ "proxy_score": 0,
+ "threat_score": 0,
+ "trust_score": 100
+ }
+ },
+ "count": "1500"
+}
diff --git a/src/test/resources/io/ipdata/client/fixtures/8.8.8.8.json b/src/test/resources/io/ipdata/client/fixtures/8.8.8.8.json
new file mode 100644
index 0000000..8ad870f
--- /dev/null
+++ b/src/test/resources/io/ipdata/client/fixtures/8.8.8.8.json
@@ -0,0 +1,77 @@
+{
+ "ip": "8.8.8.8",
+ "is_eu": false,
+ "city": "Mountain View",
+ "region": "California",
+ "region_code": "CA",
+ "region_type": "state",
+ "country_name": "United States",
+ "country_code": "US",
+ "continent_name": "North America",
+ "continent_code": "NA",
+ "latitude": 37.386,
+ "longitude": -122.0838,
+ "postal": "94035",
+ "calling_code": "1",
+ "flag": "https://ipdata.co/flags/us.png",
+ "emoji_flag": "\ud83c\uddfa\ud83c\uddf8",
+ "emoji_unicode": "U+1F1FA U+1F1F8",
+ "asn": {
+ "asn": "AS15169",
+ "name": "Google LLC",
+ "domain": "google.com",
+ "route": "8.8.8.0/24",
+ "type": "hosting"
+ },
+ "carrier": {
+ "name": "Google",
+ "mcc": "310",
+ "mnc": "000"
+ },
+ "company": {
+ "name": "Google LLC",
+ "domain": "google.com",
+ "network": "8.8.8.0/24",
+ "type": "hosting"
+ },
+ "languages": [
+ {
+ "name": "English",
+ "native": "English"
+ }
+ ],
+ "currency": {
+ "name": "US Dollar",
+ "code": "USD",
+ "symbol": "$",
+ "native": "$",
+ "plural": "US dollars"
+ },
+ "time_zone": {
+ "name": "America/Los_Angeles",
+ "abbr": "PDT",
+ "offset": "-0700",
+ "is_dst": true,
+ "current_time": "2020-06-12T06:37:16.595612-07:00"
+ },
+ "threat": {
+ "is_tor": false,
+ "is_vpn": false,
+ "is_proxy": false,
+ "is_anonymous": false,
+ "is_known_attacker": false,
+ "is_known_abuser": false,
+ "is_threat": false,
+ "is_bogon": false,
+ "is_icloud_relay": false,
+ "is_datacenter": false,
+ "blocklists": [],
+ "scores": {
+ "vpn_score": 0,
+ "proxy_score": 0,
+ "threat_score": 0,
+ "trust_score": 100
+ }
+ },
+ "count": "1500"
+}
diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties
deleted file mode 100644
index 67f7a69..0000000
--- a/src/test/resources/log4j.properties
+++ /dev/null
@@ -1,9 +0,0 @@
-log4j.rootLogger=DEBUG, A1
-log4j.appender.A1=org.apache.log4j.ConsoleAppender
-log4j.appender.A1.layout=org.apache.log4j.PatternLayout
-
-# Print the date in ISO 8601 format
-log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n
-
-# Print only messages of level WARN or above in the package com.foo.
-log4j.logger.io=DEBUG
diff --git a/src/test/resources/simplelogger.properties b/src/test/resources/simplelogger.properties
new file mode 100644
index 0000000..572da4d
--- /dev/null
+++ b/src/test/resources/simplelogger.properties
@@ -0,0 +1,6 @@
+org.slf4j.simpleLogger.defaultLogLevel=debug
+org.slf4j.simpleLogger.log.org.apache.http=info
+org.slf4j.simpleLogger.showDateTime=true
+org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss,SSS
+org.slf4j.simpleLogger.showThreadName=true
+org.slf4j.simpleLogger.showShortLogName=true