mirror of
https://github.com/corda/corda.git
synced 2025-02-20 09:26:41 +00:00
Matchers (#3716)
* 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:
parent
01d896394a
commit
7182542724
@ -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
|
||||
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)) =
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>())
|
@ -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>())
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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"
|
||||
|
@ -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.
|
||||
|
@ -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) })
|
||||
}
|
||||
}
|
||||
}
|
@ -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}" }
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
@ -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}" }
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user