mirror of
https://github.com/corda/corda.git
synced 2025-01-10 15:03:02 +00:00
Add simple smoke tests for CordaRPCClient. (#667)
This commit is contained in:
parent
8040c01123
commit
9632ec157f
@ -11,6 +11,9 @@ configurations {
|
|||||||
|
|
||||||
integrationTestCompile.extendsFrom testCompile
|
integrationTestCompile.extendsFrom testCompile
|
||||||
integrationTestRuntime.extendsFrom testRuntime
|
integrationTestRuntime.extendsFrom testRuntime
|
||||||
|
|
||||||
|
smokeTestCompile.extendsFrom compile
|
||||||
|
smokeTestRuntime.extendsFrom runtime
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@ -26,6 +29,24 @@ sourceSets {
|
|||||||
srcDir "../../config/test"
|
srcDir "../../config/test"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
smokeTest {
|
||||||
|
kotlin {
|
||||||
|
// We must NOT have any Node code on the classpath, so do NOT
|
||||||
|
// include the test or integrationTest dependencies here.
|
||||||
|
compileClasspath += main.output
|
||||||
|
runtimeClasspath += main.output
|
||||||
|
srcDir file('src/smoke-test/kotlin')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processSmokeTestResources {
|
||||||
|
from(file("$rootDir/config/test/log4j2.xml")) {
|
||||||
|
rename 'log4j2\\.xml', 'log4j2-test.xml'
|
||||||
|
}
|
||||||
|
from(project(':node:capsule').tasks.buildCordaJAR) {
|
||||||
|
rename 'corda-(.*)', 'corda.jar'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
|
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
|
||||||
@ -42,11 +63,22 @@ dependencies {
|
|||||||
|
|
||||||
testCompile project(':test-utils')
|
testCompile project(':test-utils')
|
||||||
|
|
||||||
// Integration test helpers
|
// Smoke tests do NOT have any Node code on the classpath!
|
||||||
integrationTestCompile "junit:junit:$junit_version"
|
smokeTestCompile project(':finance')
|
||||||
|
smokeTestCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||||
|
smokeTestCompile "org.apache.logging.log4j:log4j-core:$log4j_version"
|
||||||
|
smokeTestCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||||
|
smokeTestCompile "org.assertj:assertj-core:${assertj_version}"
|
||||||
|
smokeTestCompile "junit:junit:$junit_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
task integrationTest(type: Test) {
|
task integrationTest(type: Test) {
|
||||||
testClassesDir = sourceSets.integrationTest.output.classesDir
|
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
task smokeTest(type: Test) {
|
||||||
|
testClassesDir = sourceSets.smokeTest.output.classesDir
|
||||||
|
classpath = sourceSets.smokeTest.runtimeClasspath
|
||||||
|
systemProperties['build.dir'] = buildDir
|
||||||
|
}
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
package net.corda.kotlin.rpc
|
||||||
|
|
||||||
|
import com.typesafe.config.*
|
||||||
|
import net.corda.core.crypto.Party
|
||||||
|
import net.corda.nodeapi.User
|
||||||
|
|
||||||
|
class NodeConfig(
|
||||||
|
val party: Party,
|
||||||
|
val p2pPort: Int,
|
||||||
|
val rpcPort: Int,
|
||||||
|
val webPort: Int,
|
||||||
|
val extraServices: List<String>,
|
||||||
|
val users: List<User>,
|
||||||
|
var networkMap: NodeConfig? = null
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val commonName: String = party.name
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The configuration object depends upon the networkMap,
|
||||||
|
* which is mutable.
|
||||||
|
*/
|
||||||
|
fun toFileConfig(): Config = ConfigFactory.empty()
|
||||||
|
.withValue("myLegalName", valueFor(party.name))
|
||||||
|
.withValue("p2pAddress", addressValueFor(p2pPort))
|
||||||
|
.withValue("extraAdvertisedServiceIds", valueFor(extraServices))
|
||||||
|
.withFallback(optional("networkMapService", networkMap, { c, n ->
|
||||||
|
c.withValue("address", addressValueFor(n.p2pPort))
|
||||||
|
.withValue("legalName", valueFor(n.party.name))
|
||||||
|
}))
|
||||||
|
.withValue("webAddress", addressValueFor(webPort))
|
||||||
|
.withValue("rpcAddress", addressValueFor(rpcPort))
|
||||||
|
.withValue("rpcUsers", valueFor(users.map(User::toMap).toList()))
|
||||||
|
.withValue("useTestClock", valueFor(true))
|
||||||
|
|
||||||
|
fun toText(): String = toFileConfig().root().render(renderOptions)
|
||||||
|
|
||||||
|
private fun <T> valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any)
|
||||||
|
private fun addressValueFor(port: Int) = valueFor("localhost:$port")
|
||||||
|
private inline fun <T> optional(path: String, obj: T?, body: (Config, T) -> Config): Config {
|
||||||
|
val config = ConfigFactory.empty()
|
||||||
|
return if (obj == null) config else body(config, obj).atPath(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun User.toMap(): Map<String, Any> = mapOf(
|
||||||
|
"username" to username,
|
||||||
|
"password" to password,
|
||||||
|
"permissions" to permissions
|
||||||
|
)
|
@ -0,0 +1,100 @@
|
|||||||
|
package net.corda.kotlin.rpc
|
||||||
|
|
||||||
|
import com.google.common.net.HostAndPort
|
||||||
|
import net.corda.client.rpc.CordaRPCClient
|
||||||
|
import net.corda.core.utilities.loggerFor
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URI
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.TimeUnit.SECONDS
|
||||||
|
import kotlin.test.*
|
||||||
|
|
||||||
|
class NodeProcess(
|
||||||
|
val config: NodeConfig,
|
||||||
|
val nodeDir: Path,
|
||||||
|
private val node: Process,
|
||||||
|
private val client: CordaRPCClient
|
||||||
|
) : AutoCloseable {
|
||||||
|
private companion object {
|
||||||
|
val log = loggerFor<NodeProcess>()
|
||||||
|
val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java")
|
||||||
|
val corda: URI = this::class.java.getResource("/corda.jar").toURI()
|
||||||
|
val buildDir: Path = Paths.get(System.getProperty("build.dir"))
|
||||||
|
val capsuleDir: Path = buildDir.resolve("capsule")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun connect(): CordaRPCClient {
|
||||||
|
val user = config.users[0]
|
||||||
|
return client.start(user.username, user.password)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
node.destroy()
|
||||||
|
val isDead = node.waitFor(60, SECONDS)
|
||||||
|
assertTrue(isDead, "Node '${config.commonName}' has not shutdown correctly")
|
||||||
|
|
||||||
|
log.info("Deleting Artemis directories, because they're large!")
|
||||||
|
nodeDir.resolve("artemis").toFile().deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory(val nodesDir: Path) {
|
||||||
|
init {
|
||||||
|
assertTrue(nodesDir.toFile().forceDirectory(), "Nodes directory does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun create(config: NodeConfig): NodeProcess {
|
||||||
|
val nodeDir = Files.createTempDirectory(nodesDir, config.commonName)
|
||||||
|
log.info("Node directory: {}", nodeDir)
|
||||||
|
|
||||||
|
val confFile = nodeDir.resolve("node.conf").toFile()
|
||||||
|
confFile.writeText(config.toText())
|
||||||
|
|
||||||
|
val process = startNode(nodeDir)
|
||||||
|
val client = CordaRPCClient(HostAndPort.fromParts("localhost", config.rpcPort))
|
||||||
|
val user = config.users[0]
|
||||||
|
|
||||||
|
val setupExecutor = Executors.newSingleThreadScheduledExecutor()
|
||||||
|
try {
|
||||||
|
setupExecutor.scheduleWithFixedDelay({
|
||||||
|
try {
|
||||||
|
val conn = client.start(user.username, user.password)
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
// Cancel the "setup" task now that we've created the RPC client.
|
||||||
|
setupExecutor.shutdown()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.warn("Node '{}' not ready yet (Error: {})", config.commonName, e.message)
|
||||||
|
}
|
||||||
|
}, 5, 1, SECONDS)
|
||||||
|
|
||||||
|
val setupOK = setupExecutor.awaitTermination(120, SECONDS)
|
||||||
|
assertTrue(setupOK, "Failed to create RPC connection")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
process.destroy()
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
setupExecutor.shutdownNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
return NodeProcess(config, nodeDir, process, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startNode(nodeDir: Path): Process {
|
||||||
|
val builder = ProcessBuilder()
|
||||||
|
.command(javaPath.toString(), "-jar", corda.path)
|
||||||
|
.directory(nodeDir.toFile())
|
||||||
|
|
||||||
|
builder.environment().putAll(mapOf(
|
||||||
|
"CAPSULE_CACHE_DIR" to capsuleDir.toString()
|
||||||
|
))
|
||||||
|
|
||||||
|
return builder.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun File.forceDirectory(): Boolean = this.isDirectory || this.mkdirs()
|
||||||
|
|
@ -0,0 +1,154 @@
|
|||||||
|
package net.corda.kotlin.rpc
|
||||||
|
|
||||||
|
import net.corda.client.rpc.CordaRPCClient
|
||||||
|
import java.io.FilterInputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.time.Duration.ofSeconds
|
||||||
|
import java.util.Currency
|
||||||
|
import kotlin.test.*
|
||||||
|
import net.corda.client.rpc.notUsed
|
||||||
|
import net.corda.core.contracts.*
|
||||||
|
import net.corda.core.crypto.Party
|
||||||
|
import net.corda.core.getOrThrow
|
||||||
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
|
import net.corda.core.messaging.StateMachineUpdate
|
||||||
|
import net.corda.core.messaging.startFlow
|
||||||
|
import net.corda.core.messaging.startTrackedFlow
|
||||||
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
|
import net.corda.core.sizedInputStreamAndHash
|
||||||
|
import net.corda.core.utilities.DUMMY_NOTARY
|
||||||
|
import net.corda.core.utilities.loggerFor
|
||||||
|
import net.corda.flows.CashIssueFlow
|
||||||
|
import net.corda.nodeapi.User
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class StandaloneCordaRPClientTest {
|
||||||
|
private companion object {
|
||||||
|
val log = loggerFor<StandaloneCordaRPClientTest>()
|
||||||
|
val buildDir: Path = Paths.get(System.getProperty("build.dir"))
|
||||||
|
val nodesDir: Path = buildDir.resolve("nodes")
|
||||||
|
val user = User("user1", "test", permissions = setOf("ALL"))
|
||||||
|
val factory = NodeProcess.Factory(nodesDir)
|
||||||
|
const val attachmentSize = 2116
|
||||||
|
const val timeout = 60L
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var notary: NodeProcess
|
||||||
|
private lateinit var rpcProxy: CordaRPCOps
|
||||||
|
private lateinit var client: CordaRPCClient
|
||||||
|
private lateinit var notaryIdentity: Party
|
||||||
|
|
||||||
|
private val notaryConfig = NodeConfig(
|
||||||
|
party = DUMMY_NOTARY,
|
||||||
|
p2pPort = 10002,
|
||||||
|
rpcPort = 10003,
|
||||||
|
webPort = 10004,
|
||||||
|
extraServices = listOf("corda.notary.validating"),
|
||||||
|
users = listOf(user)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
notary = factory.create(notaryConfig)
|
||||||
|
client = notary.connect()
|
||||||
|
rpcProxy = client.proxy()
|
||||||
|
notaryIdentity = fetchNotaryIdentity()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun done() {
|
||||||
|
client.close()
|
||||||
|
notary.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test attachment upload`() {
|
||||||
|
val attachment = sizedInputStreamAndHash(attachmentSize)
|
||||||
|
|
||||||
|
assertFalse(rpcProxy.attachmentExists(attachment.sha256))
|
||||||
|
val id = WrapperStream(attachment.inputStream).use { rpcProxy.uploadAttachment(it) }
|
||||||
|
assertEquals(id, attachment.sha256, "Attachment has incorrect SHA256 hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test starting flow`() {
|
||||||
|
rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity)
|
||||||
|
.returnValue.getOrThrow(ofSeconds(timeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test starting tracked flow`() {
|
||||||
|
var trackCount = 0
|
||||||
|
val handle = rpcProxy.startTrackedFlow(
|
||||||
|
::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity
|
||||||
|
)
|
||||||
|
handle.progress.subscribe { msg ->
|
||||||
|
log.info("Flow>> $msg")
|
||||||
|
++trackCount
|
||||||
|
}
|
||||||
|
handle.returnValue.getOrThrow(ofSeconds(timeout))
|
||||||
|
assertNotEquals(0, trackCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test network map`() {
|
||||||
|
assertEquals(DUMMY_NOTARY.name, notaryIdentity.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test state machines`() {
|
||||||
|
val (stateMachines, updates) = rpcProxy.stateMachinesAndUpdates()
|
||||||
|
assertEquals(0, stateMachines.size)
|
||||||
|
|
||||||
|
var updateCount = 0
|
||||||
|
updates.subscribe { update ->
|
||||||
|
if (update is StateMachineUpdate.Added) {
|
||||||
|
log.info("StateMachine>> Id=${update.id}")
|
||||||
|
++updateCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now issue some cash
|
||||||
|
rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity)
|
||||||
|
.returnValue.getOrThrow(ofSeconds(timeout))
|
||||||
|
assertEquals(1, updateCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test vault`() {
|
||||||
|
val (vault, vaultUpdates) = rpcProxy.vaultAndUpdates()
|
||||||
|
assertEquals(0, vault.size)
|
||||||
|
|
||||||
|
var updateCount = 0
|
||||||
|
vaultUpdates.subscribe { update ->
|
||||||
|
log.info("Vault>> FlowId=${update.flowId}")
|
||||||
|
++updateCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now issue some cash
|
||||||
|
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity)
|
||||||
|
.returnValue.getOrThrow(ofSeconds(timeout))
|
||||||
|
assertNotEquals(0, updateCount)
|
||||||
|
|
||||||
|
// Check that this cash exists in the vault
|
||||||
|
val cashBalance = rpcProxy.getCashBalances()
|
||||||
|
log.info("Cash Balances: $cashBalance")
|
||||||
|
assertEquals(1, cashBalance.size)
|
||||||
|
assertEquals(629.POUNDS, cashBalance.get(Currency.getInstance("GBP")))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun fetchNotaryIdentity(): Party {
|
||||||
|
val (nodeInfo, nodeUpdates) = rpcProxy.networkMapUpdates()
|
||||||
|
nodeUpdates.notUsed()
|
||||||
|
assertEquals(1, nodeInfo.size)
|
||||||
|
return nodeInfo[0].legalIdentity
|
||||||
|
}
|
||||||
|
|
||||||
|
// This InputStream cannot have been whitelisted.
|
||||||
|
private class WrapperStream(input: InputStream) : FilterInputStream(input)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user