Skip to content

Commit

Permalink
issue #104 - basic transfer test
Browse files Browse the repository at this point in the history
  • Loading branch information
tonowie committed Sep 9, 2019
1 parent 47942da commit f6bf961
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 0 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,7 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}"
testImplementation "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"

// use cucumber for tests
testImplementation 'io.cucumber:cucumber-java:4.7.1'
}
13 changes: 13 additions & 0 deletions gradle/integration.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,16 @@ task e2eTest(type: Test) {
// first run jUni tests then integration tests
mustRunAfter tasks.test
}

task cucumberTest() {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = 'Runs the cucumber tests'
dependsOn assemble, compileTestJava, integrationTestClasses
doLast {
javaexec {
main = "io.cucumber.core.cli.Main"
classpath = sourceSets.integrationTest.runtimeClasspath + sourceSets.main.output
args = ['--plugin', 'pretty', '--snippets', 'camelcase', '--glue', 'io.proximax.sdk.steps', 'src/main/resources']
}
}
}
37 changes: 37 additions & 0 deletions src/e2e/java/io/proximax/sdk/steps/BaseSteps.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2019 ProximaX Limited. All rights reserved.
* Use of this source code is governed by the Apache 2.0
* license that can be found in the LICENSE file.
*/
package io.proximax.sdk.steps;

import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URL;

import io.proximax.sdk.BaseTest;
import io.proximax.sdk.BlockchainApi;
import io.proximax.sdk.FeeCalculationStrategy;
import io.proximax.sdk.model.transaction.builder.TransactionBuilderFactory;

/**
* TODO add proper description
*/
public class BaseSteps extends BaseTest {
// use an hour for a transaction deadline
protected static final BigInteger DEFAULT_DEADLINE_DURATION = BigInteger.valueOf(60*60*1000l);

protected final BlockchainApi api;
protected final TransactionBuilderFactory transact;


public BaseSteps() throws MalformedURLException {
String nodeUrl = this.getNodeUrl();
// create HTTP APIs
api = new BlockchainApi(new URL(nodeUrl), getNetworkType());
// init the transaction builder factory
transact = api.transact();
transact.setDeadlineMillis(DEFAULT_DEADLINE_DURATION);
transact.setFeeCalculationStrategy(FeeCalculationStrategy.ZERO);
}
}
154 changes: 154 additions & 0 deletions src/e2e/java/io/proximax/sdk/steps/TransferSteps.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* Copyright 2019 ProximaX Limited. All rights reserved.
* Use of this source code is governed by the Apache 2.0
* license that can be found in the LICENSE file.
*/
package io.proximax.sdk.steps;

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;

import io.cucumber.core.api.Scenario;
import io.cucumber.java.After;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import io.proximax.core.crypto.KeyPair;
import io.proximax.sdk.AccountRepository;
import io.proximax.sdk.MosaicRepository;
import io.proximax.sdk.NamespaceRepository;
import io.proximax.sdk.model.account.Account;
import io.proximax.sdk.model.account.AccountInfo;
import io.proximax.sdk.model.mosaic.Mosaic;
import io.proximax.sdk.model.mosaic.MosaicId;
import io.proximax.sdk.model.mosaic.NetworkCurrencyMosaic;
import io.proximax.sdk.model.namespace.NamespaceId;
import io.proximax.sdk.model.namespace.NamespaceInfo;
import io.proximax.sdk.model.transaction.PlainMessage;
import io.proximax.sdk.model.transaction.Recipient;
import io.proximax.sdk.model.transaction.TransferTransaction;

/**
* test steps involving transfer transaction
*/
public class TransferSteps extends BaseSteps {

private static final Object NETWORK_CURRENCY = "network currency";

private final Map<String, Account> accounts;

public TransferSteps() throws MalformedURLException {
super();
accounts = new HashMap<>();
}

@Given("{string} is granted {bigdecimal} of {string}")
public void grantFundsToAccount(String recipientName, BigDecimal amount, String mosaicName)
throws InterruptedException, ExecutionException {
Mosaic mosaic = getMosaic(mosaicName, amount);
// create transaction
TransferTransaction tx = TransferUtils.transfer(transact,
Recipient.from(getAccount(recipientName).getAddress()),
Optional.of(Arrays.asList(mosaic)),
Optional.empty()).build();
// sign, announce and wait for confirmation
TransferUtils.announce(api, getSeedAccount(), tx, getTimeoutSeconds());
}

@When("{string} sends {bigdecimal} of {string} to {string} with plaintext message {string} and fee {bigdecimal}")
public void sendMosaicWithPlaintextMessage(String sender, BigDecimal amount, String mosaicName, String recipientName,
String message, BigDecimal fee) throws InterruptedException, ExecutionException {
Mosaic mosaic = getMosaic(mosaicName, amount);
// calculate max fee based on specified fee and network currency mosaic divisibility
BigInteger maxFee = fee.scaleByPowerOfTen(NetworkCurrencyMosaic.DIVISIBILITY).toBigInteger();
// create transaction
TransferTransaction tx = TransferUtils.transfer(transact,
Recipient.from(getAccount(recipientName).getAddress()),
Optional.of(Arrays.asList(mosaic)),
Optional.of(PlainMessage.create(message))).maxFee(maxFee).build();
// sign, announce and wait for confirmation
TransferUtils.announce(api, getAccount(sender), tx, getTimeoutSeconds());

}

@Then("{string} has {bigdecimal} of {string}")
public void verifyFunds(String accountName, BigDecimal mosaicAmount, String mosaicName) {
// inputs
Account acc = getAccount(accountName);
Mosaic mosaic = getMosaic(mosaicName, mosaicAmount);
// repos
AccountRepository accRepo = api.createAccountRepository();
MosaicRepository mosaicRepo = api.createMosaicRepository();
NamespaceRepository nsRepo = api.createNamespaceRepository();
// get the mosaicId
MosaicId mosaicId;
// get the id of the mosaic which might be hidden by the alias namespace id
if (mosaic.getId() instanceof NamespaceId) {
NamespaceInfo ns = nsRepo.getNamespace((NamespaceId) mosaic.getId()).blockingFirst();
mosaicId = ns.getMosaicAlias().get();
} else {
mosaicId = (MosaicId) mosaic.getId();
}
final MosaicId desiredMosaicId = mosaicId;
// determine divisibility
int divisibility = mosaicRepo.getMosaic(desiredMosaicId).blockingFirst().getDivisibility();
// find owned mosaics
AccountInfo accInfo = accRepo.getAccountInfo(acc.getAddress()).blockingFirst();
// check that owned mosaics contain the required mosaic
Optional<Mosaic> ownedMosaic = accInfo.getMosaics().stream().filter(mos -> mos.getId().equals(desiredMosaicId))
.findFirst();
// definitely need to have the mosaic in the list
assertTrue(ownedMosaic.isPresent());
// check balance
assertTrue(
mosaicAmount.scaleByPowerOfTen(divisibility).toBigInteger().compareTo(ownedMosaic.get().getAmount()) <= 0);
}

@After
public void returnMosaicsToSeed(Scenario scenario) throws InterruptedException, ExecutionException {
for (Account acct : accounts.values()) {
List<Mosaic> mosaics = api.createAccountRepository().getAccountInfo(acct.getAddress()).blockingFirst()
.getMosaics();
// return funds from accounts to seed
TransferTransaction tx = TransferUtils.transfer(transact,
Recipient.from(getSeedAccount().getAddress()),
Optional.of(mosaics),
Optional.empty()).build();
// sign, announce and wait for confirmation
TransferUtils.announce(api, acct, tx, getTimeoutSeconds());
}
}

/**
* get existing account by name or create a new one
*
* @param name name of the account
* @return existing or new account for the specified account name
*/
private synchronized Account getAccount(final String name) {
final String accountKey = name.toLowerCase();
Account acc = accounts.get(accountKey);
if (acc == null) {
acc = new Account(new KeyPair(), getNetworkType());
accounts.put(accountKey, acc);
}
return acc;
}

private static Mosaic getMosaic(String name, BigDecimal amount) {
if (name.toLowerCase().equals(NETWORK_CURRENCY)) {
return NetworkCurrencyMosaic.createRelative(amount);
} else {
throw new cucumber.api.PendingException();
}
}
}
57 changes: 57 additions & 0 deletions src/e2e/java/io/proximax/sdk/steps/TransferUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2019 ProximaX Limited. All rights reserved.
* Use of this source code is governed by the Apache 2.0
* license that can be found in the LICENSE file.
*/
package io.proximax.sdk.steps;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import io.proximax.sdk.BlockchainApi;
import io.proximax.sdk.ListenerRepository;
import io.proximax.sdk.model.account.Account;
import io.proximax.sdk.model.mosaic.Mosaic;
import io.proximax.sdk.model.transaction.Message;
import io.proximax.sdk.model.transaction.Recipient;
import io.proximax.sdk.model.transaction.SignedTransaction;
import io.proximax.sdk.model.transaction.Transaction;
import io.proximax.sdk.model.transaction.builder.TransactionBuilderFactory;
import io.proximax.sdk.model.transaction.builder.TransferTransactionBuilder;

/**
* TODO add proper description
*/
public class TransferUtils {

public static TransferTransactionBuilder transfer(TransactionBuilderFactory transact, Recipient recipient, Optional<List<Mosaic>> mosaics, Optional<Message> message) {
// mandatory fields
TransferTransactionBuilder builder = transact.transfer().to(recipient);
// optional mosaics
if (mosaics.isPresent()) {
builder.mosaics(mosaics.get());
}
// optional message
if (message.isPresent()) {
builder.message(message.get());
}
// return the builder
return builder;
}

public static Transaction announce(BlockchainApi api, Account signer, Transaction tx, long timeout) throws InterruptedException, ExecutionException {
// sign the transaction
SignedTransaction signedTx = api.sign(tx, signer);
// announce the transaction
api.createTransactionRepository().announce(signedTx).blockingFirst();
// create and init listener
ListenerRepository listener = api.createListener();
listener.open().get();
// wait for confirmation of signed transaction
return listener.confirmed(signer.getAddress())
.filter(trans -> trans.getTransactionInfo().get().getHash().get().equals(signedTx.getHash()))
.timeout(timeout, TimeUnit.SECONDS).blockingFirst();
}
}
10 changes: 10 additions & 0 deletions src/main/resources/features/transfer.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Feature: Transfer mosaics and messages
As Alice
I want to make transfers to Bob
So that Bob receives mosaics and messages

Scenario: Send network currency
Given "Alice" is granted 10.0 of "network currency"
When "Alice" sends 8.5 of "network currency" to "Bob" with plaintext message "hi Bob" and fee 1.0
Then "Alice" has 0.5 of "network currency"
And "Bob" has 8.5 of "network currency"

0 comments on commit f6bf961

Please sign in to comment.