Move test cases into subproject test sources

This change reorganizes the ':apitest' subproject to conform to a
typical gradle project, where all JUnit test cases are located in
the subproject's test sources folder.  This makes running tests
from an IDE or gradle command line interface work as expected.
It will also help keep Travis CI configuration simple.

To avoid interfering in normal builds, the gradle ':apitest test' task
is disable by default;  API tests will only run when a '-Dforce-true'
system property is passed to gradle.

To run API tests, run a normal build and install dao-setup files:

    ./gradlew clean build :apitest:installDaoSetup

Then run the tests:

    ./gradlew :apitest:test -Dforce=true

Try to avoid adding the '-Dforce=true' property to any other gradle
tasks, because this enables the ':apitest test' task, and would kick
off API tests before a normal build completed.

The build.gradle file was modified to support this code reorg, and
the 'org.junit.jupiter' dependendency was upgraded to v5.6.2 -- only
in the ':apitest:test' dependency definiitions, not anywhere else in
the bisq dependency definitions.  The upgrade is necessary for
running ordered tests.

Since the scaffolding may be set up from either test cases (under the
test src folder), or a class under the main src folder, some changes
were made to ensure configuration paths are correct for either use
case.  For example, when the 'bisq-apitest' script is run from the root
project directory, the current working directory is the root project
directory.  When gradle or an IDE is used to run @Test cases, the
current working directory is :apitest subproject directory.

The main source's ApiTestMain class has been stripped down, and exists
only to keep the gradle build happy -- it needs a 'mainClassName'
property.  But this main driver does have uses.  See the class comments.

The other changes in this commit were made to fix style and syntax
problems.
This commit is contained in:
ghubstan 2020-07-19 16:31:46 -03:00
parent 7c974b22ac
commit bf584c218f
No known key found for this signature in database
GPG key ID: E35592D6800A861E
16 changed files with 838 additions and 31 deletions

View file

@ -29,9 +29,19 @@ import static java.lang.System.exit;
import bisq.apitest.config.ApiTestConfig;
/**
* ApiTest Application
* ApiTestMain is a placeholder for the gradle build file, which requires a valid
* 'mainClassName' property in the :apitest subproject configuration.
*
* Runs all method tests, scenario tests, and end to end tests (e2e).
* It does has some uses:
*
* It can be used to print test scaffolding options: bisq-apitest --help.
*
* It can be used to smoke test your bitcoind environment: bisq-apitest.
*
* It can be used to run the regtest/dao environment for release testing:
* bisq-test --shutdownAfterTests=false
*
* All method, scenario and end to end tests are found in the test sources folder.
*
* Requires bitcoind v0.19.x
*/

View file

@ -96,7 +96,7 @@ public class Scaffold {
* @param supportingApps String
*/
public Scaffold(String supportingApps) {
this(new ApiTestConfig(new String[]{"--supportingApps", supportingApps}));
this(new ApiTestConfig("--supportingApps", supportingApps));
}
/**
@ -181,29 +181,29 @@ public class Scaffold {
public void installDaoSetupDirectories() {
cleanDaoSetupDirectories();
String srcResourcesDir = Paths.get("apitest", "src", "main", "resources", "dao-setup").toFile().getAbsolutePath();
String daoSetupDir = Paths.get(config.baseSrcResourcesDir, "dao-setup").toFile().getAbsolutePath();
String buildDataDir = config.rootAppDataDir.getAbsolutePath();
try {
if (!new File(srcResourcesDir).exists())
if (!new File(daoSetupDir).exists())
throw new FileNotFoundException(
format("Dao setup dir '%s' not found. Run gradle :apitest:installDaoSetup"
+ " to download dao-setup.zip and extract contents to resources folder",
srcResourcesDir));
daoSetupDir));
BashCommand copyBitcoinRegtestDir = new BashCommand(
"cp -rf " + srcResourcesDir + "/Bitcoin-regtest/regtest"
"cp -rf " + daoSetupDir + "/Bitcoin-regtest/regtest"
+ " " + config.bitcoinDatadir);
if (copyBitcoinRegtestDir.run().getExitStatus() != 0)
throw new IllegalStateException("Could not install bitcoin regtest dir");
BashCommand copyAliceDataDir = new BashCommand(
"cp -rf " + srcResourcesDir + "/" + alicedaemon.appName
"cp -rf " + daoSetupDir + "/" + alicedaemon.appName
+ " " + config.rootAppDataDir);
if (copyAliceDataDir.run().getExitStatus() != 0)
throw new IllegalStateException("Could not install alice data dir");
BashCommand copyBobDataDir = new BashCommand(
"cp -rf " + srcResourcesDir + "/" + bobdaemon.appName
"cp -rf " + daoSetupDir + "/" + bobdaemon.appName
+ " " + config.rootAppDataDir);
if (copyBobDataDir.run().getExitStatus() != 0)
throw new IllegalStateException("Could not install bob data dir");
@ -217,7 +217,7 @@ public class Scaffold {
installBitcoinBlocknotify();
} catch (IOException | InterruptedException ex) {
throw new IllegalStateException("Could not install dao-setup files from " + srcResourcesDir, ex);
throw new IllegalStateException("Could not install dao-setup files from " + daoSetupDir, ex);
}
}
@ -254,7 +254,7 @@ public class Scaffold {
private void installBitcoinBlocknotify() {
// gradle is not working for this
try {
Path srcPath = Paths.get("apitest", "src", "main", "resources", "blocknotify");
Path srcPath = Paths.get(config.baseSrcResourcesDir, "blocknotify");
Path destPath = Paths.get(config.bitcoinDatadir, "blocknotify");
Files.copy(srcPath, destPath, REPLACE_EXISTING);
String chmod700Perms = "rwx------";
@ -283,7 +283,7 @@ public class Scaffold {
bitcoinDaemon.verifyBitcoinPathsExist(true);
bitcoindTask = new SetupTask(bitcoinDaemon, countdownLatch);
bitcoindTaskFuture = executor.submit(bitcoindTask);
MILLISECONDS.sleep(3500);
MILLISECONDS.sleep(3500); // todo make configurable
bitcoinDaemon.verifyBitcoindRunning();
}

View file

@ -104,20 +104,32 @@ public class ApiTestConfig {
public final boolean shutdownAfterTests;
public final List<String> supportingApps;
// Immutable system configurations.
// Immutable system configurations set in the constructor.
public final String bitcoinDatadir;
public final String userDir;
public final boolean isRunningTest;
public final String rootProjectDir;
public final String baseBuildResourcesDir;
public final String baseSrcResourcesDir;
// The parser that will be used to parse both cmd line and config file options
private final OptionParser parser = new OptionParser();
public ApiTestConfig(String... args) {
this.defaultConfigFile = absoluteConfigFile(
Paths.get("apitest", "build", "resources", "main").toFile().getAbsolutePath(),
DEFAULT_CONFIG_FILE_NAME);
this.bitcoinDatadir = Paths.get("apitest", "build", "resources", "main", "Bitcoin-regtest")
.toFile().getAbsolutePath();
this.userDir = getProperty("user.dir");
// If running a @Test, the current working directory is the :apitest subproject
// folder. If running ApiTestMain, the current working directory is the
// bisq root project folder.
this.isRunningTest = Paths.get(userDir).getFileName().toString().equals("apitest");
this.rootProjectDir = isRunningTest
? Paths.get(userDir).getParent().toFile().getAbsolutePath()
: Paths.get(userDir).toFile().getAbsolutePath();
this.baseBuildResourcesDir = Paths.get(rootProjectDir, "apitest", "build", "resources", "main").toFile().getAbsolutePath();
this.baseSrcResourcesDir = Paths.get(rootProjectDir, "apitest", "src", "main", "resources").toFile().getAbsolutePath();
this.defaultConfigFile = absoluteConfigFile(baseBuildResourcesDir, DEFAULT_CONFIG_FILE_NAME);
this.bitcoinDatadir = Paths.get(baseBuildResourcesDir, "Bitcoin-regtest").toFile().getAbsolutePath();
AbstractOptionSpec<Void> helpOpt =
parser.accepts(HELP, "Print this help text")
@ -134,8 +146,7 @@ public class ApiTestConfig {
parser.accepts(ROOT_APP_DATA_DIR, "Application data directory")
.withRequiredArg()
.ofType(File.class)
.defaultsTo(Paths.get("apitest", "build", "resources", "main")
.toFile().getAbsoluteFile());
.defaultsTo(new File(baseBuildResourcesDir));
ArgumentAcceptingOptionSpec<String> bashPathOpt =
parser.accepts(BASH_PATH, "Bash path")
@ -193,7 +204,7 @@ public class ApiTestConfig {
"Run Arbitration node as desktop")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(false); // TODO how do I register arbitrator?
.defaultsTo(false); // TODO how do I register mediator?
ArgumentAcceptingOptionSpec<Boolean> runAliceNodeAsDesktopOpt =
parser.accepts(RUN_ALICE_NODE_AS_DESKTOP,
@ -309,7 +320,7 @@ public class ApiTestConfig {
}
public boolean hasSupportingApp(String... supportingApp) {
return stream(supportingApp).anyMatch(a -> this.supportingApps.contains(a));
return stream(supportingApp).anyMatch(this.supportingApps::contains);
}
public void printHelp(OutputStream sink, HelpFormatter formatter) {

View file

@ -65,11 +65,12 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess {
this.fullDaoNode = true;
this.useLocalhostForP2P = true;
this.useDevPrivilegeKeys = true;
this.findBisqPidScript = config.userDir + "/apitest/scripts/get-bisq-pid.sh";
this.findBisqPidScript = (config.isRunningTest ? "." : "./apitest")
+ "/scripts/get-bisq-pid.sh";
}
@Override
public void start() throws InterruptedException, IOException {
public void start() {
try {
if (config.runSubprojectJars)
runJar(); // run subproject/build/lib/*.jar (not full build)
@ -86,7 +87,7 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess {
}
@Override
public void shutdown() throws IOException, InterruptedException {
public void shutdown() {
try {
log.info("Shutting down {} ...", bisqAppConfig.appName);
if (!isAlive(pid))
@ -156,8 +157,10 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess {
// It runs a bisq-* startup script, and depends on a full build. Bisq jars
// are loaded from the root project's lib directory.
private void runStartupScript() throws IOException, InterruptedException {
String startupScriptPath = config.rootProjectDir
+ "/" + bisqAppConfig.startupScript;
String bisqCmd = getJavaOptsSpec()
+ " " + config.userDir + "/" + bisqAppConfig.startupScript
+ " " + startupScriptPath
+ " " + String.join(" ", getOptsList())
+ " &"; // run in background without nohup
runBashCommand(bisqCmd);
@ -172,7 +175,8 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess {
bashCommand.runInBackground();
if (bashCommand.getExitStatus() != 0)
throw new IllegalStateException(format("Error starting BisqApp\n%s\nError: %s",
throw new IllegalStateException(format("Error starting BisqApp%n%s%nError: %s",
bisqAppConfig.appName,
bashCommand.getError()));
// Sometimes it takes a little extra time to find the linux process id.

View file

@ -62,14 +62,14 @@ class SystemCommandExecutor {
if (log.isDebugEnabled())
log.debug("cmd options {}", cmdOptions.toString());
if (cmdOptions.isEmpty())
throw new IllegalStateException("No command params specified.");
if (cmdOptions.contains("sudo")) {
log.error("", new IllegalStateException("'sudo' commands are prohibited."));
exit(1);
}
if (cmdOptions == null)
throw new IllegalStateException("No command params specified.");
this.cmdOptions = cmdOptions;
}

View file

@ -0,0 +1,62 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import bisq.apitest.config.ApiTestConfig;
import bisq.apitest.method.BitcoinCliHelper;
public class ApiTestCase {
// The gRPC service stubs are used by method & scenario tests, but not e2e tests.
protected static GrpcStubs grpcStubs;
protected static Scaffold scaffold;
protected static ApiTestConfig config;
protected static BitcoinCliHelper bitcoinCli;
public static void setUpScaffold(String supportingApps) {
// The supportingApps argument is a comma delimited string of supporting app
// names, e.g. "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon"
scaffold = new Scaffold(supportingApps).setUp();
config = scaffold.config;
bitcoinCli = new BitcoinCliHelper((config));
grpcStubs = new GrpcStubs(alicedaemon, config).init();
}
public static void setUpScaffold() {
scaffold = new Scaffold(new String[]{}).setUp();
config = scaffold.config;
grpcStubs = new GrpcStubs(alicedaemon, config).init();
}
public static void tearDownScaffold() {
scaffold.tearDown();
}
protected void sleep(long ms) {
try {
MILLISECONDS.sleep(ms);
} catch (InterruptedException ignored) {
}
}
}

View file

@ -0,0 +1,109 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest;
import bisq.proto.grpc.GetVersionGrpc;
import bisq.proto.grpc.OffersGrpc;
import bisq.proto.grpc.PaymentAccountsGrpc;
import bisq.proto.grpc.WalletsGrpc;
import io.grpc.CallCredentials;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import java.util.concurrent.Executor;
import static io.grpc.Metadata.ASCII_STRING_MARSHALLER;
import static io.grpc.Status.UNAUTHENTICATED;
import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.apitest.config.ApiTestConfig;
import bisq.apitest.config.BisqAppConfig;
public class GrpcStubs {
public final CallCredentials credentials;
public final String host;
public final int port;
public GetVersionGrpc.GetVersionBlockingStub versionService;
public OffersGrpc.OffersBlockingStub offersService;
public PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService;
public WalletsGrpc.WalletsBlockingStub walletsService;
public GrpcStubs(BisqAppConfig bisqAppConfig, ApiTestConfig config) {
this.credentials = new PasswordCallCredentials(config.apiPassword);
this.host = "localhost";
this.port = bisqAppConfig.apiPort;
}
public GrpcStubs init() {
var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
channel.shutdown().awaitTermination(1, SECONDS);
} catch (InterruptedException ex) {
throw new IllegalStateException(ex);
}
}));
this.versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials);
return this;
}
static class PasswordCallCredentials extends CallCredentials {
public static final String PASSWORD_KEY = "password";
private final String passwordValue;
public PasswordCallCredentials(String passwordValue) {
if (passwordValue == null)
throw new IllegalArgumentException(format("'%s' value must not be null", PASSWORD_KEY));
this.passwordValue = passwordValue;
}
@Override
public void applyRequestMetadata(RequestInfo requestInfo,
Executor appExecutor,
MetadataApplier metadataApplier) {
appExecutor.execute(() -> {
try {
var headers = new Metadata();
var passwordKey = Metadata.Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER);
headers.put(passwordKey, passwordValue);
metadataApplier.apply(headers);
} catch (Throwable ex) {
metadataApplier.fail(UNAUTHENTICATED.withCause(ex));
}
});
}
@Override
public void thisUsesUnstableApi() {
// An experimental api. A noop but never called; tries to make it clearer to
// implementors that they may break in the future.
}
}
}

View file

@ -0,0 +1,58 @@
package bisq.apitest;
import lombok.extern.slf4j.Slf4j;
import org.junit.runner.Description;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;
import static java.lang.String.format;
@Slf4j
public class JUnitHelper {
private static boolean allPass;
public static void runTests(Class<?>... testClasses) {
JUnitCore jUnitCore = new JUnitCore();
jUnitCore.addListener(new RunListener() {
public void testStarted(Description description) {
log.info("{}", description);
}
public void testIgnored(Description description) {
log.info("Ignored {}", description);
}
public void testFailure(Failure failure) {
log.error("Failed {}", failure.getTrace());
}
});
Result result = jUnitCore.run(testClasses);
printTestResults(result);
}
public static void printTestResults(Result result) {
log.info("Total tests: {}, Failed: {}, Ignored: {}",
result.getRunCount(),
result.getFailureCount(),
result.getIgnoreCount());
if (result.wasSuccessful()) {
log.info("All {} tests passed", result.getRunCount());
allPass = true;
} else if (result.getFailureCount() > 0) {
log.error("{} test(s) failed", result.getFailureCount());
result.getFailures().iterator().forEachRemaining(f -> log.error(format("%s.%s()%n\t%s",
f.getDescription().getTestClass().getName(),
f.getDescription().getMethodName(),
f.getTrace())));
}
}
public static boolean allTestsPassed() {
return allPass;
}
}

View file

@ -0,0 +1,86 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.method;
import java.io.IOException;
import static java.lang.String.format;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import bisq.apitest.config.ApiTestConfig;
import bisq.apitest.linux.BitcoinCli;
public final class BitcoinCliHelper {
private final ApiTestConfig config;
public BitcoinCliHelper(ApiTestConfig config) {
this.config = config;
}
// Convenience methods for making bitcoin-cli calls.
public String getNewBtcAddress() {
try {
String newAddress = new BitcoinCli(config, "getnewaddress").run().getOutput();
assertNotNull(newAddress);
return newAddress;
} catch (IOException | InterruptedException ex) {
fail(ex.getMessage());
return null;
}
}
public String[] generateToAddress(int blocks, String address) {
try {
String generateToAddressCmd = format("generatetoaddress %d \"%s\"", blocks, address);
BitcoinCli generateToAddress = new BitcoinCli(config, generateToAddressCmd).run();
String[] txids = generateToAddress.getOutputValueAsStringArray();
assertNotNull(txids);
return txids;
} catch (IOException | InterruptedException ex) {
fail(ex.getMessage());
return null;
}
}
public void generateBlocks(int blocks) {
generateToAddress(blocks, getNewBtcAddress());
}
public String sendToAddress(String address, String amount) {
// sendtoaddress "address" amount \
// ( "comment" "comment_to" subtractfeefromamount \
// replaceable conf_target "estimate_mode" )
// returns a transaction id
try {
String sendToAddressCmd = format("sendtoaddress \"%s\" %s \"\" \"\" false",
address, amount);
BitcoinCli sendToAddress = new BitcoinCli(config, sendToAddressCmd).run();
String txid = sendToAddress.getOutput();
assertNotNull(txid);
return txid;
} catch (IOException | InterruptedException ex) {
fail(ex.getMessage());
return null;
}
}
}

View file

@ -0,0 +1,66 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.method;
import bisq.proto.grpc.GetBalanceRequest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
@Slf4j
@TestMethodOrder(OrderAnnotation.class)
public class GetBalanceTest extends MethodTest {
@BeforeAll
public static void setUp() {
try {
setUpScaffold("bitcoind,seednode,alicedaemon");
// Have to generate 1 regtest block for alice's wallet to show 10 BTC balance.
bitcoinCli.generateBlocks(1);
// Give the alicedaemon time to parse the new block.
MILLISECONDS.sleep(1500);
} catch (InterruptedException ex) {
fail(ex.getMessage());
}
}
@Test
@Order(1)
public void testGetBalance() {
var balance = grpcStubs.walletsService.getBalance(GetBalanceRequest.newBuilder().build()).getBalance();
assertEquals(1000000000, balance);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}

View file

@ -0,0 +1,56 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.method;
import bisq.proto.grpc.GetVersionRequest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.common.app.Version.VERSION;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
@Slf4j
@TestMethodOrder(OrderAnnotation.class)
public class GetVersionTest extends MethodTest {
@BeforeAll
public static void setUp() {
setUpScaffold(alicedaemon.name());
}
@Test
@Order(1)
public void testGetVersion() {
var version = grpcStubs.versionService.getVersion(GetVersionRequest.newBuilder().build()).getVersion();
assertEquals(VERSION, version);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}

View file

@ -0,0 +1,89 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.method;
import bisq.proto.grpc.GetBalanceRequest;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.apitest.ApiTestCase;
public class MethodTest extends ApiTestCase {
// Convenience methods for building gRPC request objects
protected final GetBalanceRequest createBalanceRequest() {
return GetBalanceRequest.newBuilder().build();
}
protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String password) {
return SetWalletPasswordRequest.newBuilder().setPassword(password).build();
}
protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String oldPassword, String newPassword) {
return SetWalletPasswordRequest.newBuilder().setPassword(oldPassword).setNewPassword(newPassword).build();
}
protected final RemoveWalletPasswordRequest createRemoveWalletPasswordRequest(String password) {
return RemoveWalletPasswordRequest.newBuilder().setPassword(password).build();
}
protected final UnlockWalletRequest createUnlockWalletRequest(String password, long timeout) {
return UnlockWalletRequest.newBuilder().setPassword(password).setTimeout(timeout).build();
}
protected final LockWalletRequest createLockWalletRequest() {
return LockWalletRequest.newBuilder().build();
}
protected final GetFundingAddressesRequest createGetFundingAddressesRequest() {
return GetFundingAddressesRequest.newBuilder().build();
}
// Convenience methods for calling frequently used & thoroughly tested gRPC services.
protected final long getBalance() {
return grpcStubs.walletsService.getBalance(createBalanceRequest()).getBalance();
}
protected final void unlockWallet(String password, long timeout) {
//noinspection ResultOfMethodCallIgnored
grpcStubs.walletsService.unlockWallet(createUnlockWalletRequest(password, timeout));
}
protected final void lockWallet() {
//noinspection ResultOfMethodCallIgnored
grpcStubs.walletsService.lockWallet(createLockWalletRequest());
}
protected final String getUnusedBtcAddress() {
return grpcStubs.walletsService.getFundingAddresses(createGetFundingAddressesRequest())
.getAddressBalanceInfoList()
.stream()
.filter(a -> a.getBalance() == 0 && a.getNumConfirmations() == 0)
.findFirst()
.get()
.getAddress();
}
}

View file

@ -0,0 +1,137 @@
package bisq.apitest.method;
import io.grpc.StatusRuntimeException;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
@SuppressWarnings("ResultOfMethodCallIgnored")
@Slf4j
@TestMethodOrder(OrderAnnotation.class)
public class WalletProtectionTest extends MethodTest {
@BeforeAll
public static void setUp() {
setUpScaffold(alicedaemon.name());
try {
TimeUnit.MILLISECONDS.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Test
@Order(1)
public void testSetWalletPassword() {
var request = createSetWalletPasswordRequest("first-password");
grpcStubs.walletsService.setWalletPassword(request);
}
@Test
@Order(2)
public void testGetBalanceOnEncryptedWalletShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, this::getBalance);
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
}
@Test
@Order(3)
public void testUnlockWalletFor4Seconds() {
var request = createUnlockWalletRequest("first-password", 4);
grpcStubs.walletsService.unlockWallet(request);
getBalance(); // should not throw 'wallet locked' exception
sleep(4500); // let unlock timeout expire
Throwable exception = assertThrows(StatusRuntimeException.class, this::getBalance);
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
}
@Test
@Order(4)
public void testGetBalanceAfterUnlockTimeExpiryShouldThrowException() {
var request = createUnlockWalletRequest("first-password", 3);
grpcStubs.walletsService.unlockWallet(request);
sleep(4000); // let unlock timeout expire
Throwable exception = assertThrows(StatusRuntimeException.class, this::getBalance);
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
}
@Test
@Order(5)
public void testLockWalletBeforeUnlockTimeoutExpiry() {
unlockWallet("first-password", 60);
var request = createLockWalletRequest();
grpcStubs.walletsService.lockWallet(request);
Throwable exception = assertThrows(StatusRuntimeException.class, this::getBalance);
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
}
@Test
@Order(6)
public void testLockWalletWhenWalletAlreadyLockedShouldThrowException() {
var request = createLockWalletRequest();
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
grpcStubs.walletsService.lockWallet(request));
assertEquals("UNKNOWN: wallet is already locked", exception.getMessage());
}
@Test
@Order(7)
public void testUnlockWalletTimeoutOverride() {
unlockWallet("first-password", 2);
sleep(500); // override unlock timeout after 0.5s
unlockWallet("first-password", 6);
sleep(5000);
getBalance(); // getbalance 5s after resetting unlock timeout to 6s
}
@Test
@Order(8)
public void testSetNewWalletPassword() {
var request = createSetWalletPasswordRequest("first-password", "second-password");
grpcStubs.walletsService.setWalletPassword(request);
unlockWallet("second-password", 2);
getBalance();
sleep(2500); // allow time for wallet save
}
@Test
@Order(9)
public void testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException() {
var request = createSetWalletPasswordRequest("bad old password", "irrelevant");
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
grpcStubs.walletsService.setWalletPassword(request));
assertEquals("UNKNOWN: incorrect old password", exception.getMessage());
}
@Test
@Order(10)
public void testRemoveNewWalletPassword() {
var request = createRemoveWalletPasswordRequest("second-password");
grpcStubs.walletsService.removeWalletPassword(request);
getBalance(); // should not throw 'wallet locked' exception
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}

View file

@ -0,0 +1,69 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class FundWalletScenarioTest extends ScenarioTest {
@BeforeAll
public static void setUp() {
try {
setUpScaffold("bitcoind,seednode,alicedaemon");
bitcoinCli.generateBlocks(1);
MILLISECONDS.sleep(1500);
} catch (InterruptedException ex) {
fail(ex.getMessage());
}
}
@Test
@Order(1)
public void testFundWallet() {
long balance = getBalance(); // bisq wallet was initialized with 10 btc
assertEquals(1000000000, balance);
String unusedAddress = getUnusedBtcAddress();
bitcoinCli.sendToAddress(unusedAddress, "2.5");
bitcoinCli.generateBlocks(1);
sleep(1500);
balance = getBalance();
assertEquals(1250000000L, balance); // new balance is 12.5 btc
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}

View file

@ -0,0 +1,28 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario;
import lombok.extern.slf4j.Slf4j;
import bisq.apitest.method.MethodTest;
@Slf4j
public class ScenarioTest extends MethodTest {
}

View file

@ -579,6 +579,14 @@ configure(project(':apitest')) {
// ./gradlew :apitest:cleanDaoSetup -x test
apply from: 'dao-setup.gradle'
// We have to disable the :apitest 'test' task by default because we do not want
// to interfere with normal builds. To run JUnit tests in this subproject:
// Run a normal build and install dao-setup files:
// 'gradle clean build :apitest:installDaoSetup'
// Run apitest tests cases
// 'gradle :apitest:test' -Dforce=true
test.enabled = System.getProperty("force") == "true"
sourceSets {
main {
resources {
@ -588,6 +596,13 @@ configure(project(':apitest')) {
}
}
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
}
}
dependencies {
compile project(':proto')
compile project(':common')
@ -609,10 +624,17 @@ configure(project(':apitest')) {
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation "ch.qos.logback:logback-core:$logbackVersion"
implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation "junit:junit:$junitVersion"
compileOnly "org.projectlombok:lombok:$lombokVersion"
compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
testCompile "org.junit.jupiter:junit-jupiter-api:5.6.2"
testCompile "org.junit.jupiter:junit-jupiter-params:5.6.2"
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
testRuntime "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.6.2")
}
}