Skip to content

Commit

Permalink
Proxy integration tests (Consensys#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
jframe authored and CjHare committed Mar 13, 2019
1 parent 7bdfa5d commit ad6bee9
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 4 deletions.
3 changes: 3 additions & 0 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ try {
stage('Test') {
sh './gradlew --no-daemon --parallel test'
}
stage('Integration Test') {
sh './gradlew --no-daemon --parallel integrationTest'
}
} finally {
archiveArtifacts '**/build/reports/**'
archiveArtifacts '**/build/test-results/**'
Expand Down
4 changes: 4 additions & 0 deletions ethfirewall/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ dependencies {
testImplementation 'junit:junit'
testImplementation 'org.assertj:assertj-core'
testImplementation 'org.mockito:mockito-core'

integrationTestImplementation 'io.rest-assured:rest-assured'
integrationTestImplementation 'org.assertj:assertj-core'
integrationTestImplementation 'org.mock-server:mockserver-netty'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright 2019 ConsenSys AG.
*
* 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 tech.pegasys.ethfirewall.jsonrpcproxy;

import static io.restassured.RestAssured.given;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.mockserver.integration.ClientAndServer.startClientAndServer;
import static org.mockserver.matchers.Times.exactly;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;
import static org.mockserver.model.JsonBody.json;
import static org.web3j.utils.Async.defaultExecutorService;

import tech.pegasys.ethfirewall.Runner;

import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.Resources;
import io.restassured.RestAssured;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpServerOptions;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.mockserver.integration.ClientAndServer;
import org.mockserver.model.Header;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.web3j.crypto.CipherException;
import org.web3j.protocol.core.JsonRpc2_0Web3j;
import org.web3j.protocol.core.Request;
import org.web3j.protocol.core.Response;

public class IntegrationTestBase {
private static final Logger LOG = LoggerFactory.getLogger(IntegrationTestBase.class);

private static final String LOCALHOST = "127.0.0.1";
private static Runner runner;
private static ClientAndServer ethNode;
private ObjectMapper objectMapper = new ObjectMapper();
JsonRpc2_0Web3j jsonRpc;

@BeforeClass
public static void setupEthFirewall() throws IOException, CipherException {
ethNode = startClientAndServer();

final File keyFile = createKeyFile();
final TransactionSigner transactionSigner = TransactionSigner.createFrom(keyFile, "password");

final HttpClientOptions httpClientOptions = new HttpClientOptions();
httpClientOptions.setDefaultHost(LOCALHOST);
httpClientOptions.setDefaultPort(ethNode.getLocalPort());

final ServerSocket serverSocket = new ServerSocket(0);
RestAssured.port = serverSocket.getLocalPort();
final HttpServerOptions httpServerOptions = new HttpServerOptions();
httpServerOptions.setPort(serverSocket.getLocalPort());
httpServerOptions.setHost("localhost");

runner = new Runner(transactionSigner, httpClientOptions, httpServerOptions);
runner.start();

LOG.info(
"Started ethFirewall on port {}, eth stub node on port {}",
serverSocket.getLocalPort(),
ethNode.getLocalPort());
serverSocket.close();
}

@SuppressWarnings("UnstableApiUsage")
private static File createKeyFile() throws IOException {
final URL walletResource = Resources.getResource("keyfile.json");
final Path wallet = Files.createTempFile("ethfirewall_intg_keyfile", ".json");
Files.write(wallet, Resources.toString(walletResource, UTF_8).getBytes(UTF_8));
File keyFile = wallet.toFile();
keyFile.deleteOnExit();
return keyFile;
}

@Before
public void setup() {
jsonRpc = new JsonRpc2_0Web3j(null, 2000, defaultExecutorService());
ethNode.reset();
}

@AfterClass
public static void teardown() {
ethNode.stop();
runner.stop();
}

public void configureEthNode(
final Request<?, ? extends Response<?>> request,
final Object response,
final Map<String, String> responseHeaders,
final int responseStatusCode)
throws JsonProcessingException {
final String requestBody = objectMapper.writeValueAsString(request);
final String responseBody = objectMapper.writeValueAsString(response);
List<Header> headers = convertHeadersToMockServerHeaders(responseHeaders);
ethNode
.when(request().withBody(json(requestBody)), exactly(1))
.respond(
response()
.withBody(responseBody)
.withHeaders(headers)
.withStatusCode(responseStatusCode));
}

public void sendRequestAndVerify(
final Request<?, ? extends Response<?>> proxyBodyRequest,
final Map<String, String> proxyHeaders,
final Object response,
final int ethNodeStatusCode,
final Map<String, String> ethNodeHeaders)
throws JsonProcessingException {
String responseBody = objectMapper.writeValueAsString(response);
given()
.when()
.body(proxyBodyRequest)
.headers(proxyHeaders)
.post()
.then()
.statusCode(ethNodeStatusCode)
.body(equalTo(responseBody))
.headers(ethNodeHeaders);
}

public void verifyEthNodeRequest(
final Request<?, ? extends Response<?>> proxyBodyRequest,
final Map<String, String> proxyHeaders) {
ethNode.verify(
request()
.withBody(json(proxyBodyRequest))
.withHeaders(convertHeadersToMockServerHeaders(proxyHeaders)));
}

private List<Header> convertHeadersToMockServerHeaders(final Map<String, String> headers) {
return headers.entrySet().stream()
.map(e -> new Header(e.getKey(), e.getValue()))
.collect(toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2019 ConsenSys AG.
*
* 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 tech.pegasys.ethfirewall.jsonrpcproxy;

import static java.util.Collections.emptyMap;

import java.math.BigInteger;
import java.util.Map;

import com.google.common.collect.ImmutableMap;
import org.junit.Test;
import org.web3j.protocol.core.Request;
import org.web3j.protocol.core.Response;
import org.web3j.protocol.core.methods.request.Transaction;
import org.web3j.protocol.core.methods.response.EthProtocolVersion;
import org.web3j.protocol.core.methods.response.EthSendTransaction;
import org.web3j.protocol.core.methods.response.NetVersion;

public class ProxyIntegrationTest extends IntegrationTestBase {

@Test
public void requestWithHeadersIsProxied() throws Exception {
final Request<?, NetVersion> netVersionRequest = jsonRpc.netVersion();
final Response<String> netVersionResponse = new NetVersion();
netVersionResponse.setResult("4");

final Map<String, String> requestHeaders = ImmutableMap.of("Accept", "*/*");
final Map<String, String> responseHeaders = ImmutableMap.of("Content-Type", "Application/Json");

configureEthNode(netVersionRequest, netVersionResponse, responseHeaders, 200);
sendRequestAndVerify(
netVersionRequest, requestHeaders, netVersionResponse, 200, responseHeaders);
verifyEthNodeRequest(netVersionRequest, requestHeaders);
}

@Test
public void requestReturningErrorIsProxied() throws Exception {
final Request<?, EthProtocolVersion> ethProtocolVersionRequest = jsonRpc.ethProtocolVersion();
configureEthNode(ethProtocolVersionRequest, "Not Found", emptyMap(), 404);
sendRequestAndVerify(ethProtocolVersionRequest, emptyMap(), "Not Found", 404, emptyMap());
verifyEthNodeRequest(ethProtocolVersionRequest, emptyMap());
}

@Test
public void requestWithSendTransactionIsSignedBeforeProxying() throws Exception {
final Transaction transaction =
new Transaction(
"0xb60e8dd61c5d32be8058bb8eb970870f07233155",
new BigInteger(
"101454411220705080123888225389655371100299455501706857686025051036223022797554"),
new BigInteger("10000000000000"),
new BigInteger("30400"),
"0xd46e8dd67c5d32be8058bb8eb970870f07244567",
new BigInteger("2441406250"),
"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675");
final Request<?, ? extends Response<?>> ethSendTransactionRequest =
jsonRpc.ethSendTransaction(transaction);
ethSendTransactionRequest.setId(5);

Request<?, ? extends Response<?>> ethSendRawTransactionRequest =
jsonRpc.ethSendRawTransaction(
"0xf8b2a0e04d296d2460cfb8472af2c5fd05b5a214109c25688d3704aed5484f9a7792f2"
+ "8609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f07244567849184e72aa9d46e8dd67c5"
+ "d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f0724456751ca03631b1cec6"
+ "e5033e8a2bff6d1b2d08bfe106cbbb82df5eb7b380a1fdb5c06be2a06d15eeb833f26114de087c930e375"
+ "56d93a47d86e6554e988d32cbbb273cfda4");
// we create the eth_sendRawTransaction req with same id as the eth_sendTransaction req
ethSendRawTransactionRequest.setId(5);

Response<String> ethSendRawTransactionResponse = new EthSendTransaction();
ethSendRawTransactionResponse.setResult(
"0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1527331");

configureEthNode(ethSendRawTransactionRequest, ethSendRawTransactionResponse, emptyMap(), 200);
sendRequestAndVerify(
ethSendTransactionRequest, emptyMap(), ethSendRawTransactionResponse, 200, emptyMap());
verifyEthNodeRequest(ethSendRawTransactionRequest, emptyMap());
}
}
1 change: 1 addition & 0 deletions ethfirewall/src/integration-test/resources/keyfile.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"address":"7577919ae5df4941180eac211965f275cdce314d","id":"9862127a-cd5f-4295-90ab-45fad5615fca","version":3,"crypto":{"cipher":"aes-128-ctr","ciphertext":"1f98ace266ca154c920554291173c0878956253e6a6637b731297551a7d34e53","cipherparams":{"iv":"c0210872526f6e4a41cbb6664e6588b4"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"685fbefccd6075cd1cbd63f9f6a013b69df1cfd110652386353edcbdb0777fc9"},"mac":"186192bc59e6f2a5796df853435507e9662bfa8b98f7e47fd54f631c9a584692"}}
14 changes: 10 additions & 4 deletions ethfirewall/src/main/java/tech/pegasys/ethfirewall/Runner.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public class Runner {
private TransactionSigner transactionSigner;
private HttpClientOptions clientOptions;
private HttpServerOptions serverOptions;
private Vertx vertx;
private String deploymentId;

public Runner(
final TransactionSigner transactionSigner,
Expand All @@ -45,13 +47,16 @@ public Runner(

public void start() {
// NOTE: Starting vertx spawns daemon threads, meaning the app may complete, but not terminate.
final Vertx vertx = Vertx.vertx();
vertx = Vertx.vertx();
final RequestMapper requestMapper = createRequestMapper(vertx, transactionSigner);
final JsonRpcHttpService httpService = new JsonRpcHttpService(serverOptions, requestMapper);

vertx.deployVerticle(httpService, this::handleDeployResult);
}

public void stop() {
vertx.undeploy(deploymentId);
}

private RequestMapper createRequestMapper(
final Vertx vertx, final TransactionSigner transactionSigner) {

Expand All @@ -71,9 +76,10 @@ private RequestMapper createRequestMapper(
return requestMapper;
}

private void handleDeployResult(final AsyncResult<?> result) {
private void handleDeployResult(final AsyncResult<String> result) {
if (result.succeeded()) {
LOG.info("Deployment id is: {}", result.result());
deploymentId = result.result();
LOG.info("Deployment id is: {}", deploymentId);
} else {
LOG.warn("Deployment failed! ", result.cause());
}
Expand Down
4 changes: 4 additions & 0 deletions gradle/versions.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ dependencyManagement {

dependency 'org.mockito:mockito-core:2.23.4'

dependency 'io.rest-assured:rest-assured:3.0.7'

dependency 'org.mock-server:mockserver-netty:5.5.1'

dependency 'org.web3j:abi:4.1.1'
dependency 'org.web3j:core:4.1.1'
dependency 'org.web3j:crypto:4.1.1'
Expand Down

0 comments on commit ad6bee9

Please sign in to comment.