INFRA-553 Move HashLookupCommandTest to unit test (#6537)

Move HashLookupCommandTest to unit test and add a test covering that a nonsense hash ID is correctly rejected.
This commit is contained in:
Ross Nicoll 2020-07-31 15:54:53 +01:00 committed by GitHub
parent 250ed8a21a
commit 9aa745ef14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 85 additions and 105 deletions

View File

@ -1,96 +0,0 @@
package net.corda.tools.shell
import co.paralleluniverse.fibers.Suspendable
import com.google.common.io.Files
import com.jcraft.jsch.ChannelExec
import com.jcraft.jsch.JSch
import com.jcraft.jsch.Session
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.node.services.Permissions
import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.driver
import net.corda.testing.node.User
import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP
import org.bouncycastle.util.io.Streams
import org.junit.Test
import kotlin.test.assertTrue
class HashLookupCommandTest {
@Test(timeout=300_000)
fun `hash lookup command returns correct response`() {
val user = User("u", "p", setOf(Permissions.all()))
driver(DriverParameters(notarySpecs = emptyList(), cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP))) {
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
val node = nodeFuture.getOrThrow()
val txId = issueTransaction(node)
val txIdHashed = txId.sha256()
testCommand(user, node, command = "hashLookup $txId", expected = "Found a matching transaction with Id: $txId")
testCommand(user, node, command = "hashLookup $txIdHashed", expected = "Found a matching transaction with Id: $txId")
testCommand(user, node, command = "hashLookup ${SecureHash.randomSHA256()}", expected = "No matching transaction found")
}
}
private fun testCommand(user: User, node: NodeHandle, command: String, expected: String) {
val session = connectToShell(user, node)
val channel = session.openChannel("exec") as ChannelExec
channel.setCommand(command)
channel.connect(5000)
assertTrue(channel.isConnected)
val response = String(Streams.readAll(channel.inputStream))
val matchFound = response.lines().any { line ->
line.contains(expected)
}
channel.disconnect()
assertTrue(matchFound)
session.disconnect()
}
private fun connectToShell(user: User, node: NodeHandle): Session {
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
user = user.username, password = user.password,
hostAndPort = node.rpcAddress,
sshdPort = 2224)
InteractiveShell.startShell(conf)
InteractiveShell.nodeInfo()
val session = JSch().getSession("u", "localhost", 2224)
session.setConfig("StrictHostKeyChecking", "no")
session.setPassword("p")
session.connect()
assertTrue(session.isConnected)
return session
}
private fun issueTransaction(node: NodeHandle): SecureHash {
return node.rpc.startFlow(::DummyIssue).returnValue.get()
}
}
@StartableByRPC
internal class DummyIssue : FlowLogic<SecureHash>() {
@Suspendable
override fun call(): SecureHash {
val me = serviceHub.myInfo.legalIdentities.first().ref(0)
val fakeNotary = me.party
val builder = DummyContract.generateInitial(1, fakeNotary as Party, me)
val stx = serviceHub.signInitialTransaction(builder)
serviceHub.recordTransactions(stx)
return stx.id
}
}

View File

@ -1,6 +1,7 @@
package net.corda.tools.shell;
import net.corda.core.crypto.SecureHash;
import net.corda.core.internal.VisibleForTesting;
import net.corda.core.messaging.CordaRPCOps;
import net.corda.core.messaging.StateMachineTransactionMapping;
import org.crsh.cli.Argument;
@ -13,13 +14,14 @@ import org.crsh.text.Decoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.PrintWriter;
import java.util.List;
import java.util.Optional;
@Named("hashLookup")
public class HashLookupShellCommand extends InteractiveShellCommand {
private static Logger logger = LoggerFactory.getLogger(HashLookupShellCommand.class);
final private String manualText ="Checks if a transaction matching a specified Id hash value is recorded on this node.\n\n" +
private static final String manualText ="Checks if a transaction matching a specified Id hash value is recorded on this node.\n\n" +
"Both the transaction Id and the hashed value of a transaction Id (as returned by the Notary in case of a double-spend) is a valid input.\n" +
"This is mainly intended to be used for troubleshooting notarisation issues when a\n" +
"state is claimed to be already consumed by another transaction.\n\n" +
@ -29,25 +31,32 @@ public class HashLookupShellCommand extends InteractiveShellCommand {
@Man(manualText)
public void main(@Usage("A transaction Id or a hexadecimal SHA-256 hash value representing the hashed transaction Id") @Argument(unquote = false) String txIdHash) {
CordaRPCOps proxy = ops();
try {
hashLookup(out, proxy, txIdHash);
} catch (IllegalArgumentException ex) {
out.println(manualText);
out.println(ex.getMessage(), Decoration.bold, Color.red);
}
}
@VisibleForTesting
protected static void hashLookup(PrintWriter out, CordaRPCOps proxy, String txIdHash) throws IllegalArgumentException {
logger.info("Executing command \"hashLookup\".");
if (txIdHash == null) {
out.println(manualText);
out.println("Please provide a hexadecimal transaction Id hash value or a transaction Id", Decoration.bold, Color.red);
return;
throw new IllegalArgumentException("Please provide a hexadecimal transaction Id hash value or a transaction Id");
}
CordaRPCOps proxy = ops();
List<StateMachineTransactionMapping> mapping = proxy.stateMachineRecordedTransactionMappingSnapshot();
SecureHash txIdHashParsed;
try {
txIdHashParsed = SecureHash.parse(txIdHash);
} catch (IllegalArgumentException e) {
out.println("The provided string is not a valid hexadecimal SHA-256 hash value", Decoration.bold, Color.red);
return;
throw new IllegalArgumentException("The provided string is not a valid hexadecimal SHA-256 hash value");
}
List<StateMachineTransactionMapping> mapping = proxy.stateMachineRecordedTransactionMappingSnapshot();
Optional<SecureHash> match = mapping.stream()
.map(StateMachineTransactionMapping::getTransactionId)
.filter(
@ -59,7 +68,7 @@ public class HashLookupShellCommand extends InteractiveShellCommand {
SecureHash found = match.get();
out.println("Found a matching transaction with Id: " + found.toString());
} else {
out.println("No matching transaction found", Decoration.bold, Color.red);
throw new IllegalArgumentException("No matching transaction found");
}
}
}

View File

@ -0,0 +1,67 @@
package net.corda.tools.shell
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.flows.StateMachineRunId
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineTransactionMapping
import org.hamcrest.MatcherAssert
import org.hamcrest.core.StringContains
import org.junit.Test
import org.mockito.Mockito
import java.io.CharArrayWriter
import java.io.PrintWriter
import java.util.UUID
import kotlin.test.assertFailsWith
class HashLookupCommandTest {
companion object {
private val DEFAULT_TXID: SecureHash = SecureHash.randomSHA256()
private fun ops(vararg txIds: SecureHash): CordaRPCOps? {
val snapshot: List<StateMachineTransactionMapping> = txIds.map { txId ->
StateMachineTransactionMapping(StateMachineRunId(UUID.randomUUID()), txId)
}
return Mockito.mock(CordaRPCOps::class.java).apply {
Mockito.`when`(stateMachineRecordedTransactionMappingSnapshot()).thenReturn(snapshot)
}
}
private fun runCommand(ops: CordaRPCOps?, txIdHash: String): String {
val arrayWriter = CharArrayWriter()
return PrintWriter(arrayWriter).use {
HashLookupShellCommand.hashLookup(it, ops, txIdHash)
it.flush()
arrayWriter.toString()
}
}
}
@Test(timeout=300_000)
fun `hash lookup command returns correct response`() {
val ops = ops(DEFAULT_TXID)
var response = runCommand(ops, DEFAULT_TXID.toString())
MatcherAssert.assertThat(response, StringContains.containsString("Found a matching transaction with Id: $DEFAULT_TXID"))
// Verify the hash of the TX ID also works
response = runCommand(ops, DEFAULT_TXID.sha256().toString())
MatcherAssert.assertThat(response, StringContains.containsString("Found a matching transaction with Id: $DEFAULT_TXID"))
}
@Test(timeout=300_000)
fun `should reject invalid txid`() {
val ops = ops(DEFAULT_TXID)
assertFailsWith<IllegalArgumentException>("The provided string is not a valid hexadecimal SHA-256 hash value") {
runCommand(ops, "abcdefgh")
}
}
@Test(timeout=300_000)
fun `should reject unknown txid`() {
val ops = ops(DEFAULT_TXID)
assertFailsWith<IllegalArgumentException>("No matching transaction found") {
runCommand(ops, SecureHash.randomSHA256().toString())
}
}
}