Skip to content

Commit de13c90

Browse files
anthonykeenanKatelyn Baker
authored andcommitted
CORDA-1292 - Add CordaService testing documentation and improve tests in irs-demo (#2929) (#2938)
* Add CordaService testing documentation and improve tests in irs-demo * Addressed review comments
1 parent ecea714 commit de13c90

File tree

6 files changed

+196
-56
lines changed

6 files changed

+196
-56
lines changed

docs/source/api-persistence.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,5 @@ which is then referenced within a custom flow:
144144
:start-after: DOCSTART TopupIssuer
145145
:end-before: DOCEND TopupIssuer
146146

147+
For examples on testing ``@CordaService`` implementations, see the oracle example :doc:`here <oracles>`
148+

docs/source/api-testing.rst

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ object, or by using named paramters in Kotlin:
5959
.. sourcecode:: kotlin
6060

6161
val network = MockNetwork(
62+
// A list of packages to scan. Any contracts, flows and Corda services within these
63+
// packages will be automatically available to any nodes within the mock network
6264
cordappPackages = listOf("my.cordapp.package", "my.other.cordapp.package"),
6365
// If true then each node will be run in its own thread. This can result in race conditions in your
6466
// code if not carefully written, but is more realistic and may help if you have flows in your app that
@@ -77,7 +79,10 @@ object, or by using named paramters in Kotlin:
7779
// notary implementations.
7880
servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random())
7981

80-
val network2 = MockNetwork(listOf("my.cordapp.package", "my.other.cordapp.package"), MockNetworkParameters(
82+
val network2 = MockNetwork(
83+
// A list of packages to scan. Any contracts, flows and Corda services within these
84+
// packages will be automatically available to any nodes within the mock network
85+
listOf("my.cordapp.package", "my.other.cordapp.package"), MockNetworkParameters(
8186
// If true then each node will be run in its own thread. This can result in race conditions in your
8287
// code if not carefully written, but is more realistic and may help if you have flows in your app that
8388
// do long blocking operations.
@@ -98,7 +103,10 @@ object, or by using named paramters in Kotlin:
98103

99104
.. sourcecode:: java
100105

101-
MockNetwork network = MockNetwork(ImmutableList.of("my.cordapp.package", "my.other.cordapp.package"),
106+
MockNetwork network = MockNetwork(
107+
// A list of packages to scan. Any contracts, flows and Corda services within these
108+
// packages will be automatically available to any nodes within the mock network
109+
ImmutableList.of("my.cordapp.package", "my.other.cordapp.package"),
102110
new MockNetworkParameters()
103111
// If true then each node will be run in its own thread. This can result in race conditions in
104112
// your code if not carefully written, but is more realistic and may help if you have flows in
@@ -294,6 +302,7 @@ Further examples
294302
^^^^^^^^^^^^^^^^
295303

296304
* See the flow testing tutorial :doc:`here <flow-testing>`
305+
* See the oracle tutorial :doc:`here <oracles>` for information on testing ``@CordaService`` classes
297306
* Further examples are available in the Example CorDapp in
298307
`Java <https://github.com/corda/cordapp-example/blob/release-V3/java-source/src/test/java/com/example/flow/IOUFlowTests.java>`_ and
299308
`Kotlin <https://github.com/corda/cordapp-example/blob/release-V3/kotlin-source/src/test/kotlin/com/example/flow/IOUFlowTests.kt>`_

docs/source/oracles.rst

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,26 @@ Here's an example of it in action from ``FixingFlow.Fixer``.
269269
Testing
270270
-------
271271

272-
When unit testing, we make use of the ``MockNetwork`` which allows us to create ``MockNode`` instances. A ``MockNode``
273-
is a simplified node suitable for tests. One feature that isn't available (and which is not suitable for unit testing
274-
anyway) is the node's ability to scan and automatically install oracles it finds in the CorDapp jars. Instead, when
275-
working with ``MockNode``, use the ``installCordaService`` method to manually install the oracle on the relevant node.
272+
The ``MockNetwork`` allows the creation of ``MockNode`` instances, which are simplified nodes which can be used for
273+
testing (see :doc:`api-testing`). When creating the ``MockNetwork`` you supply a list of packages to scan for CorDapps.
274+
Make sure the packages you provide include your oracle service, and it automatically be installed in the test nodes.
275+
Then you can create an oracle node on the ``MockNetwork`` and insert any initialisation logic you want to use. In this
276+
case, our ``Oracle`` service is in the ``net.corda.irs.api`` package, so the following test setup will install
277+
the service in each node. Then an oracle node with an oracle service which is initialised with some data is created on
278+
the mock network:
279+
280+
.. literalinclude:: ../../samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt
281+
:language: kotlin
282+
:start-after: DOCSTART 1
283+
:end-before: DOCEND 1
284+
:dedent: 4
285+
286+
You can then write tests on your mock network to verify the nodes interact with your Oracle correctly.
287+
288+
.. literalinclude:: ../../samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt
289+
:language: kotlin
290+
:start-after: DOCSTART 2
291+
:end-before: DOCEND 2
292+
:dedent: 4
293+
294+
See `here <https://github.com/corda/corda/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt>`_ for more examples.

samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ object FixingFlow {
6969
@Suspendable
7070
override fun filtering(elem: Any): Boolean {
7171
return when (elem) {
72+
// Only expose Fix commands in which the oracle is on the list of requested signers
73+
// to the oracle node, to avoid leaking privacy
7274
is Command<*> -> handshake.payload.oracle.owningKey in elem.signers && elem.value is Fix
7375
else -> false
7476
}
@@ -81,7 +83,7 @@ object FixingFlow {
8183
}
8284

8385
/**
84-
* One side of the fixing flow for an interest rate swap, but could easily be generalised furher.
86+
* One side of the fixing flow for an interest rate swap, but could easily be generalised further.
8587
*
8688
* As per the [Fixer], do not infer too much from this class name in terms of business roles. This
8789
* is just the "side" of the flow run by the party with the floating leg as a way of deciding who

samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt

Lines changed: 2 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,15 @@ import net.corda.core.contracts.ContractState
55
import net.corda.core.contracts.TransactionState
66
import net.corda.core.crypto.generateKeyPair
77
import net.corda.core.identity.CordaX500Name
8-
import net.corda.core.identity.Party
98
import net.corda.core.transactions.TransactionBuilder
10-
import net.corda.core.utilities.ProgressTracker
11-
import net.corda.core.utilities.getOrThrow
129
import net.corda.finance.DOLLARS
1310
import net.corda.finance.contracts.Fix
14-
import net.corda.finance.contracts.FixOf
1511
import net.corda.finance.contracts.asset.CASH
1612
import net.corda.finance.contracts.asset.Cash
17-
import net.corda.irs.flows.RatesFixFlow
1813
import net.corda.node.internal.configureDatabase
1914
import net.corda.nodeapi.internal.persistence.CordaPersistence
2015
import net.corda.nodeapi.internal.persistence.DatabaseConfig
2116
import net.corda.testing.core.*
22-
import net.corda.testing.internal.withoutTestSerialization
23-
import net.corda.testing.internal.LogHelper
2417
import net.corda.testing.internal.rigorousMock
2518
import net.corda.testing.node.*
2619
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
@@ -210,50 +203,10 @@ class NodeInterestRatesTest {
210203
assertFailsWith<IllegalArgumentException> { oracle.sign(ftx) } // It throws failed requirement (as it is empty there is no command to check and sign).
211204
}
212205

213-
@Test
214-
fun `network tearoff`() = withoutTestSerialization {
215-
val mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts", "net.corda.irs"))
216-
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
217-
val oracleNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)).apply {
218-
registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java)
219-
registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java)
220-
database.transaction {
221-
services.cordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA
222-
}
223-
}
224-
val oracle = oracleNode.services.myInfo.singleIdentity()
225-
val tx = makePartialTX()
226-
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
227-
val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1"))
228-
LogHelper.setLevel("rates")
229-
mockNet.runNetwork()
230-
val future = aliceNode.startFlow(flow)
231-
mockNet.runNetwork()
232-
future.getOrThrow()
233-
// We should now have a valid fix of our tx from the oracle.
234-
val fix = tx.toWireTransaction(services).commands.map { it.value as Fix }.first()
235-
assertEquals(fixOf, fix.of)
236-
assertEquals(BigDecimal("0.678"), fix.value)
237-
mockNet.stopNodes()
238-
}
239-
240-
class FilteredRatesFlow(tx: TransactionBuilder,
241-
oracle: Party,
242-
fixOf: FixOf,
243-
expectedRate: BigDecimal,
244-
rateTolerance: BigDecimal,
245-
progressTracker: ProgressTracker = RatesFixFlow.tracker(fixOf.name))
246-
: RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) {
247-
override fun filtering(elem: Any): Boolean {
248-
return when (elem) {
249-
is Command<*> -> oracle.owningKey in elem.signers && elem.value is Fix
250-
else -> false
251-
}
252-
}
253-
}
254-
255206
private fun makePartialTX() = TransactionBuilder(DUMMY_NOTARY).withItems(
256207
TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy ALICE, Cash.PROGRAM_ID, DUMMY_NOTARY))
257208

258209
private fun makeFullTx() = makePartialTX().withItems(dummyCommand())
259210
}
211+
212+
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package net.corda.irs.api
2+
3+
import com.google.common.collect.testing.Helpers
4+
import com.google.common.collect.testing.Helpers.assertContains
5+
import net.corda.core.contracts.Command
6+
import net.corda.core.contracts.TransactionState
7+
import net.corda.core.flows.UnexpectedFlowEndException
8+
import net.corda.core.identity.CordaX500Name
9+
import net.corda.core.identity.Party
10+
import net.corda.core.transactions.TransactionBuilder
11+
import net.corda.core.utilities.ProgressTracker
12+
import net.corda.core.utilities.getOrThrow
13+
import net.corda.finance.DOLLARS
14+
import net.corda.finance.contracts.Fix
15+
import net.corda.finance.contracts.FixOf
16+
import net.corda.finance.contracts.asset.CASH
17+
import net.corda.finance.contracts.asset.Cash
18+
import net.corda.irs.flows.RatesFixFlow
19+
import net.corda.testing.core.*
20+
import net.corda.testing.internal.LogHelper
21+
import net.corda.testing.node.MockNetwork
22+
import net.corda.testing.node.MockNodeParameters
23+
import net.corda.testing.node.StartedMockNode
24+
import org.assertj.core.api.Assertions.assertThatThrownBy
25+
import org.junit.After
26+
import org.junit.Before
27+
import org.junit.Test
28+
import java.math.BigDecimal
29+
import kotlin.test.assertEquals
30+
31+
class OracleNodeTearOffTests {
32+
private val TEST_DATA = NodeInterestRates.parseFile("""
33+
LIBOR 2016-03-16 1M = 0.678
34+
LIBOR 2016-03-16 2M = 0.685
35+
LIBOR 2016-03-16 1Y = 0.890
36+
LIBOR 2016-03-16 2Y = 0.962
37+
EURIBOR 2016-03-15 1M = 0.123
38+
EURIBOR 2016-03-15 2M = 0.111
39+
""".trimIndent())
40+
41+
private val dummyCashIssuer = TestIdentity(CordaX500Name("Cash issuer", "London", "GB"))
42+
43+
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
44+
val alice = TestIdentity(ALICE_NAME, 70)
45+
private lateinit var mockNet: MockNetwork
46+
private lateinit var aliceNode: StartedMockNode
47+
private lateinit var oracleNode: StartedMockNode
48+
private val oracle get() = oracleNode.services.myInfo.singleIdentity()
49+
50+
@Before
51+
// DOCSTART 1
52+
fun setUp() {
53+
mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts", "net.corda.irs"))
54+
aliceNode = mockNet.createPartyNode(ALICE_NAME)
55+
oracleNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)).apply {
56+
transaction {
57+
services.cordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA
58+
}
59+
}
60+
}
61+
// DOCEND 1
62+
63+
@After
64+
fun tearDown() {
65+
mockNet.stopNodes()
66+
}
67+
68+
// DOCSTART 2
69+
@Test
70+
fun `verify that the oracle signs the transaction if the interest rate within allowed limit`() {
71+
// Create a partial transaction
72+
val tx = TransactionBuilder(DUMMY_NOTARY)
73+
.withItems(TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy alice.party, Cash.PROGRAM_ID, DUMMY_NOTARY))
74+
// Specify the rate we wish to get verified by the oracle
75+
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
76+
77+
// Create a new flow for the fix
78+
val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1"))
79+
// Run the mock network and wait for a result
80+
mockNet.runNetwork()
81+
val future = aliceNode.startFlow(flow)
82+
mockNet.runNetwork()
83+
future.getOrThrow()
84+
85+
// We should now have a valid rate on our tx from the oracle.
86+
val fix = tx.toWireTransaction(aliceNode.services).commands.map { it }.first()
87+
assertEquals(fixOf, (fix.value as Fix).of)
88+
// Check that the response contains the valid rate, which is within the supplied tolerance
89+
assertEquals(BigDecimal("0.678"), (fix.value as Fix).value)
90+
// Check that the transaction has been signed by the oracle
91+
assertContains(fix.signers, oracle.owningKey)
92+
}
93+
// DOCEND 2
94+
95+
@Test
96+
fun `verify that the oracle rejects the transaction if the interest rate is outside the allowed limit`() {
97+
val tx = makePartialTX()
98+
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
99+
val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.695"), BigDecimal("0.01"))
100+
LogHelper.setLevel("rates")
101+
102+
mockNet.runNetwork()
103+
val future = aliceNode.startFlow(flow)
104+
mockNet.runNetwork()
105+
assertThatThrownBy{
106+
future.getOrThrow()
107+
}.isInstanceOf(RatesFixFlow.FixOutOfRange::class.java).hasMessage("Fix out of range by 0.017")
108+
}
109+
110+
@Test
111+
fun `verify that the oracle rejects the transaction if there is a privacy leak`() {
112+
val tx = makePartialTX()
113+
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
114+
val flow = OverFilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1"))
115+
LogHelper.setLevel("rates")
116+
117+
mockNet.runNetwork()
118+
val future = aliceNode.startFlow(flow)
119+
mockNet.runNetwork()
120+
//The oracle
121+
assertThatThrownBy{
122+
future.getOrThrow()
123+
}.isInstanceOf(UnexpectedFlowEndException::class.java)
124+
}
125+
126+
// Creates a version of [RatesFixFlow] that makes the command
127+
class FilteredRatesFlow(tx: TransactionBuilder,
128+
oracle: Party,
129+
fixOf: FixOf,
130+
expectedRate: BigDecimal,
131+
rateTolerance: BigDecimal,
132+
progressTracker: ProgressTracker = tracker(fixOf.name))
133+
: RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) {
134+
override fun filtering(elem: Any): Boolean {
135+
return when (elem) {
136+
is Command<*> -> oracle.owningKey in elem.signers && elem.value is Fix
137+
else -> false
138+
}
139+
}
140+
}
141+
142+
// Creates a version of [RatesFixFlow] that makes the command
143+
class OverFilteredRatesFlow(tx: TransactionBuilder,
144+
oracle: Party,
145+
fixOf: FixOf,
146+
expectedRate: BigDecimal,
147+
rateTolerance: BigDecimal,
148+
progressTracker: ProgressTracker = tracker(fixOf.name))
149+
: RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) {
150+
override fun filtering(elem: Any): Boolean = true
151+
}
152+
153+
private fun makePartialTX() = TransactionBuilder(DUMMY_NOTARY).withItems(
154+
TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy alice.party, Cash.PROGRAM_ID, DUMMY_NOTARY))
155+
}

0 commit comments

Comments
 (0)