mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Clarify test intention with generators, operations and matchers
This commit is contained in:
parent
aac4d64b64
commit
851d1fa557
@ -68,6 +68,9 @@ dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$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.
|
||||
compileOnly "$quasar_group:quasar-core:$quasar_version:jdk8"
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.natpryce.hamkrest.*
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
@ -8,7 +9,6 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FetchAttachmentsFlow
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.internal.hash
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
@ -23,8 +23,8 @@ import java.io.ByteArrayOutputStream
|
||||
import java.util.*
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import com.natpryce.hamkrest.assertion.assert
|
||||
import net.corda.core.matchers.*
|
||||
|
||||
class AttachmentTests {
|
||||
companion object {
|
||||
@ -35,103 +35,68 @@ class AttachmentTests {
|
||||
fun cleanUp() = mockNet.stopNodes()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
private fun createAlice() = mockNet.createPartyNode(randomiseName(ALICE_NAME))
|
||||
private fun createBob() = mockNet.createPartyNode(randomiseName(BOB_NAME))
|
||||
private fun randomiseName(name: CordaX500Name) = name.copy(commonName = "${name.commonName}_${UUID.randomUUID()}")
|
||||
// Test nodes
|
||||
private val aliceNode = makeNode(ALICE_NAME)
|
||||
private val bobNode = makeNode(BOB_NAME)
|
||||
private val alice = aliceNode.info.singleIdentity()
|
||||
|
||||
@Test
|
||||
fun `download and store`() {
|
||||
val aliceNode = createAlice()
|
||||
val bobNode = createBob()
|
||||
val alice = aliceNode.info.singleIdentity()
|
||||
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
// Insert an attachment into node zero's store directly.
|
||||
val id = aliceNode.database.transaction {
|
||||
aliceNode.attachments.importAttachment(fakeAttachment().inputStream(), "test", null)
|
||||
}
|
||||
val id = aliceNode.importAttachment(fakeAttachment())
|
||||
|
||||
// Get node one to run a flow to fetch it and insert it.
|
||||
mockNet.runNetwork()
|
||||
val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice)
|
||||
mockNet.runNetwork()
|
||||
assertEquals(0, bobFlow.resultFuture.getOrThrow().fromDisk.size)
|
||||
assert.that(
|
||||
bobNode.startAttachmentFlow(id, alice),
|
||||
succeedsWith(noAttachments()))
|
||||
|
||||
// Verify it was inserted into node one's store.
|
||||
val attachment = bobNode.database.transaction {
|
||||
bobNode.attachments.openAttachment(id)!!
|
||||
}
|
||||
|
||||
assertEquals(id, attachment.open().hash())
|
||||
val attachment = bobNode.getAttachmentWithId(id)
|
||||
assert.that(attachment, hashesTo(id))
|
||||
|
||||
// Shut down node zero and ensure node one can still resolve the attachment.
|
||||
aliceNode.dispose()
|
||||
|
||||
val response: FetchDataFlow.Result<Attachment> = bobNode.startAttachmentFlow(setOf(id), alice).resultFuture.getOrThrow()
|
||||
assertEquals(attachment, response.fromDisk[0])
|
||||
assert.that(
|
||||
bobNode.startAttachmentFlow(id, alice),
|
||||
succeedsWith(soleAttachment(attachment)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun missing() {
|
||||
val aliceNode = createAlice()
|
||||
val bobNode = createBob()
|
||||
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
val hash: SecureHash = SecureHash.randomSHA256()
|
||||
|
||||
// Get node one to fetch a non-existent attachment.
|
||||
val hash = SecureHash.randomSHA256()
|
||||
val alice = aliceNode.info.singleIdentity()
|
||||
val bobFlow = bobNode.startAttachmentFlow(setOf(hash), alice)
|
||||
mockNet.runNetwork()
|
||||
val e = assertFailsWith<FetchDataFlow.HashNotFound> { bobFlow.resultFuture.getOrThrow() }
|
||||
assertEquals(hash, e.requested)
|
||||
assert.that(
|
||||
bobNode.startAttachmentFlow(hash, alice),
|
||||
failsWith<FetchDataFlow.HashNotFound>(
|
||||
has("requested hash", { it.requested }, equalTo(hash))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun maliciousResponse() {
|
||||
// Make a node that doesn't do sanity checking at load time.
|
||||
val aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = randomiseName(ALICE_NAME)), nodeFactory = { args ->
|
||||
object : InternalMockNetwork.MockNode(args) {
|
||||
override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false }
|
||||
}
|
||||
})
|
||||
val bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = randomiseName(BOB_NAME)))
|
||||
val alice = aliceNode.info.singleIdentity()
|
||||
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
val attachment = fakeAttachment()
|
||||
val badAliceNode = makeBadNode(ALICE_NAME)
|
||||
val badAlice = badAliceNode.info.singleIdentity()
|
||||
|
||||
// Insert an attachment into node zero's store directly.
|
||||
val id = aliceNode.database.transaction {
|
||||
aliceNode.attachments.importAttachment(attachment.inputStream(), "test", null)
|
||||
}
|
||||
val attachment = fakeAttachment()
|
||||
val id = badAliceNode.importAttachment(attachment)
|
||||
|
||||
// Corrupt its store.
|
||||
val corruptBytes = "arggghhhh".toByteArray()
|
||||
System.arraycopy(corruptBytes, 0, attachment, 0, corruptBytes.size)
|
||||
|
||||
val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment)
|
||||
aliceNode.database.transaction {
|
||||
session.update(corruptAttachment)
|
||||
}
|
||||
badAliceNode.updateAttachment(corruptAttachment)
|
||||
|
||||
// Get n1 to fetch the attachment. Should receive corrupted bytes.
|
||||
mockNet.runNetwork()
|
||||
val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice)
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<FetchDataFlow.DownloadedVsRequestedDataMismatch> { bobFlow.resultFuture.getOrThrow() }
|
||||
assert.that(
|
||||
bobNode.startAttachmentFlow(id, badAlice),
|
||||
failsWith<FetchDataFlow.DownloadedVsRequestedDataMismatch>()
|
||||
)
|
||||
}
|
||||
|
||||
private fun StartedNode<*>.startAttachmentFlow(hashes: Set<SecureHash>, otherSide: Party) = services.startFlow(InitiatingFetchAttachmentsFlow(otherSide, hashes))
|
||||
|
||||
@InitiatingFlow
|
||||
private class InitiatingFetchAttachmentsFlow(val otherSide: Party, val hashes: Set<SecureHash>) : FlowLogic<FetchDataFlow.Result<Attachment>>() {
|
||||
@Suspendable
|
||||
@ -146,4 +111,69 @@ class AttachmentTests {
|
||||
@Suspendable
|
||||
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…
Reference in New Issue
Block a user