Merge pull request #3615 from corda/df-reuse-mock-network-example

Reuse mock network, randomising party names to avoid clash
This commit is contained in:
Dominic Fox 2018-07-17 17:10:33 +01:00 committed by GitHub
commit 73d3af504d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 164 additions and 74 deletions

View File

@ -68,6 +68,9 @@ dependencies {
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// Hamkrest, for fluent, composable matchers
testCompile 'com.natpryce:hamkrest:1.4.2.2'
// Quasar, for suspendable fibres. // Quasar, for suspendable fibres.
compileOnly("$quasar_group:quasar-core:$quasar_version:jdk8") { compileOnly("$quasar_group:quasar-core:$quasar_version:jdk8") {
transitive = false transitive = false

View File

@ -1,13 +1,14 @@
package net.corda.core.flows package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.natpryce.hamkrest.*
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.identity.CordaX500Name
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.FetchAttachmentsFlow import net.corda.core.internal.FetchAttachmentsFlow
import net.corda.core.internal.FetchDataFlow import net.corda.core.internal.FetchDataFlow
import net.corda.core.internal.hash import net.corda.core.internal.hash
import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.StartedNode import net.corda.node.internal.StartedNode
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
@ -16,121 +17,86 @@ 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
import net.corda.testing.node.internal.startFlow import net.corda.testing.node.internal.startFlow
import org.junit.After import org.junit.AfterClass
import org.junit.Before
import org.junit.Test import org.junit.Test
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.util.*
import java.util.jar.JarOutputStream import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import kotlin.test.assertEquals import com.natpryce.hamkrest.assertion.assert
import kotlin.test.assertFailsWith import net.corda.core.matchers.*
class AttachmentTests { class AttachmentTests {
lateinit var mockNet: InternalMockNetwork companion object {
val mockNet = InternalMockNetwork()
@Before @JvmStatic
fun setUp() { @AfterClass
mockNet = InternalMockNetwork() fun cleanUp() = mockNet.stopNodes()
} }
@After // Test nodes
fun cleanUp() { private val aliceNode = makeNode(ALICE_NAME)
mockNet.stopNodes() private val bobNode = makeNode(BOB_NAME)
} private val alice = aliceNode.info.singleIdentity()
private fun fakeAttachment(): ByteArray {
val bs = ByteArrayOutputStream()
val js = JarOutputStream(bs)
js.putNextEntry(ZipEntry("file1.txt"))
js.writer().apply { append("Some useful content"); flush() }
js.closeEntry()
js.close()
return bs.toByteArray()
}
@Test @Test
fun `download and store`() { fun `download and store`() {
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
val bobNode = mockNet.createPartyNode(BOB_NAME)
val alice = aliceNode.info.singleIdentity()
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
// Insert an attachment into node zero's store directly. // Insert an attachment into node zero's store directly.
val id = aliceNode.database.transaction { val id = aliceNode.importAttachment(fakeAttachment())
aliceNode.attachments.importAttachment(fakeAttachment().inputStream(), "test", null)
}
// Get node one to run a flow to fetch it and insert it. // Get node one to run a flow to fetch it and insert it.
mockNet.runNetwork() assert.that(
val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice) bobNode.startAttachmentFlow(id, alice),
mockNet.runNetwork() succeedsWith(noAttachments()))
assertEquals(0, bobFlow.resultFuture.getOrThrow().fromDisk.size)
// Verify it was inserted into node one's store. // Verify it was inserted into node one's store.
val attachment = bobNode.database.transaction { val attachment = bobNode.getAttachmentWithId(id)
bobNode.attachments.openAttachment(id)!! assert.that(attachment, hashesTo(id))
}
assertEquals(id, attachment.open().hash())
// Shut down node zero and ensure node one can still resolve the attachment. // Shut down node zero and ensure node one can still resolve the attachment.
aliceNode.dispose() aliceNode.dispose()
val response: FetchDataFlow.Result<Attachment> = bobNode.startAttachmentFlow(setOf(id), alice).resultFuture.getOrThrow() assert.that(
assertEquals(attachment, response.fromDisk[0]) bobNode.startAttachmentFlow(id, alice),
succeedsWith(soleAttachment(attachment)))
} }
@Test @Test
fun missing() { fun missing() {
val aliceNode = mockNet.createPartyNode(ALICE_NAME) val hash: SecureHash = SecureHash.randomSHA256()
val bobNode = mockNet.createPartyNode(BOB_NAME)
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
// Get node one to fetch a non-existent attachment. // Get node one to fetch a non-existent attachment.
val hash = SecureHash.randomSHA256() assert.that(
val alice = aliceNode.info.singleIdentity() bobNode.startAttachmentFlow(hash, alice),
val bobFlow = bobNode.startAttachmentFlow(setOf(hash), alice) failsWith<FetchDataFlow.HashNotFound>(
mockNet.runNetwork() has("requested hash", { it.requested }, equalTo(hash))))
val e = assertFailsWith<FetchDataFlow.HashNotFound> { bobFlow.resultFuture.getOrThrow() }
assertEquals(hash, e.requested)
} }
@Test @Test
fun maliciousResponse() { fun maliciousResponse() {
// Make a node that doesn't do sanity checking at load time. // Make a node that doesn't do sanity checking at load time.
val aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME), nodeFactory = { args -> val badAliceNode = makeBadNode(ALICE_NAME)
object : InternalMockNetwork.MockNode(args) { val badAlice = badAliceNode.info.singleIdentity()
override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false }
}
})
val bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
val alice = aliceNode.info.singleIdentity()
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
val attachment = fakeAttachment()
// Insert an attachment into node zero's store directly. // Insert an attachment into node zero's store directly.
val id = aliceNode.database.transaction { val attachment = fakeAttachment()
aliceNode.attachments.importAttachment(attachment.inputStream(), "test", null) val id = badAliceNode.importAttachment(attachment)
}
// Corrupt its store. // Corrupt its store.
val corruptBytes = "arggghhhh".toByteArray() val corruptBytes = "arggghhhh".toByteArray()
System.arraycopy(corruptBytes, 0, attachment, 0, corruptBytes.size) System.arraycopy(corruptBytes, 0, attachment, 0, corruptBytes.size)
val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment) val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment)
aliceNode.database.transaction { badAliceNode.updateAttachment(corruptAttachment)
session.update(corruptAttachment)
}
// Get n1 to fetch the attachment. Should receive corrupted bytes. // Get n1 to fetch the attachment. Should receive corrupted bytes.
mockNet.runNetwork() assert.that(
val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice) bobNode.startAttachmentFlow(id, badAlice),
mockNet.runNetwork() failsWith<FetchDataFlow.DownloadedVsRequestedDataMismatch>()
assertFailsWith<FetchDataFlow.DownloadedVsRequestedDataMismatch> { bobFlow.resultFuture.getOrThrow() } )
} }
private fun StartedNode<*>.startAttachmentFlow(hashes: Set<SecureHash>, otherSide: Party) = services.startFlow(InitiatingFetchAttachmentsFlow(otherSide, hashes))
@InitiatingFlow @InitiatingFlow
private class InitiatingFetchAttachmentsFlow(val otherSide: Party, val hashes: Set<SecureHash>) : FlowLogic<FetchDataFlow.Result<Attachment>>() { private class InitiatingFetchAttachmentsFlow(val otherSide: Party, val hashes: Set<SecureHash>) : FlowLogic<FetchDataFlow.Result<Attachment>>() {
@Suspendable @Suspendable
@ -145,4 +111,69 @@ class AttachmentTests {
@Suspendable @Suspendable
override fun call() = subFlow(TestDataVendingFlow(otherSideSession)) override fun call() = subFlow(TestDataVendingFlow(otherSideSession))
} }
//region Generators
private fun makeNode(name: CordaX500Name) =
mockNet.createPartyNode(randomiseName(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 = randomiseName(name)),
nodeFactory = { args ->
object : InternalMockNetwork.MockNode(args) {
override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false }
}
}).apply { registerInitiatedFlow(FetchAttachmentsResponse::class.java) }
private fun randomiseName(name: CordaX500Name) = name.copy(commonName = "${name.commonName}_${UUID.randomUUID()}")
private fun fakeAttachment(): ByteArray =
ByteArrayOutputStream().use { baos ->
JarOutputStream(baos).use { jos ->
jos.putNextEntry(ZipEntry("file1.txt"))
jos.writer().apply {
append("Some useful content")
flush()
}
jos.closeEntry()
}
baos.toByteArray()
}
//endregion
//region Operations
private fun StartedNode<*>.importAttachment(attachment: ByteArray) = database.transaction {
attachments.importAttachment(attachment.inputStream(), "test", null)
}.andRunNetwork()
private fun StartedNode<*>.updateAttachment(attachment: NodeAttachmentService.DBAttachment) =
database.transaction { session.update(attachment) }.andRunNetwork()
private fun StartedNode<*>.startAttachmentFlow(hash: SecureHash, otherSide: Party) = services.startFlow(
InitiatingFetchAttachmentsFlow(otherSide, setOf(hash))).andRunNetwork()
private fun StartedNode<*>.getAttachmentWithId(id: SecureHash) = database.transaction {
attachments.openAttachment(id)!!
}
private fun <T : Any> T.andRunNetwork(): T {
mockNet.runNetwork()
return this
}
//endregion
//region Matchers
private fun noAttachments() = has(FetchDataFlow.Result<Attachment>::fromDisk, isEmpty)
private fun soleAttachment(attachment: Attachment) = has(FetchDataFlow.Result<Attachment>::fromDisk,
hasSize(equalTo(1)) and
hasElement(attachment))
private fun hashesTo(hash: SecureHash) = has<Attachment, SecureHash>(
"hash",
{ it.open().hash() },
equalTo(hash))
//endregion
} }

View File

@ -0,0 +1,56 @@
package net.corda.core.matchers
import com.natpryce.hamkrest.MatchResult
import com.natpryce.hamkrest.Matcher
import net.corda.core.internal.FlowStateMachine
import net.corda.core.utilities.getOrThrow
/**
* Matches a Flow that succeeds with a result matched by the given matcher
*/
fun <T> succeedsWith(successMatcher: Matcher<T>) = object : Matcher<FlowStateMachine<T>> {
override val description: String
get() = "A flow that succeeds with ${successMatcher.description}"
override fun invoke(actual: FlowStateMachine<T>): MatchResult = try {
successMatcher(actual.resultFuture.getOrThrow())
} catch (e: Exception) {
MatchResult.Mismatch("Failed with $e")
}
}
/**
* Matches a Flow that fails, with an exception matched by the given matcher.
*/
inline fun <reified E: Exception> failsWith(failureMatcher: Matcher<E>) = object : Matcher<FlowStateMachine<*>> {
override val description: String
get() = "A flow that fails with a ${E::class.java} that ${failureMatcher.description}"
override fun invoke(actual: FlowStateMachine<*>): MatchResult = try {
actual.resultFuture.getOrThrow()
MatchResult.Mismatch("Succeeded")
} catch (e: Exception) {
when(e) {
is E -> failureMatcher(e)
else -> MatchResult.Mismatch("Failure class was ${e.javaClass}")
}
}
}
/**
* Matches a Flow that fails, with an exception of the specified type.
*/
inline fun <reified E: Exception> failsWith() = object : Matcher<FlowStateMachine<*>> {
override val description: String
get() = "A flow that fails with a ${E::class.java}"
override fun invoke(actual: FlowStateMachine<*>): MatchResult = try {
actual.resultFuture.getOrThrow()
MatchResult.Mismatch("Succeeded")
} catch (e: Exception) {
when(e) {
is E -> MatchResult.Match
else -> MatchResult.Mismatch("Failure class was ${e.javaClass}")
}
}
}