From aac4d64b64340cc8c26ecb126c5ce848f2df80da Mon Sep 17 00:00:00 2001 From: Dominic Fox Date: Mon, 16 Jul 2018 10:52:26 +0100 Subject: [PATCH 1/2] Reuse mock network, randomising party names to avoid clash --- .../net/corda/core/flows/AttachmentTests.kt | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index 1b0974559d..65b360f0e7 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -3,6 +3,7 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash +import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.FetchAttachmentsFlow import net.corda.core.internal.FetchDataFlow @@ -16,26 +17,22 @@ import net.corda.testing.core.singleIdentity import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNodeParameters import net.corda.testing.node.internal.startFlow -import org.junit.After -import org.junit.Before +import org.junit.AfterClass import org.junit.Test 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 class AttachmentTests { - lateinit var mockNet: InternalMockNetwork + companion object { + val mockNet = InternalMockNetwork() - @Before - fun setUp() { - mockNet = InternalMockNetwork() - } - - @After - fun cleanUp() { - mockNet.stopNodes() + @JvmStatic + @AfterClass + fun cleanUp() = mockNet.stopNodes() } private fun fakeAttachment(): ByteArray { @@ -48,10 +45,14 @@ class AttachmentTests { 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 fun `download and store`() { - val aliceNode = mockNet.createPartyNode(ALICE_NAME) - val bobNode = mockNet.createPartyNode(BOB_NAME) + val aliceNode = createAlice() + val bobNode = createBob() val alice = aliceNode.info.singleIdentity() aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java) bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java) @@ -82,8 +83,8 @@ class AttachmentTests { @Test fun missing() { - val aliceNode = mockNet.createPartyNode(ALICE_NAME) - val bobNode = mockNet.createPartyNode(BOB_NAME) + val aliceNode = createAlice() + val bobNode = createBob() aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java) bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java) // Get node one to fetch a non-existent attachment. @@ -98,12 +99,12 @@ class AttachmentTests { @Test fun maliciousResponse() { // Make a node that doesn't do sanity checking at load time. - val aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME), nodeFactory = { args -> + 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 = BOB_NAME)) + val bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = randomiseName(BOB_NAME))) val alice = aliceNode.info.singleIdentity() aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java) bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java) From 851d1fa5574b82bfa41e86881effa78c22c0907f Mon Sep 17 00:00:00 2001 From: Dominic Fox Date: Mon, 16 Jul 2018 17:51:20 +0100 Subject: [PATCH 2/2] Clarify test intention with generators, operations and matchers --- core/build.gradle | 3 + .../net/corda/core/flows/AttachmentTests.kt | 164 +++++++++++------- .../net/corda/core/matchers/FlowMatchers.kt | 56 ++++++ 3 files changed, 156 insertions(+), 67 deletions(-) create mode 100644 core/src/test/kotlin/net/corda/core/matchers/FlowMatchers.kt diff --git a/core/build.gradle b/core/build.gradle index 32d253d978..9998046d71 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -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" diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index 65b360f0e7..1b49150698 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -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 = 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 { bobFlow.resultFuture.getOrThrow() } - assertEquals(hash, e.requested) + assert.that( + bobNode.startAttachmentFlow(hash, alice), + failsWith( + 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 { bobFlow.resultFuture.getOrThrow() } + assert.that( + bobNode.startAttachmentFlow(id, badAlice), + failsWith() + ) } - private fun StartedNode<*>.startAttachmentFlow(hashes: Set, otherSide: Party) = services.startFlow(InitiatingFetchAttachmentsFlow(otherSide, hashes)) - @InitiatingFlow private class InitiatingFetchAttachmentsFlow(val otherSide: Party, val hashes: Set) : FlowLogic>() { @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.andRunNetwork(): T { + mockNet.runNetwork() + return this + } + //endregion + + //region Matchers + private fun noAttachments() = has(FetchDataFlow.Result::fromDisk, isEmpty) + private fun soleAttachment(attachment: Attachment) = has(FetchDataFlow.Result::fromDisk, + hasSize(equalTo(1)) and + hasElement(attachment)) + + private fun hashesTo(hash: SecureHash) = has( + "hash", + { it.open().hash() }, + equalTo(hash)) + //endregion + } diff --git a/core/src/test/kotlin/net/corda/core/matchers/FlowMatchers.kt b/core/src/test/kotlin/net/corda/core/matchers/FlowMatchers.kt new file mode 100644 index 0000000000..261cdf2088 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/matchers/FlowMatchers.kt @@ -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 succeedsWith(successMatcher: Matcher) = object : Matcher> { + override val description: String + get() = "A flow that succeeds with ${successMatcher.description}" + + override fun invoke(actual: FlowStateMachine): 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 failsWith(failureMatcher: Matcher) = object : Matcher> { + 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 failsWith() = object : Matcher> { + 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}") + } + } +} \ No newline at end of file