From 4542e0cd06ad354605eb77e9bc63514130978b9d Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Tue, 31 Jul 2018 13:33:18 +0100 Subject: [PATCH] CORDA-916: Add registerFlowFactory method to public test API (#3720) * 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 --- .ci/api-current.txt | 4 + docs/source/changelog.rst | 2 + .../net/corda/testing/node/MockNetwork.kt | 65 ++++++++++ .../TestResponseFlowInIsolationInJava.java | 115 ++++++++++++++++++ .../internal/TestResponseFlowInIsolation.kt | 89 ++++++++++++++ 5 files changed, 275 insertions(+) create mode 100644 testing/node-driver/src/test/java/net/corda/testing/node/TestResponseFlowInIsolationInJava.java create mode 100644 testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/TestResponseFlowInIsolation.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index cf5f0a1573..976152d456 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -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>> findStateMachines(Class) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 95503f57fc..f9e3ffacd8 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -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. diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt index 21f2f6021e..82730cefc1 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt @@ -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 > registerResponderFlow(initiatingFlowClass: Class>, + flowFactory: ResponderFlowFactory, + responderFlowClass: Class): CordaFuture = + 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> { + /** + * 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 > StartedMockNode.registerResponderFlow( + initiatingFlowClass: Class>, + noinline flowFactory: (FlowSession) -> F): Future = + registerResponderFlow( + initiatingFlowClass, + object : ResponderFlowFactory { + 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 diff --git a/testing/node-driver/src/test/java/net/corda/testing/node/TestResponseFlowInIsolationInJava.java b/testing/node-driver/src/test/java/net/corda/testing/node/TestResponseFlowInIsolationInJava.java new file mode 100644 index 0000000000..af4bb5a14b --- /dev/null +++ b/testing/node-driver/src/test/java/net/corda/testing/node/TestResponseFlowInIsolationInJava.java @@ -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 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 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 { + 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 { + private final FlowSession counterpartySession; + + Responder(FlowSession counterpartySession) { + this.counterpartySession = counterpartySession; + } + + @Suspendable + @Override + public Void call() throws FlowException { + UntrustworthyData 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 { + private final Party counterparty; + + BadInitiator(Party counterparty) { + this.counterparty = counterparty; + } + + @Suspendable + @Override public Void call() { + FlowSession session = initiateFlow(counterparty); + session.send("badString"); + return null; + } + } +} diff --git a/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/TestResponseFlowInIsolation.kt b/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/TestResponseFlowInIsolation.kt new file mode 100644 index 0000000000..07e260019d --- /dev/null +++ b/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/TestResponseFlowInIsolation.kt @@ -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() { + @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() { + @Suspendable + override fun call() { + val string = counterpartySession.receive().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() { + @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 { + initiatedResponderFlowResultFuture.get() + }.cause + assertThat(exceptionFromFlow) + .isInstanceOf(FlowException::class.java) + .hasMessage("String did not contain the expected message.") + } +} \ No newline at end of file