mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-28 05:35:44 +00:00
Payments.
Co-authored-by: Alan Evans <alan@signal.org> Co-authored-by: Alex Hart <alex@signal.org> Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.thoughtcrime.securesms.payments.proto.PaymentMetaData;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
public final class PaymentMetaDataUtilTest {
|
||||
|
||||
@Test
|
||||
public void extract_single_public_key() {
|
||||
byte[] random = Util.getSecretBytes(32);
|
||||
byte[] bytes = PaymentMetaDataUtil.receiptPublic(PaymentMetaData.newBuilder()
|
||||
.setMobileCoinTxoIdentification(PaymentMetaData.MobileCoinTxoIdentification.newBuilder()
|
||||
.addPublicKey(ByteString.copyFrom(random))).build());
|
||||
assertArrayEquals(random, bytes);
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,8 @@ import static org.junit.Assert.assertTrue;
|
||||
public final class SupportArticleTest {
|
||||
|
||||
private static final File MAIN_STRINGS = new File("src/main/res/values/strings.xml");
|
||||
private static final Pattern SUPPORT_ARTICLE = Pattern.compile(".*:\\/\\/support.signal.org\\/.*articles\\/.*" );
|
||||
private static final Pattern CORRECT_SUPPORT_ARTICLE = Pattern.compile("https:\\/\\/support.signal.org\\/hc\\/articles\\/\\d+");
|
||||
private static final Pattern SUPPORT_ARTICLE = Pattern.compile(".*:\\/\\/support.signal.org\\/.*articles\\/.*");
|
||||
private static final Pattern CORRECT_SUPPORT_ARTICLE = Pattern.compile("https:\\/\\/support.signal.org\\/hc\\/articles\\/\\d+(#[a-z_]+)?");
|
||||
|
||||
/**
|
||||
* Tests that support articles found in strings.xml:
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.thoughtcrime.securesms.payments;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.testutil.EmptyLogger;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assume.assumeFalse;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
public final class GeographicalRestrictionsTest {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
Log.initialize(new EmptyLogger());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bad_number_not_allowed() {
|
||||
assertFalse(GeographicalRestrictions.e164Allowed("bad_number"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void null_not_allowed() {
|
||||
assertFalse(GeographicalRestrictions.e164Allowed(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uk_allowed() {
|
||||
assertTrue(GeographicalRestrictions.e164Allowed("+441617151234"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void us_allowed_in_debug() {
|
||||
assumeTrue(BuildConfig.DEBUG);
|
||||
assertTrue(GeographicalRestrictions.e164Allowed("+15407011234"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void us_not_allowed_in_release() {
|
||||
assumeFalse(BuildConfig.DEBUG);
|
||||
assertFalse(GeographicalRestrictions.e164Allowed("+15407011234"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package org.thoughtcrime.securesms.payments;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.thoughtcrime.securesms.testutil.LibSignalLibraryUtil.assumeLibSignalSupportedOnOS;
|
||||
|
||||
public final class MobileCoinPublicAddressProfileUtilTest {
|
||||
|
||||
@Before
|
||||
public void ensureNativeSupported() {
|
||||
assumeLibSignalSupportedOnOS();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_verify_an_address() throws PaymentsAddressException {
|
||||
IdentityKeyPair identityKeyPair = IdentityKeyUtil.generateIdentityKeyPair();
|
||||
byte[] address = Util.getSecretBytes(100);
|
||||
SignalServiceProtos.PaymentAddress signedPaymentAddress = MobileCoinPublicAddressProfileUtil.signPaymentsAddress(address, identityKeyPair);
|
||||
|
||||
byte[] paymentsAddress = MobileCoinPublicAddressProfileUtil.verifyPaymentsAddress(signedPaymentAddress, identityKeyPair.getPublicKey());
|
||||
|
||||
assertArrayEquals(address, paymentsAddress);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_not_verify_an_address_with_the_wrong_key() {
|
||||
IdentityKeyPair identityKeyPair = IdentityKeyUtil.generateIdentityKeyPair();
|
||||
IdentityKey wrongPublicKey = IdentityKeyUtil.generateIdentityKeyPair().getPublicKey();
|
||||
byte[] address = Util.getSecretBytes(100);
|
||||
SignalServiceProtos.PaymentAddress signedPaymentAddress = MobileCoinPublicAddressProfileUtil.signPaymentsAddress(address, identityKeyPair);
|
||||
|
||||
assertThatThrownBy(() -> MobileCoinPublicAddressProfileUtil.verifyPaymentsAddress(signedPaymentAddress, wrongPublicKey))
|
||||
.isInstanceOf(PaymentsAddressException.class)
|
||||
.hasMessage("Invalid MobileCoin address signature on payments address proto");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_not_verify_a_tampered_signature() {
|
||||
IdentityKeyPair identityKeyPair = IdentityKeyUtil.generateIdentityKeyPair();
|
||||
byte[] address = Util.getSecretBytes(100);
|
||||
SignalServiceProtos.PaymentAddress signedPaymentAddress = MobileCoinPublicAddressProfileUtil.signPaymentsAddress(address, identityKeyPair);
|
||||
|
||||
byte[] signature = signedPaymentAddress.getMobileCoinAddress().getSignature().toByteArray();
|
||||
signature[0] = (byte) (signature[0] ^ 0x01);
|
||||
SignalServiceProtos.PaymentAddress tamperedSignature = signedPaymentAddress.toBuilder()
|
||||
.setMobileCoinAddress(signedPaymentAddress.getMobileCoinAddress()
|
||||
.toBuilder()
|
||||
.setSignature(ByteString.copyFrom(signature)))
|
||||
.build();
|
||||
|
||||
assertThatThrownBy(() -> MobileCoinPublicAddressProfileUtil.verifyPaymentsAddress(tamperedSignature, identityKeyPair.getPublicKey()))
|
||||
.isInstanceOf(PaymentsAddressException.class)
|
||||
.hasMessage("Invalid MobileCoin address signature on payments address proto");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_not_verify_a_tampered_address() {
|
||||
IdentityKeyPair identityKeyPair = IdentityKeyUtil.generateIdentityKeyPair();
|
||||
byte[] addressBytes = Util.getSecretBytes(100);
|
||||
SignalServiceProtos.PaymentAddress signedPaymentAddress = MobileCoinPublicAddressProfileUtil.signPaymentsAddress(addressBytes, identityKeyPair);
|
||||
|
||||
byte[] address = signedPaymentAddress.getMobileCoinAddress().getAddress().toByteArray();
|
||||
address[0] = (byte) (address[0] ^ 0x01);
|
||||
SignalServiceProtos.PaymentAddress tamperedAddress = signedPaymentAddress.toBuilder()
|
||||
.setMobileCoinAddress(signedPaymentAddress.getMobileCoinAddress()
|
||||
.toBuilder()
|
||||
.setAddress(ByteString.copyFrom(address)))
|
||||
.build();
|
||||
|
||||
assertThatThrownBy(() -> MobileCoinPublicAddressProfileUtil.verifyPaymentsAddress(tamperedAddress, identityKeyPair.getPublicKey()))
|
||||
.isInstanceOf(PaymentsAddressException.class)
|
||||
.hasMessage("Invalid MobileCoin address signature on payments address proto");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_not_verify_a_missing_signature() {
|
||||
IdentityKeyPair identityKeyPair = IdentityKeyUtil.generateIdentityKeyPair();
|
||||
byte[] address = Util.getSecretBytes(100);
|
||||
SignalServiceProtos.PaymentAddress signedPaymentAddress = MobileCoinPublicAddressProfileUtil.signPaymentsAddress(address, identityKeyPair);
|
||||
|
||||
SignalServiceProtos.PaymentAddress removedSignature = signedPaymentAddress.toBuilder()
|
||||
.setMobileCoinAddress(signedPaymentAddress.getMobileCoinAddress()
|
||||
.toBuilder()
|
||||
.clearSignature())
|
||||
.build();
|
||||
|
||||
assertThatThrownBy(() -> MobileCoinPublicAddressProfileUtil.verifyPaymentsAddress(removedSignature, identityKeyPair.getPublicKey()))
|
||||
.isInstanceOf(PaymentsAddressException.class)
|
||||
.hasMessage("Invalid MobileCoin address signature on payments address proto");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_not_verify_a_missing_address() {
|
||||
IdentityKeyPair identityKeyPair = IdentityKeyUtil.generateIdentityKeyPair();
|
||||
byte[] address = Util.getSecretBytes(100);
|
||||
SignalServiceProtos.PaymentAddress signedPaymentAddress = MobileCoinPublicAddressProfileUtil.signPaymentsAddress(address, identityKeyPair);
|
||||
|
||||
SignalServiceProtos.PaymentAddress removedAddress = signedPaymentAddress.toBuilder()
|
||||
.setMobileCoinAddress(signedPaymentAddress.getMobileCoinAddress()
|
||||
.toBuilder()
|
||||
.clearAddress())
|
||||
.build();
|
||||
|
||||
assertThatThrownBy(() -> MobileCoinPublicAddressProfileUtil.verifyPaymentsAddress(removedAddress, identityKeyPair.getPublicKey()))
|
||||
.isInstanceOf(PaymentsAddressException.class)
|
||||
.hasMessage("Invalid MobileCoin address signature on payments address proto");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.thoughtcrime.securesms.payments;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.whispersystems.signalservice.api.payments.Currency;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE, application = Application.class)
|
||||
public class MoneyViewTest {
|
||||
|
||||
private final Currency currency = Currency.fromCodeAndPrecision("MOB", 12);
|
||||
|
||||
private MoneyView testSubject;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
testSubject = new MoneyView(ApplicationProvider.getApplicationContext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void given1AndMOB_whenISetMoney_thenIExpect1MOB() {
|
||||
testSubject.setMoney("1", currency);
|
||||
|
||||
String formatted = testSubject.getText().toString();
|
||||
|
||||
assertEquals("1MOB", formatted);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenTrailingDecimal_whenISetMoney_thenIExpectTrailingDecimal() {
|
||||
testSubject.setMoney("1.", currency);
|
||||
|
||||
String formatted = testSubject.getText().toString();
|
||||
|
||||
assertEquals("1.MOB", formatted);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenNoTrailingDecimal_whenISetMoney_thenIExpectNoTrailingDecimal() {
|
||||
testSubject.setMoney("1.0", currency);
|
||||
|
||||
String formatted = testSubject.getText().toString();
|
||||
|
||||
assertEquals("1.0MOB", formatted);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenLongNoTrailingDecimal_whenISetMoney_thenIExpectNoTrailingDecimal() {
|
||||
testSubject.setMoney("1.00000000000", currency);
|
||||
|
||||
String formatted = testSubject.getText().toString();
|
||||
|
||||
assertEquals("1.00000000000MOB", formatted);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenDecimalWithTrailingZero_whenISetMoney_thenIExpectDecimalWithTrailingZero() {
|
||||
testSubject.setMoney("1.230", currency);
|
||||
|
||||
String formatted = testSubject.getText().toString();
|
||||
|
||||
assertEquals("1.230MOB", formatted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.thoughtcrime.securesms.payments.currency;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Currency;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
public final class CurrencyUtilTest_getCurrencyByE164 {
|
||||
|
||||
@Test
|
||||
public void get_gbp_from_uk_number() {
|
||||
String e164 = "+441617151234";
|
||||
|
||||
Currency currency = CurrencyUtil.getCurrencyByE164(e164);
|
||||
|
||||
assertNotNull(currency);
|
||||
assertEquals("GBP", currency.getCurrencyCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void get_euros_from_german_number() {
|
||||
String e164 = "+4915223433333";
|
||||
|
||||
Currency currency = CurrencyUtil.getCurrencyByE164(e164);
|
||||
|
||||
assertNotNull(currency);
|
||||
assertEquals("EUR", currency.getCurrencyCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void get_usd_from_us_number() {
|
||||
String e164 = "+15407011234";
|
||||
|
||||
Currency currency = CurrencyUtil.getCurrencyByE164(e164);
|
||||
|
||||
assertNotNull(currency);
|
||||
assertEquals("USD", currency.getCurrencyCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void get_cad_from_canadian_number() {
|
||||
String e164 = "+15064971234";
|
||||
|
||||
Currency currency = CurrencyUtil.getCurrencyByE164(e164);
|
||||
|
||||
assertNotNull(currency);
|
||||
assertEquals("CAD", currency.getCurrencyCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.thoughtcrime.securesms.payments.currency;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.whispersystems.signalservice.api.payments.Money;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Currency;
|
||||
import java.util.HashMap;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class ExchangeRate_exchange {
|
||||
|
||||
private final BigDecimal expected;
|
||||
private final CurrencyExchange.ExchangeRate exchange;
|
||||
private final Money money;
|
||||
|
||||
@Parameterized.Parameters
|
||||
public static Collection<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{BigDecimal.ONE, "USD", BigDecimal.valueOf(1.536), BigDecimal.valueOf(1.54).setScale(2, RoundingMode.UNNECESSARY)},
|
||||
{BigDecimal.ONE, "USD", BigDecimal.valueOf(1.535), BigDecimal.valueOf(1.54).setScale(2, RoundingMode.UNNECESSARY)},
|
||||
{BigDecimal.ONE, "USD", BigDecimal.valueOf(1.534), BigDecimal.valueOf(1.53).setScale(2, RoundingMode.UNNECESSARY)},
|
||||
|
||||
{BigDecimal.ONE, "USD", BigDecimal.valueOf(1.526), BigDecimal.valueOf(1.53).setScale(2, RoundingMode.UNNECESSARY)},
|
||||
{BigDecimal.ONE, "USD", BigDecimal.valueOf(1.525), BigDecimal.valueOf(1.52).setScale(2, RoundingMode.UNNECESSARY)},
|
||||
{BigDecimal.ONE, "USD", BigDecimal.valueOf(1.524), BigDecimal.valueOf(1.52).setScale(2, RoundingMode.UNNECESSARY)},
|
||||
|
||||
{BigDecimal.ONE, "USD", BigDecimal.valueOf(1.5), BigDecimal.valueOf(1.5).setScale(2, RoundingMode.UNNECESSARY)},
|
||||
{BigDecimal.ONE, "USD", BigDecimal.valueOf(1d), BigDecimal.valueOf(1).setScale(2, RoundingMode.UNNECESSARY)},
|
||||
|
||||
{BigDecimal.ONE, "JPY", BigDecimal.valueOf(1.6), BigDecimal.valueOf(2).setScale(0, RoundingMode.UNNECESSARY)},
|
||||
{BigDecimal.ONE, "JPY", BigDecimal.valueOf(1.5), BigDecimal.valueOf(2).setScale(0, RoundingMode.UNNECESSARY)},
|
||||
{BigDecimal.ONE, "JPY", BigDecimal.valueOf(1.4), BigDecimal.valueOf(1).setScale(0, RoundingMode.UNNECESSARY)},
|
||||
|
||||
{BigDecimal.ONE, "JPY", BigDecimal.valueOf(2.6), BigDecimal.valueOf(3).setScale(0, RoundingMode.UNNECESSARY)},
|
||||
{BigDecimal.ONE, "JPY", BigDecimal.valueOf(2.5), BigDecimal.valueOf(2).setScale(0, RoundingMode.UNNECESSARY)},
|
||||
{BigDecimal.ONE, "JPY", BigDecimal.valueOf(2.4), BigDecimal.valueOf(2).setScale(0, RoundingMode.UNNECESSARY)},
|
||||
|
||||
{BigDecimal.ONE, "JPY", BigDecimal.valueOf(1d), BigDecimal.valueOf(1).setScale(0, RoundingMode.UNNECESSARY)},
|
||||
});
|
||||
}
|
||||
|
||||
public ExchangeRate_exchange(@NonNull BigDecimal money,
|
||||
@NonNull String currencyCode,
|
||||
@NonNull BigDecimal rate,
|
||||
@NonNull BigDecimal expected)
|
||||
{
|
||||
this.money = Money.mobileCoin(money);
|
||||
this.exchange = new CurrencyExchange.ExchangeRate(Currency.getInstance(currencyCode), rate, 0);
|
||||
this.expected = expected;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exchange() {
|
||||
FiatMoney amount = exchange.exchange(money).get();
|
||||
|
||||
assertEquals(expected, amount.getAmount());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package org.thoughtcrime.securesms.payments.history;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.thoughtcrime.securesms.payments.Direction;
|
||||
import org.whispersystems.signalservice.api.payments.Formatter;
|
||||
import org.whispersystems.signalservice.api.payments.FormatterOptions;
|
||||
import org.whispersystems.signalservice.api.payments.Money;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static java.util.stream.IntStream.range;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public final class BlockTransactionReconstructionTests {
|
||||
|
||||
private final static Formatter MONEY_FORMATTER = Money.MobileCoin.CURRENCY.getFormatter(FormatterOptions.builder(Locale.US).build());
|
||||
|
||||
@Parameterized.Parameter(0)
|
||||
public SpentList spentTransactionOutputs;
|
||||
|
||||
@Parameterized.Parameter(1)
|
||||
public UnspentList unspentTransactionOutputs;
|
||||
|
||||
@Parameterized.Parameter(2)
|
||||
public SentList expectedSentTransactions;
|
||||
|
||||
@Parameterized.Parameter(3)
|
||||
public ReceivedList expectedReceivedTransactions;
|
||||
|
||||
@Parameterized.Parameters(name = "Spent: {0}, Unspent: {1} => Sent: {2}, Received: {3}")
|
||||
public static Collection<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{spent(1), unspent(), sent(1), received()},
|
||||
{spent(1, 2, 3), unspent(), sent(6), received()},
|
||||
{spent(range(1, 30).boxed().toArray()), unspent(), sent(435), received()},
|
||||
{spent(1, 2, 3), unspent(0.5), sent(5.5), received()},
|
||||
{spent(1, 2, 3), unspent(1), sent(5), received()},
|
||||
{spent(1, 2, 3), unspent(1.2), sent(4.8), received()},
|
||||
{spent(1, 1, 1, 1, 1), unspent(2), sent(3), received()},
|
||||
{spent(1, 1, 1, 1, 1), unspent(2, 2), sent(1), received()},
|
||||
{spent(1, 1, 1, 1, 1), unspent(4.99), sent(0.01), received()},
|
||||
{spent(1), unspent(1), sent(1), received(1)},
|
||||
{spent(1, 1, 1, 1, 1), unspent(5.1), sent(5), received(5.1)},
|
||||
{spent(1000), unspent(999, 15), sent(1), received(15)},
|
||||
{spent(1000), unspent(15, 999), sent(1), received(15)},
|
||||
{spent(1, 1, 1, 1, 1), unspent(9, 3, 1), sent(1), received(9)},
|
||||
{spent(10, 10), unspent(5, 9, 3, 7, 1), sent(1), received(1, 5)},
|
||||
|
||||
// Zero cases
|
||||
{spent(0), unspent(), sent(), received()},
|
||||
{spent(), unspent(0), sent(), received()},
|
||||
{spent(0), unspent(0), sent(), received()},
|
||||
{spent(1), unspent(0, 0.2), sent(0.8), received()},
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void received_transactions_value_and_order() {
|
||||
TransactionReconstruction estimate = TransactionReconstruction.estimateBlockLevelActivity(spentTransactionOutputs.getMoney(), unspentTransactionOutputs.getMoney());
|
||||
|
||||
assertEquals(expectedReceivedTransactions.getMoney(), toValueList(estimate.received()));
|
||||
assertTrue(Stream.of(estimate.received()).allMatch(t -> t.getDirection() == Direction.RECEIVED));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sent_transactions_value() {
|
||||
TransactionReconstruction estimate = TransactionReconstruction.estimateBlockLevelActivity(spentTransactionOutputs.getMoney(), unspentTransactionOutputs.getMoney());
|
||||
|
||||
assertEquals(expectedSentTransactions.getMoney(), toValueList(estimate.sent()));
|
||||
assertTrue(Stream.of(estimate.sent()).allMatch(t -> t.getDirection() == Direction.SENT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void total_in_matches_total_out() {
|
||||
Money net = unspentTransactionOutputs.sum().subtract(spentTransactionOutputs.sum());
|
||||
|
||||
TransactionReconstruction estimate = TransactionReconstruction.estimateBlockLevelActivity(spentTransactionOutputs.getMoney(), unspentTransactionOutputs.getMoney());
|
||||
|
||||
Money transactionIn = Money.MobileCoin.sum(toValueList(estimate.received()));
|
||||
Money transactionOut = Money.MobileCoin.sum(toValueList(estimate.sent()));
|
||||
Money netTransactions = transactionIn.subtract(transactionOut);
|
||||
|
||||
assertEquals(net, netTransactions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sum_of_all_transactions() {
|
||||
Money net = unspentTransactionOutputs.sum().subtract(spentTransactionOutputs.sum());
|
||||
|
||||
TransactionReconstruction estimate = TransactionReconstruction.estimateBlockLevelActivity(spentTransactionOutputs.getMoney(), unspentTransactionOutputs.getMoney());
|
||||
|
||||
Money netValueWithDirections = Money.MobileCoin.sum(toValueListWithDirection(estimate.getAllTransactions()));
|
||||
|
||||
assertEquals(net, netValueWithDirections);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void all_transaction_order() {
|
||||
TransactionReconstruction estimate = TransactionReconstruction.estimateBlockLevelActivity(spentTransactionOutputs.getMoney(), unspentTransactionOutputs.getMoney());
|
||||
|
||||
assertEquals(sort(estimate.getAllTransactions()), estimate.getAllTransactions());
|
||||
}
|
||||
|
||||
private static @NonNull List<TransactionReconstruction.Transaction> sort(@NonNull List<TransactionReconstruction.Transaction> transactions) {
|
||||
return Stream.of(transactions)
|
||||
.sorted((o1, o2) -> {
|
||||
if (o1.getDirection() != o2.getDirection()) {
|
||||
if (o1.getDirection() == Direction.RECEIVED) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return o1.getValue().toPicoMobBigInteger().compareTo(o2.getValue().toPicoMobBigInteger());
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static List<Money.MobileCoin> toValueList(List<TransactionReconstruction.Transaction> received) {
|
||||
return Stream.of(received)
|
||||
.map(TransactionReconstruction.Transaction::getValue)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static List<Money.MobileCoin> toValueListWithDirection(List<TransactionReconstruction.Transaction> received) {
|
||||
return Stream.of(received)
|
||||
.map(TransactionReconstruction.Transaction::getValueWithDirection)
|
||||
.toList();
|
||||
}
|
||||
|
||||
abstract static class MoneyList {
|
||||
|
||||
private final List<Money.MobileCoin> money;
|
||||
|
||||
MoneyList(List<Money.MobileCoin> money) {
|
||||
this.money = money;
|
||||
}
|
||||
|
||||
List<Money.MobileCoin> getMoney() {
|
||||
return money;
|
||||
}
|
||||
|
||||
Money.MobileCoin sum() {
|
||||
return Money.MobileCoin.sum(money);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return "[" + Stream.of(money)
|
||||
.map(f -> f.toString(MONEY_FORMATTER))
|
||||
.collect(Collectors.joining(", ")) +
|
||||
"]";
|
||||
}
|
||||
}
|
||||
|
||||
static class SpentList extends MoneyList {
|
||||
SpentList(List<Money.MobileCoin> money) {
|
||||
super(money);
|
||||
}
|
||||
}
|
||||
|
||||
static class UnspentList extends MoneyList {
|
||||
UnspentList(List<Money.MobileCoin> money) {
|
||||
super(money);
|
||||
}
|
||||
}
|
||||
|
||||
static class SentList extends MoneyList {
|
||||
SentList(List<Money.MobileCoin> money) {
|
||||
super(money);
|
||||
}
|
||||
}
|
||||
|
||||
static class ReceivedList extends MoneyList {
|
||||
ReceivedList(List<Money.MobileCoin> money) {
|
||||
super(money);
|
||||
}
|
||||
}
|
||||
|
||||
private static SpentList spent(Object... mob) {
|
||||
return new SpentList(toMobileCoinList(mob));
|
||||
}
|
||||
|
||||
private static UnspentList unspent(Object... mob) {
|
||||
return new UnspentList(toMobileCoinList(mob));
|
||||
}
|
||||
|
||||
private static SentList sent(Object... mob) {
|
||||
return new SentList(toMobileCoinList(mob));
|
||||
}
|
||||
|
||||
private static ReceivedList received(Object... mob) {
|
||||
return new ReceivedList(toMobileCoinList(mob));
|
||||
}
|
||||
|
||||
private static List<Money.MobileCoin> toMobileCoinList(Object[] mob) {
|
||||
return Stream.of(mob)
|
||||
.map(value -> Money.mobileCoin(BigDecimal.valueOf(Double.parseDouble(value.toString()))))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
package org.thoughtcrime.securesms.payments.reconciliation;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.payments.Direction;
|
||||
import org.thoughtcrime.securesms.payments.FailureReason;
|
||||
import org.thoughtcrime.securesms.payments.MobileCoinLedgerWrapper;
|
||||
import org.thoughtcrime.securesms.payments.Payee;
|
||||
import org.thoughtcrime.securesms.payments.Payment;
|
||||
import org.thoughtcrime.securesms.payments.State;
|
||||
import org.thoughtcrime.securesms.payments.proto.MobileCoinLedger;
|
||||
import org.thoughtcrime.securesms.payments.proto.PaymentMetaData;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.testutil.LogRecorder;
|
||||
import org.thoughtcrime.securesms.testutil.SystemOutLogger;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
import org.whispersystems.signalservice.api.payments.Money;
|
||||
import org.whispersystems.signalservice.api.util.Uint64RangeException;
|
||||
import org.whispersystems.signalservice.api.util.Uint64Util;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public final class LedgerReconcileTest {
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
Log.initialize(new LogRecorder());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void empty_lists() {
|
||||
List<Payment> payments = reconcile(Collections.emptyList(), new MobileCoinLedgerWrapper(MobileCoinLedger.getDefaultInstance()));
|
||||
|
||||
assertEquals(Collections.emptyList(), payments);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void single_unspent_transaction_on_ledger_only() {
|
||||
MobileCoinLedger ledger = ledger(unspentTxo(mob(2.5), keyImage(2), publicKey(3), block(2)));
|
||||
|
||||
List<Payment> payments = reconcile(Collections.emptyList(), new MobileCoinLedgerWrapper(ledger));
|
||||
|
||||
assertEquals(1, payments.size());
|
||||
|
||||
Payment payment = payments.get(0);
|
||||
|
||||
assertEquals(mob(2.5), payment.getAmount());
|
||||
assertEquals(mob(2.5), payment.getAmountWithDirection());
|
||||
assertEquals(Direction.RECEIVED, payment.getDirection());
|
||||
assertEquals(mob(0), payment.getFee());
|
||||
assertEquals(Payee.UNKNOWN, payment.getPayee());
|
||||
assertEquals(State.SUCCESSFUL, payment.getState());
|
||||
assertEquals(UuidUtil.UNKNOWN_UUID, payment.getUuid());
|
||||
assertEquals("", payment.getNote());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void single_spent_transaction_on_ledger() {
|
||||
MobileCoinLedger ledger = ledger(spentTxo(mob(10), keyImage(1), publicKey(2), block(1), block(2)));
|
||||
|
||||
List<Payment> payments = reconcile(Collections.emptyList(), new MobileCoinLedgerWrapper(ledger));
|
||||
|
||||
assertEquals(2, payments.size());
|
||||
|
||||
Payment payment1 = payments.get(0);
|
||||
assertEquals(mob(10), payment1.getAmount());
|
||||
assertEquals(mob(-10), payment1.getAmountWithDirection());
|
||||
assertEquals(Direction.SENT, payment1.getDirection());
|
||||
assertEquals(mob(0), payment1.getFee());
|
||||
assertEquals(Payee.UNKNOWN, payment1.getPayee());
|
||||
assertEquals(State.SUCCESSFUL, payment1.getState());
|
||||
assertEquals(UuidUtil.UNKNOWN_UUID, payment1.getUuid());
|
||||
assertEquals("", payment1.getNote());
|
||||
|
||||
Payment payment2 = payments.get(1);
|
||||
|
||||
assertEquals(mob(10), payment2.getAmount());
|
||||
assertEquals(mob(10), payment2.getAmountWithDirection());
|
||||
assertEquals(Direction.RECEIVED, payment2.getDirection());
|
||||
assertEquals(mob(0), payment2.getFee());
|
||||
assertEquals(Payee.UNKNOWN, payment2.getPayee());
|
||||
assertEquals(State.SUCCESSFUL, payment2.getState());
|
||||
assertEquals(UuidUtil.UNKNOWN_UUID, payment2.getUuid());
|
||||
assertEquals("", payment2.getNote());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void one_received_and_one_spent_transaction_on_ledger_only_different_blocks() {
|
||||
MobileCoinLedger ledger = ledger(spentTxo(mob(2.5), keyImage(1), publicKey(2), block(1), block(2)),
|
||||
unspentTxo(mob(1), keyImage(3), publicKey(4), block(3)));
|
||||
|
||||
List<Payment> payments = reconcile(Collections.emptyList(), new MobileCoinLedgerWrapper(ledger));
|
||||
|
||||
assertEquals(3, payments.size());
|
||||
|
||||
assertEquals(mob(1), payments.get(0).getAmountWithDirection());
|
||||
assertEquals(mob(-2.5), payments.get(1).getAmountWithDirection());
|
||||
assertEquals(mob(2.5), payments.get(2).getAmountWithDirection());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void one_received_and_one_spent_transaction_on_ledger_only_same_block_is_treated_as_change() {
|
||||
MobileCoinLedger ledger = ledger(spentTxo(mob(2.5), keyImage(1), publicKey(2), block(1), block(2)),
|
||||
unspentTxo(mob(1), keyImage(3), publicKey(4), block(2)));
|
||||
|
||||
List<Payment> payments = reconcile(Collections.emptyList(), new MobileCoinLedgerWrapper(ledger));
|
||||
|
||||
assertEquals(2, payments.size());
|
||||
|
||||
assertEquals(mob(-1.5), payments.get(0).getAmountWithDirection());
|
||||
assertEquals(mob(2.5), payments.get(1).getAmountWithDirection());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void single_spend_payment_that_can_be_found_on_ledger_reconstructed_receipt() {
|
||||
List<Payment> localPayments = Collections.singletonList(payment("sent", mob(-1.5), keyImages(5), publicKeys(4)));
|
||||
|
||||
MobileCoinLedger ledger = ledger(spentTxo(mob(2.5), keyImage(5), publicKey(2), block(1), block(2)),
|
||||
unspentTxo(mob(1), keyImage(3), publicKey(4), block(2)));
|
||||
|
||||
List<Payment> payments = reconcile(localPayments, new MobileCoinLedgerWrapper(ledger));
|
||||
|
||||
assertEquals(2, payments.size());
|
||||
|
||||
assertEquals(mob(-1.5), payments.get(0).getAmountWithDirection());
|
||||
assertEquals(mob(2.5), payments.get(1).getAmountWithDirection());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void single_receipt_payment_that_can_be_found_on_ledger_reconstructed_spend() {
|
||||
List<Payment> localPayments = Collections.singletonList(payment("received", mob(2.5), keyImages(6), publicKeys(2)));
|
||||
|
||||
MobileCoinLedger ledger = ledger(spentTxo(mob(2.5), keyImage(5), publicKey(2), block(1), block(2)));
|
||||
|
||||
List<Payment> payments = reconcile(localPayments, new MobileCoinLedgerWrapper(ledger));
|
||||
|
||||
assertEquals(2, payments.size());
|
||||
|
||||
assertEquals(mob(-2.5), payments.get(0).getAmountWithDirection());
|
||||
assertEquals(mob(2.5), payments.get(1).getAmountWithDirection());
|
||||
assertEquals(2, payments.get(0).getBlockIndex());
|
||||
assertEquals("received", payments.get(1).getNote());
|
||||
assertEquals(1, payments.get(1).getBlockIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void single_receipt_payment_that_can_be_found_on_ledger_reconstructed_spend_with_change() {
|
||||
List<Payment> localPayments = Collections.singletonList(payment("received", mob(2.5), keyImages(6), publicKeys(2)));
|
||||
|
||||
MobileCoinLedger ledger = ledger(spentTxo(mob(2.5), keyImage(5), publicKey(2), block(1), block(2)),
|
||||
unspentTxo(mob(1.5), keyImage(7), publicKey(8), block(2)));
|
||||
|
||||
List<Payment> payments = reconcile(localPayments, new MobileCoinLedgerWrapper(ledger));
|
||||
|
||||
assertEquals(Arrays.asList(mob(-1), mob(2.5)), Stream.of(payments).map(Payment::getAmountWithDirection).toList());
|
||||
assertEquals("received", payments.get(1).getNote());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void single_receipt_payment_that_can_be_found_on_ledger_reconstructed_spend_but_not_with_change_due_to_block() {
|
||||
List<Payment> localPayments = Collections.singletonList(payment("received", mob(2.5), keyImages(6), publicKeys(2)));
|
||||
|
||||
MobileCoinLedger ledger = ledger(spentTxo(mob(2.5), keyImage(5), publicKey(2), block(1), block(2)),
|
||||
unspentTxo(mob(1.5), keyImage(7), publicKey(8), block(3)));
|
||||
|
||||
List<Payment> payments = reconcile(localPayments, new MobileCoinLedgerWrapper(ledger));
|
||||
|
||||
assertEquals(Arrays.asList(mob(1.5), mob(-2.5), mob(2.5)), Stream.of(payments).map(Payment::getAmountWithDirection).toList());
|
||||
assertEquals("received", payments.get(2).getNote());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_payment_is_first_in_list() {
|
||||
List<Payment> localPayments = Collections.singletonList(payment("received", mob(2.5), keyImages(16), publicKeys(12)));
|
||||
|
||||
MobileCoinLedger ledger = ledger(spentTxo(mob(2.5), keyImage(5), publicKey(2), block(1), block(2)),
|
||||
unspentTxo(mob(1.5), keyImage(7), publicKey(8), block(3)));
|
||||
|
||||
List<Payment> payments = reconcile(localPayments, new MobileCoinLedgerWrapper(ledger));
|
||||
|
||||
assertEquals(Arrays.asList(mob(2.5), mob(1.5), mob(-2.5), mob(2.5)), Stream.of(payments).map(Payment::getAmountWithDirection).toList());
|
||||
assertEquals("received", payments.get(0).getNote());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unmatched_payment_remains_in_place_behind_matched() {
|
||||
List<Payment> localPayments = Arrays.asList(payment("matched payment", mob(10), keyImages(6), publicKeys(2)),
|
||||
payment("unmatched payment", mob(20), keyImages(16), publicKeys(12)));
|
||||
|
||||
MobileCoinLedger ledger = ledger(spentTxo(mob(2.5), keyImage(5), publicKey(2), block(1), block(2)),
|
||||
unspentTxo(mob(1.5), keyImage(7), publicKey(8), block(3)));
|
||||
|
||||
List<Payment> payments = reconcile(localPayments, new MobileCoinLedgerWrapper(ledger));
|
||||
|
||||
assertEquals(Arrays.asList(mob(1.5), mob(-2.5), mob(10), mob(20)), Stream.of(payments).map(Payment::getAmountWithDirection).toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_payment_remains_in_place_behind_known_alternative_block_order() {
|
||||
List<Payment> localPayments = Arrays.asList(payment("matched payment", mob(10), keyImages(60), publicKeys(7)),
|
||||
payment("unmatched payment", mob(20), keyImages(16), publicKeys(12)));
|
||||
|
||||
MobileCoinLedger ledger = ledger(spentTxo(mob(2.5), keyImage(5), publicKey(2), block(10), block(20)),
|
||||
unspentTxo(mob(10), keyImage(9), publicKey(7), block(15)));
|
||||
|
||||
List<Payment> payments = reconcile(localPayments, new MobileCoinLedgerWrapper(ledger));
|
||||
|
||||
assertEquals(Arrays.asList(20L, 15L, 0L, 10L), Stream.of(payments).map(Payment::getBlockIndex).toList());
|
||||
assertEquals(Arrays.asList(mob(-2.5), mob(10), mob(20), mob(2.5)), Stream.of(payments).map(Payment::getAmountWithDirection).toList());
|
||||
}
|
||||
|
||||
private static @NonNull List<Payment> reconcile(@NonNull Collection<Payment> localPaymentTransactions,
|
||||
@NonNull MobileCoinLedgerWrapper ledger)
|
||||
{
|
||||
return LedgerReconcile.reconcile(localPaymentTransactions, ledger);
|
||||
}
|
||||
|
||||
private MobileCoinLedger.Block block(long blockIndex) {
|
||||
return MobileCoinLedger.Block.newBuilder()
|
||||
.setBlockNumber(blockIndex)
|
||||
.build();
|
||||
}
|
||||
|
||||
private MobileCoinLedger ledger(MobileCoinLedger.OwnedTXO... txos) {
|
||||
MobileCoinLedger.Builder builder = MobileCoinLedger.newBuilder();
|
||||
for (MobileCoinLedger.OwnedTXO txo : txos) {
|
||||
builder.addUnspentTxos(txo);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private MobileCoinLedger.OwnedTXO unspentTxo(Money.MobileCoin mob, ByteString keyImage, ByteString publicKey, MobileCoinLedger.Block receivedBlock) {
|
||||
return txo(mob, keyImage, publicKey, receivedBlock).build();
|
||||
}
|
||||
|
||||
private MobileCoinLedger.OwnedTXO spentTxo(Money.MobileCoin mob, ByteString keyImage, ByteString publicKey, MobileCoinLedger.Block receivedBlock, MobileCoinLedger.Block spentBlock) {
|
||||
return txo(mob, keyImage, publicKey, receivedBlock).setSpentInBlock(spentBlock).build();
|
||||
}
|
||||
|
||||
private MobileCoinLedger.OwnedTXO.Builder txo(Money.MobileCoin mob, ByteString keyImage, ByteString publicKey, MobileCoinLedger.Block receivedBlock) {
|
||||
if (mob.isNegative()) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
MobileCoinLedger.OwnedTXO.Builder builder = MobileCoinLedger.OwnedTXO.newBuilder()
|
||||
.setReceivedInBlock(receivedBlock)
|
||||
.setKeyImage(keyImage)
|
||||
.setPublicKey(publicKey);
|
||||
try {
|
||||
builder.setAmount(Uint64Util.bigIntegerToUInt64(mob.toPicoMobBigInteger()));
|
||||
} catch (Uint64RangeException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static Payment payment(String note, Money.MobileCoin valueAndDirection, Set<ByteString> keyImages, Set<ByteString> publicKeys) {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
|
||||
PaymentMetaData.MobileCoinTxoIdentification.Builder builderForValue = PaymentMetaData.MobileCoinTxoIdentification.newBuilder();
|
||||
|
||||
builderForValue.addAllKeyImages(keyImages);
|
||||
builderForValue.addAllPublicKey(publicKeys);
|
||||
|
||||
PaymentMetaData paymentMetaData = PaymentMetaData.newBuilder()
|
||||
.setMobileCoinTxoIdentification(builderForValue)
|
||||
.build();
|
||||
|
||||
return new Payment() {
|
||||
@Override
|
||||
public @NonNull UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Payee getPayee() {
|
||||
return new Payee(RecipientId.from(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBlockIndex() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBlockTimestamp() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestamp() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Direction getDirection() {
|
||||
return valueAndDirection.isNegative() ? Direction.SENT : Direction.RECEIVED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull State getState() {
|
||||
return State.SUCCESSFUL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable FailureReason getFailureReason() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getNote() {
|
||||
return note;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Money getAmount() {
|
||||
return valueAndDirection.abs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Money getFee() {
|
||||
return getAmount().toZero();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull PaymentMetaData getPaymentMetaData() {
|
||||
return paymentMetaData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSeen() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Set<ByteString> keyImages(long... ids) {
|
||||
Set<ByteString> idList = new HashSet<>(ids.length);
|
||||
for (long id : ids) {
|
||||
idList.add(keyImage(id));
|
||||
}
|
||||
return idList;
|
||||
}
|
||||
|
||||
private static Set<ByteString> publicKeys(long... ids) {
|
||||
Set<ByteString> idList = new HashSet<>(ids.length);
|
||||
for (long id : ids) {
|
||||
idList.add(publicKey(id));
|
||||
}
|
||||
return idList;
|
||||
}
|
||||
|
||||
private static ByteString keyImage(long id) {
|
||||
return id(0x7f00000000000000L | id);
|
||||
}
|
||||
|
||||
private static ByteString publicKey(long id) {
|
||||
return id(0x0f00000000000000L | id);
|
||||
}
|
||||
|
||||
private static ByteString id(long id) {
|
||||
byte[] bytes = ByteUtil.longToByteArray(id);
|
||||
return ByteString.copyFrom(bytes);
|
||||
}
|
||||
|
||||
private static Money.MobileCoin mob(double value) {
|
||||
return Money.mobileCoin(BigDecimal.valueOf(value));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.thoughtcrime.securesms.payments.reconciliation;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.thoughtcrime.securesms.payments.reconciliation.ZipList.zipList;
|
||||
|
||||
public final class ZipListTest {
|
||||
|
||||
@Test
|
||||
public void empty_list_zip() {
|
||||
List<Long> a = emptyList();
|
||||
List<Long> b = emptyList();
|
||||
assertEquals(emptyList(), zipList(a, b, Long::compare));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void empty_list_rhs_zip() {
|
||||
List<Long> a = Arrays.asList(1L, 2L, 3L);
|
||||
List<Long> b = emptyList();
|
||||
assertEquals(a, zipList(a, b, Long::compare));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void empty_list_lhs_zip() {
|
||||
List<Long> a = emptyList();
|
||||
List<Long> b = Arrays.asList(1L, 2L, 3L);
|
||||
assertEquals(b, zipList(a, b, Long::compare));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void two_lists_no_overlap() {
|
||||
List<Integer> a = Arrays.asList(1, 2, 3);
|
||||
List<Integer> b = Arrays.asList(4, 5, 6);
|
||||
assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6), zipList(a, b, Integer::compare));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void two_lists_overlap() {
|
||||
List<Integer> a = Arrays.asList(1, 2, 4);
|
||||
List<Integer> b = Arrays.asList(3, 5, 6);
|
||||
assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6), zipList(a, b, Integer::compare));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void two_lists_overlap_reversed() {
|
||||
List<Integer> a = Arrays.asList(3, 5, 6);
|
||||
List<Integer> b = Arrays.asList(1, 2, 4);
|
||||
assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6), zipList(a, b, Integer::compare));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void two_lists_with_out_of_order_items() {
|
||||
List<Integer> a = Arrays.asList(3, 0, 4);
|
||||
List<Integer> b = Arrays.asList(1, 0, 2);
|
||||
assertEquals(Arrays.asList(1, 0, 2, 3, 0, 4), zipList(a, b, Integer::compare));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void two_lists_with_out_of_order_items_and_overlap() {
|
||||
List<Integer> a = Arrays.asList(3, -2, 5);
|
||||
List<Integer> b = Arrays.asList(1, -1, 4);
|
||||
assertEquals(Arrays.asList(1, -1, 3, -2, 4, 5), zipList(a, b, Integer::compare));
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyDifferenceResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.MergeResult;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
@@ -38,6 +39,7 @@ import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
@@ -84,6 +86,7 @@ public final class StorageSyncHelperTest {
|
||||
when(Recipient.self()).thenReturn(SELF);
|
||||
Log.initialize(new Log.Logger[0]);
|
||||
mockStatic(FeatureFlags.class);
|
||||
StorageSyncHelper.setTestKeyGenerator(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -398,6 +401,83 @@ public final class StorageSyncHelperTest {
|
||||
assertTrue(StorageSyncHelper.profileKeyChanged(update(a, b)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_payments_enabled_remotely() {
|
||||
SignalAccountRecord remoteAccount = accountWithPayments(1, true, new byte[32]);
|
||||
SignalAccountRecord localAccount = accountWithPayments(2, false, new byte[32]);
|
||||
|
||||
Set<SignalStorageRecord> remoteOnly = recordSetOf(remoteAccount);
|
||||
Set<SignalStorageRecord> localOnly = recordSetOf(localAccount);
|
||||
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly, r -> false);
|
||||
|
||||
assertTrue(result.getLocalAccountUpdate().get().getNew().getPayments().isEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_payments_disabled_remotely() {
|
||||
SignalAccountRecord remoteAccount = accountWithPayments(1, false, new byte[32]);
|
||||
SignalAccountRecord localAccount = accountWithPayments(2, true, new byte[32]);
|
||||
|
||||
Set<SignalStorageRecord> remoteOnly = recordSetOf(remoteAccount);
|
||||
Set<SignalStorageRecord> localOnly = recordSetOf(localAccount);
|
||||
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly, r -> false);
|
||||
|
||||
assertFalse(result.getLocalAccountUpdate().get().getNew().getPayments().isEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_payments_remote_entropy_overrides_local_if_correct_length_32() {
|
||||
byte[] remoteEntropy = Util.getSecretBytes(32);
|
||||
byte[] localEntropy = Util.getSecretBytes(32);
|
||||
SignalAccountRecord remoteAccount = accountWithPayments(1, true, remoteEntropy);
|
||||
SignalAccountRecord localAccount = accountWithPayments(2, true, localEntropy);
|
||||
|
||||
Set<SignalStorageRecord> remoteOnly = recordSetOf(remoteAccount);
|
||||
Set<SignalStorageRecord> localOnly = recordSetOf(localAccount);
|
||||
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly, r -> false);
|
||||
|
||||
SignalAccountRecord.Payments payments = result.getLocalAccountUpdate().get().getNew().getPayments();
|
||||
assertTrue(payments.isEnabled());
|
||||
assertArrayEquals(remoteEntropy, payments.getEntropy().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_payments_local_entropy_preserved_if_remote_empty() {
|
||||
byte[] remoteEntropy = new byte[0];
|
||||
byte[] localEntropy = Util.getSecretBytes(32);
|
||||
SignalAccountRecord remoteAccount = accountWithPayments(1, true, remoteEntropy);
|
||||
SignalAccountRecord localAccount = accountWithPayments(2, true, localEntropy);
|
||||
|
||||
Set<SignalStorageRecord> remoteOnly = recordSetOf(remoteAccount);
|
||||
Set<SignalStorageRecord> localOnly = recordSetOf(localAccount);
|
||||
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly, r -> false);
|
||||
|
||||
SignalAccountRecord.Payments payments = result.getLocalAccountUpdate().get().getNew().getPayments();
|
||||
assertFalse(payments.isEnabled());
|
||||
assertArrayEquals(localEntropy, payments.getEntropy().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_payments_local_entropy_preserved_if_remote_is_a_bad_length() {
|
||||
byte[] remoteEntropy = Util.getSecretBytes(30);
|
||||
byte[] localEntropy = Util.getSecretBytes(32);
|
||||
SignalAccountRecord remoteAccount = accountWithPayments(1, true, remoteEntropy);
|
||||
SignalAccountRecord localAccount = accountWithPayments(2, true, localEntropy);
|
||||
|
||||
Set<SignalStorageRecord> remoteOnly = recordSetOf(remoteAccount);
|
||||
Set<SignalStorageRecord> localOnly = recordSetOf(localAccount);
|
||||
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly, r -> false);
|
||||
|
||||
SignalAccountRecord.Payments payments = result.getLocalAccountUpdate().get().getNew().getPayments();
|
||||
assertFalse(payments.isEnabled());
|
||||
assertArrayEquals(localEntropy, payments.getEntropy().get());
|
||||
}
|
||||
|
||||
private static Set<SignalStorageRecord> recordSetOf(SignalRecord... records) {
|
||||
LinkedHashSet<SignalStorageRecord> storageRecords = new LinkedHashSet<>();
|
||||
|
||||
@@ -445,6 +525,10 @@ public final class StorageSyncHelperTest {
|
||||
return new SignalAccountRecord.Builder(byteArray(key)).build();
|
||||
}
|
||||
|
||||
private static SignalAccountRecord accountWithPayments(int key, boolean enabled, byte[] entropy) {
|
||||
return new SignalAccountRecord.Builder(byteArray(key)).setPayments(enabled, entropy).build();
|
||||
}
|
||||
|
||||
private static SignalContactRecord contact(int key,
|
||||
UUID uuid,
|
||||
String e164,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms.testutil;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
public final class SystemOutLogger extends Log.Logger {
|
||||
@Override
|
||||
public void v(String tag, String message, Throwable t) {
|
||||
printlnFormatted('v', tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void d(String tag, String message, Throwable t) {
|
||||
printlnFormatted('d', tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void i(String tag, String message, Throwable t) {
|
||||
printlnFormatted('i', tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void w(String tag, String message, Throwable t) {
|
||||
printlnFormatted('w', tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void e(String tag, String message, Throwable t) {
|
||||
printlnFormatted('e', tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void wtf(String tag, String message, Throwable t) {
|
||||
printlnFormatted('x', tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void blockUntilAllWritesFinished() { }
|
||||
|
||||
private void printlnFormatted(char level, String tag, String message, Throwable t) {
|
||||
System.out.println(format(level, tag, message, t));
|
||||
}
|
||||
|
||||
private String format(char level, String tag, String message, Throwable t) {
|
||||
if (t != null) {
|
||||
return String.format("%c[%s] %s %s:%s", level, tag, message, t.getClass().getSimpleName(), t.getMessage());
|
||||
} else {
|
||||
return String.format("%c[%s] %s", level, tag, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public final class StringUtilTest_abbreviateInMiddle {
|
||||
|
||||
@Parameterized.Parameter(0)
|
||||
public CharSequence input;
|
||||
|
||||
@Parameterized.Parameter(1)
|
||||
public int maxChars;
|
||||
|
||||
@Parameterized.Parameter(2)
|
||||
public CharSequence expected;
|
||||
|
||||
@Parameterized.Parameters
|
||||
public static Collection<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{null, 0, null},
|
||||
{null, 1, null},
|
||||
{"", 0, ""},
|
||||
{"", 1, ""},
|
||||
{"0123456789", 10, "0123456789"},
|
||||
{"0123456789", 11, "0123456789"},
|
||||
{"0123456789", 9, "0123…6789"},
|
||||
{"0123456789", 8, "012…6789"},
|
||||
{"0123456789", 7, "012…789"},
|
||||
{"0123456789", 6, "01…789"},
|
||||
{"0123456789", 5, "01…89"},
|
||||
{"0123456789", 4, "0…89"},
|
||||
{"0123456789", 3, "0…9"},
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void abbreviateInMiddle() {
|
||||
CharSequence output = StringUtil.abbreviateInMiddle(input, maxChars);
|
||||
assertEquals(expected, output);
|
||||
if (Objects.equals(input, output)) {
|
||||
assertSame(output, input);
|
||||
} else {
|
||||
assertNotNull(output);
|
||||
assertEquals(maxChars, output.length());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@@ -23,14 +24,16 @@ public class UriUtilTest_isValidExternalUri {
|
||||
private final String input;
|
||||
private final boolean output;
|
||||
|
||||
private static final String APPLICATION_ID = BuildConfig.APPLICATION_ID;
|
||||
|
||||
@ParameterizedRobolectricTestRunner.Parameters
|
||||
public static Collection<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{ "content://other.app.package.name.org/path/public.txt", true },
|
||||
{ "file:///sdcard/public.txt", true },
|
||||
{ "file:///data/data/org.thoughtcrime.securesms/private.txt", false },
|
||||
{ "file:///any/path/with/package/name/org.thoughtcrime.securesms", false },
|
||||
{ "file:///org.thoughtcrime.securesms/any/path/with/package/name", false },
|
||||
{"file:///data/data/" + APPLICATION_ID + "/private.txt", false },
|
||||
{"file:///any/path/with/package/name/" + APPLICATION_ID, false },
|
||||
{"file:///" + APPLICATION_ID + "/any/path/with/package/name", false },
|
||||
{ "file:///any/path/../with/back/references/private.txt", false },
|
||||
{ "file:///any/path/with/back/references/../private.txt", false },
|
||||
{ "file:///../any/path/with/back/references/private.txt", false },
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package org.thoughtcrime.securesms.util.livedata;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user