mirror of
https://github.com/corda/corda.git
synced 2025-02-20 09:26:41 +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.junit_version = '4.12'
|
||||
ext.mockito_version = '2.18.3'
|
||||
ext.hamkrest_version = '1.4.2.2'
|
||||
ext.jopt_simple_version = '5.0.2'
|
||||
ext.jansi_version = '1.14'
|
||||
ext.hibernate_version = '5.2.6.Final'
|
||||
|
@ -69,7 +69,7 @@ dependencies {
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
|
||||
// Hamkrest, for fluent, composable matchers
|
||||
testCompile 'com.natpryce:hamkrest:1.4.2.2'
|
||||
testCompile "com.natpryce:hamkrest:$hamkrest_version"
|
||||
|
||||
// Quasar, for suspendable fibres.
|
||||
compileOnly("$quasar_group:quasar-core:$quasar_version:jdk8") {
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.finance.*
|
||||
import net.corda.core.contracts.Amount.Companion.sumOrZero
|
||||
import net.corda.finance.*
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
|
@ -2,8 +2,12 @@ package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.natpryce.hamkrest.*
|
||||
import com.natpryce.hamkrest.assertion.assert
|
||||
import net.corda.core.contracts.Attachment
|
||||
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.Party
|
||||
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.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.InternalMockNodeParameters
|
||||
import net.corda.testing.node.internal.startFlow
|
||||
import org.junit.AfterClass
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.*
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import com.natpryce.hamkrest.assertion.assert
|
||||
import net.corda.core.matchers.*
|
||||
|
||||
class AttachmentTests {
|
||||
class AttachmentTests : WithMockNet {
|
||||
companion object {
|
||||
val mockNet = InternalMockNetwork()
|
||||
val classMockNet = InternalMockNetwork()
|
||||
|
||||
@JvmStatic
|
||||
@AfterClass
|
||||
fun cleanUp() = mockNet.stopNodes()
|
||||
fun cleanUp() = classMockNet.stopNodes()
|
||||
}
|
||||
|
||||
override val mockNet = classMockNet
|
||||
|
||||
// Test nodes
|
||||
private val aliceNode = makeNode(ALICE_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.
|
||||
assert.that(
|
||||
bobNode.startAttachmentFlow(id, alice),
|
||||
succeedsWith(noAttachments()))
|
||||
willReturn(noAttachments()))
|
||||
|
||||
// Verify it was inserted into node one's store.
|
||||
val attachment = bobNode.getAttachmentWithId(id)
|
||||
@ -59,7 +61,7 @@ class AttachmentTests {
|
||||
|
||||
assert.that(
|
||||
bobNode.startAttachmentFlow(id, alice),
|
||||
succeedsWith(soleAttachment(attachment)))
|
||||
willReturn(soleAttachment(attachment)))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -69,10 +71,14 @@ class AttachmentTests {
|
||||
// Get node one to fetch a non-existent attachment.
|
||||
assert.that(
|
||||
bobNode.startAttachmentFlow(hash, alice),
|
||||
failsWith<FetchDataFlow.HashNotFound>(
|
||||
has("requested hash", { it.requested }, equalTo(hash))))
|
||||
willThrow(withRequestedHash(hash)))
|
||||
}
|
||||
|
||||
fun withRequestedHash(expected: SecureHash) = has(
|
||||
"requested hash",
|
||||
FetchDataFlow.HashNotFound::requested,
|
||||
equalTo(expected))
|
||||
|
||||
@Test
|
||||
fun maliciousResponse() {
|
||||
// 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.
|
||||
assert.that(
|
||||
bobNode.startAttachmentFlow(id, badAlice),
|
||||
failsWith<FetchDataFlow.DownloadedVsRequestedDataMismatch>()
|
||||
willThrow<FetchDataFlow.DownloadedVsRequestedDataMismatch>()
|
||||
)
|
||||
}
|
||||
|
||||
@ -113,22 +119,20 @@ class AttachmentTests {
|
||||
}
|
||||
|
||||
//region Generators
|
||||
private fun makeNode(name: CordaX500Name) =
|
||||
mockNet.createPartyNode(randomiseName(name)).apply {
|
||||
override fun makeNode(name: CordaX500Name) =
|
||||
mockNet.createPartyNode(randomise(name)).apply {
|
||||
registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
}
|
||||
|
||||
// Makes a node that doesn't do sanity checking at load time.
|
||||
private fun makeBadNode(name: CordaX500Name) = mockNet.createNode(
|
||||
InternalMockNodeParameters(legalName = randomiseName(name)),
|
||||
InternalMockNodeParameters(legalName = randomise(name)),
|
||||
nodeFactory = { args ->
|
||||
object : InternalMockNetwork.MockNode(args) {
|
||||
override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false }
|
||||
}
|
||||
}).apply { registerInitiatedFlow(FetchAttachmentsResponse::class.java) }
|
||||
|
||||
private fun randomiseName(name: CordaX500Name) = name.copy(commonName = "${name.commonName}_${UUID.randomUUID()}")
|
||||
|
||||
private fun fakeAttachment(): ByteArray =
|
||||
ByteArrayOutputStream().use { baos ->
|
||||
JarOutputStream(baos).use { jos ->
|
||||
@ -144,24 +148,19 @@ class AttachmentTests {
|
||||
//endregion
|
||||
|
||||
//region Operations
|
||||
private fun StartedNode<*>.importAttachment(attachment: ByteArray) = database.transaction {
|
||||
private fun StartedNode<*>.importAttachment(attachment: ByteArray) =
|
||||
attachments.importAttachment(attachment.inputStream(), "test", null)
|
||||
.andRunNetwork()
|
||||
|
||||
private fun StartedNode<*>.updateAttachment(attachment: NodeAttachmentService.DBAttachment) = database.transaction {
|
||||
session.update(attachment)
|
||||
}.andRunNetwork()
|
||||
|
||||
private fun StartedNode<*>.updateAttachment(attachment: NodeAttachmentService.DBAttachment) =
|
||||
database.transaction { session.update(attachment) }.andRunNetwork()
|
||||
private fun StartedNode<*>.startAttachmentFlow(hash: SecureHash, otherSide: Party) = startFlowAndRunNetwork(
|
||||
InitiatingFetchAttachmentsFlow(otherSide, setOf(hash)))
|
||||
|
||||
private fun StartedNode<*>.startAttachmentFlow(hash: SecureHash, otherSide: Party) = services.startFlow(
|
||||
InitiatingFetchAttachmentsFlow(otherSide, setOf(hash))).andRunNetwork()
|
||||
|
||||
private fun StartedNode<*>.getAttachmentWithId(id: SecureHash) = database.transaction {
|
||||
private fun StartedNode<*>.getAttachmentWithId(id: SecureHash) =
|
||||
attachments.openAttachment(id)!!
|
||||
}
|
||||
|
||||
private fun <T : Any> T.andRunNetwork(): T {
|
||||
mockNet.runNetwork()
|
||||
return this
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Matchers
|
||||
|
@ -1,64 +1,105 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.natpryce.hamkrest.assertion.assert
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.StateAndContract
|
||||
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.Party
|
||||
import net.corda.core.identity.excludeHostNode
|
||||
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
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.core.*
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.InternalMockNetwork.MockNode
|
||||
import net.corda.testing.node.internal.startFlow
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.AfterClass
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class CollectSignaturesFlowTests {
|
||||
class CollectSignaturesFlowTests : WithContracts {
|
||||
companion object {
|
||||
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
|
||||
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
|
||||
override val mockNet = classMockNet
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.core.flows"))
|
||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||
charlieNode = mockNet.createPartyNode(CHARLIE_NAME)
|
||||
alice = aliceNode.info.singleIdentity()
|
||||
bob = bobNode.info.singleIdentity()
|
||||
charlie = charlieNode.info.singleIdentity()
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
private val aliceNode = makeNode(ALICE_NAME)
|
||||
private val bobNode = makeNode(BOB_NAME)
|
||||
private val charlieNode = makeNode(CHARLIE_NAME)
|
||||
|
||||
private val alice = aliceNode.info.singleIdentity()
|
||||
private val bob = bobNode.info.singleIdentity()
|
||||
private val charlie = charlieNode.info.singleIdentity()
|
||||
|
||||
@Test
|
||||
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
|
||||
fun tearDown() {
|
||||
mockNet.stopNodes()
|
||||
@Test
|
||||
fun `no need to collect any signatures`() {
|
||||
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
|
||||
// override "checkTransaction" and add whatever logic their require to verify the SignedTransaction they are
|
||||
// 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.inputs.isEmpty())
|
||||
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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
//region
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import com.natpryce.hamkrest.*
|
||||
import com.natpryce.hamkrest.assertion.assert
|
||||
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.Party
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.transactions.ContractUpgradeLedgerTransaction
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
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.flows.CashIssueFlow
|
||||
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.After
|
||||
import org.junit.Before
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.startFlow
|
||||
import org.junit.AfterClass
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ContractUpgradeFlowTest {
|
||||
private lateinit var mockNet: InternalMockNetwork
|
||||
private lateinit var aliceNode: StartedNode<MockNode>
|
||||
private lateinit var bobNode: StartedNode<MockNode>
|
||||
private lateinit var notary: Party
|
||||
private lateinit var alice: Party
|
||||
private lateinit var bob: Party
|
||||
class ContractUpgradeFlowTest : WithContracts, WithFinality {
|
||||
companion object {
|
||||
private val classMockNet = InternalMockNetwork(cordappPackages = listOf(
|
||||
"net.corda.testing.contracts",
|
||||
"net.corda.finance.contracts.asset",
|
||||
"net.corda.core.flows"))
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset", "net.corda.core.flows"))
|
||||
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()
|
||||
@JvmStatic
|
||||
@AfterClass
|
||||
fun tearDown() = classMockNet.stopNodes()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockNet.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()
|
||||
private val notary = mockNet.defaultNotaryIdentity
|
||||
|
||||
@Test
|
||||
fun `2 parties contract upgrade`() {
|
||||
// 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 signedByA = aliceNode.signDummyContract(alice.ref(1),0, bob.ref(1))
|
||||
val stx = bobNode.addSignatureTo(signedByA)
|
||||
|
||||
aliceNode.services.startFlow(FinalityFlow(stx, setOf(bob)))
|
||||
mockNet.runNetwork()
|
||||
aliceNode.finalise(stx, bob)
|
||||
|
||||
val atx = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(stx.id) }
|
||||
val btx = bobNode.database.transaction { bobNode.services.validatedTransactions.getTransaction(stx.id) }
|
||||
requireNotNull(atx)
|
||||
requireNotNull(btx)
|
||||
val atx = aliceNode.getValidatedTransaction(stx)
|
||||
val btx = bobNode.getValidatedTransaction(stx)
|
||||
|
||||
// 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))
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith(UnexpectedFlowEndException::class) { rejectedFuture.resultFuture.getOrThrow() }
|
||||
assert.that(
|
||||
aliceNode.initiateDummyContractUpgrade(atx),
|
||||
willThrow<UnexpectedFlowEndException>())
|
||||
|
||||
// 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()
|
||||
bobNode.services.startFlow(ContractUpgradeFlow.Deauthorise(btx.tx.outRef<ContractState>(0).ref)).resultFuture.getOrThrow()
|
||||
assert.that(bobNode.authoriseDummyContractUpgrade(btx), willReturn())
|
||||
assert.that(bobNode.deauthoriseContractUpgrade(btx), willReturn())
|
||||
|
||||
// The request is expected to be rejected because party B has subsequently deauthorised and a previously authorised upgrade.
|
||||
val deauthorisedFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java))
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith(UnexpectedFlowEndException::class) { deauthorisedFuture.resultFuture.getOrThrow() }
|
||||
// The request is expected to be rejected because party B has subsequently deauthorised a previously authorised upgrade.
|
||||
assert.that(
|
||||
aliceNode.initiateDummyContractUpgrade(atx),
|
||||
willThrow<UnexpectedFlowEndException>())
|
||||
|
||||
// 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.
|
||||
val resultFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java))
|
||||
mockNet.runNetwork()
|
||||
|
||||
val result = resultFuture.resultFuture.getOrThrow()
|
||||
|
||||
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)
|
||||
assert.that(
|
||||
aliceNode.initiateDummyContractUpgrade(atx),
|
||||
willReturn(
|
||||
aliceNode.hasDummyContractUpgradeTransaction()
|
||||
and bobNode.hasDummyContractUpgradeTransaction()))
|
||||
}
|
||||
|
||||
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 StartedNode<*>.issueCash(amount: Amount<Currency> = Amount(1000, USD)) =
|
||||
services.startFlow(CashIssueFlow(amount, OpaqueBytes.of(1), notary))
|
||||
.andRunNetwork()
|
||||
.resultFuture.getOrThrow()
|
||||
|
||||
@Test
|
||||
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)
|
||||
private fun StartedNode<*>.getBaseStateFromVault() = getStateFromVault(ContractState::class)
|
||||
|
||||
val user = rpcTestUser.copy(permissions = setOf(
|
||||
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()
|
||||
private fun StartedNode<*>.getCashStateFromVault() = getStateFromVault(CashV2.State::class)
|
||||
|
||||
val atx = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(stx.id) }
|
||||
val btx = bobNode.database.transaction { bobNode.services.validatedTransactions.getTransaction(stx.id) }
|
||||
requireNotNull(atx)
|
||||
requireNotNull(btx)
|
||||
private fun hasIssuedAmount(expected: Amount<Issued<Currency>>) =
|
||||
hasContractState(has(CashV2.State::amount, equalTo(expected)))
|
||||
|
||||
val rejectedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Initiate(stateAndRef, upgrade) },
|
||||
atx!!.tx.outRef<DummyContract.State>(0),
|
||||
DummyContractV2::class.java).returnValue
|
||||
private fun belongsTo(vararg recipients: AbstractParty) =
|
||||
hasContractState(has(CashV2.State::owners, equalTo(recipients.toList())))
|
||||
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith(CordaRuntimeException::class) { rejectedFuture.getOrThrow() }
|
||||
|
||||
// Party B authorise the contract state upgrade, and immediately deauthorise the same.
|
||||
rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) },
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun <T : ContractState> hasContractState(expectation: Matcher<T>) =
|
||||
has<StateAndRef<T>, T>(
|
||||
"contract state",
|
||||
{ it.state.data },
|
||||
expectation)
|
||||
|
||||
@Test
|
||||
fun `upgrade Cash to v2`() {
|
||||
// Create some cash.
|
||||
val chosenIdentity = alice
|
||||
val result = aliceNode.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), notary))
|
||||
mockNet.runNetwork()
|
||||
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)
|
||||
val cashFlowResult = aliceNode.issueCash()
|
||||
val anonymisedRecipient = cashFlowResult.recipient!!
|
||||
val stateAndRef = cashFlowResult.stx.tx.outRef<Cash.State>(0)
|
||||
|
||||
)
|
||||
aliceNode.services.startFlow(FinalityFlow(spendUpgradedTx)).resultFuture.apply {
|
||||
mockNet.runNetwork()
|
||||
get()
|
||||
// The un-upgraded state is Cash.State
|
||||
assert.that(aliceNode.getBaseStateFromVault(), hasContractState(isA<Cash.State>(anything)))
|
||||
|
||||
// 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> {
|
||||
@ -254,10 +163,35 @@ class ContractUpgradeFlowTest {
|
||||
override fun verify(tx: LedgerTransaction) {}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class FinalityInvoker(private val transaction: SignedTransaction,
|
||||
private val extraRecipients: Set<Party>) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction = subFlow(FinalityFlow(transaction, extraRecipients))
|
||||
}
|
||||
//region Operations
|
||||
private fun StartedNode<*>.initiateDummyContractUpgrade(tx: SignedTransaction) =
|
||||
initiateContractUpgrade(tx, DummyContractV2::class)
|
||||
|
||||
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 org.assertj.core.api.Assertions.catchThrowable
|
||||
import org.hamcrest.Matchers.lessThanOrEqualTo
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertThat
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
@ -31,30 +30,9 @@ class FastThreadLocalTest {
|
||||
}
|
||||
|
||||
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
|
||||
fun `ThreadLocal with plain old Thread is fiber-local`() {
|
||||
init(3, ::Thread)
|
||||
fun `ThreadLocal with plain old Thread is fiber-local`() = scheduled(3, ::Thread) {
|
||||
val threadLocal = object : ThreadLocal<ExpensiveObj>() {
|
||||
override fun initialValue() = ExpensiveObj()
|
||||
}
|
||||
@ -63,8 +41,7 @@ class FastThreadLocalTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ThreadLocal with FastThreadLocalThread is fiber-local`() {
|
||||
init(3, ::FastThreadLocalThread)
|
||||
fun `ThreadLocal with FastThreadLocalThread is fiber-local`() = scheduled(3, ::FastThreadLocalThread) {
|
||||
val threadLocal = object : ThreadLocal<ExpensiveObj>() {
|
||||
override fun initialValue() = ExpensiveObj()
|
||||
}
|
||||
@ -73,8 +50,7 @@ class FastThreadLocalTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FastThreadLocal with plain old Thread is fiber-local`() {
|
||||
init(3, ::Thread)
|
||||
fun `FastThreadLocal with plain old Thread is fiber-local`() = scheduled(3, ::Thread) {
|
||||
val threadLocal = object : FastThreadLocal<ExpensiveObj>() {
|
||||
override fun initialValue() = ExpensiveObj()
|
||||
}
|
||||
@ -83,8 +59,8 @@ class FastThreadLocalTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FastThreadLocal with FastThreadLocalThread is not fiber-local`() {
|
||||
init(3, ::FastThreadLocalThread)
|
||||
fun `FastThreadLocal with FastThreadLocalThread is not fiber-local`() =
|
||||
scheduled(3, ::FastThreadLocalThread) {
|
||||
val threadLocal = object : FastThreadLocal<ExpensiveObj>() {
|
||||
override fun initialValue() = ExpensiveObj()
|
||||
}
|
||||
@ -93,7 +69,7 @@ class FastThreadLocalTest {
|
||||
}
|
||||
|
||||
/** @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 startedFibers = fibers.map { it.start() }
|
||||
return startedFibers.map { it.get() }.count { it }
|
||||
@ -127,8 +103,7 @@ class FastThreadLocalTest {
|
||||
}::get)
|
||||
}
|
||||
|
||||
private fun contentIsNotSerialized(threadLocalGet: () -> UnserializableObj) {
|
||||
init(1, ::FastThreadLocalThread)
|
||||
private fun contentIsNotSerialized(threadLocalGet: () -> UnserializableObj) = scheduled(1, ::FastThreadLocalThread) {
|
||||
// Use false like AbstractKryoSerializationScheme, the default of true doesn't work at all:
|
||||
val serializer = Fiber.getFiberSerializer(false)
|
||||
val returnValue = UUID.randomUUID()
|
||||
@ -162,4 +137,21 @@ class FastThreadLocalTest {
|
||||
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
|
||||
|
||||
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.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.POUNDS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.issuedBy
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import org.junit.AfterClass
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class FinalityFlowTests {
|
||||
class FinalityFlowTests : WithFinality {
|
||||
companion object {
|
||||
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
|
||||
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
|
||||
override val mockNet = classMockNet
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
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
|
||||
}
|
||||
private val aliceNode = makeNode(ALICE_NAME)
|
||||
private val bobNode = makeNode(BOB_NAME)
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
private val alice = aliceNode.info.singleIdentity()
|
||||
private val bob = bobNode.info.singleIdentity()
|
||||
private val notary = mockNet.defaultNotaryIdentity
|
||||
|
||||
@Test
|
||||
fun `finalise a simple transaction`() {
|
||||
val amount = 1000.POUNDS.issuedBy(alice.ref(0))
|
||||
val builder = TransactionBuilder(notary)
|
||||
Cash().generateIssue(builder, amount, bob, notary)
|
||||
val stx = aliceNode.services.signInitialTransaction(builder)
|
||||
val flow = aliceNode.startFlow(FinalityFlow(stx))
|
||||
mockNet.runNetwork()
|
||||
val notarisedTx = flow.getOrThrow()
|
||||
notarisedTx.verifyRequiredSignatures()
|
||||
val transactionSeenByB = bobNode.transaction {
|
||||
bobNode.services.validatedTransactions.getTransaction(notarisedTx.id)
|
||||
}
|
||||
assertEquals(notarisedTx, transactionSeenByB)
|
||||
val stx = aliceNode.signCashTransactionWith(bob)
|
||||
|
||||
assert.that(
|
||||
aliceNode.finalise(stx),
|
||||
willReturn(
|
||||
requiredSignatures(1)
|
||||
and visibleTo(bobNode)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reject a transaction with unknown parties`() {
|
||||
val amount = 1000.POUNDS.issuedBy(alice.ref(0))
|
||||
val fakeIdentity = CHARLIE // Charlie isn't part of this network, so node A won't recognise them
|
||||
val builder = TransactionBuilder(notary)
|
||||
Cash().generateIssue(builder, amount, fakeIdentity, notary)
|
||||
val stx = aliceNode.services.signInitialTransaction(builder)
|
||||
val flow = aliceNode.startFlow(FinalityFlow(stx))
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
flow.getOrThrow()
|
||||
}
|
||||
// Charlie isn't part of this network, so node A won't recognise them
|
||||
val stx = aliceNode.signCashTransactionWith(CHARLIE)
|
||||
|
||||
assert.that(
|
||||
aliceNode.finalise(stx),
|
||||
willThrow<IllegalArgumentException>())
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.startFlow
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.AfterClass
|
||||
import org.junit.Test
|
||||
|
||||
class ReceiveMultipleFlowTests {
|
||||
private val mockNet = InternalMockNetwork()
|
||||
private val nodes = (0..2).map { mockNet.createPartyNode() }
|
||||
@After
|
||||
fun stopNodes() {
|
||||
mockNet.stopNodes()
|
||||
|
||||
class ReceiveMultipleFlowTests : WithMockNet {
|
||||
companion object {
|
||||
private val classMockNet = InternalMockNetwork()
|
||||
|
||||
@JvmStatic
|
||||
@AfterClass
|
||||
fun stopNodes() = classMockNet.stopNodes()
|
||||
}
|
||||
|
||||
override val mockNet = classMockNet
|
||||
|
||||
private val nodes = (0..2).map { mockNet.createPartyNode() }
|
||||
|
||||
@Test
|
||||
fun showcase_flows_as_closures() {
|
||||
val answer = 10.0
|
||||
@ -49,10 +58,9 @@ class ReceiveMultipleFlowTests {
|
||||
} as FlowLogic<Unit>
|
||||
}
|
||||
|
||||
val flow = nodes[0].services.startFlow(initiatingFlow)
|
||||
mockNet.runNetwork()
|
||||
val receivedAnswer = flow.resultFuture.getOrThrow()
|
||||
assertThat(receivedAnswer).isEqualTo(answer)
|
||||
assert.that(
|
||||
nodes[0].startFlowAndRunNetwork(initiatingFlow),
|
||||
willReturn(answer as Any))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -61,10 +69,10 @@ class ReceiveMultipleFlowTests {
|
||||
nodes[1].registerAnswer(AlgorithmDefinition::class, doubleValue)
|
||||
val stringValue = "Thriller"
|
||||
nodes[2].registerAnswer(AlgorithmDefinition::class, stringValue)
|
||||
val flow = nodes[0].services.startFlow(ParallelAlgorithmMap(nodes[1].info.singleIdentity(), nodes[2].info.singleIdentity()))
|
||||
mockNet.runNetwork()
|
||||
val result = flow.resultFuture.getOrThrow()
|
||||
assertThat(result).isEqualTo(doubleValue * stringValue.length)
|
||||
|
||||
assert.that(
|
||||
nodes[0].startFlowAndRunNetwork(ParallelAlgorithmMap(nodes[1].info.singleIdentity(), nodes[2].info.singleIdentity())),
|
||||
willReturn(doubleValue * stringValue.length))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -73,12 +81,10 @@ class ReceiveMultipleFlowTests {
|
||||
nodes[1].registerAnswer(ParallelAlgorithmList::class, value1)
|
||||
val value2 = 6.0
|
||||
nodes[2].registerAnswer(ParallelAlgorithmList::class, value2)
|
||||
val flow = nodes[0].services.startFlow(ParallelAlgorithmList(nodes[1].info.singleIdentity(), nodes[2].info.singleIdentity()))
|
||||
mockNet.runNetwork()
|
||||
val data = flow.resultFuture.getOrThrow()
|
||||
assertThat(data[0]).isEqualTo(value1)
|
||||
assertThat(data[1]).isEqualTo(value2)
|
||||
assertThat(data.fold(1.0) { a, b -> a * b }).isEqualTo(value1 * value2)
|
||||
|
||||
assert.that(
|
||||
nodes[0].startFlowAndRunNetwork(ParallelAlgorithmList(nodes[1].info.singleIdentity(), nodes[2].info.singleIdentity())),
|
||||
willReturn(listOf(value1, value2)))
|
||||
}
|
||||
|
||||
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.utilities.seconds
|
||||
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.rigorousMock
|
||||
import net.corda.testing.node.MockServices
|
||||
|
@ -1,8 +1,8 @@
|
||||
package net.corda.core.utilities
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
|
||||
class NetworkHostAndPortTest {
|
||||
/**
|
||||
|
@ -1,11 +1,11 @@
|
||||
package net.corda.core.utilities
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFails
|
||||
import org.assertj.core.api.Assertions.*
|
||||
|
||||
class ProgressTrackerTest {
|
||||
object SimpleSteps {
|
||||
|
Loading…
x
Reference in New Issue
Block a user