diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml deleted file mode 100644 index 3abb8f2..0000000 --- a/.mvn/extensions.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - io.takari.polyglot - polyglot-groovy - 0.4.3 - - - diff --git a/README.md b/README.md index 3a0d557..7c1176b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ An 100% compliant [ipdata.co](https://ipdata.co) API java client. - [Install](#install) - [Use](#use) - [Configuration](#configuration) - - [API](#create-an-instance) + - [API](#api) - [Basic Usage](#basic-usage) - [Connection reuse and HTTP/2](#connection-reuse-and-http2) - [Single Field Selection](#single-field-selection) @@ -81,30 +81,38 @@ Output: { "ip": "1.1.1.1", "is_eu": false, - "city": null, - "region": null, - "region_code": null, - "region_type": null, - "country_name": "Australia", - "country_code": "AU", - "continent_name": "Oceania", - "continent_code": "OC", - "latitude": -33.494, - "longitude": 143.2104, - "postal": null, - "calling_code": "61", - "flag": "https://ipdata.co/flags/au.png", - "emoji_flag": "\ud83c\udde6\ud83c\uddfa", - "emoji_unicode": "U+1F1E6 U+1F1FA", + "is_satellite": false, + "is_anycast": false, + "city": "Los Angeles", + "organisation": null, + "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.", + "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.", + "name": "Cloudflare Inc", "domain": "cloudflare.com", "network": "1.1.1.0/24", "type": "hosting" @@ -112,25 +120,28 @@ Output: "languages": [ { "name": "English", - "native": "English" + "native": "English", + "code": "en", + "rtl": 0 } ], "currency": { - "name": "Australian Dollar", - "code": "AUD", - "symbol": "AU$", + "name": "US Dollar", + "code": "USD", + "symbol": "$", "native": "$", - "plural": "Australian dollars" + "plural": "US dollars" }, "time_zone": { - "name": "Australia/Sydney", - "abbr": "AEDT", - "offset": "+1100", + "name": "America/Los_Angeles", + "abbr": "PDT", + "offset": "-0700", "is_dst": true, - "current_time": "2020-01-29T20:22:52.283874+11:00" + "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, @@ -138,9 +149,16 @@ Output: "is_threat": false, "is_bogon": false, "is_icloud_relay": false, - "is_datacenter": false + "is_datacenter": false, + "blocklists": [], + "scores": { + "vpn_score": 0, + "proxy_score": 0, + "threat_score": 0, + "trust_score": 100 + } }, - "count": "0" + "count": "1500" } ``` @@ -151,6 +169,7 @@ The following fields were added to the API responses recently and are mapped by - `threat.blocklists[].first_seen` – `Blocklist.getFirstSeen()`: the first day the address was seen on that blocklist, as a `YYYY-MM-DD` (UTC) string, or `null` when the feed does not report it. - `threat.blocklists[].last_seen` – `Blocklist.getLastSeen()`: the most recent such day, same format, or `null`. - `threat.blocklists[].days_seen` – `Blocklist.getDaysSeen()`: the number of days the address was seen on that blocklist, as an `Integer`, or `null`. +- `languages[].code` – `Language.code()`: the code of the language (`en` for English), or `null` when the API does not send it. #### Connection reuse and HTTP/2 An `IpdataService` keeps a persistent HTTPS connection pool for as long as it lives. Create one service per process (or one per API key or endpoint), keep it, and reuse it for every lookup: each new service pays a fresh TLS handshake. Requests to api.ipdata.co are multiplexed over HTTP/2 when the platform negotiates it, and fall back to HTTP/1.1 keep-alive otherwise. @@ -206,7 +225,7 @@ IpdataModel flags = ipdataService.getFields("1.1.1.1", IpdataField.IS_SATELLITE, ``` -### Bulk data +#### Bulk data You can as well get multiple responses at once by using the ``bulk`` api: ```java diff --git a/pom.xml b/pom.xml index b20011d..b242050 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,9 @@ 0.8.14 3.5.5 11.10 + 2.22.4 + + 2.22 @@ -57,6 +60,16 @@ feign-java11 ${version.client.feign} + + com.fasterxml.jackson.core + jackson-databind + ${version.client.jackson} + + + com.fasterxml.jackson.core + jackson-annotations + ${version.client.jackson.annotations} + io.github.openfeign feign-httpclient @@ -66,7 +79,7 @@ com.google.guava guava - 33.4.8-jre + 33.7.2-jre org.slf4j diff --git a/src/main/java/io/ipdata/client/model/Language.java b/src/main/java/io/ipdata/client/model/Language.java index 395358c..a300465 100644 --- a/src/main/java/io/ipdata/client/model/Language.java +++ b/src/main/java/io/ipdata/client/model/Language.java @@ -10,5 +10,7 @@ public class Language { private String name; @JsonProperty("native") private String nativeName; + /** The language code, for example {@code en} for English; {@code null} when the API does not send it. */ + private String code; private int rtl; } diff --git a/src/main/java/io/ipdata/client/service/ApiErrorDecoder.java b/src/main/java/io/ipdata/client/service/ApiErrorDecoder.java index e526eb7..c2fcea1 100644 --- a/src/main/java/io/ipdata/client/service/ApiErrorDecoder.java +++ b/src/main/java/io/ipdata/client/service/ApiErrorDecoder.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.net.URL; +import java.nio.charset.StandardCharsets; @RequiredArgsConstructor @Slf4j @@ -30,7 +31,7 @@ public Exception decode(String methodKey, Response response) { URL url = new URL(response.request().url()); logger.error("An error occurred during an Ipdata API call: got status '{}' for path '{}'", responseCode, url.getPath()); - message = CharStreams.toString(response.body().asReader()); + message = CharStreams.toString(response.body().asReader(StandardCharsets.UTF_8)); Error error = mapper.readValue(message, Error.class); if (responseCode == RATE_LIMIT_STATUS) { return new RateLimitException(error.getMessage(), responseCode); diff --git a/src/main/java/io/ipdata/client/service/FieldDecoder.java b/src/main/java/io/ipdata/client/service/FieldDecoder.java index 1eb1b58..fb0bd38 100644 --- a/src/main/java/io/ipdata/client/service/FieldDecoder.java +++ b/src/main/java/io/ipdata/client/service/FieldDecoder.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; /** * The API returns usable but invalid(/unquoted) strings for String fields @@ -22,7 +23,7 @@ class FieldDecoder implements Decoder { @Override public Object decode(Response response, Type type) throws IOException { if (type.equals(String.class)) { - return CharStreams.toString(response.body().asReader()); + return CharStreams.toString(response.body().asReader(StandardCharsets.UTF_8)); } return mapper.readValue(response.body().asInputStream(), mapper.constructType(type)); } diff --git a/src/main/java/io/ipdata/client/service/IpdataServiceBuilder.java b/src/main/java/io/ipdata/client/service/IpdataServiceBuilder.java index b22ef6d..22bcc40 100644 --- a/src/main/java/io/ipdata/client/service/IpdataServiceBuilder.java +++ b/src/main/java/io/ipdata/client/service/IpdataServiceBuilder.java @@ -19,6 +19,7 @@ import org.slf4j.LoggerFactory; import java.net.URL; +import java.time.Duration; import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SNAKE_CASE; @@ -64,7 +65,7 @@ public static IpdataServiceBuilder of(URL url, String key, CacheConfig cacheConf public IpdataService build() { final ObjectMapper mapper = new ObjectMapper(); mapper.setPropertyNamingStrategy(SNAKE_CASE); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); @@ -98,13 +99,14 @@ public IpdataService build() { .target(IpdataInternalSingleFieldClient.class, url.toString()); if (cacheConfig != null) { + final Duration expiry = Duration.of(cacheConfig.timeout(), cacheConfig.unit().toChronoUnit()); CachingInternalClient cachingInternalClient = CachingInternalClient.builder() .expiry(cacheConfig.timeout()) .unit(cacheConfig.unit()) .maxSize(cacheConfig.maxSize()) .ipdataCache( CacheBuilder.newBuilder() - .expireAfterWrite(cacheConfig.timeout(), cacheConfig.unit()) + .expireAfterWrite(expiry) .maximumSize(cacheConfig.maxSize()) .build(new CacheLoader() { @Override @@ -115,7 +117,7 @@ public IpdataModel load(String key) throws Exception { ) .fieldsCache( CacheBuilder.newBuilder() - .expireAfterWrite(cacheConfig.timeout(), cacheConfig.unit()) + .expireAfterWrite(expiry) .maximumSize(cacheConfig.maxSize()) .build(new CacheLoader, IpdataModel>() { @Override @@ -126,7 +128,7 @@ public IpdataModel load(HashPair key) throws Exception { ) .asnCache( CacheBuilder.newBuilder() - .expireAfterWrite(cacheConfig.timeout(), cacheConfig.unit()) + .expireAfterWrite(expiry) .maximumSize(cacheConfig.maxSize()) .build(new CacheLoader() { @Override @@ -137,7 +139,7 @@ public AsnModel load(String key) throws Exception { ) .tzCache( CacheBuilder.newBuilder() - .expireAfterWrite(cacheConfig.timeout(), cacheConfig.unit()) + .expireAfterWrite(expiry) .maximumSize(cacheConfig.maxSize()) .build(new CacheLoader() { @Override @@ -148,7 +150,7 @@ public TimeZone load(String key) throws Exception { ) .currencyCache( CacheBuilder.newBuilder() - .expireAfterWrite(cacheConfig.timeout(), cacheConfig.unit()) + .expireAfterWrite(expiry) .maximumSize(cacheConfig.maxSize()) .build(new CacheLoader() { @Override @@ -159,7 +161,7 @@ public Currency load(String key) throws Exception { ) .threatCache( CacheBuilder.newBuilder() - .expireAfterWrite(cacheConfig.timeout(), cacheConfig.unit()) + .expireAfterWrite(expiry) .maximumSize(cacheConfig.maxSize()) .build(new CacheLoader() { @Override diff --git a/src/main/java/io/ipdata/client/service/IpdataServiceSupport.java b/src/main/java/io/ipdata/client/service/IpdataServiceSupport.java index 7531ba8..78982b5 100644 --- a/src/main/java/io/ipdata/client/service/IpdataServiceSupport.java +++ b/src/main/java/io/ipdata/client/service/IpdataServiceSupport.java @@ -21,12 +21,12 @@ class IpdataServiceSupport implements IpdataService { private final IpdataInternalSingleFieldClient singleFieldClient; @Delegate(types = IpdataInternalClient.class) - public IpdataInternalClient getSingleFieldClient() { + private IpdataInternalClient getApi() { return api; } @Delegate(types = IpdataInternalSingleFieldClient.class) - private IpdataInternalSingleFieldClient getApi() { + private IpdataInternalSingleFieldClient getSingleFieldClient() { return singleFieldClient; } diff --git a/src/main/java/io/ipdata/client/service/Ipv6SafeClient.java b/src/main/java/io/ipdata/client/service/Ipv6SafeClient.java index f8f6f38..917ee4b 100644 --- a/src/main/java/io/ipdata/client/service/Ipv6SafeClient.java +++ b/src/main/java/io/ipdata/client/service/Ipv6SafeClient.java @@ -36,7 +36,7 @@ public Response execute(Request request, Request.Options options) throws IOExcep String fixedUrl = fixedPath + query; request = Request.create( request.httpMethod(), fixedUrl, request.headers(), - request.body(), request.charset() + request.body(), request.charset(), request.requestTemplate() ); } return delegate.execute(request, options); diff --git a/src/test/java/io/ipdata/client/LanguageTest.java b/src/test/java/io/ipdata/client/LanguageTest.java new file mode 100644 index 0000000..39422ac --- /dev/null +++ b/src/test/java/io/ipdata/client/LanguageTest.java @@ -0,0 +1,80 @@ +package io.ipdata.client; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.ipdata.client.model.IpdataModel; +import io.ipdata.client.model.Language; +import io.ipdata.client.service.IpdataService; +import lombok.SneakyThrows; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static io.ipdata.client.service.IpdataField.LANGUAGES; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * The {@code code} attribute of the {@code languages[]} entries. + */ +public class LanguageTest { + + private static final MockIpdataServer MOCK = MockIpdataServer.getInstance(); + private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MOCK.getUrl()); + + private static List services() { + return Arrays.asList(TEST_CONTEXT.ipdataService(), TEST_CONTEXT.cachingIpdataService()); + } + + private static Language onlyLanguage(IpdataModel model) { + assertEquals(1, model.languages().size()); + return model.languages().get(0); + } + + @Test + @SneakyThrows + public void codeIsMappedOnTheFullModel() { + for (IpdataService service : services()) { + Language english = onlyLanguage(service.ipdata("8.8.8.8")); + assertEquals("English", english.name()); + assertEquals("English", english.nativeName()); + assertEquals("en", english.code()); + assertEquals("pt", onlyLanguage(service.ipdata("187.13.250.1")).code()); + assertEquals("ar", onlyLanguage(service.ipdata("41.128.21.123")).code()); + } + } + + @Test + @SneakyThrows + public void codeIsMappedOnFieldSelection() { + for (IpdataService service : services()) { + IpdataModel model = service.getFields("8.8.8.8", LANGUAGES); + assertEquals("en", onlyLanguage(model).code()); + assertNull(model.city()); + } + } + + @Test + @SneakyThrows + public void codeIsNullWhenAbsent() { + String ip = "203.0.113.8"; + ObjectNode response = (ObjectNode) TEST_CONTEXT.mapper().readTree(TEST_CONTEXT.get("/8.8.8.8", null)); + response.put("ip", ip); + ((ObjectNode) response.get("languages").get(0)).remove("code"); + MOCK.registerFixture(ip, response.toString()); + + for (IpdataService service : services()) { + Language language = onlyLanguage(service.ipdata(ip)); + assertEquals("English", language.name()); + assertNull(language.code()); + } + } + + @Test + @SneakyThrows + public void strictMapperAcceptsTheCodeAttribute() { + // the test mapper fails on unknown properties, so every attribute the fixtures carry must exist on the model + IpdataModel model = TEST_CONTEXT.mapper().readValue(TEST_CONTEXT.get("/8.8.8.8", null), IpdataModel.class); + assertEquals("en", onlyLanguage(model).code()); + } +} diff --git a/src/test/java/io/ipdata/client/MappingTest.java b/src/test/java/io/ipdata/client/MappingTest.java index 0d5c5fd..77eb722 100644 --- a/src/test/java/io/ipdata/client/MappingTest.java +++ b/src/test/java/io/ipdata/client/MappingTest.java @@ -8,6 +8,7 @@ import org.junit.Test; import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; public class MappingTest { @@ -16,7 +17,7 @@ public class MappingTest { @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"))); + String expected = CharStreams.toString(new InputStreamReader(getClass().getResourceAsStream("fixture.json"), StandardCharsets.UTF_8)); 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 index e8c87e9..f0e06a2 100644 --- a/src/test/java/io/ipdata/client/MockIpdataServer.java +++ b/src/test/java/io/ipdata/client/MockIpdataServer.java @@ -11,9 +11,9 @@ 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.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -212,7 +212,7 @@ private void handleGet(HttpExchange exchange, String path, Map p } private void handleBulk(HttpExchange exchange) throws IOException { - String body = CharStreams.toString(new InputStreamReader(exchange.getRequestBody())); + String body = CharStreams.toString(new InputStreamReader(exchange.getRequestBody(), StandardCharsets.UTF_8)); String[] ips = mapper.readValue(body, String[].class); ArrayNode result = mapper.createArrayNode(); for (String ip : ips) { @@ -225,7 +225,7 @@ private void handleBulk(HttpExchange exchange) throws IOException { } private void sendResponse(HttpExchange exchange, int status, String body) throws IOException { - byte[] bytes = body.getBytes("UTF-8"); + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "application/json"); exchange.sendResponseHeaders(status, bytes.length); exchange.getResponseBody().write(bytes); @@ -243,11 +243,7 @@ private Map parseQuery(String rawQuery) { 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]); - } + params.put(URLDecoder.decode(pair[0], StandardCharsets.UTF_8), URLDecoder.decode(pair[1], StandardCharsets.UTF_8)); } } } diff --git a/src/test/java/io/ipdata/client/TestContext.java b/src/test/java/io/ipdata/client/TestContext.java index c367d3b..8bf26d1 100644 --- a/src/test/java/io/ipdata/client/TestContext.java +++ b/src/test/java/io/ipdata/client/TestContext.java @@ -21,6 +21,7 @@ import java.io.InputStreamReader; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.Map; import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SNAKE_CASE; @@ -71,7 +72,8 @@ void assertEqualJson(String actual, String expected, Configuration configuration } /* - Used to get responses of a raw http call, without logical proxying + Used to get responses of a raw http call, without logical proxying. + Bodies are always decoded as UTF-8, whatever the platform default charset is. */ @SneakyThrows String get(String path, Map params) { @@ -84,7 +86,7 @@ String get(String path, Map params) { } HttpGet httpget = new HttpGet(builder.build()); HttpResponse response = httpClient.execute(httpget); - return CharStreams.toString(new InputStreamReader(response.getEntity().getContent())); + return CharStreams.toString(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8)); } @SneakyThrows @@ -97,9 +99,9 @@ String post(String path, String content, Map params) { } } HttpPost httpPost = new HttpPost(builder.build()); - httpPost.setEntity(new StringEntity(content)); + httpPost.setEntity(new StringEntity(content, StandardCharsets.UTF_8)); HttpResponse response = httpClient.execute(httpPost); - return CharStreams.toString(new InputStreamReader(response.getEntity().getContent())); + return CharStreams.toString(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8)); } diff --git a/src/test/resources/io/ipdata/client/fixture.json b/src/test/resources/io/ipdata/client/fixture.json index f5ecb49..3ece9f8 100644 --- a/src/test/resources/io/ipdata/client/fixture.json +++ b/src/test/resources/io/ipdata/client/fixture.json @@ -39,7 +39,8 @@ "languages": [ { "name": "English", - "native": "English" + "native": "English", + "code": "en" } ], "currency": { 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 index 11e2002..3475de5 100644 --- 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 @@ -37,7 +37,8 @@ "languages": [ { "name": "English", - "native": "English" + "native": "English", + "code": "en" } ], "currency": { diff --git a/src/test/resources/io/ipdata/client/fixtures/187.13.250.1.json b/src/test/resources/io/ipdata/client/fixtures/187.13.250.1.json index 3e179dc..685d07f 100644 --- a/src/test/resources/io/ipdata/client/fixtures/187.13.250.1.json +++ b/src/test/resources/io/ipdata/client/fixtures/187.13.250.1.json @@ -32,7 +32,8 @@ "languages": [ { "name": "Portuguese", - "native": "Português" + "native": "Português", + "code": "pt" } ], "currency": { 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 index 62b19c9..fd66fe7 100644 --- 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 @@ -37,7 +37,8 @@ "languages": [ { "name": "English", - "native": "English" + "native": "English", + "code": "en" } ], "currency": { 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 index 766ef8c..5ba5553 100644 --- 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 @@ -37,7 +37,8 @@ "languages": [ { "name": "English", - "native": "English" + "native": "English", + "code": "en" } ], "currency": { 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 index 58c661a..211ff33 100644 --- 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 @@ -37,7 +37,8 @@ "languages": [ { "name": "Arabic", - "native": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629" + "native": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629", + "code": "ar" } ], "currency": { 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 index 4a7013d..a3a2666 100644 --- 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 @@ -38,7 +38,8 @@ "languages": [ { "name": "English", - "native": "English" + "native": "English", + "code": "en" } ], "currency": { diff --git a/src/test/resources/io/ipdata/client/fixtures/98.97.34.0.json b/src/test/resources/io/ipdata/client/fixtures/98.97.34.0.json index 2f780b5..c17c0c7 100644 --- a/src/test/resources/io/ipdata/client/fixtures/98.97.34.0.json +++ b/src/test/resources/io/ipdata/client/fixtures/98.97.34.0.json @@ -33,7 +33,8 @@ "languages": [ { "name": "English", - "native": "English" + "native": "English", + "code": "en" } ], "currency": {