* Move common matches to testing, add some missing ones

* Clarify test logic

* Move common matches to testing, add some missing ones

* Clarify test logic

* Rename 'randomise'

* endregion

* Fix broken unit test
This commit is contained in:
Dominic Fox 2018-08-01 10:18:54 +01:00 committed by GitHub
parent 01d896394a
commit 7182542724
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 403 additions and 180 deletions

View File

@ -1,54 +1,59 @@
package net.corda.confidential 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.identity.*
import net.corda.core.utilities.getOrThrow
import net.corda.testing.core.* 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.InternalMockNetwork
import net.corda.testing.node.internal.startFlow import net.corda.testing.node.internal.startFlow
import org.junit.Before
import org.junit.Test import org.junit.Test
import kotlin.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 { class SwapIdentitiesFlowTests {
private lateinit var mockNet: InternalMockNetwork companion object {
private val mockNet = InternalMockNetwork(networkSendManuallyPumped = false, threadPerNode = true)
@Before @AfterClass
fun setup() { @JvmStatic
// We run this in parallel threads to help catch any race conditions that may exist. fun tearDown() = mockNet.stopNodes()
mockNet = InternalMockNetwork(networkSendManuallyPumped = false, threadPerNode = true)
} }
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 @Test
fun `issue key`() { fun `issue key`() {
// Set up values we'll need assert.that(
val aliceNode = mockNet.createPartyNode(ALICE_NAME) aliceNode.services.startFlow(SwapIdentitiesFlow(bob)),
val bobNode = mockNet.createPartyNode(BOB_NAME) willReturn(
val alice = aliceNode.info.singleIdentity() hasOnlyEntries(
val bob = bobNode.services.myInfo.singleIdentity() alice to allOf(
!equalTo<AbstractParty>(alice),
// Run the flows aliceNode.resolvesToWellKnownParty(alice),
val requesterFlow = aliceNode.services.startFlow(SwapIdentitiesFlow(bob)).resultFuture aliceNode.holdsOwningKey(),
!bobNode.holdsOwningKey()
// Get the results ),
val actual: Map<Party, AnonymousParty> = requesterFlow.getOrThrow().toMap() bob to allOf(
assertEquals(2, actual.size) !equalTo<AbstractParty>(bob),
// Verify that the generated anonymous identities do not match the well known identities bobNode.resolvesToWellKnownParty(bob),
val aliceAnonymousIdentity = actual[alice] ?: throw IllegalStateException() bobNode.holdsOwningKey(),
val bobAnonymousIdentity = actual[bob] ?: throw IllegalStateException() !aliceNode.holdsOwningKey()
assertNotEquals<AbstractParty>(alice, aliceAnonymousIdentity) )
assertNotEquals<AbstractParty>(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()
} }
/** /**
@ -56,58 +61,101 @@ class SwapIdentitiesFlowTests {
*/ */
@Test @Test
fun `verifies identity name`() { fun `verifies identity name`() {
// Set up values we'll need val notBob = charlieNode.issueFreshKeyAndCert()
val aliceNode = mockNet.createPartyNode(ALICE_NAME) val signature = charlieNode.signSwapIdentitiesFlowData(notBob, notBob.owningKey)
val bobNode = mockNet.createPartyNode(BOB_NAME) assertFailsWith<SwapIdentitiesException>(
val charlieNode = mockNet.createPartyNode(CHARLIE_NAME) "Certificate subject must match counterparty's well known identity.") {
val bob: Party = bobNode.services.myInfo.singleIdentity() aliceNode.validateSwapIdentitiesFlow(bob, notBob, signature)
val notBob = charlieNode.database.transaction {
charlieNode.services.keyManagementService.freshKeyAndCert(charlieNode.services.myInfo.singleIdentityAndCert(), false)
} }
val sigData = SwapIdentitiesFlow.buildDataToSign(notBob)
val signature = charlieNode.services.keyManagementService.sign(sigData, notBob.owningKey)
assertFailsWith<SwapIdentitiesException>("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. * Check that flow is actually validating its the signature presented by the counterparty.
*/ */
@Test @Test
fun `verifies signature`() { fun `verification rejects signature if name is right but key is wrong`() {
// Set up values we'll need val evilBobNode = mockNet.createPartyNode(bobNode.info.singleIdentity().name)
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)
val evilBob = evilBobNode.info.singleIdentityAndCert() val evilBob = evilBobNode.info.singleIdentityAndCert()
evilBobNode.database.transaction { val anonymousEvilBob = evilBobNode.issueFreshKeyAndCert()
val anonymousEvilBob = evilBobNode.services.keyManagementService.freshKeyAndCert(evilBob, false) val signature = evilBobNode.signSwapIdentitiesFlowData(evilBob, anonymousEvilBob.owningKey)
val sigData = SwapIdentitiesFlow.buildDataToSign(evilBob)
val signature = evilBobNode.services.keyManagementService.sign(sigData, anonymousEvilBob.owningKey)
assertFailsWith<SwapIdentitiesException>("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<SwapIdentitiesException>("Signature does not match the given identity and nonce.") {
SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob.party, anonymousBob, signature.withoutKey())
}
}
mockNet.stopNodes() assertFailsWith<SwapIdentitiesException>(
"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<SwapIdentitiesException>(
"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<AnonymousParty> {
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<AnonymousParty> {
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<AnonymousParty> {
return copy(negated=!negated)
}
}
private fun TestStartedNode.holdsOwningKey() = HoldsOwningKeyMatcher(this)
//endregion
} }

View File

@ -5,8 +5,8 @@ import com.natpryce.hamkrest.*
import com.natpryce.hamkrest.assertion.assert import com.natpryce.hamkrest.assertion.assert
import net.corda.core.contracts.Attachment import net.corda.core.contracts.Attachment
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.matchers.flow.willReturn import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.core.flows.matchers.flow.willThrow import net.corda.testing.internal.matchers.flow.willThrow
import net.corda.core.flows.mixins.WithMockNet import net.corda.core.flows.mixins.WithMockNet
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party import net.corda.core.identity.Party
@ -16,6 +16,7 @@ import net.corda.core.internal.hash
import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.makeUnique
import net.corda.testing.core.singleIdentity import net.corda.testing.core.singleIdentity
import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.InternalMockNodeParameters import net.corda.testing.node.internal.InternalMockNodeParameters
@ -120,13 +121,13 @@ class AttachmentTests : WithMockNet {
//region Generators //region Generators
override fun makeNode(name: CordaX500Name) = override fun makeNode(name: CordaX500Name) =
mockNet.createPartyNode(randomise(name)).apply { mockNet.createPartyNode(makeUnique(name)).apply {
registerInitiatedFlow(FetchAttachmentsResponse::class.java) registerInitiatedFlow(FetchAttachmentsResponse::class.java)
} }
// Makes a node that doesn't do sanity checking at load time. // Makes a node that doesn't do sanity checking at load time.
private fun makeBadNode(name: CordaX500Name) = mockNet.createNode( private fun makeBadNode(name: CordaX500Name) = mockNet.createNode(
InternalMockNodeParameters(legalName = randomise(name)), InternalMockNodeParameters(legalName = makeUnique(name)),
nodeFactory = { args, _ -> nodeFactory = { args, _ ->
object : InternalMockNetwork.MockNode(args) { object : InternalMockNetwork.MockNode(args) {
override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false } override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false }

View File

@ -5,8 +5,8 @@ import com.natpryce.hamkrest.assertion.assert
import net.corda.core.contracts.Command import net.corda.core.contracts.Command
import net.corda.core.contracts.StateAndContract import net.corda.core.contracts.StateAndContract
import net.corda.core.contracts.requireThat import net.corda.core.contracts.requireThat
import net.corda.core.flows.matchers.flow.willReturn import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.core.flows.matchers.flow.willThrow import net.corda.testing.internal.matchers.flow.willThrow
import net.corda.core.flows.mixins.WithContracts import net.corda.core.flows.mixins.WithContracts
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party import net.corda.core.identity.Party

View File

@ -8,8 +8,8 @@ import com.natpryce.hamkrest.isA
import net.corda.core.CordaRuntimeException import net.corda.core.CordaRuntimeException
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
import net.corda.core.flows.matchers.rpc.willReturn import net.corda.testing.internal.matchers.rpc.willReturn
import net.corda.core.flows.matchers.rpc.willThrow import net.corda.testing.internal.matchers.rpc.willThrow
import net.corda.core.flows.mixins.WithContracts import net.corda.core.flows.mixins.WithContracts
import net.corda.core.flows.mixins.WithFinality import net.corda.core.flows.mixins.WithFinality
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps

View File

@ -3,8 +3,8 @@ package net.corda.core.flows
import com.natpryce.hamkrest.* import com.natpryce.hamkrest.*
import com.natpryce.hamkrest.assertion.assert import com.natpryce.hamkrest.assertion.assert
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.flows.matchers.flow.willReturn import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.core.flows.matchers.flow.willThrow import net.corda.testing.internal.matchers.flow.willThrow
import net.corda.core.flows.mixins.WithContracts import net.corda.core.flows.mixins.WithContracts
import net.corda.core.flows.mixins.WithFinality import net.corda.core.flows.mixins.WithFinality
import net.corda.core.identity.AbstractParty 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. // Party A initiates contract upgrade flow, expected to succeed this time.
assert.that( assert.that(
aliceNode.initiateDummyContractUpgrade(atx), aliceNode.initiateDummyContractUpgrade(atx),
willReturn( willReturn(
aliceNode.hasDummyContractUpgradeTransaction() aliceNode.hasDummyContractUpgradeTransaction()
and bobNode.hasDummyContractUpgradeTransaction())) and bobNode.hasDummyContractUpgradeTransaction()))
} }
private fun TestStartedNode.issueCash(amount: Amount<Currency> = Amount(1000, USD)) = private fun TestStartedNode.issueCash(amount: Amount<Currency> = Amount(1000, USD)) =

View File

@ -2,8 +2,8 @@ package net.corda.core.flows
import com.natpryce.hamkrest.and import com.natpryce.hamkrest.and
import com.natpryce.hamkrest.assertion.assert 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.matchers.flow.willThrow import net.corda.testing.internal.matchers.flow.willThrow
import net.corda.core.flows.mixins.WithFinality import net.corda.core.flows.mixins.WithFinality
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction

View File

@ -2,7 +2,7 @@ package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.natpryce.hamkrest.assertion.assert import com.natpryce.hamkrest.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.flows.mixins.WithMockNet
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.UntrustworthyData

View File

@ -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 <T> willReturn() = has(FlowStateMachine<T>::resultFuture, willReturn())
fun <T> willReturn(expected: T): Matcher<FlowStateMachine<out T?>> = net.corda.core.flows.matchers.flow.willReturn(equalTo(expected))
/**
* Matches a Flow that succeeds with a result matched by the given matcher
*/
fun <T> willReturn(successMatcher: Matcher<T>) = has(
FlowStateMachine<out T>::resultFuture,
willReturn(successMatcher))
/**
* Matches a Flow that fails, with an exception matched by the given matcher.
*/
inline fun <reified E: Exception> willThrow(failureMatcher: Matcher<E>) = has(
FlowStateMachine<*>::resultFuture,
willThrow(failureMatcher))
/**
* Matches a Flow that fails, with an exception of the specified type.
*/
inline fun <reified E: Exception> willThrow() = has(
FlowStateMachine<*>::resultFuture,
willThrow<E>())

View File

@ -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 <T> willReturn() = has(FlowHandle<T>::returnValue, willReturn())
/**
* Matches a flow handle that succeeds with a result matched by the given matcher
*/
fun <T> willReturn(successMatcher: Matcher<T>) = has(FlowHandle<out T>::returnValue, willReturn(successMatcher))
/**
* Matches a flow handle that fails, with an exception matched by the given matcher.
*/
inline fun <reified E: Exception> willThrow(failureMatcher: Matcher<E>) = has(
FlowHandle<*>::returnValue,
willThrow(failureMatcher))
/**
* Matches a flow handle that fails, with an exception of the specified type.
*/
inline fun <reified E: Exception> willThrow() = has(
FlowHandle<*>::returnValue,
willThrow<E>())

View File

@ -9,10 +9,10 @@ import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.FlowStateMachine
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.testing.core.makeUnique
import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.TestStartedNode import net.corda.testing.node.internal.TestStartedNode
import net.corda.testing.node.internal.startFlow import net.corda.testing.node.internal.startFlow
import java.util.*
import kotlin.reflect.KClass import kotlin.reflect.KClass
/** /**
@ -25,12 +25,7 @@ interface WithMockNet {
/** /**
* Create a node using a randomised version of the given name * Create a node using a randomised version of the given name
*/ */
fun makeNode(name: CordaX500Name) = mockNet.createPartyNode(randomise(name)) fun makeNode(name: CordaX500Name) = mockNet.createPartyNode(makeUnique(name))
/**
* Randomise a party name to avoid clashes with other tests
*/
fun randomise(name: CordaX500Name) = name.copy(commonName = "${name.commonName}_${UUID.randomUUID()}")
/** /**
* Run the mock network before proceeding * Run the mock network before proceeding

View File

@ -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 { override fun createStartedNode(nodeInfo: NodeInfo, rpcOps: CordaRPCOps, notaryService: NotaryService?): TestStartedNode {
return TestStartedNodeImpl( return TestStartedNodeImpl(

View File

@ -24,6 +24,7 @@ dependencies {
compile 'com.nhaarman:mockito-kotlin:1.5.0' compile 'com.nhaarman:mockito-kotlin:1.5.0'
compile "org.mockito:mockito-core:$mockito_version" compile "org.mockito:mockito-core:$mockito_version"
compile "org.assertj:assertj-core:$assertj_version" compile "org.assertj:assertj-core:$assertj_version"
compile "com.natpryce:hamkrest:$hamkrest_version"
// Guava: Google test library (collections test suite) // Guava: Google test library (collections test suite)
compile "com.google.guava:guava-testlib:$guava_version" compile "com.google.guava:guava-testlib:$guava_version"

View File

@ -25,6 +25,7 @@ import java.math.BigInteger
import java.security.KeyPair import java.security.KeyPair
import java.security.PublicKey import java.security.PublicKey
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.*
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
/** /**
@ -108,6 +109,18 @@ fun getTestPartyAndCertificate(name: CordaX500Name, publicKey: PublicKey): Party
return getTestPartyAndCertificate(Party(name, publicKey)) 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 * A class that encapsulates a test identity containing a [CordaX500Name] and a [KeyPair], alongside a range
* of utility methods for use during testing. * of utility methods for use during testing.

View File

@ -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<Map<*, *>> {
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 <T> Matcher<T>.redescribe(redescriber: (String) -> String) = object : Matcher<T> {
override val description = redescriber(this@redescribe.description)
override fun invoke(actual: T) = this@redescribe(actual)
}
fun <T> Matcher<T>.redescribeMismatch(redescriber: (String) -> String) = object : Matcher<T> {
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 <O, I> Matcher<I>.extrude(projection: (O) -> I) = object : Matcher<O> {
override val description = this@extrude.description
override fun invoke(actual: O) = this@extrude(projection(actual))
}
internal fun <K, V> hasAnEntry(key: K, valueMatcher: Matcher<V>) = object : Matcher<Map<K, V>> {
override val description = "$key: ${valueMatcher.description}"
override fun invoke(actual: Map<K, V>): 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 <K, V> hasEntry(key: K, valueMatcher: Matcher<V>) =
hasAnEntry(key, valueMatcher).redescribe { "Is a map containing the entry:\n${indent(it)}"}
fun <K, V> hasOnlyEntries(vararg entryMatchers: Pair<K, Matcher<V>>) = hasOnlyEntries(entryMatchers.toList())
fun <K, V> hasOnlyEntries(entryMatchers: Collection<Pair<K, Matcher<V>>>) =
allOf(
hasEntrySetSize(entryMatchers.size),
hasEntries(entryMatchers)
)
fun <K, V> hasEntries(vararg entryMatchers: Pair<K, Matcher<V>>) = hasEntries(entryMatchers.toList())
fun <K, V> hasEntries(entryMatchers: Collection<Pair<K, Matcher<V>>>) = object : Matcher<Map<K, V>> {
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<K, V>): MatchResult {
val mismatches = entryMatchers.map { hasAnEntry(it.first, it.second)(actual) }
.filterIsInstance<MatchResult.Mismatch>()
return if (mismatches.isEmpty()) {
MatchResult.Match
} else {
MatchResult.Mismatch(
"had entries which did not meet criteria:\n" +
mismatches.joinToString("\n") { indent(it.description) })
}
}
}
fun <T> allOf(vararg matchers: Matcher<T>) = allOf(matchers.toList())
fun <T> allOf(matchers: Collection<Matcher<T>>) = object : Matcher<T> {
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<MatchResult.Mismatch>()
return if (mismatches.isEmpty()) {
MatchResult.Match
} else {
MatchResult.Mismatch(
"did not meet criteria:\n" +
mismatches.joinToString("\n") { indent(it.description) })
}
}
}

View File

@ -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 <T> willReturn(): Matcher<FlowStateMachine<T>> = net.corda.testing.internal.matchers.future.willReturn<T>()
.extrude(FlowStateMachine<T>::resultFuture)
.redescribe { "is a flow that will return" }
fun <T> willReturn(expected: T): Matcher<FlowStateMachine<T>> = willReturn(equalTo(expected))
/**
* Matches a Flow that succeeds with a result matched by the given matcher
*/
fun <T> willReturn(successMatcher: Matcher<T>) = net.corda.testing.internal.matchers.future.willReturn(successMatcher)
.extrude(FlowStateMachine<out T>::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 <reified E: Exception> willThrow(failureMatcher: Matcher<E>) =
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 <reified E: Exception> willThrow() =
net.corda.testing.internal.matchers.future.willThrow<E>()
.extrude(FlowStateMachine<*>::resultFuture)
.redescribe { "is a flow that will fail with an exception of type ${E::class.java.simpleName}" }

View File

@ -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.MatchResult
import com.natpryce.hamkrest.Matcher import com.natpryce.hamkrest.Matcher
import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.equalTo
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.testing.internal.matchers.modifyMismatchDescription
import java.util.concurrent.Future import java.util.concurrent.Future
/** /**
@ -16,7 +17,7 @@ fun <T> willReturn() = object : Matcher<Future<T>> {
actual.getOrThrow() actual.getOrThrow()
MatchResult.Match MatchResult.Match
} catch (e: Exception) { } catch (e: Exception) {
MatchResult.Mismatch("Failed with $e") MatchResult.Mismatch("failed with $e")
} }
} }
@ -29,9 +30,9 @@ fun <T> willReturn(successMatcher: Matcher<T>) = object : Matcher<Future<out T>>
override val description: String = "is a future that will succeed with a value that ${successMatcher.description}" override val description: String = "is a future that will succeed with a value that ${successMatcher.description}"
override fun invoke(actual: Future<out T>): MatchResult = try { override fun invoke(actual: Future<out T>): MatchResult = try {
successMatcher(actual.getOrThrow()) successMatcher(actual.getOrThrow()).modifyMismatchDescription { "succeeded with value that $it" }
} catch (e: Exception) { } catch (e: Exception) {
MatchResult.Mismatch("Failed with $e") MatchResult.Mismatch("failed with $e")
} }
} }
@ -44,11 +45,11 @@ inline fun <reified E: Exception> willThrow(failureMatcher: Matcher<E>) = object
override fun invoke(actual: Future<*>): MatchResult = try { override fun invoke(actual: Future<*>): MatchResult = try {
actual.getOrThrow() actual.getOrThrow()
MatchResult.Mismatch("Succeeded") MatchResult.Mismatch("succeeded")
} catch (e: Exception) { } catch (e: Exception) {
when(e) { when(e) {
is E -> failureMatcher(e) is E -> failureMatcher(e).modifyMismatchDescription { "failed with ${E::class.java.simpleName} that $it" }
else -> MatchResult.Mismatch("Failure class was ${e.javaClass}") else -> MatchResult.Mismatch("failed with ${e.javaClass}")
} }
} }
} }
@ -62,11 +63,11 @@ inline fun <reified E: Exception> willThrow() = object : Matcher<Future<*>> {
override fun invoke(actual: Future<*>): MatchResult = try { override fun invoke(actual: Future<*>): MatchResult = try {
actual.getOrThrow() actual.getOrThrow()
MatchResult.Mismatch("Succeeded") MatchResult.Mismatch("succeeded")
} catch (e: Exception) { } catch (e: Exception) {
when(e) { when(e) {
is E -> MatchResult.Match is E -> MatchResult.Match
else -> MatchResult.Mismatch("Failure class was ${e.javaClass}") else -> MatchResult.Mismatch("failed with ${e.javaClass}")
} }
} }
} }

View File

@ -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 <T> willReturn() = net.corda.testing.internal.matchers.future.willReturn<T>()
.extrude(FlowHandle<T>::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 <T> willReturn(successMatcher: Matcher<T>) = net.corda.testing.internal.matchers.future.willReturn(successMatcher)
.extrude(FlowHandle<T>::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 <reified E: Exception> willThrow(failureMatcher: Matcher<E>) =
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 <reified E: Exception> willThrow() =
net.corda.testing.internal.matchers.future.willThrow<E>()
.extrude(FlowHandle<*>::returnValue)
.redescribe { "is an RPG flow handle that will fail with an exception of type ${E::class.java.simpleName}" }

View File

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