BrianRice
BrianRice

Reputation: 81

How can an Acceptor flow in Corda be isolated for unit testing?

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

Answers (1)

Joel
Joel

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:

  • We define a BadInitiator that should cause the response flow to throw an error
  • We register test node B to respond to BadInitiator by invoking Responder, the response flow
  • Test node A runs the BadInitiator flow with test node B
  • We check that this has caused test node B's Responder 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

Related Questions