* 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
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<Party, AnonymousParty> = 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<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()
assert.that(
aliceNode.services.startFlow(SwapIdentitiesFlow(bob)),
willReturn(
hasOnlyEntries(
alice to allOf(
!equalTo<AbstractParty>(alice),
aliceNode.resolvesToWellKnownParty(alice),
aliceNode.holdsOwningKey(),
!bobNode.holdsOwningKey()
),
bob to allOf(
!equalTo<AbstractParty>(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<SwapIdentitiesException>(
"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<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.
*/
@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<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())
}
}
val anonymousEvilBob = evilBobNode.issueFreshKeyAndCert()
val signature = evilBobNode.signSwapIdentitiesFlowData(evilBob, anonymousEvilBob.owningKey)
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 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 }

View File

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

View File

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

View File

@ -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<Currency> = Amount(1000, USD)) =

View File

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

View File

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

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.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

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 {
return TestStartedNodeImpl(

View File

@ -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"

View File

@ -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.

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.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 <T> willReturn() = object : Matcher<Future<T>> {
actual.getOrThrow()
MatchResult.Match
} 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 fun invoke(actual: Future<out T>): 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 <reified E: Exception> willThrow(failureMatcher: Matcher<E>) = 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 <reified E: Exception> willThrow() = object : Matcher<Future<*>> {
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}")
}
}
}

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