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:
Android Team
2021-04-06 13:03:33 -03:00
committed by Alan Evans
parent c42023855b
commit fddba2906a
311 changed files with 18956 additions and 235 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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