CORDA-916: Add registerFlowFactory method to public test API ()

* Add registerFlowFactory method to public test API

* Return CordaFuture rather than a plain Future

* Rename method

* Improve KDoc

* Hide internal interface with public wrapper

* Modify API current to include ResponderFlowFactory interface

* Take API definition change from generated file

* Note API change in changelog
This commit is contained in:
Dominic Fox 2018-07-31 13:33:18 +01:00 committed by GitHub
parent 93bb24ed17
commit 4542e0cd06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 275 additions and 0 deletions
.ci
docs/source
testing/node-driver/src
main/kotlin/net/corda/testing/node
test
java/net/corda/testing/node
kotlin/net/corda/testing/node/internal

View File

@ -6517,6 +6517,10 @@ public final class net.corda.testing.node.NotarySpec extends java.lang.Object
public int hashCode()
public String toString()
##
public interface net.corda.testing.node.ResponderFlowFactory
@NotNull
public abstract F invoke(net.corda.core.flows.FlowSession)
##
public final class net.corda.testing.node.StartedMockNode extends java.lang.Object
@NotNull
public final java.util.List<kotlin.Pair<F, net.corda.core.concurrent.CordaFuture<?>>> findStateMachines(Class<F>)

View File

@ -6,6 +6,8 @@ release, see :doc:`upgrade-notes`.
Unreleased
----------
* Added ``registerResponderFlow`` method to ``StartedMockNode``, to support isolated testing of responder flow behaviour.
* Introduced ``TestCorDapp`` and utilities to support asymmetric setups for nodes through ``DriverDSL``, ``MockNetwork`` and ``MockServices``.
* Change type of the `checkpoint_value` column. Please check the upgrade-notes on how to update your database.

View File

@ -4,12 +4,15 @@ import com.google.common.jimfs.Jimfs
import net.corda.core.concurrent.CordaFuture
import net.corda.core.crypto.random63BitValue
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo
import net.corda.core.node.ServiceHub
import net.corda.core.toFuture
import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.InitiatedFlowFactory
import net.corda.node.services.config.NodeConfiguration
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.DUMMY_NOTARY_NAME
@ -18,6 +21,7 @@ import net.corda.testing.node.internal.*
import rx.Observable
import java.math.BigInteger
import java.nio.file.Path
import java.util.concurrent.Future
/**
* Immutable builder for configuring a [StartedMockNode] or an [UnstartedMockNode] via [MockNetwork.createNode] and
@ -177,8 +181,69 @@ class StartedMockNode private constructor(private val node: TestStartedNode) {
statement()
}
}
/**
* Register an [InitiatedFlowFactory], to control relationship between initiating and receiving flow classes
* explicitly on a node-by-node basis. This is used when we want to manually specify that a particular initiating
* flow class will have a particular responder.
*
* An [ResponderFlowFactory] is responsible for converting a [FlowSession] into the [FlowLogic] that will respond
* to the initiated flow. The registry records one responder type, and hence one factory, for each initiator flow
* type. If a factory is already registered for the type, it is overwritten in the registry when a new factory is
* registered.
*
* Note that this change affects _only_ the node on which this method is called, and not the entire network.
*
* @property initiatingFlowClass The [FlowLogic]-inheriting class to register a new responder for.
* @property flowFactory The flow factory that will create the responding flow.
* @property responderFlowClass The class of the responder flow.
* @return A [CordaFuture] that will complete the first time the responding flow is created.
*/
fun <F : FlowLogic<*>> registerResponderFlow(initiatingFlowClass: Class<out FlowLogic<*>>,
flowFactory: ResponderFlowFactory<F>,
responderFlowClass: Class<F>): CordaFuture<F> =
node.registerFlowFactory(
initiatingFlowClass,
InitiatedFlowFactory.CorDapp(flowVersion = 0, appName = "", factory = flowFactory::invoke),
responderFlowClass, true)
.toFuture()
}
/**
* Responsible for converting a [FlowSession] into the [FlowLogic] that will respond to an initiated flow.
*
* @param F The [FlowLogic]-inherited type of the responder class this factory creates.
*/
@FunctionalInterface
interface ResponderFlowFactory<F : FlowLogic<*>> {
/**
* Given the provided [FlowSession], create a responder [FlowLogic] of the desired type.
*
* @param flowSession The [FlowSession] to use to create the responder flow object.
* @return The constructed responder flow object.
*/
fun invoke(flowSession: FlowSession): F
}
/**
* Kotlin-only utility function using a reified type parameter and a lambda parameter to simplify the
* [InitiatedFlowFactory.registerFlowFactory] function.
*
* @param F The [FlowLogic]-inherited type of the responder to register.
* @property initiatingFlowClass The [FlowLogic]-inheriting class to register a new responder for.
* @property flowFactory A lambda converting a [FlowSession] into an instance of the responder class [F].
* @return A [CordaFuture] that will complete the first time the responding flow is created.
*/
inline fun <reified F : FlowLogic<*>> StartedMockNode.registerResponderFlow(
initiatingFlowClass: Class<out FlowLogic<*>>,
noinline flowFactory: (FlowSession) -> F): Future<F> =
registerResponderFlow(
initiatingFlowClass,
object : ResponderFlowFactory<F> {
override fun invoke(flowSession: FlowSession) = flowFactory(flowSession)
},
F::class.java)
/**
* A mock node brings up a suite of in-memory services in a fast manner suitable for unit testing.
* Components that do IO are either swapped out for mocks, or pointed to a [Jimfs] in memory filesystem or an in

View File

@ -0,0 +1,115 @@
package net.corda.testing.node;
import co.paralleluniverse.fibers.Suspendable;
import net.corda.core.concurrent.CordaFuture;
import net.corda.core.flows.*;
import net.corda.core.identity.Party;
import net.corda.core.utilities.UntrustworthyData;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.util.concurrent.Future;
import static java.util.Collections.singletonList;
import static org.hamcrest.Matchers.instanceOf;
/**
* Java version of test based on the example given as an answer to this SO question:
*
* https://stackoverflow.com/questions/48166626/how-can-an-acceptor-flow-in-corda-be-isolated-for-unit-testing/
*
* but using the `registerFlowFactory` method implemented in response to https://r3-cev.atlassian.net/browse/CORDA-916
*/
public class TestResponseFlowInIsolationInJava {
private final MockNetwork network = new MockNetwork(singletonList("com.template"));
private final StartedMockNode a = network.createNode();
private final StartedMockNode b = network.createNode();
@After
public void tearDown() {
network.stopNodes();
}
@Rule
public final ExpectedException exception = ExpectedException.none();
@Test
public void test() throws Exception {
// This method returns the Responder flow object used by node B.
Future<Responder> initiatedResponderFlowFuture = b.registerResponderFlow(
// We tell node B to respond to BadInitiator with Responder.
// We want to observe the Responder flow object to check for errors.
BadInitiator.class, Responder::new, Responder.class);
// We run the BadInitiator flow on node A.
BadInitiator flow = new BadInitiator(b.getInfo().getLegalIdentities().get(0));
CordaFuture<Void> future = a.startFlow(flow);
network.runNetwork();
future.get();
// We check that the invocation of the Responder flow object has caused an ExecutionException.
Responder initiatedResponderFlow = initiatedResponderFlowFuture.get();
CordaFuture initiatedResponderFlowResultFuture = initiatedResponderFlow.getStateMachine().getResultFuture();
exception.expectCause(instanceOf(FlowException.class));
exception.expectMessage("String did not contain the expected message.");
initiatedResponderFlowResultFuture.get();
}
// This is the real implementation of Initiator.
@InitiatingFlow
@StartableByRPC
public static class Initiator extends FlowLogic<Void> {
private Party counterparty;
public Initiator(Party counterparty) {
this.counterparty = counterparty;
}
@Suspendable
@Override public Void call() {
FlowSession session = initiateFlow(counterparty);
session.send("goodString");
return null;
}
}
// This is the response flow that we want to isolate for testing.
@InitiatedBy(Initiator.class)
public static class Responder extends FlowLogic<Void> {
private final FlowSession counterpartySession;
Responder(FlowSession counterpartySession) {
this.counterpartySession = counterpartySession;
}
@Suspendable
@Override
public Void call() throws FlowException {
UntrustworthyData<String> packet = counterpartySession.receive(String.class);
String string = packet.unwrap(data -> data);
if (!string.equals("goodString")) {
throw new FlowException("String did not contain the expected message.");
}
return null;
}
}
@InitiatingFlow
public static final class BadInitiator extends FlowLogic<Void> {
private final Party counterparty;
BadInitiator(Party counterparty) {
this.counterparty = counterparty;
}
@Suspendable
@Override public Void call() {
FlowSession session = initiateFlow(counterparty);
session.send("badString");
return null;
}
}
}

View File

@ -0,0 +1,89 @@
package net.corda.testing.node.internal
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.utilities.unwrap
import net.corda.testing.internal.chooseIdentity
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.registerResponderFlow
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Test
import java.util.concurrent.ExecutionException
import kotlin.test.assertFailsWith
/**
* Test based on the example given as an answer to this SO question:
*
* https://stackoverflow.com/questions/48166626/how-can-an-acceptor-flow-in-corda-be-isolated-for-unit-testing/
*
* but using the `registerFlowFactory` method implemented in response to https://r3-cev.atlassian.net/browse/CORDA-916
*/
class TestResponseFlowInIsolation {
private val network: MockNetwork = MockNetwork(listOf("com.template"))
private val a = network.createNode()
private val b = network.createNode()
@After
fun tearDown() = network.stopNodes()
// This is the real implementation of Initiator.
@InitiatingFlow
open class Initiator(val counterparty: Party) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val session = initiateFlow(counterparty)
session.send("goodString")
}
}
// This is the response flow that we want to isolate for testing.
@InitiatedBy(Initiator::class)
class Responder(val counterpartySession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val string = counterpartySession.receive<String>().unwrap { contents -> contents }
if (string != "goodString") {
throw FlowException("String did not contain the expected message.")
}
}
}
// This is a fake implementation of Initiator to check how Responder responds to non-golden-path scenarios.
@InitiatingFlow
class BadInitiator(val counterparty: Party): FlowLogic<Unit>() {
@Suspendable
override fun call() {
val session = initiateFlow(counterparty)
session.send("badString")
}
}
@Test
fun `test`() {
// This method returns the Responder flow object used by node B.
// We tell node B to respond to BadInitiator with Responder.
val initiatedResponderFlowFuture = b.registerResponderFlow(
initiatingFlowClass = BadInitiator::class.java,
flowFactory = ::Responder)
// We run the BadInitiator flow on node A.
val flow = BadInitiator(b.info.chooseIdentity())
val future = a.startFlow(flow)
network.runNetwork()
future.get()
// We check that the invocation of the Responder flow object has caused an ExecutionException.
val initiatedResponderFlow = initiatedResponderFlowFuture.get()
val initiatedResponderFlowResultFuture = initiatedResponderFlow.stateMachine.resultFuture
val exceptionFromFlow = assertFailsWith<ExecutionException> {
initiatedResponderFlowResultFuture.get()
}.cause
assertThat(exceptionFromFlow)
.isInstanceOf(FlowException::class.java)
.hasMessage("String did not contain the expected message.")
}
}