diff --git a/exonum-light-client/pom.xml b/exonum-light-client/pom.xml index 33aeb1469a..6f99881a78 100644 --- a/exonum-light-client/pom.xml +++ b/exonum-light-client/pom.xml @@ -76,7 +76,7 @@ 5.4.0 2.24.0 2.0.0.0 - + 2.4.0 warning @@ -121,6 +121,12 @@ test + + org.junit.jupiter + junit-jupiter-params + test + + org.mockito mockito-junit-jupiter @@ -142,6 +148,13 @@ test + + com.jayway.jsonpath + json-path-assert + ${jsonpath.version} + test + + diff --git a/exonum-light-client/src/main/java/com/exonum/client/ExonumClient.java b/exonum-light-client/src/main/java/com/exonum/client/ExonumClient.java index d3d554ecc2..e1fbc33f39 100644 --- a/exonum-light-client/src/main/java/com/exonum/client/ExonumClient.java +++ b/exonum-light-client/src/main/java/com/exonum/client/ExonumClient.java @@ -21,6 +21,7 @@ import com.exonum.binding.common.hash.HashCode; import com.exonum.binding.common.message.TransactionMessage; +import com.exonum.client.response.HealthCheckInfo; import java.net.MalformedURLException; import java.net.URL; import okhttp3.OkHttpClient; @@ -28,6 +29,9 @@ /** * Main interface for Exonum Light client. * Provides a convenient way for interaction with Exonum framework APIs. + * All the methods of the interface work in a blocking way + * i.e. invoke underlying request immediately, and block until the response can be processed + * or an error occurs. *

Implementations of that interface are required to be thread-safe. **/ public interface ExonumClient { @@ -40,6 +44,28 @@ public interface ExonumClient { */ HashCode submitTransaction(TransactionMessage tx); + /** + * Returns a number of unconfirmed transactions which are currently located in + * the unconfirmed transactions pool and are waiting for acceptance to a block. + * @throws RuntimeException if the client is unable to complete a request + * (e.g., in case of connectivity problems) + */ + int getUnconfirmedTransactionsCount(); + + /** + * Returns the node health check information. + * @throws RuntimeException if the client is unable to complete a request + * (e.g., in case of connectivity problems) + */ + HealthCheckInfo healthCheck(); + + /** + * Returns string containing information about Exonum, Rust and OS version. + * @throws RuntimeException if the client is unable to complete a request + * (e.g., in case of connectivity problems) + */ + String getUserAgentInfo(); + /** * Returns Exonum client builder. */ diff --git a/exonum-light-client/src/main/java/com/exonum/client/ExonumHttpClient.java b/exonum-light-client/src/main/java/com/exonum/client/ExonumHttpClient.java index dfc080bae4..9d96995c43 100644 --- a/exonum-light-client/src/main/java/com/exonum/client/ExonumHttpClient.java +++ b/exonum-light-client/src/main/java/com/exonum/client/ExonumHttpClient.java @@ -17,16 +17,20 @@ package com.exonum.client; -import static com.exonum.binding.common.serialization.json.JsonSerializer.json; +import static com.exonum.client.ExonumUrls.HEALTH_CHECK; +import static com.exonum.client.ExonumUrls.MEMORY_POOL; import static com.exonum.client.ExonumUrls.SUBMIT_TRANSACTION; +import static com.exonum.client.ExonumUrls.USER_AGENT; import com.exonum.binding.common.hash.HashCode; import com.exonum.binding.common.message.TransactionMessage; +import com.exonum.client.response.HealthCheckInfo; import com.google.common.annotations.VisibleForTesting; import com.google.common.io.BaseEncoding; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.function.Function; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -53,20 +57,45 @@ class ExonumHttpClient implements ExonumClient { @Override public HashCode submitTransaction(TransactionMessage transactionMessage) { String msg = toHex(transactionMessage.toBytes()); + Request request = postRequest(toFullUrl(SUBMIT_TRANSACTION), + ExplorerApiHelper.createSubmitTxBody(msg)); - Request request = createRequest(toFullUrl(SUBMIT_TRANSACTION), new SubmitTxRequest(msg)); - SubmitTxResponse result = blockingExecuteWithResponse(request, SubmitTxResponse.class); + return blockingExecuteAndParse(request, ExplorerApiHelper::parseSubmitTxResponse); + } + + @Override + public int getUnconfirmedTransactionsCount() { + Request request = getRequest(toFullUrl(MEMORY_POOL)); + + return blockingExecuteAndParse(request, SystemApiHelper::parseMemoryPoolJson); + } + + @Override + public HealthCheckInfo healthCheck() { + Request request = getRequest(toFullUrl(HEALTH_CHECK)); + + return blockingExecuteAndParse(request, SystemApiHelper::parseHealthCheckJson); + } - return result.getHash(); + @Override + public String getUserAgentInfo() { + Request request = getRequest(toFullUrl(USER_AGENT)); + + return blockingExecutePlainText(request); } private static String toHex(byte[] array) { return HEX_ENCODER.encode(array); } - private static Request createRequest(URL url, Object requestBody) { - String jsonBody = json().toJson(requestBody); + private static Request getRequest(URL url) { + return new Request.Builder() + .url(url) + .get() + .build(); + } + private static Request postRequest(URL url, String jsonBody) { return new Request.Builder() .url(url) .post(RequestBody.create(MEDIA_TYPE_JSON, jsonBody)) @@ -81,17 +110,20 @@ private URL toFullUrl(String relativeUrl) { } } - private T blockingExecuteWithResponse(Request request, Class responseObject) { + private String blockingExecutePlainText(Request request) { try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { throw new RuntimeException("Execution wasn't success: " + response.toString()); } - String responseJson = response.body().string(); - - return json().fromJson(responseJson, responseObject); + return response.body().string(); } catch (IOException e) { throw new RuntimeException(e); } } + private T blockingExecuteAndParse(Request request, Function parser) { + String response = blockingExecutePlainText(request); + return parser.apply(response); + } + } diff --git a/exonum-light-client/src/main/java/com/exonum/client/ExonumUrls.java b/exonum-light-client/src/main/java/com/exonum/client/ExonumUrls.java index 069994c17d..68caf101e9 100644 --- a/exonum-light-client/src/main/java/com/exonum/client/ExonumUrls.java +++ b/exonum-light-client/src/main/java/com/exonum/client/ExonumUrls.java @@ -21,5 +21,11 @@ * Contains Exonum API URLs. */ final class ExonumUrls { - static final String SUBMIT_TRANSACTION = "/api/explorer/v1/transactions"; + private static final String EXPLORER_PATHS_PREFIX = "/api/explorer/v1"; + private static final String SYS_PATHS_PREFIX = "/api/system/v1"; + static final String SUBMIT_TRANSACTION = EXPLORER_PATHS_PREFIX + "/transactions"; + static final String MEMORY_POOL = SYS_PATHS_PREFIX + "/mempool"; + static final String HEALTH_CHECK = SYS_PATHS_PREFIX + "/healthcheck"; + static final String USER_AGENT = SYS_PATHS_PREFIX + "/user_agent"; + } diff --git a/exonum-light-client/src/main/java/com/exonum/client/ExplorerApiHelper.java b/exonum-light-client/src/main/java/com/exonum/client/ExplorerApiHelper.java new file mode 100644 index 0000000000..128dba7ccd --- /dev/null +++ b/exonum-light-client/src/main/java/com/exonum/client/ExplorerApiHelper.java @@ -0,0 +1,64 @@ +/* + * Copyright 2019 The Exonum Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.exonum.client; + +import static com.exonum.binding.common.serialization.json.JsonSerializer.json; + +import com.exonum.binding.common.hash.HashCode; +import com.google.common.annotations.VisibleForTesting; +import com.google.gson.annotations.SerializedName; +import lombok.Value; + +/** + * Utility class for Exonum Explorer API. + */ +final class ExplorerApiHelper { + + static String createSubmitTxBody(String message) { + SubmitTxRequest request = new SubmitTxRequest(message); + return json().toJson(request); + } + + static HashCode parseSubmitTxResponse(String json) { + SubmitTxResponse response = json().fromJson(json, SubmitTxResponse.class); + return response.getHash(); + } + + /** + * Json object wrapper for submit transaction request. + */ + @Value + @VisibleForTesting + static class SubmitTxRequest { + @SerializedName("tx_body") + String body; + } + + /** + * Json object wrapper for submit transaction response. + */ + @Value + private static class SubmitTxResponse { + @SerializedName("tx_hash") + HashCode hash; + } + + private ExplorerApiHelper() { + throw new UnsupportedOperationException("Not instantiable"); + } +} diff --git a/exonum-light-client/src/main/java/com/exonum/client/SystemApiHelper.java b/exonum-light-client/src/main/java/com/exonum/client/SystemApiHelper.java new file mode 100644 index 0000000000..15af670b69 --- /dev/null +++ b/exonum-light-client/src/main/java/com/exonum/client/SystemApiHelper.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 The Exonum Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.exonum.client; + +import static com.exonum.binding.common.serialization.json.JsonSerializer.json; + +import com.exonum.client.response.ConsensusStatus; +import com.exonum.client.response.HealthCheckInfo; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; +import lombok.Value; + +/** + * Utility class for Exonum System API. + */ +final class SystemApiHelper { + + static HealthCheckInfo parseHealthCheckJson(String json) { + // TODO: ECR-2925 (core dependency) change after the response format update + HealthCheckResponse response = json().fromJson(json, HealthCheckResponse.class); + String consensusStatus = response.getConsensusStatus().toUpperCase(); + JsonElement connectivity = response.getConnectivity(); + if (connectivity.isJsonObject()) { + JsonObject connectivityObject = connectivity.getAsJsonObject(); + int connectionsNumber = connectivityObject + .get("Connected").getAsJsonObject() + .get("amount").getAsInt(); + return new HealthCheckInfo(ConsensusStatus.valueOf(consensusStatus), connectionsNumber); + } else { + return new HealthCheckInfo(ConsensusStatus.valueOf(consensusStatus), 0); + } + } + + static int parseMemoryPoolJson(String json) { + MemoryPoolResponse response = json().fromJson(json, MemoryPoolResponse.class); + return response.getSize(); + } + + /** + * Json object wrapper for memory pool response. + */ + @Value + private static class MemoryPoolResponse { + int size; + } + + /** + * Json object wrapper for health check response. + */ + @Value + private class HealthCheckResponse { + @SerializedName("consensus_status") + String consensusStatus; + JsonElement connectivity; + } + + private SystemApiHelper() { + throw new UnsupportedOperationException("Not instantiable"); + } +} diff --git a/exonum-light-client/src/main/java/com/exonum/client/SubmitTxRequest.java b/exonum-light-client/src/main/java/com/exonum/client/response/ConsensusStatus.java similarity index 61% rename from exonum-light-client/src/main/java/com/exonum/client/SubmitTxRequest.java rename to exonum-light-client/src/main/java/com/exonum/client/response/ConsensusStatus.java index 9feadb2ed0..ba7fd3ac37 100644 --- a/exonum-light-client/src/main/java/com/exonum/client/SubmitTxRequest.java +++ b/exonum-light-client/src/main/java/com/exonum/client/response/ConsensusStatus.java @@ -15,16 +15,23 @@ * */ -package com.exonum.client; - -import com.google.gson.annotations.SerializedName; -import lombok.Value; +package com.exonum.client.response; /** - * Json object wrapper for submit transaction request. + * Consensus status of a particular node. */ -@Value -class SubmitTxRequest { - @SerializedName("tx_body") - String body; +public enum ConsensusStatus { + /** + * Shows that consensus is active + * i.e. it is enabled and the node has enough connected peers. + */ + ACTIVE, + /** + * Shows that consensus is enabled on the node. + */ + ENABLED, + /** + * Shows that consensus is disabled on the node. + */ + DISABLED } diff --git a/exonum-light-client/src/main/java/com/exonum/client/SubmitTxResponse.java b/exonum-light-client/src/main/java/com/exonum/client/response/HealthCheckInfo.java similarity index 59% rename from exonum-light-client/src/main/java/com/exonum/client/SubmitTxResponse.java rename to exonum-light-client/src/main/java/com/exonum/client/response/HealthCheckInfo.java index f04341a70c..2b8f7284a0 100644 --- a/exonum-light-client/src/main/java/com/exonum/client/SubmitTxResponse.java +++ b/exonum-light-client/src/main/java/com/exonum/client/response/HealthCheckInfo.java @@ -15,17 +15,22 @@ * */ -package com.exonum.client; +package com.exonum.client.response; -import com.exonum.binding.common.hash.HashCode; -import com.google.gson.annotations.SerializedName; import lombok.Value; -/** - * Json object wrapper for submit transaction response. - */ @Value -class SubmitTxResponse { - @SerializedName("tx_hash") - HashCode hash; +public class HealthCheckInfo { + /** + * Shows information about whether it is possible to achieve the consensus between + * validators in the current state. + */ + ConsensusStatus consensusStatus; + + /** + * The number of peers that the node is connected to; + * {@code = 0} if the node is not connected to the network, + * or it's the single node network. + */ + int connectionsNumber; } diff --git a/exonum-light-client/src/test/java/com/exonum/client/ExonumHttpClientIntegrationTest.java b/exonum-light-client/src/test/java/com/exonum/client/ExonumHttpClientIntegrationTest.java index b5477795dd..ee1c41d74a 100644 --- a/exonum-light-client/src/test/java/com/exonum/client/ExonumHttpClientIntegrationTest.java +++ b/exonum-light-client/src/test/java/com/exonum/client/ExonumHttpClientIntegrationTest.java @@ -20,13 +20,19 @@ import static com.exonum.binding.common.crypto.CryptoFunctions.ed25519; import static com.exonum.binding.common.serialization.json.JsonSerializer.json; import static com.exonum.client.ExonumHttpClient.HEX_ENCODER; +import static com.exonum.client.ExonumUrls.HEALTH_CHECK; +import static com.exonum.client.ExonumUrls.MEMORY_POOL; import static com.exonum.client.ExonumUrls.SUBMIT_TRANSACTION; +import static com.exonum.client.ExonumUrls.USER_AGENT; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import com.exonum.binding.common.crypto.KeyPair; import com.exonum.binding.common.hash.HashCode; import com.exonum.binding.common.message.TransactionMessage; +import com.exonum.client.ExplorerApiHelper.SubmitTxRequest; +import com.exonum.client.response.ConsensusStatus; +import com.exonum.client.response.HealthCheckInfo; import java.io.IOException; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -89,4 +95,59 @@ void submitTransactionTest() throws InterruptedException { assertThat(actualTxMessage, is(txMessage)); } + @Test + void getUnconfirmedTransactions() throws InterruptedException { + // Mock response + int mockCount = 10; + String mockResponse = "{\"size\": " + mockCount + " }"; + server.enqueue(new MockResponse().setBody(mockResponse)); + + // Call + int actualCount = exonumClient.getUnconfirmedTransactionsCount(); + + // Assert response + assertThat(actualCount, is(mockCount)); + + // Assert request params + RecordedRequest recordedRequest = server.takeRequest(); + assertThat(recordedRequest.getMethod(), is("GET")); + assertThat(recordedRequest.getPath(), is(MEMORY_POOL)); + } + + @Test + void healthCheck() throws InterruptedException { + // Mock response + HealthCheckInfo expected = new HealthCheckInfo(ConsensusStatus.ENABLED, 0); + String mockResponse = "{\"consensus_status\": \"Enabled\", \"connectivity\": \"NotConnected\"}"; + server.enqueue(new MockResponse().setBody(mockResponse)); + + // Call + HealthCheckInfo actual = exonumClient.healthCheck(); + + // Assert response + assertThat(actual, is(expected)); + + // Assert request params + RecordedRequest recordedRequest = server.takeRequest(); + assertThat(recordedRequest.getMethod(), is("GET")); + assertThat(recordedRequest.getPath(), is(HEALTH_CHECK)); + } + + @Test + void getUserAgentInfo() throws InterruptedException { + // Mock response + String mockResponse = "exonum 0.6.0/rustc 1.26.0 (2789b067d 2018-03-06)\n\n/Mac OS10.13.3"; + server.enqueue(new MockResponse().setBody(mockResponse)); + + // Call + String actualResponse = exonumClient.getUserAgentInfo(); + + // Assert response + assertThat(actualResponse, is(mockResponse)); + + // Assert request params + RecordedRequest recordedRequest = server.takeRequest(); + assertThat(recordedRequest.getMethod(), is("GET")); + assertThat(recordedRequest.getPath(), is(USER_AGENT)); + } } diff --git a/exonum-light-client/src/test/java/com/exonum/client/ExplorerApiHelperTest.java b/exonum-light-client/src/test/java/com/exonum/client/ExplorerApiHelperTest.java new file mode 100644 index 0000000000..2cdd95d6c5 --- /dev/null +++ b/exonum-light-client/src/test/java/com/exonum/client/ExplorerApiHelperTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019 The Exonum Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.exonum.client; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.isJson; +import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import com.exonum.binding.common.hash.HashCode; +import org.junit.jupiter.api.Test; + +class ExplorerApiHelperTest { + + @Test + void createSubmitTxBody() { + String msg = "some data"; + String json = ExplorerApiHelper.createSubmitTxBody(msg); + + assertThat(json, isJson(withJsonPath("$.tx_body", equalTo(msg)))); + } + + @Test + void parseSubmitTxResponse() { + String expected = "f128c720e04b8243"; + String json = "{\"tx_hash\":\"" + expected + "\"}"; + + HashCode actual = ExplorerApiHelper.parseSubmitTxResponse(json); + assertThat(actual, equalTo(HashCode.fromString(expected))); + } +} diff --git a/exonum-light-client/src/test/java/com/exonum/client/SystemApiHelperTest.java b/exonum-light-client/src/test/java/com/exonum/client/SystemApiHelperTest.java new file mode 100644 index 0000000000..c08ae55d48 --- /dev/null +++ b/exonum-light-client/src/test/java/com/exonum/client/SystemApiHelperTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019 The Exonum Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.exonum.client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import com.exonum.client.response.ConsensusStatus; +import com.exonum.client.response.HealthCheckInfo; +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class SystemApiHelperTest { + + @ParameterizedTest + @MethodSource("healthCheckSource") + void parseHealthCheckJson(String json, HealthCheckInfo expected) { + HealthCheckInfo actual = SystemApiHelper.parseHealthCheckJson(json); + assertThat(actual, is(expected)); + } + + @ParameterizedTest + @MethodSource("memoryPoolSource") + void parseMemoryPoolJson(String json, int expected) { + int actual = SystemApiHelper.parseMemoryPoolJson(json); + assertThat(actual, is(expected)); + } + + private static List memoryPoolSource() { + return ImmutableList.of( + Arguments.of("{\"size\": 0}", 0), + Arguments.of("{\"size\": 2}", 2), + Arguments.of("{\"size\": " + Integer.MAX_VALUE + "}", Integer.MAX_VALUE) + ); + } + + private static List healthCheckSource() { + return ImmutableList.of( + Arguments.of("{\"consensus_status\": \"Enabled\", \"connectivity\": \"NotConnected\"}", + new HealthCheckInfo(ConsensusStatus.ENABLED, 0)), + Arguments.of("{\"consensus_status\": \"Disabled\", \"connectivity\": \"NotConnected\"}", + new HealthCheckInfo(ConsensusStatus.DISABLED, 0)), + Arguments.of("{\"consensus_status\": \"Active\"," + + "\"connectivity\": {\"Connected\": {\"amount\": 1 } }" + + "}", + new HealthCheckInfo(ConsensusStatus.ACTIVE, 1)) + ); + } + +}