diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt index 5b1541b597..7023242a07 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt @@ -1,54 +1,59 @@ package net.corda.confidential +import com.natpryce.hamkrest.MatchResult +import com.natpryce.hamkrest.Matcher +import com.natpryce.hamkrest.equalTo import net.corda.core.identity.* -import net.corda.core.utilities.getOrThrow import net.corda.testing.core.* +import net.corda.testing.internal.matchers.allOf +import net.corda.testing.internal.matchers.flow.willReturn import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.startFlow -import org.junit.Before import org.junit.Test import kotlin.test.* +import com.natpryce.hamkrest.assertion.assert +import net.corda.core.crypto.DigitalSignature +import net.corda.testing.internal.matchers.hasOnlyEntries +import net.corda.testing.node.internal.TestStartedNode +import org.junit.AfterClass +import java.security.PublicKey class SwapIdentitiesFlowTests { - private lateinit var mockNet: InternalMockNetwork + companion object { + private val mockNet = InternalMockNetwork(networkSendManuallyPumped = false, threadPerNode = true) - @Before - fun setup() { - // We run this in parallel threads to help catch any race conditions that may exist. - mockNet = InternalMockNetwork(networkSendManuallyPumped = false, threadPerNode = true) + @AfterClass + @JvmStatic + fun tearDown() = mockNet.stopNodes() } + private val aliceNode = mockNet.createPartyNode(makeUnique(ALICE_NAME)) + private val bobNode = mockNet.createPartyNode(makeUnique(BOB_NAME)) + private val charlieNode = mockNet.createPartyNode(makeUnique(CHARLIE_NAME)) + private val alice = aliceNode.info.singleIdentity() + private val bob = bobNode.info.singleIdentity() + @Test fun `issue key`() { - // Set up values we'll need - val aliceNode = mockNet.createPartyNode(ALICE_NAME) - val bobNode = mockNet.createPartyNode(BOB_NAME) - val alice = aliceNode.info.singleIdentity() - val bob = bobNode.services.myInfo.singleIdentity() - - // Run the flows - val requesterFlow = aliceNode.services.startFlow(SwapIdentitiesFlow(bob)).resultFuture - - // Get the results - val actual: Map = requesterFlow.getOrThrow().toMap() - assertEquals(2, actual.size) - // Verify that the generated anonymous identities do not match the well known identities - val aliceAnonymousIdentity = actual[alice] ?: throw IllegalStateException() - val bobAnonymousIdentity = actual[bob] ?: throw IllegalStateException() - assertNotEquals(alice, aliceAnonymousIdentity) - assertNotEquals(bob, bobAnonymousIdentity) - - // Verify that the anonymous identities look sane - assertEquals(alice.name, aliceNode.database.transaction { aliceNode.services.identityService.wellKnownPartyFromAnonymous(aliceAnonymousIdentity)!!.name }) - assertEquals(bob.name, bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(bobAnonymousIdentity)!!.name }) - - // Verify that the nodes have the right anonymous identities - assertTrue { aliceAnonymousIdentity.owningKey in aliceNode.services.keyManagementService.keys } - assertTrue { bobAnonymousIdentity.owningKey in bobNode.services.keyManagementService.keys } - assertFalse { aliceAnonymousIdentity.owningKey in bobNode.services.keyManagementService.keys } - assertFalse { bobAnonymousIdentity.owningKey in aliceNode.services.keyManagementService.keys } - - mockNet.stopNodes() + assert.that( + aliceNode.services.startFlow(SwapIdentitiesFlow(bob)), + willReturn( + hasOnlyEntries( + alice to allOf( + !equalTo(alice), + aliceNode.resolvesToWellKnownParty(alice), + aliceNode.holdsOwningKey(), + !bobNode.holdsOwningKey() + ), + bob to allOf( + !equalTo(bob), + bobNode.resolvesToWellKnownParty(bob), + bobNode.holdsOwningKey(), + !aliceNode.holdsOwningKey() + ) + ) + ) + ) } /** @@ -56,58 +61,101 @@ class SwapIdentitiesFlowTests { */ @Test fun `verifies identity name`() { - // Set up values we'll need - val aliceNode = mockNet.createPartyNode(ALICE_NAME) - val bobNode = mockNet.createPartyNode(BOB_NAME) - val charlieNode = mockNet.createPartyNode(CHARLIE_NAME) - val bob: Party = bobNode.services.myInfo.singleIdentity() - val notBob = charlieNode.database.transaction { - charlieNode.services.keyManagementService.freshKeyAndCert(charlieNode.services.myInfo.singleIdentityAndCert(), false) + val notBob = charlieNode.issueFreshKeyAndCert() + val signature = charlieNode.signSwapIdentitiesFlowData(notBob, notBob.owningKey) + assertFailsWith( + "Certificate subject must match counterparty's well known identity.") { + aliceNode.validateSwapIdentitiesFlow(bob, notBob, signature) } - val sigData = SwapIdentitiesFlow.buildDataToSign(notBob) - val signature = charlieNode.services.keyManagementService.sign(sigData, notBob.owningKey) - assertFailsWith("Certificate subject must match counterparty's well known identity.") { - SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob, notBob, signature.withoutKey()) - } - - mockNet.stopNodes() } /** * Check that flow is actually validating its the signature presented by the counterparty. */ @Test - fun `verifies signature`() { - // Set up values we'll need - val aliceNode = mockNet.createPartyNode(ALICE_NAME) - val bobNode = mockNet.createPartyNode(BOB_NAME) - val alice: PartyAndCertificate = aliceNode.info.singleIdentityAndCert() - val bob: PartyAndCertificate = bobNode.info.singleIdentityAndCert() - // Check that the right name but wrong key is rejected - val evilBobNode = mockNet.createPartyNode(BOB_NAME) + fun `verification rejects signature if name is right but key is wrong`() { + val evilBobNode = mockNet.createPartyNode(bobNode.info.singleIdentity().name) val evilBob = evilBobNode.info.singleIdentityAndCert() - evilBobNode.database.transaction { - val anonymousEvilBob = evilBobNode.services.keyManagementService.freshKeyAndCert(evilBob, false) - val sigData = SwapIdentitiesFlow.buildDataToSign(evilBob) - val signature = evilBobNode.services.keyManagementService.sign(sigData, anonymousEvilBob.owningKey) - assertFailsWith("Signature does not match the given identity and nonce") { - SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob.party, anonymousEvilBob, signature.withoutKey()) - } - } - // Check that the right signing key, but wrong identity is rejected - val anonymousAlice: PartyAndCertificate = aliceNode.database.transaction { - aliceNode.services.keyManagementService.freshKeyAndCert(alice, false) - } - bobNode.database.transaction { - bobNode.services.keyManagementService.freshKeyAndCert(bob, false) - }.let { anonymousBob -> - val sigData = SwapIdentitiesFlow.buildDataToSign(anonymousAlice) - val signature = bobNode.services.keyManagementService.sign(sigData, anonymousBob.owningKey) - assertFailsWith("Signature does not match the given identity and nonce.") { - SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob.party, anonymousBob, signature.withoutKey()) - } - } + val anonymousEvilBob = evilBobNode.issueFreshKeyAndCert() + val signature = evilBobNode.signSwapIdentitiesFlowData(evilBob, anonymousEvilBob.owningKey) - mockNet.stopNodes() + assertFailsWith( + "Signature does not match the given identity and nonce") { + aliceNode.validateSwapIdentitiesFlow(bob, anonymousEvilBob, signature) + } } + + @Test + fun `verification rejects signature if key is right but name is wrong`() { + val anonymousAlice = aliceNode.issueFreshKeyAndCert() + val anonymousBob = bobNode.issueFreshKeyAndCert() + val signature = bobNode.signSwapIdentitiesFlowData(anonymousAlice, anonymousBob.owningKey) + + assertFailsWith( + "Signature does not match the given identity and nonce.") { + aliceNode.validateSwapIdentitiesFlow(bob, anonymousBob, signature) + } + } + + //region Operations + private fun TestStartedNode.issueFreshKeyAndCert() = database.transaction { + services.keyManagementService.freshKeyAndCert(services.myInfo.singleIdentityAndCert(), false) + } + + private fun TestStartedNode.signSwapIdentitiesFlowData(party: PartyAndCertificate, owningKey: PublicKey) = + services.keyManagementService.sign( + SwapIdentitiesFlow.buildDataToSign(party), + owningKey) + + private fun TestStartedNode.validateSwapIdentitiesFlow( + party: Party, + counterparty: PartyAndCertificate, + signature: DigitalSignature.WithKey) = + SwapIdentitiesFlow.validateAndRegisterIdentity( + services.identityService, + party, + counterparty, + signature.withoutKey() + ) + //endregion + + //region Matchers + private fun TestStartedNode.resolvesToWellKnownParty(party: Party) = object : Matcher { + override val description = """ + is resolved by "${this@resolvesToWellKnownParty.info.singleIdentity().name}" to well-known party "${party.name}" + """.trimIndent() + + override fun invoke(actual: AnonymousParty): MatchResult { + val resolvedName = services.identityService.wellKnownPartyFromAnonymous(actual)!!.name + return if (resolvedName == party.name) { + MatchResult.Match + } else { + MatchResult.Mismatch("was resolved to $resolvedName") + } + } + } + + private data class HoldsOwningKeyMatcher(val node: TestStartedNode, val negated: Boolean = false) : Matcher { + private fun sayNotIf(negation: Boolean) = if (negation) { "not " } else { "" } + + override val description = + "has an owning key which is ${sayNotIf(negated)}held by ${node.info.singleIdentity().name}" + + override fun invoke(actual: AnonymousParty) = + if (negated != actual.owningKey in node.services.keyManagementService.keys) { + MatchResult.Match + } else { + MatchResult.Mismatch(""" + had an owning key which was ${sayNotIf(!negated)}held by ${node.info.singleIdentity().name} + """.trimIndent()) + } + + override fun not(): Matcher { + return copy(negated=!negated) + } + } + + private fun TestStartedNode.holdsOwningKey() = HoldsOwningKeyMatcher(this) + //endregion + } diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index 81299f7ada..3a19824b7b 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -5,8 +5,8 @@ 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.testing.internal.matchers.flow.willReturn +import net.corda.testing.internal.matchers.flow.willThrow import net.corda.core.flows.mixins.WithMockNet import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party @@ -16,6 +16,7 @@ import net.corda.core.internal.hash import net.corda.node.services.persistence.NodeAttachmentService import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.makeUnique import net.corda.testing.core.singleIdentity import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNodeParameters @@ -120,13 +121,13 @@ class AttachmentTests : WithMockNet { //region Generators override fun makeNode(name: CordaX500Name) = - mockNet.createPartyNode(randomise(name)).apply { + mockNet.createPartyNode(makeUnique(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 = randomise(name)), + InternalMockNodeParameters(legalName = makeUnique(name)), nodeFactory = { args, _ -> object : InternalMockNetwork.MockNode(args) { override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false } diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 96d9b77f5a..c8bf1fad7a 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -5,8 +5,8 @@ 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.testing.internal.matchers.flow.willReturn +import net.corda.testing.internal.matchers.flow.willThrow import net.corda.core.flows.mixins.WithContracts import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowRPCTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowRPCTest.kt index f185bf33ec..585e269de1 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowRPCTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowRPCTest.kt @@ -8,8 +8,8 @@ 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.testing.internal.matchers.rpc.willReturn +import net.corda.testing.internal.matchers.rpc.willThrow import net.corda.core.flows.mixins.WithContracts import net.corda.core.flows.mixins.WithFinality import net.corda.core.messaging.CordaRPCOps diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 9f4bbc7bf1..ead8da3ae4 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -3,8 +3,8 @@ package net.corda.core.flows 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.testing.internal.matchers.flow.willReturn +import net.corda.testing.internal.matchers.flow.willThrow import net.corda.core.flows.mixins.WithContracts import net.corda.core.flows.mixins.WithFinality import net.corda.core.identity.AbstractParty @@ -80,9 +80,9 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality { // Party A initiates contract upgrade flow, expected to succeed this time. assert.that( aliceNode.initiateDummyContractUpgrade(atx), - willReturn( - aliceNode.hasDummyContractUpgradeTransaction() - and bobNode.hasDummyContractUpgradeTransaction())) + willReturn( + aliceNode.hasDummyContractUpgradeTransaction() + and bobNode.hasDummyContractUpgradeTransaction())) } private fun TestStartedNode.issueCash(amount: Amount = Amount(1000, USD)) = diff --git a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt index 17e2a5c6ec..08d6741580 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt @@ -2,8 +2,8 @@ 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.testing.internal.matchers.flow.willReturn +import net.corda.testing.internal.matchers.flow.willThrow import net.corda.core.flows.mixins.WithFinality import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction diff --git a/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt index 2001f26b9c..c546a06c20 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt @@ -2,7 +2,7 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import com.natpryce.hamkrest.assertion.assert -import net.corda.core.flows.matchers.flow.willReturn +import net.corda.testing.internal.matchers.flow.willReturn import net.corda.core.flows.mixins.WithMockNet import net.corda.core.identity.Party import net.corda.core.utilities.UntrustworthyData diff --git a/core/src/test/kotlin/net/corda/core/flows/matchers/flow/FlowMatchers.kt b/core/src/test/kotlin/net/corda/core/flows/matchers/flow/FlowMatchers.kt deleted file mode 100644 index 7ebea9131b..0000000000 --- a/core/src/test/kotlin/net/corda/core/flows/matchers/flow/FlowMatchers.kt +++ /dev/null @@ -1,36 +0,0 @@ -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.willReturn -import net.corda.core.flows.matchers.willThrow -import net.corda.core.internal.FlowStateMachine - -/** - * Matches a Flow that succeeds with a result matched by the given matcher - */ -fun willReturn() = has(FlowStateMachine::resultFuture, willReturn()) - -fun willReturn(expected: T): Matcher> = net.corda.core.flows.matchers.flow.willReturn(equalTo(expected)) - -/** - * Matches a Flow that succeeds with a result matched by the given matcher - */ -fun willReturn(successMatcher: Matcher) = has( - FlowStateMachine::resultFuture, - willReturn(successMatcher)) - -/** - * Matches a Flow that fails, with an exception matched by the given matcher. - */ -inline fun willThrow(failureMatcher: Matcher) = has( - FlowStateMachine<*>::resultFuture, - willThrow(failureMatcher)) - -/** - * Matches a Flow that fails, with an exception of the specified type. - */ -inline fun willThrow() = has( - FlowStateMachine<*>::resultFuture, - willThrow()) \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/flows/matchers/rpc/RpcMatchers.kt b/core/src/test/kotlin/net/corda/core/flows/matchers/rpc/RpcMatchers.kt deleted file mode 100644 index dcc365d301..0000000000 --- a/core/src/test/kotlin/net/corda/core/flows/matchers/rpc/RpcMatchers.kt +++ /dev/null @@ -1,31 +0,0 @@ -package net.corda.core.flows.matchers.rpc - -import com.natpryce.hamkrest.Matcher -import com.natpryce.hamkrest.has -import net.corda.core.flows.matchers.willReturn -import net.corda.core.flows.matchers.willThrow -import net.corda.core.messaging.FlowHandle - -/** - * Matches a flow handle that succeeds with a result matched by the given matcher - */ -fun willReturn() = has(FlowHandle::returnValue, willReturn()) - -/** - * Matches a flow handle that succeeds with a result matched by the given matcher - */ -fun willReturn(successMatcher: Matcher) = has(FlowHandle::returnValue, willReturn(successMatcher)) - -/** - * Matches a flow handle that fails, with an exception matched by the given matcher. - */ -inline fun willThrow(failureMatcher: Matcher) = has( - FlowHandle<*>::returnValue, - willThrow(failureMatcher)) - -/** - * Matches a flow handle that fails, with an exception of the specified type. - */ -inline fun willThrow() = has( - FlowHandle<*>::returnValue, - willThrow()) \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/flows/mixins/WithMockNet.kt b/core/src/test/kotlin/net/corda/core/flows/mixins/WithMockNet.kt index 0901da704d..02c70ecc3f 100644 --- a/core/src/test/kotlin/net/corda/core/flows/mixins/WithMockNet.kt +++ b/core/src/test/kotlin/net/corda/core/flows/mixins/WithMockNet.kt @@ -9,10 +9,10 @@ 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.testing.core.makeUnique import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.TestStartedNode import net.corda.testing.node.internal.startFlow -import java.util.* import kotlin.reflect.KClass /** @@ -25,12 +25,7 @@ interface WithMockNet { /** * 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()}") + fun makeNode(name: CordaX500Name) = mockNet.createPartyNode(makeUnique(name)) /** * Run the mock network before proceeding diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 739dccf3df..91d36fe238 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -337,7 +337,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe } } - override val started: TestStartedNode? get() = uncheckedCast(super.started) + override val started: TestStartedNode? get() = super.started override fun createStartedNode(nodeInfo: NodeInfo, rpcOps: CordaRPCOps, notaryService: NotaryService?): TestStartedNode { return TestStartedNodeImpl( diff --git a/testing/test-utils/build.gradle b/testing/test-utils/build.gradle index 98beb3372a..36c44a076b 100644 --- a/testing/test-utils/build.gradle +++ b/testing/test-utils/build.gradle @@ -24,6 +24,7 @@ dependencies { compile 'com.nhaarman:mockito-kotlin:1.5.0' compile "org.mockito:mockito-core:$mockito_version" compile "org.assertj:assertj-core:$assertj_version" + compile "com.natpryce:hamkrest:$hamkrest_version" // Guava: Google test library (collections test suite) compile "com.google.guava:guava-testlib:$guava_version" diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt index 9b092d5f45..3de6c0a16b 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt @@ -25,6 +25,7 @@ import java.math.BigInteger import java.security.KeyPair import java.security.PublicKey import java.security.cert.X509Certificate +import java.util.* import java.util.concurrent.atomic.AtomicInteger /** @@ -108,6 +109,18 @@ fun getTestPartyAndCertificate(name: CordaX500Name, publicKey: PublicKey): Party return getTestPartyAndCertificate(Party(name, publicKey)) } + +private val count = AtomicInteger(0) +/** + * Randomise a party name to avoid clashes with other tests + */ +fun makeUnique(name: CordaX500Name) = name.copy(commonName = + if (name.commonName == null) { + count.incrementAndGet().toString() + } else { + "${ name.commonName }_${ count.incrementAndGet() }" + }) + /** * A class that encapsulates a test identity containing a [CordaX500Name] and a [KeyPair], alongside a range * of utility methods for use during testing. diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/Matchers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/Matchers.kt new file mode 100644 index 0000000000..e65c1b1cdb --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/Matchers.kt @@ -0,0 +1,99 @@ +package net.corda.testing.internal.matchers + +import com.natpryce.hamkrest.* + +internal fun indent(description: String) = description.lineSequence().map { "\t$it" }.joinToString("\n") + +fun hasEntrySetSize(expected: Int) = object : Matcher> { + override val description = "is a map of size $expected" + override fun invoke(actual: Map<*, *>) = + if (actual.size == expected) { + MatchResult.Match + } else { + MatchResult.Mismatch("was a map of size ${actual.size}") + } +} + +fun Matcher.redescribe(redescriber: (String) -> String) = object : Matcher { + override val description = redescriber(this@redescribe.description) + override fun invoke(actual: T) = this@redescribe(actual) +} + +fun Matcher.redescribeMismatch(redescriber: (String) -> String) = object : Matcher { + override val description = this@redescribeMismatch.description + override fun invoke(actual: T) = this@redescribeMismatch(actual).modifyMismatchDescription(redescriber) +} + +fun MatchResult.modifyMismatchDescription(modify: (String) -> String) = when(this) { + is MatchResult.Match -> MatchResult.Match + is MatchResult.Mismatch -> MatchResult.Mismatch(modify(this.description)) +} + +fun Matcher.extrude(projection: (O) -> I) = object : Matcher { + override val description = this@extrude.description + override fun invoke(actual: O) = this@extrude(projection(actual)) +} + +internal fun hasAnEntry(key: K, valueMatcher: Matcher) = object : Matcher> { + override val description = "$key: ${valueMatcher.description}" + override fun invoke(actual: Map): MatchResult = + actual[key]?.let { valueMatcher(it) }?.let { when(it) { + is MatchResult.Match -> it + is MatchResult.Mismatch -> MatchResult.Mismatch("$key: ${it.description}") + }} ?: MatchResult.Mismatch("$key was not present") +} + +fun hasEntry(key: K, valueMatcher: Matcher) = + hasAnEntry(key, valueMatcher).redescribe { "Is a map containing the entry:\n${indent(it)}"} + +fun hasOnlyEntries(vararg entryMatchers: Pair>) = hasOnlyEntries(entryMatchers.toList()) + +fun hasOnlyEntries(entryMatchers: Collection>>) = + allOf( + hasEntrySetSize(entryMatchers.size), + hasEntries(entryMatchers) + ) + +fun hasEntries(vararg entryMatchers: Pair>) = hasEntries(entryMatchers.toList()) + +fun hasEntries(entryMatchers: Collection>>) = object : Matcher> { + override val description = + "is a map containing the entries:\n" + + entryMatchers.asSequence() + .joinToString("\n") { indent("${it.first}: ${it.second.description}") } + + override fun invoke(actual: Map): MatchResult { + val mismatches = entryMatchers.map { hasAnEntry(it.first, it.second)(actual) } + .filterIsInstance() + + return if (mismatches.isEmpty()) { + MatchResult.Match + } else { + MatchResult.Mismatch( + "had entries which did not meet criteria:\n" + + mismatches.joinToString("\n") { indent(it.description) }) + } + } +} + +fun allOf(vararg matchers: Matcher) = allOf(matchers.toList()) + +fun allOf(matchers: Collection>) = object : Matcher { + override val description = + "meets all of the criteria:\n" + + matchers.asSequence() + .joinToString("\n") { indent(it.description) } + + override fun invoke(actual: T) : MatchResult { + val mismatches = matchers.map { it(actual) } + .filterIsInstance() + + return if (mismatches.isEmpty()) { + MatchResult.Match + } else { + MatchResult.Mismatch( + "did not meet criteria:\n" + + mismatches.joinToString("\n") { indent(it.description) }) + } + } +} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/flow/FlowMatchers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/flow/FlowMatchers.kt new file mode 100644 index 0000000000..2e66073fc9 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/flow/FlowMatchers.kt @@ -0,0 +1,38 @@ +package net.corda.testing.internal.matchers.flow + +import com.natpryce.hamkrest.Matcher +import com.natpryce.hamkrest.equalTo +import net.corda.core.internal.FlowStateMachine +import net.corda.testing.internal.matchers.* + +/** + * Matches a Flow that succeeds with a result matched by the given matcher + */ +fun willReturn(): Matcher> = net.corda.testing.internal.matchers.future.willReturn() + .extrude(FlowStateMachine::resultFuture) + .redescribe { "is a flow that will return" } + +fun willReturn(expected: T): Matcher> = willReturn(equalTo(expected)) + +/** + * Matches a Flow that succeeds with a result matched by the given matcher + */ +fun willReturn(successMatcher: Matcher) = net.corda.testing.internal.matchers.future.willReturn(successMatcher) + .extrude(FlowStateMachine::resultFuture) + .redescribe { "is a flow that will return with a value that ${successMatcher.description}" } + +/** + * Matches a Flow that fails, with an exception matched by the given matcher. + */ +inline fun willThrow(failureMatcher: Matcher) = + net.corda.testing.internal.matchers.future.willThrow(failureMatcher) + .extrude(FlowStateMachine<*>::resultFuture) + .redescribe { "is a flow that will fail, throwing an exception that ${failureMatcher.description}" } + +/** + * Matches a Flow that fails, with an exception of the specified type. + */ +inline fun willThrow() = + net.corda.testing.internal.matchers.future.willThrow() + .extrude(FlowStateMachine<*>::resultFuture) + .redescribe { "is a flow that will fail with an exception of type ${E::class.java.simpleName}" } \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/flows/matchers/FutureMatchers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/future/FutureMatchers.kt similarity index 75% rename from core/src/test/kotlin/net/corda/core/flows/matchers/FutureMatchers.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/future/FutureMatchers.kt index dd0edc4747..e1e42108c8 100644 --- a/core/src/test/kotlin/net/corda/core/flows/matchers/FutureMatchers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/future/FutureMatchers.kt @@ -1,9 +1,10 @@ -package net.corda.core.flows.matchers +package net.corda.testing.internal.matchers.future import com.natpryce.hamkrest.MatchResult import com.natpryce.hamkrest.Matcher import com.natpryce.hamkrest.equalTo import net.corda.core.utilities.getOrThrow +import net.corda.testing.internal.matchers.modifyMismatchDescription import java.util.concurrent.Future /** @@ -16,7 +17,7 @@ fun willReturn() = object : Matcher> { actual.getOrThrow() MatchResult.Match } catch (e: Exception) { - MatchResult.Mismatch("Failed with $e") + MatchResult.Mismatch("failed with $e") } } @@ -29,9 +30,9 @@ fun willReturn(successMatcher: Matcher) = object : Matcher> override val description: String = "is a future that will succeed with a value that ${successMatcher.description}" override fun invoke(actual: Future): MatchResult = try { - successMatcher(actual.getOrThrow()) + successMatcher(actual.getOrThrow()).modifyMismatchDescription { "succeeded with value that $it" } } catch (e: Exception) { - MatchResult.Mismatch("Failed with $e") + MatchResult.Mismatch("failed with $e") } } @@ -44,11 +45,11 @@ inline fun willThrow(failureMatcher: Matcher) = object override fun invoke(actual: Future<*>): MatchResult = try { actual.getOrThrow() - MatchResult.Mismatch("Succeeded") + MatchResult.Mismatch("succeeded") } catch (e: Exception) { when(e) { - is E -> failureMatcher(e) - else -> MatchResult.Mismatch("Failure class was ${e.javaClass}") + is E -> failureMatcher(e).modifyMismatchDescription { "failed with ${E::class.java.simpleName} that $it" } + else -> MatchResult.Mismatch("failed with ${e.javaClass}") } } } @@ -62,11 +63,11 @@ inline fun willThrow() = object : Matcher> { override fun invoke(actual: Future<*>): MatchResult = try { actual.getOrThrow() - MatchResult.Mismatch("Succeeded") + MatchResult.Mismatch("succeeded") } catch (e: Exception) { when(e) { is E -> MatchResult.Match - else -> MatchResult.Mismatch("Failure class was ${e.javaClass}") + else -> MatchResult.Mismatch("failed with ${e.javaClass}") } } } \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/rpc/RpcMatchers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/rpc/RpcMatchers.kt new file mode 100644 index 0000000000..a5fb84997a --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/rpc/RpcMatchers.kt @@ -0,0 +1,36 @@ +package net.corda.testing.internal.matchers.rpc + +import com.natpryce.hamkrest.Matcher +import net.corda.core.messaging.FlowHandle +import net.corda.testing.internal.matchers.extrude +import net.corda.testing.internal.matchers.redescribe + +/** + * Matches a flow handle that succeeds with a result matched by the given matcher + */ +fun willReturn() = net.corda.testing.internal.matchers.future.willReturn() + .extrude(FlowHandle::returnValue) + .redescribe { "is an RPG flow handle that will return" } + +/** + * Matches a flow handle that succeeds with a result matched by the given matcher + */ +fun willReturn(successMatcher: Matcher) = net.corda.testing.internal.matchers.future.willReturn(successMatcher) + .extrude(FlowHandle::returnValue) + .redescribe { "is an RPG flow handle that will return a value that ${successMatcher.description}" } + +/** + * Matches a flow handle that fails, with an exception matched by the given matcher. + */ +inline fun willThrow(failureMatcher: Matcher) = + net.corda.testing.internal.matchers.future.willThrow(failureMatcher) + .extrude(FlowHandle<*>::returnValue) + .redescribe { "is an RPG flow handle that will fail with an exception that ${failureMatcher.description}" } + +/** + * Matches a flow handle that fails, with an exception of the specified type. + */ +inline fun willThrow() = + net.corda.testing.internal.matchers.future.willThrow() + .extrude(FlowHandle<*>::returnValue) + .redescribe { "is an RPG flow handle that will fail with an exception of type ${E::class.java.simpleName}" } \ No newline at end of file diff --git a/testing/test-utils/src/test/kotlin/net/corda/testing/internal/MatcherTests.kt b/testing/test-utils/src/test/kotlin/net/corda/testing/internal/MatcherTests.kt new file mode 100644 index 0000000000..b36a5fb6e9 --- /dev/null +++ b/testing/test-utils/src/test/kotlin/net/corda/testing/internal/MatcherTests.kt @@ -0,0 +1,58 @@ +package net.corda.testing.internal + +import com.natpryce.hamkrest.MatchResult +import com.natpryce.hamkrest.equalTo +import net.corda.testing.internal.matchers.hasEntries +import org.junit.Test +import kotlin.test.assertEquals + +class MatcherTests { + @Test + fun `nested items indent`() { + val nestedMap = mapOf( + "a" to mapOf( + "apple" to "vegetable", + "aardvark" to "animal", + "anthracite" to "mineral"), + "b" to mapOf( + "broccoli" to "mineral", + "bison" to "animal", + "bauxite" to "vegetable") + ) + + val matcher = hasEntries( + "a" to hasEntries( + "aardvark" to equalTo("animal"), + "anthracite" to equalTo("mineral") + ), + "b" to hasEntries( + "bison" to equalTo("animal"), + "bauxite" to equalTo("mineral") + ) + ) + + println(matcher.description) + println((matcher(nestedMap) as MatchResult.Mismatch).description) + + assertEquals( + """ + is a map containing the entries: + a: is a map containing the entries: + aardvark: is equal to "animal" + anthracite: is equal to "mineral" + b: is a map containing the entries: + bison: is equal to "animal" + bauxite: is equal to "mineral" + """.trimIndent().replace(" ", "\t"), + matcher.description) + + assertEquals( + """ + had entries which did not meet criteria: + b: had entries which did not meet criteria: + bauxite: was: "vegetable" + """.trimIndent().replace(" ", "\t"), + (matcher(nestedMap) as MatchResult.Mismatch).description + ) + } +} \ No newline at end of file