mirror of
https://github.com/corda/corda.git
synced 2025-06-05 17:01:45 +00:00
Merge pull request #3644 from corda/flow-test-rationalisation
Flow test rationalisation
This commit is contained in:
commit
7a18dbb8ed
@ -39,6 +39,7 @@ buildscript {
|
|||||||
ext.fileupload_version = '1.3.3'
|
ext.fileupload_version = '1.3.3'
|
||||||
ext.junit_version = '4.12'
|
ext.junit_version = '4.12'
|
||||||
ext.mockito_version = '2.18.3'
|
ext.mockito_version = '2.18.3'
|
||||||
|
ext.hamkrest_version = '1.4.2.2'
|
||||||
ext.jopt_simple_version = '5.0.2'
|
ext.jopt_simple_version = '5.0.2'
|
||||||
ext.jansi_version = '1.14'
|
ext.jansi_version = '1.14'
|
||||||
ext.hibernate_version = '5.2.6.Final'
|
ext.hibernate_version = '5.2.6.Final'
|
||||||
|
@ -69,7 +69,7 @@ dependencies {
|
|||||||
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||||
|
|
||||||
// Hamkrest, for fluent, composable matchers
|
// Hamkrest, for fluent, composable matchers
|
||||||
testCompile 'com.natpryce:hamkrest:1.4.2.2'
|
testCompile "com.natpryce:hamkrest:$hamkrest_version"
|
||||||
|
|
||||||
// Quasar, for suspendable fibres.
|
// Quasar, for suspendable fibres.
|
||||||
compileOnly("$quasar_group:quasar-core:$quasar_version:jdk8") {
|
compileOnly("$quasar_group:quasar-core:$quasar_version:jdk8") {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package net.corda.core.contracts
|
package net.corda.core.contracts
|
||||||
|
|
||||||
import net.corda.finance.*
|
|
||||||
import net.corda.core.contracts.Amount.Companion.sumOrZero
|
import net.corda.core.contracts.Amount.Companion.sumOrZero
|
||||||
|
import net.corda.finance.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -2,8 +2,12 @@ package net.corda.core.flows
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import com.natpryce.hamkrest.*
|
import com.natpryce.hamkrest.*
|
||||||
|
import com.natpryce.hamkrest.assertion.assert
|
||||||
import net.corda.core.contracts.Attachment
|
import net.corda.core.contracts.Attachment
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.flows.matchers.flow.willReturn
|
||||||
|
import net.corda.core.flows.matchers.flow.willThrow
|
||||||
|
import net.corda.core.flows.mixins.WithMockNet
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.FetchAttachmentsFlow
|
import net.corda.core.internal.FetchAttachmentsFlow
|
||||||
@ -16,25 +20,23 @@ import net.corda.testing.core.BOB_NAME
|
|||||||
import net.corda.testing.core.singleIdentity
|
import net.corda.testing.core.singleIdentity
|
||||||
import net.corda.testing.node.internal.InternalMockNetwork
|
import net.corda.testing.node.internal.InternalMockNetwork
|
||||||
import net.corda.testing.node.internal.InternalMockNodeParameters
|
import net.corda.testing.node.internal.InternalMockNodeParameters
|
||||||
import net.corda.testing.node.internal.startFlow
|
|
||||||
import org.junit.AfterClass
|
import org.junit.AfterClass
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.util.*
|
|
||||||
import java.util.jar.JarOutputStream
|
import java.util.jar.JarOutputStream
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import com.natpryce.hamkrest.assertion.assert
|
|
||||||
import net.corda.core.matchers.*
|
|
||||||
|
|
||||||
class AttachmentTests {
|
class AttachmentTests : WithMockNet {
|
||||||
companion object {
|
companion object {
|
||||||
val mockNet = InternalMockNetwork()
|
val classMockNet = InternalMockNetwork()
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@AfterClass
|
@AfterClass
|
||||||
fun cleanUp() = mockNet.stopNodes()
|
fun cleanUp() = classMockNet.stopNodes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val mockNet = classMockNet
|
||||||
|
|
||||||
// Test nodes
|
// Test nodes
|
||||||
private val aliceNode = makeNode(ALICE_NAME)
|
private val aliceNode = makeNode(ALICE_NAME)
|
||||||
private val bobNode = makeNode(BOB_NAME)
|
private val bobNode = makeNode(BOB_NAME)
|
||||||
@ -48,7 +50,7 @@ class AttachmentTests {
|
|||||||
// Get node one to run a flow to fetch it and insert it.
|
// Get node one to run a flow to fetch it and insert it.
|
||||||
assert.that(
|
assert.that(
|
||||||
bobNode.startAttachmentFlow(id, alice),
|
bobNode.startAttachmentFlow(id, alice),
|
||||||
succeedsWith(noAttachments()))
|
willReturn(noAttachments()))
|
||||||
|
|
||||||
// Verify it was inserted into node one's store.
|
// Verify it was inserted into node one's store.
|
||||||
val attachment = bobNode.getAttachmentWithId(id)
|
val attachment = bobNode.getAttachmentWithId(id)
|
||||||
@ -59,7 +61,7 @@ class AttachmentTests {
|
|||||||
|
|
||||||
assert.that(
|
assert.that(
|
||||||
bobNode.startAttachmentFlow(id, alice),
|
bobNode.startAttachmentFlow(id, alice),
|
||||||
succeedsWith(soleAttachment(attachment)))
|
willReturn(soleAttachment(attachment)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -69,10 +71,14 @@ class AttachmentTests {
|
|||||||
// Get node one to fetch a non-existent attachment.
|
// Get node one to fetch a non-existent attachment.
|
||||||
assert.that(
|
assert.that(
|
||||||
bobNode.startAttachmentFlow(hash, alice),
|
bobNode.startAttachmentFlow(hash, alice),
|
||||||
failsWith<FetchDataFlow.HashNotFound>(
|
willThrow(withRequestedHash(hash)))
|
||||||
has("requested hash", { it.requested }, equalTo(hash))))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun withRequestedHash(expected: SecureHash) = has(
|
||||||
|
"requested hash",
|
||||||
|
FetchDataFlow.HashNotFound::requested,
|
||||||
|
equalTo(expected))
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun maliciousResponse() {
|
fun maliciousResponse() {
|
||||||
// Make a node that doesn't do sanity checking at load time.
|
// Make a node that doesn't do sanity checking at load time.
|
||||||
@ -93,7 +99,7 @@ class AttachmentTests {
|
|||||||
// Get n1 to fetch the attachment. Should receive corrupted bytes.
|
// Get n1 to fetch the attachment. Should receive corrupted bytes.
|
||||||
assert.that(
|
assert.that(
|
||||||
bobNode.startAttachmentFlow(id, badAlice),
|
bobNode.startAttachmentFlow(id, badAlice),
|
||||||
failsWith<FetchDataFlow.DownloadedVsRequestedDataMismatch>()
|
willThrow<FetchDataFlow.DownloadedVsRequestedDataMismatch>()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,22 +119,20 @@ class AttachmentTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//region Generators
|
//region Generators
|
||||||
private fun makeNode(name: CordaX500Name) =
|
override fun makeNode(name: CordaX500Name) =
|
||||||
mockNet.createPartyNode(randomiseName(name)).apply {
|
mockNet.createPartyNode(randomise(name)).apply {
|
||||||
registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Makes a node that doesn't do sanity checking at load time.
|
// Makes a node that doesn't do sanity checking at load time.
|
||||||
private fun makeBadNode(name: CordaX500Name) = mockNet.createNode(
|
private fun makeBadNode(name: CordaX500Name) = mockNet.createNode(
|
||||||
InternalMockNodeParameters(legalName = randomiseName(name)),
|
InternalMockNodeParameters(legalName = randomise(name)),
|
||||||
nodeFactory = { args ->
|
nodeFactory = { args ->
|
||||||
object : InternalMockNetwork.MockNode(args) {
|
object : InternalMockNetwork.MockNode(args) {
|
||||||
override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false }
|
override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false }
|
||||||
}
|
}
|
||||||
}).apply { registerInitiatedFlow(FetchAttachmentsResponse::class.java) }
|
}).apply { registerInitiatedFlow(FetchAttachmentsResponse::class.java) }
|
||||||
|
|
||||||
private fun randomiseName(name: CordaX500Name) = name.copy(commonName = "${name.commonName}_${UUID.randomUUID()}")
|
|
||||||
|
|
||||||
private fun fakeAttachment(): ByteArray =
|
private fun fakeAttachment(): ByteArray =
|
||||||
ByteArrayOutputStream().use { baos ->
|
ByteArrayOutputStream().use { baos ->
|
||||||
JarOutputStream(baos).use { jos ->
|
JarOutputStream(baos).use { jos ->
|
||||||
@ -144,24 +148,19 @@ class AttachmentTests {
|
|||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
//region Operations
|
//region Operations
|
||||||
private fun StartedNode<*>.importAttachment(attachment: ByteArray) = database.transaction {
|
private fun StartedNode<*>.importAttachment(attachment: ByteArray) =
|
||||||
attachments.importAttachment(attachment.inputStream(), "test", null)
|
attachments.importAttachment(attachment.inputStream(), "test", null)
|
||||||
|
.andRunNetwork()
|
||||||
|
|
||||||
|
private fun StartedNode<*>.updateAttachment(attachment: NodeAttachmentService.DBAttachment) = database.transaction {
|
||||||
|
session.update(attachment)
|
||||||
}.andRunNetwork()
|
}.andRunNetwork()
|
||||||
|
|
||||||
private fun StartedNode<*>.updateAttachment(attachment: NodeAttachmentService.DBAttachment) =
|
private fun StartedNode<*>.startAttachmentFlow(hash: SecureHash, otherSide: Party) = startFlowAndRunNetwork(
|
||||||
database.transaction { session.update(attachment) }.andRunNetwork()
|
InitiatingFetchAttachmentsFlow(otherSide, setOf(hash)))
|
||||||
|
|
||||||
private fun StartedNode<*>.startAttachmentFlow(hash: SecureHash, otherSide: Party) = services.startFlow(
|
private fun StartedNode<*>.getAttachmentWithId(id: SecureHash) =
|
||||||
InitiatingFetchAttachmentsFlow(otherSide, setOf(hash))).andRunNetwork()
|
|
||||||
|
|
||||||
private fun StartedNode<*>.getAttachmentWithId(id: SecureHash) = database.transaction {
|
|
||||||
attachments.openAttachment(id)!!
|
attachments.openAttachment(id)!!
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T : Any> T.andRunNetwork(): T {
|
|
||||||
mockNet.runNetwork()
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
//region Matchers
|
//region Matchers
|
||||||
|
@ -1,64 +1,105 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.core.flows
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import com.natpryce.hamkrest.assertion.assert
|
||||||
import net.corda.core.contracts.Command
|
import net.corda.core.contracts.Command
|
||||||
import net.corda.core.contracts.StateAndContract
|
import net.corda.core.contracts.StateAndContract
|
||||||
import net.corda.core.contracts.requireThat
|
import net.corda.core.contracts.requireThat
|
||||||
|
import net.corda.core.flows.matchers.flow.willReturn
|
||||||
|
import net.corda.core.flows.matchers.flow.willThrow
|
||||||
|
import net.corda.core.flows.mixins.WithContracts
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.identity.excludeHostNode
|
import net.corda.core.identity.excludeHostNode
|
||||||
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.getOrThrow
|
|
||||||
import net.corda.node.internal.StartedNode
|
import net.corda.node.internal.StartedNode
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
import net.corda.testing.core.ALICE_NAME
|
import net.corda.testing.core.*
|
||||||
import net.corda.testing.core.BOB_NAME
|
|
||||||
import net.corda.testing.core.CHARLIE_NAME
|
|
||||||
import net.corda.testing.core.TestIdentity
|
|
||||||
import net.corda.testing.core.singleIdentity
|
|
||||||
import net.corda.testing.internal.rigorousMock
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.internal.InternalMockNetwork
|
import net.corda.testing.node.internal.InternalMockNetwork
|
||||||
import net.corda.testing.node.internal.InternalMockNetwork.MockNode
|
import org.junit.AfterClass
|
||||||
import net.corda.testing.node.internal.startFlow
|
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertFailsWith
|
|
||||||
|
|
||||||
class CollectSignaturesFlowTests {
|
class CollectSignaturesFlowTests : WithContracts {
|
||||||
companion object {
|
companion object {
|
||||||
private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
||||||
|
private val miniCorpServices = MockServices(listOf("net.corda.testing.contracts"), miniCorp, rigorousMock())
|
||||||
|
private val classMockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.core.flows"))
|
||||||
|
|
||||||
|
private const val MAGIC_NUMBER = 1337
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@AfterClass
|
||||||
|
fun tearDown() = classMockNet.stopNodes()
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var mockNet: InternalMockNetwork
|
override val mockNet = classMockNet
|
||||||
private lateinit var aliceNode: StartedNode<MockNode>
|
|
||||||
private lateinit var bobNode: StartedNode<MockNode>
|
|
||||||
private lateinit var charlieNode: StartedNode<MockNode>
|
|
||||||
private lateinit var alice: Party
|
|
||||||
private lateinit var bob: Party
|
|
||||||
private lateinit var charlie: Party
|
|
||||||
private lateinit var notary: Party
|
|
||||||
|
|
||||||
@Before
|
private val aliceNode = makeNode(ALICE_NAME)
|
||||||
fun setup() {
|
private val bobNode = makeNode(BOB_NAME)
|
||||||
mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.core.flows"))
|
private val charlieNode = makeNode(CHARLIE_NAME)
|
||||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
|
||||||
bobNode = mockNet.createPartyNode(BOB_NAME)
|
private val alice = aliceNode.info.singleIdentity()
|
||||||
charlieNode = mockNet.createPartyNode(CHARLIE_NAME)
|
private val bob = bobNode.info.singleIdentity()
|
||||||
alice = aliceNode.info.singleIdentity()
|
private val charlie = charlieNode.info.singleIdentity()
|
||||||
bob = bobNode.info.singleIdentity()
|
|
||||||
charlie = charlieNode.info.singleIdentity()
|
@Test
|
||||||
notary = mockNet.defaultNotaryIdentity
|
fun `successfully collects three signatures`() {
|
||||||
|
val bConfidentialIdentity = bobNode.createConfidentialIdentity(bob)
|
||||||
|
aliceNode.verifyAndRegister(bConfidentialIdentity)
|
||||||
|
|
||||||
|
assert.that(
|
||||||
|
aliceNode.startTestFlow(alice, bConfidentialIdentity.party, charlie),
|
||||||
|
willReturn(requiredSignatures(3))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@Test
|
||||||
fun tearDown() {
|
fun `no need to collect any signatures`() {
|
||||||
mockNet.stopNodes()
|
val ptx = aliceNode.signDummyContract(alice.ref(1))
|
||||||
|
|
||||||
|
assert.that(
|
||||||
|
aliceNode.collectSignatures(ptx),
|
||||||
|
willReturn(requiredSignatures(1))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fails when not signed by initiator`() {
|
||||||
|
val ptx = miniCorpServices.signDummyContract(alice.ref(1))
|
||||||
|
|
||||||
|
assert.that(
|
||||||
|
aliceNode.collectSignatures(ptx),
|
||||||
|
willThrow(errorMessage("The Initiator of CollectSignaturesFlow must have signed the transaction.")))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `passes with multiple initial signatures`() {
|
||||||
|
val signedByA = aliceNode.signDummyContract(
|
||||||
|
alice.ref(1),
|
||||||
|
MAGIC_NUMBER,
|
||||||
|
bob.ref(2),
|
||||||
|
bob.ref(3))
|
||||||
|
val signedByBoth = bobNode.addSignatureTo(signedByA)
|
||||||
|
|
||||||
|
assert.that(
|
||||||
|
aliceNode.collectSignatures(signedByBoth),
|
||||||
|
willReturn(requiredSignatures(2))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//region Operators
|
||||||
|
private fun StartedNode<*>.startTestFlow(vararg party: Party) =
|
||||||
|
startFlowAndRunNetwork(
|
||||||
|
TestFlow.Initiator(DummyContract.MultiOwnerState(
|
||||||
|
MAGIC_NUMBER,
|
||||||
|
listOf(*party)),
|
||||||
|
mockNet.defaultNotaryIdentity))
|
||||||
|
|
||||||
|
//region Test Flow
|
||||||
// With this flow, the initiator starts the "CollectTransactionFlow". It is then the responders responsibility to
|
// With this flow, the initiator starts the "CollectTransactionFlow". It is then the responders responsibility to
|
||||||
// override "checkTransaction" and add whatever logic their require to verify the SignedTransaction they are
|
// override "checkTransaction" and add whatever logic their require to verify the SignedTransaction they are
|
||||||
// receiving off the wire.
|
// receiving off the wire.
|
||||||
@ -89,7 +130,7 @@ class CollectSignaturesFlowTests {
|
|||||||
"There should only be one output state" using (tx.outputs.size == 1)
|
"There should only be one output state" using (tx.outputs.size == 1)
|
||||||
"There should only be one output state" using (tx.inputs.isEmpty())
|
"There should only be one output state" using (tx.inputs.isEmpty())
|
||||||
val magicNumberState = ltx.outputsOfType<DummyContract.MultiOwnerState>().single()
|
val magicNumberState = ltx.outputsOfType<DummyContract.MultiOwnerState>().single()
|
||||||
"Must be 1337 or greater" using (magicNumberState.magicNumber >= 1337)
|
"Must be $MAGIC_NUMBER or greater" using (magicNumberState.magicNumber >= MAGIC_NUMBER)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,64 +139,5 @@ class CollectSignaturesFlowTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//region
|
||||||
@Test
|
|
||||||
fun `successfully collects two signatures`() {
|
|
||||||
val bConfidentialIdentity = bobNode.database.transaction {
|
|
||||||
val bobCert = bobNode.services.myInfo.legalIdentitiesAndCerts.single { it.name == bob.name }
|
|
||||||
bobNode.services.keyManagementService.freshKeyAndCert(bobCert, false)
|
|
||||||
}
|
|
||||||
aliceNode.database.transaction {
|
|
||||||
// Normally this is handled by TransactionKeyFlow, but here we have to manually let A know about the identity
|
|
||||||
aliceNode.services.identityService.verifyAndRegisterIdentity(bConfidentialIdentity)
|
|
||||||
}
|
|
||||||
val magicNumber = 1337
|
|
||||||
val parties = listOf(alice, bConfidentialIdentity.party, charlie)
|
|
||||||
val state = DummyContract.MultiOwnerState(magicNumber, parties)
|
|
||||||
val flow = aliceNode.services.startFlow(TestFlow.Initiator(state, notary))
|
|
||||||
mockNet.runNetwork()
|
|
||||||
val result = flow.resultFuture.getOrThrow()
|
|
||||||
result.verifyRequiredSignatures()
|
|
||||||
println(result.tx)
|
|
||||||
println(result.sigs)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `no need to collect any signatures`() {
|
|
||||||
val onePartyDummyContract = DummyContract.generateInitial(1337, notary, alice.ref(1))
|
|
||||||
val ptx = aliceNode.services.signInitialTransaction(onePartyDummyContract)
|
|
||||||
val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet()))
|
|
||||||
mockNet.runNetwork()
|
|
||||||
val result = flow.resultFuture.getOrThrow()
|
|
||||||
result.verifyRequiredSignatures()
|
|
||||||
println(result.tx)
|
|
||||||
println(result.sigs)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `fails when not signed by initiator`() {
|
|
||||||
val onePartyDummyContract = DummyContract.generateInitial(1337, notary, alice.ref(1))
|
|
||||||
val miniCorpServices = MockServices(listOf("net.corda.testing.contracts"), miniCorp, rigorousMock())
|
|
||||||
val ptx = miniCorpServices.signInitialTransaction(onePartyDummyContract)
|
|
||||||
val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet()))
|
|
||||||
mockNet.runNetwork()
|
|
||||||
assertFailsWith<IllegalArgumentException>("The Initiator of CollectSignaturesFlow must have signed the transaction.") {
|
|
||||||
flow.resultFuture.getOrThrow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `passes with multiple initial signatures`() {
|
|
||||||
val twoPartyDummyContract = DummyContract.generateInitial(1337, notary,
|
|
||||||
alice.ref(1),
|
|
||||||
bob.ref(2),
|
|
||||||
bob.ref(3))
|
|
||||||
val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract)
|
|
||||||
val signedByBoth = bobNode.services.addSignature(signedByA)
|
|
||||||
val flow = aliceNode.services.startFlow(CollectSignaturesFlow(signedByBoth, emptySet()))
|
|
||||||
mockNet.runNetwork()
|
|
||||||
val result = flow.resultFuture.getOrThrow()
|
|
||||||
println(result.tx)
|
|
||||||
println(result.sigs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,141 @@
|
|||||||
|
package net.corda.core.flows
|
||||||
|
|
||||||
|
import com.natpryce.hamkrest.and
|
||||||
|
import com.natpryce.hamkrest.anything
|
||||||
|
import com.natpryce.hamkrest.assertion.assert
|
||||||
|
import com.natpryce.hamkrest.has
|
||||||
|
import com.natpryce.hamkrest.isA
|
||||||
|
import net.corda.core.CordaRuntimeException
|
||||||
|
import net.corda.core.contracts.ContractState
|
||||||
|
import net.corda.core.contracts.StateAndRef
|
||||||
|
import net.corda.core.flows.matchers.rpc.willReturn
|
||||||
|
import net.corda.core.flows.matchers.rpc.willThrow
|
||||||
|
import net.corda.core.flows.mixins.WithContracts
|
||||||
|
import net.corda.core.flows.mixins.WithFinality
|
||||||
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
|
import net.corda.core.transactions.ContractUpgradeLedgerTransaction
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
import net.corda.node.internal.StartedNode
|
||||||
|
import net.corda.node.services.Permissions.Companion.startFlow
|
||||||
|
import net.corda.testing.contracts.DummyContract
|
||||||
|
import net.corda.testing.contracts.DummyContractV2
|
||||||
|
import net.corda.testing.core.ALICE_NAME
|
||||||
|
import net.corda.testing.core.BOB_NAME
|
||||||
|
import net.corda.testing.core.singleIdentity
|
||||||
|
import net.corda.testing.node.User
|
||||||
|
import net.corda.testing.node.internal.*
|
||||||
|
import net.corda.testing.node.internal.InternalMockNetwork.MockNode
|
||||||
|
import org.junit.AfterClass
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ContractUpgradeFlowRPCTest : WithContracts, WithFinality {
|
||||||
|
companion object {
|
||||||
|
private val classMockNet = InternalMockNetwork(cordappPackages = listOf(
|
||||||
|
"net.corda.testing.contracts",
|
||||||
|
"net.corda.finance.contracts.asset",
|
||||||
|
"net.corda.core.flows"))
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@AfterClass
|
||||||
|
fun tearDown() = classMockNet.stopNodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val mockNet = classMockNet
|
||||||
|
|
||||||
|
private val aliceNode = makeNode(ALICE_NAME)
|
||||||
|
private val bobNode = makeNode(BOB_NAME)
|
||||||
|
|
||||||
|
private val alice = aliceNode.info.singleIdentity()
|
||||||
|
private val bob = bobNode.info.singleIdentity()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `2 parties contract upgrade using RPC`() = rpcDriver {
|
||||||
|
val testUser = createTestUser()
|
||||||
|
val rpcA = startProxy(aliceNode, testUser)
|
||||||
|
val rpcB = startProxy(bobNode, testUser)
|
||||||
|
|
||||||
|
// Create, sign and finalise dummy contract.
|
||||||
|
val signedByA = aliceNode.signDummyContract(alice.ref(1), 0, bob.ref(1))
|
||||||
|
val stx = bobNode.addSignatureTo(signedByA)
|
||||||
|
assert.that(rpcA.finalise(stx, bob), willReturn())
|
||||||
|
|
||||||
|
val atx = aliceNode.getValidatedTransaction(stx)
|
||||||
|
val btx = bobNode.getValidatedTransaction(stx)
|
||||||
|
|
||||||
|
// Cannot upgrade contract without prior authorisation from counterparty
|
||||||
|
assert.that(
|
||||||
|
rpcA.initiateDummyContractUpgrade(atx),
|
||||||
|
willThrow<CordaRuntimeException>())
|
||||||
|
|
||||||
|
// Party B authorises the contract state upgrade, and immediately deauthorises the same.
|
||||||
|
assert.that(rpcB.authoriseDummyContractUpgrade(btx), willReturn())
|
||||||
|
assert.that(rpcB.deauthoriseContractUpgrade(btx), willReturn())
|
||||||
|
|
||||||
|
// Cannot upgrade contract if counterparty has deauthorised a previously-given authority
|
||||||
|
assert.that(
|
||||||
|
rpcA.initiateDummyContractUpgrade(atx),
|
||||||
|
willThrow<CordaRuntimeException>())
|
||||||
|
|
||||||
|
// Party B authorise the contract state upgrade.
|
||||||
|
assert.that(rpcB.authoriseDummyContractUpgrade(btx), willReturn())
|
||||||
|
|
||||||
|
// Party A initiates contract upgrade flow, expected to succeed this time.
|
||||||
|
assert.that(
|
||||||
|
rpcA.initiateDummyContractUpgrade(atx),
|
||||||
|
willReturn(
|
||||||
|
aliceNode.hasDummyContractUpgradeTransaction()
|
||||||
|
and bobNode.hasDummyContractUpgradeTransaction()))
|
||||||
|
}
|
||||||
|
|
||||||
|
//region RPC DSL
|
||||||
|
private fun RPCDriverDSL.startProxy(node: StartedNode<MockNode>, user: User): CordaRPCOps {
|
||||||
|
return startRpcClient<CordaRPCOps>(
|
||||||
|
rpcAddress = startRpcServer(
|
||||||
|
rpcUser = user,
|
||||||
|
ops = node.rpcOps
|
||||||
|
).get().broker.hostAndPort!!,
|
||||||
|
username = user.username,
|
||||||
|
password = user.password
|
||||||
|
).get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RPCDriverDSL.createTestUser() = rpcTestUser.copy(permissions = setOf(
|
||||||
|
startFlow<WithFinality.FinalityInvoker>(),
|
||||||
|
startFlow<ContractUpgradeFlow.Initiate<*, *>>(),
|
||||||
|
startFlow<ContractUpgradeFlow.Authorise>(),
|
||||||
|
startFlow<ContractUpgradeFlow.Deauthorise>()
|
||||||
|
))
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Operations
|
||||||
|
private fun CordaRPCOps.initiateDummyContractUpgrade(tx: SignedTransaction) =
|
||||||
|
initiateContractUpgrade(tx, DummyContractV2::class)
|
||||||
|
|
||||||
|
private fun CordaRPCOps.authoriseDummyContractUpgrade(tx: SignedTransaction) =
|
||||||
|
authoriseContractUpgrade(tx, DummyContractV2::class)
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Matchers
|
||||||
|
private fun StartedNode<*>.hasDummyContractUpgradeTransaction() =
|
||||||
|
hasContractUpgradeTransaction<DummyContract.State, DummyContractV2.State>()
|
||||||
|
|
||||||
|
private inline fun <reified FROM : Any, reified TO: Any> StartedNode<*>.hasContractUpgradeTransaction() =
|
||||||
|
has<StateAndRef<ContractState>, ContractUpgradeLedgerTransaction>(
|
||||||
|
"a contract upgrade transaction",
|
||||||
|
{ getContractUpgradeTransaction(it) },
|
||||||
|
isUpgrade<FROM, TO>())
|
||||||
|
|
||||||
|
private fun StartedNode<*>.getContractUpgradeTransaction(state: StateAndRef<ContractState>) =
|
||||||
|
services.validatedTransactions.getTransaction(state.ref.txhash)!!
|
||||||
|
.resolveContractUpgradeTransaction(services)
|
||||||
|
|
||||||
|
private inline fun <reified FROM : Any, reified TO : Any> isUpgrade() =
|
||||||
|
isUpgradeFrom<FROM>() and isUpgradeTo<TO>()
|
||||||
|
|
||||||
|
private inline fun <reified T: Any> isUpgradeFrom() =
|
||||||
|
has<ContractUpgradeLedgerTransaction, Any>("input data", { it.inputs.single().state.data }, isA<T>(anything))
|
||||||
|
|
||||||
|
private inline fun <reified T: Any> isUpgradeTo() =
|
||||||
|
has<ContractUpgradeLedgerTransaction, Any>("output data", { it.outputs.single().data }, isA<T>(anything))
|
||||||
|
//endregion
|
||||||
|
}
|
@ -1,17 +1,17 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.core.flows
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import com.natpryce.hamkrest.*
|
||||||
import net.corda.core.CordaRuntimeException
|
import com.natpryce.hamkrest.assertion.assert
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
|
import net.corda.core.flows.matchers.flow.willReturn
|
||||||
|
import net.corda.core.flows.matchers.flow.willThrow
|
||||||
|
import net.corda.core.flows.mixins.WithContracts
|
||||||
|
import net.corda.core.flows.mixins.WithFinality
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.internal.Emoji
|
import net.corda.core.internal.Emoji
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
import net.corda.core.transactions.ContractUpgradeLedgerTransaction
|
||||||
import net.corda.core.messaging.startFlow
|
|
||||||
import net.corda.core.node.services.queryBy
|
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.finance.USD
|
import net.corda.finance.USD
|
||||||
@ -19,217 +19,126 @@ import net.corda.finance.`issued by`
|
|||||||
import net.corda.finance.contracts.asset.Cash
|
import net.corda.finance.contracts.asset.Cash
|
||||||
import net.corda.finance.flows.CashIssueFlow
|
import net.corda.finance.flows.CashIssueFlow
|
||||||
import net.corda.node.internal.StartedNode
|
import net.corda.node.internal.StartedNode
|
||||||
import net.corda.node.services.Permissions.Companion.startFlow
|
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
import net.corda.testing.contracts.DummyContractV2
|
import net.corda.testing.contracts.DummyContractV2
|
||||||
import net.corda.testing.core.ALICE_NAME
|
import net.corda.testing.core.ALICE_NAME
|
||||||
import net.corda.testing.core.BOB_NAME
|
import net.corda.testing.core.BOB_NAME
|
||||||
import net.corda.testing.core.singleIdentity
|
import net.corda.testing.core.singleIdentity
|
||||||
import net.corda.testing.node.User
|
import net.corda.testing.node.internal.InternalMockNetwork
|
||||||
import net.corda.testing.node.internal.*
|
import net.corda.testing.node.internal.startFlow
|
||||||
import net.corda.testing.node.internal.InternalMockNetwork.MockNode
|
import org.junit.AfterClass
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertFailsWith
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class ContractUpgradeFlowTest {
|
class ContractUpgradeFlowTest : WithContracts, WithFinality {
|
||||||
private lateinit var mockNet: InternalMockNetwork
|
companion object {
|
||||||
private lateinit var aliceNode: StartedNode<MockNode>
|
private val classMockNet = InternalMockNetwork(cordappPackages = listOf(
|
||||||
private lateinit var bobNode: StartedNode<MockNode>
|
"net.corda.testing.contracts",
|
||||||
private lateinit var notary: Party
|
"net.corda.finance.contracts.asset",
|
||||||
private lateinit var alice: Party
|
"net.corda.core.flows"))
|
||||||
private lateinit var bob: Party
|
|
||||||
|
|
||||||
@Before
|
@JvmStatic
|
||||||
fun setup() {
|
@AfterClass
|
||||||
mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset", "net.corda.core.flows"))
|
fun tearDown() = classMockNet.stopNodes()
|
||||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
|
||||||
bobNode = mockNet.createPartyNode(BOB_NAME)
|
|
||||||
notary = mockNet.defaultNotaryIdentity
|
|
||||||
alice = aliceNode.info.singleIdentity()
|
|
||||||
bob = bobNode.info.singleIdentity()
|
|
||||||
|
|
||||||
// Process registration
|
|
||||||
mockNet.runNetwork()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
override val mockNet = classMockNet
|
||||||
fun tearDown() {
|
|
||||||
mockNet.stopNodes()
|
private val aliceNode = makeNode(ALICE_NAME)
|
||||||
}
|
private val bobNode = makeNode(BOB_NAME)
|
||||||
|
|
||||||
|
private val alice = aliceNode.info.singleIdentity()
|
||||||
|
private val bob = bobNode.info.singleIdentity()
|
||||||
|
private val notary = mockNet.defaultNotaryIdentity
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `2 parties contract upgrade`() {
|
fun `2 parties contract upgrade`() {
|
||||||
// Create dummy contract.
|
// Create dummy contract.
|
||||||
val twoPartyDummyContract = DummyContract.generateInitial(0, notary, alice.ref(1), bob.ref(1))
|
val signedByA = aliceNode.signDummyContract(alice.ref(1),0, bob.ref(1))
|
||||||
val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract)
|
val stx = bobNode.addSignatureTo(signedByA)
|
||||||
val stx = bobNode.services.addSignature(signedByA)
|
|
||||||
|
|
||||||
aliceNode.services.startFlow(FinalityFlow(stx, setOf(bob)))
|
aliceNode.finalise(stx, bob)
|
||||||
mockNet.runNetwork()
|
|
||||||
|
|
||||||
val atx = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(stx.id) }
|
val atx = aliceNode.getValidatedTransaction(stx)
|
||||||
val btx = bobNode.database.transaction { bobNode.services.validatedTransactions.getTransaction(stx.id) }
|
val btx = bobNode.getValidatedTransaction(stx)
|
||||||
requireNotNull(atx)
|
|
||||||
requireNotNull(btx)
|
|
||||||
|
|
||||||
// The request is expected to be rejected because party B hasn't authorised the upgrade yet.
|
// The request is expected to be rejected because party B hasn't authorised the upgrade yet.
|
||||||
val rejectedFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx!!.tx.outRef(0), DummyContractV2::class.java))
|
assert.that(
|
||||||
mockNet.runNetwork()
|
aliceNode.initiateDummyContractUpgrade(atx),
|
||||||
assertFailsWith(UnexpectedFlowEndException::class) { rejectedFuture.resultFuture.getOrThrow() }
|
willThrow<UnexpectedFlowEndException>())
|
||||||
|
|
||||||
// Party B authorise the contract state upgrade, and immediately deauthorise the same.
|
// Party B authorise the contract state upgrade, and immediately deauthorise the same.
|
||||||
bobNode.services.startFlow(ContractUpgradeFlow.Authorise(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)).resultFuture.getOrThrow()
|
assert.that(bobNode.authoriseDummyContractUpgrade(btx), willReturn())
|
||||||
bobNode.services.startFlow(ContractUpgradeFlow.Deauthorise(btx.tx.outRef<ContractState>(0).ref)).resultFuture.getOrThrow()
|
assert.that(bobNode.deauthoriseContractUpgrade(btx), willReturn())
|
||||||
|
|
||||||
// The request is expected to be rejected because party B has subsequently deauthorised and a previously authorised upgrade.
|
// The request is expected to be rejected because party B has subsequently deauthorised a previously authorised upgrade.
|
||||||
val deauthorisedFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java))
|
assert.that(
|
||||||
mockNet.runNetwork()
|
aliceNode.initiateDummyContractUpgrade(atx),
|
||||||
assertFailsWith(UnexpectedFlowEndException::class) { deauthorisedFuture.resultFuture.getOrThrow() }
|
willThrow<UnexpectedFlowEndException>())
|
||||||
|
|
||||||
// Party B authorise the contract state upgrade
|
// Party B authorise the contract state upgrade
|
||||||
bobNode.services.startFlow(ContractUpgradeFlow.Authorise(btx.tx.outRef<ContractState>(0), DummyContractV2::class.java)).resultFuture.getOrThrow()
|
assert.that(bobNode.authoriseDummyContractUpgrade(btx), willReturn())
|
||||||
|
|
||||||
// Party A initiates contract upgrade flow, expected to succeed this time.
|
// Party A initiates contract upgrade flow, expected to succeed this time.
|
||||||
val resultFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java))
|
assert.that(
|
||||||
mockNet.runNetwork()
|
aliceNode.initiateDummyContractUpgrade(atx),
|
||||||
|
willReturn(
|
||||||
val result = resultFuture.resultFuture.getOrThrow()
|
aliceNode.hasDummyContractUpgradeTransaction()
|
||||||
|
and bobNode.hasDummyContractUpgradeTransaction()))
|
||||||
fun check(node: StartedNode<MockNode>) {
|
|
||||||
val upgradeTx = node.database.transaction {
|
|
||||||
val wtx = node.services.validatedTransactions.getTransaction(result.ref.txhash)
|
|
||||||
wtx!!.resolveContractUpgradeTransaction(node.services)
|
|
||||||
}
|
|
||||||
assertTrue(upgradeTx.inputs.single().state.data is DummyContract.State)
|
|
||||||
assertTrue(upgradeTx.outputs.single().data is DummyContractV2.State)
|
|
||||||
}
|
|
||||||
check(aliceNode)
|
|
||||||
check(bobNode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun RPCDriverDSL.startProxy(node: StartedNode<MockNode>, user: User): CordaRPCOps {
|
private fun StartedNode<*>.issueCash(amount: Amount<Currency> = Amount(1000, USD)) =
|
||||||
return startRpcClient<CordaRPCOps>(
|
services.startFlow(CashIssueFlow(amount, OpaqueBytes.of(1), notary))
|
||||||
rpcAddress = startRpcServer(
|
.andRunNetwork()
|
||||||
rpcUser = user,
|
.resultFuture.getOrThrow()
|
||||||
ops = node.rpcOps
|
|
||||||
).get().broker.hostAndPort!!,
|
|
||||||
username = user.username,
|
|
||||||
password = user.password
|
|
||||||
).get()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
private fun StartedNode<*>.getBaseStateFromVault() = getStateFromVault(ContractState::class)
|
||||||
fun `2 parties contract upgrade using RPC`() {
|
|
||||||
rpcDriver {
|
|
||||||
// Create dummy contract.
|
|
||||||
val twoPartyDummyContract = DummyContract.generateInitial(0, notary, alice.ref(1), bob.ref(1))
|
|
||||||
val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract)
|
|
||||||
val stx = bobNode.services.addSignature(signedByA)
|
|
||||||
|
|
||||||
val user = rpcTestUser.copy(permissions = setOf(
|
private fun StartedNode<*>.getCashStateFromVault() = getStateFromVault(CashV2.State::class)
|
||||||
startFlow<FinalityInvoker>(),
|
|
||||||
startFlow<ContractUpgradeFlow.Initiate<*, *>>(),
|
|
||||||
startFlow<ContractUpgradeFlow.Authorise>(),
|
|
||||||
startFlow<ContractUpgradeFlow.Deauthorise>()
|
|
||||||
))
|
|
||||||
val rpcA = startProxy(aliceNode, user)
|
|
||||||
val rpcB = startProxy(bobNode, user)
|
|
||||||
val handle = rpcA.startFlow(::FinalityInvoker, stx, setOf(bob))
|
|
||||||
mockNet.runNetwork()
|
|
||||||
handle.returnValue.getOrThrow()
|
|
||||||
|
|
||||||
val atx = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(stx.id) }
|
private fun hasIssuedAmount(expected: Amount<Issued<Currency>>) =
|
||||||
val btx = bobNode.database.transaction { bobNode.services.validatedTransactions.getTransaction(stx.id) }
|
hasContractState(has(CashV2.State::amount, equalTo(expected)))
|
||||||
requireNotNull(atx)
|
|
||||||
requireNotNull(btx)
|
|
||||||
|
|
||||||
val rejectedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Initiate(stateAndRef, upgrade) },
|
private fun belongsTo(vararg recipients: AbstractParty) =
|
||||||
atx!!.tx.outRef<DummyContract.State>(0),
|
hasContractState(has(CashV2.State::owners, equalTo(recipients.toList())))
|
||||||
DummyContractV2::class.java).returnValue
|
|
||||||
|
|
||||||
mockNet.runNetwork()
|
private fun <T : ContractState> hasContractState(expectation: Matcher<T>) =
|
||||||
assertFailsWith(CordaRuntimeException::class) { rejectedFuture.getOrThrow() }
|
has<StateAndRef<T>, T>(
|
||||||
|
"contract state",
|
||||||
// Party B authorise the contract state upgrade, and immediately deauthorise the same.
|
{ it.state.data },
|
||||||
rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) },
|
expectation)
|
||||||
btx!!.tx.outRef<ContractState>(0),
|
|
||||||
DummyContractV2::class.java).returnValue
|
|
||||||
rpcB.startFlow({ stateRef -> ContractUpgradeFlow.Deauthorise(stateRef) },
|
|
||||||
btx.tx.outRef<ContractState>(0).ref).returnValue
|
|
||||||
|
|
||||||
// The request is expected to be rejected because party B has subsequently deauthorised and a previously authorised upgrade.
|
|
||||||
val deauthorisedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Initiate(stateAndRef, upgrade) },
|
|
||||||
atx.tx.outRef<DummyContract.State>(0),
|
|
||||||
DummyContractV2::class.java).returnValue
|
|
||||||
|
|
||||||
mockNet.runNetwork()
|
|
||||||
assertFailsWith(CordaRuntimeException::class) { deauthorisedFuture.getOrThrow() }
|
|
||||||
|
|
||||||
// Party B authorise the contract state upgrade.
|
|
||||||
rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) },
|
|
||||||
btx.tx.outRef<ContractState>(0),
|
|
||||||
DummyContractV2::class.java).returnValue
|
|
||||||
|
|
||||||
// Party A initiates contract upgrade flow, expected to succeed this time.
|
|
||||||
val resultFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Initiate(stateAndRef, upgrade) },
|
|
||||||
atx.tx.outRef<DummyContract.State>(0),
|
|
||||||
DummyContractV2::class.java).returnValue
|
|
||||||
|
|
||||||
mockNet.runNetwork()
|
|
||||||
val result = resultFuture.getOrThrow()
|
|
||||||
// Check results.
|
|
||||||
listOf(aliceNode, bobNode).forEach {
|
|
||||||
val upgradeTx = aliceNode.database.transaction {
|
|
||||||
val wtx = aliceNode.services.validatedTransactions.getTransaction(result.ref.txhash)
|
|
||||||
wtx!!.resolveContractUpgradeTransaction(aliceNode.services)
|
|
||||||
}
|
|
||||||
assertTrue(upgradeTx.inputs.single().state.data is DummyContract.State)
|
|
||||||
assertTrue(upgradeTx.outputs.single().data is DummyContractV2.State)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `upgrade Cash to v2`() {
|
fun `upgrade Cash to v2`() {
|
||||||
// Create some cash.
|
// Create some cash.
|
||||||
val chosenIdentity = alice
|
val cashFlowResult = aliceNode.issueCash()
|
||||||
val result = aliceNode.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), notary))
|
val anonymisedRecipient = cashFlowResult.recipient!!
|
||||||
mockNet.runNetwork()
|
val stateAndRef = cashFlowResult.stx.tx.outRef<Cash.State>(0)
|
||||||
val stx = result.resultFuture.getOrThrow().stx
|
|
||||||
val anonymisedRecipient = result.resultFuture.get().recipient!!
|
|
||||||
val stateAndRef = stx.tx.outRef<Cash.State>(0)
|
|
||||||
val baseState = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy<ContractState>().states.single() }
|
|
||||||
assertTrue(baseState.state.data is Cash.State, "Contract state is old version.")
|
|
||||||
// Starts contract upgrade flow.
|
|
||||||
val upgradeResult = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(stateAndRef, CashV2::class.java))
|
|
||||||
mockNet.runNetwork()
|
|
||||||
upgradeResult.resultFuture.getOrThrow()
|
|
||||||
// Get contract state from the vault.
|
|
||||||
val upgradedStateFromVault = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy<CashV2.State>().states.single() }
|
|
||||||
assertEquals(Amount(1000000, USD).`issued by`(chosenIdentity.ref(1)), upgradedStateFromVault.state.data.amount, "Upgraded cash contain the correct amount.")
|
|
||||||
assertEquals<Collection<AbstractParty>>(listOf(anonymisedRecipient), upgradedStateFromVault.state.data.owners, "Upgraded cash belongs to the right owner.")
|
|
||||||
// Make sure the upgraded state can be spent
|
|
||||||
val movedState = upgradedStateFromVault.state.data.copy(amount = upgradedStateFromVault.state.data.amount.times(2))
|
|
||||||
val spendUpgradedTx = aliceNode.services.signInitialTransaction(
|
|
||||||
TransactionBuilder(notary)
|
|
||||||
.addInputState(upgradedStateFromVault)
|
|
||||||
.addOutputState(
|
|
||||||
upgradedStateFromVault.state.copy(data = movedState)
|
|
||||||
)
|
|
||||||
.addCommand(CashV2.Move(), alice.owningKey)
|
|
||||||
|
|
||||||
)
|
// The un-upgraded state is Cash.State
|
||||||
aliceNode.services.startFlow(FinalityFlow(spendUpgradedTx)).resultFuture.apply {
|
assert.that(aliceNode.getBaseStateFromVault(), hasContractState(isA<Cash.State>(anything)))
|
||||||
mockNet.runNetwork()
|
|
||||||
get()
|
// Starts contract upgrade flow.
|
||||||
|
assert.that(aliceNode.initiateContractUpgrade(stateAndRef, CashV2::class), willReturn())
|
||||||
|
|
||||||
|
// Get contract state from the vault.
|
||||||
|
val upgradedState = aliceNode.getCashStateFromVault()
|
||||||
|
assert.that(upgradedState,
|
||||||
|
hasIssuedAmount(Amount(1000000, USD) `issued by` (alice.ref(1)))
|
||||||
|
and belongsTo(anonymisedRecipient))
|
||||||
|
|
||||||
|
// Make sure the upgraded state can be spent
|
||||||
|
val movedState = upgradedState.state.data.copy(amount = upgradedState.state.data.amount.times(2))
|
||||||
|
val spendUpgradedTx = aliceNode.signInitialTransaction {
|
||||||
|
addInputState(upgradedState)
|
||||||
|
addOutputState(
|
||||||
|
upgradedState.state.copy(data = movedState)
|
||||||
|
)
|
||||||
|
addCommand(CashV2.Move(), alice.owningKey)
|
||||||
}
|
}
|
||||||
val movedStateFromVault = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy<CashV2.State>().states.single() }
|
|
||||||
assertEquals(movedState, movedStateFromVault.state.data)
|
assert.that(aliceNode.finalise(spendUpgradedTx), willReturn())
|
||||||
|
assert.that(aliceNode.getCashStateFromVault(), hasContractState(equalTo(movedState)))
|
||||||
}
|
}
|
||||||
|
|
||||||
class CashV2 : UpgradedContractWithLegacyConstraint<Cash.State, CashV2.State> {
|
class CashV2 : UpgradedContractWithLegacyConstraint<Cash.State, CashV2.State> {
|
||||||
@ -254,10 +163,35 @@ class ContractUpgradeFlowTest {
|
|||||||
override fun verify(tx: LedgerTransaction) {}
|
override fun verify(tx: LedgerTransaction) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@StartableByRPC
|
//region Operations
|
||||||
class FinalityInvoker(private val transaction: SignedTransaction,
|
private fun StartedNode<*>.initiateDummyContractUpgrade(tx: SignedTransaction) =
|
||||||
private val extraRecipients: Set<Party>) : FlowLogic<SignedTransaction>() {
|
initiateContractUpgrade(tx, DummyContractV2::class)
|
||||||
@Suspendable
|
|
||||||
override fun call(): SignedTransaction = subFlow(FinalityFlow(transaction, extraRecipients))
|
private fun StartedNode<*>.authoriseDummyContractUpgrade(tx: SignedTransaction) =
|
||||||
}
|
authoriseContractUpgrade(tx, DummyContractV2::class)
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Matchers
|
||||||
|
private fun StartedNode<*>.hasDummyContractUpgradeTransaction() =
|
||||||
|
hasContractUpgradeTransaction<DummyContract.State, DummyContractV2.State>()
|
||||||
|
|
||||||
|
private inline fun <reified FROM : Any, reified TO: Any> StartedNode<*>.hasContractUpgradeTransaction() =
|
||||||
|
has<StateAndRef<ContractState>, ContractUpgradeLedgerTransaction>(
|
||||||
|
"a contract upgrade transaction",
|
||||||
|
{ getContractUpgradeTransaction(it) },
|
||||||
|
isUpgrade<FROM, TO>())
|
||||||
|
|
||||||
|
private fun StartedNode<*>.getContractUpgradeTransaction(state: StateAndRef<ContractState>) =
|
||||||
|
services.validatedTransactions.getTransaction(state.ref.txhash)!!
|
||||||
|
.resolveContractUpgradeTransaction(services)
|
||||||
|
|
||||||
|
private inline fun <reified FROM : Any, reified TO : Any> isUpgrade() =
|
||||||
|
isUpgradeFrom<FROM>() and isUpgradeTo<TO>()
|
||||||
|
|
||||||
|
private inline fun <reified T: Any> isUpgradeFrom() =
|
||||||
|
has<ContractUpgradeLedgerTransaction, Any>("input data", { it.inputs.single().state.data }, isA<T>(anything))
|
||||||
|
|
||||||
|
private inline fun <reified T: Any> isUpgradeTo() =
|
||||||
|
has<ContractUpgradeLedgerTransaction, Any>("output data", { it.outputs.single().data }, isA<T>(anything))
|
||||||
|
//endregion
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ import net.corda.core.internal.rootCause
|
|||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import org.assertj.core.api.Assertions.catchThrowable
|
import org.assertj.core.api.Assertions.catchThrowable
|
||||||
import org.hamcrest.Matchers.lessThanOrEqualTo
|
import org.hamcrest.Matchers.lessThanOrEqualTo
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Assert.assertThat
|
import org.junit.Assert.assertThat
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -31,30 +30,9 @@ class FastThreadLocalTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val expensiveObjCount = AtomicInteger()
|
private val expensiveObjCount = AtomicInteger()
|
||||||
private lateinit var pool: ExecutorService
|
|
||||||
private lateinit var scheduler: FiberExecutorScheduler
|
|
||||||
private fun init(threadCount: Int, threadImpl: (Runnable) -> Thread) {
|
|
||||||
pool = Executors.newFixedThreadPool(threadCount, threadImpl)
|
|
||||||
scheduler = FiberExecutorScheduler(null, pool)
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun poolShutdown() = try {
|
|
||||||
pool.shutdown()
|
|
||||||
} catch (e: UninitializedPropertyAccessException) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun schedulerShutdown() = try {
|
|
||||||
scheduler.shutdown()
|
|
||||||
} catch (e: UninitializedPropertyAccessException) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ThreadLocal with plain old Thread is fiber-local`() {
|
fun `ThreadLocal with plain old Thread is fiber-local`() = scheduled(3, ::Thread) {
|
||||||
init(3, ::Thread)
|
|
||||||
val threadLocal = object : ThreadLocal<ExpensiveObj>() {
|
val threadLocal = object : ThreadLocal<ExpensiveObj>() {
|
||||||
override fun initialValue() = ExpensiveObj()
|
override fun initialValue() = ExpensiveObj()
|
||||||
}
|
}
|
||||||
@ -63,8 +41,7 @@ class FastThreadLocalTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ThreadLocal with FastThreadLocalThread is fiber-local`() {
|
fun `ThreadLocal with FastThreadLocalThread is fiber-local`() = scheduled(3, ::FastThreadLocalThread) {
|
||||||
init(3, ::FastThreadLocalThread)
|
|
||||||
val threadLocal = object : ThreadLocal<ExpensiveObj>() {
|
val threadLocal = object : ThreadLocal<ExpensiveObj>() {
|
||||||
override fun initialValue() = ExpensiveObj()
|
override fun initialValue() = ExpensiveObj()
|
||||||
}
|
}
|
||||||
@ -73,8 +50,7 @@ class FastThreadLocalTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `FastThreadLocal with plain old Thread is fiber-local`() {
|
fun `FastThreadLocal with plain old Thread is fiber-local`() = scheduled(3, ::Thread) {
|
||||||
init(3, ::Thread)
|
|
||||||
val threadLocal = object : FastThreadLocal<ExpensiveObj>() {
|
val threadLocal = object : FastThreadLocal<ExpensiveObj>() {
|
||||||
override fun initialValue() = ExpensiveObj()
|
override fun initialValue() = ExpensiveObj()
|
||||||
}
|
}
|
||||||
@ -83,8 +59,8 @@ class FastThreadLocalTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `FastThreadLocal with FastThreadLocalThread is not fiber-local`() {
|
fun `FastThreadLocal with FastThreadLocalThread is not fiber-local`() =
|
||||||
init(3, ::FastThreadLocalThread)
|
scheduled(3, ::FastThreadLocalThread) {
|
||||||
val threadLocal = object : FastThreadLocal<ExpensiveObj>() {
|
val threadLocal = object : FastThreadLocal<ExpensiveObj>() {
|
||||||
override fun initialValue() = ExpensiveObj()
|
override fun initialValue() = ExpensiveObj()
|
||||||
}
|
}
|
||||||
@ -93,7 +69,7 @@ class FastThreadLocalTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @return the number of times a different expensive object was obtained post-suspend. */
|
/** @return the number of times a different expensive object was obtained post-suspend. */
|
||||||
private fun runFibers(fiberCount: Int, threadLocalGet: () -> ExpensiveObj): Int {
|
private fun SchedulerContext.runFibers(fiberCount: Int, threadLocalGet: () -> ExpensiveObj): Int {
|
||||||
val fibers = (0 until fiberCount).map { Fiber(scheduler, FiberTask(threadLocalGet)) }
|
val fibers = (0 until fiberCount).map { Fiber(scheduler, FiberTask(threadLocalGet)) }
|
||||||
val startedFibers = fibers.map { it.start() }
|
val startedFibers = fibers.map { it.start() }
|
||||||
return startedFibers.map { it.get() }.count { it }
|
return startedFibers.map { it.get() }.count { it }
|
||||||
@ -127,8 +103,7 @@ class FastThreadLocalTest {
|
|||||||
}::get)
|
}::get)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun contentIsNotSerialized(threadLocalGet: () -> UnserializableObj) {
|
private fun contentIsNotSerialized(threadLocalGet: () -> UnserializableObj) = scheduled(1, ::FastThreadLocalThread) {
|
||||||
init(1, ::FastThreadLocalThread)
|
|
||||||
// Use false like AbstractKryoSerializationScheme, the default of true doesn't work at all:
|
// Use false like AbstractKryoSerializationScheme, the default of true doesn't work at all:
|
||||||
val serializer = Fiber.getFiberSerializer(false)
|
val serializer = Fiber.getFiberSerializer(false)
|
||||||
val returnValue = UUID.randomUUID()
|
val returnValue = UUID.randomUUID()
|
||||||
@ -162,4 +137,21 @@ class FastThreadLocalTest {
|
|||||||
return returnValue
|
return returnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class SchedulerContext(private val pool: ExecutorService, val scheduler: FiberExecutorScheduler) {
|
||||||
|
fun shutdown() {
|
||||||
|
pool.shutdown()
|
||||||
|
scheduler.shutdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduled(threadCount: Int, threadImpl: (Runnable) -> Thread, test: SchedulerContext.() -> Unit) {
|
||||||
|
val pool = Executors.newFixedThreadPool(threadCount, threadImpl)
|
||||||
|
val ctx = SchedulerContext(pool, FiberExecutorScheduler(null, pool))
|
||||||
|
try {
|
||||||
|
ctx.test()
|
||||||
|
} finally {
|
||||||
|
ctx.shutdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,74 +1,68 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.core.flows
|
||||||
|
|
||||||
|
import com.natpryce.hamkrest.and
|
||||||
|
import com.natpryce.hamkrest.assertion.assert
|
||||||
|
import net.corda.core.flows.matchers.flow.willReturn
|
||||||
|
import net.corda.core.flows.matchers.flow.willThrow
|
||||||
|
import net.corda.core.flows.mixins.WithFinality
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.getOrThrow
|
|
||||||
import net.corda.finance.POUNDS
|
import net.corda.finance.POUNDS
|
||||||
import net.corda.finance.contracts.asset.Cash
|
import net.corda.finance.contracts.asset.Cash
|
||||||
import net.corda.finance.issuedBy
|
import net.corda.finance.issuedBy
|
||||||
|
import net.corda.node.internal.StartedNode
|
||||||
import net.corda.testing.core.*
|
import net.corda.testing.core.*
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.internal.InternalMockNetwork
|
||||||
import net.corda.testing.node.StartedMockNode
|
import org.junit.AfterClass
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertFailsWith
|
|
||||||
|
|
||||||
class FinalityFlowTests {
|
class FinalityFlowTests : WithFinality {
|
||||||
companion object {
|
companion object {
|
||||||
private val CHARLIE = TestIdentity(CHARLIE_NAME, 90).party
|
private val CHARLIE = TestIdentity(CHARLIE_NAME, 90).party
|
||||||
|
private val classMockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.finance.contracts.asset"))
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@AfterClass
|
||||||
|
fun tearDown() = classMockNet.stopNodes()
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var mockNet: MockNetwork
|
override val mockNet = classMockNet
|
||||||
private lateinit var aliceNode: StartedMockNode
|
|
||||||
private lateinit var bobNode: StartedMockNode
|
|
||||||
private lateinit var alice: Party
|
|
||||||
private lateinit var bob: Party
|
|
||||||
private lateinit var notary: Party
|
|
||||||
|
|
||||||
@Before
|
private val aliceNode = makeNode(ALICE_NAME)
|
||||||
fun setup() {
|
private val bobNode = makeNode(BOB_NAME)
|
||||||
mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts.asset"))
|
|
||||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
|
||||||
bobNode = mockNet.createPartyNode(BOB_NAME)
|
|
||||||
alice = aliceNode.info.singleIdentity()
|
|
||||||
bob = bobNode.info.singleIdentity()
|
|
||||||
notary = mockNet.defaultNotaryIdentity
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
private val alice = aliceNode.info.singleIdentity()
|
||||||
fun tearDown() {
|
private val bob = bobNode.info.singleIdentity()
|
||||||
mockNet.stopNodes()
|
private val notary = mockNet.defaultNotaryIdentity
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `finalise a simple transaction`() {
|
fun `finalise a simple transaction`() {
|
||||||
val amount = 1000.POUNDS.issuedBy(alice.ref(0))
|
val stx = aliceNode.signCashTransactionWith(bob)
|
||||||
val builder = TransactionBuilder(notary)
|
|
||||||
Cash().generateIssue(builder, amount, bob, notary)
|
assert.that(
|
||||||
val stx = aliceNode.services.signInitialTransaction(builder)
|
aliceNode.finalise(stx),
|
||||||
val flow = aliceNode.startFlow(FinalityFlow(stx))
|
willReturn(
|
||||||
mockNet.runNetwork()
|
requiredSignatures(1)
|
||||||
val notarisedTx = flow.getOrThrow()
|
and visibleTo(bobNode)))
|
||||||
notarisedTx.verifyRequiredSignatures()
|
|
||||||
val transactionSeenByB = bobNode.transaction {
|
|
||||||
bobNode.services.validatedTransactions.getTransaction(notarisedTx.id)
|
|
||||||
}
|
|
||||||
assertEquals(notarisedTx, transactionSeenByB)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `reject a transaction with unknown parties`() {
|
fun `reject a transaction with unknown parties`() {
|
||||||
val amount = 1000.POUNDS.issuedBy(alice.ref(0))
|
// Charlie isn't part of this network, so node A won't recognise them
|
||||||
val fakeIdentity = CHARLIE // Charlie isn't part of this network, so node A won't recognise them
|
val stx = aliceNode.signCashTransactionWith(CHARLIE)
|
||||||
val builder = TransactionBuilder(notary)
|
|
||||||
Cash().generateIssue(builder, amount, fakeIdentity, notary)
|
assert.that(
|
||||||
val stx = aliceNode.services.signInitialTransaction(builder)
|
aliceNode.finalise(stx),
|
||||||
val flow = aliceNode.startFlow(FinalityFlow(stx))
|
willThrow<IllegalArgumentException>())
|
||||||
mockNet.runNetwork()
|
|
||||||
assertFailsWith<IllegalArgumentException> {
|
|
||||||
flow.getOrThrow()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun StartedNode<*>.signCashTransactionWith(other: Party): SignedTransaction {
|
||||||
|
val amount = 1000.POUNDS.issuedBy(alice.ref(0))
|
||||||
|
val builder = TransactionBuilder(notary)
|
||||||
|
Cash().generateIssue(builder, amount, other, notary)
|
||||||
|
|
||||||
|
return services.signInitialTransaction(builder)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,25 +1,34 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.core.flows
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import com.natpryce.hamkrest.assertion.assert
|
||||||
|
import com.natpryce.hamkrest.equalTo
|
||||||
|
import com.natpryce.hamkrest.isA
|
||||||
|
import net.corda.core.flows.matchers.flow.willReturn
|
||||||
|
import net.corda.core.flows.mixins.WithMockNet
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.utilities.UntrustworthyData
|
import net.corda.core.utilities.UntrustworthyData
|
||||||
import net.corda.core.utilities.getOrThrow
|
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
import net.corda.testing.core.singleIdentity
|
import net.corda.testing.core.singleIdentity
|
||||||
import net.corda.testing.node.internal.InternalMockNetwork
|
import net.corda.testing.node.internal.InternalMockNetwork
|
||||||
import net.corda.testing.node.internal.startFlow
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.After
|
import org.junit.AfterClass
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class ReceiveMultipleFlowTests {
|
|
||||||
private val mockNet = InternalMockNetwork()
|
class ReceiveMultipleFlowTests : WithMockNet {
|
||||||
private val nodes = (0..2).map { mockNet.createPartyNode() }
|
companion object {
|
||||||
@After
|
private val classMockNet = InternalMockNetwork()
|
||||||
fun stopNodes() {
|
|
||||||
mockNet.stopNodes()
|
@JvmStatic
|
||||||
|
@AfterClass
|
||||||
|
fun stopNodes() = classMockNet.stopNodes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val mockNet = classMockNet
|
||||||
|
|
||||||
|
private val nodes = (0..2).map { mockNet.createPartyNode() }
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun showcase_flows_as_closures() {
|
fun showcase_flows_as_closures() {
|
||||||
val answer = 10.0
|
val answer = 10.0
|
||||||
@ -49,10 +58,9 @@ class ReceiveMultipleFlowTests {
|
|||||||
} as FlowLogic<Unit>
|
} as FlowLogic<Unit>
|
||||||
}
|
}
|
||||||
|
|
||||||
val flow = nodes[0].services.startFlow(initiatingFlow)
|
assert.that(
|
||||||
mockNet.runNetwork()
|
nodes[0].startFlowAndRunNetwork(initiatingFlow),
|
||||||
val receivedAnswer = flow.resultFuture.getOrThrow()
|
willReturn(answer as Any))
|
||||||
assertThat(receivedAnswer).isEqualTo(answer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -61,10 +69,10 @@ class ReceiveMultipleFlowTests {
|
|||||||
nodes[1].registerAnswer(AlgorithmDefinition::class, doubleValue)
|
nodes[1].registerAnswer(AlgorithmDefinition::class, doubleValue)
|
||||||
val stringValue = "Thriller"
|
val stringValue = "Thriller"
|
||||||
nodes[2].registerAnswer(AlgorithmDefinition::class, stringValue)
|
nodes[2].registerAnswer(AlgorithmDefinition::class, stringValue)
|
||||||
val flow = nodes[0].services.startFlow(ParallelAlgorithmMap(nodes[1].info.singleIdentity(), nodes[2].info.singleIdentity()))
|
|
||||||
mockNet.runNetwork()
|
assert.that(
|
||||||
val result = flow.resultFuture.getOrThrow()
|
nodes[0].startFlowAndRunNetwork(ParallelAlgorithmMap(nodes[1].info.singleIdentity(), nodes[2].info.singleIdentity())),
|
||||||
assertThat(result).isEqualTo(doubleValue * stringValue.length)
|
willReturn(doubleValue * stringValue.length))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -73,12 +81,10 @@ class ReceiveMultipleFlowTests {
|
|||||||
nodes[1].registerAnswer(ParallelAlgorithmList::class, value1)
|
nodes[1].registerAnswer(ParallelAlgorithmList::class, value1)
|
||||||
val value2 = 6.0
|
val value2 = 6.0
|
||||||
nodes[2].registerAnswer(ParallelAlgorithmList::class, value2)
|
nodes[2].registerAnswer(ParallelAlgorithmList::class, value2)
|
||||||
val flow = nodes[0].services.startFlow(ParallelAlgorithmList(nodes[1].info.singleIdentity(), nodes[2].info.singleIdentity()))
|
|
||||||
mockNet.runNetwork()
|
assert.that(
|
||||||
val data = flow.resultFuture.getOrThrow()
|
nodes[0].startFlowAndRunNetwork(ParallelAlgorithmList(nodes[1].info.singleIdentity(), nodes[2].info.singleIdentity())),
|
||||||
assertThat(data[0]).isEqualTo(value1)
|
willReturn(listOf(value1, value2)))
|
||||||
assertThat(data[1]).isEqualTo(value2)
|
|
||||||
assertThat(data.fold(1.0) { a, b -> a * b }).isEqualTo(value1 * value2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ParallelAlgorithmMap(doubleMember: Party, stringMember: Party) : AlgorithmDefinition(doubleMember, stringMember) {
|
class ParallelAlgorithmMap(doubleMember: Party, stringMember: Party) : AlgorithmDefinition(doubleMember, stringMember) {
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
package net.corda.core.flows.matchers
|
||||||
|
|
||||||
|
import com.natpryce.hamkrest.MatchResult
|
||||||
|
import com.natpryce.hamkrest.Matcher
|
||||||
|
import com.natpryce.hamkrest.equalTo
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import java.util.concurrent.Future
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a Flow that succeeds with a result matched by the given matcher
|
||||||
|
*/
|
||||||
|
fun <T> willReturn() = object : Matcher<Future<T>> {
|
||||||
|
override val description: String = "is a future that will succeed"
|
||||||
|
|
||||||
|
override fun invoke(actual: Future<T>): MatchResult = try {
|
||||||
|
actual.getOrThrow()
|
||||||
|
MatchResult.Match
|
||||||
|
} catch (e: Exception) {
|
||||||
|
MatchResult.Mismatch("Failed with $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> willReturn(expected: T): Matcher<Future<out T?>> = willReturn(equalTo(expected))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a Flow that succeeds with a result matched by the given matcher
|
||||||
|
*/
|
||||||
|
fun <T> willReturn(successMatcher: Matcher<T>) = object : Matcher<Future<out T>> {
|
||||||
|
override val description: String = "is a future that will succeed with a value that ${successMatcher.description}"
|
||||||
|
|
||||||
|
override fun invoke(actual: Future<out T>): MatchResult = try {
|
||||||
|
successMatcher(actual.getOrThrow())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
MatchResult.Mismatch("Failed with $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a Flow that fails, with an exception matched by the given matcher.
|
||||||
|
*/
|
||||||
|
inline fun <reified E: Exception> willThrow(failureMatcher: Matcher<E>) = object : Matcher<Future<*>> {
|
||||||
|
override val description: String
|
||||||
|
get() = "is a future that will fail with a ${E::class.java.simpleName} that ${failureMatcher.description}"
|
||||||
|
|
||||||
|
override fun invoke(actual: Future<*>): MatchResult = try {
|
||||||
|
actual.getOrThrow()
|
||||||
|
MatchResult.Mismatch("Succeeded")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
when(e) {
|
||||||
|
is E -> failureMatcher(e)
|
||||||
|
else -> MatchResult.Mismatch("Failure class was ${e.javaClass}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a Flow that fails, with an exception of the specified type.
|
||||||
|
*/
|
||||||
|
inline fun <reified E: Exception> willThrow() = object : Matcher<Future<*>> {
|
||||||
|
override val description: String
|
||||||
|
get() = "is a future that will fail with a ${E::class.java}"
|
||||||
|
|
||||||
|
override fun invoke(actual: Future<*>): MatchResult = try {
|
||||||
|
actual.getOrThrow()
|
||||||
|
MatchResult.Mismatch("Succeeded")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
when(e) {
|
||||||
|
is E -> MatchResult.Match
|
||||||
|
else -> MatchResult.Mismatch("Failure class was ${e.javaClass}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package net.corda.core.flows.matchers.flow
|
||||||
|
|
||||||
|
import com.natpryce.hamkrest.Matcher
|
||||||
|
import com.natpryce.hamkrest.equalTo
|
||||||
|
import com.natpryce.hamkrest.has
|
||||||
|
import net.corda.core.flows.matchers.willThrow
|
||||||
|
import net.corda.core.flows.matchers.willReturn
|
||||||
|
import net.corda.core.internal.FlowStateMachine
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a Flow that succeeds with a result matched by the given matcher
|
||||||
|
*/
|
||||||
|
fun <T> willReturn() = has(FlowStateMachine<T>::resultFuture, willReturn())
|
||||||
|
|
||||||
|
fun <T> willReturn(expected: T): Matcher<FlowStateMachine<out T?>> = net.corda.core.flows.matchers.flow.willReturn(equalTo(expected))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a Flow that succeeds with a result matched by the given matcher
|
||||||
|
*/
|
||||||
|
fun <T> willReturn(successMatcher: Matcher<T>) = has(
|
||||||
|
FlowStateMachine<out T>::resultFuture,
|
||||||
|
willReturn(successMatcher))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a Flow that fails, with an exception matched by the given matcher.
|
||||||
|
*/
|
||||||
|
inline fun <reified E: Exception> willThrow(failureMatcher: Matcher<E>) = has(
|
||||||
|
FlowStateMachine<*>::resultFuture,
|
||||||
|
willThrow(failureMatcher))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a Flow that fails, with an exception of the specified type.
|
||||||
|
*/
|
||||||
|
inline fun <reified E: Exception> willThrow() = has(
|
||||||
|
FlowStateMachine<*>::resultFuture,
|
||||||
|
willThrow<E>())
|
@ -0,0 +1,31 @@
|
|||||||
|
package net.corda.core.flows.matchers.rpc
|
||||||
|
|
||||||
|
import com.natpryce.hamkrest.Matcher
|
||||||
|
import com.natpryce.hamkrest.has
|
||||||
|
import net.corda.core.flows.matchers.willThrow
|
||||||
|
import net.corda.core.flows.matchers.willReturn
|
||||||
|
import net.corda.core.messaging.FlowHandle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a flow handle that succeeds with a result matched by the given matcher
|
||||||
|
*/
|
||||||
|
fun <T> willReturn() = has(FlowHandle<T>::returnValue, willReturn())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a flow handle that succeeds with a result matched by the given matcher
|
||||||
|
*/
|
||||||
|
fun <T> willReturn(successMatcher: Matcher<T>) = has(FlowHandle<out T>::returnValue, willReturn(successMatcher))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a flow handle that fails, with an exception matched by the given matcher.
|
||||||
|
*/
|
||||||
|
inline fun <reified E: Exception> willThrow(failureMatcher: Matcher<E>) = has(
|
||||||
|
FlowHandle<*>::returnValue,
|
||||||
|
willThrow(failureMatcher))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a flow handle that fails, with an exception of the specified type.
|
||||||
|
*/
|
||||||
|
inline fun <reified E: Exception> willThrow() = has(
|
||||||
|
FlowHandle<*>::returnValue,
|
||||||
|
willThrow<E>())
|
@ -0,0 +1,83 @@
|
|||||||
|
package net.corda.core.flows.mixins
|
||||||
|
|
||||||
|
import net.corda.core.contracts.ContractState
|
||||||
|
import net.corda.core.contracts.PartyAndReference
|
||||||
|
import net.corda.core.contracts.StateAndRef
|
||||||
|
import net.corda.core.contracts.UpgradedContract
|
||||||
|
import net.corda.core.flows.CollectSignaturesFlow
|
||||||
|
import net.corda.core.flows.ContractUpgradeFlow
|
||||||
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
|
import net.corda.core.messaging.startFlow
|
||||||
|
import net.corda.core.node.ServiceHub
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
import net.corda.node.internal.StartedNode
|
||||||
|
import net.corda.testing.contracts.DummyContract
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mix this interface into a test class to get useful generator and operation functions for working with dummy contracts
|
||||||
|
*/
|
||||||
|
interface WithContracts : WithMockNet {
|
||||||
|
|
||||||
|
//region Generators
|
||||||
|
fun createDummyContract(owner: PartyAndReference, magicNumber: Int = 0, vararg others: PartyAndReference) =
|
||||||
|
DummyContract.generateInitial(
|
||||||
|
magicNumber,
|
||||||
|
mockNet.defaultNotaryIdentity,
|
||||||
|
owner,
|
||||||
|
*others)
|
||||||
|
//region
|
||||||
|
|
||||||
|
//region Operations
|
||||||
|
fun StartedNode<*>.signDummyContract(owner: PartyAndReference, magicNumber: Int = 0, vararg others: PartyAndReference) =
|
||||||
|
services.signDummyContract(owner, magicNumber, *others).andRunNetwork()
|
||||||
|
|
||||||
|
fun ServiceHub.signDummyContract(owner: PartyAndReference, magicNumber: Int = 0, vararg others: PartyAndReference) =
|
||||||
|
signInitialTransaction(createDummyContract(owner, magicNumber, *others))
|
||||||
|
|
||||||
|
fun StartedNode<*>.collectSignatures(ptx: SignedTransaction) =
|
||||||
|
startFlowAndRunNetwork(CollectSignaturesFlow(ptx, emptySet()))
|
||||||
|
|
||||||
|
fun StartedNode<*>.addSignatureTo(ptx: SignedTransaction) =
|
||||||
|
services.addSignature(ptx).andRunNetwork()
|
||||||
|
|
||||||
|
fun <T : UpgradedContract<*, *>>
|
||||||
|
StartedNode<*>.initiateContractUpgrade(tx: SignedTransaction, toClass: KClass<T>) =
|
||||||
|
initiateContractUpgrade(tx.tx.outRef(0), toClass)
|
||||||
|
|
||||||
|
fun <S : ContractState, T : UpgradedContract<S, *>>
|
||||||
|
StartedNode<*>.initiateContractUpgrade(stateAndRef: StateAndRef<S>, toClass: KClass<T>) =
|
||||||
|
startFlowAndRunNetwork(ContractUpgradeFlow.Initiate(stateAndRef, toClass.java))
|
||||||
|
|
||||||
|
fun <T : UpgradedContract<*, *>> StartedNode<*>.authoriseContractUpgrade(
|
||||||
|
tx: SignedTransaction, toClass: KClass<T>) =
|
||||||
|
startFlow(
|
||||||
|
ContractUpgradeFlow.Authorise(tx.tx.outRef<ContractState>(0), toClass.java)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun StartedNode<*>.deauthoriseContractUpgrade(tx: SignedTransaction) = startFlow(
|
||||||
|
ContractUpgradeFlow.Deauthorise(tx.tx.outRef<ContractState>(0).ref)
|
||||||
|
)
|
||||||
|
|
||||||
|
// RPC versions of the above
|
||||||
|
fun <S : ContractState, T : UpgradedContract<S, *>> CordaRPCOps.initiateContractUpgrade(
|
||||||
|
tx: SignedTransaction, toClass: KClass<T>) =
|
||||||
|
startFlow(
|
||||||
|
{ stateAndRef, upgrade -> ContractUpgradeFlow.Initiate(stateAndRef, upgrade) },
|
||||||
|
tx.tx.outRef<S>(0),
|
||||||
|
toClass.java)
|
||||||
|
.andRunNetwork()
|
||||||
|
|
||||||
|
fun <S : ContractState, T : UpgradedContract<S, *>> CordaRPCOps.authoriseContractUpgrade(
|
||||||
|
tx: SignedTransaction, toClass: KClass<T>) =
|
||||||
|
startFlow(
|
||||||
|
{ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) },
|
||||||
|
tx.tx.outRef<S>(0),
|
||||||
|
toClass.java)
|
||||||
|
|
||||||
|
fun CordaRPCOps.deauthoriseContractUpgrade(tx: SignedTransaction) =
|
||||||
|
startFlow(
|
||||||
|
{ stateRef -> ContractUpgradeFlow.Deauthorise(stateRef) },
|
||||||
|
tx.tx.outRef<ContractState>(0).ref)
|
||||||
|
//region
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package net.corda.core.flows.mixins
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import com.natpryce.hamkrest.Matcher
|
||||||
|
import com.natpryce.hamkrest.equalTo
|
||||||
|
import net.corda.core.flows.FinalityFlow
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.StartableByRPC
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
|
import net.corda.core.messaging.startFlow
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
import net.corda.node.internal.StartedNode
|
||||||
|
import net.corda.testing.core.singleIdentity
|
||||||
|
|
||||||
|
interface WithFinality : WithMockNet {
|
||||||
|
|
||||||
|
//region Operations
|
||||||
|
fun StartedNode<*>.finalise(stx: SignedTransaction, vararg additionalParties: Party) =
|
||||||
|
startFlowAndRunNetwork(FinalityFlow(stx, additionalParties.toSet()))
|
||||||
|
|
||||||
|
fun StartedNode<*>.getValidatedTransaction(stx: SignedTransaction) =
|
||||||
|
services.validatedTransactions.getTransaction(stx.id)!!
|
||||||
|
|
||||||
|
fun CordaRPCOps.finalise(stx: SignedTransaction, vararg parties: Party) =
|
||||||
|
startFlow(::FinalityInvoker, stx, parties.toSet())
|
||||||
|
.andRunNetwork()
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Matchers
|
||||||
|
fun visibleTo(other: StartedNode<*>) = object : Matcher<SignedTransaction> {
|
||||||
|
override val description = "has a transaction visible to ${other.info.singleIdentity()}"
|
||||||
|
override fun invoke(actual: SignedTransaction) =
|
||||||
|
equalTo(actual)(other.getValidatedTransaction(actual))
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
@StartableByRPC
|
||||||
|
class FinalityInvoker(private val transaction: SignedTransaction,
|
||||||
|
private val extraRecipients: Set<Party>) : FlowLogic<SignedTransaction>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): SignedTransaction = subFlow(FinalityFlow(transaction, extraRecipients))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
package net.corda.core.flows.mixins
|
||||||
|
|
||||||
|
import com.natpryce.hamkrest.*
|
||||||
|
import net.corda.core.contracts.ContractState
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
|
import net.corda.core.internal.FlowStateMachine
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
import net.corda.node.internal.StartedNode
|
||||||
|
import net.corda.testing.node.internal.InternalMockNetwork
|
||||||
|
import net.corda.testing.node.internal.startFlow
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mix this interface into a test to provide functions useful for working with a mock network
|
||||||
|
*/
|
||||||
|
interface WithMockNet {
|
||||||
|
|
||||||
|
val mockNet: InternalMockNetwork
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a node using a randomised version of the given name
|
||||||
|
*/
|
||||||
|
fun makeNode(name: CordaX500Name) = mockNet.createPartyNode(randomise(name))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Randomise a party name to avoid clashes with other tests
|
||||||
|
*/
|
||||||
|
fun randomise(name: CordaX500Name) = name.copy(commonName = "${name.commonName}_${UUID.randomUUID()}")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the mock network before proceeding
|
||||||
|
*/
|
||||||
|
fun <T: Any> T.andRunNetwork(): T = apply { mockNet.runNetwork() }
|
||||||
|
|
||||||
|
//region Operations
|
||||||
|
/**
|
||||||
|
* Sign an initial transaction
|
||||||
|
*/
|
||||||
|
fun StartedNode<*>.signInitialTransaction(build: TransactionBuilder.() -> TransactionBuilder) =
|
||||||
|
services.signInitialTransaction(TransactionBuilder(mockNet.defaultNotaryIdentity).build())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the sole instance of a state of a particular class from the node's vault
|
||||||
|
*/
|
||||||
|
fun <S: ContractState> StartedNode<*>.getStateFromVault(stateClass: KClass<S>) =
|
||||||
|
services.vaultService.queryBy(stateClass.java).states.single()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a flow
|
||||||
|
*/
|
||||||
|
fun <T> StartedNode<*>.startFlow(logic: FlowLogic<T>): FlowStateMachine<T> = services.startFlow(logic)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a flow and run the network immediately afterwards
|
||||||
|
*/
|
||||||
|
fun <T> StartedNode<*>.startFlowAndRunNetwork(logic: FlowLogic<T>): FlowStateMachine<T> =
|
||||||
|
startFlow(logic).andRunNetwork()
|
||||||
|
|
||||||
|
fun StartedNode<*>.createConfidentialIdentity(party: Party) =
|
||||||
|
services.keyManagementService.freshKeyAndCert(
|
||||||
|
services.myInfo.legalIdentitiesAndCerts.single { it.name == party.name },
|
||||||
|
false)
|
||||||
|
|
||||||
|
fun StartedNode<*>.verifyAndRegister(identity: PartyAndCertificate) =
|
||||||
|
services.identityService.verifyAndRegisterIdentity(identity)
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Matchers
|
||||||
|
/**
|
||||||
|
* The transaction has the required number of verified signatures
|
||||||
|
*/
|
||||||
|
fun requiredSignatures(count: Int = 1) = object : Matcher<SignedTransaction> {
|
||||||
|
override val description: String = "A transaction with valid required signatures"
|
||||||
|
|
||||||
|
override fun invoke(actual: SignedTransaction): MatchResult = try {
|
||||||
|
actual.verifyRequiredSignatures()
|
||||||
|
has(SignedTransaction::sigs, hasSize(equalTo(count)))(actual)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
MatchResult.Mismatch("$e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The exception has the expected error message
|
||||||
|
*/
|
||||||
|
fun errorMessage(expected: String) = has(
|
||||||
|
Exception::message,
|
||||||
|
equalTo(expected))
|
||||||
|
//endregion
|
||||||
|
}
|
@ -1,56 +0,0 @@
|
|||||||
package net.corda.core.matchers
|
|
||||||
|
|
||||||
import com.natpryce.hamkrest.MatchResult
|
|
||||||
import com.natpryce.hamkrest.Matcher
|
|
||||||
import net.corda.core.internal.FlowStateMachine
|
|
||||||
import net.corda.core.utilities.getOrThrow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Matches a Flow that succeeds with a result matched by the given matcher
|
|
||||||
*/
|
|
||||||
fun <T> succeedsWith(successMatcher: Matcher<T>) = object : Matcher<FlowStateMachine<T>> {
|
|
||||||
override val description: String
|
|
||||||
get() = "A flow that succeeds with ${successMatcher.description}"
|
|
||||||
|
|
||||||
override fun invoke(actual: FlowStateMachine<T>): MatchResult = try {
|
|
||||||
successMatcher(actual.resultFuture.getOrThrow())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
MatchResult.Mismatch("Failed with $e")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Matches a Flow that fails, with an exception matched by the given matcher.
|
|
||||||
*/
|
|
||||||
inline fun <reified E: Exception> failsWith(failureMatcher: Matcher<E>) = object : Matcher<FlowStateMachine<*>> {
|
|
||||||
override val description: String
|
|
||||||
get() = "A flow that fails with a ${E::class.java} that ${failureMatcher.description}"
|
|
||||||
|
|
||||||
override fun invoke(actual: FlowStateMachine<*>): MatchResult = try {
|
|
||||||
actual.resultFuture.getOrThrow()
|
|
||||||
MatchResult.Mismatch("Succeeded")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
when(e) {
|
|
||||||
is E -> failureMatcher(e)
|
|
||||||
else -> MatchResult.Mismatch("Failure class was ${e.javaClass}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Matches a Flow that fails, with an exception of the specified type.
|
|
||||||
*/
|
|
||||||
inline fun <reified E: Exception> failsWith() = object : Matcher<FlowStateMachine<*>> {
|
|
||||||
override val description: String
|
|
||||||
get() = "A flow that fails with a ${E::class.java}"
|
|
||||||
|
|
||||||
override fun invoke(actual: FlowStateMachine<*>): MatchResult = try {
|
|
||||||
actual.resultFuture.getOrThrow()
|
|
||||||
MatchResult.Mismatch("Succeeded")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
when(e) {
|
|
||||||
is E -> MatchResult.Match
|
|
||||||
else -> MatchResult.Mismatch("Failure class was ${e.javaClass}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,7 +8,10 @@ import net.corda.core.transactions.LedgerTransaction
|
|||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.finance.POUNDS
|
import net.corda.finance.POUNDS
|
||||||
import net.corda.testing.core.*
|
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||||
|
import net.corda.testing.core.SerializationEnvironmentRule
|
||||||
|
import net.corda.testing.core.TestIdentity
|
||||||
|
import net.corda.testing.core.generateStateRef
|
||||||
import net.corda.testing.internal.TEST_TX_TIME
|
import net.corda.testing.internal.TEST_TX_TIME
|
||||||
import net.corda.testing.internal.rigorousMock
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package net.corda.core.utilities
|
package net.corda.core.utilities
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
|
||||||
|
|
||||||
class NetworkHostAndPortTest {
|
class NetworkHostAndPortTest {
|
||||||
/**
|
/**
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
package net.corda.core.utilities
|
package net.corda.core.utilities
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFails
|
import kotlin.test.assertFails
|
||||||
import org.assertj.core.api.Assertions.*
|
|
||||||
|
|
||||||
class ProgressTrackerTest {
|
class ProgressTrackerTest {
|
||||||
object SimpleSteps {
|
object SimpleSteps {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user