mirror of
https://github.com/corda/corda.git
synced 2024-12-24 07:06:44 +00:00
ENT-11676: Support for testing backwards compatible transactions in the node driver (#7704)
* ENT-11676: Support for testing backwards compatible transactions in the node driver * Introduction of a new way to reference CorDapps for the node driver: `TestCordapp.of(URI)` * New `TestCordapp.asSigned()` method which creates a copy of the CorDapp jar but signed by a dev key. * Added `NodeParameters.legacyContracts` for specifying legacy contract CorDapps for the node `TransactionBuilderDriverTest` has been updated to use these new APIs. * ENT-11676: Support for testing backwards compatible transactions in the node driver * Introduction of a new way to reference CorDapps for the node driver: `TestCordapp.of(URI)` * New `TestCordapp.asSigned()` method which creates a copy of the CorDapp jar but signed by a dev key. * Added `NodeParameters.legacyContracts` for specifying legacy contract CorDapps for the node `TransactionBuilderDriverTest` has been updated to use these new APIs. * ENT-11676: Added removed api and fixed alias issue. --------- Co-authored-by: Adel El-Beik <adel.el-beik@r3.com>
This commit is contained in:
parent
0f713aaa44
commit
18e5f7d68f
@ -4,7 +4,6 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.internal.copyToDirectory
|
||||
import net.corda.core.internal.hash
|
||||
import net.corda.core.internal.mapToSet
|
||||
import net.corda.core.internal.toPath
|
||||
@ -23,30 +22,26 @@ import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||
import net.corda.finance.issuedBy
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar
|
||||
import net.corda.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.NodeParameters
|
||||
import net.corda.testing.node.TestCordapp
|
||||
import net.corda.testing.node.internal.DriverDSLImpl
|
||||
import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP
|
||||
import net.corda.testing.node.internal.TestCordappInternal
|
||||
import net.corda.testing.node.internal.UriTestCordapp
|
||||
import net.corda.testing.node.internal.enclosedCordapp
|
||||
import net.corda.testing.node.internal.internalDriver
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.copyTo
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.div
|
||||
import kotlin.io.path.inputStream
|
||||
import kotlin.io.path.isRegularFile
|
||||
import kotlin.io.path.moveTo
|
||||
@ -57,30 +52,16 @@ class TransactionBuilderDriverTest {
|
||||
val legacyFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts-4.11.jar")!!.toPath()
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val tempFolder = TemporaryFolder()
|
||||
|
||||
@Before
|
||||
fun initJarSigner() {
|
||||
tempFolder.root.toPath().generateKey("testAlias", "testPassword", ALICE_NAME.toString())
|
||||
}
|
||||
|
||||
private fun signJar(jar: Path) {
|
||||
tempFolder.root.toPath().signJar(jar.absolutePathString(), "testAlias", "testPassword")
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `adds CorDapp dependencies`() {
|
||||
internalDriver(cordappsForAllNodes = listOf(FINANCE_WORKFLOWS_CORDAPP), startNodesInProcess = false) {
|
||||
val (cordapp, dependency) = splitFinanceContractCordapp(currentFinanceContractsJar)
|
||||
|
||||
cordapp.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
|
||||
dependency.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
|
||||
cordapp.jarFile.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
|
||||
dependency.jarFile.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
|
||||
|
||||
// Start the node with the CorDapp but without the dependency
|
||||
cordapp.copyToDirectory((baseDirectory(ALICE_NAME) / "cordapps").createDirectories())
|
||||
val node = startNode(NodeParameters(ALICE_NAME)).getOrThrow()
|
||||
val node = startNode(NodeParameters(ALICE_NAME, additionalCordapps = listOf(cordapp))).getOrThrow()
|
||||
|
||||
// First make sure the missing dependency causes an issue
|
||||
assertThatThrownBy {
|
||||
@ -88,10 +69,10 @@ class TransactionBuilderDriverTest {
|
||||
}.hasMessageContaining("Transaction being built has a missing attachment for class net/corda/finance/contracts/asset/")
|
||||
|
||||
// Upload the missing dependency
|
||||
dependency.inputStream().use(node.rpc::uploadAttachment)
|
||||
dependency.jarFile.inputStream().use(node.rpc::uploadAttachment)
|
||||
|
||||
val stx = createTransaction(node)
|
||||
assertThat(stx.tx.attachments).contains(cordapp.hash, dependency.hash)
|
||||
assertThat(stx.tx.attachments).contains(cordapp.jarFile.hash, dependency.jarFile.hash)
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,17 +84,16 @@ class TransactionBuilderDriverTest {
|
||||
networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
|
||||
) {
|
||||
val (legacyContracts, legacyDependency) = splitFinanceContractCordapp(legacyFinanceContractsJar)
|
||||
// Re-sign the current finance contracts CorDapp with the same key as the split legacy CorDapp
|
||||
val currentContracts = currentFinanceContractsJar.copyTo(Path("${currentFinanceContractsJar.toString().substringBeforeLast(".")}-RESIGNED.jar"), overwrite = true)
|
||||
currentContracts.unsignJar()
|
||||
signJar(currentContracts)
|
||||
val currentContracts = TestCordapp.of(currentFinanceContractsJar.toUri()).asSigned() as TestCordappInternal
|
||||
|
||||
currentContracts.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
|
||||
currentContracts.jarFile.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
|
||||
|
||||
// Start the node with the legacy CorDapp but without the dependency
|
||||
legacyContracts.copyToDirectory((baseDirectory(ALICE_NAME) / "legacy-contracts").createDirectories())
|
||||
currentContracts.copyToDirectory((baseDirectory(ALICE_NAME) / "cordapps").createDirectories())
|
||||
val node = startNode(NodeParameters(ALICE_NAME)).getOrThrow()
|
||||
val node = startNode(NodeParameters(
|
||||
ALICE_NAME,
|
||||
additionalCordapps = listOf(currentContracts),
|
||||
legacyContracts = listOf(legacyContracts)
|
||||
)).getOrThrow()
|
||||
|
||||
// First make sure the missing dependency causes an issue
|
||||
assertThatThrownBy {
|
||||
@ -121,10 +101,10 @@ class TransactionBuilderDriverTest {
|
||||
}.hasMessageContaining("Transaction being built has a missing legacy attachment for class net/corda/finance/contracts/asset/")
|
||||
|
||||
// Upload the missing dependency
|
||||
legacyDependency.inputStream().use(node.rpc::uploadAttachment)
|
||||
legacyDependency.jarFile.inputStream().use(node.rpc::uploadAttachment)
|
||||
|
||||
val stx = createTransaction(node)
|
||||
assertThat(stx.tx.legacyAttachments).contains(legacyContracts.hash, legacyDependency.hash)
|
||||
assertThat(stx.tx.legacyAttachments).contains(legacyContracts.jarFile.hash, legacyDependency.jarFile.hash)
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,16 +119,16 @@ class TransactionBuilderDriverTest {
|
||||
val (currentCashContract, currentCpContract) = splitJar(currentFinanceContractsJar) { "CommercialPaper" in it.absolutePathString() }
|
||||
val (legacyCashContract, _) = splitJar(legacyFinanceContractsJar) { "CommercialPaper" in it.absolutePathString() }
|
||||
|
||||
currentCashContract.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
|
||||
currentCpContract.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
|
||||
currentCashContract.jarFile.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
|
||||
currentCpContract.jarFile.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
|
||||
|
||||
// The node has the legacy CommericalPaper contract missing
|
||||
val cordappsDir = (baseDirectory(ALICE_NAME) / "cordapps").createDirectories()
|
||||
currentCashContract.copyToDirectory(cordappsDir)
|
||||
currentCpContract.copyToDirectory(cordappsDir)
|
||||
legacyCashContract.copyToDirectory((baseDirectory(ALICE_NAME) / "legacy-contracts").createDirectories())
|
||||
val node = startNode(NodeParameters(
|
||||
ALICE_NAME,
|
||||
additionalCordapps = listOf(currentCashContract, currentCpContract),
|
||||
legacyContracts = listOf(legacyCashContract)
|
||||
)).getOrThrow()
|
||||
|
||||
val node = startNode(NodeParameters(ALICE_NAME)).getOrThrow()
|
||||
assertThatThrownBy { node.rpc.startFlow(::TwoContractTransactionFlow).returnValue.getOrThrow() }
|
||||
.hasMessageContaining("Transaction being built has a missing legacy attachment")
|
||||
.hasMessageContaining("CommercialPaper")
|
||||
@ -158,11 +138,11 @@ class TransactionBuilderDriverTest {
|
||||
/**
|
||||
* Split the given finance contracts jar into two such that the second jar becomes a dependency to the first.
|
||||
*/
|
||||
private fun DriverDSLImpl.splitFinanceContractCordapp(contractsJar: Path): Pair<Path, Path> {
|
||||
private fun DriverDSLImpl.splitFinanceContractCordapp(contractsJar: Path): Pair<UriTestCordapp, UriTestCordapp> {
|
||||
return splitJar(contractsJar) { it.absolutePathString() == "/net/corda/finance/contracts/asset/CashUtilities.class" }
|
||||
}
|
||||
|
||||
private fun DriverDSLImpl.splitJar(path: Path, move: (Path) -> Boolean): Pair<Path, Path> {
|
||||
private fun DriverDSLImpl.splitJar(path: Path, move: (Path) -> Boolean): Pair<UriTestCordapp, UriTestCordapp> {
|
||||
val jar1 = Files.createTempFile(driverDirectory, "jar1-", ".jar")
|
||||
val jar2 = Files.createTempFile(driverDirectory, "jar2-", ".jar")
|
||||
|
||||
@ -181,10 +161,10 @@ class TransactionBuilderDriverTest {
|
||||
}
|
||||
jar1.unsignJar()
|
||||
|
||||
signJar(jar1)
|
||||
signJar(jar2)
|
||||
|
||||
return Pair(jar1, jar2)
|
||||
return Pair(
|
||||
TestCordapp.of(jar1.toUri()).asSigned() as UriTestCordapp,
|
||||
TestCordapp.of(jar2.toUri()).asSigned() as UriTestCordapp
|
||||
)
|
||||
}
|
||||
|
||||
private fun DriverDSLImpl.createTransaction(node: NodeHandle): SignedTransaction {
|
||||
|
@ -12,6 +12,7 @@ import java.nio.file.Path
|
||||
import java.util.jar.Attributes
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.jar.Manifest
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.fileSize
|
||||
import kotlin.io.path.inputStream
|
||||
import kotlin.io.path.outputStream
|
||||
@ -36,9 +37,10 @@ inline fun <T> Path.useZipFile(block: (FileSystem) -> T): T {
|
||||
return FileSystems.newFileSystem(this).use(block)
|
||||
}
|
||||
|
||||
inline fun <T> Path.modifyJarManifest(block: (Manifest) -> T): T {
|
||||
inline fun <T> Path.modifyJarManifest(block: (Manifest) -> T): T? {
|
||||
return useZipFile { zipFs ->
|
||||
val manifestFile = zipFs.getPath("META-INF", "MANIFEST.MF")
|
||||
if (!manifestFile.exists()) return null
|
||||
val manifest = manifestFile.inputStream().use(::Manifest)
|
||||
val result = block(manifest)
|
||||
manifestFile.outputStream().use(manifest::write)
|
||||
|
@ -21,6 +21,7 @@ import java.util.jar.JarOutputStream
|
||||
import java.util.jar.Manifest
|
||||
import kotlin.io.path.deleteExisting
|
||||
import kotlin.io.path.div
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@ -75,7 +76,7 @@ object JarSignatureTestUtils {
|
||||
fun Path.unsignJar() {
|
||||
// Remove the signatures
|
||||
useZipFile { zipFs ->
|
||||
zipFs.getPath("META-INF").listDirectoryEntries("*.{SF,DSA,RSA,EC}").forEach(Path::deleteExisting)
|
||||
zipFs.getPath("META-INF").takeIf { it.exists() }?.listDirectoryEntries("*.{SF,DSA,RSA,EC}")?.forEach(Path::deleteExisting)
|
||||
}
|
||||
// Remove all the hash information of the jar contents
|
||||
modifyJarManifest { manifest ->
|
||||
|
@ -25,7 +25,7 @@ import net.corda.testing.node.User
|
||||
* log level argument.
|
||||
* @property rpcAddress optional override for RPC address on which node will be accepting RPC connections from the clients. Port provided must be vacant.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
@Suppress("unused", "TooManyFunctions")
|
||||
data class NodeParameters(
|
||||
val providedName: CordaX500Name? = null,
|
||||
val rpcUsers: List<User> = emptyList(),
|
||||
@ -37,7 +37,8 @@ data class NodeParameters(
|
||||
val flowOverrides: Map<out Class<out FlowLogic<*>>, Class<out FlowLogic<*>>> = emptyMap(),
|
||||
val logLevelOverride: String? = null,
|
||||
val rpcAddress: NetworkHostAndPort? = null,
|
||||
val systemProperties: Map<String, String> = emptyMap()
|
||||
val systemProperties: Map<String, String> = emptyMap(),
|
||||
val legacyContracts: Collection<TestCordapp> = emptySet()
|
||||
) {
|
||||
/**
|
||||
* Create a new node parameters object with default values. Each parameter can be specified with its wither method which returns a copy
|
||||
@ -54,6 +55,9 @@ data class NodeParameters(
|
||||
fun withAdditionalCordapps(additionalCordapps: Set<TestCordapp>): NodeParameters = copy(additionalCordapps = additionalCordapps)
|
||||
fun withFlowOverrides(flowOverrides: Map<Class<out FlowLogic<*>>, Class<out FlowLogic<*>>>): NodeParameters = copy(flowOverrides = flowOverrides)
|
||||
fun withLogLevelOverride(logLevelOverride: String?): NodeParameters = copy(logLevelOverride = logLevelOverride)
|
||||
fun withRpcAddress(rpcAddress: NetworkHostAndPort?): NodeParameters = copy(rpcAddress = rpcAddress)
|
||||
fun withSystemProperties(systemProperties: Map<String, String>): NodeParameters = copy(systemProperties = systemProperties)
|
||||
fun withLegacyContracts(legacyContracts: Collection<TestCordapp>): NodeParameters = copy(legacyContracts = legacyContracts)
|
||||
|
||||
constructor(
|
||||
providedName: CordaX500Name?,
|
||||
@ -221,4 +225,58 @@ data class NodeParameters(
|
||||
logLevelOverride = logLevelOverride,
|
||||
rpcAddress = rpcAddress,
|
||||
systemProperties = systemProperties)
|
||||
|
||||
constructor(
|
||||
providedName: CordaX500Name?,
|
||||
rpcUsers: List<User>,
|
||||
verifierType: VerifierType,
|
||||
customOverrides: Map<String, Any?>,
|
||||
startInSameProcess: Boolean?,
|
||||
maximumHeapSize: String,
|
||||
additionalCordapps: Collection<TestCordapp> = emptySet(),
|
||||
flowOverrides: Map<out Class<out FlowLogic<*>>, Class<out FlowLogic<*>>>,
|
||||
logLevelOverride: String? = null,
|
||||
rpcAddress: NetworkHostAndPort? = null,
|
||||
systemProperties: Map<String, String> = emptyMap()
|
||||
) : this(
|
||||
providedName,
|
||||
rpcUsers,
|
||||
verifierType,
|
||||
customOverrides,
|
||||
startInSameProcess,
|
||||
maximumHeapSize,
|
||||
additionalCordapps,
|
||||
flowOverrides,
|
||||
logLevelOverride,
|
||||
rpcAddress,
|
||||
systemProperties,
|
||||
legacyContracts = emptySet())
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun copy(
|
||||
providedName: CordaX500Name?,
|
||||
rpcUsers: List<User>,
|
||||
verifierType: VerifierType,
|
||||
customOverrides: Map<String, Any?>,
|
||||
startInSameProcess: Boolean?,
|
||||
maximumHeapSize: String,
|
||||
additionalCordapps: Collection<TestCordapp> = emptySet(),
|
||||
flowOverrides: Map<out Class<out FlowLogic<*>>, Class<out FlowLogic<*>>>,
|
||||
logLevelOverride: String? = null,
|
||||
rpcAddress: NetworkHostAndPort? = null,
|
||||
systemProperties: Map<String, String> = emptyMap()
|
||||
) = this.copy(
|
||||
providedName = providedName,
|
||||
rpcUsers = rpcUsers,
|
||||
verifierType = verifierType,
|
||||
customOverrides = customOverrides,
|
||||
startInSameProcess = startInSameProcess,
|
||||
maximumHeapSize = maximumHeapSize,
|
||||
additionalCordapps = additionalCordapps,
|
||||
flowOverrides = flowOverrides,
|
||||
logLevelOverride = logLevelOverride,
|
||||
rpcAddress = rpcAddress,
|
||||
systemProperties = systemProperties,
|
||||
legacyContracts = legacyContracts)
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,10 @@ package net.corda.testing.node
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.NodeParameters
|
||||
import net.corda.testing.node.internal.TestCordappImpl
|
||||
import net.corda.testing.node.internal.ScanPackageTestCordapp
|
||||
import net.corda.testing.node.internal.UriTestCordapp
|
||||
import java.net.URI
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Encapsulates a CorDapp that exists on the current classpath, which can be pulled in for testing. Use [TestCordapp.findCordapp]
|
||||
@ -25,6 +28,12 @@ abstract class TestCordapp {
|
||||
/** Returns a copy of this [TestCordapp] but with the specified CorDapp config. */
|
||||
abstract fun withConfig(config: Map<String, Any>): TestCordapp
|
||||
|
||||
/**
|
||||
* Returns a copy of this [TestCordapp] signed with a development signing key. The same signing key will be used for all signed
|
||||
* [TestCordapp]s. If the CorDapp jar is already signed, then the new jar created will its signing key replaced by the development key.
|
||||
*/
|
||||
abstract fun asSigned(): TestCordapp
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Scans the current classpath to find the CorDapp that contains the given package. All the CorDapp's metdata present in its
|
||||
@ -34,6 +43,14 @@ abstract class TestCordapp {
|
||||
* @param scanPackage The package name used to find the CorDapp. This does not need to be the root package of the CorDapp.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun findCordapp(scanPackage: String): TestCordapp = TestCordappImpl(scanPackage = scanPackage, config = emptyMap())
|
||||
fun findCordapp(scanPackage: String): TestCordapp = ScanPackageTestCordapp(scanPackage)
|
||||
|
||||
/**
|
||||
* [URI] location to a CorDapp jar. This may be a path on the local file system or a URL to an external resource.
|
||||
*
|
||||
* A [Path] can be converted into a [URI] with [Path.toUri].
|
||||
*/
|
||||
@JvmStatic
|
||||
fun of(uri: URI): TestCordapp = UriTestCordapp(uri)
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,6 @@ import net.corda.core.node.services.AttachmentFixup
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.containsKey
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.attribute.FileTime
|
||||
@ -49,6 +46,8 @@ data class CustomCordapp(
|
||||
|
||||
override fun withOnlyJarContents(): CustomCordapp = CustomCordapp(packages = packages, classes = classes, fixups = fixups)
|
||||
|
||||
override fun asSigned(): CustomCordapp = signed()
|
||||
|
||||
fun signed(keyStorePath: Path? = null, numberOfSignatures: Int = 1, keyAlgorithm: String = "RSA"): CustomCordapp =
|
||||
copy(signingInfo = SigningInfo(keyStorePath, numberOfSignatures, keyAlgorithm))
|
||||
|
||||
@ -114,23 +113,6 @@ data class CustomCordapp(
|
||||
}
|
||||
}
|
||||
|
||||
private fun signJar(jarFile: Path) {
|
||||
if (signingInfo != null) {
|
||||
val keyStorePathToUse = signingInfo.keyStorePath ?: defaultJarSignerDirectory.createDirectories()
|
||||
for (i in 1 .. signingInfo.numberOfSignatures) {
|
||||
val alias = "alias$i"
|
||||
val pwd = "secret!"
|
||||
if (!keyStorePathToUse.containsKey(alias, pwd)) {
|
||||
keyStorePathToUse.generateKey(alias, pwd, "O=Test Company Ltd $i,OU=Test,L=London,C=GB", signingInfo.keyAlgorithm)
|
||||
}
|
||||
val pk = keyStorePathToUse.signJar(jarFile.toString(), alias, pwd)
|
||||
logger.debug { "Signed Jar: $jarFile with public key $pk" }
|
||||
}
|
||||
} else {
|
||||
logger.debug { "Unsigned Jar: $jarFile" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTestManifest(name: String, versionId: Int, targetPlatformVersion: Int): Manifest {
|
||||
val manifest = Manifest()
|
||||
|
||||
@ -160,13 +142,12 @@ data class CustomCordapp(
|
||||
}
|
||||
}
|
||||
|
||||
data class SigningInfo(val keyStorePath: Path?, val numberOfSignatures: Int, val keyAlgorithm: String)
|
||||
data class SigningInfo(val keyStorePath: Path?, val signatureCount: Int, val algorithm: String)
|
||||
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
private val epochFileTime = FileTime.from(Instant.EPOCH)
|
||||
private val cordappsDirectory: Path
|
||||
private val defaultJarSignerDirectory: Path
|
||||
private val whitespace = "\\s++".toRegex()
|
||||
private val cache = ConcurrentHashMap<CustomCordapp, Path>()
|
||||
|
||||
@ -174,7 +155,6 @@ data class CustomCordapp(
|
||||
val buildDir = Paths.get("build").toAbsolutePath()
|
||||
val timeDirName = getTimestampAsDirectoryName()
|
||||
cordappsDirectory = buildDir / "generated-custom-cordapps" / timeDirName
|
||||
defaultJarSignerDirectory = buildDir / "jar-signer" / timeDirName
|
||||
}
|
||||
|
||||
fun getJarFile(cordapp: CustomCordapp): Path {
|
||||
@ -187,7 +167,9 @@ data class CustomCordapp(
|
||||
} else if (it.packages.isNotEmpty() || it.classes.isNotEmpty()) {
|
||||
it.packageAsJar(jarFile)
|
||||
}
|
||||
it.signJar(jarFile)
|
||||
if (it.signingInfo != null) {
|
||||
TestCordappSigner.signJar(jarFile, it.signingInfo.keyStorePath, it.signingInfo.signatureCount, it.signingInfo.algorithm)
|
||||
}
|
||||
logger.debug { "$it packaged into $jarFile" }
|
||||
jarFile
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import net.corda.core.internal.concurrent.fork
|
||||
import net.corda.core.internal.concurrent.map
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.internal.copyToDirectory
|
||||
import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_LICENCE
|
||||
import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_NAME
|
||||
import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_VENDOR
|
||||
@ -55,6 +56,7 @@ import net.corda.node.internal.DataSourceFactory
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.internal.NodeWithInfo
|
||||
import net.corda.node.internal.clientSslOptionsCompatibleWith
|
||||
import net.corda.node.internal.cordapp.JarScanningCordappLoader.Companion.LEGACY_CONTRACTS_DIR_NAME
|
||||
import net.corda.node.services.Permissions
|
||||
import net.corda.node.services.config.ConfigHelper
|
||||
import net.corda.node.services.config.FlowOverride
|
||||
@ -716,6 +718,11 @@ class DriverDSLImpl(
|
||||
extraCustomCordapps + (cordappsForAllNodes ?: emptySet())
|
||||
)
|
||||
|
||||
if (parameters.legacyContracts.isNotEmpty()) {
|
||||
val legacyContractsDir = (baseDirectory / LEGACY_CONTRACTS_DIR_NAME).createDirectories()
|
||||
parameters.legacyContracts.forEach { (it as TestCordappInternal).jarFile.copyToDirectory(legacyContractsDir) }
|
||||
}
|
||||
|
||||
val nodeFuture = if (parameters.startInSameProcess ?: startNodesInProcess) {
|
||||
val nodeAndThreadFuture = startInProcessNode(executorService, config, allowHibernateToManageAppSchema)
|
||||
shutdownManager.registerShutdown(
|
||||
|
@ -61,7 +61,7 @@ private val log = LoggerFactory.getLogger("net.corda.testing.internal.InternalTe
|
||||
* You will probably need to use [FINANCE_CORDAPPS] instead to get access to the flows as well.
|
||||
*/
|
||||
@JvmField
|
||||
val FINANCE_CONTRACTS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.contracts")
|
||||
val FINANCE_CONTRACTS_CORDAPP: ScanPackageTestCordapp = findCordapp("net.corda.finance.contracts")
|
||||
|
||||
/**
|
||||
* Reference to the finance-workflows CorDapp in this repo. The metadata is taken directly from finance/workflows/build.gradle, including the
|
||||
@ -70,10 +70,10 @@ val FINANCE_CONTRACTS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.
|
||||
* You will probably need to use [FINANCE_CORDAPPS] instead to get access to the contract classes as well.
|
||||
*/
|
||||
@JvmField
|
||||
val FINANCE_WORKFLOWS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.workflows")
|
||||
val FINANCE_WORKFLOWS_CORDAPP: ScanPackageTestCordapp = findCordapp("net.corda.finance.workflows")
|
||||
|
||||
@JvmField
|
||||
val FINANCE_CORDAPPS: Set<TestCordappImpl> = setOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP)
|
||||
val FINANCE_CORDAPPS: Set<ScanPackageTestCordapp> = setOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP)
|
||||
|
||||
/**
|
||||
* *Custom* CorDapp containing the contents of the `net.corda.testing.contracts` package, i.e. the dummy contracts. This is not a real CorDapp
|
||||
@ -105,9 +105,9 @@ fun cordappWithFixups(fixups: List<AttachmentFixup>) = CustomCordapp(fixups = fi
|
||||
|
||||
/**
|
||||
* Find the single CorDapp jar on the current classpath which contains the given package. This is a convenience method for
|
||||
* [TestCordapp.findCordapp] but returns the internal [TestCordappImpl].
|
||||
* [TestCordapp.findCordapp] but returns the internal [ScanPackageTestCordapp].
|
||||
*/
|
||||
fun findCordapp(scanPackage: String): TestCordappImpl = TestCordapp.findCordapp(scanPackage) as TestCordappImpl
|
||||
fun findCordapp(scanPackage: String): ScanPackageTestCordapp = TestCordapp.findCordapp(scanPackage) as ScanPackageTestCordapp
|
||||
|
||||
/** Create a *custom* CorDapp which just contains the enclosed classes of the receiver class. */
|
||||
fun Any.enclosedCordapp(): CustomCordapp {
|
||||
|
@ -2,6 +2,7 @@ package net.corda.testing.node.internal
|
||||
|
||||
import io.github.classgraph.ClassGraph
|
||||
import net.corda.core.internal.attributes
|
||||
import net.corda.core.internal.mapToSet
|
||||
import net.corda.core.internal.pooledScan
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.testing.node.TestCordapp
|
||||
@ -23,39 +24,43 @@ import kotlin.io.path.useDirectoryEntries
|
||||
* the [scanPackage] may reference a gradle CorDapp project on the local system. In this scenerio the project's "jar" task is executed to
|
||||
* build the CorDapp jar. This allows us to inherit the CorDapp's MANIFEST information without having to do any extra processing.
|
||||
*/
|
||||
data class TestCordappImpl(val scanPackage: String, override val config: Map<String, Any>) : TestCordappInternal() {
|
||||
override fun withConfig(config: Map<String, Any>): TestCordappImpl = copy(config = config)
|
||||
data class ScanPackageTestCordapp(val scanPackage: String,
|
||||
override val config: Map<String, Any> = emptyMap(),
|
||||
val signed: Boolean = false) : TestCordappInternal() {
|
||||
override fun withConfig(config: Map<String, Any>): ScanPackageTestCordapp = copy(config = config)
|
||||
|
||||
override fun withOnlyJarContents(): TestCordappImpl = copy(config = emptyMap())
|
||||
override fun asSigned(): TestCordapp = copy(signed = true)
|
||||
|
||||
override val jarFile: Path
|
||||
get() {
|
||||
val jars = findJars(scanPackage)
|
||||
when (jars.size) {
|
||||
0 -> throw IllegalArgumentException("There are no CorDapps containing the package $scanPackage on the classpath. Make sure " +
|
||||
"the package name is correct and that the CorDapp is added as a gradle dependency.")
|
||||
1 -> return jars.first()
|
||||
else -> throw IllegalArgumentException("There is more than one CorDapp containing the package $scanPackage on the classpath " +
|
||||
"$jars. Specify a package name which is unique to the CorDapp.")
|
||||
}
|
||||
override fun withOnlyJarContents(): ScanPackageTestCordapp = copy(config = emptyMap(), signed = false)
|
||||
|
||||
override val jarFile: Path by lazy {
|
||||
val jars = findJars()
|
||||
val jar = when (jars.size) {
|
||||
0 -> throw IllegalArgumentException("There are no CorDapps containing the package $scanPackage on the classpath. Make sure " +
|
||||
"the package name is correct and that the CorDapp is added as a gradle dependency.")
|
||||
1 -> jars.first()
|
||||
else -> throw IllegalArgumentException("There is more than one CorDapp containing the package $scanPackage on the classpath " +
|
||||
"$jars. Specify a package name which is unique to the CorDapp.")
|
||||
}
|
||||
if (signed) TestCordappSigner.signJarCopy(jar) else jar
|
||||
}
|
||||
|
||||
private fun findJars(): Set<Path> {
|
||||
val rootPaths = findRootPaths(scanPackage)
|
||||
return if (rootPaths.all { it.toString().endsWith(".jar") }) {
|
||||
// We don't need to do anything more if all the root paths are jars
|
||||
rootPaths
|
||||
} else {
|
||||
// Otherwise we need to build those paths which are local projects and extract the built jar from them
|
||||
rootPaths.mapToSet { if (it.toString().endsWith(".jar")) it else buildCordappJar(it) }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val packageToRootPaths = ConcurrentHashMap<String, Set<Path>>()
|
||||
private val projectRootToBuiltJar = ConcurrentHashMap<Path, Path>()
|
||||
private val log = contextLogger()
|
||||
|
||||
fun findJars(scanPackage: String): Set<Path> {
|
||||
val rootPaths = findRootPaths(scanPackage)
|
||||
return if (rootPaths.all { it.toString().endsWith(".jar") }) {
|
||||
// We don't need to do anything more if all the root paths are jars
|
||||
rootPaths
|
||||
} else {
|
||||
// Otherwise we need to build those paths which are local projects and extract the built jar from them
|
||||
rootPaths.mapTo(HashSet()) { if (it.toString().endsWith(".jar")) it else buildCordappJar(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun findRootPaths(scanPackage: String): Set<Path> {
|
||||
return packageToRootPaths.computeIfAbsent(scanPackage) {
|
||||
val classGraph = ClassGraph().acceptPaths(scanPackage.replace('.', '/'))
|
@ -0,0 +1,50 @@
|
||||
package net.corda.testing.node.internal
|
||||
|
||||
import net.corda.core.internal.JarSignatureCollector
|
||||
import net.corda.core.internal.deleteRecursively
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.containsKey
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.jar.JarInputStream
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.copyTo
|
||||
import kotlin.io.path.inputStream
|
||||
import kotlin.io.path.name
|
||||
|
||||
object TestCordappSigner {
|
||||
private val defaultSignerDir = Files.createTempDirectory("testcordapp-signer")
|
||||
|
||||
init {
|
||||
defaultSignerDir.generateKey(alias = "testcordapp")
|
||||
Runtime.getRuntime().addShutdownHook(Thread(defaultSignerDir::deleteRecursively))
|
||||
}
|
||||
|
||||
fun signJarCopy(jar: Path, signerDir: Path? = null, signatureCount: Int = 1, algorithm: String = "RSA"): Path {
|
||||
val copy = Files.createTempFile(jar.name, ".jar")
|
||||
copy.toFile().deleteOnExit()
|
||||
jar.copyTo(copy, overwrite = true)
|
||||
signJar(copy, signerDir, signatureCount, algorithm)
|
||||
return copy
|
||||
}
|
||||
|
||||
fun signJar(jar: Path, signerDir: Path? = null, signatureCount: Int = 1, algorithm: String = "RSA") {
|
||||
jar.unsignJar()
|
||||
val signerDirToUse = signerDir ?: defaultSignerDir
|
||||
for (i in 1 .. signatureCount) {
|
||||
println("On signer $i")
|
||||
// Note in the jarsigner tool if -sigfile is not specified then the first 8 chars of alias are used as the file
|
||||
// name for the .SF and .DSA files. (See jarsigner doc). So $i below needs to be at beginning so unique files are
|
||||
// created.
|
||||
val alias = "$i-testcordapp-$algorithm"
|
||||
val password = "secret!"
|
||||
if (!signerDirToUse.containsKey(alias, password)) {
|
||||
signerDirToUse.generateKey(alias, password, "O=Test Company Ltd $i,OU=Test,L=London,C=GB", algorithm)
|
||||
}
|
||||
signerDirToUse.signJar(jar.absolutePathString(), alias, password)
|
||||
println("Number of actual signers: ${JarInputStream(jar.inputStream()).use { JarSignatureCollector.collectSigners(it).size }}")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package net.corda.testing.node.internal
|
||||
|
||||
import net.corda.core.internal.copyTo
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.Try.Failure
|
||||
import net.corda.core.utilities.Try.Success
|
||||
import net.corda.testing.node.TestCordapp
|
||||
import java.net.URI
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
|
||||
import kotlin.io.path.toPath
|
||||
|
||||
data class UriTestCordapp(val uri: URI,
|
||||
override val config: Map<String, Any> = emptyMap(),
|
||||
val signed: Boolean = false) : TestCordappInternal() {
|
||||
override fun withConfig(config: Map<String, Any>): TestCordapp = copy(config = config)
|
||||
|
||||
override fun asSigned(): TestCordapp = copy(signed = true)
|
||||
|
||||
override fun withOnlyJarContents(): TestCordappInternal = copy(config = emptyMap(), signed = false)
|
||||
|
||||
override val jarFile: Path by lazy {
|
||||
val toPathAttempt = Try.on(uri::toPath)
|
||||
when (toPathAttempt) {
|
||||
is Success -> if (signed) TestCordappSigner.signJarCopy(toPathAttempt.value) else toPathAttempt.value
|
||||
is Failure -> {
|
||||
// URI is not a local path, so we copy it to a temp file and use that.
|
||||
val downloaded = Files.createTempFile("test-cordapp-${uri.path.substringAfterLast("/").substringBeforeLast(".jar")}", ".jar")
|
||||
downloaded.toFile().deleteOnExit()
|
||||
uri.toURL().openStream().use { it.copyTo(downloaded, REPLACE_EXISTING) }
|
||||
if (signed) {
|
||||
TestCordappSigner.signJar(downloaded)
|
||||
}
|
||||
downloaded
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user