Compare commits

...

3 Commits

Author SHA1 Message Date
a0c91ee9ce SpringBoot: Flow Polling 2023-09-01 23:06:34 +02:00
5035a16930 SpringBoot: CordaClient: ResponseBody DAO 2023-09-01 16:58:43 +02:00
f534b70bd2 SpringBoot: test reorganization 2023-09-01 16:20:40 +02:00
15 changed files with 311 additions and 124 deletions

View File

@ -29,3 +29,15 @@ dependencies {
tasks.named('test') { tasks.named('test') {
useJUnitPlatform() useJUnitPlatform()
} }
// Mostly necessary for VsCode IDE
sourceSets {
test {
java {
srcDirs = ['src/test/java']
}
resources {
srcDirs = ['src/test/resources']
}
}
}

View File

@ -1,6 +1,10 @@
package djmil.cordacheckers.cordaclient; package djmil.cordacheckers.cordaclient;
import static java.util.Objects.requireNonNull;
import java.util.List; import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@ -10,21 +14,27 @@ import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import djmil.cordacheckers.cordaclient.dao.HoldingIdentity;
import djmil.cordacheckers.cordaclient.dao.VirtualNode; import djmil.cordacheckers.cordaclient.dao.VirtualNode;
import djmil.cordacheckers.cordaclient.dao.VirtualNodeList; import djmil.cordacheckers.cordaclient.dao.VirtualNodeList;
import djmil.cordacheckers.cordaclient.dao.flow.RequestBody;
import static java.util.Objects.requireNonNull; import djmil.cordacheckers.cordaclient.dao.flow.ResponseBody;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.Empty;
@Service @Service
public class CordaClient { public class CordaClient {
private final RestTemplate restTemplate; private final RestTemplate restTemplate;
private final ObjectMapper jsonMapper;
public CordaClient(RestTemplate restTemplate) { public CordaClient(RestTemplate restTemplate, ObjectMapper jsonMapper) {
//System.out.println("Creating REST Service"); //System.out.println("Creating REST Service");
// this.restTemplate = restTemplateBuilder // this.restTemplate = restTemplateBuilder
// .basicAuthentication("admin", "admin") // .basicAuthentication("admin", "admin")
// .build(); // .build();
this.restTemplate = restTemplate; this.restTemplate = restTemplate;
this.jsonMapper = jsonMapper;
} }
public List<VirtualNode> getVirtualNodeList() { public List<VirtualNode> getVirtualNodeList() {
@ -49,14 +59,126 @@ public class CordaClient {
.virtualNodes(); .virtualNodes();
} }
// public String getGemeProposals(String ) { /**
// // Request authorization header * Obtain list of unconsumed (active) GameProposals
// HttpHeaders headers = basicAuthorizationHeader(); * @param holdingIdentity
* @return GameProposals list in JSON form
*/
public String listGameProposals(HoldingIdentity holdingIdentity) {
// // Request final RequestBody requestBody = new RequestBody(
// final HttpEntity<String> request = new HttpEntity<>(headers); "list-" + UUID.randomUUID(),
"djmil.cordacheckers.gameproposal.ListFlow",
new Empty()
);
// } final String gameProposalsJsonString = cordaFlowExecute(
holdingIdentity,
requestBody
);
return gameProposalsJsonString;
}
private String cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody) {
try {
final String requestBodyJson = this.jsonMapper.writeValueAsString(requestBody);
final ResponseBody startedFlow = cordaFlowPost(
holdingIdentity,
requestBodyJson
);
final String flowExecutionResult = cordaFlowPoll(startedFlow);
// NOTE:
// At this point, real production code, probably should convert data between CordaFlow
// abstarction into ReactApp abstraction. Instead, to limit boring json shuffling, all
// family of Corda.List flows were deliberatly designed to return frontend frendly JSONs.
// At the same time, all other Corda flows, simply return plain text string with
// operation result.
return flowExecutionResult;
}
catch (Exception e) {
throw new RuntimeException("Unable to perform "+requestBody.flowClassName()
+". Reason "+e.getMessage());
}
}
private ResponseBody cordaFlowPost(HoldingIdentity holdingIdentity, String requestBodyJson) {
final HttpHeaders requestHeaders = basicAuthorizationHeader();
final HttpEntity<String> request = new HttpEntity<>(requestBodyJson, requestHeaders);
final ResponseEntity<ResponseBody> responce = this.restTemplate.exchange(
"https://localhost:8888/api/v1/flow/" + holdingIdentity.shortHash(),
HttpMethod.POST,
request,
ResponseBody.class
);
if (!responce.getStatusCode().is2xxSuccessful()) {
throw new RuntimeException("CordaClient.startCordaFlow: unexpected request status "
+responce.getStatusCode()) ;
}
final ResponseBody responseBody = requireNonNull(
responce.getBody(),
"CordaClient.startCordaFlow: empty getBody()"
);
if (!responseBody.isFlowStarted()) {
throw new RuntimeException("CordaClient.startCordaFlow: Unexpected status: "
+responseBody.flowStatus() + ": "
+responseBody.flowError());
}
return responseBody;
}
private String cordaFlowPoll(ResponseBody startedFlow) throws InterruptedException {
final HttpHeaders requestHeaders = basicAuthorizationHeader();
final HttpEntity<String> request = new HttpEntity<>(requestHeaders);
for (int retry = 0; retry < 6; retry++) {
// Give Corda cluster some time to process our request
TimeUnit.SECONDS.sleep(retry*retry +1); // 1 2 5 8 17 33 sec
final ResponseEntity<ResponseBody> responce = this.restTemplate.exchange(
"https://localhost:8888/api/v1/flow/"
+ startedFlow.holdingIdentityShortHash()+"/"
+ startedFlow.clientRequestId(),
HttpMethod.GET,
request,
ResponseBody.class
);
if (responce.getStatusCode() != HttpStatus.OK) {
throw new RuntimeException("CordaClient.cordaFlowPoll: unexpected request status "
+responce.getStatusCode()) ;
}
final ResponseBody responseBody = requireNonNull(
responce.getBody(),
"CordaClient.cordaFlowPoll: empty getBody()"
);
if (responseBody.isFlowCompleted() && responseBody.flowResult() != null) {
System.out.println("Completed "+responseBody.flowResult());
return responseBody.flowResult();
} else
if (responseBody.flowError() != null) {
return "Flow execution error: " +responseBody.flowError();
} else
if (!responseBody.isFlowRunning()) {
return "Unexpect ResponseBody status: " +responseBody.flowStatus();
}
}
return "CordaClient.cordaFlowPoll: retry limit";
}
private HttpHeaders basicAuthorizationHeader() { private HttpHeaders basicAuthorizationHeader() {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();

View File

@ -30,9 +30,9 @@ public class HoldingIdentityResolver {
private static Map<String, HoldingIdentity> setCache(CordaClient cordaClient) { private static Map<String, HoldingIdentity> setCache(CordaClient cordaClient) {
Map<String, HoldingIdentity> map = new HashMap<>(); Map<String, HoldingIdentity> map = new HashMap<>();
try {
List<VirtualNode> vNodeList = cordaClient.getVirtualNodeList(); List<VirtualNode> vNodeList = cordaClient.getVirtualNodeList();
try {
for (VirtualNode vNode : vNodeList) { for (VirtualNode vNode : vNodeList) {
var identity = vNode.holdingIdentity(); var identity = vNode.holdingIdentity();

View File

@ -0,0 +1,7 @@
package djmil.cordacheckers.cordaclient.dao.flow;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@JsonSerialize
public record RequestBody (String clientRequestId, String flowClassName, Object requestBody) {
}

View File

@ -0,0 +1,25 @@
package djmil.cordacheckers.cordaclient.dao.flow;
public record ResponseBody(
String holdingIdentityShortHash,
String clientRequestId,
String flowId,
String flowStatus,
String flowResult,
String flowError,
String timestamp
) {
public boolean isFlowStarted() {
return this.flowStatus.equals("START_REQUESTED");
}
public boolean isFlowRunning() {
return this.flowStatus.equals("RUNNING");
}
public boolean isFlowCompleted() {
return this.flowStatus.equals("COMPLETED");
}
}

View File

@ -0,0 +1,5 @@
package djmil.cordacheckers.cordaclient.dao.flow.arguments;
public record CreateGameProposal(String opponentName, String opponentColor, String additionalMessage) {
}

View File

@ -0,0 +1,8 @@
package djmil.cordacheckers.cordaclient.dao.flow.arguments;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@JsonSerialize
public class Empty {
}

View File

@ -1,33 +0,0 @@
package djmil.cordacheckers;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import djmil.cordacheckers.cordaclient.CordaClient;
import djmil.cordacheckers.cordaclient.dao.HoldingIdentity;
import djmil.cordacheckers.cordaclient.dao.VirtualNode;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.List;
import javax.naming.InvalidNameException;
@SpringBootTest
public class CordaClientTest {
@Autowired
CordaClient cordaclient;
@Test
void whenGetVirtualnode_thenListHoldingIdentity() throws GeneralSecurityException, IOException, InvalidNameException {
List<VirtualNode> vNodes = cordaclient.getVirtualNodeList();
HoldingIdentity identity = vNodes.get(0).holdingIdentity();
assertThat(identity.getName()).isEqualTo("Bob");
}
}

View File

@ -1,36 +1,9 @@
package djmil.cordacheckers; package djmil.cordacheckers;
import java.io.IOException;
import java.security.GeneralSecurityException;
import javax.net.ssl.SSLContext;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.ssl.SSLContexts;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import djmil.cordacheckers.cordaclient.dao.HoldingIdentity;
import djmil.cordacheckers.cordaclient.dao.VirtualNodeList;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest @SpringBootTest
@ -46,53 +19,4 @@ class CordacheckersApplicationTests {
void contextLoads() { void contextLoads() {
} }
@Test
void givenAcceptOnlyCACertificates_whenHttpsUrlIsConsumed_thenOk() throws GeneralSecurityException, IOException {
final SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(trustStore.getURL(), trustStorePassword.toCharArray())
.build();
final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
final Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory> create()
.register("https", sslsf)
.register("http", new PlainConnectionSocketFactory())
.build();
final BasicHttpClientConnectionManager connectionManager =
new BasicHttpClientConnectionManager(socketFactoryRegistry);
final CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.build();
final HttpComponentsClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory(httpClient);
//requestFactory.setReadTimeout(readTimeout);
//requestFactory.setConnectTimeout(connectTimeout);
// Request authorization header
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth("admin", "admin");
// String authStr = "username:password";
// String base64Creds = Base64.getEncoder().encodeToString(authStr.getBytes());
// headers.add("Authorization", "Basic " + base64Creds);
// Request
final HttpEntity<String> request = new HttpEntity<>(headers);
final ResponseEntity<VirtualNodeList> response = new RestTemplate(requestFactory)
.exchange("https://localhost:8888/api/v1/virtualnode", HttpMethod.GET, request, VirtualNodeList.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.hasBody());
VirtualNodeList vNodeList = response.getBody();
assertThat(vNodeList).isNotNull();
if (vNodeList != null) {
assertThat(vNodeList.virtualNodes().size() == 5);
}
HoldingIdentity identity = vNodeList.virtualNodes().get(0).holdingIdentity();
assertThat(identity.x500Name().contains("NotaryRep1"));
}
} }

View File

@ -0,0 +1,41 @@
package djmil.cordacheckers.cordaclient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.fasterxml.jackson.core.JsonProcessingException;
import djmil.cordacheckers.cordaclient.dao.VirtualNode;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import javax.naming.InvalidNameException;
@SpringBootTest
public class CordaClientTest {
@Autowired
CordaClient cordaClient;
@Autowired
HoldingIdentityResolver holdingIdentityResolver;
@Test
void testGetVirtualNodeList() throws InvalidNameException {
List<VirtualNode> vNodes = cordaClient.getVirtualNodeList();
assertThat(vNodes.size()).isEqualTo(5); // default vNode config for CSDE
}
@Test
void testListGameProposals() throws JsonProcessingException {
String resp = cordaClient.listGameProposals(
holdingIdentityResolver.getByCommonName("alice"));
System.out.println("testListGameProposals "+ resp);
}
}

View File

@ -1,9 +1,7 @@
package djmil.cordacheckers; package djmil.cordacheckers.cordaclient.dao;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import djmil.cordacheckers.cordaclient.dao.HoldingIdentity;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import javax.naming.InvalidNameException; import javax.naming.InvalidNameException;

View File

@ -0,0 +1,31 @@
package djmil.cordacheckers.cordaclient.dao.flow;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.Empty;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
@JsonTest
public class RequestBodyTest {
@Autowired
private JacksonTester<RequestBody> jTester;
@Test
void listFlowTest() throws IOException {
RequestBody requestBody = new RequestBody(
"list-2",
"djmil.cordacheckers.gameproposal.ListFlow",
new Empty()
);
assertThat(jTester.write(requestBody)).isEqualToJson("requestBody/ListFlow.json");
}
}

View File

@ -0,0 +1,33 @@
package djmil.cordacheckers.cordaclient.dao.flow;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
@JsonTest
public class ResponseBodyTest {
@Autowired
private JacksonTester<ResponseBody> jTester;
@Test
void GameProposalTest() throws IOException {
ResponseBody responseBody = new ResponseBody(
"804313813EED",
"game-proposal-11",
"ec913fe4-12cb-4867-acf0-4b8d24879362",
"COMPLETED",
"SHA-256D:67B08F2A94B1C38F6A42AC109FDCD6126B6B8F96FB2097DF81555956CC2AA7FB",
null,
"2023-08-29T14:41:05.600Z"
);
assertThat(jTester.write(responseBody)).isEqualToJson("ResponseBody-GameProposal.json");
}
}

View File

@ -0,0 +1,9 @@
{
"holdingIdentityShortHash": "804313813EED",
"clientRequestId": "game-proposal-11",
"flowId": "ec913fe4-12cb-4867-acf0-4b8d24879362",
"flowStatus": "COMPLETED",
"flowResult": "SHA-256D:67B08F2A94B1C38F6A42AC109FDCD6126B6B8F96FB2097DF81555956CC2AA7FB",
"flowError": null,
"timestamp": "2023-08-29T14:41:05.600Z"
}

View File

@ -0,0 +1,5 @@
{
"clientRequestId": "list-2",
"flowClassName": "djmil.cordacheckers.gameproposal.ListFlow",
"requestBody": {}
}