Clarify test intention with generators, operations and matchers

This commit is contained in:
Dominic Fox 2018-07-16 17:51:20 +01:00
parent aac4d64b64
commit 851d1fa557
3 changed files with 156 additions and 67 deletions

View File

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

View File

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

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