From b8602ee0040faf70ccb06be35b77179bc0e8c448 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 17 Jan 2020 18:32:26 -0500 Subject: [PATCH] Fix issues with Mexican phone number formatting. Fixes #9317 --- .../contacts/sync/DirectoryHelperV1.java | 208 ++++++++++-------- .../contacts/sync/FuzzyPhoneNumberHelper.java | 130 +++++++++++ .../securesms/database/RecipientDatabase.java | 22 ++ .../securesms/recipients/Recipient.java | 9 +- .../securesms/util/ProfileUtil.java | 2 +- .../sync/FuzzyPhoneNumberHelperTest.java | 164 ++++++++++++++ 6 files changed, 436 insertions(+), 99 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/contacts/sync/FuzzyPhoneNumberHelper.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/contacts/sync/FuzzyPhoneNumberHelperTest.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelperV1.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelperV1.java index ba53296ebac..e721ff6d174 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelperV1.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelperV1.java @@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.permissions.Permissions; +import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -41,7 +42,6 @@ import org.thoughtcrime.securesms.sms.IncomingJoinedMessage; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; -import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessagePipe; @@ -53,13 +53,14 @@ import java.io.IOException; import java.util.Calendar; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; class DirectoryHelperV1 { @@ -90,25 +91,14 @@ static void refreshDirectory(@NonNull Context context, boolean notifyOfNewUsers) } RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); - Stream eligibleRecipientDatabaseContactNumbers = Stream.of(recipientDatabase.getAllPhoneNumbers()); + Set allRecipientNumbers = recipientDatabase.getAllPhoneNumbers(); + Stream eligibleRecipientDatabaseContactNumbers = Stream.of(allRecipientNumbers); Stream eligibleSystemDatabaseContactNumbers = Stream.of(ContactAccessor.getInstance().getAllContactsWithNumbers(context)); Set eligibleContactNumbers = Stream.concat(eligibleRecipientDatabaseContactNumbers, eligibleSystemDatabaseContactNumbers).collect(Collectors.toSet()); + Set storedNumbers = Stream.of(allRecipientNumbers).collect(Collectors.toSet()); + DirectoryResult directoryResult = getDirectoryResult(context, accountManager, recipientDatabase, storedNumbers, eligibleContactNumbers); - try { - Future legacyRequest = getLegacyDirectoryResult(context, accountManager, recipientDatabase, eligibleContactNumbers); - DirectoryResult legacyResult = legacyRequest.get(); - - return legacyResult.getNewlyActiveRecipients(); - } catch (InterruptedException e) { - throw new IOException("[Batch] Operation was interrupted.", e); - } catch (ExecutionException e) { - if (e.getCause() instanceof IOException) { - throw (IOException) e.getCause(); - } else { - Log.e(TAG, "[Batch] Experienced an unexpected exception.", e); - throw new AssertionError(e); - } - } + return directoryResult.getNewlyActiveRecipients(); } @WorkerThread @@ -126,24 +116,10 @@ static RegisteredState refreshDirectoryFor(@NonNull Context context, @NonNull Re return isRegistered ? RegisteredState.REGISTERED : RegisteredState.NOT_REGISTERED; } - SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - Future legacyRequest = getLegacyRegisteredState(context, accountManager, recipientDatabase, recipient); - - try { - return legacyRequest.get(); - } catch (InterruptedException e) { - throw new IOException("[Singular] Operation was interrupted.", e); - } catch (ExecutionException e) { - if (e.getCause() instanceof IOException) { - throw (IOException) e.getCause(); - } else { - Log.e(TAG, "[Singular] Experienced an unexpected exception.", e); - throw new AssertionError(e); - } - } + return getRegisteredState(context, ApplicationDependencies.getSignalServiceAccountManager(), recipientDatabase, recipient); } - private static void updateContactsDatabase(@NonNull Context context, @NonNull List activeIds, boolean removeMissing) { + private static void updateContactsDatabase(@NonNull Context context, @NonNull List activeIds, boolean removeMissing, Map rewrites) { Optional account = getOrCreateAccount(context); if (account.isPresent()) { @@ -161,7 +137,9 @@ private static void updateContactsDatabase(@NonNull Context context, @NonNull Li String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER)); if (isValidContactNumber(number)) { - RecipientId recipientId = Recipient.externalContact(context, number).getId(); + String formattedNumber = PhoneNumberFormatter.get(context).format(number); + String realNumber = Util.getFirstNonEmpty(rewrites.get(formattedNumber), formattedNumber); + RecipientId recipientId = Recipient.externalContact(context, realNumber).getId(); String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); String contactPhotoUri = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.PHOTO_URI)); String contactLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL)); @@ -169,7 +147,6 @@ private static void updateContactsDatabase(@NonNull Context context, @NonNull Li Uri contactUri = ContactsContract.Contacts.getLookupUri(cursor.getLong(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone._ID)), cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY))); - handle.setSystemContactInfo(recipientId, displayName, contactPhotoUri, contactLabel, phoneType, contactUri.toString()); } } @@ -244,83 +221,121 @@ private static Optional createAccount(Context context) { } } - private static Future getLegacyDirectoryResult(@NonNull Context context, - @NonNull SignalServiceAccountManager accountManager, - @NonNull RecipientDatabase recipientDatabase, - @NonNull Set eligibleContactNumbers) + private static DirectoryResult getDirectoryResult(@NonNull Context context, + @NonNull SignalServiceAccountManager accountManager, + @NonNull RecipientDatabase recipientDatabase, + @NonNull Set locallyStoredNumbers, + @NonNull Set eligibleContactNumbers) + throws IOException { - return SignalExecutors.UNBOUNDED.submit(() -> { - List activeTokens = accountManager.getContacts(eligibleContactNumbers); + FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(eligibleContactNumbers, locallyStoredNumbers); + List activeTokens = accountManager.getContacts(inputResult.getNumbers()); + Set activeNumbers = Stream.of(activeTokens).map(ContactTokenDetails::getNumber).collect(Collectors.toSet()); + FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(activeNumbers, inputResult); - if (activeTokens != null) { - List activeIds = new LinkedList<>(); - List inactiveIds = new LinkedList<>(); + if (inputResult.getFuzzies().size() > 0) { + Log.i(TAG, "[getDirectoryResult] Got a fuzzy number result."); + } - Set inactiveContactNumbers = new HashSet<>(eligibleContactNumbers); + if (outputResult.getRewrites().size() > 0) { + Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers."); + } - for (ContactTokenDetails activeToken : activeTokens) { - activeIds.add(recipientDatabase.getOrInsertFromE164(activeToken.getNumber())); - inactiveContactNumbers.remove(activeToken.getNumber()); - } + recipientDatabase.updatePhoneNumbers(outputResult.getRewrites()); - for (String inactiveContactNumber : inactiveContactNumbers) { - inactiveIds.add(recipientDatabase.getOrInsertFromE164(inactiveContactNumber)); - } + List activeIds = new LinkedList<>(); + List inactiveIds = new LinkedList<>(); - Set currentActiveIds = new HashSet<>(recipientDatabase.getRegistered()); - Set contactIds = new HashSet<>(recipientDatabase.getSystemContacts()); - List newlyActiveIds = Stream.of(activeIds) - .filter(id -> !currentActiveIds.contains(id)) - .filter(contactIds::contains) - .toList(); + Set inactiveContactNumbers = new HashSet<>(inputResult.getNumbers()); + inactiveContactNumbers.removeAll(outputResult.getRewrites().keySet()); - recipientDatabase.setRegistered(activeIds, inactiveIds); - updateContactsDatabase(context, activeIds, true); + for (String number : outputResult.getNumbers()) { + activeIds.add(recipientDatabase.getOrInsertFromE164(number)); + inactiveContactNumbers.remove(number); + } - Set activeContactNumbers = Stream.of(activeIds).map(Recipient::resolved).filter(Recipient::hasSmsAddress).map(Recipient::requireSmsAddress).collect(Collectors.toSet()); + for (String inactiveContactNumber : inactiveContactNumbers) { + inactiveIds.add(recipientDatabase.getOrInsertFromE164(inactiveContactNumber)); + } - if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context)) { - return new DirectoryResult(activeContactNumbers, newlyActiveIds); - } else { - TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true); - return new DirectoryResult(activeContactNumbers); - } - } - return new DirectoryResult(Collections.emptySet(), Collections.emptyList()); - }); - } + Set currentActiveIds = new HashSet<>(recipientDatabase.getRegistered()); + Set contactIds = new HashSet<>(recipientDatabase.getSystemContacts()); + List newlyActiveIds = Stream.of(activeIds) + .filter(id -> !currentActiveIds.contains(id)) + .filter(contactIds::contains) + .toList(); - private static Future getLegacyRegisteredState(@NonNull Context context, - @NonNull SignalServiceAccountManager accountManager, - @NonNull RecipientDatabase recipientDatabase, - @NonNull Recipient recipient) - { - return SignalExecutors.UNBOUNDED.submit(() -> { - boolean activeUser = recipient.resolve().getRegistered() == RegisteredState.REGISTERED; - boolean systemContact = recipient.isSystemContact(); - Optional details = recipient.hasE164() ? accountManager.getContact(recipient.requireE164()) : Optional.absent(); + recipientDatabase.setRegistered(activeIds, inactiveIds); + updateContactsDatabase(context, activeIds, true, outputResult.getRewrites()); - if (details.isPresent()) { - recipientDatabase.setRegistered(recipient.getId(), RegisteredState.REGISTERED); + Set activeContactNumbers = Stream.of(activeIds).map(Recipient::resolved).filter(Recipient::hasSmsAddress).map(Recipient::requireSmsAddress).collect(Collectors.toSet()); - if (Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) { - updateContactsDatabase(context, Util.asList(recipient.getId()), false); - } + if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context)) { + return new DirectoryResult(activeContactNumbers, newlyActiveIds); + } else { + TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true); + return new DirectoryResult(activeContactNumbers); + } + } - if (!activeUser && TextSecurePreferences.isMultiDevice(context)) { - ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob()); + private static RegisteredState getRegisteredState(@NonNull Context context, + @NonNull SignalServiceAccountManager accountManager, + @NonNull RecipientDatabase recipientDatabase, + @NonNull Recipient recipient) + throws IOException + { + boolean activeUser = recipient.resolve().getRegistered() == RegisteredState.REGISTERED; + boolean systemContact = recipient.isSystemContact(); + Optional details = Optional.absent(); + Map rewrites = new HashMap<>(); + + if (recipient.hasE164()) { + FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(Collections.singletonList(recipient.requireE164()), recipientDatabase.getAllPhoneNumbers()); + + if (inputResult.getNumbers().size() > 1) { + Log.i(TAG, "[getRegisteredState] Got a fuzzy number result."); + + List detailList = accountManager.getContacts(inputResult.getNumbers()); + Collection registered = Stream.of(detailList).map(ContactTokenDetails::getNumber).collect(Collectors.toSet()); + FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(registered, inputResult); + String finalNumber = recipient.requireE164(); + ContactTokenDetails detail = new ContactTokenDetails(); + + if (outputResult.getRewrites().size() > 0 && outputResult.getRewrites().containsKey(finalNumber)) { + Log.i(TAG, "[getRegisteredState] Need to rewrite a number."); + finalNumber = outputResult.getRewrites().get(finalNumber); + rewrites = outputResult.getRewrites(); } - if (!activeUser && systemContact && !TextSecurePreferences.getNeedsSqlCipherMigration(context)) { - notifyNewUsers(context, Collections.singletonList(recipient.getId())); - } + detail.setNumber(finalNumber); + details = Optional.of(detail); - return RegisteredState.REGISTERED; + recipientDatabase.updatePhoneNumbers(outputResult.getRewrites()); } else { - recipientDatabase.setRegistered(recipient.getId(), RegisteredState.NOT_REGISTERED); - return RegisteredState.NOT_REGISTERED; + details = accountManager.getContact(recipient.requireE164()); } - }); + } + + if (details.isPresent()) { + recipientDatabase.setRegistered(recipient.getId(), RegisteredState.REGISTERED); + + if (Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) { + updateContactsDatabase(context, Util.asList(recipient.getId()), false, rewrites); + } + + if (!activeUser && TextSecurePreferences.isMultiDevice(context)) { + ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob()); + } + + if (!activeUser && systemContact && !TextSecurePreferences.getNeedsSqlCipherMigration(context)) { + notifyNewUsers(context, Collections.singletonList(recipient.getId())); + } + + return RegisteredState.REGISTERED; + } else { + recipientDatabase.setRegistered(recipient.getId(), RegisteredState.NOT_REGISTERED); + return RegisteredState.NOT_REGISTERED; + } } private static boolean isValidContactNumber(@Nullable String number) { @@ -396,5 +411,4 @@ public Account getAccount() { } } - } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/FuzzyPhoneNumberHelper.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/FuzzyPhoneNumberHelper.java new file mode 100644 index 00000000000..2618e42d745 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/FuzzyPhoneNumberHelper.java @@ -0,0 +1,130 @@ +package org.thoughtcrime.securesms.contacts.sync; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A helper class to match a single number with multiple possible registered numbers. An example is + * Mexican phone numbers, which recently removed a '1' after their country code. The idea is that + * when doing contact intersection, we can try both with and without the '1' and make a decision + * based on the results. + */ +class FuzzyPhoneNumberHelper { + + /** + * This should be run on the list of eligible numbers for contact intersection so that we can + * create an updated list that has potentially more "fuzzy" number matches in it. + */ + static @NonNull InputResult generateInput(@NonNull Collection testNumbers, @NonNull Collection storedNumbers) { + Set allNumbers = new HashSet<>(testNumbers); + Map fuzzies = new HashMap<>(); + + for (String number : testNumbers) { + if (mx(number)) { + String add1 = mxAdd1(number); + String strip1 = mxStrip1(number); + + if (mxMissing1(number) && !storedNumbers.contains(add1) && allNumbers.add(add1)) { + fuzzies.put(number, add1); + } else if (mxHas1(number) && !storedNumbers.contains(strip1) && allNumbers.add(strip1)) { + fuzzies.put(number, strip1); + } + } + } + + return new InputResult(allNumbers, fuzzies); + } + + /** + * This should be run on the list of numbers we find out are registered with the server. Based on + * these results and our initial input set, we can decide if we need to rewrite which number we + * have stored locally. + */ + static @NonNull OutputResult generateOutput(@NonNull Collection registeredNumbers, @NonNull InputResult inputResult) { + Set allNumbers = new HashSet<>(registeredNumbers); + Map rewrites = new HashMap<>(); + + for (Map.Entry entry : inputResult.getFuzzies().entrySet()) { + if (registeredNumbers.contains(entry.getKey()) && registeredNumbers.contains(entry.getValue())) { + if (mxHas1(entry.getKey())) { + rewrites.put(entry.getKey(), entry.getValue()); + allNumbers.remove(entry.getKey()); + } else { + allNumbers.remove(entry.getValue()); + } + } else if (registeredNumbers.contains(entry.getValue())) { + rewrites.put(entry.getKey(), entry.getValue()); + allNumbers.remove(entry.getKey()); + } + } + + return new OutputResult(allNumbers, rewrites); + } + + + private static boolean mx(@NonNull String number) { + return number.startsWith("+52") && (number.length() == 13 || number.length() == 14); + } + + private static boolean mxHas1(@NonNull String number) { + return number.startsWith("+521") && number.length() == 14; + } + + private static boolean mxMissing1(@NonNull String number) { + return number.startsWith("+52") && !number.startsWith("+521") && number.length() == 13; + } + + private static @NonNull String mxStrip1(@NonNull String number) { + return mxHas1(number) ? "+52" + number.substring("+521".length()) + : number; + } + + private static @NonNull String mxAdd1(@NonNull String number) { + return mxMissing1(number) ? "+521" + number.substring("+52".length()) + : number; + } + + + public static class InputResult { + private final Set numbers; + private final Map fuzzies; + + @VisibleForTesting + InputResult(@NonNull Set numbers, @NonNull Map fuzzies) { + this.numbers = numbers; + this.fuzzies = fuzzies; + } + + public @NonNull Set getNumbers() { + return numbers; + } + + public @NonNull Map getFuzzies() { + return fuzzies; + } + } + + public static class OutputResult { + private final Set numbers; + private final Map rewrites; + + private OutputResult(@NonNull Set numbers, @NonNull Map rewrites) { + this.numbers = numbers; + this.rewrites = rewrites; + } + + public @NonNull Set getNumbers() { + return numbers; + } + + public @NonNull Map getRewrites() { + return rewrites; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index 7354840fdfc..254db429b0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -456,6 +456,28 @@ public void applyStorageSyncUpdates(@NonNull Collection ins } } + public void updatePhoneNumbers(@NonNull Map mapping) { + if (mapping.isEmpty()) return; + + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + + db.beginTransaction(); + try { + String query = PHONE + " = ?"; + + for (Map.Entry entry : mapping.entrySet()) { + ContentValues values = new ContentValues(); + values.put(PHONE, entry.getValue()); + + db.updateWithOnConflict(TABLE_NAME, values, query, new String[] { entry.getKey() }, SQLiteDatabase.CONFLICT_IGNORE); + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + private @NonNull RecipientId getByStorageKeyOrThrow(byte[] storageKey) { SQLiteDatabase db = databaseHelper.getReadableDatabase(); String query = STORAGE_SERVICE_KEY + " = ?"; diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index 6a54cc66c21..fa7fda32bbc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -214,11 +214,18 @@ public class Recipient { */ @WorkerThread public static @NonNull Recipient externalContact(@NonNull Context context, @NonNull String identifier) { + RecipientDatabase db = DatabaseFactory.getRecipientDatabase(context); + RecipientId id = null; + if (UuidUtil.isUuid(identifier)) { throw new UuidRecipientError(); + } else if (NumberUtil.isValidEmail(identifier)) { + id = db.getOrInsertFromEmail(identifier); } else { - return external(context, identifier); + id = db.getOrInsertFromE164(identifier); } + + return Recipient.resolved(id); } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java index 667194e97b4..e90221e0d88 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java @@ -70,7 +70,7 @@ private static SignalServiceProfile retrieveProfileInternal(@NonNull SignalServi SignalServiceMessagePipe authPipe = IncomingMessageObserver.getPipe(); SignalServiceMessagePipe unidentifiedPipe = IncomingMessageObserver.getUnidentifiedPipe(); SignalServiceMessagePipe pipe = unidentifiedPipe != null && unidentifiedAccess.isPresent() ? unidentifiedPipe - : authPipe; + : authPipe; if (pipe != null) { try { diff --git a/app/src/test/java/org/thoughtcrime/securesms/contacts/sync/FuzzyPhoneNumberHelperTest.java b/app/src/test/java/org/thoughtcrime/securesms/contacts/sync/FuzzyPhoneNumberHelperTest.java new file mode 100644 index 00000000000..a8c6cb8fb6d --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/contacts/sync/FuzzyPhoneNumberHelperTest.java @@ -0,0 +1,164 @@ +package org.thoughtcrime.securesms.contacts.sync; + +import org.junit.Test; +import org.thoughtcrime.securesms.contacts.sync.FuzzyPhoneNumberHelper.InputResult; +import org.thoughtcrime.securesms.contacts.sync.FuzzyPhoneNumberHelper.OutputResult; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import edu.emory.mathcs.backport.java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class FuzzyPhoneNumberHelperTest { + + private static final String US_A = "+16108675309"; + private static final String US_B = "+16101234567"; + + private static final String MX_A = "+525512345678"; + private static final String MX_A_1 = "+5215512345678"; + + @Test + public void generateInput_noMxNumbers() { + InputResult result = FuzzyPhoneNumberHelper.generateInput(setOf(US_A, US_B), setOf(US_A, US_B)); + + assertEquals(2, result.getNumbers().size()); + assertTrue(result.getNumbers().containsAll(setOf(US_A, US_B))); + assertTrue(result.getFuzzies().isEmpty()); + } + + @Test + public void generateInput_mxWith1_without1NotStored() { + InputResult result = FuzzyPhoneNumberHelper.generateInput(setOf(US_A, MX_A_1), setOf(US_A, MX_A_1)); + + assertEquals(3, result.getNumbers().size()); + assertTrue(result.getNumbers().containsAll(setOf(US_A, MX_A_1, MX_A))); + assertEquals(MX_A, result.getFuzzies().get(MX_A_1)); + } + + @Test + public void generateInput_mxWith1_without1AlreadyStored() { + InputResult result = FuzzyPhoneNumberHelper.generateInput(setOf(US_A, MX_A_1), setOf(US_A, MX_A_1, MX_A)); + + assertEquals(2, result.getNumbers().size()); + assertTrue(result.getNumbers().containsAll(setOf(US_A, MX_A_1))); + assertTrue(result.getFuzzies().isEmpty()); + } + + @Test + public void generateInput_mxWithout1_with1NotStored() { + InputResult result = FuzzyPhoneNumberHelper.generateInput(setOf(US_A, MX_A), setOf(US_A, MX_A)); + + assertEquals(3, result.getNumbers().size()); + assertTrue(result.getNumbers().containsAll(setOf(US_A, MX_A_1, MX_A))); + assertEquals(MX_A_1, result.getFuzzies().get(MX_A)); + } + + @Test + public void generateInput_mxWithout1_with1AlreadyStored() { + InputResult result = FuzzyPhoneNumberHelper.generateInput(setOf(US_A, MX_A), setOf(US_A, MX_A_1, MX_A)); + + assertEquals(2, result.getNumbers().size()); + assertTrue(result.getNumbers().containsAll(setOf(US_A, MX_A))); + assertTrue(result.getFuzzies().isEmpty()); + } + + @Test + public void generateInput_mxWithAndWithout1_neitherStored() { + InputResult result = FuzzyPhoneNumberHelper.generateInput(setOf(US_A, MX_A_1, MX_A), setOf(US_A)); + + assertEquals(3, result.getNumbers().size()); + assertTrue(result.getNumbers().containsAll(setOf(US_A, MX_A_1, MX_A))); + assertTrue(result.getFuzzies().isEmpty()); + } + + @Test + public void generateInput_mxWithAndWithout1_with1AlreadyStored() { + InputResult result = FuzzyPhoneNumberHelper.generateInput(setOf(US_A, MX_A_1, MX_A), setOf(US_A, MX_A_1)); + + assertEquals(3, result.getNumbers().size()); + assertTrue(result.getNumbers().containsAll(setOf(US_A, MX_A_1, MX_A))); + assertTrue(result.getFuzzies().isEmpty()); + } + + @Test + public void generateInput_mxWithAndWithout1_without1AlreadyStored() { + InputResult result = FuzzyPhoneNumberHelper.generateInput(setOf(US_A, MX_A_1, MX_A), setOf(US_A, MX_A)); + + assertEquals(3, result.getNumbers().size()); + assertTrue(result.getNumbers().containsAll(setOf(US_A, MX_A_1, MX_A))); + assertTrue(result.getFuzzies().isEmpty()); + } + + @Test + public void generateOutput_noMxNumbers() { + OutputResult result = FuzzyPhoneNumberHelper.generateOutput(setOf(US_A, US_B), new InputResult(setOf(US_A, US_B), Collections.emptyMap())); + + assertEquals(2, result.getNumbers().size()); + assertTrue(result.getNumbers().containsAll(setOf(US_A, US_B))); + assertTrue(result.getRewrites().isEmpty()); + } + + @Test + public void generateOutput_bothMatch_no1To1() { + OutputResult result = FuzzyPhoneNumberHelper.generateOutput(setOf(MX_A, MX_A_1), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A, MX_A_1))); + + assertEquals(1, result.getNumbers().size()); + assertTrue(result.getNumbers().containsAll(setOf(MX_A))); + assertTrue(result.getRewrites().isEmpty()); + } + + @Test + public void generateOutput_bothMatch_1toNo1() { + OutputResult result = FuzzyPhoneNumberHelper.generateOutput(setOf(MX_A, MX_A_1), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A_1, MX_A))); + + assertEquals(1, result.getNumbers().size()); + assertTrue(result.getNumbers().containsAll(setOf(MX_A))); + assertEquals(MX_A, result.getRewrites().get(MX_A_1)); + } + + @Test + public void generateOutput_no1Match_no1To1() { + OutputResult result = FuzzyPhoneNumberHelper.generateOutput(setOf(MX_A), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A, MX_A_1))); + + assertEquals(1, result.getNumbers().size()); + assertTrue(result.getNumbers().containsAll(setOf(MX_A))); + assertTrue(result.getRewrites().isEmpty()); + } + + @Test + public void generateOutput_no1Match_1ToNo1() { + OutputResult result = FuzzyPhoneNumberHelper.generateOutput(setOf(MX_A), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A_1, MX_A))); + + assertEquals(1, result.getNumbers().size()); + assertTrue(result.getNumbers().containsAll(setOf(MX_A))); + assertEquals(MX_A, result.getRewrites().get(MX_A_1)); + } + + @Test + public void generateOutput_1Match_1ToNo1() { + OutputResult result = FuzzyPhoneNumberHelper.generateOutput(setOf(MX_A_1), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A_1, MX_A))); + + assertEquals(1, result.getNumbers().size()); + assertTrue(result.getNumbers().containsAll(setOf(MX_A_1))); + assertTrue(result.getRewrites().isEmpty()); + } + + @Test + public void generateOutput_1Match_no1To1() { + OutputResult result = FuzzyPhoneNumberHelper.generateOutput(setOf(MX_A_1), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A, MX_A_1))); + + assertEquals(1, result.getNumbers().size()); + assertTrue(result.getNumbers().containsAll(setOf(MX_A_1))); + assertEquals(MX_A_1, result.getRewrites().get(MX_A)); + } + + + private static Set setOf(E... values) { + //noinspection unchecked + return new HashSet<>(Arrays.asList(values)); + } +}