diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_getAndPossiblyMerge.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_getAndPossiblyMergeLegacy.kt similarity index 95% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_getAndPossiblyMerge.kt rename to app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_getAndPossiblyMergeLegacy.kt index d1a5a2e9e3..0f08fca545 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_getAndPossiblyMerge.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_getAndPossiblyMergeLegacy.kt @@ -46,7 +46,7 @@ import java.util.Optional import java.util.UUID @RunWith(AndroidJUnit4::class) -class RecipientDatabaseTest_getAndPossiblyMerge { +class RecipientDatabaseTest_getAndPossiblyMergeLegacy { private lateinit var recipientDatabase: RecipientDatabase private lateinit var identityDatabase: IdentityDatabase @@ -93,7 +93,7 @@ class RecipientDatabaseTest_getAndPossiblyMerge { /** If all you have is an ACI, you can just store that, regardless of trust level. */ @Test fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciOnly() { - val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null) + val recipientId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, null) val recipient = Recipient.resolved(recipientId) assertEquals(ACI_A, recipient.requireServiceId()) @@ -103,7 +103,7 @@ class RecipientDatabaseTest_getAndPossiblyMerge { /** If all you have is an E164, you can just store that, regardless of trust level. */ @Test fun getAndPossiblyMerge_aciAndE164MapToNoOne_e164Only() { - val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A) + val recipientId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(null, E164_A) val recipient = Recipient.resolved(recipientId) assertEquals(E164_A, recipient.requireE164()) @@ -113,7 +113,7 @@ class RecipientDatabaseTest_getAndPossiblyMerge { /** With high trust, you can associate an ACI-e164 pair. */ @Test fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciAndE164() { - val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A) + val recipientId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A) val recipient = Recipient.resolved(recipientId) assertEquals(ACI_A, recipient.requireServiceId()) @@ -129,7 +129,7 @@ class RecipientDatabaseTest_getAndPossiblyMerge { fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164() { val existingId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A) - val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A) + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A) assertEquals(existingId, retrievedId) val retrievedRecipient = Recipient.resolved(retrievedId) @@ -140,9 +140,9 @@ class RecipientDatabaseTest_getAndPossiblyMerge { /** Basically the ‘change number’ case. Update the existing user. */ @Test fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2() { - val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A) + val existingId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A) - val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B) + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_B) assertEquals(existingId, retrievedId) val retrievedRecipient = Recipient.resolved(retrievedId) @@ -159,7 +159,7 @@ class RecipientDatabaseTest_getAndPossiblyMerge { fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164() { val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A) - val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A) + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A) assertEquals(existingId, retrievedId) val retrievedRecipient = Recipient.resolved(retrievedId) @@ -170,10 +170,10 @@ class RecipientDatabaseTest_getAndPossiblyMerge { /** We never change the ACI of an existing row. New ACI = new person. Take the e164 from the current holder. */ @Test fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2() { - val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A) + val existingId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A) recipientDatabase.setPni(existingId, PNI_A) - val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A) + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_B, E164_A) recipientDatabase.setPni(retrievedId, PNI_A) assertNotEquals(existingId, retrievedId) @@ -195,9 +195,9 @@ class RecipientDatabaseTest_getAndPossiblyMerge { } SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet))) - val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A) + val existingId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A) - val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A) + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_B, E164_A) assertNotEquals(existingId, retrievedId) val retrievedRecipient = Recipient.resolved(retrievedId) @@ -216,9 +216,9 @@ class RecipientDatabaseTest_getAndPossiblyMerge { /** If your ACI and e164 match, you’re good. */ @Test fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164() { - val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A) + val existingId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A) - val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A) + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A) assertEquals(existingId, retrievedId) val retrievedRecipient = Recipient.resolved(retrievedId) @@ -232,10 +232,10 @@ class RecipientDatabaseTest_getAndPossiblyMerge { val changeNumberListener = ChangeNumberListener() changeNumberListener.enqueue() - val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null) - val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A) + val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, null) + val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(null, E164_A) - val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A) + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A) assertEquals(existingAciId, retrievedId) val retrievedRecipient = Recipient.resolved(retrievedId) @@ -255,10 +255,10 @@ class RecipientDatabaseTest_getAndPossiblyMerge { val changeNumberListener = ChangeNumberListener() changeNumberListener.enqueue() - val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B) - val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A) + val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_B) + val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(null, E164_A) - val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A) + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A) assertEquals(existingAciId, retrievedId) val retrievedRecipient = Recipient.resolved(retrievedId) @@ -278,10 +278,10 @@ class RecipientDatabaseTest_getAndPossiblyMerge { val changeNumberListener = ChangeNumberListener() changeNumberListener.enqueue() - val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B) - val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A) + val existingId1: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_B) + val existingId2: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_B, E164_A) - val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A) + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A) assertEquals(existingId1, retrievedId) val retrievedRecipient = Recipient.resolved(retrievedId) @@ -301,10 +301,10 @@ class RecipientDatabaseTest_getAndPossiblyMerge { */ @Test fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_mergeAndPhoneNumberChange() { - val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B) - val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A) + val existingId1: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_B) + val existingId2: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(null, E164_A) - val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A) + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A) assertEquals(existingId1, retrievedId) val retrievedRecipient = Recipient.resolved(retrievedId) @@ -326,10 +326,10 @@ class RecipientDatabaseTest_getAndPossiblyMerge { } SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet))) - val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A) - val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null) + val existingId1: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_B, E164_A) + val existingId2: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, null) - val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A) + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A) assertEquals(existingId2, retrievedId) val retrievedRecipient = Recipient.resolved(retrievedId) @@ -350,9 +350,9 @@ class RecipientDatabaseTest_getAndPossiblyMerge { } SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet))) - val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A) + val existingId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A) - val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, changeSelf = false) + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_B, changeSelf = false) assertEquals(existingId, retrievedId) val retrievedRecipient = Recipient.resolved(retrievedId) @@ -369,9 +369,9 @@ class RecipientDatabaseTest_getAndPossiblyMerge { } SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet))) - val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A) + val existingId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A) - val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, changeSelf = true) + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_B, changeSelf = true) assertEquals(existingId, retrievedId) val retrievedRecipient = Recipient.resolved(retrievedId) @@ -385,9 +385,9 @@ class RecipientDatabaseTest_getAndPossiblyMerge { val changeNumberListener = ChangeNumberListener() changeNumberListener.enqueue() - val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A) + val existingId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A) - val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B) + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_B) assertEquals(existingId, retrievedId) val retrievedRecipient = Recipient.resolved(retrievedId) @@ -445,7 +445,7 @@ class RecipientDatabaseTest_getAndPossiblyMerge { val distributionListId: DistributionListId = distributionListDatabase.createList("testlist", listOf(recipientIdE164, recipientIdAciB))!! // Merge - val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true) + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A, true) val retrievedThreadId: Long = threadDatabase.getThreadIdFor(retrievedId)!! assertEquals(recipientIdAci, retrievedId) @@ -572,7 +572,7 @@ class RecipientDatabaseTest_getAndPossiblyMerge { @Test(expected = IllegalArgumentException::class) fun getAndPossiblyMerge_noArgs_invalid() { - recipientDatabase.getAndPossiblyMerge(null, null, true) + recipientDatabase.getAndPossiblyMergeLegacy(null, null, true) } private fun ensureDbEmpty() { diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_getAndPossiblyMergePnp.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_getAndPossiblyMergePnp.kt new file mode 100644 index 0000000000..f23dbe5872 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_getAndPossiblyMergePnp.kt @@ -0,0 +1,671 @@ +package org.thoughtcrime.securesms.database + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import org.signal.core.util.CursorUtil +import org.signal.core.util.ThreadUtil +import org.signal.libsignal.protocol.IdentityKey +import org.signal.libsignal.protocol.SignalProtocolAddress +import org.signal.libsignal.protocol.state.SessionRecord +import org.signal.libsignal.zkgroup.groups.GroupMasterKey +import org.signal.storageservice.protos.groups.local.DecryptedGroup +import org.signal.storageservice.protos.groups.local.DecryptedMember +import org.thoughtcrime.securesms.conversation.colors.AvatarColor +import org.thoughtcrime.securesms.database.model.DistributionListId +import org.thoughtcrime.securesms.database.model.DistributionListRecord +import org.thoughtcrime.securesms.database.model.Mention +import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.model.ReactionRecord +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.jobs.RecipientChangedNumberJob +import org.thoughtcrime.securesms.keyvalue.AccountValues +import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet +import org.thoughtcrime.securesms.keyvalue.KeyValueStore +import org.thoughtcrime.securesms.keyvalue.MockKeyValuePersistentStorage +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.mms.IncomingMediaMessage +import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.sms.IncomingTextMessage +import org.whispersystems.signalservice.api.push.ACI +import org.whispersystems.signalservice.api.push.PNI +import org.whispersystems.signalservice.api.util.UuidUtil +import java.util.Optional +import java.util.UUID + +@RunWith(AndroidJUnit4::class) +class RecipientDatabaseTest_getAndPossiblyMergePnp { + + private lateinit var recipientDatabase: RecipientDatabase + private lateinit var identityDatabase: IdentityDatabase + private lateinit var groupReceiptDatabase: GroupReceiptDatabase + private lateinit var groupDatabase: GroupDatabase + private lateinit var threadDatabase: ThreadDatabase + private lateinit var smsDatabase: MessageDatabase + private lateinit var mmsDatabase: MessageDatabase + private lateinit var sessionDatabase: SessionDatabase + private lateinit var mentionDatabase: MentionDatabase + private lateinit var reactionDatabase: ReactionDatabase + private lateinit var notificationProfileDatabase: NotificationProfileDatabase + private lateinit var distributionListDatabase: DistributionListDatabase + + private val localAci = ACI.from(UUID.randomUUID()) + private val localPni = PNI.from(UUID.randomUUID()) + + @Before + fun setup() { + recipientDatabase = SignalDatabase.recipients + recipientDatabase = SignalDatabase.recipients + identityDatabase = SignalDatabase.identities + groupReceiptDatabase = SignalDatabase.groupReceipts + groupDatabase = SignalDatabase.groups + threadDatabase = SignalDatabase.threads + smsDatabase = SignalDatabase.sms + mmsDatabase = SignalDatabase.mms + sessionDatabase = SignalDatabase.sessions + mentionDatabase = SignalDatabase.mentions + reactionDatabase = SignalDatabase.reactions + notificationProfileDatabase = SignalDatabase.notificationProfiles + distributionListDatabase = SignalDatabase.distributionLists + + ensureDbEmpty() + + SignalStore.account().setAci(localAci) + SignalStore.account().setPni(localPni) + } + + // ============================================================== + // If both the ACI and E164 map to no one + // ============================================================== + + /** If all you have is an ACI, you can just store that, regardless of trust level. */ + @Test + fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciOnly() { + val recipientId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, null) + + val recipient = Recipient.resolved(recipientId) + assertEquals(ACI_A, recipient.requireServiceId()) + assertFalse(recipient.hasE164()) + } + + /** If all you have is an E164, you can just store that, regardless of trust level. */ + @Test + fun getAndPossiblyMerge_aciAndE164MapToNoOne_e164Only() { + val recipientId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(null, E164_A) + + val recipient = Recipient.resolved(recipientId) + assertEquals(E164_A, recipient.requireE164()) + assertFalse(recipient.hasServiceId()) + } + + /** With high trust, you can associate an ACI-e164 pair. */ + @Test + fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciAndE164() { + val recipientId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A) + + val recipient = Recipient.resolved(recipientId) + assertEquals(ACI_A, recipient.requireServiceId()) + assertEquals(E164_A, recipient.requireE164()) + } + + // ============================================================== + // If the ACI maps to an existing user, but the E164 doesn't + // ============================================================== + + /** You can associate an e164 with an existing ACI. */ + @Test + fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164() { + val existingId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A) + assertEquals(existingId, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireServiceId()) + assertEquals(E164_A, retrievedRecipient.requireE164()) + } + + /** Basically the ‘change number’ case. Update the existing user. */ + @Test + fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2() { + val existingId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_B) + assertEquals(existingId, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireServiceId()) + assertEquals(E164_B, retrievedRecipient.requireE164()) + } + + // ============================================================== + // If the E164 maps to an existing user, but the ACI doesn't + // ============================================================== + + /** You can associate an e164 with an existing ACI. */ + @Test + fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164() { + val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A) + assertEquals(existingId, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireServiceId()) + assertEquals(E164_A, retrievedRecipient.requireE164()) + } + + /** We never change the ACI of an existing row. New ACI = new person. Take the e164 from the current holder. */ + @Test + fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2() { + val existingId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A) + recipientDatabase.setPni(existingId, PNI_A) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_B, E164_A) + recipientDatabase.setPni(retrievedId, PNI_A) + assertNotEquals(existingId, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_B, retrievedRecipient.requireServiceId()) + assertEquals(E164_A, retrievedRecipient.requireE164()) + + val existingRecipient = Recipient.resolved(existingId) + assertEquals(ACI_A, existingRecipient.requireServiceId()) + assertFalse(existingRecipient.hasE164()) + } + + /** We never want to remove the e164 of our own contact entry. Leave the e164 alone. */ + @Ignore("Change self isn't implemented yet!") + @Test + fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_e164BelongsToLocalUser() { + val dataSet = KeyValueDataSet().apply { + putString(AccountValues.KEY_E164, E164_A) + putString(AccountValues.KEY_ACI, ACI_A.toString()) + } + SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet))) + + val existingId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_B, E164_A) + assertNotEquals(existingId, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_B, retrievedRecipient.requireServiceId()) + assertFalse(retrievedRecipient.hasE164()) + + val existingRecipient = Recipient.resolved(existingId) + assertEquals(ACI_A, existingRecipient.requireServiceId()) + assertEquals(E164_A, existingRecipient.requireE164()) + } + + // ============================================================== + // If both the ACI and E164 map to an existing user + // ============================================================== + + /** If your ACI and e164 match, you’re good. */ + @Test + fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164() { + val existingId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A) + assertEquals(existingId, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireServiceId()) + assertEquals(E164_A, retrievedRecipient.requireE164()) + } + + /** Merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */ + @Test + fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge() { + val changeNumberListener = ChangeNumberListener() + changeNumberListener.enqueue() + + val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, null) + val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMergePnp(null, E164_A) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A) + assertEquals(existingAciId, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireServiceId()) + assertEquals(E164_A, retrievedRecipient.requireE164()) + + // TODO: Recipient remapping! +// val existingE164Recipient = Recipient.resolved(existingE164Id) +// assertEquals(retrievedId, existingE164Recipient.id) + + changeNumberListener.waitForJobManager() + assertFalse(changeNumberListener.numberChangeWasEnqueued) + } + + /** Same as [getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge], but with a number change. */ + @Test + fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_changedNumber() { + val changeNumberListener = ChangeNumberListener() + changeNumberListener.enqueue() + + val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_B) + val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMergePnp(null, E164_A) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A) + assertEquals(existingAciId, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireServiceId()) + assertEquals(E164_A, retrievedRecipient.requireE164()) + + // TODO: Recipient remapping! +// val existingE164Recipient = Recipient.resolved(existingE164Id) +// assertEquals(retrievedId, existingE164Recipient.id) + + // TODO: Change number! +// changeNumberListener.waitForJobManager() +// assert(changeNumberListener.numberChangeWasEnqueued) + } + + /** No new rules here, just a more complex scenario to show how different rules interact. */ + @Test + fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex() { + val changeNumberListener = ChangeNumberListener() + changeNumberListener.enqueue() + + val existingId1: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_B) + val existingId2: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_B, E164_A) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A) + assertEquals(existingId1, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireServiceId()) + assertEquals(E164_A, retrievedRecipient.requireE164()) + + val existingRecipient2 = Recipient.resolved(existingId2) + assertEquals(ACI_B, existingRecipient2.requireServiceId()) + assertFalse(existingRecipient2.hasE164()) + + // TODO: Change number! +// assert(changeNumberListener.numberChangeWasEnqueued) + } + + /** + * Another case that results in a merge. Nothing strictly new here, but this case is called out because it’s a merge but *also* an E164 change, + * which clients may need to know for UX purposes. + */ + @Test + fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_mergeAndPhoneNumberChange() { + val existingId1: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_B) + val existingId2: RecipientId = recipientDatabase.getAndPossiblyMergePnp(null, E164_A) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A) + assertEquals(existingId1, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireServiceId()) + assertEquals(E164_A, retrievedRecipient.requireE164()) + + assertFalse(recipientDatabase.getByE164(E164_B).isPresent) + + // TODO: Recipient remapping! +// val recipientWithId2 = Recipient.resolved(existingId2) +// assertEquals(retrievedId, recipientWithId2.id) + } + + /** We never want to remove the e164 of our own contact entry. Leave the e164 alone. */ + @Ignore("Change self isn't implemented yet!") + @Test + fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_e164BelongsToLocalUser() { + val dataSet = KeyValueDataSet().apply { + putString(AccountValues.KEY_E164, E164_A) + putString(AccountValues.KEY_ACI, ACI_B.toString()) + } + SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet))) + + val existingId1: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_B, E164_A) + val existingId2: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, null) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A) + assertEquals(existingId2, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireServiceId()) + assertFalse(retrievedRecipient.hasE164()) + + val recipientWithId1 = Recipient.resolved(existingId1) + assertEquals(ACI_B, recipientWithId1.requireServiceId()) + assertEquals(E164_A, recipientWithId1.requireE164()) + } + + /** This is a case where normally we'd update the E164 of a user, but here the changeSelf flag is disabled, so we shouldn't. */ + @Ignore("Change self isn't implemented yet!") + @Test + fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_changeSelfFalse() { + val dataSet = KeyValueDataSet().apply { + putString(AccountValues.KEY_E164, E164_A) + putString(AccountValues.KEY_ACI, ACI_A.toString()) + } + SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet))) + + val existingId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_B, changeSelf = false) + assertEquals(existingId, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireServiceId()) + assertEquals(E164_A, retrievedRecipient.requireE164()) + } + + /** This is a case where we're changing our own number, and it's allowed because changeSelf = true. */ + @Test + fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_changeSelfTrue() { + val dataSet = KeyValueDataSet().apply { + putString(AccountValues.KEY_E164, E164_A) + putString(AccountValues.KEY_ACI, ACI_A.toString()) + } + SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet))) + + val existingId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_B, changeSelf = true) + assertEquals(existingId, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireServiceId()) + assertEquals(E164_B, retrievedRecipient.requireE164()) + } + + /** Verifying a case where a change number job is expected to be enqueued. */ + @Test + fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_changedNumber() { + val changeNumberListener = ChangeNumberListener() + changeNumberListener.enqueue() + + val existingId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_B) + assertEquals(existingId, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireServiceId()) + assertEquals(E164_B, retrievedRecipient.requireE164()) + + // TODO: Change number! +// changeNumberListener.waitForJobManager() +// assert(changeNumberListener.numberChangeWasEnqueued) + } + + /** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */ + @Ignore("This level of merging isn't implemented yet!") + @Test + fun getAndPossiblyMerge_merge_general() { + // Setup + val recipientIdAci: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A) + val recipientIdE164: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A) + val recipientIdAciB: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_B) + + val smsId1: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 0, body = "0")).get().messageId + val smsId2: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdE164, time = 1, body = "1")).get().messageId + val smsId3: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 2, body = "2")).get().messageId + + val mmsId1: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 3, body = "3"), -1).get().messageId + val mmsId2: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdE164, time = 4, body = "4"), -1).get().messageId + val mmsId3: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 5, body = "5"), -1).get().messageId + + val threadIdAci: Long = threadDatabase.getThreadIdFor(recipientIdAci)!! + val threadIdE164: Long = threadDatabase.getThreadIdFor(recipientIdE164)!! + assertNotEquals(threadIdAci, threadIdE164) + + mentionDatabase.insert(threadIdAci, mmsId1, listOf(Mention(recipientIdE164, 0, 1))) + mentionDatabase.insert(threadIdE164, mmsId2, listOf(Mention(recipientIdAci, 0, 1))) + + groupReceiptDatabase.insert(listOf(recipientIdAci, recipientIdE164), mmsId1, 0, 3) + + val identityKeyAci: IdentityKey = identityKey(1) + val identityKeyE164: IdentityKey = identityKey(2) + + identityDatabase.saveIdentity(ACI_A.toString(), recipientIdAci, identityKeyAci, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false) + identityDatabase.saveIdentity(E164_A, recipientIdE164, identityKeyE164, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false) + + sessionDatabase.store(localAci, SignalProtocolAddress(ACI_A.toString(), 1), SessionRecord()) + + reactionDatabase.addReaction(MessageId(smsId1, false), ReactionRecord("a", recipientIdAci, 1, 1)) + reactionDatabase.addReaction(MessageId(mmsId1, true), ReactionRecord("b", recipientIdE164, 1, 1)) + + val profile1: NotificationProfile = notificationProfile(name = "Test") + val profile2: NotificationProfile = notificationProfile(name = "Test2") + + notificationProfileDatabase.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdAci) + notificationProfileDatabase.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdE164) + notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdE164) + notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdAciB) + + val distributionListId: DistributionListId = distributionListDatabase.createList("testlist", listOf(recipientIdE164, recipientIdAciB))!! + + // Merge + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A, true) + val retrievedThreadId: Long = threadDatabase.getThreadIdFor(retrievedId)!! + assertEquals(recipientIdAci, retrievedId) + + // Recipient validation + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireServiceId()) + assertEquals(E164_A, retrievedRecipient.requireE164()) + + val existingE164Recipient = Recipient.resolved(recipientIdE164) + assertEquals(retrievedId, existingE164Recipient.id) + + // Thread validation + assertEquals(threadIdAci, retrievedThreadId) + Assert.assertNull(threadDatabase.getThreadIdFor(recipientIdE164)) + Assert.assertNull(threadDatabase.getThreadRecord(threadIdE164)) + + // SMS validation + val sms1: MessageRecord = smsDatabase.getMessageRecord(smsId1)!! + val sms2: MessageRecord = smsDatabase.getMessageRecord(smsId2)!! + val sms3: MessageRecord = smsDatabase.getMessageRecord(smsId3)!! + + assertEquals(retrievedId, sms1.recipient.id) + assertEquals(retrievedId, sms2.recipient.id) + assertEquals(retrievedId, sms3.recipient.id) + + assertEquals(retrievedThreadId, sms1.threadId) + assertEquals(retrievedThreadId, sms2.threadId) + assertEquals(retrievedThreadId, sms3.threadId) + + // MMS validation + val mms1: MessageRecord = mmsDatabase.getMessageRecord(mmsId1)!! + val mms2: MessageRecord = mmsDatabase.getMessageRecord(mmsId2)!! + val mms3: MessageRecord = mmsDatabase.getMessageRecord(mmsId3)!! + + assertEquals(retrievedId, mms1.recipient.id) + assertEquals(retrievedId, mms2.recipient.id) + assertEquals(retrievedId, mms3.recipient.id) + + assertEquals(retrievedThreadId, mms1.threadId) + assertEquals(retrievedThreadId, mms2.threadId) + assertEquals(retrievedThreadId, mms3.threadId) + + // Mention validation + val mention1: MentionModel = getMention(mmsId1) + assertEquals(retrievedId, mention1.recipientId) + assertEquals(retrievedThreadId, mention1.threadId) + + val mention2: MentionModel = getMention(mmsId2) + assertEquals(retrievedId, mention2.recipientId) + assertEquals(retrievedThreadId, mention2.threadId) + + // Group receipt validation + val groupReceipts: List = groupReceiptDatabase.getGroupReceiptInfo(mmsId1) + assertEquals(retrievedId, groupReceipts[0].recipientId) + assertEquals(retrievedId, groupReceipts[1].recipientId) + + // Identity validation + assertEquals(identityKeyAci, identityDatabase.getIdentityStoreRecord(ACI_A.toString())!!.identityKey) + Assert.assertNull(identityDatabase.getIdentityStoreRecord(E164_A)) + + // Session validation + Assert.assertNotNull(sessionDatabase.load(localAci, SignalProtocolAddress(ACI_A.toString(), 1))) + + // Reaction validation + val reactionsSms: List = reactionDatabase.getReactions(MessageId(smsId1, false)) + val reactionsMms: List = reactionDatabase.getReactions(MessageId(mmsId1, true)) + + assertEquals(1, reactionsSms.size) + assertEquals(ReactionRecord("a", recipientIdAci, 1, 1), reactionsSms[0]) + + assertEquals(1, reactionsMms.size) + assertEquals(ReactionRecord("b", recipientIdAci, 1, 1), reactionsMms[0]) + + // Notification Profile validation + val updatedProfile1: NotificationProfile = notificationProfileDatabase.getProfile(profile1.id)!! + val updatedProfile2: NotificationProfile = notificationProfileDatabase.getProfile(profile2.id)!! + + MatcherAssert.assertThat("Notification Profile 1 should now only contain ACI $recipientIdAci", updatedProfile1.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci)) + MatcherAssert.assertThat("Notification Profile 2 should now contain ACI A ($recipientIdAci) and ACI B ($recipientIdAciB)", updatedProfile2.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB)) + + // Distribution List validation + val updatedList: DistributionListRecord = distributionListDatabase.getList(distributionListId)!! + + MatcherAssert.assertThat("Distribution list should have updated $recipientIdE164 to $recipientIdAci", updatedList.members, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB)) + } + + // ============================================================== + // Misc + // ============================================================== + + @Test + fun createByE164SanityCheck() { + // GIVEN one recipient + val recipientId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A) + + // WHEN I retrieve one by E164 + val possible: Optional = recipientDatabase.getByE164(E164_A) + + // THEN I get it back, and it has the properties I expect + assertTrue(possible.isPresent) + assertEquals(recipientId, possible.get()) + + val recipient = Recipient.resolved(recipientId) + assertTrue(recipient.e164.isPresent) + assertEquals(E164_A, recipient.e164.get()) + } + + @Test + fun createByUuidSanityCheck() { + // GIVEN one recipient + val recipientId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A) + + // WHEN I retrieve one by UUID + val possible: Optional = recipientDatabase.getByServiceId(ACI_A) + + // THEN I get it back, and it has the properties I expect + assertTrue(possible.isPresent) + assertEquals(recipientId, possible.get()) + + val recipient = Recipient.resolved(recipientId) + assertTrue(recipient.serviceId.isPresent) + assertEquals(ACI_A, recipient.serviceId.get()) + } + + @Test(expected = IllegalArgumentException::class) + fun getAndPossiblyMerge_noArgs_invalid() { + recipientDatabase.getAndPossiblyMergePnp(null, null, true) + } + + private fun ensureDbEmpty() { + SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME} WHERE ${RecipientDatabase.DISTRIBUTION_LIST_ID} IS NULL ", null).use { cursor -> + assertTrue(cursor.moveToFirst()) + assertEquals(0, cursor.getLong(0)) + } + } + + private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional = Optional.empty()): IncomingTextMessage { + return IncomingTextMessage(sender, 1, time, time, time, body, groupId, 0, true, null) + } + + private fun mmsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional = Optional.empty()): IncomingMediaMessage { + return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.empty()) + } + + private fun identityKey(value: Byte): IdentityKey { + val bytes = ByteArray(33) + bytes[0] = 0x05 + bytes[1] = value + return IdentityKey(bytes) + } + + private fun notificationProfile(name: String): NotificationProfile { + return (notificationProfileDatabase.createProfile(name = name, emoji = "", color = AvatarColor.A210, System.currentTimeMillis()) as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile + } + + private fun groupMasterKey(value: Byte): GroupMasterKey { + val bytes = ByteArray(32) + bytes[0] = value + return GroupMasterKey(bytes) + } + + private fun decryptedGroup(members: Collection): DecryptedGroup { + return DecryptedGroup.newBuilder() + .addAllMembers(members.map { DecryptedMember.newBuilder().setUuid(UuidUtil.toByteString(it)).build() }) + .build() + } + + private fun getMention(messageId: Long): MentionModel { + SignalDatabase.rawDatabase.rawQuery("SELECT * FROM ${MentionDatabase.TABLE_NAME} WHERE ${MentionDatabase.MESSAGE_ID} = $messageId").use { cursor -> + cursor.moveToFirst() + return MentionModel( + recipientId = RecipientId.from(CursorUtil.requireLong(cursor, MentionDatabase.RECIPIENT_ID)), + threadId = CursorUtil.requireLong(cursor, MentionDatabase.THREAD_ID) + ) + } + } + + /** The normal mention model doesn't have a threadId, so we need to do it ourselves for the test */ + data class MentionModel( + val recipientId: RecipientId, + val threadId: Long + ) + + private class ChangeNumberListener { + + var numberChangeWasEnqueued = false + private set + + fun waitForJobManager() { + ApplicationDependencies.getJobManager().flush() + ThreadUtil.sleep(500) + } + + fun enqueue() { + ApplicationDependencies.getJobManager().addListener( + { job -> job.factoryKey == RecipientChangedNumberJob.KEY }, + { _, _ -> numberChangeWasEnqueued = true } + ) + } + } + + companion object { + val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e")) + val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed")) + + val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999")) + val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533")) + + const val E164_A = "+12221234567" + const val E164_B = "+13331234567" + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_processPnpTuple.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_processPnpTuple.kt index d47fde2d60..f282790d86 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_processPnpTuple.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_processPnpTuple.kt @@ -101,6 +101,17 @@ class RecipientDatabaseTest_processPnpTuple { } } + @Test + fun onlyE164Matches_differentAci() { + test { + given(E164_A, null, ACI_B) + process(E164_A, PNI_A, ACI_A) + + expect(null, null, ACI_B) + expect(E164_A, PNI_A, ACI_A) + } + } + @Test fun e164AndPniMatches() { test { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt index ff7930e613..76616c978c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -419,12 +419,17 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : return getByColumn(USERNAME, username) } - fun getAndPossiblyMerge(serviceId: ServiceId?, e164: String?): RecipientId { - return getAndPossiblyMerge(serviceId, e164, changeSelf = false) + @JvmOverloads + fun getAndPossiblyMerge(serviceId: ServiceId?, e164: String?, changeSelf: Boolean = false): RecipientId { + return if (FeatureFlags.phoneNumberPrivacy()) { + getAndPossiblyMergePnp(serviceId, e164, changeSelf) + } else { + getAndPossiblyMergeLegacy(serviceId, e164, changeSelf) + } } @VisibleForTesting - fun getAndPossiblyMerge(serviceId: ServiceId?, e164: String?, changeSelf: Boolean): RecipientId { + fun getAndPossiblyMergeLegacy(serviceId: ServiceId?, e164: String?, changeSelf: Boolean = false): RecipientId { require(!(serviceId == null && e164 == null)) { "Must provide an ACI or E164!" } val db = writableDatabase @@ -516,6 +521,21 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } } + @VisibleForTesting + fun getAndPossiblyMergePnp(serviceId: ServiceId?, e164: String?, changeSelf: Boolean = false): RecipientId { + require(!(serviceId == null && e164 == null)) { "Must provide an ACI or E164!" } + + return writableDatabase.withinTransaction { + when { + serviceId is PNI -> processPnpTuple(e164, serviceId, null, pniVerified = false) + serviceId is ACI -> processPnpTuple(e164, null, serviceId, pniVerified = false) + serviceId == null -> processPnpTuple(e164, null, null, pniVerified = false) + getByPni(PNI.from(serviceId.uuid())).isPresent -> processPnpTuple(e164, PNI.from(serviceId.uuid()), null, pniVerified = false) + else -> processPnpTuple(e164, null, ACI.fromNullable(serviceId), pniVerified = false) + } + } + } + fun getAllServiceIdProfileKeyPairs(): Map { val serviceIdToProfileKey: MutableMap = mutableMapOf() @@ -2014,10 +2034,17 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } fun setPni(id: RecipientId, pni: PNI) { - val values = ContentValues().apply { - put(PNI_COLUMN, pni.toString()) - } - writableDatabase.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(id)) + writableDatabase + .update(TABLE_NAME) + .values(SERVICE_ID to pni.toString()) + .where("$ID = ? AND ($SERVICE_ID IS NULL OR $SERVICE_ID = $PNI_COLUMN)", id) + .run() + + writableDatabase + .update(TABLE_NAME) + .values(PNI_COLUMN to pni.toString()) + .where("$ID = ?", id) + .run() } /** @@ -2170,6 +2197,12 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : return ids } + /** + * Takes a tuple of (e164, pni, aci) and incorporates it into our database. + * It is assumed that we are in a transaction. + * + * @return The [RecipientId] of the resulting recipient. + */ @VisibleForTesting fun processPnpTuple(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean): RecipientId { val changeSet = processPnpTupleToChangeSet(e164, pni, aci, pniVerified) @@ -2178,16 +2211,6 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : @VisibleForTesting fun writePnpChangeSetToDisk(changeSet: PnpChangeSet): RecipientId { - val id: RecipientId = when (changeSet.id) { - is PnpIdResolver.PnpNoopId -> { - changeSet.id.recipientId - } - is PnpIdResolver.PnpInsert -> { - val id: Long = writableDatabase.insert(TABLE_NAME, null, buildContentValuesForPnpInsert(changeSet.id.e164, changeSet.id.pni, changeSet.id.aci)) - RecipientId.from(id) - } - } - for (operation in changeSet.operations) { @Exhaustive when (operation) { @@ -2224,7 +2247,10 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : is PnpOperation.SetAci -> { writableDatabase .update(TABLE_NAME) - .values(SERVICE_ID to operation.aci.toString()) + .values( + SERVICE_ID to operation.aci.toString(), + REGISTERED to RegisteredState.REGISTERED.id + ) .where("$ID = ?", operation.recipientId) .run() } @@ -2244,7 +2270,10 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : writableDatabase .update(TABLE_NAME) - .values(PNI_COLUMN to operation.pni.toString()) + .values( + PNI_COLUMN to operation.pni.toString(), + REGISTERED to RegisteredState.REGISTERED.id + ) .where("$ID = ?", operation.recipientId) .run() } @@ -2264,7 +2293,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : .values( PHONE to (primary.e164 ?: secondary.e164), PNI_COLUMN to (primary.pni ?: secondary.pni)?.toString(), - SERVICE_ID to (primary.serviceId ?: secondary.serviceId)?.toString() + SERVICE_ID to (primary.serviceId ?: secondary.serviceId)?.toString(), + REGISTERED to RegisteredState.REGISTERED.id ) .where("$ID = ?", operation.primaryId) .run() @@ -2280,7 +2310,15 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } } - return id + return when (changeSet.id) { + is PnpIdResolver.PnpNoopId -> { + changeSet.id.recipientId + } + is PnpIdResolver.PnpInsert -> { + val id: Long = writableDatabase.insert(TABLE_NAME, null, buildContentValuesForPnpInsert(changeSet.id.e164, changeSet.id.pni, changeSet.id.aci)) + RecipientId.from(id) + } + } } /** @@ -2324,6 +2362,21 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : val operations: MutableList = mutableListOf() + // This is a special case. The ACI passed in doesn't match the common record. We can't change ACIs, so we need to make a new record. + if (aci != null && aci != record.serviceId && record.serviceId != null && !record.sidIsPni()) { + if (record.e164 == e164) { + operations += PnpOperation.RemoveE164(record.id) + operations += PnpOperation.RemovePni(record.id) + } else if (record.pni == pni) { + operations += PnpOperation.RemovePni(record.id) + } + + return PnpChangeSet( + id = PnpIdResolver.PnpInsert(e164, pni, aci), + operations = operations + ) + } + if (e164 != null && record.e164 != e164) { operations += PnpOperation.SetE164( recipientId = partialData.commonId, @@ -3328,14 +3381,19 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : private fun buildContentValuesForPnpInsert(e164: String?, pni: PNI?, aci: ACI?): ContentValues { check(e164 != null || pni != null || aci != null) { "Must provide some sort of identifier!" } - return contentValuesOf( + val values = contentValuesOf( PHONE to e164, SERVICE_ID to (aci ?: pni)?.toString(), - PNI_COLUMN to pni.toString(), - REGISTERED to RegisteredState.REGISTERED.id, + PNI_COLUMN to pni?.toString(), STORAGE_SERVICE_ID to Base64.encodeBytes(StorageSyncHelper.generateKey()), AVATAR_COLOR to AvatarColor.random().serialize() ) + + if (pni != null || aci != null) { + values.put(REGISTERED, RegisteredState.REGISTERED.id) + } + + return values } private fun getValuesForStorageContact(contact: SignalContactRecord, isInsert: Boolean): ContentValues {