diff --git a/contracts/build.gradle b/contracts/build.gradle index 52ca6f0..dc26c0b 100644 --- a/contracts/build.gradle +++ b/contracts/build.gradle @@ -55,6 +55,7 @@ dependencies { testImplementation "org.mockito:mockito-core:$mockitoVersion" testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion" + testImplementation "com.r3.corda.ledger.utxo:contract-testing:$contractTestingVersion" } // The CordApp section. diff --git a/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.java b/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.java index 04a289e..846b286 100644 --- a/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.java +++ b/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.java @@ -13,37 +13,54 @@ public class ChatContract implements Contract { private final static Logger log = LoggerFactory.getLogger(ChatContract.class); + // Use constants to hold the error messages + // This allows the tests to use them, meaning if they are updated you won't need to fix tests just because the wording was updated + static final String REQUIRE_SINGLE_COMMAND = "Require a single command."; + static final String UNKNOWN_COMMAND = "Unsupported command"; + static final String OUTPUT_STATE_SHOULD_ONLY_HAVE_TWO_PARTICIPANTS = "The output state should have two and only two participants."; + static final String TRANSACTION_SHOULD_BE_SIGNED_BY_ALL_PARTICIPANTS = "The transaction should have been signed by both participants."; + + static final String CREATE_COMMAND_SHOULD_HAVE_NO_INPUT_STATES = "When command is Create there should be no input states."; + static final String CREATE_COMMAND_SHOULD_HAVE_ONLY_ONE_OUTPUT_STATE = "When command is Create there should be one and only one output state."; + + static final String UPDATE_COMMAND_SHOULD_HAVE_ONLY_ONE_INPUT_STATE = "When command is Update there should be one and only one input state."; + static final String UPDATE_COMMAND_SHOULD_HAVE_ONLY_ONE_OUTPUT_STATE = "When command is Update there should be one and only one output state."; + static final String UPDATE_COMMAND_ID_SHOULD_NOT_CHANGE = "When command is Update id must not change."; + static final String UPDATE_COMMAND_CHATNAME_SHOULD_NOT_CHANGE = "When command is Update chatName must not change."; + static final String UPDATE_COMMAND_PARTICIPANTS_SHOULD_NOT_CHANGE = "When command is Update participants must not change."; + public static class Create implements Command { } public static class Update implements Command { } @Override public void verify(UtxoLedgerTransaction transaction) { - requireThat( transaction.getCommands().size() == 1, "Require a single command."); + requireThat( transaction.getCommands().size() == 1, REQUIRE_SINGLE_COMMAND); Command command = transaction.getCommands().get(0); ChatState output = transaction.getOutputStates(ChatState.class).get(0); - requireThat(output.getParticipants().size() == 2, "The output state should have two and only two participants."); + requireThat(output.getParticipants().size() == 2, OUTPUT_STATE_SHOULD_ONLY_HAVE_TWO_PARTICIPANTS); + requireThat(transaction.getSignatories().containsAll(output.getParticipants()), TRANSACTION_SHOULD_BE_SIGNED_BY_ALL_PARTICIPANTS); if(command.getClass() == Create.class) { - requireThat(transaction.getInputContractStates().isEmpty(), "When command is Create there should be no input states."); - requireThat(transaction.getOutputContractStates().size() == 1, "When command is Create there should be one and only one output state."); + requireThat(transaction.getInputContractStates().isEmpty(), CREATE_COMMAND_SHOULD_HAVE_NO_INPUT_STATES); + requireThat(transaction.getOutputContractStates().size() == 1, CREATE_COMMAND_SHOULD_HAVE_ONLY_ONE_OUTPUT_STATE); } else if(command.getClass() == Update.class) { - requireThat(transaction.getInputContractStates().size() == 1, "When command is Update there should be one and only one input state."); - requireThat(transaction.getOutputContractStates().size() == 1, "When command is Update there should be one and only one output state."); + requireThat(transaction.getInputContractStates().size() == 1, UPDATE_COMMAND_SHOULD_HAVE_ONLY_ONE_INPUT_STATE); + requireThat(transaction.getOutputContractStates().size() == 1, UPDATE_COMMAND_SHOULD_HAVE_ONLY_ONE_OUTPUT_STATE); ChatState input = transaction.getInputStates(ChatState.class).get(0); - requireThat(input.getId().equals(output.getId()), "When command is Update id must not change."); - requireThat(input.getChatName().equals(output.getChatName()), "When command is Update chatName must not change."); + requireThat(input.getId().equals(output.getId()), UPDATE_COMMAND_ID_SHOULD_NOT_CHANGE); + requireThat(input.getChatName().equals(output.getChatName()), UPDATE_COMMAND_CHATNAME_SHOULD_NOT_CHANGE); requireThat( input.getParticipants().containsAll(output.getParticipants()) && output.getParticipants().containsAll(input.getParticipants()), - "When command is Update participants must not change."); + UPDATE_COMMAND_PARTICIPANTS_SHOULD_NOT_CHANGE); } else { - throw new CordaRuntimeException("Unsupported command"); + throw new CordaRuntimeException(UNKNOWN_COMMAND); } } diff --git a/contracts/src/test/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContractCreateCommandTest.java b/contracts/src/test/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContractCreateCommandTest.java new file mode 100644 index 0000000..5c3340a --- /dev/null +++ b/contracts/src/test/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContractCreateCommandTest.java @@ -0,0 +1,158 @@ +package com.r3.developers.csdetemplate.utxoexample.contracts; + +import com.r3.corda.ledger.utxo.testing.ContractTest; +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +import net.corda.v5.ledger.utxo.Command; +import net.corda.v5.ledger.utxo.StateAndRef; +import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +import static com.r3.developers.csdetemplate.utxoexample.contracts.ChatContract.*; +import static java.util.Collections.emptyList; + +public class ChatContractCreateCommandTest extends ContractTest { + + protected ChatState outputChatState = new ChatState( + UUID.randomUUID(), + "aliceChatName", + aliceName, + "aliceChatMessage", + List.of(aliceKey, bobKey) + ); + + @Test + public void happyPath() { + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addOutputState(outputChatState) + .addCommand(new ChatContract.Create()) + .addSignatories(outputChatState.participants) + .toSignedTransaction(); + assertVerifies(transaction); + } + + @Test + public void missingCommand() { + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addOutputState(outputChatState) + .toSignedTransaction(); + assertFailsWith(transaction, "Failed requirement: " + REQUIRE_SINGLE_COMMAND); + } + + @Test + public void shouldNotAcceptUnknownCommand() { + class MyDummyCommand implements Command { + } + + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addOutputState(outputChatState) + .addCommand(new MyDummyCommand()) + .addSignatories(outputChatState.participants) + .toSignedTransaction(); + + assertFailsWith(transaction, UNKNOWN_COMMAND); + } + + @Test + public void outputStateCannotHaveZeroParticipants() { + ChatState state = new ChatState( + UUID.randomUUID(), + "myChatName", + aliceName, + "myChatMessage", + emptyList() + ); + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addOutputState(state) + .addCommand(new ChatContract.Create()) + .toSignedTransaction(); + assertFailsWith(transaction, "Failed requirement: " + OUTPUT_STATE_SHOULD_ONLY_HAVE_TWO_PARTICIPANTS); + } + + @Test + public void outputStateCannotHaveOneParticipant() { + ChatState state = new ChatState( + UUID.randomUUID(), + "myChatName", + aliceName, + "myChatMessage", + List.of(aliceKey) + ); + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addOutputState(state) + .addCommand(new ChatContract.Create()) + .toSignedTransaction(); + assertFailsWith(transaction, "Failed requirement: " + OUTPUT_STATE_SHOULD_ONLY_HAVE_TWO_PARTICIPANTS); + } + + @Test + public void outputStateCannotHaveThreeParticipants() { + ChatState state = new ChatState( + UUID.randomUUID(), + "myChatName", + aliceName, + "myChatMessage", + List.of(aliceKey, bobKey, charlieKey) + ); + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addOutputState(state) + .addCommand(new ChatContract.Create()) + .toSignedTransaction(); + assertFailsWith(transaction, "Failed requirement: " + OUTPUT_STATE_SHOULD_ONLY_HAVE_TWO_PARTICIPANTS); + } + + @Test + public void outputStateMustBeSigned() { + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addOutputState(outputChatState) + .addCommand(new ChatContract.Create()) + .toSignedTransaction(); + assertFailsWith(transaction, "Failed requirement: " + TRANSACTION_SHOULD_BE_SIGNED_BY_ALL_PARTICIPANTS); + } + + @Test + public void outputStateCannotBeSignedByOnlyOneParticipant() { + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addOutputState(outputChatState) + .addCommand(new ChatContract.Create()) + .addSignatories(outputChatState.participants.get(0)) + .toSignedTransaction(); + assertFailsWith(transaction, "Failed requirement: " + TRANSACTION_SHOULD_BE_SIGNED_BY_ALL_PARTICIPANTS); + } + + @Test + public void shouldNotIncludeInputState() { + happyPath(); // generate an existing state to search for + StateAndRef existingState = getLedgerService().findUnconsumedStatesByType(ChatState.class).get(0); // doesn't matter which as this will fail validation + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addInputState(existingState.getRef()) + .addOutputState(outputChatState) + .addCommand(new ChatContract.Create()) + .addSignatories(outputChatState.participants) + .toSignedTransaction(); + assertFailsWith(transaction, "Failed requirement: " + CREATE_COMMAND_SHOULD_HAVE_NO_INPUT_STATES); + } + + @Test + public void shouldNotHaveTwoOutputStates() { + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addOutputState(outputChatState) + .addOutputState(outputChatState) + .addCommand(new ChatContract.Create()) + .addSignatories(outputChatState.participants) + .toSignedTransaction(); + assertFailsWith(transaction, "Failed requirement: " + CREATE_COMMAND_SHOULD_HAVE_ONLY_ONE_OUTPUT_STATE); + } +} diff --git a/contracts/src/test/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContractUpdateCommandTest.java b/contracts/src/test/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContractUpdateCommandTest.java new file mode 100644 index 0000000..e9bcf4f --- /dev/null +++ b/contracts/src/test/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContractUpdateCommandTest.java @@ -0,0 +1,174 @@ +package com.r3.developers.csdetemplate.utxoexample.contracts; + +import com.r3.corda.ledger.utxo.testing.ContractTest; +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +import net.corda.v5.ledger.utxo.StateAndRef; +import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +import static com.r3.developers.csdetemplate.utxoexample.contracts.ChatContract.*; + +public class ChatContractUpdateCommandTest extends ContractTest { + + private StateAndRef createInitialChatState() { + ChatState outputChatState = new ChatContractCreateCommandTest().outputChatState; + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addOutputState(outputChatState) + .addCommand(new ChatContract.Create()) + .addSignatories(outputChatState.participants) + .toSignedTransaction(); + transaction.toLedgerTransaction(); + return (StateAndRef) transaction.getOutputStateAndRefs().get(0); + } + + @Test + public void happyPath() { + StateAndRef existingState = createInitialChatState(); + ChatState updatedOutputChatState = existingState.getState().getContractState().updateMessage(bobName, "bobResponse"); + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addInputState(existingState.getRef()) + .addOutputState(updatedOutputChatState) + .addCommand(new ChatContract.Update()) + .addSignatories(updatedOutputChatState.participants) + .toSignedTransaction(); + assertVerifies(transaction); + } + + @Test + public void shouldNotHaveNoInputState() { + StateAndRef existingState = createInitialChatState(); + ChatState updatedOutputChatState = existingState.getState().getContractState().updateMessage(bobName, "bobResponse"); + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addOutputState(updatedOutputChatState) + .addCommand(new ChatContract.Update()) + .addSignatories(updatedOutputChatState.participants) + .toSignedTransaction(); + assertFailsWith(transaction, "Failed requirement: " + UPDATE_COMMAND_SHOULD_HAVE_ONLY_ONE_INPUT_STATE); + } + + @Test + public void shouldNotHaveTwoInputStates() { + StateAndRef existingState = createInitialChatState(); + ChatState updatedOutputChatState = existingState.getState().getContractState().updateMessage(bobName, "bobResponse"); + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addInputState(existingState.getRef()) + .addInputState(existingState.getRef()) + .addOutputState(updatedOutputChatState) + .addCommand(new ChatContract.Update()) + .addSignatories(updatedOutputChatState.participants) + .toSignedTransaction(); + assertFailsWith(transaction, "Failed requirement: " + UPDATE_COMMAND_SHOULD_HAVE_ONLY_ONE_INPUT_STATE); + } + + @Test + public void shouldNotHaveTwoOutputStates() { + StateAndRef existingState = createInitialChatState(); + ChatState updatedOutputChatState = existingState.getState().getContractState().updateMessage(bobName, "bobResponse"); + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addInputState(existingState.getRef()) + .addOutputState(updatedOutputChatState) + .addOutputState(updatedOutputChatState) + .addCommand(new ChatContract.Update()) + .addSignatories(updatedOutputChatState.participants) + .toSignedTransaction(); + assertFailsWith(transaction, "Failed requirement: " + UPDATE_COMMAND_SHOULD_HAVE_ONLY_ONE_OUTPUT_STATE); + } + + @Test + public void idShouldNotChange() { + StateAndRef existingState = createInitialChatState(); + ChatState esDetails = existingState.getState().getContractState(); + ChatState updatedOutputChatState = new ChatState( + UUID.randomUUID(), + esDetails.getChatName(), + bobName, + "bobResponse", + esDetails.getParticipants() + ); + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addInputState(existingState.getRef()) + .addOutputState(updatedOutputChatState) + .addCommand(new ChatContract.Update()) + .addSignatories(updatedOutputChatState.participants) + .toSignedTransaction(); + assertFailsWith(transaction, "Failed requirement: " + UPDATE_COMMAND_ID_SHOULD_NOT_CHANGE); + } + + @Test + public void chatNameShouldNotChange() { + StateAndRef existingState = createInitialChatState(); + ChatState esDetails = existingState.getState().getContractState(); + ChatState updatedOutputChatState = new ChatState( + esDetails.getId(), + "newName", + bobName, + "bobResponse", + esDetails.getParticipants() + ); + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addInputState(existingState.getRef()) + .addOutputState(updatedOutputChatState) + .addCommand(new ChatContract.Update()) + .addSignatories(updatedOutputChatState.participants) + .toSignedTransaction(); + assertFailsWith(transaction, "Failed requirement: " + UPDATE_COMMAND_CHATNAME_SHOULD_NOT_CHANGE); + } + + @Test + public void participantsShouldNotChange() { + StateAndRef existingState = createInitialChatState(); + ChatState esDetails = existingState.getState().getContractState(); + ChatState updatedOutputChatState = new ChatState( + esDetails.getId(), + esDetails.getChatName(), + bobName, + "bobResponse", + List.of(bobKey, charlieKey) + ); + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addInputState(existingState.getRef()) + .addOutputState(updatedOutputChatState) + .addCommand(new ChatContract.Update()) + .addSignatories(updatedOutputChatState.participants) + .toSignedTransaction(); + assertFailsWith(transaction, "Failed requirement: " + UPDATE_COMMAND_PARTICIPANTS_SHOULD_NOT_CHANGE); + } + + @Test + public void outputStateMustBeSigned() { + StateAndRef existingState = createInitialChatState(); + ChatState updatedOutputChatState = existingState.getState().getContractState().updateMessage(bobName, "bobResponse"); + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addInputState(existingState.getRef()) + .addOutputState(updatedOutputChatState) + .addCommand(new ChatContract.Update()) + .toSignedTransaction(); + assertFailsWith(transaction, "Failed requirement: " + TRANSACTION_SHOULD_BE_SIGNED_BY_ALL_PARTICIPANTS); + } + + @Test + public void outputStateCannotBeSignedByOnlyOneParticipant() { + StateAndRef existingState = createInitialChatState(); + ChatState updatedOutputChatState = existingState.getState().getContractState().updateMessage(bobName, "bobResponse"); + UtxoSignedTransaction transaction = getLedgerService() + .createTransactionBuilder() + .addInputState(existingState.getRef()) + .addOutputState(updatedOutputChatState) + .addCommand(new ChatContract.Update()) + .addSignatories(updatedOutputChatState.participants.get(0)) + .toSignedTransaction(); + assertFailsWith(transaction, "Failed requirement: " + TRANSACTION_SHOULD_BE_SIGNED_BY_ALL_PARTICIPANTS); + } +} diff --git a/gradle.properties b/gradle.properties index dc5b821..0515780 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,6 +35,7 @@ junitVersion = 5.8.2 mockitoKotlinVersion=4.0.0 mockitoVersion=4.6.1 hamcrestVersion=2.2 +contractTestingVersion=0.9.0-beta-+ # Specify the maximum amount of time allowed for the CPI upload # As your CorDapp grows you might need to increase this