package com.stripe.wrap.pay.utils; import android.support.annotation.NonNull; import com.google.android.gms.wallet.IsReadyToPayRequest; import com.google.android.gms.wallet.LineItem; import com.google.android.gms.wallet.WalletConstants; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import java.util.ArrayList; import java.util.Currency; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import static com.stripe.wrap.pay.testutils.AssertUtils.assertEmpty; import static com.stripe.wrap.pay.utils.PaymentUtils.getPriceLong; import static com.stripe.wrap.pay.utils.PaymentUtils.getPriceString; import static com.stripe.wrap.pay.utils.PaymentUtils.getStripeIsReadyToPayRequest; import static com.stripe.wrap.pay.utils.PaymentUtils.getTotalPrice; import static com.stripe.wrap.pay.utils.PaymentUtils.validateLineItemList; import static com.stripe.wrap.pay.utils.PaymentUtils.searchLineItemForErrors; import static com.stripe.wrap.pay.utils.PaymentUtils.matchesCurrencyPatternOrEmpty; import static com.stripe.wrap.pay.utils.PaymentUtils.matchesQuantityPatternOrEmpty; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.fail; import static junit.framework.TestCase.assertTrue; /** * Test class for {@link PaymentUtils}. */ @RunWith(RobolectricTestRunner.class) @Config(sdk = 23) public class PaymentUtilsTest { @Test public void matchesCurrencyPattern_whenValid_returnsTrue() { assertTrue(matchesCurrencyPatternOrEmpty(null)); assertTrue(matchesCurrencyPatternOrEmpty("")); assertTrue(matchesCurrencyPatternOrEmpty("10.00")); assertTrue(matchesCurrencyPatternOrEmpty("01.11")); assertTrue(matchesCurrencyPatternOrEmpty("5000.05")); assertTrue(matchesCurrencyPatternOrEmpty("100")); assertTrue(matchesCurrencyPatternOrEmpty("-5")); } @Test public void matchesCurrencyPattern_whenInvalid_returnsFalse() { assertFalse(matchesCurrencyPatternOrEmpty(".99")); assertFalse(matchesCurrencyPatternOrEmpty("0.123")); assertFalse(matchesCurrencyPatternOrEmpty("1.")); } @Test public void matchesQuantityPattern_whenValid_returnsTrue() { assertTrue(matchesQuantityPatternOrEmpty(null)); assertTrue(matchesQuantityPatternOrEmpty("")); assertTrue(matchesQuantityPatternOrEmpty("10.1")); assertTrue(matchesQuantityPatternOrEmpty("01")); assertTrue(matchesQuantityPatternOrEmpty("500000000000")); assertTrue(matchesQuantityPatternOrEmpty("100")); } @Test public void matchesQuantityPattern_whenInvalid_returnsFalse() { assertFalse(matchesQuantityPatternOrEmpty("1.23")); assertFalse(matchesQuantityPatternOrEmpty(".99")); assertFalse(matchesQuantityPatternOrEmpty("0.123")); assertFalse(matchesQuantityPatternOrEmpty("1.")); assertFalse(matchesQuantityPatternOrEmpty("-5")); } @Test public void validateLineItemList_whenEmptyList_returnsNoErrors() { assertEmpty(validateLineItemList(new ArrayList<LineItem>(), "USD")); } @Test public void validateLineItemList_whenNull_returnsNoErrors() { assertEmpty(validateLineItemList(null, "JPY")); } @Test public void validateLineItemList_whenEmptyCurrencyCode_hasExpectedCartError() { List<CartError> errors = validateLineItemList(new ArrayList<LineItem>(), ""); assertEquals(1, errors.size()); CartError error = errors.get(0); assertEquals(CartError.CART_CURRENCY, error.getErrorType()); assertEquals("Cart does not have a valid currency code. " + "[empty] was used, but not recognized.", error.getMessage()); assertNull(error.getLineItem()); } @Test public void validateLineItemList_whenInvalidCurrencyCode_hasExpectedCartError() { List<CartError> errors = validateLineItemList(new ArrayList<LineItem>(), "fakebucks"); assertEquals(1, errors.size()); CartError error = errors.get(0); assertEquals(CartError.CART_CURRENCY, error.getErrorType()); assertEquals("Cart does not have a valid currency code. " + "fakebucks was used, but not recognized.", error.getMessage()); assertNull(error.getLineItem()); } @Test public void validateLineItems_whenOneOrZeroTaxItems_returnsNoErrors() { Locale.setDefault(Locale.US); LineItem item0 = new LineItemBuilder("USD").setTotalPrice(1000L).build(); LineItem item1 = new LineItemBuilder("USD").setTotalPrice(2000L) .setRole(LineItem.Role.TAX).build(); List<LineItem> noTaxList = new ArrayList<>(); List<LineItem> oneTaxList = new ArrayList<>(); noTaxList.add(item0); oneTaxList.add(item0); oneTaxList.add(item1); assertEmpty(validateLineItemList(noTaxList, "USD")); assertEmpty(validateLineItemList(oneTaxList, "USD")); } @Test public void validateLineItems_whenTwoTaxItems_hasExpectedCartError() { Locale.setDefault(Locale.US); LineItem item0 = LineItem.newBuilder().setCurrencyCode("USD") .setRole(LineItem.Role.REGULAR).build(); LineItem item1 = LineItem.newBuilder().setCurrencyCode("USD") .setRole(LineItem.Role.TAX).build(); LineItem item2 = LineItem.newBuilder().setCurrencyCode("USD") .setRole(LineItem.Role.TAX).build(); LineItem item3 = LineItem.newBuilder().setCurrencyCode("USD") .setRole(LineItem.Role.REGULAR).build(); List<LineItem> tooMuchTaxList = new ArrayList<>(); tooMuchTaxList.add(item0); tooMuchTaxList.add(item1); tooMuchTaxList.add(item2); tooMuchTaxList.add(item3); List<CartError> cartErrors = validateLineItemList(tooMuchTaxList, "USD"); assertEquals(1, cartErrors.size()); CartError error = cartErrors.get(0); assertEquals(CartError.DUPLICATE_TAX, error.getErrorType()); assertEquals("A cart may only have one item with a role of " + "LineItem.Role.TAX, but more than one was found.", error.getMessage()); assertEquals(item2, error.getLineItem()); } @Test public void validateLineItemList_withOneBadItem_hasExpectedCartError() { LineItem badItem = LineItem.newBuilder().setTotalPrice("10.999") .setCurrencyCode("USD").build(); LineItem goodItem0 = LineItem.newBuilder().setTotalPrice("10.00") .setCurrencyCode("USD").build(); LineItem goodItem1 = LineItem.newBuilder().setTotalPrice("10.00") .setCurrencyCode("USD").build(); List<LineItem> oneBadAppleList = new ArrayList<>(); oneBadAppleList.add(goodItem0); oneBadAppleList.add(badItem); oneBadAppleList.add(goodItem1); List<CartError> cartErrors = validateLineItemList(oneBadAppleList, "USD"); assertEquals(1, cartErrors.size()); CartError error = cartErrors.get(0); assertEquals(CartError.LINE_ITEM_PRICE, error.getErrorType()); assertEquals(getExpectedErrorStringForPrice("10.999"), error.getMessage()); assertEquals(badItem, error.getLineItem()); } @Test public void validateLineItemList_whenOneItemHasNoCurrency_hasExpectedCartError() { LineItem badItem = LineItem.newBuilder().setTotalPrice("10.99").build(); LineItem goodItem0 = LineItem.newBuilder().setTotalPrice("10.00") .setCurrencyCode("USD").build(); LineItem goodItem1 = LineItem.newBuilder().setTotalPrice("10.00") .setCurrencyCode("USD").build(); List<LineItem> mixedList = new ArrayList<>(); mixedList.add(goodItem0); mixedList.add(badItem); mixedList.add(goodItem1); List<CartError> cartErrors = validateLineItemList(mixedList, "USD"); assertEquals(1, cartErrors.size()); CartError error = cartErrors.get(0); assertEquals(CartError.LINE_ITEM_CURRENCY, error.getErrorType()); assertEquals("Line item currency of [empty] does not match cart currency of USD.", error.getMessage()); assertEquals(badItem, error.getLineItem()); } @Test public void validateLineItemList_whenMixedCurrencies_hasExpectedCartErrors() { LineItem euroItem = LineItem.newBuilder().setTotalPrice("10.99") .setCurrencyCode("EUR").build(); LineItem dollarItem = LineItem.newBuilder().setTotalPrice("10.00") .setCurrencyCode("USD").build(); LineItem yenItem = LineItem.newBuilder().setTotalPrice("1000") .setCurrencyCode("JPY").build(); List<LineItem> mixedList = new ArrayList<>(); mixedList.add(euroItem); mixedList.add(dollarItem); mixedList.add(yenItem); List<CartError> cartErrors = validateLineItemList(mixedList, "EUR"); assertEquals(2, cartErrors.size()); CartError dollarError = cartErrors.get(0); assertEquals(CartError.LINE_ITEM_CURRENCY, dollarError.getErrorType()); assertEquals("Line item currency of USD does not match cart currency of EUR.", dollarError.getMessage()); assertEquals(dollarItem, dollarError.getLineItem()); CartError yenError = cartErrors.get(1); assertEquals(CartError.LINE_ITEM_CURRENCY, yenError.getErrorType()); assertEquals("Line item currency of JPY does not match cart currency of EUR.", yenError.getMessage()); assertEquals(yenItem, yenError.getLineItem()); } @Test public void searchLineItemForErrors_whenNoFieldsEntered_returnsNull() { assertNull(searchLineItemForErrors(LineItem.newBuilder().build())); } @Test public void searchLineItemForErrors_whenNull_returnsNull() { assertNull(searchLineItemForErrors(null)); } @Test public void searchLineItemForErrors_withAllNumericFieldsCorrect_returnsNull() { // Note that we don't assert that unitPrice * quantity == totalPrice assertNull(searchLineItemForErrors(LineItem.newBuilder() .setTotalPrice("10.00") .setQuantity("1.3") .setUnitPrice("1.50") .build())); } @Test public void searchLineItemForErrors_whenJustOneIncorrectField_returnsExpectedError() { LineItem badTotalPriceItem = LineItem.newBuilder() .setTotalPrice("10.999") .setQuantity("1.3") .setUnitPrice("1.50") .build(); CartError totalPriceError = searchLineItemForErrors(badTotalPriceItem); assertNotNull(totalPriceError); assertEquals(CartError.LINE_ITEM_PRICE, totalPriceError.getErrorType()); assertEquals(getExpectedErrorStringForPrice("10.999"), totalPriceError.getMessage()); assertEquals(badTotalPriceItem, totalPriceError.getLineItem()); LineItem badQuantityItem = LineItem.newBuilder() .setTotalPrice("10.99") .setQuantity("1.33") .setUnitPrice("1.50") .build(); CartError quantityError = searchLineItemForErrors(badQuantityItem); assertNotNull(quantityError); assertEquals(CartError.LINE_ITEM_QUANTITY, quantityError.getErrorType()); assertEquals(getExpectedErrorStringForQuantity("1.33"), quantityError.getMessage()); assertEquals(badQuantityItem, quantityError.getLineItem()); LineItem badUnitPriceItem = LineItem.newBuilder() .setTotalPrice("10.99") .setQuantity("1.3") .setUnitPrice(".50") .build(); CartError unitPriceError = searchLineItemForErrors(badUnitPriceItem); assertNotNull(unitPriceError); assertEquals(CartError.LINE_ITEM_PRICE, unitPriceError.getErrorType()); assertEquals(getExpectedErrorStringForPrice(".50"), unitPriceError.getMessage()); assertEquals(badUnitPriceItem, unitPriceError.getLineItem()); } @Test public void searchLineItemForErrors_withOneCorrectFieldAndOthersNull_returnsNull() { assertNull(searchLineItemForErrors(LineItem.newBuilder() .setTotalPrice("10.00") .build())); } @Test public void getPriceString_whenCurrencyWithDecimals_returnsExpectedValue() { String priceString = getPriceString(100L, Currency.getInstance("USD")); assertEquals("1.00", priceString); assertTrue(matchesCurrencyPatternOrEmpty(priceString)); String littlePrice = getPriceString(8L, Currency.getInstance("EUR")); assertEquals("0.08", littlePrice); assertTrue(matchesCurrencyPatternOrEmpty(littlePrice)); String bigPrice = getPriceString(20000000L, Currency.getInstance("GBP")); assertEquals("200000.00", bigPrice); assertTrue(matchesCurrencyPatternOrEmpty(bigPrice)); } @Test public void getPriceString_whenCurrencyWithoutDecimals_returnsExpectedValue() { String priceString = getPriceString(250L, Currency.getInstance("JPY")); assertEquals("250", priceString); assertTrue(matchesCurrencyPatternOrEmpty(priceString)); String bigPrice = getPriceString(250000L, Currency.getInstance("KRW")); assertEquals("250000", bigPrice); assertTrue(matchesCurrencyPatternOrEmpty(bigPrice)); String littlePrice = getPriceString(7L, Currency.getInstance("CLP")); assertEquals("7", littlePrice); assertTrue(matchesCurrencyPatternOrEmpty(littlePrice)); } @Test public void getPriceLong_whenDecimalValueGiven_correctlyParsesResult() { assertEquals(Long.valueOf(1000L), getPriceLong("10.00", Currency.getInstance("USD"))); assertEquals(Long.valueOf(55555555L), getPriceLong("555555.55", Currency.getInstance("EUR"))); assertEquals(Long.valueOf(99L), getPriceLong("0.99", Currency.getInstance("AUD"))); } @Test public void getPriceLong_whenNonDecimalValueGivenInNonDecimalCurrency_correctlyParsesResult() { assertEquals(Long.valueOf(999L), getPriceLong("999", Currency.getInstance("JPY"))); assertEquals(Long.valueOf(1L), getPriceLong("1", Currency.getInstance("KRW"))); } @Test public void getPriceLong_whenNonDecimalValueGivenInDecimalCurrency_correctlyMovesUpResult() { assertEquals(Long.valueOf(1000L), getPriceLong("10", Currency.getInstance("USD"))); // The Rial Omani has 3 decimal places assertEquals(Long.valueOf(30000L), getPriceLong("30", Currency.getInstance("OMR"))); } @Test(expected = NumberFormatException.class) public void getPriceLong_whenDecimalValueInNonDecimalCurrency_throwsNumberFormatException() { getPriceLong("100.00", Currency.getInstance("JPY")); fail("Should throw NumberFormatException when trying to convert decimal amounts of Yen"); } @Test(expected = IllegalArgumentException.class) public void getPriceLong_whenPriceStringHasAlphaCharacters_throwsIllegalArgumentException() { getPriceLong("55 bucks", Currency.getInstance("USD")); fail("Should throw IllegalArgumentException when trying to convert illegal characters"); } @Test(expected = IllegalArgumentException.class) public void getPriceLong_whenPriceStringHasNoLeadingZero_throwsIllegalArgumentException() { getPriceLong(".88", Currency.getInstance("USD")); fail("Should throw IllegalArgumentException when trying to parse bad Strings"); } @Test(expected = IllegalArgumentException.class) public void getPriceLong_whenPriceStringHasSeparators_throwsIllegalArgumentException() { getPriceLong("1,000", Currency.getInstance("USD")); fail("Should throw IllegalArgumentException when trying to parse bad Strings"); } @Test public void getPriceLong_whenPriceStringIsNegative_correctlyParsesValue() { assertEquals(Long.valueOf(-15L), getPriceLong("-15", Currency.getInstance("JPY"))); assertEquals(Long.valueOf(-1500L), getPriceLong("-15", Currency.getInstance("USD"))); assertEquals(Long.valueOf(-9999L), getPriceLong("-99.99", Currency.getInstance("EUR"))); } @Test public void getPriceLong_whenPriceStringIsEmpty_returnsNull() { assertNull(getPriceLong("", Currency.getInstance("USD"))); assertNull(getPriceLong(null, Currency.getInstance("KRW"))); } @Test public void getTotalPrice_forGroupOfStandardLineItemsInUsd_returnsExpectedValue() { Locale.setDefault(Locale.US); LineItem item1 = new LineItemBuilder("USD").setTotalPrice(1000L).build(); LineItem item2 = new LineItemBuilder("USD").setTotalPrice(2000L).build(); LineItem item3 = new LineItemBuilder("USD").setTotalPrice(3000L).build(); List<LineItem> items = new ArrayList<>(); items.add(item1); items.add(item2); items.add(item3); assertEquals(Long.valueOf(6000L), getTotalPrice(items, Currency.getInstance("USD"))); } @Test public void getTotalPrice_forGroupOfStandardLineItemsInKrw_returnsExpectedValue() { Locale.setDefault(Locale.KOREA); LineItem item1 = new LineItemBuilder("KRW").setTotalPrice(1000L).build(); LineItem item2 = new LineItemBuilder("KRW").setTotalPrice(2000L).build(); LineItem item3 = new LineItemBuilder("KRW").setTotalPrice(3000L).build(); List<LineItem> items = new ArrayList<>(); items.add(item1); items.add(item2); items.add(item3); assertEquals(Long.valueOf(6000L), getTotalPrice(items, Currency.getInstance("KRW"))); } @Test public void getTotalPrice_whenOneItemHasNoPrice_returnsExpectedValue() { Locale.setDefault(Locale.US); LineItem item1 = new LineItemBuilder("USD").setTotalPrice(1000L).build(); LineItem item2 = new LineItemBuilder("USD").build(); LineItem item3 = new LineItemBuilder("USD").setTotalPrice(3000L).build(); List<LineItem> items = new ArrayList<>(); items.add(item1); items.add(item2); items.add(item3); assertEquals(Long.valueOf(4000L), getTotalPrice(items, Currency.getInstance("USD"))); } @Test public void getTotalPrice_whenNoItemHasPrice_returnsZero() { Locale.setDefault(Locale.CANADA); LineItem item1 = new LineItemBuilder("CAD").build(); LineItem item2 = new LineItemBuilder("CAD").build(); List<LineItem> items = new ArrayList<>(); items.add(item1); items.add(item2); assertEquals(Long.valueOf(0L), getTotalPrice(items, Currency.getInstance("CAD"))); } @Test public void getTotalPrice_whenOneItemHasInvalidCurrency_returnsNull() { LineItem item1 = new LineItemBuilder("USD").setTotalPrice(100L).build(); LineItem item2 = new LineItemBuilder("CAD").setTotalPrice(100L).build(); List<LineItem> items = new ArrayList<>(); items.add(item1); items.add(item2); assertNull(getTotalPrice(items, Currency.getInstance("USD"))); } @Test public void getTotalPrice_whenEmptyList_returnsZero() { assertEquals(Long.valueOf(0L), getTotalPrice( new ArrayList<LineItem>(), Currency.getInstance("OMR"))); } @Test public void getIsReadyToPayRequest_hasExpectedCardNetworks() { IsReadyToPayRequest isReadyToPayRequest = getStripeIsReadyToPayRequest(); Set<Integer> allowedNetworks = new HashSet<>(); allowedNetworks.addAll(isReadyToPayRequest.getAllowedCardNetworks()); assertEquals(5, allowedNetworks.size()); assertTrue(allowedNetworks.contains(WalletConstants.CardNetwork.VISA)); assertTrue(allowedNetworks.contains(WalletConstants.CardNetwork.AMEX)); assertTrue(allowedNetworks.contains(WalletConstants.CardNetwork.MASTERCARD)); assertTrue(allowedNetworks.contains(WalletConstants.CardNetwork.JCB)); assertTrue(allowedNetworks.contains(WalletConstants.CardNetwork.DISCOVER)); } @Test public void removeErrorType_whenListContainsErrorsOfType_returnsListWithoutThoseErrors() { final CartError error1 = new CartError(CartError.DUPLICATE_TAX, "Dupe Tax"); final CartError error2 = new CartError(CartError.LINE_ITEM_CURRENCY, "Bad line item"); final CartError error3 = new CartError(CartError.LINE_ITEM_PRICE, "Bad price on line item"); final CartError error4 = new CartError(CartError.LINE_ITEM_QUANTITY, "Bad quantity"); final CartError error5 = new CartError(CartError.DUPLICATE_TAX, "Dupe Tax"); List<CartError> errorList = new ArrayList<CartError>() {{ add(error1); add(error2); add(error3); add(error4); add(error5); }}; List<CartError> filteredErrorList = PaymentUtils.removeErrorType(errorList, CartError.DUPLICATE_TAX); assertEquals(3, filteredErrorList.size()); assertEquals(CartError.LINE_ITEM_CURRENCY, filteredErrorList.get(0).getErrorType()); assertEquals(CartError.LINE_ITEM_PRICE, filteredErrorList.get(1).getErrorType()); assertEquals(CartError.LINE_ITEM_QUANTITY, filteredErrorList.get(2).getErrorType()); } @Test public void removeErrorType_whenListHasNoErrorsOfType_returnsOriginalList() { final CartError error1 = new CartError(CartError.DUPLICATE_TAX, "Dupe Tax"); final CartError error2 = new CartError(CartError.CART_CURRENCY, "Bad line item"); List<CartError> errorList = new ArrayList<CartError>() {{ add(error1); add(error2); }}; List<CartError> filteredErrorList = PaymentUtils.removeErrorType(errorList, CartError.LINE_ITEM_CURRENCY); assertEquals(2, filteredErrorList.size()); assertEquals(CartError.DUPLICATE_TAX, filteredErrorList.get(0).getErrorType()); assertEquals(CartError.CART_CURRENCY, filteredErrorList.get(1).getErrorType()); } @Test public void removeErrorType_onEmptyList_returnsEmptyList() { assertEmpty(PaymentUtils.removeErrorType(new ArrayList<CartError>(), CartError.LINE_ITEM_QUANTITY)); } // ************ Test Helper Methods ************ // @NonNull private static String getExpectedErrorStringForPrice(String price) { return String.format(Locale.ENGLISH, "Invalid price string: %s does not match required pattern of " + "\"^-?[0-9]+(\\.[0-9][0-9])?\"", price); } @NonNull private static String getExpectedErrorStringForQuantity(String quantity) { return String.format(Locale.ENGLISH, "Invalid quantity string: %s does not match required pattern of " + "\"[0-9]+(\\.[0-9])?\"", quantity); } }