CORE-14586: Utxo Example Contract Tests (#51)

* CORE-14586: Utxo Example Contract Tests

* CORE-14586: Use beta builds rather than alpha builds

* CORE-14586: Add signing constraint
This commit is contained in:
Tony Lawson 2023-06-23 14:37:51 +01:00 committed by GitHub
parent 7b3144001d
commit 20e3dd38b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 361 additions and 10 deletions

View File

@ -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.

View File

@ -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);
}
}

View File

@ -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<ChatState> 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);
}
}

View File

@ -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<ChatState> createInitialChatState() {
ChatState outputChatState = new ChatContractCreateCommandTest().outputChatState;
UtxoSignedTransaction transaction = getLedgerService()
.createTransactionBuilder()
.addOutputState(outputChatState)
.addCommand(new ChatContract.Create())
.addSignatories(outputChatState.participants)
.toSignedTransaction();
transaction.toLedgerTransaction();
return (StateAndRef<ChatState>) transaction.getOutputStateAndRefs().get(0);
}
@Test
public void happyPath() {
StateAndRef<ChatState> 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<ChatState> 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<ChatState> 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<ChatState> 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<ChatState> 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<ChatState> 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<ChatState> 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<ChatState> 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<ChatState> 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);
}
}

View File

@ -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