mirror of
https://github.com/corda/corda.git
synced 2025-05-02 16:53:22 +00:00
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:
commit
73d3af504d
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
56
core/src/test/kotlin/net/corda/core/matchers/FlowMatchers.kt
Normal file
56
core/src/test/kotlin/net/corda/core/matchers/FlowMatchers.kt
Normal 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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user