Reputation: 81
I'm trying to unit test an Acceptor flow in Corda, without calling it from the Initiating flow. Specifically I want to send in an invalid SignedTransaction (invalid because it fails the output state contract verification), to ensure that the Acceptor flow handles it as expected and refuses to sign it.
If the Acceptor flow is tested by calling the Initiating Flow, then an invalid txn will get kicked out on the Initiator flow side, before calling the Acceptor flow.
Upvotes: 1
Views: 358
Reputation: 23140
There is currently no public API to achieve this. I have raised a JIRA here to have one added: https://r3-cev.atlassian.net/browse/CORDA-916.
However, as of Corda V2, you can isolate a response flow for testing as follows:
// 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.")
}
}
}
class FlowTests {
private lateinit var network: MockNetwork
private lateinit var a: StartedNode<MockNode>
private lateinit var b: StartedNode<MockNode>
@Before
fun setup() {
setCordappPackages("com.template")
network = MockNetwork()
val nodes = network.createSomeNodes(2)
a = nodes.partyNodes[0]
b = nodes.partyNodes[1]
nodes.partyNodes.forEach {
it.registerInitiatedFlow(Responder::class.java)
}
network.runNetwork()
}
@After
fun tearDown() {
network.stopNodes()
unsetCordappPackages()
}
// 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.
val initiatedResponderFlowObservable = b.internals.internalRegisterFlowFactory(
// We tell node B to respond to BadInitiator with Responder.
initiatingFlowClass = BadInitiator::class.java,
initiatedFlowClass = Responder::class.java,
flowFactory = InitiatedFlowFactory.CorDapp(flowVersion = 0, appName = "", factory = { flowSession -> Responder(flowSession) }),
// We want to observe the Responder flow object to check for errors.
track = true)
val initiatedResponderFlowFuture = initiatedResponderFlowObservable.toFuture()
// We run the BadInitiator flow on node A.
val flow = BadInitiator(b.info.chooseIdentity())
val future = a.services.startFlow(flow)
network.runNetwork()
future.resultFuture.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.")
}
}
Let's step through this code:
BadInitiator
that should cause the response flow to throw an errorBadInitiator
by invoking Responder
, the response flowBadInitiator
flow with test node BResponder
flow to throw an FlowException
with the correct message.Here's the same code in Java:
First file - the flows:
public class TemplateFlow {
// 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 FlowSession counterpartySession;
public 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;
}
}
}
Second file - the fake "bad initiator" flow:
@InitiatingFlow
class BadInitiator extends FlowLogic<Void> {
private Party counterparty;
public BadInitiator(Party counterparty) {
this.counterparty = counterparty;
}
@Suspendable
@Override public Void call() {
FlowSession session = initiateFlow(counterparty);
session.send("badString");
return null;
}
}
Third file - the flow tests:
public class FlowTests {
private MockNetwork network;
private StartedNode<MockNetwork.MockNode> a;
private StartedNode<MockNetwork.MockNode> b;
@Before
public void setup() {
setCordappPackages("com.template");
network = new MockNetwork();
MockNetwork.BasketOfNodes nodes = network.createSomeNodes(2);
a = nodes.getPartyNodes().get(0);
b = nodes.getPartyNodes().get(1);
for (StartedNode<MockNetwork.MockNode> node : nodes.getPartyNodes()) {
node.registerInitiatedFlow(Responder.class);
}
network.runNetwork();
}
@After
public void tearDown() {
network.stopNodes();
unsetCordappPackages();
}
@Rule
public final ExpectedException exception = ExpectedException.none();
// TODO: Move this into being a lambda.
private static Responder factory(Object flowSession) {
return new Responder((FlowSession) flowSession);
}
private InitiatedFlowFactory.CorDapp<Responder> flowFactory = new InitiatedFlowFactory.CorDapp<>(0, "", FlowTests::factory);
@Test
public void test() throws Exception {
// This method returns the Responder flow object used by node B.
Observable initiatedResponderFlowObservable = b.getInternals().internalRegisterFlowFactory(
// We tell node B to respond to BadInitiator with Responder.
// We want to observe the Responder flow object to check for errors.
BadInitiator.class, flowFactory, Responder.class, true);
CordaFuture<Responder> initiatedResponderFlowFuture = Utils.toFuture(initiatedResponderFlowObservable);
// We run the BadInitiator flow on node A.
BadInitiator flow = new BadInitiator(b.getInfo().getLegalIdentities().get(0));
CordaFuture<Void> future = a.getServices().startFlow(flow).getResultFuture();
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();
}
}
Upvotes: 1