diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/String.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/String.kt new file mode 100644 index 0000000000..00bc38e4f9 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/String.kt @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.creditcards + +import androidx.annotation.VisibleForTesting + +private const val MAX_CREDIT_CARD_NUMBER_LENGTH = 19 +private const val MIN_CREDIT_CARD_NUMBER_LENGTH = 12 + +/** + * Strips characters other than digits from a string. + * Used to strip a credit card number user input of spaces and separators. + */ +fun String.toCreditCardNumber(): String { + return this.filter { it.isDigit() } +} + +/** + * Uses string size and Luhn Algorithm validation to validate a credit card number. + */ +fun String.validateCreditCardNumber(): Boolean { + val creditCardNumber = this.toCreditCardNumber() + + if (creditCardNumber != this) return false + + // credit card numbers have at least 12 digits and at most 19 digits + if (creditCardNumber.length < MIN_CREDIT_CARD_NUMBER_LENGTH || + creditCardNumber.length > MAX_CREDIT_CARD_NUMBER_LENGTH + ) return false + + return luhnAlgorithmValidation(creditCardNumber) +} + +/** + * Implementation of Luhn Algorithm validation (https://en.wikipedia.org/wiki/Luhn_algorithm) + */ +@Suppress("MagicNumber") +@VisibleForTesting +internal fun luhnAlgorithmValidation(creditCardNumber: String): Boolean { + var checksum = 0 + val reversedCardNumber = creditCardNumber.reversed() + + for (index in reversedCardNumber.indices) { + val digit = Character.getNumericValue(reversedCardNumber[index]) + checksum += if (index % 2 == 0) digit else (digit * 2).let { (it / 10) + (it % 10) } + } + + return (checksum % 10) == 0 +} diff --git a/app/src/test/java/org/mozilla/fenix/settings/creditcards/StringTest.kt b/app/src/test/java/org/mozilla/fenix/settings/creditcards/StringTest.kt new file mode 100644 index 0000000000..608041e60d --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/settings/creditcards/StringTest.kt @@ -0,0 +1,102 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.creditcards + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@RunWith(FenixRobolectricTestRunner::class) +class StringTest { + + @Test + fun `toCreditCardNumber returns a string with only digits `() { + assertEquals("123456789", "1 234 5678 9".toCreditCardNumber()) + assertEquals("123456789", "1.23.4+5678/9".toCreditCardNumber()) + assertEquals("123456789", ",12r34t5678&9".toCreditCardNumber()) + assertEquals("123456789", " 1 234 5678 9 ".toCreditCardNumber()) + assertEquals("123456789", " abc 1 234 abc 5678 9".toCreditCardNumber()) + assertEquals("123456789", "1-234-5678-9".toCreditCardNumber()) + } + + @Test + fun `validateCreditCardNumber returns true for valid credit card numbers `() { + val americanExpressCard = "371449635398431" + val dinnersClubCard = "30569309025904" + val discoverCard = "6011111111111117" + val jcbCard = "3530111333300000" + val masterCardCard = "5555555555554444" + val visaCard = "4111111111111111" + val voyagerCard = "869941728035895" + + assertTrue(americanExpressCard.validateCreditCardNumber()) + assertTrue(dinnersClubCard.validateCreditCardNumber()) + assertTrue(discoverCard.validateCreditCardNumber()) + assertTrue(jcbCard.validateCreditCardNumber()) + assertTrue(masterCardCard.validateCreditCardNumber()) + assertTrue(visaCard.validateCreditCardNumber()) + assertTrue(voyagerCard.validateCreditCardNumber()) + } + + @Test + fun `validateCreditCardNumber returns false got invalid credit card numbers `() { + val shortCardNumber = "12345678901" + val longCardNumber = "12345678901234567890" + + val americanExpressCardInvalid = "371449635398432" + val dinnersClubCardInvalid = "30569309025905" + val discoverCardInvalid = "6011111111111118" + val jcbCardInvalid = "3530111333300001" + val masterCardCardInvalid = "5555555555554445" + val visaCardInvalid = "4111111111111112" + val voyagerCardInvalid = "869941728035896" + + assertFalse(shortCardNumber.validateCreditCardNumber()) + assertFalse(longCardNumber.validateCreditCardNumber()) + + assertFalse(americanExpressCardInvalid.validateCreditCardNumber()) + assertFalse(dinnersClubCardInvalid.validateCreditCardNumber()) + assertFalse(discoverCardInvalid.validateCreditCardNumber()) + assertFalse(jcbCardInvalid.validateCreditCardNumber()) + assertFalse(masterCardCardInvalid.validateCreditCardNumber()) + assertFalse(visaCardInvalid.validateCreditCardNumber()) + assertFalse(voyagerCardInvalid.validateCreditCardNumber()) + } + + @Test + fun `luhnAlgorithmValidation returns false for invalid identification numbers `() { + // "4242424242424242" is a valid identification number + assertFalse(luhnAlgorithmValidation("4242424242424240")) + assertFalse(luhnAlgorithmValidation("4242424242424241")) + assertFalse(luhnAlgorithmValidation("4242424242424243")) + assertFalse(luhnAlgorithmValidation("4242424242424244")) + assertFalse(luhnAlgorithmValidation("4242424242424245")) + assertFalse(luhnAlgorithmValidation("4242424242424246")) + assertFalse(luhnAlgorithmValidation("4242424242424247")) + assertFalse(luhnAlgorithmValidation("4242424242424248")) + assertFalse(luhnAlgorithmValidation("4242424242424249")) + assertFalse(luhnAlgorithmValidation("1")) + assertFalse(luhnAlgorithmValidation("12")) + assertFalse(luhnAlgorithmValidation("123")) + } + + @Test + fun `luhnAlgorithmValidation returns true for valid identification numbers `() { + assertTrue(luhnAlgorithmValidation("0")) + assertTrue(luhnAlgorithmValidation("00")) + assertTrue(luhnAlgorithmValidation("18")) + assertTrue(luhnAlgorithmValidation("0000000000000000")) + assertTrue(luhnAlgorithmValidation("4242424242424242")) + assertTrue(luhnAlgorithmValidation("42424242424242426")) + assertTrue(luhnAlgorithmValidation("424242424242424267")) + assertTrue(luhnAlgorithmValidation("4242424242424242675")) + assertTrue(luhnAlgorithmValidation("000000018")) + assertTrue(luhnAlgorithmValidation("99999999999999999999")) + assertTrue(luhnAlgorithmValidation("1234567812345670")) + } +}