Merge branch 'ak' into anthony-os-merge-20180406

# Conflicts:
#	docs/source/api-persistence.rst
This commit is contained in:
Anthony Keenan 2018-04-06 11:01:34 +01:00
commit 2999f0eba2
6 changed files with 196 additions and 56 deletions

View File

@ -145,6 +145,8 @@ which is then referenced within a custom flow:
:start-after: DOCSTART TopupIssuer
:end-before: DOCEND TopupIssuer
For examples on testing ``@CordaService`` implementations, see the oracle example :doc:`here <oracles>`
.. _database_migration_ref:
Database Migration

View File

@ -59,6 +59,8 @@ object, or by using named paramters in Kotlin:
.. sourcecode:: kotlin
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"),
// 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
@ -77,7 +79,10 @@ object, or by using named paramters in Kotlin:
// notary implementations.
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
// code if not carefully written, but is more realistic and may help if you have flows in your app that
// do long blocking operations.
@ -98,7 +103,10 @@ object, or by using named paramters in Kotlin:
.. 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()
// 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
@ -294,6 +302,7 @@ Further examples
^^^^^^^^^^^^^^^^
* 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
`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>`_

View File

@ -269,7 +269,26 @@ Here's an example of it in action from ``FixingFlow.Fixer``.
Testing
-------
When unit testing, we make use of the ``MockNetwork`` which allows us to create ``MockNode`` instances. A ``MockNode``
is a simplified node suitable for tests. One feature that isn't available (and which is not suitable for unit testing
anyway) is the node's ability to scan and automatically install oracles it finds in the CorDapp jars. Instead, when
working with ``MockNode``, use the ``installCordaService`` method to manually install the oracle on the relevant node.
The ``MockNetwork`` allows the creation of ``MockNode`` instances, which are simplified nodes which can be used for
testing (see :doc:`api-testing`). When creating the ``MockNetwork`` you supply a list of packages to scan for CorDapps.
Make sure the packages you provide include your oracle service, and it automatically be installed in the test nodes.
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.

View File

@ -79,6 +79,8 @@ object FixingFlow {
@Suspendable
override fun filtering(elem: Any): Boolean {
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
else -> false
}
@ -91,7 +93,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
* is just the "side" of the flow run by the party with the floating leg as a way of deciding who

View File

@ -15,22 +15,15 @@ import net.corda.core.contracts.ContractState
import net.corda.core.contracts.TransactionState
import net.corda.core.crypto.generateKeyPair
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.node.internal.configureDatabase
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
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.node.*
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
@ -220,50 +213,10 @@ class NodeInterestRatesTest {
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(
TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy ALICE, Cash.PROGRAM_ID, DUMMY_NOTARY))
private fun makeFullTx() = makePartialTX().withItems(dummyCommand())
}

View File

@ -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))
}