cmdOptions) {
+ if (log.isDebugEnabled())
+ log.debug("cmd options {}", cmdOptions.toString());
+
+ if (cmdOptions.isEmpty())
+ throw new IllegalStateException("No command params specified.");
+
+ if (cmdOptions.contains("sudo"))
+ throw new IllegalStateException("'sudo' commands are prohibited.");
+
+ this.cmdOptions = cmdOptions;
+ }
+
+ // Execute a system command and return its status code (0 or 1).
+ // The system command's output (stderr or stdout) can be accessed from accessors.
+ public int exec() throws IOException, InterruptedException {
+ return exec(true);
+ }
+
+ // Execute a system command and return its status code (0 or 1).
+ // The system command's output (stderr or stdout) can be accessed from accessors
+ // if the waitOnErrStream flag is true, else the method will not wait on (join)
+ // the error stream handler thread.
+ public int exec(boolean waitOnErrStream) throws IOException, InterruptedException {
+ Process process = new ProcessBuilder(cmdOptions).start();
+
+ // I'm currently doing these on a separate line here in case i need to set them to null
+ // to get the threads to stop.
+ // see http://java.sun.com/j2se/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html
+ InputStream inputStream = process.getInputStream();
+ InputStream errorStream = process.getErrorStream();
+
+ // These need to run as java threads to get the standard output and error from the command.
+ // the inputstream handler gets a reference to our stdOutput in case we need to write
+ // something to it.
+ inputStreamHandler = new ThreadedStreamHandler(inputStream);
+ errorStreamHandler = new ThreadedStreamHandler(errorStream);
+
+ inputStreamHandler.start();
+ errorStreamHandler.start();
+
+ int exitStatus = process.waitFor();
+
+ inputStreamHandler.interrupt();
+ errorStreamHandler.interrupt();
+
+ inputStreamHandler.join();
+ if (waitOnErrStream)
+ errorStreamHandler.join();
+
+ return exitStatus;
+ }
+
+ // Get the standard error from an executed system command.
+ public StringBuilder getStandardErrorFromCommand() {
+ return errorStreamHandler.getOutputBuffer();
+ }
+
+ // Get the standard output from an executed system command.
+ public StringBuilder getStandardOutputFromCommand() {
+ return inputStreamHandler.getOutputBuffer();
+ }
+}
diff --git a/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java b/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java
new file mode 100644
index 00000000000..540b1361009
--- /dev/null
+++ b/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java
@@ -0,0 +1,91 @@
+/*
+ * 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 .
+ */
+
+package bisq.apitest.linux;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * This class is intended to be used with the SystemCommandExecutor
+ * class to let users execute system commands from Java applications.
+ *
+ * This class is based on work that was shared in a JavaWorld article
+ * named "When System.exec() won't". That article is available at this
+ * url:
+ *
+ * http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html
+ *
+ * Documentation for this class is available at this URL:
+ *
+ * http://devdaily.com/java/java-processbuilder-process-system-exec
+ *
+ *
+ * Copyright 2010 alvin j. alexander, devdaily.com.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program 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 Lesser Public License for more details.
+ * You should have received a copy of the GNU Lesser Public License
+ * along with this program. If not, see .
+ *
+ * Please ee the following page for the LGPL license:
+ * http://www.gnu.org/licenses/lgpl.txt
+ *
+ */
+@Slf4j
+class ThreadedStreamHandler extends Thread {
+ final InputStream inputStream;
+ final StringBuilder outputBuffer = new StringBuilder();
+
+ ThreadedStreamHandler(InputStream inputStream) {
+ this.inputStream = inputStream;
+ }
+
+ public void run() {
+ try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
+ String line;
+ while ((line = bufferedReader.readLine()) != null)
+ outputBuffer.append(line).append("\n");
+
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private void doSleep(long millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (InterruptedException ignored) {
+ // empty
+ }
+ }
+
+ public StringBuilder getOutputBuffer() {
+ return outputBuffer;
+ }
+}
+
diff --git a/apitest/src/main/resources/apitest.properties b/apitest/src/main/resources/apitest.properties
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/apitest/src/main/resources/blocknotify b/apitest/src/main/resources/blocknotify
new file mode 100755
index 00000000000..210d23f17c6
--- /dev/null
+++ b/apitest/src/main/resources/blocknotify
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# Regtest ports start with 512*
+
+# To avoid pesky bitcoind io errors, do not specify ports Bisq is not listening to.
+
+# SeedNode listens on port 5120
+echo $1 | nc -w 1 127.0.0.1 5120
+
+# Arb Node listens on port 5121
+echo $1 | nc -w 1 127.0.0.1 5121
+
+# Alice Node listens on port 5122
+echo $1 | nc -w 1 127.0.0.1 5122
+
+# Bob Node listens on port 5123
+echo $1 | nc -w 1 127.0.0.1 5123
+
+# Some other node listens on port 5124, etc.
+# echo $1 | nc -w 1 127.0.0.1 5124
diff --git a/apitest/src/main/resources/logback.xml b/apitest/src/main/resources/logback.xml
new file mode 100644
index 00000000000..9c5d0974bff
--- /dev/null
+++ b/apitest/src/main/resources/logback.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+ %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n)
+
+
+
+
+
+
+
+
+
+
diff --git a/apitest/src/test/java/bisq/apitest/ApiTestCase.java b/apitest/src/test/java/bisq/apitest/ApiTestCase.java
new file mode 100644
index 00000000000..286e7f8c206
--- /dev/null
+++ b/apitest/src/test/java/bisq/apitest/ApiTestCase.java
@@ -0,0 +1,94 @@
+/*
+ * 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 .
+ */
+
+package bisq.apitest;
+
+import java.io.IOException;
+
+import java.util.concurrent.ExecutionException;
+
+import static bisq.apitest.config.BisqAppConfig.alicedaemon;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+
+
+import bisq.apitest.config.ApiTestConfig;
+import bisq.apitest.method.BitcoinCliHelper;
+
+/**
+ * Base class for all test types: 'method', 'scenario' and 'e2e'.
+ *
+ * During scaffold setup, various combinations of bitcoind and bisq instances
+ * can be started in the background before test cases are run. Currently, this test
+ * harness supports only the "Bisq DAO development environment running against a
+ * local Bitcoin regtest network" as described in
+ * dev-setup.md
+ * and dao-setup.md.
+ *
+ * Those documents contain information about the configurations used by this test harness:
+ * bitcoin-core's bitcoin.conf and blocknotify values, bisq instance options, the DAO genesis
+ * transaction id, initial BSQ and BTC balances for Bob & Alice accounts, and default
+ * PerfectMoney dummy payment accounts (USD) for Bob and Alice.
+ *
+ * During a build, the
+ * dao-setup.zip
+ * file is downloaded and extracted if necessary. In each test case's @BeforeClass
+ * method, the DAO setup files are re-installed into the run time's data directories
+ * (each test case runs on a refreshed DAO/regtest environment setup).
+ *
+ * Initial Alice balances & accounts: 10.0 BTC, 1000000.00 BSQ, USD PerfectMoney dummy
+ *
+ * Initial Bob balances & accounts: 10.0 BTC, 1500000.00 BSQ, USD PerfectMoney dummy
+ */
+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)
+ throws InterruptedException, ExecutionException, IOException {
+ // 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()
+ throws InterruptedException, ExecutionException, IOException {
+ 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) {
+ // empty
+ }
+ }
+}
diff --git a/apitest/src/test/java/bisq/apitest/GrpcStubs.java b/apitest/src/test/java/bisq/apitest/GrpcStubs.java
new file mode 100644
index 00000000000..6279c61489f
--- /dev/null
+++ b/apitest/src/test/java/bisq/apitest/GrpcStubs.java
@@ -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 .
+ */
+
+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.
+ }
+ }
+}
diff --git a/apitest/src/test/java/bisq/apitest/JUnitHelper.java b/apitest/src/test/java/bisq/apitest/JUnitHelper.java
new file mode 100644
index 00000000000..8ea80ad0d5e
--- /dev/null
+++ b/apitest/src/test/java/bisq/apitest/JUnitHelper.java
@@ -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 boolean allTestsPassed() {
+ return allPass;
+ }
+
+ private 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())));
+ }
+ }
+}
diff --git a/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java b/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java
new file mode 100644
index 00000000000..6e5b52080f3
--- /dev/null
+++ b/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java
@@ -0,0 +1,92 @@
+/*
+ * 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 .
+ */
+
+package bisq.apitest.method;
+
+import java.io.IOException;
+
+import static java.lang.String.format;
+import static org.junit.jupiter.api.Assertions.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 {
+ BitcoinCli newAddress = new BitcoinCli(config, "getnewaddress").run();
+
+ if (newAddress.isError())
+ fail(format("Could not generate new bitcoin address:%n%s", newAddress.getErrorMessage()));
+
+ return newAddress.getOutput();
+ } catch (IOException | InterruptedException ex) {
+ fail(ex);
+ 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();
+
+ if (generateToAddress.isError())
+ fail(format("Could not generate bitcoin block(s):%n%s", generateToAddress.getErrorMessage()));
+
+ return generateToAddress.getOutputValueAsStringArray();
+ } catch (IOException | InterruptedException ex) {
+ fail(ex);
+ 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();
+
+ if (sendToAddress.isError())
+ fail(format("Could not send BTC to address:%n%s", sendToAddress.getErrorMessage()));
+
+ return sendToAddress.getOutput();
+ } catch (IOException | InterruptedException ex) {
+ fail(ex);
+ return null;
+ }
+ }
+}
diff --git a/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java b/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java
new file mode 100644
index 00000000000..2cf4e8ae1cd
--- /dev/null
+++ b/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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 .
+ */
+
+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 (Exception ex) {
+ fail(ex);
+ }
+ }
+
+ @Test
+ @Order(1)
+ public void testGetBalance() {
+ // All tests depend on the DAO / regtest environment, and Alice's wallet is
+ // initialized with 10 BTC during the scaffolding setup.
+ var balance = grpcStubs.walletsService.getBalance(GetBalanceRequest.newBuilder().build()).getBalance();
+ assertEquals(1000000000, balance);
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ tearDownScaffold();
+ }
+}
diff --git a/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java b/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java
new file mode 100644
index 00000000000..22413cf9d3c
--- /dev/null
+++ b/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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 .
+ */
+
+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.Assertions.fail;
+import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
+
+
+@Slf4j
+@TestMethodOrder(OrderAnnotation.class)
+public class GetVersionTest extends MethodTest {
+
+ @BeforeAll
+ public static void setUp() {
+ try {
+ setUpScaffold(alicedaemon.name());
+ } catch (Exception ex) {
+ fail(ex);
+ }
+ }
+
+ @Test
+ @Order(1)
+ public void testGetVersion() {
+ var version = grpcStubs.versionService.getVersion(GetVersionRequest.newBuilder().build()).getVersion();
+ assertEquals(VERSION, version);
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ tearDownScaffold();
+ }
+}
diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java
new file mode 100644
index 00000000000..694aa6806e3
--- /dev/null
+++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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 .
+ */
+
+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();
+ }
+}
diff --git a/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java b/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java
new file mode 100644
index 00000000000..450fb58e010
--- /dev/null
+++ b/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java
@@ -0,0 +1,135 @@
+package bisq.apitest.method;
+
+import io.grpc.StatusRuntimeException;
+
+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 java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
+
+@SuppressWarnings("ResultOfMethodCallIgnored")
+@Slf4j
+@TestMethodOrder(OrderAnnotation.class)
+public class WalletProtectionTest extends MethodTest {
+
+ @BeforeAll
+ public static void setUp() {
+ try {
+ setUpScaffold(alicedaemon.name());
+ MILLISECONDS.sleep(2000);
+ } catch (Exception ex) {
+ fail(ex);
+ }
+ }
+
+ @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();
+ }
+}
diff --git a/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java b/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java
new file mode 100644
index 00000000000..e95e310eb58
--- /dev/null
+++ b/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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 .
+ */
+
+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.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.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 (Exception ex) {
+ fail(ex);
+ }
+ }
+
+ @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();
+ }
+}
diff --git a/apitest/src/test/java/bisq/apitest/scenario/ScenarioTest.java b/apitest/src/test/java/bisq/apitest/scenario/ScenarioTest.java
new file mode 100644
index 00000000000..9750b2ed9d6
--- /dev/null
+++ b/apitest/src/test/java/bisq/apitest/scenario/ScenarioTest.java
@@ -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 .
+ */
+
+package bisq.apitest.scenario;
+
+import lombok.extern.slf4j.Slf4j;
+
+
+
+import bisq.apitest.method.MethodTest;
+
+@Slf4j
+public class ScenarioTest extends MethodTest {
+}
diff --git a/build.gradle b/build.gradle
index 8e83abb1830..406c99e327a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -98,7 +98,8 @@ configure([project(':cli'),
project(':relay'),
project(':seednode'),
project(':statsnode'),
- project(':pricenode')]) {
+ project(':pricenode'),
+ project(':apitest')]) {
apply plugin: 'application'
@@ -143,6 +144,16 @@ configure([project(':cli'),
'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="-XX:MaxRAM=4g"')
}
+ if (applicationName == 'apitest') {
+ // Pass the logback config file as a system property to avoid chatty
+ // logback startup due to multiple logback.xml files in the classpath
+ // (:daemon & :cli).
+ def script = file("${rootProject.projectDir}/bisq-$applicationName")
+ script.text = script.text.replace(
+ 'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="' +
+ '-Dlogback.configurationFile=apitest/build/resources/main/logback.xml"')
+ }
+
if (osdetector.os != 'windows')
delete fileTree(dir: rootProject.projectDir, include: 'bisq-*.bat')
else
@@ -551,3 +562,77 @@ configure(project(':daemon')) {
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
}
}
+
+configure(project(':apitest')) {
+ mainClassName = 'bisq.apitest.ApiTestMain'
+
+ // The external dao-setup.gradle file contains tasks to install and clean dao-setup
+ // files downloaded from
+ // https://github.com/bisq-network/bisq/raw/master/docs/dao-setup.zip
+ // These tasks are not run by the default build, but they can can be run during
+ // full or partial builds, or by themselves.
+ // To run the regular clean + build + test (non api), and install dao-setup files:
+ // ./gradlew clean build :apitest:installDaoSetup
+ // To install or re-install dao-setup file only:
+ // ./gradlew :apitest:installDaoSetup -x test
+ // To clean installed dao-setup files:
+ // ./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 first, then run:
+ // 'gradle :apitest:test -DrunApiTests=true'
+ test.enabled = System.getProperty("runApiTests") == "true"
+
+ sourceSets {
+ main {
+ resources {
+ exclude 'dao-setup'
+ exclude 'dao-setup.zip'
+ }
+ }
+ }
+
+ test {
+ useJUnitPlatform()
+ testLogging {
+ events "passed", "skipped", "failed"
+ }
+ }
+
+ dependencies {
+ compile project(':proto')
+ compile project(':common')
+ compile project(':seednode')
+ compile project(':desktop')
+ compile project(':daemon')
+ compile project(':cli')
+ implementation "net.sf.jopt-simple:jopt-simple:$joptVersion"
+ implementation "com.google.guava:guava:$guavaVersion"
+ implementation "com.google.protobuf:protobuf-java:$protobufVersion"
+ implementation("io.grpc:grpc-protobuf:$grpcVersion") {
+ exclude(module: 'guava')
+ exclude(module: 'animal-sniffer-annotations')
+ }
+ implementation("io.grpc:grpc-stub:$grpcVersion") {
+ exclude(module: 'guava')
+ exclude(module: 'animal-sniffer-annotations')
+ }
+ implementation "org.slf4j:slf4j-api:$slf4jVersion"
+ implementation "ch.qos.logback:logback-core:$logbackVersion"
+ implementation "ch.qos.logback:logback-classic:$logbackVersion"
+
+ 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")
+ }
+}
+
diff --git a/cli/src/main/java/bisq/cli/CurrencyFormat.java b/cli/src/main/java/bisq/cli/CurrencyFormat.java
index 859dc4e8e8f..527ab8ca612 100644
--- a/cli/src/main/java/bisq/cli/CurrencyFormat.java
+++ b/cli/src/main/java/bisq/cli/CurrencyFormat.java
@@ -16,7 +16,7 @@ class CurrencyFormat {
static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000");
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
- static final String formatSatoshis(long sats) {
+ static String formatSatoshis(long sats) {
return BTC_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR));
}
diff --git a/cli/test.sh b/cli/test.sh
index 54af77c717b..9878d93fd02 100755
--- a/cli/test.sh
+++ b/cli/test.sh
@@ -48,14 +48,14 @@
run ./bisq-cli --password="xyz" getversion
[ "$status" -eq 0 ]
echo "actual output: $output" >&2
- [ "$output" = "1.3.4" ]
+ [ "$output" = "1.3.5" ]
}
@test "test getversion" {
run ./bisq-cli --password=xyz getversion
[ "$status" -eq 0 ]
echo "actual output: $output" >&2
- [ "$output" = "1.3.4" ]
+ [ "$output" = "1.3.5" ]
}
@test "test setwalletpassword \"a b c\"" {
diff --git a/settings.gradle b/settings.gradle
index cdfa7c7126a..45ca5993367 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -11,5 +11,6 @@ include 'pricenode'
include 'relay'
include 'seednode'
include 'statsnode'
+include 'apitest'
rootProject.name = 'bisq'