mirror of
https://github.com/corda/corda.git
synced 2024-12-23 23:02:29 +00:00
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
This commit is contained in:
parent
93bb24ed17
commit
4542e0cd06
.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
@ -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>)
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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.")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user