mirror of
https://github.com/corda/corda.git
synced 2025-01-20 11:39:09 +00:00
[CORDA-1292] Add CordaService testing documentation and improve tests in irs-demo (#2929)
* Add CordaService testing documentation and improve tests in irs-demo * Addressed review comments
This commit is contained in:
parent
d5317e074c
commit
ec09188559
@ -144,3 +144,5 @@ which is then referenced within a custom flow:
|
|||||||
:start-after: DOCSTART TopupIssuer
|
:start-after: DOCSTART TopupIssuer
|
||||||
:end-before: DOCEND TopupIssuer
|
:end-before: DOCEND TopupIssuer
|
||||||
|
|
||||||
|
For examples on testing ``@CordaService`` implementations, see the oracle example :doc:`here <oracles>`
|
||||||
|
|
||||||
|
@ -59,6 +59,8 @@ object, or by using named paramters in Kotlin:
|
|||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
val network = MockNetwork(
|
val network = MockNetwork(
|
||||||
|
// A list of packages to scan. Any contracts, flows and Corda services within these
|
||||||
|
// packages will be automatically available to any nodes within the mock network
|
||||||
cordappPackages = listOf("my.cordapp.package", "my.other.cordapp.package"),
|
cordappPackages = listOf("my.cordapp.package", "my.other.cordapp.package"),
|
||||||
// If true then each node will be run in its own thread. This can result in race conditions in your
|
// If true then each node will be run in its own thread. This can result in race conditions in your
|
||||||
// code if not carefully written, but is more realistic and may help if you have flows in your app that
|
// 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:
|
|||||||
// notary implementations.
|
// notary implementations.
|
||||||
servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random())
|
servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random())
|
||||||
|
|
||||||
val network2 = MockNetwork(listOf("my.cordapp.package", "my.other.cordapp.package"), MockNetworkParameters(
|
val network2 = MockNetwork(
|
||||||
|
// A list of packages to scan. Any contracts, flows and Corda services within these
|
||||||
|
// packages will be automatically available to any nodes within the mock network
|
||||||
|
listOf("my.cordapp.package", "my.other.cordapp.package"), MockNetworkParameters(
|
||||||
// If true then each node will be run in its own thread. This can result in race conditions in your
|
// If true then each node will be run in its own thread. This can result in race conditions in your
|
||||||
// code if not carefully written, but is more realistic and may help if you have flows in your app that
|
// code if not carefully written, but is more realistic and may help if you have flows in your app that
|
||||||
// do long blocking operations.
|
// do long blocking operations.
|
||||||
@ -98,7 +103,10 @@ object, or by using named paramters in Kotlin:
|
|||||||
|
|
||||||
.. sourcecode:: java
|
.. sourcecode:: java
|
||||||
|
|
||||||
MockNetwork network = MockNetwork(ImmutableList.of("my.cordapp.package", "my.other.cordapp.package"),
|
MockNetwork network = MockNetwork(
|
||||||
|
// A list of packages to scan. Any contracts, flows and Corda services within these
|
||||||
|
// packages will be automatically available to any nodes within the mock network
|
||||||
|
ImmutableList.of("my.cordapp.package", "my.other.cordapp.package"),
|
||||||
new MockNetworkParameters()
|
new MockNetworkParameters()
|
||||||
// If true then each node will be run in its own thread. This can result in race conditions in
|
// If true then each node will be run in its own thread. This can result in race conditions in
|
||||||
// your code if not carefully written, but is more realistic and may help if you have flows in
|
// your code if not carefully written, but is more realistic and may help if you have flows in
|
||||||
@ -294,6 +302,7 @@ Further examples
|
|||||||
^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
* See the flow testing tutorial :doc:`here <flow-testing>`
|
* See the flow testing tutorial :doc:`here <flow-testing>`
|
||||||
|
* See the oracle tutorial :doc:`here <oracles>` for information on testing ``@CordaService`` classes
|
||||||
* Further examples are available in the Example CorDapp in
|
* Further examples are available in the Example CorDapp in
|
||||||
`Java <https://github.com/corda/cordapp-example/blob/release-V3/java-source/src/test/java/com/example/flow/IOUFlowTests.java>`_ and
|
`Java <https://github.com/corda/cordapp-example/blob/release-V3/java-source/src/test/java/com/example/flow/IOUFlowTests.java>`_ and
|
||||||
`Kotlin <https://github.com/corda/cordapp-example/blob/release-V3/kotlin-source/src/test/kotlin/com/example/flow/IOUFlowTests.kt>`_
|
`Kotlin <https://github.com/corda/cordapp-example/blob/release-V3/kotlin-source/src/test/kotlin/com/example/flow/IOUFlowTests.kt>`_
|
||||||
|
@ -269,7 +269,26 @@ Here's an example of it in action from ``FixingFlow.Fixer``.
|
|||||||
Testing
|
Testing
|
||||||
-------
|
-------
|
||||||
|
|
||||||
When unit testing, we make use of the ``MockNetwork`` which allows us to create ``MockNode`` instances. A ``MockNode``
|
The ``MockNetwork`` allows the creation of ``MockNode`` instances, which are simplified nodes which can be used for
|
||||||
is a simplified node suitable for tests. One feature that isn't available (and which is not suitable for unit testing
|
testing (see :doc:`api-testing`). When creating the ``MockNetwork`` you supply a list of packages to scan for CorDapps.
|
||||||
anyway) is the node's ability to scan and automatically install oracles it finds in the CorDapp jars. Instead, when
|
Make sure the packages you provide include your oracle service, and it automatically be installed in the test nodes.
|
||||||
working with ``MockNode``, use the ``installCordaService`` method to manually install the oracle on the relevant node.
|
Then you can create an oracle node on the ``MockNetwork`` and insert any initialisation logic you want to use. In this
|
||||||
|
case, our ``Oracle`` service is in the ``net.corda.irs.api`` package, so the following test setup will install
|
||||||
|
the service in each node. Then an oracle node with an oracle service which is initialised with some data is created on
|
||||||
|
the mock network:
|
||||||
|
|
||||||
|
.. literalinclude:: ../../samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 1
|
||||||
|
:end-before: DOCEND 1
|
||||||
|
:dedent: 4
|
||||||
|
|
||||||
|
You can then write tests on your mock network to verify the nodes interact with your Oracle correctly.
|
||||||
|
|
||||||
|
.. literalinclude:: ../../samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 2
|
||||||
|
:end-before: DOCEND 2
|
||||||
|
:dedent: 4
|
||||||
|
|
||||||
|
See `here <https://github.com/corda/corda/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt>`_ for more examples.
|
||||||
|
@ -69,6 +69,8 @@ object FixingFlow {
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
override fun filtering(elem: Any): Boolean {
|
override fun filtering(elem: Any): Boolean {
|
||||||
return when (elem) {
|
return when (elem) {
|
||||||
|
// Only expose Fix commands in which the oracle is on the list of requested signers
|
||||||
|
// to the oracle node, to avoid leaking privacy
|
||||||
is Command<*> -> handshake.payload.oracle.owningKey in elem.signers && elem.value is Fix
|
is Command<*> -> handshake.payload.oracle.owningKey in elem.signers && elem.value is Fix
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
@ -81,7 +83,7 @@ object FixingFlow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One side of the fixing flow for an interest rate swap, but could easily be generalised furher.
|
* One side of the fixing flow for an interest rate swap, but could easily be generalised further.
|
||||||
*
|
*
|
||||||
* As per the [Fixer], do not infer too much from this class name in terms of business roles. This
|
* As per the [Fixer], do not infer too much from this class name in terms of business roles. This
|
||||||
* is just the "side" of the flow run by the party with the floating leg as a way of deciding who
|
* is just the "side" of the flow run by the party with the floating leg as a way of deciding who
|
||||||
|
@ -5,22 +5,15 @@ import net.corda.core.contracts.ContractState
|
|||||||
import net.corda.core.contracts.TransactionState
|
import net.corda.core.contracts.TransactionState
|
||||||
import net.corda.core.crypto.generateKeyPair
|
import net.corda.core.crypto.generateKeyPair
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.ProgressTracker
|
|
||||||
import net.corda.core.utilities.getOrThrow
|
|
||||||
import net.corda.finance.DOLLARS
|
import net.corda.finance.DOLLARS
|
||||||
import net.corda.finance.contracts.Fix
|
import net.corda.finance.contracts.Fix
|
||||||
import net.corda.finance.contracts.FixOf
|
|
||||||
import net.corda.finance.contracts.asset.CASH
|
import net.corda.finance.contracts.asset.CASH
|
||||||
import net.corda.finance.contracts.asset.Cash
|
import net.corda.finance.contracts.asset.Cash
|
||||||
import net.corda.irs.flows.RatesFixFlow
|
|
||||||
import net.corda.node.internal.configureDatabase
|
import net.corda.node.internal.configureDatabase
|
||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||||
import net.corda.testing.core.*
|
import net.corda.testing.core.*
|
||||||
import net.corda.testing.internal.withoutTestSerialization
|
|
||||||
import net.corda.testing.internal.LogHelper
|
|
||||||
import net.corda.testing.internal.rigorousMock
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.*
|
import net.corda.testing.node.*
|
||||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||||
@ -210,50 +203,10 @@ class NodeInterestRatesTest {
|
|||||||
assertFailsWith<IllegalArgumentException> { oracle.sign(ftx) } // It throws failed requirement (as it is empty there is no command to check and sign).
|
assertFailsWith<IllegalArgumentException> { oracle.sign(ftx) } // It throws failed requirement (as it is empty there is no command to check and sign).
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `network tearoff`() = withoutTestSerialization {
|
|
||||||
val mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts", "net.corda.irs"))
|
|
||||||
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
|
||||||
val oracleNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)).apply {
|
|
||||||
registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java)
|
|
||||||
registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java)
|
|
||||||
database.transaction {
|
|
||||||
services.cordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val oracle = oracleNode.services.myInfo.singleIdentity()
|
|
||||||
val tx = makePartialTX()
|
|
||||||
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
|
||||||
val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1"))
|
|
||||||
LogHelper.setLevel("rates")
|
|
||||||
mockNet.runNetwork()
|
|
||||||
val future = aliceNode.startFlow(flow)
|
|
||||||
mockNet.runNetwork()
|
|
||||||
future.getOrThrow()
|
|
||||||
// We should now have a valid fix of our tx from the oracle.
|
|
||||||
val fix = tx.toWireTransaction(services).commands.map { it.value as Fix }.first()
|
|
||||||
assertEquals(fixOf, fix.of)
|
|
||||||
assertEquals(BigDecimal("0.678"), fix.value)
|
|
||||||
mockNet.stopNodes()
|
|
||||||
}
|
|
||||||
|
|
||||||
class FilteredRatesFlow(tx: TransactionBuilder,
|
|
||||||
oracle: Party,
|
|
||||||
fixOf: FixOf,
|
|
||||||
expectedRate: BigDecimal,
|
|
||||||
rateTolerance: BigDecimal,
|
|
||||||
progressTracker: ProgressTracker = RatesFixFlow.tracker(fixOf.name))
|
|
||||||
: RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) {
|
|
||||||
override fun filtering(elem: Any): Boolean {
|
|
||||||
return when (elem) {
|
|
||||||
is Command<*> -> oracle.owningKey in elem.signers && elem.value is Fix
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun makePartialTX() = TransactionBuilder(DUMMY_NOTARY).withItems(
|
private fun makePartialTX() = TransactionBuilder(DUMMY_NOTARY).withItems(
|
||||||
TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy ALICE, Cash.PROGRAM_ID, DUMMY_NOTARY))
|
TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy ALICE, Cash.PROGRAM_ID, DUMMY_NOTARY))
|
||||||
|
|
||||||
private fun makeFullTx() = makePartialTX().withItems(dummyCommand())
|
private fun makeFullTx() = makePartialTX().withItems(dummyCommand())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -0,0 +1,155 @@
|
|||||||
|
package net.corda.irs.api
|
||||||
|
|
||||||
|
import com.google.common.collect.testing.Helpers
|
||||||
|
import com.google.common.collect.testing.Helpers.assertContains
|
||||||
|
import net.corda.core.contracts.Command
|
||||||
|
import net.corda.core.contracts.TransactionState
|
||||||
|
import net.corda.core.flows.UnexpectedFlowEndException
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
import net.corda.core.utilities.ProgressTracker
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.finance.DOLLARS
|
||||||
|
import net.corda.finance.contracts.Fix
|
||||||
|
import net.corda.finance.contracts.FixOf
|
||||||
|
import net.corda.finance.contracts.asset.CASH
|
||||||
|
import net.corda.finance.contracts.asset.Cash
|
||||||
|
import net.corda.irs.flows.RatesFixFlow
|
||||||
|
import net.corda.testing.core.*
|
||||||
|
import net.corda.testing.internal.LogHelper
|
||||||
|
import net.corda.testing.node.MockNetwork
|
||||||
|
import net.corda.testing.node.MockNodeParameters
|
||||||
|
import net.corda.testing.node.StartedMockNode
|
||||||
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class OracleNodeTearOffTests {
|
||||||
|
private val TEST_DATA = NodeInterestRates.parseFile("""
|
||||||
|
LIBOR 2016-03-16 1M = 0.678
|
||||||
|
LIBOR 2016-03-16 2M = 0.685
|
||||||
|
LIBOR 2016-03-16 1Y = 0.890
|
||||||
|
LIBOR 2016-03-16 2Y = 0.962
|
||||||
|
EURIBOR 2016-03-15 1M = 0.123
|
||||||
|
EURIBOR 2016-03-15 2M = 0.111
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
private val dummyCashIssuer = TestIdentity(CordaX500Name("Cash issuer", "London", "GB"))
|
||||||
|
|
||||||
|
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||||
|
val alice = TestIdentity(ALICE_NAME, 70)
|
||||||
|
private lateinit var mockNet: MockNetwork
|
||||||
|
private lateinit var aliceNode: StartedMockNode
|
||||||
|
private lateinit var oracleNode: StartedMockNode
|
||||||
|
private val oracle get() = oracleNode.services.myInfo.singleIdentity()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
// DOCSTART 1
|
||||||
|
fun setUp() {
|
||||||
|
mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts", "net.corda.irs"))
|
||||||
|
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||||
|
oracleNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)).apply {
|
||||||
|
transaction {
|
||||||
|
services.cordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// DOCEND 1
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
mockNet.stopNodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOCSTART 2
|
||||||
|
@Test
|
||||||
|
fun `verify that the oracle signs the transaction if the interest rate within allowed limit`() {
|
||||||
|
// Create a partial transaction
|
||||||
|
val tx = TransactionBuilder(DUMMY_NOTARY)
|
||||||
|
.withItems(TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy alice.party, Cash.PROGRAM_ID, DUMMY_NOTARY))
|
||||||
|
// Specify the rate we wish to get verified by the oracle
|
||||||
|
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||||
|
|
||||||
|
// Create a new flow for the fix
|
||||||
|
val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1"))
|
||||||
|
// Run the mock network and wait for a result
|
||||||
|
mockNet.runNetwork()
|
||||||
|
val future = aliceNode.startFlow(flow)
|
||||||
|
mockNet.runNetwork()
|
||||||
|
future.getOrThrow()
|
||||||
|
|
||||||
|
// We should now have a valid rate on our tx from the oracle.
|
||||||
|
val fix = tx.toWireTransaction(aliceNode.services).commands.map { it }.first()
|
||||||
|
assertEquals(fixOf, (fix.value as Fix).of)
|
||||||
|
// Check that the response contains the valid rate, which is within the supplied tolerance
|
||||||
|
assertEquals(BigDecimal("0.678"), (fix.value as Fix).value)
|
||||||
|
// Check that the transaction has been signed by the oracle
|
||||||
|
assertContains(fix.signers, oracle.owningKey)
|
||||||
|
}
|
||||||
|
// DOCEND 2
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `verify that the oracle rejects the transaction if the interest rate is outside the allowed limit`() {
|
||||||
|
val tx = makePartialTX()
|
||||||
|
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||||
|
val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.695"), BigDecimal("0.01"))
|
||||||
|
LogHelper.setLevel("rates")
|
||||||
|
|
||||||
|
mockNet.runNetwork()
|
||||||
|
val future = aliceNode.startFlow(flow)
|
||||||
|
mockNet.runNetwork()
|
||||||
|
assertThatThrownBy{
|
||||||
|
future.getOrThrow()
|
||||||
|
}.isInstanceOf(RatesFixFlow.FixOutOfRange::class.java).hasMessage("Fix out of range by 0.017")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `verify that the oracle rejects the transaction if there is a privacy leak`() {
|
||||||
|
val tx = makePartialTX()
|
||||||
|
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||||
|
val flow = OverFilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1"))
|
||||||
|
LogHelper.setLevel("rates")
|
||||||
|
|
||||||
|
mockNet.runNetwork()
|
||||||
|
val future = aliceNode.startFlow(flow)
|
||||||
|
mockNet.runNetwork()
|
||||||
|
//The oracle
|
||||||
|
assertThatThrownBy{
|
||||||
|
future.getOrThrow()
|
||||||
|
}.isInstanceOf(UnexpectedFlowEndException::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a version of [RatesFixFlow] that makes the command
|
||||||
|
class FilteredRatesFlow(tx: TransactionBuilder,
|
||||||
|
oracle: Party,
|
||||||
|
fixOf: FixOf,
|
||||||
|
expectedRate: BigDecimal,
|
||||||
|
rateTolerance: BigDecimal,
|
||||||
|
progressTracker: ProgressTracker = tracker(fixOf.name))
|
||||||
|
: RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) {
|
||||||
|
override fun filtering(elem: Any): Boolean {
|
||||||
|
return when (elem) {
|
||||||
|
is Command<*> -> oracle.owningKey in elem.signers && elem.value is Fix
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a version of [RatesFixFlow] that makes the command
|
||||||
|
class OverFilteredRatesFlow(tx: TransactionBuilder,
|
||||||
|
oracle: Party,
|
||||||
|
fixOf: FixOf,
|
||||||
|
expectedRate: BigDecimal,
|
||||||
|
rateTolerance: BigDecimal,
|
||||||
|
progressTracker: ProgressTracker = tracker(fixOf.name))
|
||||||
|
: RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) {
|
||||||
|
override fun filtering(elem: Any): Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makePartialTX() = TransactionBuilder(DUMMY_NOTARY).withItems(
|
||||||
|
TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy alice.party, Cash.PROGRAM_ID, DUMMY_NOTARY))
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user