From 17a6f61eba907c63b599f780a58ea312ae4eb2bc Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 25 Jan 2018 16:29:26 +0100 Subject: [PATCH] Simplify CashTests and add some comments/convenience APIs to MockServices (#2241) --- .../kotlin/net/corda/core/crypto/Crypto.kt | 1 + .../tutorial/testdsl/CommercialPaperTest.java | 2 +- .../finance/contracts/CommercialPaperTests.kt | 208 +++++------- .../finance/contracts/asset/CashTests.kt | 313 ++++++++---------- .../net/corda/testing/node/MockServices.kt | 65 +++- 5 files changed, 291 insertions(+), 298 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index ef365c7a4f..98c150b5b8 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -497,6 +497,7 @@ object Crypto { * It returns true if it succeeds, but it always throws an exception if verification fails. * Strategy on identifying the actual signing scheme is based on the [PublicKey] type, but if the schemeCodeName is known, * then better use doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray). + * * @param publicKey the signer's [PublicKey]. * @param signatureData the signatureData on a message. * @param clearData the clear data/message that was signed (usually the Merkle root). diff --git a/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java b/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java index 91516ee751..0e206bf77c 100644 --- a/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java +++ b/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java @@ -32,7 +32,7 @@ public class CommercialPaperTest { private static final TestIdentity BOB = new TestIdentity(BOB_NAME, 80L); private static final TestIdentity MEGA_CORP = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB")); private final byte[] defaultRef = {123}; - private final MockServices ledgerServices = new MockServices(emptyList(), makeTestIdentityService(MEGA_CORP.getIdentity())); + private final MockServices ledgerServices = new MockServices(MEGA_CORP); // DOCSTART 1 private ICommercialPaperState getPaper() { diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt index 12aa01f5f1..43ebe167fd 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -1,27 +1,23 @@ package net.corda.finance.contracts -import com.nhaarman.mockito_kotlin.doReturn -import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.* -import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.node.services.Vault -import net.corda.core.node.services.VaultService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.days import net.corda.core.utilities.seconds import net.corda.finance.DOLLARS import net.corda.finance.`issued by` -import net.corda.finance.contracts.asset.* -import net.corda.node.services.api.IdentityServiceInternal +import net.corda.finance.contracts.asset.CASH +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.contracts.asset.STATE import net.corda.testing.core.* import net.corda.testing.dsl.EnforceVerifyOrFail import net.corda.testing.dsl.TransactionDSL import net.corda.testing.dsl.TransactionDSLInterpreter -import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.vault.VaultFiller import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices @@ -48,15 +44,12 @@ interface ICommercialPaperTestTemplate { } private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) -private val MEGA_CORP get() = megaCorp.party -private val MEGA_CORP_IDENTITY get() = megaCorp.identity -private val MEGA_CORP_PUBKEY get() = megaCorp.publicKey class JavaCommercialPaperTest : ICommercialPaperTestTemplate { override fun getPaper(): ICommercialPaperState = JavaCommercialPaper.State( - MEGA_CORP.ref(123), - MEGA_CORP, - 1000.DOLLARS `issued by` MEGA_CORP.ref(123), + megaCorp.ref(123), + megaCorp.party, + 1000.DOLLARS `issued by` megaCorp.ref(123), TEST_TX_TIME + 7.days ) @@ -68,9 +61,9 @@ class JavaCommercialPaperTest : ICommercialPaperTestTemplate { class KotlinCommercialPaperTest : ICommercialPaperTestTemplate { override fun getPaper(): ICommercialPaperState = CommercialPaper.State( - issuance = MEGA_CORP.ref(123), - owner = MEGA_CORP, - faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123), + issuance = megaCorp.ref(123), + owner = megaCorp.party, + faceValue = 1000.DOLLARS `issued by` megaCorp.ref(123), maturityDate = TEST_TX_TIME + 7.days ) @@ -82,9 +75,9 @@ class KotlinCommercialPaperTest : ICommercialPaperTestTemplate { class KotlinCommercialPaperLegacyTest : ICommercialPaperTestTemplate { override fun getPaper(): ICommercialPaperState = CommercialPaper.State( - issuance = MEGA_CORP.ref(123), - owner = MEGA_CORP, - faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123), + issuance = megaCorp.ref(123), + owner = megaCorp.party, + faceValue = 1000.DOLLARS `issued by` megaCorp.ref(123), maturityDate = TEST_TX_TIME + 7.days ) @@ -102,49 +95,36 @@ class CommercialPaperTestsGeneric { fun data() = listOf(JavaCommercialPaperTest(), KotlinCommercialPaperTest(), KotlinCommercialPaperLegacyTest()) private val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10) - private val DUMMY_CASH_ISSUER_IDENTITY get() = dummyCashIssuer.identity - private val DUMMY_CASH_ISSUER = dummyCashIssuer.ref(1) - private val alice = TestIdentity(ALICE_NAME, 70) - private val BIG_CORP_KEY = generateKeyPair() private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) + private val alice = TestIdentity(ALICE_NAME, 70) private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) - private val ALICE get() = alice.party - private val ALICE_KEY get() = alice.keyPair - private val ALICE_PUBKEY get() = alice.publicKey - private val DUMMY_NOTARY get() = dummyNotary.party - private val DUMMY_NOTARY_IDENTITY get() = dummyNotary.identity - private val MINI_CORP get() = miniCorp.party - private val MINI_CORP_IDENTITY get() = miniCorp.identity - private val MINI_CORP_PUBKEY get() = miniCorp.publicKey } @Parameterized.Parameter lateinit var thisTest: ICommercialPaperTestTemplate + @Rule @JvmField val testSerialization = SerializationEnvironmentRule() - val issuer = MEGA_CORP.ref(123) - private val ledgerServices = MockServices(emptyList(), rigorousMock().also { - doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) - doReturn(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY) - doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY) - }, MEGA_CORP.name) + + private val megaCorpRef = megaCorp.ref(123) + private val ledgerServices = MockServices(megaCorp, miniCorp) @Test fun `trade lifecycle test`() { - val someProfits = 1200.DOLLARS `issued by` issuer - ledgerServices.ledger(DUMMY_NOTARY) { + val someProfits = 1200.DOLLARS `issued by` megaCorpRef + ledgerServices.ledger(dummyNotary.party) { unverifiedTransaction { attachment(Cash.PROGRAM_ID) - output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE) - output(Cash.PROGRAM_ID, "some profits", someProfits.STATE ownedBy MEGA_CORP) + output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy megaCorpRef ownedBy alice.party) + output(Cash.PROGRAM_ID, "some profits", someProfits.STATE ownedBy megaCorp.party) } // Some CP is issued onto the ledger by MegaCorp. transaction("Issuance") { attachments(CP_PROGRAM_ID, JavaCommercialPaper.JCP_PROGRAM_ID) output(thisTest.getContract(), "paper", thisTest.getPaper()) - command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY)) + command(megaCorp.publicKey, thisTest.getIssueCommand(dummyNotary.party)) timeWindow(TEST_TX_TIME) this.verifies() } @@ -155,10 +135,10 @@ class CommercialPaperTestsGeneric { attachments(Cash.PROGRAM_ID, JavaCommercialPaper.JCP_PROGRAM_ID) input("paper") input("alice's $900") - output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP) - output(thisTest.getContract(), "alice's paper", "paper".output().withOwner(ALICE)) - command(ALICE_PUBKEY, Cash.Commands.Move()) - command(MEGA_CORP_PUBKEY, thisTest.getMoveCommand()) + output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy megaCorpRef ownedBy megaCorp.party) + output(thisTest.getContract(), "alice's paper", "paper".output().withOwner(alice.party)) + command(alice.publicKey, Cash.Commands.Move()) + command(megaCorp.publicKey, thisTest.getMoveCommand()) this.verifies() } @@ -170,17 +150,17 @@ class CommercialPaperTestsGeneric { input("some profits") fun TransactionDSL.outputs(aliceGetsBack: Amount>) { - output(Cash.PROGRAM_ID, "Alice's profit", aliceGetsBack.STATE ownedBy ALICE) - output(Cash.PROGRAM_ID, "Change", (someProfits - aliceGetsBack).STATE ownedBy MEGA_CORP) + output(Cash.PROGRAM_ID, "Alice's profit", aliceGetsBack.STATE ownedBy alice.party) + output(Cash.PROGRAM_ID, "Change", (someProfits - aliceGetsBack).STATE ownedBy megaCorp.party) } - command(MEGA_CORP_PUBKEY, Cash.Commands.Move()) - command(ALICE_PUBKEY, thisTest.getRedeemCommand(DUMMY_NOTARY)) + command(megaCorp.publicKey, Cash.Commands.Move()) + command(alice.publicKey, thisTest.getRedeemCommand(dummyNotary.party)) tweak { - outputs(700.DOLLARS `issued by` issuer) + outputs(700.DOLLARS `issued by` megaCorpRef) timeWindow(TEST_TX_TIME + 8.days) this `fails with` "received amount equals the face value" } - outputs(1000.DOLLARS `issued by` issuer) + outputs(1000.DOLLARS `issued by` megaCorpRef) tweak { @@ -200,7 +180,7 @@ class CommercialPaperTestsGeneric { } private fun transaction(script: TransactionDSL.() -> EnforceVerifyOrFail) = run { - ledgerServices.transaction(DUMMY_NOTARY, script) + ledgerServices.transaction(dummyNotary.party, script) } @Test @@ -209,7 +189,7 @@ class CommercialPaperTestsGeneric { attachment(CP_PROGRAM_ID) attachment(JavaCommercialPaper.JCP_PROGRAM_ID) output(thisTest.getContract(), thisTest.getPaper()) - command(MINI_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY)) + command(miniCorp.publicKey, thisTest.getIssueCommand(dummyNotary.party)) timeWindow(TEST_TX_TIME) this `fails with` "output states are issued by a command signer" } @@ -220,8 +200,8 @@ class CommercialPaperTestsGeneric { transaction { attachment(CP_PROGRAM_ID) attachment(JavaCommercialPaper.JCP_PROGRAM_ID) - output(thisTest.getContract(), thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer)) - command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY)) + output(thisTest.getContract(), thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` megaCorpRef)) + command(megaCorp.publicKey, thisTest.getIssueCommand(dummyNotary.party)) timeWindow(TEST_TX_TIME) this `fails with` "output values sum to more than the inputs" } @@ -233,7 +213,7 @@ class CommercialPaperTestsGeneric { attachment(CP_PROGRAM_ID) attachment(JavaCommercialPaper.JCP_PROGRAM_ID) output(thisTest.getContract(), thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days)) - command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY)) + command(megaCorp.publicKey, thisTest.getIssueCommand(dummyNotary.party)) timeWindow(TEST_TX_TIME) this `fails with` "maturity date is not in the past" } @@ -246,95 +226,83 @@ class CommercialPaperTestsGeneric { attachment(JavaCommercialPaper.JCP_PROGRAM_ID) input(thisTest.getContract(), thisTest.getPaper()) output(thisTest.getContract(), thisTest.getPaper()) - command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY)) + command(megaCorp.publicKey, thisTest.getIssueCommand(dummyNotary.party)) timeWindow(TEST_TX_TIME) this `fails with` "output values sum to more than the inputs" } } - /** - * Unit test requires two separate Database instances to represent each of the two - * transaction participants (enforces uniqueness of vault content in lieu of partipant identity) - */ - - private lateinit var bigCorpServices: MockServices - private lateinit var bigCorpVault: Vault - private lateinit var bigCorpVaultService: VaultService - - private lateinit var aliceServices: MockServices - private lateinit var aliceVaultService: VaultService - private lateinit var alicesVault: Vault - private val notaryServices = MockServices(emptyList(), rigorousMock(), MEGA_CORP.name, dummyNotary.keyPair) - private val issuerServices = MockServices(listOf("net.corda.finance.contracts"), rigorousMock(), MEGA_CORP.name, dummyCashIssuer.keyPair) - private lateinit var moveTX: SignedTransaction @Test fun `issue move and then redeem`() { - val aliceDatabaseAndServices = makeTestDatabaseAndMockServices( - listOf("net.corda.finance.contracts"), - makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY), - TestIdentity(MEGA_CORP.name, ALICE_KEY)) - val databaseAlice = aliceDatabaseAndServices.first - aliceServices = aliceDatabaseAndServices.second - aliceVaultService = aliceServices.vaultService + // Set up a test environment with 4 parties: + // 1. The notary + // 2. A dummy cash issuer e.g. central bank + // 3. Alice + // 4. MegaCorp + // + // MegaCorp will issue some commercial paper and Alice will buy it, using cash issued to her in the name + // of the dummy cash issuer. - databaseAlice.transaction { - alicesVault = VaultFiller(aliceServices, dummyNotary, rngFactory = ::Random).fillWithSomeTestCash(9000.DOLLARS, issuerServices, 1, DUMMY_CASH_ISSUER) - aliceVaultService = aliceServices.vaultService + val allIdentities = arrayOf(megaCorp.identity, miniCorp.identity, dummyCashIssuer.identity, dummyNotary.identity) + val notaryServices = MockServices(dummyNotary) + val issuerServices = MockServices(dummyCashIssuer, dummyNotary) + val (aliceDatabase, aliceServices) = makeTestDatabaseAndMockServices( + listOf("net.corda.finance.contracts"), + makeTestIdentityService(*allIdentities), + alice + ) + val aliceCash: Vault = aliceDatabase.transaction { + VaultFiller(aliceServices, dummyNotary).fillWithSomeTestCash(9000.DOLLARS, issuerServices, 1, dummyCashIssuer.ref(1)) } - val bigCorpDatabaseAndServices = makeTestDatabaseAndMockServices( - listOf("net.corda.finance.contracts"), - makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY), - TestIdentity(MEGA_CORP.name, BIG_CORP_KEY)) - val databaseBigCorp = bigCorpDatabaseAndServices.first - bigCorpServices = bigCorpDatabaseAndServices.second - bigCorpVaultService = bigCorpServices.vaultService - databaseBigCorp.transaction { - bigCorpVault = VaultFiller(bigCorpServices, dummyNotary, rngFactory = ::Random).fillWithSomeTestCash(13000.DOLLARS, issuerServices, 1, DUMMY_CASH_ISSUER) - bigCorpVaultService = bigCorpServices.vaultService + val (megaCorpDatabase, megaCorpServices) = makeTestDatabaseAndMockServices( + listOf("net.corda.finance.contracts"), + makeTestIdentityService(*allIdentities), + megaCorp + ) + val bigCorpCash: Vault = megaCorpDatabase.transaction { + VaultFiller(megaCorpServices, dummyNotary).fillWithSomeTestCash(13000.DOLLARS, issuerServices, 1, dummyCashIssuer.ref(1)) } // Propagate the cash transactions to each side. - aliceServices.recordTransactions(bigCorpVault.states.map { bigCorpServices.validatedTransactions.getTransaction(it.ref.txhash)!! }) - bigCorpServices.recordTransactions(alicesVault.states.map { aliceServices.validatedTransactions.getTransaction(it.ref.txhash)!! }) + aliceServices.recordTransactions(bigCorpCash.states.map { megaCorpServices.validatedTransactions.getTransaction(it.ref.txhash)!! }) + megaCorpServices.recordTransactions(aliceCash.states.map { aliceServices.validatedTransactions.getTransaction(it.ref.txhash)!! }) - // BigCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. - val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER - val issuance = bigCorpServices.myInfo.chooseIdentity().ref(1) - val issueBuilder = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY) + // MegaCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. + val faceValue = 10000.DOLLARS `issued by` dummyCashIssuer.ref(1) + val issuance = megaCorpServices.myInfo.chooseIdentity().ref(1) + val issueBuilder = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, dummyNotary.party) issueBuilder.setTimeWindow(TEST_TX_TIME, 30.seconds) - val issuePtx = bigCorpServices.signInitialTransaction(issueBuilder) + val issuePtx = megaCorpServices.signInitialTransaction(issueBuilder) val issueTx = notaryServices.addSignature(issuePtx) - databaseAlice.transaction { + val moveTX = aliceDatabase.transaction { // Alice pays $9000 to BigCorp to own some of their debt. - moveTX = run { - val builder = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(BIG_CORP_KEY.public)) - CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(ALICE_KEY.public)) - val ptx = aliceServices.signInitialTransaction(builder) - val ptx2 = bigCorpServices.addSignature(ptx) - val stx = notaryServices.addSignature(ptx2) - stx - } + val builder = TransactionBuilder(dummyNotary.party) + Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, alice.identity, AnonymousParty(megaCorp.publicKey)) + CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(alice.keyPair.public)) + val ptx = aliceServices.signInitialTransaction(builder) + val ptx2 = megaCorpServices.addSignature(ptx) + val stx = notaryServices.addSignature(ptx2) + stx } - databaseBigCorp.transaction { + megaCorpDatabase.transaction { // Verify the txns are valid and insert into both sides. - listOf(issueTx, moveTX).forEach { - it.toLedgerTransaction(aliceServices).verify() - aliceServices.recordTransactions(it) - bigCorpServices.recordTransactions(it) + for (tx in listOf(issueTx, moveTX)) { + tx.toLedgerTransaction(aliceServices).verify() + aliceServices.recordTransactions(tx) + megaCorpServices.recordTransactions(tx) } } - databaseBigCorp.transaction { + megaCorpDatabase.transaction { fun makeRedeemTX(time: Instant): Pair { - val builder = TransactionBuilder(DUMMY_NOTARY) + val builder = TransactionBuilder(dummyNotary.party) builder.setTimeWindow(time, 30.seconds) - CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), bigCorpServices, bigCorpServices.myInfo.chooseIdentityAndCert()) + CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), megaCorpServices, megaCorpServices.myInfo.chooseIdentityAndCert()) val ptx = aliceServices.signInitialTransaction(builder) - val ptx2 = bigCorpServices.addSignature(ptx) + val ptx2 = megaCorpServices.addSignature(ptx) val stx = notaryServices.addSignature(ptx2) return Pair(stx, builder.lockId) } @@ -347,7 +315,7 @@ class CommercialPaperTestsGeneric { } // manually release locks held by this failing transaction aliceServices.vaultService.softLockRelease(tooEarlyRedemptionLockId) - assertTrue(e.cause!!.message!!.contains("paper must have matured")) + assertTrue("paper must have matured" in e.cause!!.message!!) val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days).first validRedemption.toLedgerTransaction(aliceServices).verify() diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index aed8d302f8..a2fa76e92c 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -1,9 +1,11 @@ package net.corda.finance.contracts.asset -import com.nhaarman.mockito_kotlin.* import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash -import net.corda.core.identity.* +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy @@ -15,16 +17,14 @@ import net.corda.finance.utils.sumCash import net.corda.finance.utils.sumCashBy import net.corda.finance.utils.sumCashOrNull import net.corda.finance.utils.sumCashOrZero -import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.vault.NodeVaultService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.contracts.DummyState import net.corda.testing.core.* -import net.corda.testing.internal.LogHelper import net.corda.testing.dsl.EnforceVerifyOrFail import net.corda.testing.dsl.TransactionDSL import net.corda.testing.dsl.TransactionDSLInterpreter -import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.LogHelper import net.corda.testing.internal.vault.VaultFiller import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices @@ -41,41 +41,25 @@ import kotlin.test.* class CashTests { private companion object { val alice = TestIdentity(ALICE_NAME, 70) - val BOB_PUBKEY = TestIdentity(BOB_NAME, 80).publicKey + val bob = TestIdentity(BOB_NAME, 80) val charlie = TestIdentity(CHARLIE_NAME, 90) - val DUMMY_CASH_ISSUER_IDENTITY = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10).identity + val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10) val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) - val ALICE get() = alice.party - val ALICE_PUBKEY get() = alice.publicKey - val CHARLIE get() = charlie.party - val CHARLIE_IDENTITY get() = charlie.identity - val DUMMY_NOTARY get() = dummyNotary.party - val DUMMY_NOTARY_IDENTITY get() = dummyNotary.identity - val DUMMY_NOTARY_KEY get() = dummyNotary.keyPair - val MEGA_CORP get() = megaCorp.party - val MEGA_CORP_IDENTITY get() = megaCorp.identity - val MEGA_CORP_KEY get() = megaCorp.keyPair - val MEGA_CORP_PUBKEY get() = megaCorp.publicKey - val MINI_CORP get() = miniCorp.party - val MINI_CORP_IDENTITY get() = miniCorp.identity - val MINI_CORP_KEY get() = miniCorp.keyPair - val MINI_CORP_PUBKEY get() = miniCorp.publicKey } @Rule @JvmField val testSerialization = SerializationEnvironmentRule() - private val defaultRef = OpaqueBytes.of(1) - private val defaultIssuer = MEGA_CORP.ref(defaultRef) + private val defaultIssuer = megaCorp.ref(1) private val inState = Cash.State( amount = 1000.DOLLARS `issued by` defaultIssuer, - owner = AnonymousParty(ALICE_PUBKEY) + owner = AnonymousParty(alice.publicKey) ) // Input state held by the issuer private val issuerInState = inState.copy(owner = defaultIssuer.party) - private val outState = issuerInState.copy(owner = AnonymousParty(BOB_PUBKEY)) + private val outState = issuerInState.copy(owner = AnonymousParty(bob.publicKey)) private fun Cash.State.editDepositRef(ref: Byte) = copy( amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref)))) @@ -90,48 +74,42 @@ class CashTests { private lateinit var ourIdentity: AbstractParty private lateinit var miniCorpAnonymised: AnonymousParty - private val CHARLIE_ANONYMISED = CHARLIE_IDENTITY.party.anonymise() - - private lateinit var WALLET: List> + private lateinit var cashStates: List> + // TODO: Optimise this so that we don't throw away and rebuild state that can be shared across tests. @Before fun setUp() { LogHelper.setLevel(NodeVaultService::class) - megaCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY) - miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock().also { - doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == MINI_CORP.name }) - }, MINI_CORP.name, MINI_CORP_KEY) - val notaryServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) + megaCorpServices = MockServices(megaCorp) + miniCorpServices = MockServices(miniCorp) val databaseAndServices = makeTestDatabaseAndMockServices( listOf("net.corda.finance.contracts.asset"), - makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY), - TestIdentity(CordaX500Name("Me", "London", "GB"))) + makeTestIdentityService(megaCorp.identity, miniCorp.identity, dummyCashIssuer.identity, dummyNotary.identity), + TestIdentity(CordaX500Name("Me", "London", "GB")) + ) database = databaseAndServices.first ourServices = databaseAndServices.second // Set up and register identities ourIdentity = ourServices.myInfo.singleIdentity() miniCorpAnonymised = miniCorpServices.myInfo.singleIdentityAndCert().party.anonymise() - (miniCorpServices.myInfo.legalIdentitiesAndCerts + megaCorpServices.myInfo.legalIdentitiesAndCerts + notaryServices.myInfo.legalIdentitiesAndCerts).forEach { identity -> - ourServices.identityService.verifyAndRegisterIdentity(identity) // TODO: Configure a mock identity service instead. - } // Create some cash. Any attempt to spend >$500 will require multiple issuers to be involved. database.transaction { - val vaultFiller = VaultFiller(ourServices, dummyNotary, rngFactory = ::Random) - vaultFiller.fillWithSomeTestCash(100.DOLLARS, megaCorpServices, 1, MEGA_CORP.ref(1), ourIdentity) - vaultFiller.fillWithSomeTestCash(400.DOLLARS, megaCorpServices, 1, MEGA_CORP.ref(1), ourIdentity) - vaultFiller.fillWithSomeTestCash(80.DOLLARS, miniCorpServices, 1, MINI_CORP.ref(1), ourIdentity) - vaultFiller.fillWithSomeTestCash(80.SWISS_FRANCS, miniCorpServices, 1, MINI_CORP.ref(1), ourIdentity) + val vaultFiller = VaultFiller(ourServices, dummyNotary) + vaultFiller.fillWithSomeTestCash(100.DOLLARS, megaCorpServices, 1, megaCorp.ref(1), ourIdentity) + vaultFiller.fillWithSomeTestCash(400.DOLLARS, megaCorpServices, 1, megaCorp.ref(1), ourIdentity) + vaultFiller.fillWithSomeTestCash(80.DOLLARS, miniCorpServices, 1, miniCorp.ref(1), ourIdentity) + vaultFiller.fillWithSomeTestCash(80.SWISS_FRANCS, miniCorpServices, 1, miniCorp.ref(1), ourIdentity) } database.transaction { vaultStatesUnconsumed = ourServices.vaultService.queryBy().states } - WALLET = listOf( - makeCash(100.DOLLARS, MEGA_CORP), - makeCash(400.DOLLARS, MEGA_CORP), - makeCash(80.DOLLARS, MINI_CORP), - makeCash(80.SWISS_FRANCS, MINI_CORP, 2) + cashStates = listOf( + makeCash(100.DOLLARS, megaCorp.party), + makeCash(400.DOLLARS, megaCorp.party), + makeCash(80.DOLLARS, miniCorp.party), + makeCash(80.SWISS_FRANCS, miniCorp.party, 2) ) } @@ -140,13 +118,8 @@ class CashTests { database.close() } - private fun transaction(script: TransactionDSL.() -> EnforceVerifyOrFail) = run { - MockServices(emptyList(), rigorousMock().also { - doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) - doReturn(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY) - doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY) - doReturn(null).whenever(it).partyFromKey(BOB_PUBKEY) - }, MEGA_CORP.name).transaction(DUMMY_NOTARY, script) + private fun transaction(script: TransactionDSL.() -> EnforceVerifyOrFail) { + MockServices(megaCorp).transaction(dummyNotary.party, script) } @Test @@ -156,30 +129,30 @@ class CashTests { input(Cash.PROGRAM_ID, inState) tweak { output(Cash.PROGRAM_ID, outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer)) - command(ALICE_PUBKEY, Cash.Commands.Move()) + command(alice.publicKey, Cash.Commands.Move()) this `fails with` "the amounts balance" } tweak { output(Cash.PROGRAM_ID, outState) - command(ALICE_PUBKEY, DummyCommandData) + command(alice.publicKey, DummyCommandData) // Invalid command this `fails with` "required net.corda.finance.contracts.asset.Cash.Commands.Move command" } tweak { output(Cash.PROGRAM_ID, outState) - command(BOB_PUBKEY, Cash.Commands.Move()) + command(bob.publicKey, Cash.Commands.Move()) this `fails with` "the owning keys are a subset of the signing keys" } tweak { output(Cash.PROGRAM_ID, outState) - output(Cash.PROGRAM_ID, outState issuedBy MINI_CORP) - command(ALICE_PUBKEY, Cash.Commands.Move()) + output(Cash.PROGRAM_ID, outState issuedBy miniCorp.party) + command(alice.publicKey, Cash.Commands.Move()) this `fails with` "at least one cash input" } // Simple reallocation works. tweak { output(Cash.PROGRAM_ID, outState) - command(ALICE_PUBKEY, Cash.Commands.Move()) + command(alice.publicKey, Cash.Commands.Move()) this.verifies() } } @@ -192,7 +165,7 @@ class CashTests { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, DummyState()) output(Cash.PROGRAM_ID, outState) - command(MINI_CORP_PUBKEY, Cash.Commands.Move()) + command(miniCorp.publicKey, Cash.Commands.Move()) this `fails with` "there is at least one cash input for this group" } } @@ -204,16 +177,16 @@ class CashTests { transaction { attachment(Cash.PROGRAM_ID) output(Cash.PROGRAM_ID, outState) - command(ALICE_PUBKEY, Cash.Commands.Issue()) + command(alice.publicKey, Cash.Commands.Issue()) this `fails with` "output states are issued by a command signer" } transaction { attachment(Cash.PROGRAM_ID) output(Cash.PROGRAM_ID, Cash.State( - amount = 1000.DOLLARS `issued by` MINI_CORP.ref(12, 34), - owner = AnonymousParty(ALICE_PUBKEY))) - command(MINI_CORP_PUBKEY, Cash.Commands.Issue()) + amount = 1000.DOLLARS `issued by` miniCorp.ref(12, 34), + owner = AnonymousParty(alice.publicKey))) + command(miniCorp.publicKey, Cash.Commands.Issue()) this.verifies() } } @@ -222,23 +195,23 @@ class CashTests { fun generateIssueRaw() { // Test generation works. val tx: WireTransaction = TransactionBuilder(notary = null).apply { - Cash().generateIssue(this, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = AnonymousParty(ALICE_PUBKEY), notary = DUMMY_NOTARY) + Cash().generateIssue(this, 100.DOLLARS `issued by` miniCorp.ref(12, 34), owner = AnonymousParty(alice.publicKey), notary = dummyNotary.party) }.toWireTransaction(miniCorpServices) assertTrue(tx.inputs.isEmpty()) val s = tx.outputsOfType().single() - assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount) - assertEquals(MINI_CORP as AbstractParty, s.amount.token.issuer.party) - assertEquals(AnonymousParty(ALICE_PUBKEY), s.owner) + assertEquals(100.DOLLARS `issued by` miniCorp.ref(12, 34), s.amount) + assertEquals(miniCorp.party as AbstractParty, s.amount.token.issuer.party) + assertEquals(AnonymousParty(alice.publicKey), s.owner) assertTrue(tx.commands[0].value is Cash.Commands.Issue) - assertEquals(MINI_CORP_PUBKEY, tx.commands[0].signers[0]) + assertEquals(miniCorp.publicKey, tx.commands[0].signers[0]) } @Test fun generateIssueFromAmount() { // Test issuance from an issued amount - val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34) + val amount = 100.DOLLARS `issued by` miniCorp.ref(12, 34) val tx: WireTransaction = TransactionBuilder(notary = null).apply { - Cash().generateIssue(this, amount, owner = AnonymousParty(ALICE_PUBKEY), notary = DUMMY_NOTARY) + Cash().generateIssue(this, amount, owner = AnonymousParty(alice.publicKey), notary = dummyNotary.party) }.toWireTransaction(miniCorpServices) assertTrue(tx.inputs.isEmpty()) assertEquals(tx.outputs[0], tx.outputs[0]) @@ -253,13 +226,13 @@ class CashTests { output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2)) // Move fails: not allowed to summon money. tweak { - command(ALICE_PUBKEY, Cash.Commands.Move()) + command(alice.publicKey, Cash.Commands.Move()) this `fails with` "the amounts balance" } // Issue works. tweak { - command(MEGA_CORP_PUBKEY, Cash.Commands.Issue()) + command(megaCorp.publicKey, Cash.Commands.Issue()) this.verifies() } } @@ -269,7 +242,7 @@ class CashTests { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, inState) output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount.splitEvenly(2).first())) - command(MEGA_CORP_PUBKEY, Cash.Commands.Issue()) + command(megaCorp.publicKey, Cash.Commands.Issue()) this `fails with` "output values sum to more than the inputs" } @@ -278,7 +251,7 @@ class CashTests { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, inState) output(Cash.PROGRAM_ID, inState) - command(MEGA_CORP_PUBKEY, Cash.Commands.Issue()) + command(megaCorp.publicKey, Cash.Commands.Issue()) this `fails with` "output values sum to more than the inputs" } @@ -287,9 +260,9 @@ class CashTests { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, inState) output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2)) - command(MEGA_CORP_PUBKEY, Cash.Commands.Issue()) + command(megaCorp.publicKey, Cash.Commands.Issue()) tweak { - command(MEGA_CORP_PUBKEY, Cash.Commands.Issue()) + command(megaCorp.publicKey, Cash.Commands.Issue()) this `fails with` "there is only a single issue command" } this.verifies() @@ -303,15 +276,15 @@ class CashTests { @Test(expected = IllegalStateException::class) fun `reject issuance with inputs`() { // Issue some cash - var ptx = TransactionBuilder(DUMMY_NOTARY) + var ptx = TransactionBuilder(dummyNotary.party) - Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY) + Cash().generateIssue(ptx, 100.DOLLARS `issued by` miniCorp.ref(12, 34), owner = miniCorp.party, notary = dummyNotary.party) val tx = miniCorpServices.signInitialTransaction(ptx) // Include the previously issued cash in a new issuance command - ptx = TransactionBuilder(DUMMY_NOTARY) + ptx = TransactionBuilder(dummyNotary.party) ptx.addInputState(tx.tx.outRef(0)) - Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY) + Cash().generateIssue(ptx, 100.DOLLARS `issued by` miniCorp.ref(12, 34), owner = miniCorp.party, notary = dummyNotary.party) } @Test @@ -319,7 +292,7 @@ class CashTests { // Splitting value works. transaction { attachment(Cash.PROGRAM_ID) - command(ALICE_PUBKEY, Cash.Commands.Move()) + command(alice.publicKey, Cash.Commands.Move()) tweak { input(Cash.PROGRAM_ID, inState) val splits4 = inState.amount.splitEvenly(4) @@ -350,7 +323,7 @@ class CashTests { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, inState) input(Cash.PROGRAM_ID, inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer)) - command(ALICE_PUBKEY, Cash.Commands.Move()) + command(alice.publicKey, Cash.Commands.Move()) this `fails with` "zero sized inputs" } transaction { @@ -358,7 +331,7 @@ class CashTests { input(Cash.PROGRAM_ID, inState) output(Cash.PROGRAM_ID, inState) output(Cash.PROGRAM_ID, inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer)) - command(ALICE_PUBKEY, Cash.Commands.Move()) + command(alice.publicKey, Cash.Commands.Move()) this `fails with` "zero sized outputs" } } @@ -369,8 +342,8 @@ class CashTests { transaction { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, inState) - output(Cash.PROGRAM_ID, outState issuedBy MINI_CORP) - command(ALICE_PUBKEY, Cash.Commands.Move()) + output(Cash.PROGRAM_ID, outState issuedBy miniCorp.party) + command(alice.publicKey, Cash.Commands.Move()) this `fails with` "the amounts balance" } // Can't change deposit reference when splitting. @@ -379,7 +352,7 @@ class CashTests { val splits2 = inState.amount.splitEvenly(2) input(Cash.PROGRAM_ID, inState) for (i in 0..1) output(Cash.PROGRAM_ID, outState.copy(amount = splits2[i]).editDepositRef(i.toByte())) - command(ALICE_PUBKEY, Cash.Commands.Move()) + command(alice.publicKey, Cash.Commands.Move()) this `fails with` "the amounts balance" } // Can't mix currencies. @@ -388,7 +361,7 @@ class CashTests { input(Cash.PROGRAM_ID, inState) output(Cash.PROGRAM_ID, outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer)) output(Cash.PROGRAM_ID, outState.copy(amount = 200.POUNDS `issued by` defaultIssuer)) - command(ALICE_PUBKEY, Cash.Commands.Move()) + command(alice.publicKey, Cash.Commands.Move()) this `fails with` "the amounts balance" } transaction { @@ -397,18 +370,18 @@ class CashTests { input(Cash.PROGRAM_ID, inState.copy( amount = 150.POUNDS `issued by` defaultIssuer, - owner = AnonymousParty(BOB_PUBKEY))) + owner = AnonymousParty(bob.publicKey))) output(Cash.PROGRAM_ID, outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer)) - command(ALICE_PUBKEY, Cash.Commands.Move()) + command(alice.publicKey, Cash.Commands.Move()) this `fails with` "the amounts balance" } // Can't have superfluous input states from different issuers. transaction { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, inState) - input(Cash.PROGRAM_ID, inState issuedBy MINI_CORP) + input(Cash.PROGRAM_ID, inState issuedBy miniCorp.party) output(Cash.PROGRAM_ID, outState) - command(ALICE_PUBKEY, Cash.Commands.Move()) + command(alice.publicKey, Cash.Commands.Move()) this `fails with` "the amounts balance" } // Can't combine two different deposits at the same issuer. @@ -417,7 +390,7 @@ class CashTests { input(Cash.PROGRAM_ID, inState) input(Cash.PROGRAM_ID, inState.editDepositRef(3)) output(Cash.PROGRAM_ID, outState.copy(amount = inState.amount * 2).editDepositRef(3)) - command(ALICE_PUBKEY, Cash.Commands.Move()) + command(alice.publicKey, Cash.Commands.Move()) this `fails with` "for reference [01]" } } @@ -430,17 +403,17 @@ class CashTests { input(Cash.PROGRAM_ID, issuerInState) output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer))) tweak { - command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer)) - command(MEGA_CORP_PUBKEY, Cash.Commands.Move()) + command(megaCorp.publicKey, Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer)) + command(megaCorp.publicKey, Cash.Commands.Move()) this `fails with` "the amounts balance" } tweak { - command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer)) + command(megaCorp.publicKey, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer)) this `fails with` "required net.corda.finance.contracts.asset.Cash.Commands.Move command" tweak { - command(MEGA_CORP_PUBKEY, Cash.Commands.Move()) + command(megaCorp.publicKey, Cash.Commands.Move()) this.verifies() } } @@ -453,14 +426,14 @@ class CashTests { transaction { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, issuerInState) - input(Cash.PROGRAM_ID, issuerInState.copy(owner = MINI_CORP) issuedBy MINI_CORP) - output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) issuedBy MINI_CORP) - output(Cash.PROGRAM_ID, issuerInState.copy(owner = MINI_CORP, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer))) - command(listOf(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), Cash.Commands.Move()) + input(Cash.PROGRAM_ID, issuerInState.copy(owner = miniCorp.party) issuedBy miniCorp.party) + output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) issuedBy miniCorp.party) + output(Cash.PROGRAM_ID, issuerInState.copy(owner = miniCorp.party, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer))) + command(listOf(megaCorp.publicKey, miniCorp.publicKey), Cash.Commands.Move()) this `fails with` "the amounts balance" - command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer)) + command(megaCorp.publicKey, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer)) this `fails with` "the amounts balance" - command(MINI_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef))) + command(miniCorp.publicKey, Cash.Commands.Exit(200.DOLLARS `issued by` miniCorp.ref(1))) this.verifies() } } @@ -472,8 +445,8 @@ class CashTests { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, inState) output(Cash.PROGRAM_ID, outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer))) - command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer)) - command(ALICE_PUBKEY, Cash.Commands.Move()) + command(megaCorp.publicKey, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer)) + command(alice.publicKey, Cash.Commands.Move()) this `fails with` "the amounts balance" } } @@ -484,23 +457,23 @@ class CashTests { attachment(Cash.PROGRAM_ID) // Gather 2000 dollars from two different issuers. input(Cash.PROGRAM_ID, inState) - input(Cash.PROGRAM_ID, inState issuedBy MINI_CORP) - command(ALICE_PUBKEY, Cash.Commands.Move()) + input(Cash.PROGRAM_ID, inState issuedBy miniCorp.party) + command(alice.publicKey, Cash.Commands.Move()) // Can't merge them together. tweak { - output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY), amount = 2000.DOLLARS `issued by` defaultIssuer)) + output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(bob.publicKey), amount = 2000.DOLLARS `issued by` defaultIssuer)) this `fails with` "the amounts balance" } // Missing MiniCorp deposit tweak { - output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY))) - output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY))) + output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(bob.publicKey))) + output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(bob.publicKey))) this `fails with` "the amounts balance" } // This works. - output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY))) - output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)) issuedBy MINI_CORP) + output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(bob.publicKey))) + output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(bob.publicKey)) issuedBy miniCorp.party) this.verifies() } } @@ -510,12 +483,12 @@ class CashTests { // Check we can do an atomic currency trade tx. transaction { attachment(Cash.PROGRAM_ID) - val pounds = Cash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), AnonymousParty(BOB_PUBKEY)) - input(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(ALICE_PUBKEY)) + val pounds = Cash.State(658.POUNDS `issued by` miniCorp.ref(3, 4, 5), AnonymousParty(bob.publicKey)) + input(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(alice.publicKey)) input(Cash.PROGRAM_ID, pounds) - output(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(BOB_PUBKEY)) - output(Cash.PROGRAM_ID, pounds ownedBy AnonymousParty(ALICE_PUBKEY)) - command(listOf(ALICE_PUBKEY, BOB_PUBKEY), Cash.Commands.Move()) + output(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(bob.publicKey)) + output(Cash.PROGRAM_ID, pounds ownedBy AnonymousParty(alice.publicKey)) + command(listOf(alice.publicKey, bob.publicKey), Cash.Commands.Move()) this.verifies() } } @@ -526,7 +499,7 @@ class CashTests { private fun makeCash(amount: Amount, issuer: AbstractParty, depositRef: Byte = 1) = StateAndRef( - TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, DUMMY_NOTARY), + TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, dummyNotary.party), StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) ) @@ -534,15 +507,15 @@ class CashTests { * Generate an exit transaction, removing some amount of cash from the ledger. */ private fun makeExit(serviceHub: ServiceHub, amount: Amount, issuer: Party, depositRef: Byte = 1): WireTransaction { - val tx = TransactionBuilder(DUMMY_NOTARY) - val payChangeTo = serviceHub.keyManagementService.freshKeyAndCert(MINI_CORP_IDENTITY, false).party - Cash().generateExit(tx, Amount(amount.quantity, Issued(issuer.ref(depositRef), amount.token)), WALLET, payChangeTo) + val tx = TransactionBuilder(dummyNotary.party) + val payChangeTo = serviceHub.keyManagementService.freshKeyAndCert(miniCorp.identity, false).party + Cash().generateExit(tx, Amount(amount.quantity, Issued(issuer.ref(depositRef), amount.token)), cashStates, payChangeTo) return tx.toWireTransaction(serviceHub) } private fun makeSpend(services: ServiceHub, amount: Amount, dest: AbstractParty): WireTransaction { val ourIdentity = services.myInfo.singleIdentityAndCert() - val tx = TransactionBuilder(DUMMY_NOTARY) + val tx = TransactionBuilder(dummyNotary.party) database.transaction { Cash.generateSpend(services, tx, amount, ourIdentity, dest) } @@ -554,12 +527,12 @@ class CashTests { */ @Test fun generateSimpleExit() { - val wtx = makeExit(miniCorpServices, 100.DOLLARS, MEGA_CORP, 1) - assertEquals(WALLET[0].ref, wtx.inputs[0]) + val wtx = makeExit(miniCorpServices, 100.DOLLARS, megaCorp.party, 1) + assertEquals(cashStates[0].ref, wtx.inputs[0]) assertEquals(0, wtx.outputs.size) val expectedMove = Cash.Commands.Move() - val expectedExit = Cash.Commands.Exit(Amount(10000, Issued(MEGA_CORP.ref(1), USD))) + val expectedExit = Cash.Commands.Exit(Amount(10000, Issued(megaCorp.ref(1), USD))) assertEquals(listOf(expectedMove, expectedExit), wtx.commands.map { it.value }) } @@ -569,15 +542,15 @@ class CashTests { */ @Test fun generatePartialExit() { - val wtx = makeExit(miniCorpServices, 50.DOLLARS, MEGA_CORP, 1) + val wtx = makeExit(miniCorpServices, 50.DOLLARS, megaCorp.party, 1) val actualInput = wtx.inputs.single() // Filter the available inputs and confirm exactly one has been used - val expectedInputs = WALLET.filter { it.ref == actualInput } + val expectedInputs = cashStates.filter { it.ref == actualInput } assertEquals(1, expectedInputs.size) val inputState = expectedInputs.single() val actualChange = wtx.outputs.single().data as Cash.State val expectedChangeAmount = inputState.state.data.amount.quantity - 50.DOLLARS.quantity - val expectedChange = WALLET[0].state.data.copy(amount = WALLET[0].state.data.amount.copy(quantity = expectedChangeAmount), owner = actualChange.owner) + val expectedChange = cashStates[0].state.data.copy(amount = cashStates[0].state.data.amount.copy(quantity = expectedChangeAmount), owner = actualChange.owner) assertEquals(expectedChange, wtx.getOutput(0)) } @@ -586,7 +559,7 @@ class CashTests { */ @Test fun generateAbsentExit() { - assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 1) } + assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, megaCorp.party, 1) } } /** @@ -594,7 +567,7 @@ class CashTests { */ @Test fun generateInvalidReferenceExit() { - assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 2) } + assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, megaCorp.party, 2) } } /** @@ -602,7 +575,7 @@ class CashTests { */ @Test fun generateInsufficientExit() { - assertFailsWith { makeExit(miniCorpServices, 1000.DOLLARS, MEGA_CORP, 1) } + assertFailsWith { makeExit(miniCorpServices, 1000.DOLLARS, megaCorp.party, 1) } } /** @@ -610,7 +583,7 @@ class CashTests { */ @Test fun generateOwnerWithNoStatesExit() { - assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, CHARLIE, 1) } + assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, charlie.party, 1) } } /** @@ -619,8 +592,8 @@ class CashTests { @Test fun generateExitWithEmptyVault() { assertFailsWith { - val tx = TransactionBuilder(DUMMY_NOTARY) - Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList(), ourIdentity) + val tx = TransactionBuilder(dummyNotary.party) + Cash().generateExit(tx, Amount(100, Issued(charlie.ref(1), GBP)), emptyList(), ourIdentity) } } @@ -641,9 +614,8 @@ class CashTests { @Test fun generateSimpleSpendWithParties() { database.transaction { - - val tx = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(ourServices, tx, 80.DOLLARS, ourServices.myInfo.singleIdentityAndCert(), ALICE, setOf(MINI_CORP)) + val tx = TransactionBuilder(dummyNotary.party) + Cash.generateSpend(ourServices, tx, 80.DOLLARS, ourServices.myInfo.singleIdentityAndCert(), alice.party, setOf(miniCorp.party)) assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0]) } @@ -731,9 +703,9 @@ class CashTests { */ @Test fun aggregation() { - val fiveThousandDollarsFromMega = Cash.State(5000.DOLLARS `issued by` MEGA_CORP.ref(2), MEGA_CORP) - val twoThousandDollarsFromMega = Cash.State(2000.DOLLARS `issued by` MEGA_CORP.ref(2), MINI_CORP) - val oneThousandDollarsFromMini = Cash.State(1000.DOLLARS `issued by` MINI_CORP.ref(3), MEGA_CORP) + val fiveThousandDollarsFromMega = Cash.State(5000.DOLLARS `issued by` megaCorp.ref(2), megaCorp.party) + val twoThousandDollarsFromMega = Cash.State(2000.DOLLARS `issued by` megaCorp.ref(2), miniCorp.party) + val oneThousandDollarsFromMini = Cash.State(1000.DOLLARS `issued by` miniCorp.ref(3), megaCorp.party) // Obviously it must be possible to aggregate states with themselves assertEquals(fiveThousandDollarsFromMega.amount.token, fiveThousandDollarsFromMega.amount.token) @@ -747,7 +719,7 @@ class CashTests { // States cannot be aggregated if the currency differs assertNotEquals(oneThousandDollarsFromMini.amount.token, - Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP).amount.token) + Cash.State(1000.POUNDS `issued by` miniCorp.ref(3), megaCorp.party).amount.token) // States cannot be aggregated if the reference differs assertNotEquals(fiveThousandDollarsFromMega.amount.token, (fiveThousandDollarsFromMega withDeposit defaultIssuer).amount.token) @@ -757,20 +729,20 @@ class CashTests { @Test fun `summing by owner`() { val states = listOf( - Cash.State(1000.DOLLARS `issued by` defaultIssuer, MINI_CORP), - Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP), - Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP) + Cash.State(1000.DOLLARS `issued by` defaultIssuer, miniCorp.party), + Cash.State(2000.DOLLARS `issued by` defaultIssuer, megaCorp.party), + Cash.State(4000.DOLLARS `issued by` defaultIssuer, megaCorp.party) ) - assertEquals(6000.DOLLARS `issued by` defaultIssuer, states.sumCashBy(MEGA_CORP)) + assertEquals(6000.DOLLARS `issued by` defaultIssuer, states.sumCashBy(megaCorp.party)) } @Test(expected = UnsupportedOperationException::class) fun `summing by owner throws`() { val states = listOf( - Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP), - Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP) + Cash.State(2000.DOLLARS `issued by` defaultIssuer, megaCorp.party), + Cash.State(4000.DOLLARS `issued by` defaultIssuer, megaCorp.party) ) - states.sumCashBy(MINI_CORP) + states.sumCashBy(miniCorp.party) } @Test @@ -789,9 +761,9 @@ class CashTests { @Test fun `summing a single currency`() { val states = listOf( - Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP), - Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP), - Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP) + Cash.State(1000.DOLLARS `issued by` defaultIssuer, megaCorp.party), + Cash.State(2000.DOLLARS `issued by` defaultIssuer, megaCorp.party), + Cash.State(4000.DOLLARS `issued by` defaultIssuer, megaCorp.party) ) // Test that summing everything produces the total number of dollars val expected = 7000.DOLLARS `issued by` defaultIssuer @@ -802,8 +774,8 @@ class CashTests { @Test(expected = IllegalArgumentException::class) fun `summing multiple currencies`() { val states = listOf( - Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP), - Cash.State(4000.POUNDS `issued by` defaultIssuer, MEGA_CORP) + Cash.State(1000.DOLLARS `issued by` defaultIssuer, megaCorp.party), + Cash.State(4000.POUNDS `issued by` defaultIssuer, megaCorp.party) ) // Test that summing everything fails because we're mixing units states.sumCash() @@ -812,23 +784,20 @@ class CashTests { // Double spend. @Test fun chainCashDoubleSpendFailsWith() { - val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock().also { - doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) - }, MEGA_CORP.name, MEGA_CORP_KEY) - mockService.ledger(DUMMY_NOTARY) { + MockServices(megaCorp).ledger(dummyNotary.party) { unverifiedTransaction { attachment(Cash.PROGRAM_ID) output(Cash.PROGRAM_ID, "MEGA_CORP cash", Cash.State( - amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), - owner = MEGA_CORP)) + amount = 1000.DOLLARS `issued by` megaCorp.ref(1, 1), + owner = megaCorp.party)) } transaction { attachment(Cash.PROGRAM_ID) input("MEGA_CORP cash") - output(Cash.PROGRAM_ID, "MEGA_CORP cash 2", "MEGA_CORP cash".output().copy(owner = AnonymousParty(ALICE_PUBKEY))) - command(MEGA_CORP_PUBKEY, Cash.Commands.Move()) + output(Cash.PROGRAM_ID, "MEGA_CORP cash 2", "MEGA_CORP cash".output().copy(owner = AnonymousParty(alice.publicKey))) + command(megaCorp.publicKey, Cash.Commands.Move()) this.verifies() } @@ -837,8 +806,8 @@ class CashTests { attachment(Cash.PROGRAM_ID) input("MEGA_CORP cash") // We send it to another pubkey so that the transaction is not identical to the previous one - output(Cash.PROGRAM_ID, "MEGA_CORP cash 3", "MEGA_CORP cash".output().copy(owner = ALICE)) - command(MEGA_CORP_PUBKEY, Cash.Commands.Move()) + output(Cash.PROGRAM_ID, "MEGA_CORP cash 3", "MEGA_CORP cash".output().copy(owner = alice.party)) + command(megaCorp.publicKey, Cash.Commands.Move()) this.verifies() } this.fails() @@ -850,11 +819,11 @@ class CashTests { @Test fun multiSpend() { - val tx = TransactionBuilder(DUMMY_NOTARY) + val tx = TransactionBuilder(dummyNotary.party) database.transaction { val payments = listOf( PartyAndAmount(miniCorpAnonymised, 400.DOLLARS), - PartyAndAmount(CHARLIE_ANONYMISED, 150.DOLLARS) + PartyAndAmount(charlie.party.anonymise(), 150.DOLLARS) ) Cash.generateSpend(ourServices, tx, payments, ourServices.myInfo.singleIdentityAndCert()) } @@ -865,9 +834,9 @@ class CashTests { assertEquals(320.DOLLARS, out(1).amount.withoutIssuer()) assertEquals(150.DOLLARS, out(2).amount.withoutIssuer()) assertEquals(30.DOLLARS, out(3).amount.withoutIssuer()) - assertEquals(MINI_CORP, out(0).amount.token.issuer.party) - assertEquals(MEGA_CORP, out(1).amount.token.issuer.party) - assertEquals(MEGA_CORP, out(2).amount.token.issuer.party) - assertEquals(MEGA_CORP, out(3).amount.token.issuer.party) + assertEquals(miniCorp.party, out(0).amount.token.issuer.party) + assertEquals(megaCorp.party, out(1).amount.token.issuer.party) + assertEquals(megaCorp.party, out(2).amount.token.issuer.party) + assertEquals(megaCorp.party, out(3).amount.token.issuer.party) } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index ca7a2fa1f9..da99b28c8e 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -46,9 +46,15 @@ import java.time.Clock import java.util.* fun makeTestIdentityService(vararg identities: PartyAndCertificate) = InMemoryIdentityService(identities, DEV_ROOT_CA.certificate) + /** - * A singleton utility that only provides a mock identity, key and storage service. However, this is sufficient for - * building chains of transactions and verifying them. It isn't sufficient for testing flows however. + * An implementation of [ServiceHub] that is designed for in-memory unit tests of contract validation logic. It has + * enough functionality to do tests of code that queries the vault, inserts to the vault, and constructs/checks + * transactions. However it isn't enough to test flows and other aspects of an app that require a network. For that + * you should investigate [MockNetwork]. + * + * There are a variety of constructors that can be used to supply enough data to simulate a node. Each mock service hub + * must have at least an identity of its own. The other components have defaults that work in most situations. */ open class MockServices private constructor( cordappLoader: CordappLoader, @@ -80,6 +86,7 @@ open class MockServices private constructor( /** * Makes database and mock services appropriate for unit tests. + * * @param moreKeys a list of additional [KeyPair] instances to be used by [MockServices]. * @param identityService an instance of [IdentityServiceInternal], see [makeTestIdentityService]. * @param initialIdentity the first (typically sole) identity the services will represent. @@ -109,20 +116,68 @@ open class MockServices private constructor( } return Pair(database, mockService) } + + @JvmStatic + private fun getCallerPackage(): String { + // TODO: In Java 9 there's a new stack walker API that is better than this. + // The magic number '3' here is to chop off this method, an invisible bridge method generated by the + // compiler and then the c'tor itself. + return Throwable().stackTrace[3].className.split('.').dropLast(1).joinToString(".") + } } - private constructor(cordappLoader: CordappLoader, identityService: IdentityServiceInternal, initialIdentity: TestIdentity, moreKeys: Array) : this(cordappLoader, MockTransactionStorage(), identityService, initialIdentity, moreKeys) + private constructor(cordappLoader: CordappLoader, identityService: IdentityServiceInternal, + initialIdentity: TestIdentity, moreKeys: Array) + : this(cordappLoader, MockTransactionStorage(), identityService, initialIdentity, moreKeys) + + /** + * Create a mock [ServiceHub] that looks for app code in the given package names, uses the provided identity service + * (you can get one from [makeTestIdentityService]) and represents the given identity. + */ @JvmOverloads constructor(cordappPackages: List, identityService: IdentityServiceInternal = makeTestIdentityService(), initialIdentity: TestIdentity, vararg moreKeys: KeyPair) : this(CordappLoader.createWithTestPackages(cordappPackages), identityService, initialIdentity, moreKeys) + /** + * Create a mock [ServiceHub] that looks for app code in the given package names, uses the provided identity service + * (you can get one from [makeTestIdentityService]) and represents the given identity. + */ @JvmOverloads constructor(cordappPackages: List, identityService: IdentityServiceInternal = makeTestIdentityService(), initialIdentityName: CordaX500Name, key: KeyPair, vararg moreKeys: KeyPair) : this(cordappPackages, identityService, TestIdentity(initialIdentityName, key), *moreKeys) + /** + * Create a mock [ServiceHub] that can't load CorDapp code, which uses the provided identity service + * (you can get one from [makeTestIdentityService]) and which represents the given identity. + */ @JvmOverloads constructor(cordappPackages: List, identityService: IdentityServiceInternal = makeTestIdentityService(), initialIdentityName: CordaX500Name) : this(cordappPackages, identityService, TestIdentity(initialIdentityName)) + /** + * Create a mock [ServiceHub] which uses the package of the caller to find CorDapp code. It uses the provided identity service + * (you can get one from [makeTestIdentityService]) and which represents the given identity. + */ @JvmOverloads - constructor(cordappPackages: List, identityService: IdentityServiceInternal = makeTestIdentityService(), vararg moreKeys: KeyPair) : this(cordappPackages, identityService, TestIdentity.fresh("MockServices"), *moreKeys) + constructor(identityService: IdentityServiceInternal = makeTestIdentityService(), initialIdentityName: CordaX500Name, key: KeyPair, vararg moreKeys: KeyPair) + : this(listOf(getCallerPackage()), identityService, TestIdentity(initialIdentityName, key), *moreKeys) + + /** + * Create a mock [ServiceHub] which uses the package of the caller to find CorDapp code. It uses the provided identity service + * (you can get one from [makeTestIdentityService]) and which represents the given identity. It has no keys. + */ + @JvmOverloads + constructor(identityService: IdentityServiceInternal = makeTestIdentityService(), initialIdentityName: CordaX500Name) + : this(listOf(getCallerPackage()), identityService, TestIdentity(initialIdentityName)) + + /** + * A helper constructor that requires at least one test identity to be registered, and which takes the package of + * the caller as the package in which to find app code. This is the most convenient constructor and the one that + * is normally worth using. The first identity is the identity of this service hub, the rest are identities that + * it is aware of. + */ + constructor(firstIdentity: TestIdentity, vararg moreIdentities: TestIdentity) : this( + listOf(getCallerPackage()), + makeTestIdentityService(*listOf(firstIdentity, *moreIdentities).map { it.identity }.toTypedArray()), + firstIdentity, firstIdentity.keyPair + ) override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { txs.forEach { @@ -185,7 +240,7 @@ class MockKeyManagementService(val identityService: IdentityServiceInternal, private fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey)) private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { - val pk = publicKey.keys.first { keyStore.containsKey(it) } + val pk = publicKey.keys.firstOrNull { keyStore.containsKey(it) } ?: throw IllegalArgumentException("Public key not found: ${publicKey.toStringShort()}") return KeyPair(pk, keyStore[pk]!!) }