From 7b0df17d9ab2c62b7c29c6d1027e4879ea45b977 Mon Sep 17 00:00:00 2001 From: Jameson Williams Date: Sat, 30 Nov 2024 20:33:57 -0600 Subject: [PATCH] Convert more tests to kotlin. Resolves #13825 --- .../CursorRecyclerViewAdapterTest.java | 79 - .../database/CursorRecyclerViewAdapterTest.kt | 61 + .../GroupsV2UpdateMessageProducerTest.java | 1534 ----------------- .../GroupsV2UpdateMessageProducerTest.kt | 1512 ++++++++++++++++ .../securesms/jobmanager/JobMigratorTest.java | 117 -- .../securesms/jobmanager/JobMigratorTest.kt | 130 ++ .../RecipientIdFollowUpJobMigrationTest.java | 63 - .../RecipientIdFollowUpJobMigrationTest.kt | 57 + .../RecipientIdJobMigrationTest.java | 286 --- .../migrations/RecipientIdJobMigrationTest.kt | 365 ++++ .../SendReadReceiptsJobMigrationTest.java | 107 -- .../SendReadReceiptsJobMigrationTest.kt | 104 ++ ...ributionSendJobRecipientMigrationTest.java | 89 - ...stributionSendJobRecipientMigrationTest.kt | 94 + .../GeographicalRestrictionsTest.java | 47 - .../payments/GeographicalRestrictionsTest.kt | 37 + .../fcm/PushChallengeRequestTest.java | 109 -- .../fcm/PushChallengeRequestTest.kt | 104 ++ .../registration/v2/PinHashKbsDataTest.java | 77 - .../registration/v2/PinHashKbsDataTest.kt | 72 + .../v2/testdata/KbsTestVector.java | 63 - .../registration/v2/testdata/KbsTestVector.kt | 62 + 22 files changed, 2598 insertions(+), 2571 deletions(-) delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapterTest.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapterTest.kt delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.kt delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/jobmanager/JobMigratorTest.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/jobmanager/JobMigratorTest.kt delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigrationTest.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigrationTest.kt delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdJobMigrationTest.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdJobMigrationTest.kt delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SendReadReceiptsJobMigrationTest.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SendReadReceiptsJobMigrationTest.kt delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SenderKeyDistributionSendJobRecipientMigrationTest.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SenderKeyDistributionSendJobRecipientMigrationTest.kt delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/payments/GeographicalRestrictionsTest.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/payments/GeographicalRestrictionsTest.kt delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/registration/fcm/PushChallengeRequestTest.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/registration/fcm/PushChallengeRequestTest.kt delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashKbsDataTest.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashKbsDataTest.kt delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/KbsTestVector.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/KbsTestVector.kt diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapterTest.java b/app/src/test/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapterTest.java deleted file mode 100644 index 309ebcb10d..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapterTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.thoughtcrime.securesms.database; - -import android.content.Context; -import android.database.Cursor; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView.ViewHolder; - -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class CursorRecyclerViewAdapterTest { - private CursorRecyclerViewAdapter adapter; - private Context context; - private Cursor cursor; - - @Before - public void setUp() { - context = mock(Context.class); - cursor = mock(Cursor.class); - when(cursor.getCount()).thenReturn(100); - when(cursor.moveToPosition(anyInt())).thenReturn(true); - - adapter = new CursorRecyclerViewAdapter(context, cursor) { - @Override - public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) { - return null; - } - - @Override - public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) { - } - }; - } - - @Test - public void testSanityCount() throws Exception { - assertEquals(adapter.getItemCount(), 100); - } - - @Test - public void testHeaderCount() throws Exception { - adapter.setHeaderView(new View(context)); - assertEquals(adapter.getItemCount(), 101); - - assertEquals(adapter.getItemViewType(0), CursorRecyclerViewAdapter.HEADER_TYPE); - assertNotEquals(adapter.getItemViewType(1), CursorRecyclerViewAdapter.HEADER_TYPE); - assertNotEquals(adapter.getItemViewType(100), CursorRecyclerViewAdapter.HEADER_TYPE); - } - - @Test - public void testFooterCount() throws Exception { - adapter.setFooterView(new View(context)); - assertEquals(adapter.getItemCount(), 101); - assertEquals(adapter.getItemViewType(100), CursorRecyclerViewAdapter.FOOTER_TYPE); - assertNotEquals(adapter.getItemViewType(0), CursorRecyclerViewAdapter.FOOTER_TYPE); - assertNotEquals(adapter.getItemViewType(99), CursorRecyclerViewAdapter.FOOTER_TYPE); - } - - @Test - public void testHeaderFooterCount() throws Exception { - adapter.setHeaderView(new View(context)); - adapter.setFooterView(new View(context)); - assertEquals(adapter.getItemCount(), 102); - assertEquals(adapter.getItemViewType(101), CursorRecyclerViewAdapter.FOOTER_TYPE); - assertEquals(adapter.getItemViewType(0), CursorRecyclerViewAdapter.HEADER_TYPE); - assertNotEquals(adapter.getItemViewType(1), CursorRecyclerViewAdapter.HEADER_TYPE); - assertNotEquals(adapter.getItemViewType(100), CursorRecyclerViewAdapter.FOOTER_TYPE); - } -} - diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapterTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapterTest.kt new file mode 100644 index 0000000000..f023cb90cb --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapterTest.kt @@ -0,0 +1,61 @@ +package org.thoughtcrime.securesms.database + +import android.content.Context +import android.database.Cursor +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test +import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter.FOOTER_TYPE +import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter.HEADER_TYPE + +class CursorRecyclerViewAdapterTest { + private val context: Context = mockk() + private val cursor: Cursor = mockk(relaxUnitFun = true) { + every { count } returns 100 + every { moveToPosition(any()) } returns true + } + private val adapter = object : CursorRecyclerViewAdapter(context, cursor) { + override fun onCreateItemViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder? = null + override fun onBindItemViewHolder(viewHolder: RecyclerView.ViewHolder?, cursor: Cursor) = Unit + } + + @Test + fun testSanityCount() { + assertEquals(100, adapter.itemCount) + } + + @Test + fun testHeaderCount() { + adapter.headerView = View(context) + assertEquals(101, adapter.itemCount) + + assertEquals(HEADER_TYPE, adapter.getItemViewType(0)) + assertNotEquals(HEADER_TYPE, adapter.getItemViewType(1)) + assertNotEquals(HEADER_TYPE, adapter.getItemViewType(100)) + } + + @Test + fun testFooterCount() { + adapter.setFooterView(View(context)) + assertEquals(101, adapter.itemCount) + assertEquals(FOOTER_TYPE, adapter.getItemViewType(100)) + assertNotEquals(FOOTER_TYPE, adapter.getItemViewType(0)) + assertNotEquals(FOOTER_TYPE, adapter.getItemViewType(99)) + } + + @Test + fun testHeaderFooterCount() { + adapter.headerView = View(context) + adapter.setFooterView(View(context)) + assertEquals(102, adapter.itemCount) + assertEquals(FOOTER_TYPE, adapter.getItemViewType(101)) + assertEquals(HEADER_TYPE, adapter.getItemViewType(0)) + assertNotEquals(HEADER_TYPE, adapter.getItemViewType(1)) + assertNotEquals(FOOTER_TYPE, adapter.getItemViewType(100)) + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java deleted file mode 100644 index 23594e3943..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java +++ /dev/null @@ -1,1534 +0,0 @@ -package org.thoughtcrime.securesms.database.model; - -import android.app.Application; -import android.text.Spannable; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.test.core.app.ApplicationProvider; - -import com.annimon.stream.Stream; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; -import org.signal.storageservice.protos.groups.AccessControl; -import org.signal.storageservice.protos.groups.local.DecryptedGroup; -import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; -import org.signal.storageservice.protos.groups.local.DecryptedMember; -import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; -import org.thoughtcrime.securesms.backup.v2.proto.GroupChangeChatUpdate; -import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.whispersystems.signalservice.api.push.ServiceId; -import org.whispersystems.signalservice.api.push.ServiceId.ACI; -import org.whispersystems.signalservice.api.push.ServiceId.PNI; -import org.whispersystems.signalservice.api.push.ServiceIds; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.ListIterator; -import java.util.UUID; -import java.util.stream.Collectors; - -import kotlin.collections.CollectionsKt; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.signal.core.util.StringUtil.isolateBidi; -import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeBy; -import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeByUnknown; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; - -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE, application = Application.class) -public final class GroupsV2UpdateMessageProducerTest { - - private ACI you; - private ACI alice; - private ACI bob; - - private ServiceIds selfIds; - - private GroupsV2UpdateMessageProducer producer; - - @Rule - public MockitoRule rule = MockitoJUnit.rule(); - - @Mock - public MockedStatic recipientMockedStatic; - - @Mock - public MockedStatic recipientIdMockedStatic; - - @Before - public void setup() { - you = ACI.from(UUID.randomUUID()); - alice = ACI.from(UUID.randomUUID()); - bob = ACI.from(UUID.randomUUID()); - - selfIds = new ServiceIds(you, PNI.from(UUID.randomUUID())); - - recipientIdMockedStatic.when(() -> RecipientId.from(anyLong())).thenCallRealMethod(); - - RecipientId aliceId = RecipientId.from(1); - RecipientId bobId = RecipientId.from(2); - - Recipient aliceRecipient = recipientWithName(aliceId, "Alice"); - Recipient bobRecipient = recipientWithName(bobId, "Bob"); - - producer = new GroupsV2UpdateMessageProducer(ApplicationProvider.getApplicationContext(), selfIds, null); - - recipientIdMockedStatic.when(() -> RecipientId.from(alice)).thenReturn(aliceId); - recipientIdMockedStatic.when(() -> RecipientId.from(bob)).thenReturn(bobId); - recipientMockedStatic.when(() -> Recipient.resolved(aliceId)).thenReturn(aliceRecipient); - recipientMockedStatic.when(() -> Recipient.resolved(bobId)).thenReturn(bobRecipient); - } - - private static Recipient recipientWithName(RecipientId id, String name) { - Recipient recipient = mock(Recipient.class); - when(recipient.getId()).thenReturn(id); - when(recipient.getDisplayName(any())).thenReturn(name); - return recipient; - } - - @Test - public void empty_change() { - DecryptedGroupChange change = changeBy(alice) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice updated the group."))); - } - - @Test - public void empty_change_by_you() { - DecryptedGroupChange change = changeBy(you) - .build(); - - assertThat(describeChange(change), is(singletonList("You updated the group."))); - } - - @Test - public void empty_change_by_unknown() { - DecryptedGroupChange change = changeByUnknown() - .build(); - - assertThat(describeChange(change), is(singletonList("The group was updated."))); - } - - // Member additions - - @Test - public void member_added_member() { - DecryptedGroupChange change = changeBy(alice) - .addMember(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice added Bob."))); - } - - @Test - public void member_added_member_mentions_both() { - DecryptedGroupChange change = changeBy(alice) - .addMember(bob) - .build(); - - assertSingleChangeMentioning(change, Arrays.asList(alice, bob)); - } - - @Test - public void you_added_member() { - DecryptedGroupChange change = changeBy(you) - .addMember(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("You added Bob."))); - } - - @Test - public void you_added_member_mentions_just_member() { - DecryptedGroupChange change = changeBy(you) - .addMember(bob) - .build(); - - assertSingleChangeMentioning(change, singletonList(bob)); - } - - @Test - public void member_added_you() { - DecryptedGroupChange change = changeBy(alice) - .addMember(you) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice added you to the group."))); - } - - @Test - public void you_added_you() { - DecryptedGroupChange change = changeBy(you) - .addMember(you) - .build(); - - assertThat(describeChange(change), is(singletonList("You joined the group via the group link."))); - } - - @Test - public void member_added_themselves() { - DecryptedGroupChange change = changeBy(bob) - .addMember(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("Bob joined the group via the group link."))); - } - - @Test - public void member_added_themselves_mentions_just_member() { - DecryptedGroupChange change = changeBy(bob) - .addMember(bob) - .build(); - - assertSingleChangeMentioning(change, singletonList(bob)); - } - - @Test - public void unknown_added_you() { - DecryptedGroupChange change = changeByUnknown() - .addMember(you) - .build(); - - assertThat(describeChange(change), is(singletonList("You joined the group."))); - } - - @Test - public void unknown_added_member() { - DecryptedGroupChange change = changeByUnknown() - .addMember(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("Bob joined the group."))); - } - - @Test - public void member_added_you_and_another_where_you_are_not_first() { - DecryptedGroupChange change = changeBy(bob) - .addMember(alice) - .addMember(you) - .build(); - - assertThat(describeChange(change), is(Arrays.asList("Bob added you to the group.", "Bob added Alice."))); - } - - @Test - public void unknown_member_added_you_and_another_where_you_are_not_first() { - DecryptedGroupChange change = changeByUnknown() - .addMember(alice) - .addMember(you) - .build(); - - assertThat(describeChange(change), is(Arrays.asList("You joined the group.", "Alice joined the group."))); - } - - @Test - public void you_added_you_and_another_where_you_are_not_first() { - DecryptedGroupChange change = changeBy(you) - .addMember(alice) - .addMember(you) - .build(); - - assertThat(describeChange(change), is(Arrays.asList("You joined the group via the group link.", "You added Alice."))); - } - - // Member removals - @Test - public void member_removed_member() { - DecryptedGroupChange change = changeBy(alice) - .deleteMember(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice removed Bob."))); - } - - @Test - public void you_removed_member() { - DecryptedGroupChange change = changeBy(you) - .deleteMember(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("You removed Bob."))); - } - - @Test - public void member_removed_you() { - DecryptedGroupChange change = changeBy(alice) - .deleteMember(you) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice removed you from the group."))); - } - - @Test - public void you_removed_you() { - DecryptedGroupChange change = changeBy(you) - .deleteMember(you) - .build(); - - assertThat(describeChange(change), is(singletonList("You left the group."))); - } - - @Test - public void member_removed_themselves() { - DecryptedGroupChange change = changeBy(bob) - .deleteMember(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("Bob left the group."))); - } - - @Test - public void unknown_removed_member() { - DecryptedGroupChange change = changeByUnknown() - .deleteMember(alice) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice is no longer in the group."))); - } - - @Test - public void unknown_removed_you() { - DecryptedGroupChange change = changeByUnknown() - .deleteMember(you) - .build(); - - assertThat(describeChange(change), is(singletonList("You are no longer in the group."))); - } - - // Member role modifications - - @Test - public void you_make_member_admin() { - DecryptedGroupChange change = changeBy(you) - .promoteToAdmin(alice) - .build(); - - assertThat(describeChange(change), is(singletonList("You made Alice an admin."))); - } - - @Test - public void member_makes_member_admin() { - DecryptedGroupChange change = changeBy(bob) - .promoteToAdmin(alice) - .build(); - - assertThat(describeChange(change), is(singletonList("Bob made Alice an admin."))); - } - - @Test - public void member_makes_you_admin() { - DecryptedGroupChange change = changeBy(alice) - .promoteToAdmin(you) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice made you an admin."))); - } - - @Test - public void you_revoked_member_admin() { - DecryptedGroupChange change = changeBy(you) - .demoteToMember(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("You revoked admin privileges from Bob."))); - } - - @Test - public void member_revokes_member_admin() { - DecryptedGroupChange change = changeBy(bob) - .demoteToMember(alice) - .build(); - - assertThat(describeChange(change), is(singletonList("Bob revoked admin privileges from Alice."))); - } - - @Test - public void member_revokes_your_admin() { - DecryptedGroupChange change = changeBy(alice) - .demoteToMember(you) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice revoked your admin privileges."))); - } - - @Test - public void unknown_makes_member_admin() { - DecryptedGroupChange change = changeByUnknown() - .promoteToAdmin(alice) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice is now an admin."))); - } - - @Test - public void unknown_makes_you_admin() { - DecryptedGroupChange change = changeByUnknown() - .promoteToAdmin(you) - .build(); - - assertThat(describeChange(change), is(singletonList("You are now an admin."))); - } - - @Test - public void unknown_revokes_member_admin() { - DecryptedGroupChange change = changeByUnknown() - .demoteToMember(alice) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice is no longer an admin."))); - } - - @Test - public void unknown_revokes_your_admin() { - DecryptedGroupChange change = changeByUnknown() - .demoteToMember(you) - .build(); - - assertThat(describeChange(change), is(singletonList("You are no longer an admin."))); - } - - // Member invitation - - @Test - public void you_invited_member() { - DecryptedGroupChange change = changeBy(you) - .invite(alice) - .build(); - - assertThat(describeChange(change), is(singletonList("You invited Alice to the group."))); - } - - @Test - public void member_invited_you() { - DecryptedGroupChange change = changeBy(alice) - .invite(you) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice invited you to the group."))); - } - - @Test - public void member_invited_1_person() { - DecryptedGroupChange change = changeBy(alice) - .invite(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice invited 1 person to the group."))); - } - - @Test - public void member_invited_2_persons() { - DecryptedGroupChange change = changeBy(alice) - .invite(bob) - .invite(ACI.from(UUID.randomUUID())) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice invited 2 people to the group."))); - } - - @Test - public void member_invited_3_persons_and_you() { - DecryptedGroupChange change = changeBy(bob) - .invite(alice) - .invite(you) - .invite(ACI.from(UUID.randomUUID())) - .invite(ACI.from(UUID.randomUUID())) - .build(); - - assertThat(describeChange(change), is(Arrays.asList("Bob invited you to the group.", "Bob invited 3 people to the group."))); - } - - @Test - public void unknown_editor_but_known_invitee_invited_you() { - DecryptedGroupChange change = changeByUnknown() - .inviteBy(you, alice) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice invited you to the group."))); - } - - @Test - public void unknown_editor_and_unknown_inviter_invited_you() { - DecryptedGroupChange change = changeByUnknown() - .invite(you) - .build(); - - assertThat(describeChange(change), is(singletonList("You were invited to the group."))); - } - - @Test - public void unknown_invited_1_person() { - DecryptedGroupChange change = changeByUnknown() - .invite(alice) - .build(); - - assertThat(describeChange(change), is(singletonList("1 person was invited to the group."))); - } - - @Test - public void unknown_invited_2_persons() { - DecryptedGroupChange change = changeByUnknown() - .invite(alice) - .invite(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("2 people were invited to the group."))); - } - - @Test - public void unknown_invited_3_persons_and_you() { - DecryptedGroupChange change = changeByUnknown() - .invite(alice) - .invite(you) - .invite(ACI.from(UUID.randomUUID())) - .invite(ACI.from(UUID.randomUUID())) - .build(); - - assertThat(describeChange(change), is(Arrays.asList("You were invited to the group.", "3 people were invited to the group."))); - } - - @Test - public void unknown_editor_invited_3_persons_and_you_inviter_known() { - DecryptedGroupChange change = changeByUnknown() - .invite(alice) - .inviteBy(you, bob) - .invite(ACI.from(UUID.randomUUID())) - .invite(ACI.from(UUID.randomUUID())) - .build(); - - assertThat(describeChange(change), is(Arrays.asList("Bob invited you to the group.", "3 people were invited to the group."))); - } - - @Test - public void member_invited_3_persons_and_you_and_added_another_where_you_were_not_first() { - DecryptedGroupChange change = changeBy(bob) - .addMember(alice) - .invite(you) - .invite(ACI.from(UUID.randomUUID())) - .invite(ACI.from(UUID.randomUUID())) - .build(); - - assertThat(describeChange(change), is(Arrays.asList("Bob invited you to the group.", "Bob added Alice.", "Bob invited 2 people to the group."))); - } - - @Test - public void unknown_editor_but_known_invitee_invited_you_and_added_another_where_you_were_not_first() { - DecryptedGroupChange change = changeByUnknown() - .addMember(bob) - .inviteBy(you, alice) - .build(); - - assertThat(describeChange(change), is(Arrays.asList("Alice invited you to the group.", "Bob joined the group."))); - } - - @Test - public void unknown_editor_and_unknown_inviter_invited_you_and_added_another_where_you_were_not_first() { - DecryptedGroupChange change = changeByUnknown() - .addMember(alice) - .invite(you) - .build(); - - assertThat(describeChange(change), is(Arrays.asList("You were invited to the group.", "Alice joined the group."))); - } - - // Member invitation revocation - - @Test - public void member_uninvited_1_person() { - DecryptedGroupChange change = changeBy(alice) - .uninvite(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice revoked an invitation to the group."))); - } - - @Test - public void member_uninvited_2_people() { - DecryptedGroupChange change = changeBy(alice) - .uninvite(bob) - .uninvite(ACI.from(UUID.randomUUID())) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice revoked 2 invitations to the group."))); - } - - @Test - public void you_uninvited_1_person() { - DecryptedGroupChange change = changeBy(you) - .uninvite(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("You revoked an invitation to the group."))); - } - - @Test - public void you_uninvited_2_people() { - DecryptedGroupChange change = changeBy(you) - .uninvite(bob) - .uninvite(ACI.from(UUID.randomUUID())) - .build(); - - assertThat(describeChange(change), is(singletonList("You revoked 2 invitations to the group."))); - } - - @Test - public void pending_member_declines_invite() { - DecryptedGroupChange change = changeBy(bob) - .uninvite(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("Someone declined an invitation to the group."))); - } - - @Test - public void you_decline_invite() { - DecryptedGroupChange change = changeBy(you) - .uninvite(you) - .build(); - - assertThat(describeChange(change), is(singletonList("You declined the invitation to the group."))); - } - - @Test - public void unknown_revokes_your_invite() { - DecryptedGroupChange change = changeByUnknown() - .uninvite(you) - .build(); - - assertThat(describeChange(change), is(singletonList("An admin revoked your invitation to the group."))); - } - - @Test - public void unknown_revokes_1_invite() { - DecryptedGroupChange change = changeByUnknown() - .uninvite(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("An invitation to the group was revoked."))); - } - - @Test - public void unknown_revokes_2_invites() { - DecryptedGroupChange change = changeByUnknown() - .uninvite(bob) - .uninvite(ACI.from(UUID.randomUUID())) - .build(); - - assertThat(describeChange(change), is(singletonList("2 invitations to the group were revoked."))); - } - - @Test - public void unknown_revokes_yours_and_three_other_invites() { - DecryptedGroupChange change = changeByUnknown() - .uninvite(bob) - .uninvite(you) - .uninvite(ACI.from(UUID.randomUUID())) - .uninvite(ACI.from(UUID.randomUUID())) - .build(); - - assertThat(describeChange(change), is(Arrays.asList("An admin revoked your invitation to the group.", "3 invitations to the group were revoked."))); - } - - @Test - public void your_invite_was_revoked_by_known_member() { - DecryptedGroupChange change = changeBy(bob) - .uninvite(you) - .build(); - - assertThat(describeChange(change), is(singletonList("Bob revoked your invitation to the group."))); - } - - // Promote pending members - - @Test - public void member_accepts_invite() { - DecryptedGroupChange change = changeBy(bob) - .promote(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("Bob accepted an invitation to the group."))); - } - - @Test - public void you_accept_invite() { - DecryptedGroupChange change = changeBy(you) - .promote(you) - .build(); - - assertThat(describeChange(change), is(singletonList("You accepted the invitation to the group."))); - } - - @Test - public void member_promotes_pending_member() { - DecryptedGroupChange change = changeBy(bob) - .promote(alice) - .build(); - - assertThat(describeChange(change), is(singletonList("Bob added invited member Alice."))); - } - - @Test - public void you_promote_pending_member() { - DecryptedGroupChange change = changeBy(you) - .promote(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("You added invited member Bob."))); - } - - @Test - public void member_promotes_you() { - DecryptedGroupChange change = changeBy(bob) - .promote(you) - .build(); - - assertThat(describeChange(change), is(singletonList("Bob added you to the group."))); - } - - @Test - public void unknown_added_by_invite() { - DecryptedGroupChange change = changeByUnknown() - .promote(you) - .build(); - - assertThat(describeChange(change), is(singletonList("You joined the group."))); - } - - @Test - public void unknown_promotes_pending_member() { - DecryptedGroupChange change = changeByUnknown() - .promote(alice) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice joined the group."))); - } - - // Title change - - @Test - public void member_changes_title() { - DecryptedGroupChange change = changeBy(alice) - .title("New title") - .build(); - - assertThat(describeChange(change), is(singletonList("Alice changed the group name to \"" + isolateBidi("New title") + "\"."))); - } - - @Test - public void you_change_title() { - DecryptedGroupChange change = changeBy(you) - .title("Title 2") - .build(); - - assertThat(describeChange(change), is(singletonList("You changed the group name to \"" + isolateBidi("Title 2") + "\"."))); - } - - @Test - public void unknown_changed_title() { - DecryptedGroupChange change = changeByUnknown() - .title("Title 3") - .build(); - - assertThat(describeChange(change), is(singletonList("The group name has changed to \"" + isolateBidi("Title 3") + "\"."))); - } - - // Avatar change - - @Test - public void member_changes_avatar() { - DecryptedGroupChange change = changeBy(alice) - .avatar("Avatar1") - .build(); - - assertThat(describeChange(change), is(singletonList("Alice changed the group avatar."))); - } - - @Test - public void you_change_avatar() { - DecryptedGroupChange change = changeBy(you) - .avatar("Avatar2") - .build(); - - assertThat(describeChange(change), is(singletonList("You changed the group avatar."))); - } - - @Test - public void unknown_changed_avatar() { - DecryptedGroupChange change = changeByUnknown() - .avatar("Avatar3") - .build(); - - assertThat(describeChange(change), is(singletonList("The group avatar has been changed."))); - } - - // Timer change - - @Test - public void member_changes_timer() { - DecryptedGroupChange change = changeBy(bob) - .timer(10) - .build(); - - assertThat(describeChange(change), is(singletonList("Bob set the disappearing message timer to 10 seconds."))); - } - - @Test - public void you_change_timer() { - DecryptedGroupChange change = changeBy(you) - .timer(60) - .build(); - - assertThat(describeChange(change), is(singletonList("You set the disappearing message timer to 1 minute."))); - } - - @Test - public void unknown_change_timer() { - DecryptedGroupChange change = changeByUnknown() - .timer(120) - .build(); - - assertThat(describeChange(change), is(singletonList("The disappearing message timer has been set to 2 minutes."))); - } - - @Test - public void unknown_change_timer_mentions_no_one() { - DecryptedGroupChange change = changeByUnknown() - .timer(120) - .build(); - - assertSingleChangeMentioning(change, emptyList()); - } - - // Attribute access change - - @Test - public void member_changes_attribute_access() { - DecryptedGroupChange change = changeBy(bob) - .attributeAccess(AccessControl.AccessRequired.MEMBER) - .build(); - - assertThat(describeChange(change), is(singletonList("Bob changed who can edit group info to \"All members\"."))); - } - - @Test - public void you_changed_attribute_access() { - DecryptedGroupChange change = changeBy(you) - .attributeAccess(AccessControl.AccessRequired.ADMINISTRATOR) - .build(); - - assertThat(describeChange(change), is(singletonList("You changed who can edit group info to \"Only admins\"."))); - } - - @Test - public void unknown_changed_attribute_access() { - DecryptedGroupChange change = changeByUnknown() - .attributeAccess(AccessControl.AccessRequired.ADMINISTRATOR) - .build(); - - assertThat(describeChange(change), is(singletonList("Who can edit group info has been changed to \"Only admins\"."))); - } - - // Membership access change - - @Test - public void member_changes_membership_access() { - DecryptedGroupChange change = changeBy(alice) - .membershipAccess(AccessControl.AccessRequired.ADMINISTRATOR) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice changed who can edit group membership to \"Only admins\"."))); - } - - @Test - public void you_changed_membership_access() { - DecryptedGroupChange change = changeBy(you) - .membershipAccess(AccessControl.AccessRequired.MEMBER) - .build(); - - assertThat(describeChange(change), is(singletonList("You changed who can edit group membership to \"All members\"."))); - } - - @Test - public void unknown_changed_membership_access() { - DecryptedGroupChange change = changeByUnknown() - .membershipAccess(AccessControl.AccessRequired.ADMINISTRATOR) - .build(); - - assertThat(describeChange(change), is(singletonList("Who can edit group membership has been changed to \"Only admins\"."))); - } - - // Group link access change - - @Test - public void you_changed_group_link_access_to_any() { - DecryptedGroupChange change = changeBy(you) - .inviteLinkAccess(AccessControl.AccessRequired.ANY) - .build(); - - assertThat(describeChange(change), is(singletonList("You turned on the group link with admin approval off."))); - } - - @Test - public void you_changed_group_link_access_to_administrator_approval() { - DecryptedGroupChange change = changeBy(you) - .inviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR) - .build(); - - assertThat(describeChange(change), is(singletonList("You turned on the group link with admin approval on."))); - } - - @Test - public void you_turned_off_group_link_access() { - DecryptedGroupChange change = changeBy(you) - .inviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE) - .build(); - - assertThat(describeChange(change), is(singletonList("You turned off the group link."))); - } - - @Test - public void member_changed_group_link_access_to_any() { - DecryptedGroupChange change = changeBy(alice) - .inviteLinkAccess(AccessControl.AccessRequired.ANY) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice turned on the group link with admin approval off."))); - } - - @Test - public void member_changed_group_link_access_to_administrator_approval() { - DecryptedGroupChange change = changeBy(bob) - .inviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR) - .build(); - - assertThat(describeChange(change), is(singletonList("Bob turned on the group link with admin approval on."))); - } - - @Test - public void member_turned_off_group_link_access() { - DecryptedGroupChange change = changeBy(alice) - .inviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice turned off the group link."))); - } - - @Test - public void unknown_changed_group_link_access_to_any() { - DecryptedGroupChange change = changeByUnknown() - .inviteLinkAccess(AccessControl.AccessRequired.ANY) - .build(); - - assertThat(describeChange(change), is(singletonList("The group link has been turned on with admin approval off."))); - } - - @Test - public void unknown_changed_group_link_access_to_administrator_approval() { - DecryptedGroupChange change = changeByUnknown() - .inviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR) - .build(); - - assertThat(describeChange(change), is(singletonList("The group link has been turned on with admin approval on."))); - } - - @Test - public void unknown_turned_off_group_link_access() { - DecryptedGroupChange change = changeByUnknown() - .inviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE) - .build(); - - assertThat(describeChange(change), is(singletonList("The group link has been turned off."))); - } - - // Group link with known previous group state - - @Test - public void group_link_access_from_unknown_to_administrator() { - assertEquals("You turned on the group link with admin approval on.", describeGroupLinkChange(you, AccessControl.AccessRequired.UNKNOWN, AccessControl.AccessRequired.ADMINISTRATOR)); - assertEquals("Alice turned on the group link with admin approval on.", describeGroupLinkChange(alice, AccessControl.AccessRequired.UNKNOWN, AccessControl.AccessRequired.ADMINISTRATOR)); - assertEquals("The group link has been turned on with admin approval on.", describeGroupLinkChange(null, AccessControl.AccessRequired.UNKNOWN, AccessControl.AccessRequired.ADMINISTRATOR)); - } - - @Test - public void group_link_access_from_administrator_to_unsatisfiable() { - assertEquals("You turned off the group link.", describeGroupLinkChange(you, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.UNSATISFIABLE)); - assertEquals("Bob turned off the group link.", describeGroupLinkChange(bob, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.UNSATISFIABLE)); - assertEquals("The group link has been turned off.", describeGroupLinkChange(null, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.UNSATISFIABLE)); - } - - @Test - public void group_link_access_from_unsatisfiable_to_administrator() { - assertEquals("You turned on the group link with admin approval on.", describeGroupLinkChange(you, AccessControl.AccessRequired.UNSATISFIABLE, AccessControl.AccessRequired.ADMINISTRATOR)); - assertEquals("Alice turned on the group link with admin approval on.", describeGroupLinkChange(alice, AccessControl.AccessRequired.UNSATISFIABLE, AccessControl.AccessRequired.ADMINISTRATOR)); - assertEquals("The group link has been turned on with admin approval on.", describeGroupLinkChange(null, AccessControl.AccessRequired.UNSATISFIABLE, AccessControl.AccessRequired.ADMINISTRATOR)); - } - - @Test - public void group_link_access_from_administrator_to_any() { - assertEquals("You turned off admin approval for the group link.", describeGroupLinkChange(you, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.ANY)); - assertEquals("Bob turned off admin approval for the group link.", describeGroupLinkChange(bob, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.ANY)); - assertEquals("The admin approval for the group link has been turned off.", describeGroupLinkChange(null, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.ANY)); - } - - @Test - public void group_link_access_from_any_to_administrator() { - assertEquals("You turned on admin approval for the group link.", describeGroupLinkChange(you, AccessControl.AccessRequired.ANY, AccessControl.AccessRequired.ADMINISTRATOR)); - assertEquals("Bob turned on admin approval for the group link.", describeGroupLinkChange(bob, AccessControl.AccessRequired.ANY, AccessControl.AccessRequired.ADMINISTRATOR)); - assertEquals("The admin approval for the group link has been turned on.", describeGroupLinkChange(null, AccessControl.AccessRequired.ANY, AccessControl.AccessRequired.ADMINISTRATOR)); - } - - private String describeGroupLinkChange(@Nullable ACI editor, @NonNull AccessControl.AccessRequired fromAccess, AccessControl.AccessRequired toAccess){ - DecryptedGroup previousGroupState = new DecryptedGroup.Builder() - .accessControl(new AccessControl.Builder() - .addFromInviteLink(fromAccess) - .build()) - .build(); - DecryptedGroupChange change = (editor != null ? changeBy(editor) : changeByUnknown()).inviteLinkAccess(toAccess) - .build(); - - List strings = describeChange(previousGroupState, change); - assertEquals(1, strings.size()); - return strings.get(0); - } - - // Group link reset - - @Test - public void you_reset_group_link() { - DecryptedGroupChange change = changeBy(you) - .resetGroupLink() - .build(); - - assertThat(describeChange(change), is(singletonList("You reset the group link."))); - } - - @Test - public void member_reset_group_link() { - DecryptedGroupChange change = changeBy(alice) - .resetGroupLink() - .build(); - - assertThat(describeChange(change), is(singletonList("Alice reset the group link."))); - } - - @Test - public void unknown_reset_group_link() { - DecryptedGroupChange change = changeByUnknown() - .resetGroupLink() - .build(); - - assertThat(describeChange(change), is(singletonList("The group link has been reset."))); - } - - /** - * When the group link is turned on and reset in the same change, assume this is the first time - * the link password it being set and do not show reset message. - */ - @Test - public void member_changed_group_link_access_to_on_and_reset() { - DecryptedGroupChange change = changeBy(alice) - .inviteLinkAccess(AccessControl.AccessRequired.ANY) - .resetGroupLink() - .build(); - - assertThat(describeChange(change), is(singletonList("Alice turned on the group link with admin approval off."))); - } - - /** - * When the group link is turned on and reset in the same change, assume this is the first time - * the link password it being set and do not show reset message. - */ - @Test - public void you_changed_group_link_access_to_on_and_reset() { - DecryptedGroupChange change = changeBy(you) - .inviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR) - .resetGroupLink() - .build(); - - assertThat(describeChange(change), is(singletonList("You turned on the group link with admin approval on."))); - } - - @Test - public void you_changed_group_link_access_to_off_and_reset() { - DecryptedGroupChange change = changeBy(you) - .inviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE) - .resetGroupLink() - .build(); - - assertThat(describeChange(change), is(Arrays.asList("You turned off the group link.", "You reset the group link."))); - } - - // Group link request - - @Test - public void you_requested_to_join_the_group() { - DecryptedGroupChange change = changeBy(you) - .requestJoin() - .build(); - - assertThat(describeChange(change), is(singletonList("You sent a request to join the group."))); - } - - @Test - public void member_requested_to_join_the_group() { - DecryptedGroupChange change = changeBy(bob) - .requestJoin() - .build(); - - assertThat(describeChange(change), is(singletonList("Bob requested to join via the group link."))); - } - - @Test - public void unknown_requested_to_join_the_group() { - DecryptedGroupChange change = changeByUnknown() - .requestJoin(alice) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice requested to join via the group link."))); - } - - @Test - public void member_approved_your_join_request() { - DecryptedGroupChange change = changeBy(bob) - .approveRequest(you) - .build(); - - assertThat(describeChange(change), is(singletonList("Bob approved your request to join the group."))); - } - - @Test - public void member_approved_another_join_request() { - DecryptedGroupChange change = changeBy(alice) - .approveRequest(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice approved a request to join the group from Bob."))); - } - - @Test - public void you_approved_another_join_request() { - DecryptedGroupChange change = changeBy(you) - .approveRequest(alice) - .build(); - - assertThat(describeChange(change), is(singletonList("You approved a request to join the group from Alice."))); - } - - @Test - public void unknown_approved_your_join_request() { - DecryptedGroupChange change = changeByUnknown() - .approveRequest(you) - .build(); - - assertThat(describeChange(change), is(singletonList("Your request to join the group has been approved."))); - } - - @Test - public void unknown_approved_another_join_request() { - DecryptedGroupChange change = changeByUnknown() - .approveRequest(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("A request to join the group from Bob has been approved."))); - } - - @Test - public void member_denied_another_join_request() { - DecryptedGroupChange change = changeBy(alice) - .denyRequest(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice denied a request to join the group from Bob."))); - } - - @Test - public void member_denied_your_join_request() { - DecryptedGroupChange change = changeBy(alice) - .denyRequest(you) - .build(); - - assertThat(describeChange(change), is(singletonList("Your request to join the group has been denied by an admin."))); - } - - @Test - public void you_cancelled_your_join_request() { - DecryptedGroupChange change = changeBy(you) - .denyRequest(you) - .build(); - - assertThat(describeChange(change), is(singletonList("You canceled your request to join the group."))); - } - - @Test - public void member_cancelled_their_join_request() { - DecryptedGroupChange change = changeBy(alice) - .denyRequest(alice) - .build(); - - assertThat(describeChange(change), is(singletonList("Alice canceled their request to join the group."))); - } - - @Test - public void unknown_denied_your_join_request() { - DecryptedGroupChange change = changeByUnknown() - .denyRequest(you) - .build(); - - assertThat(describeChange(change), is(singletonList("Your request to join the group has been denied by an admin."))); - } - - @Test - public void unknown_denied_another_join_request() { - DecryptedGroupChange change = changeByUnknown() - .denyRequest(bob) - .build(); - - assertThat(describeChange(change), is(singletonList("A request to join the group from Bob has been denied."))); - } - - // Multiple changes - - @Test - public void multiple_changes() { - DecryptedGroupChange change = changeBy(alice) - .addMember(bob) - .membershipAccess(AccessControl.AccessRequired.MEMBER) - .title("Title") - .addMember(you) - .timer(300) - .build(); - - assertThat(describeChange(change), is(Arrays.asList( - "Alice added you to the group.", - "Alice added Bob.", - "Alice changed the group name to \"" + isolateBidi("Title") + "\".", - "Alice set the disappearing message timer to 5 minutes.", - "Alice changed who can edit group membership to \"All members\"."))); - } - - @Test - public void multiple_changes_leave_and_promote() { - DecryptedGroupChange change = changeBy(alice) - .deleteMember(alice) - .promoteToAdmin(bob) - .build(); - - assertThat(describeChange(change), is(Arrays.asList( - "Alice made Bob an admin.", - "Alice left the group."))); - } - - @Test - public void multiple_changes_leave_and_promote_by_unknown() { - DecryptedGroupChange change = changeByUnknown() - .deleteMember(alice) - .promoteToAdmin(bob) - .build(); - - assertThat(describeChange(change), is(Arrays.asList( - "Bob is now an admin.", - "Alice is no longer in the group."))); - } - - @Test - public void multiple_changes_by_unknown() { - DecryptedGroupChange change = changeByUnknown() - .addMember(bob) - .membershipAccess(AccessControl.AccessRequired.MEMBER) - .title("Title 2") - .avatar("Avatar 1") - .timer(600) - .build(); - - assertThat(describeChange(change), is(Arrays.asList( - "Bob joined the group.", - "The group name has changed to \"" + isolateBidi("Title 2") + "\".", - "The group avatar has been changed.", - "The disappearing message timer has been set to 10 minutes.", - "Who can edit group membership has been changed to \"All members\"."))); - } - - @Test - public void multiple_changes_join_and_leave_by_unknown() { - DecryptedGroupChange change = changeByUnknown() - .addMember(alice) - .promoteToAdmin(alice) - .deleteMember(alice) - .title("Updated title") - .build(); - - assertThat(describeChange(change), is(Arrays.asList( - "Alice joined the group.", - "Alice is now an admin.", - "The group name has changed to \"" + isolateBidi("Updated title") + "\".", - "Alice is no longer in the group."))); - } - - // Group state without a change record - - @Test - public void you_created_a_group_change_not_found() { - DecryptedGroup group = newGroupBy(you, 0) - .build(); - - assertThat(describeNewGroup(group), is("You joined the group.")); - } - - @Test - public void you_created_a_group() { - DecryptedGroup group = newGroupBy(you, 0) - .build(); - - DecryptedGroupChange change = changeBy(you) - .addMember(alice) - .addMember(you) - .addMember(bob) - .title("New title") - .build(); - - assertThat(describeNewGroup(group, change), is("You created the group.")); - } - - @Test - public void alice_created_a_group_change_not_found() { - DecryptedGroup group = newGroupBy(alice, 0) - .member(you) - .build(); - - assertThat(describeNewGroup(group), is("You joined the group.")); - } - - @Test - public void alice_created_a_group() { - DecryptedGroup group = newGroupBy(alice, 0) - .member(you) - .build(); - - DecryptedGroupChange change = changeBy(alice) - .addMember(you) - .addMember(alice) - .addMember(bob) - .title("New title") - .build(); - - assertThat(describeNewGroup(group, change), is("Alice added you to the group.")); - } - - @Test - public void alice_created_a_group_above_zero() { - DecryptedGroup group = newGroupBy(alice, 1) - .member(you) - .build(); - - assertThat(describeNewGroup(group), is("You joined the group.")); - } - - @Test - public void you_were_invited_to_a_group() { - DecryptedGroup group = newGroupBy(alice, 0) - .invite(bob, you) - .build(); - - assertThat(describeNewGroup(group), is("Bob invited you to the group.")); - } - - @Test - public void describe_a_group_you_are_not_in() { - DecryptedGroup group = newGroupBy(alice, 1) - .build(); - - assertThat(describeNewGroup(group), is("Group updated.")); - } - - @Test - public void makeRecipientsClickable_onePlaceholder() { - RecipientId id = RecipientId.from(1); - - Spannable result = GroupsV2UpdateMessageProducer.makeRecipientsClickable( - ApplicationProvider.getApplicationContext(), - GroupsV2UpdateMessageProducer.makePlaceholder(id), - Collections.singletonList(id), - null - ); - - assertEquals("Alice", result.toString()); - } - - @Test - public void makeRecipientsClickable_twoPlaceholders_sameRecipient() { - RecipientId id = RecipientId.from(1); - String placeholder = GroupsV2UpdateMessageProducer.makePlaceholder(id); - - Spannable result = GroupsV2UpdateMessageProducer.makeRecipientsClickable( - ApplicationProvider.getApplicationContext(), - placeholder + " " + placeholder, - Collections.singletonList(id), - null - ); - - assertEquals("Alice Alice", result.toString()); - } - - @Test - public void makeRecipientsClickable_twoPlaceholders_differentRecipient() { - RecipientId id1 = RecipientId.from(1); - RecipientId id2 = RecipientId.from(2); - - String placeholder1 = GroupsV2UpdateMessageProducer.makePlaceholder(id1); - String placeholder2 = GroupsV2UpdateMessageProducer.makePlaceholder(id2); - - Spannable result = GroupsV2UpdateMessageProducer.makeRecipientsClickable( - ApplicationProvider.getApplicationContext(), - placeholder1 + " " + placeholder2, - Arrays.asList(id1, id2), - null - ); - - assertEquals("Alice Bob", result.toString()); - } - - @Test - public void makeRecipientsClickable_complicated() { - RecipientId id1 = RecipientId.from(1); - RecipientId id2 = RecipientId.from(2); - - String placeholder1 = GroupsV2UpdateMessageProducer.makePlaceholder(id1); - String placeholder2 = GroupsV2UpdateMessageProducer.makePlaceholder(id2); - - Spannable result = GroupsV2UpdateMessageProducer.makeRecipientsClickable( - ApplicationProvider.getApplicationContext(), - placeholder1 + " said hello to " + placeholder2 + ", and " + placeholder2 + " said hello back to " + placeholder1 + ".", - Arrays.asList(id1, id2), - null - ); - - assertEquals("Alice said hello to Bob, and Bob said hello back to Alice.", result.toString()); - } - - private @NonNull String describeConvertedNewGroup(@NonNull DecryptedGroup groupState, @NonNull DecryptedGroupChange groupChange) { - GroupChangeChatUpdate update = GroupsV2UpdateMessageConverter.translateDecryptedChangeNewGroup(selfIds, new DecryptedGroupV2Context.Builder() - .change(groupChange) - .groupState(groupState) - .build()); - - return producer.describeChanges(update.updates).get(0).getSpannable().toString(); - } - - private @NonNull List describeConvertedChange(@Nullable DecryptedGroup previousGroupState, @NonNull DecryptedGroupChange change) { - GroupChangeChatUpdate update = GroupsV2UpdateMessageConverter.translateDecryptedChangeUpdate(selfIds, new DecryptedGroupV2Context.Builder() - .change(change) - .previousGroupState(previousGroupState) - .build()); - - return Stream.of(producer.describeChanges(update.updates)) - .map(UpdateDescription::getSpannable) - .map(Spannable::toString) - .toList(); - } - - private @NonNull List describeChange(@NonNull DecryptedGroupChange change) { - return describeChange(null, change); - } - - private @NonNull List describeChange(@Nullable DecryptedGroup previousGroupState, - @NonNull DecryptedGroupChange change) - { - List convertedChange = describeConvertedChange(previousGroupState, change); - List describedChange = Stream.of(producer.describeChanges(previousGroupState, change)) - .map(UpdateDescription::getSpannable) - .map(Spannable::toString) - .toList(); - assertEquals(describedChange.size(), convertedChange.size()); - - ListIterator convertedIterator = convertedChange.listIterator(); - ListIterator describedIterator = describedChange.listIterator(); - - while (convertedIterator.hasNext()) { - assertEquals(describedIterator.next(), convertedIterator.next()); - } - return describedChange; - } - - private @NonNull String describeNewGroup(@NonNull DecryptedGroup group) { - return describeNewGroup(group, new DecryptedGroupChange()); - } - - private @NonNull String describeNewGroup(@NonNull DecryptedGroup group, @NonNull DecryptedGroupChange groupChange) { - String newGroupString = producer.describeNewGroup(group, groupChange).getSpannable().toString(); - String convertedGroupString = describeConvertedNewGroup(group, groupChange); - - assertEquals(newGroupString, convertedGroupString); - - return newGroupString; - } - - private static GroupStateBuilder newGroupBy(ACI foundingMember, int revision) { - return new GroupStateBuilder(foundingMember, revision); - } - - private void assertSingleChangeMentioning(DecryptedGroupChange change, List expectedMentions) { - List expectedMentionSids = expectedMentions.stream().collect(Collectors.toList()); - - List changes = producer.describeChanges(null, change); - - assertThat(changes.size(), is(1)); - - UpdateDescription description = changes.get(0); - assertThat(description.getMentioned(), is(expectedMentionSids)); - - if (expectedMentions.isEmpty()) { - assertTrue(description.isStringStatic()); - } else { - assertFalse(description.isStringStatic()); - } - } - - private static class GroupStateBuilder { - - private final DecryptedGroup.Builder builder; - - GroupStateBuilder(@NonNull ACI foundingMember, int revision) { - builder = new DecryptedGroup.Builder() - .revision(revision) - .members(Collections.singletonList(new DecryptedMember.Builder().aciBytes(foundingMember.toByteString()).build())); - } - - GroupStateBuilder invite(@NonNull ACI inviter, @NonNull ServiceId invitee) { - builder.pendingMembers(CollectionsKt.plus(builder.pendingMembers, new DecryptedPendingMember.Builder().serviceIdBytes(invitee.toByteString()).addedByAci(inviter.toByteString()).build())); - return this; - } - - GroupStateBuilder member(@NonNull ACI member) { - builder.members(CollectionsKt.plus(builder.members, new DecryptedMember.Builder().aciBytes(member.toByteString()).build())); - return this; - } - - public DecryptedGroup build() { - return builder.build(); - } - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.kt new file mode 100644 index 0000000000..0893188785 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.kt @@ -0,0 +1,1512 @@ +package org.thoughtcrime.securesms.database.model + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.mockkStatic +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.util.StringUtil +import org.signal.storageservice.protos.groups.AccessControl +import org.signal.storageservice.protos.groups.AccessControl.AccessRequired +import org.signal.storageservice.protos.groups.AccessControl.AccessRequired.ADMINISTRATOR +import org.signal.storageservice.protos.groups.AccessControl.AccessRequired.ANY +import org.signal.storageservice.protos.groups.AccessControl.AccessRequired.MEMBER +import org.signal.storageservice.protos.groups.AccessControl.AccessRequired.UNKNOWN +import org.signal.storageservice.protos.groups.AccessControl.AccessRequired.UNSATISFIABLE +import org.signal.storageservice.protos.groups.local.DecryptedGroup +import org.signal.storageservice.protos.groups.local.DecryptedGroupChange +import org.signal.storageservice.protos.groups.local.DecryptedMember +import org.signal.storageservice.protos.groups.local.DecryptedPendingMember +import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter.translateDecryptedChangeNewGroup +import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter.translateDecryptedChangeUpdate +import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context +import org.thoughtcrime.securesms.groups.v2.ChangeBuilder +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.Recipient.Companion.resolved +import org.thoughtcrime.securesms.recipients.RecipientId +import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.api.push.ServiceId.ACI +import org.whispersystems.signalservice.api.push.ServiceId.PNI +import org.whispersystems.signalservice.api.push.ServiceIds +import java.util.UUID + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) +class GroupsV2UpdateMessageProducerTest { + private val you = ACI.from(UUID.randomUUID()) + private val alice = ACI.from(UUID.randomUUID()) + private val bob = ACI.from(UUID.randomUUID()) + private val selfIds = ServiceIds(you, PNI.from(UUID.randomUUID())) + private val producer = GroupsV2UpdateMessageProducer(ApplicationProvider.getApplicationContext(), selfIds, null) + + @Before + fun setup() { + mockkStatic(RecipientId::class) + val aliceId = RecipientId.from(1) + val bobId = RecipientId.from(2) + every { RecipientId.from(alice) } returns aliceId + every { RecipientId.from(bob) } returns bobId + + mockkObject(Recipient.Companion) + val aliceRecipient = recipientWithName(aliceId, "Alice") + val bobRecipient = recipientWithName(bobId, "Bob") + every { resolved(aliceId) } returns aliceRecipient + every { resolved(bobId) } returns bobRecipient + } + + @Test + fun empty_change() { + val change = ChangeBuilder.changeBy(alice) + .build() + + assertEquals(listOf("Alice updated the group."), describeChange(change)) + } + + @Test + fun empty_change_by_you() { + val change = ChangeBuilder.changeBy(you) + .build() + + assertEquals(listOf("You updated the group."), describeChange(change)) + } + + @Test + fun empty_change_by_unknown() { + val change = ChangeBuilder.changeByUnknown() + .build() + + assertEquals(listOf("The group was updated."), describeChange(change)) + } + + // Member additions + @Test + fun member_added_member() { + val change = ChangeBuilder.changeBy(alice) + .addMember(bob) + .build() + + assertEquals(listOf("Alice added Bob."), describeChange(change)) + } + + @Test + fun member_added_member_mentions_both() { + val change = ChangeBuilder.changeBy(alice) + .addMember(bob) + .build() + + assertSingleChangeMentioning(change, listOf(alice, bob)) + } + + @Test + fun you_added_member() { + val change = ChangeBuilder.changeBy(you) + .addMember(bob) + .build() + + assertEquals(listOf("You added Bob."), describeChange(change)) + } + + @Test + fun you_added_member_mentions_just_member() { + val change = ChangeBuilder.changeBy(you) + .addMember(bob) + .build() + + assertSingleChangeMentioning(change, listOf(bob)) + } + + @Test + fun member_added_you() { + val change = ChangeBuilder.changeBy(alice) + .addMember(you) + .build() + + assertEquals(listOf("Alice added you to the group."), describeChange(change)) + } + + @Test + fun you_added_you() { + val change = ChangeBuilder.changeBy(you) + .addMember(you) + .build() + + assertEquals(listOf("You joined the group via the group link."), describeChange(change)) + } + + @Test + fun member_added_themselves() { + val change = ChangeBuilder.changeBy(bob) + .addMember(bob) + .build() + + assertEquals(listOf("Bob joined the group via the group link."), describeChange(change)) + } + + @Test + fun member_added_themselves_mentions_just_member() { + val change = ChangeBuilder.changeBy(bob) + .addMember(bob) + .build() + + assertSingleChangeMentioning(change, listOf(bob)) + } + + @Test + fun unknown_added_you() { + val change = ChangeBuilder.changeByUnknown() + .addMember(you) + .build() + + assertEquals(listOf("You joined the group."), describeChange(change)) + } + + @Test + fun unknown_added_member() { + val change = ChangeBuilder.changeByUnknown() + .addMember(bob) + .build() + + assertEquals(listOf("Bob joined the group."), describeChange(change)) + } + + @Test + fun member_added_you_and_another_where_you_are_not_first() { + val change = ChangeBuilder.changeBy(bob) + .addMember(alice) + .addMember(you) + .build() + + assertEquals(listOf("Bob added you to the group.", "Bob added Alice."), describeChange(change)) + } + + @Test + fun unknown_member_added_you_and_another_where_you_are_not_first() { + val change = ChangeBuilder.changeByUnknown() + .addMember(alice) + .addMember(you) + .build() + + assertEquals(listOf("You joined the group.", "Alice joined the group."), describeChange(change)) + } + + @Test + fun you_added_you_and_another_where_you_are_not_first() { + val change = ChangeBuilder.changeBy(you) + .addMember(alice) + .addMember(you) + .build() + + assertEquals(listOf("You joined the group via the group link.", "You added Alice."), describeChange(change)) + } + + // Member removals + @Test + fun member_removed_member() { + val change = ChangeBuilder.changeBy(alice) + .deleteMember(bob) + .build() + + assertEquals(listOf("Alice removed Bob."), describeChange(change)) + } + + @Test + fun you_removed_member() { + val change = ChangeBuilder.changeBy(you) + .deleteMember(bob) + .build() + + assertEquals(listOf("You removed Bob."), describeChange(change)) + } + + @Test + fun member_removed_you() { + val change = ChangeBuilder.changeBy(alice) + .deleteMember(you) + .build() + + assertEquals(listOf("Alice removed you from the group."), describeChange(change)) + } + + @Test + fun you_removed_you() { + val change = ChangeBuilder.changeBy(you) + .deleteMember(you) + .build() + + assertEquals(listOf("You left the group."), describeChange(change)) + } + + @Test + fun member_removed_themselves() { + val change = ChangeBuilder.changeBy(bob) + .deleteMember(bob) + .build() + + assertEquals(listOf("Bob left the group."), describeChange(change)) + } + + @Test + fun unknown_removed_member() { + val change = ChangeBuilder.changeByUnknown() + .deleteMember(alice) + .build() + + assertEquals(listOf("Alice is no longer in the group."), describeChange(change)) + } + + @Test + fun unknown_removed_you() { + val change = ChangeBuilder.changeByUnknown() + .deleteMember(you) + .build() + + assertEquals(listOf("You are no longer in the group."), describeChange(change)) + } + + // Member role modifications + @Test + fun you_make_member_admin() { + val change = ChangeBuilder.changeBy(you) + .promoteToAdmin(alice) + .build() + + assertEquals(listOf("You made Alice an admin."), describeChange(change)) + } + + @Test + fun member_makes_member_admin() { + val change = ChangeBuilder.changeBy(bob) + .promoteToAdmin(alice) + .build() + + assertEquals(listOf("Bob made Alice an admin."), describeChange(change)) + } + + @Test + fun member_makes_you_admin() { + val change = ChangeBuilder.changeBy(alice) + .promoteToAdmin(you) + .build() + + assertEquals(listOf("Alice made you an admin."), describeChange(change)) + } + + @Test + fun you_revoked_member_admin() { + val change = ChangeBuilder.changeBy(you) + .demoteToMember(bob) + .build() + + assertEquals(listOf("You revoked admin privileges from Bob."), describeChange(change)) + } + + @Test + fun member_revokes_member_admin() { + val change = ChangeBuilder.changeBy(bob) + .demoteToMember(alice) + .build() + + assertEquals(listOf("Bob revoked admin privileges from Alice."), describeChange(change)) + } + + @Test + fun member_revokes_your_admin() { + val change = ChangeBuilder.changeBy(alice) + .demoteToMember(you) + .build() + + assertEquals(listOf("Alice revoked your admin privileges."), describeChange(change)) + } + + @Test + fun unknown_makes_member_admin() { + val change = ChangeBuilder.changeByUnknown() + .promoteToAdmin(alice) + .build() + + assertEquals(listOf("Alice is now an admin."), describeChange(change)) + } + + @Test + fun unknown_makes_you_admin() { + val change = ChangeBuilder.changeByUnknown() + .promoteToAdmin(you) + .build() + + assertEquals(listOf("You are now an admin."), describeChange(change)) + } + + @Test + fun unknown_revokes_member_admin() { + val change = ChangeBuilder.changeByUnknown() + .demoteToMember(alice) + .build() + + assertEquals(listOf("Alice is no longer an admin."), describeChange(change)) + } + + @Test + fun unknown_revokes_your_admin() { + val change = ChangeBuilder.changeByUnknown() + .demoteToMember(you) + .build() + + assertEquals(listOf("You are no longer an admin."), describeChange(change)) + } + + // Member invitation + @Test + fun you_invited_member() { + val change = ChangeBuilder.changeBy(you) + .invite(alice) + .build() + + assertEquals(listOf("You invited Alice to the group."), describeChange(change)) + } + + @Test + fun member_invited_you() { + val change = ChangeBuilder.changeBy(alice) + .invite(you) + .build() + + assertEquals(listOf("Alice invited you to the group."), describeChange(change)) + } + + @Test + fun member_invited_1_person() { + val change = ChangeBuilder.changeBy(alice) + .invite(bob) + .build() + + assertEquals(listOf("Alice invited 1 person to the group."), describeChange(change)) + } + + @Test + fun member_invited_2_persons() { + val change = ChangeBuilder.changeBy(alice) + .invite(bob) + .invite(ACI.from(UUID.randomUUID())) + .build() + + assertEquals(listOf("Alice invited 2 people to the group."), describeChange(change)) + } + + @Test + fun member_invited_3_persons_and_you() { + val change = ChangeBuilder.changeBy(bob) + .invite(alice) + .invite(you) + .invite(ACI.from(UUID.randomUUID())) + .invite(ACI.from(UUID.randomUUID())) + .build() + + assertEquals(listOf("Bob invited you to the group.", "Bob invited 3 people to the group."), describeChange(change)) + } + + @Test + fun unknown_editor_but_known_invitee_invited_you() { + val change = ChangeBuilder.changeByUnknown() + .inviteBy(you, alice) + .build() + + assertEquals(listOf("Alice invited you to the group."), describeChange(change)) + } + + @Test + fun unknown_editor_and_unknown_inviter_invited_you() { + val change = ChangeBuilder.changeByUnknown() + .invite(you) + .build() + + assertEquals(listOf("You were invited to the group."), describeChange(change)) + } + + @Test + fun unknown_invited_1_person() { + val change = ChangeBuilder.changeByUnknown() + .invite(alice) + .build() + + assertEquals(listOf("1 person was invited to the group."), describeChange(change)) + } + + @Test + fun unknown_invited_2_persons() { + val change = ChangeBuilder.changeByUnknown() + .invite(alice) + .invite(bob) + .build() + + assertEquals(listOf("2 people were invited to the group."), describeChange(change)) + } + + @Test + fun unknown_invited_3_persons_and_you() { + val change = ChangeBuilder.changeByUnknown() + .invite(alice) + .invite(you) + .invite(ACI.from(UUID.randomUUID())) + .invite(ACI.from(UUID.randomUUID())) + .build() + + assertEquals(listOf("You were invited to the group.", "3 people were invited to the group."), describeChange(change)) + } + + @Test + fun unknown_editor_invited_3_persons_and_you_inviter_known() { + val change = ChangeBuilder.changeByUnknown() + .invite(alice) + .inviteBy(you, bob) + .invite(ACI.from(UUID.randomUUID())) + .invite(ACI.from(UUID.randomUUID())) + .build() + + assertEquals(listOf("Bob invited you to the group.", "3 people were invited to the group."), describeChange(change)) + } + + @Test + fun member_invited_3_persons_and_you_and_added_another_where_you_were_not_first() { + val change = ChangeBuilder.changeBy(bob) + .addMember(alice) + .invite(you) + .invite(ACI.from(UUID.randomUUID())) + .invite(ACI.from(UUID.randomUUID())) + .build() + + assertEquals(listOf("Bob invited you to the group.", "Bob added Alice.", "Bob invited 2 people to the group."), describeChange(change)) + } + + @Test + fun unknown_editor_but_known_invitee_invited_you_and_added_another_where_you_were_not_first() { + val change = ChangeBuilder.changeByUnknown() + .addMember(bob) + .inviteBy(you, alice) + .build() + + assertEquals(listOf("Alice invited you to the group.", "Bob joined the group."), describeChange(change)) + } + + @Test + fun unknown_editor_and_unknown_inviter_invited_you_and_added_another_where_you_were_not_first() { + val change = ChangeBuilder.changeByUnknown() + .addMember(alice) + .invite(you) + .build() + + assertEquals(listOf("You were invited to the group.", "Alice joined the group."), describeChange(change)) + } + + // Member invitation revocation + @Test + fun member_uninvited_1_person() { + val change = ChangeBuilder.changeBy(alice) + .uninvite(bob) + .build() + + assertEquals(listOf("Alice revoked an invitation to the group."), describeChange(change)) + } + + @Test + fun member_uninvited_2_people() { + val change = ChangeBuilder.changeBy(alice) + .uninvite(bob) + .uninvite(ACI.from(UUID.randomUUID())) + .build() + + assertEquals(listOf("Alice revoked 2 invitations to the group."), describeChange(change)) + } + + @Test + fun you_uninvited_1_person() { + val change = ChangeBuilder.changeBy(you) + .uninvite(bob) + .build() + + assertEquals(listOf("You revoked an invitation to the group."), describeChange(change)) + } + + @Test + fun you_uninvited_2_people() { + val change = ChangeBuilder.changeBy(you) + .uninvite(bob) + .uninvite(ACI.from(UUID.randomUUID())) + .build() + + assertEquals(listOf("You revoked 2 invitations to the group."), describeChange(change)) + } + + @Test + fun pending_member_declines_invite() { + val change = ChangeBuilder.changeBy(bob) + .uninvite(bob) + .build() + + assertEquals(listOf("Someone declined an invitation to the group."), describeChange(change)) + } + + @Test + fun you_decline_invite() { + val change = ChangeBuilder.changeBy(you) + .uninvite(you) + .build() + + assertEquals(listOf("You declined the invitation to the group."), describeChange(change)) + } + + @Test + fun unknown_revokes_your_invite() { + val change = ChangeBuilder.changeByUnknown() + .uninvite(you) + .build() + + assertEquals(listOf("An admin revoked your invitation to the group."), describeChange(change)) + } + + @Test + fun unknown_revokes_1_invite() { + val change = ChangeBuilder.changeByUnknown() + .uninvite(bob) + .build() + + assertEquals(listOf("An invitation to the group was revoked."), describeChange(change)) + } + + @Test + fun unknown_revokes_2_invites() { + val change = ChangeBuilder.changeByUnknown() + .uninvite(bob) + .uninvite(ACI.from(UUID.randomUUID())) + .build() + + assertEquals(listOf("2 invitations to the group were revoked."), describeChange(change)) + } + + @Test + fun unknown_revokes_yours_and_three_other_invites() { + val change = ChangeBuilder.changeByUnknown() + .uninvite(bob) + .uninvite(you) + .uninvite(ACI.from(UUID.randomUUID())) + .uninvite(ACI.from(UUID.randomUUID())) + .build() + + assertEquals(listOf("An admin revoked your invitation to the group.", "3 invitations to the group were revoked."), describeChange(change)) + } + + @Test + fun your_invite_was_revoked_by_known_member() { + val change = ChangeBuilder.changeBy(bob) + .uninvite(you) + .build() + + assertEquals(listOf("Bob revoked your invitation to the group."), describeChange(change)) + } + + // Promote pending members + @Test + fun member_accepts_invite() { + val change = ChangeBuilder.changeBy(bob) + .promote(bob) + .build() + + assertEquals(listOf("Bob accepted an invitation to the group."), describeChange(change)) + } + + @Test + fun you_accept_invite() { + val change = ChangeBuilder.changeBy(you) + .promote(you) + .build() + + assertEquals(listOf("You accepted the invitation to the group."), describeChange(change)) + } + + @Test + fun member_promotes_pending_member() { + val change = ChangeBuilder.changeBy(bob) + .promote(alice) + .build() + + assertEquals(listOf("Bob added invited member Alice."), describeChange(change)) + } + + @Test + fun you_promote_pending_member() { + val change = ChangeBuilder.changeBy(you) + .promote(bob) + .build() + + assertEquals(listOf("You added invited member Bob."), describeChange(change)) + } + + @Test + fun member_promotes_you() { + val change = ChangeBuilder.changeBy(bob) + .promote(you) + .build() + + assertEquals(listOf("Bob added you to the group."), describeChange(change)) + } + + @Test + fun unknown_added_by_invite() { + val change = ChangeBuilder.changeByUnknown() + .promote(you) + .build() + + assertEquals(listOf("You joined the group."), describeChange(change)) + } + + @Test + fun unknown_promotes_pending_member() { + val change = ChangeBuilder.changeByUnknown() + .promote(alice) + .build() + + assertEquals(listOf("Alice joined the group."), describeChange(change)) + } + + // Title change + @Test + fun member_changes_title() { + val change = ChangeBuilder.changeBy(alice) + .title("New title") + .build() + + assertEquals(listOf("Alice changed the group name to \"" + StringUtil.isolateBidi("New title") + "\"."), describeChange(change)) + } + + @Test + fun you_change_title() { + val change = ChangeBuilder.changeBy(you) + .title("Title 2") + .build() + + assertEquals(listOf("You changed the group name to \"" + StringUtil.isolateBidi("Title 2") + "\"."), describeChange(change)) + } + + @Test + fun unknown_changed_title() { + val change = ChangeBuilder.changeByUnknown() + .title("Title 3") + .build() + + assertEquals(listOf("The group name has changed to \"" + StringUtil.isolateBidi("Title 3") + "\"."), describeChange(change)) + } + + // Avatar change + @Test + fun member_changes_avatar() { + val change = ChangeBuilder.changeBy(alice) + .avatar("Avatar1") + .build() + + assertEquals(listOf("Alice changed the group avatar."), describeChange(change)) + } + + @Test + fun you_change_avatar() { + val change = ChangeBuilder.changeBy(you) + .avatar("Avatar2") + .build() + + assertEquals(listOf("You changed the group avatar."), describeChange(change)) + } + + @Test + fun unknown_changed_avatar() { + val change = ChangeBuilder.changeByUnknown() + .avatar("Avatar3") + .build() + + assertEquals(listOf("The group avatar has been changed."), describeChange(change)) + } + + // Timer change + @Test + fun member_changes_timer() { + val change = ChangeBuilder.changeBy(bob) + .timer(10) + .build() + + assertEquals(listOf("Bob set the disappearing message timer to 10 seconds."), describeChange(change)) + } + + @Test + fun you_change_timer() { + val change = ChangeBuilder.changeBy(you) + .timer(60) + .build() + + assertEquals(listOf("You set the disappearing message timer to 1 minute."), describeChange(change)) + } + + @Test + fun unknown_change_timer() { + val change = ChangeBuilder.changeByUnknown() + .timer(120) + .build() + + assertEquals(listOf("The disappearing message timer has been set to 2 minutes."), describeChange(change)) + } + + @Test + fun unknown_change_timer_mentions_no_one() { + val change = ChangeBuilder.changeByUnknown() + .timer(120) + .build() + + assertSingleChangeMentioning(change, emptyList()) + } + + // Attribute access change + @Test + fun member_changes_attribute_access() { + val change = ChangeBuilder.changeBy(bob) + .attributeAccess(MEMBER) + .build() + + assertEquals(listOf("Bob changed who can edit group info to \"All members\"."), describeChange(change)) + } + + @Test + fun you_changed_attribute_access() { + val change = ChangeBuilder.changeBy(you) + .attributeAccess(ADMINISTRATOR) + .build() + + assertEquals(listOf("You changed who can edit group info to \"Only admins\"."), describeChange(change)) + } + + @Test + fun unknown_changed_attribute_access() { + val change = ChangeBuilder.changeByUnknown() + .attributeAccess(ADMINISTRATOR) + .build() + + assertEquals(listOf("Who can edit group info has been changed to \"Only admins\"."), describeChange(change)) + } + + // Membership access change + @Test + fun member_changes_membership_access() { + val change = ChangeBuilder.changeBy(alice) + .membershipAccess(ADMINISTRATOR) + .build() + + assertEquals(listOf("Alice changed who can edit group membership to \"Only admins\"."), describeChange(change)) + } + + @Test + fun you_changed_membership_access() { + val change = ChangeBuilder.changeBy(you) + .membershipAccess(MEMBER) + .build() + + assertEquals(listOf("You changed who can edit group membership to \"All members\"."), describeChange(change)) + } + + @Test + fun unknown_changed_membership_access() { + val change = ChangeBuilder.changeByUnknown() + .membershipAccess(ADMINISTRATOR) + .build() + + assertEquals(listOf("Who can edit group membership has been changed to \"Only admins\"."), describeChange(change)) + } + + // Group link access change + @Test + fun you_changed_group_link_access_to_any() { + val change = ChangeBuilder.changeBy(you) + .inviteLinkAccess(ANY) + .build() + + assertEquals(listOf("You turned on the group link with admin approval off."), describeChange(change)) + } + + @Test + fun you_changed_group_link_access_to_administrator_approval() { + val change = ChangeBuilder.changeBy(you) + .inviteLinkAccess(ADMINISTRATOR) + .build() + + assertEquals(listOf("You turned on the group link with admin approval on."), describeChange(change)) + } + + @Test + fun you_turned_off_group_link_access() { + val change = ChangeBuilder.changeBy(you) + .inviteLinkAccess(UNSATISFIABLE) + .build() + + assertEquals(listOf("You turned off the group link."), describeChange(change)) + } + + @Test + fun member_changed_group_link_access_to_any() { + val change = ChangeBuilder.changeBy(alice) + .inviteLinkAccess(ANY) + .build() + + assertEquals(listOf("Alice turned on the group link with admin approval off."), describeChange(change)) + } + + @Test + fun member_changed_group_link_access_to_administrator_approval() { + val change = ChangeBuilder.changeBy(bob) + .inviteLinkAccess(ADMINISTRATOR) + .build() + + assertEquals(listOf("Bob turned on the group link with admin approval on."), describeChange(change)) + } + + @Test + fun member_turned_off_group_link_access() { + val change = ChangeBuilder.changeBy(alice) + .inviteLinkAccess(UNSATISFIABLE) + .build() + + assertEquals(listOf("Alice turned off the group link."), describeChange(change)) + } + + @Test + fun unknown_changed_group_link_access_to_any() { + val change = ChangeBuilder.changeByUnknown() + .inviteLinkAccess(ANY) + .build() + + assertEquals(listOf("The group link has been turned on with admin approval off."), describeChange(change)) + } + + @Test + fun unknown_changed_group_link_access_to_administrator_approval() { + val change = ChangeBuilder.changeByUnknown() + .inviteLinkAccess(ADMINISTRATOR) + .build() + + assertEquals(listOf("The group link has been turned on with admin approval on."), describeChange(change)) + } + + @Test + fun unknown_turned_off_group_link_access() { + val change = ChangeBuilder.changeByUnknown() + .inviteLinkAccess(UNSATISFIABLE) + .build() + + assertEquals(listOf("The group link has been turned off."), describeChange(change)) + } + + // Group link with known previous group state + @Test + fun group_link_access_from_unknown_to_administrator() { + assertEquals("You turned on the group link with admin approval on.", describeGroupLinkChange(you, UNKNOWN, ADMINISTRATOR)) + assertEquals("Alice turned on the group link with admin approval on.", describeGroupLinkChange(alice, UNKNOWN, ADMINISTRATOR)) + assertEquals("The group link has been turned on with admin approval on.", describeGroupLinkChange(null, UNKNOWN, ADMINISTRATOR)) + } + + @Test + fun group_link_access_from_administrator_to_unsatisfiable() { + assertEquals("You turned off the group link.", describeGroupLinkChange(you, ADMINISTRATOR, UNSATISFIABLE)) + assertEquals("Bob turned off the group link.", describeGroupLinkChange(bob, ADMINISTRATOR, UNSATISFIABLE)) + assertEquals("The group link has been turned off.", describeGroupLinkChange(null, ADMINISTRATOR, UNSATISFIABLE)) + } + + @Test + fun group_link_access_from_unsatisfiable_to_administrator() { + assertEquals("You turned on the group link with admin approval on.", describeGroupLinkChange(you, UNSATISFIABLE, ADMINISTRATOR)) + assertEquals("Alice turned on the group link with admin approval on.", describeGroupLinkChange(alice, UNSATISFIABLE, ADMINISTRATOR)) + assertEquals("The group link has been turned on with admin approval on.", describeGroupLinkChange(null, UNSATISFIABLE, ADMINISTRATOR)) + } + + @Test + fun group_link_access_from_administrator_to_any() { + assertEquals("You turned off admin approval for the group link.", describeGroupLinkChange(you, ADMINISTRATOR, ANY)) + assertEquals("Bob turned off admin approval for the group link.", describeGroupLinkChange(bob, ADMINISTRATOR, ANY)) + assertEquals("The admin approval for the group link has been turned off.", describeGroupLinkChange(null, ADMINISTRATOR, ANY)) + } + + @Test + fun group_link_access_from_any_to_administrator() { + assertEquals("You turned on admin approval for the group link.", describeGroupLinkChange(you, ANY, ADMINISTRATOR)) + assertEquals("Bob turned on admin approval for the group link.", describeGroupLinkChange(bob, ANY, ADMINISTRATOR)) + assertEquals("The admin approval for the group link has been turned on.", describeGroupLinkChange(null, ANY, ADMINISTRATOR)) + } + + private fun describeGroupLinkChange(editor: ACI?, fromAccess: AccessRequired, toAccess: AccessRequired): String { + val previousGroupState = DecryptedGroup.Builder() + .accessControl( + AccessControl.Builder() + .addFromInviteLink(fromAccess) + .build() + ) + .build() + val change = (if (editor != null) ChangeBuilder.changeBy(editor) else ChangeBuilder.changeByUnknown()).inviteLinkAccess(toAccess) + .build() + + val strings = describeChange(previousGroupState, change) + return strings.single() + } + + // Group link reset + @Test + fun you_reset_group_link() { + val change = ChangeBuilder.changeBy(you) + .resetGroupLink() + .build() + + assertEquals(listOf("You reset the group link."), describeChange(change)) + } + + @Test + fun member_reset_group_link() { + val change = ChangeBuilder.changeBy(alice) + .resetGroupLink() + .build() + + assertEquals(listOf("Alice reset the group link."), describeChange(change)) + } + + @Test + fun unknown_reset_group_link() { + val change = ChangeBuilder.changeByUnknown() + .resetGroupLink() + .build() + + assertEquals(listOf("The group link has been reset."), describeChange(change)) + } + + /** + * When the group link is turned on and reset in the same change, assume this is the first time + * the link password it being set and do not show reset message. + */ + @Test + fun member_changed_group_link_access_to_on_and_reset() { + val change = ChangeBuilder.changeBy(alice) + .inviteLinkAccess(ANY) + .resetGroupLink() + .build() + + assertEquals(listOf("Alice turned on the group link with admin approval off."), describeChange(change)) + } + + /** + * When the group link is turned on and reset in the same change, assume this is the first time + * the link password it being set and do not show reset message. + */ + @Test + fun you_changed_group_link_access_to_on_and_reset() { + val change = ChangeBuilder.changeBy(you) + .inviteLinkAccess(ADMINISTRATOR) + .resetGroupLink() + .build() + + assertEquals(listOf("You turned on the group link with admin approval on."), describeChange(change)) + } + + @Test + fun you_changed_group_link_access_to_off_and_reset() { + val change = ChangeBuilder.changeBy(you) + .inviteLinkAccess(UNSATISFIABLE) + .resetGroupLink() + .build() + + assertEquals(listOf("You turned off the group link.", "You reset the group link."), describeChange(change)) + } + + // Group link request + @Test + fun you_requested_to_join_the_group() { + val change = ChangeBuilder.changeBy(you) + .requestJoin() + .build() + + assertEquals(listOf("You sent a request to join the group."), describeChange(change)) + } + + @Test + fun member_requested_to_join_the_group() { + val change = ChangeBuilder.changeBy(bob) + .requestJoin() + .build() + + assertEquals(listOf("Bob requested to join via the group link."), describeChange(change)) + } + + @Test + fun unknown_requested_to_join_the_group() { + val change = ChangeBuilder.changeByUnknown() + .requestJoin(alice) + .build() + + assertEquals(listOf("Alice requested to join via the group link."), describeChange(change)) + } + + @Test + fun member_approved_your_join_request() { + val change = ChangeBuilder.changeBy(bob) + .approveRequest(you) + .build() + + assertEquals(listOf("Bob approved your request to join the group."), describeChange(change)) + } + + @Test + fun member_approved_another_join_request() { + val change = ChangeBuilder.changeBy(alice) + .approveRequest(bob) + .build() + + assertEquals(listOf("Alice approved a request to join the group from Bob."), describeChange(change)) + } + + @Test + fun you_approved_another_join_request() { + val change = ChangeBuilder.changeBy(you) + .approveRequest(alice) + .build() + + assertEquals(listOf("You approved a request to join the group from Alice."), describeChange(change)) + } + + @Test + fun unknown_approved_your_join_request() { + val change = ChangeBuilder.changeByUnknown() + .approveRequest(you) + .build() + + assertEquals(listOf("Your request to join the group has been approved."), describeChange(change)) + } + + @Test + fun unknown_approved_another_join_request() { + val change = ChangeBuilder.changeByUnknown() + .approveRequest(bob) + .build() + + assertEquals(listOf("A request to join the group from Bob has been approved."), describeChange(change)) + } + + @Test + fun member_denied_another_join_request() { + val change = ChangeBuilder.changeBy(alice) + .denyRequest(bob) + .build() + + assertEquals(listOf("Alice denied a request to join the group from Bob."), describeChange(change)) + } + + @Test + fun member_denied_your_join_request() { + val change = ChangeBuilder.changeBy(alice) + .denyRequest(you) + .build() + + assertEquals(listOf("Your request to join the group has been denied by an admin."), describeChange(change)) + } + + @Test + fun you_cancelled_your_join_request() { + val change = ChangeBuilder.changeBy(you) + .denyRequest(you) + .build() + + assertEquals(listOf("You canceled your request to join the group."), describeChange(change)) + } + + @Test + fun member_cancelled_their_join_request() { + val change = ChangeBuilder.changeBy(alice) + .denyRequest(alice) + .build() + + assertEquals(listOf("Alice canceled their request to join the group."), describeChange(change)) + } + + @Test + fun unknown_denied_your_join_request() { + val change = ChangeBuilder.changeByUnknown() + .denyRequest(you) + .build() + + assertEquals(listOf("Your request to join the group has been denied by an admin."), describeChange(change)) + } + + @Test + fun unknown_denied_another_join_request() { + val change = ChangeBuilder.changeByUnknown() + .denyRequest(bob) + .build() + + assertEquals(listOf("A request to join the group from Bob has been denied."), describeChange(change)) + } + + // Multiple changes + @Test + fun multiple_changes() { + val change = ChangeBuilder.changeBy(alice) + .addMember(bob) + .membershipAccess(MEMBER) + .title("Title") + .addMember(you) + .timer(300) + .build() + + assertEquals( + listOf( + "Alice added you to the group.", + "Alice added Bob.", + "Alice changed the group name to \"" + StringUtil.isolateBidi("Title") + "\".", + "Alice set the disappearing message timer to 5 minutes.", + "Alice changed who can edit group membership to \"All members\"." + ), + describeChange(change) + ) + } + + @Test + fun multiple_changes_leave_and_promote() { + val change = ChangeBuilder.changeBy(alice) + .deleteMember(alice) + .promoteToAdmin(bob) + .build() + + assertEquals( + listOf( + "Alice made Bob an admin.", + "Alice left the group." + ), + describeChange(change) + ) + } + + @Test + fun multiple_changes_leave_and_promote_by_unknown() { + val change = ChangeBuilder.changeByUnknown() + .deleteMember(alice) + .promoteToAdmin(bob) + .build() + + assertEquals( + listOf( + "Bob is now an admin.", + "Alice is no longer in the group." + ), + describeChange(change) + ) + } + + @Test + fun multiple_changes_by_unknown() { + val change = ChangeBuilder.changeByUnknown() + .addMember(bob) + .membershipAccess(MEMBER) + .title("Title 2") + .avatar("Avatar 1") + .timer(600) + .build() + + assertEquals( + listOf( + "Bob joined the group.", + "The group name has changed to \"" + StringUtil.isolateBidi("Title 2") + "\".", + "The group avatar has been changed.", + "The disappearing message timer has been set to 10 minutes.", + "Who can edit group membership has been changed to \"All members\"." + ), + describeChange(change) + ) + } + + @Test + fun multiple_changes_join_and_leave_by_unknown() { + val change = ChangeBuilder.changeByUnknown() + .addMember(alice) + .promoteToAdmin(alice) + .deleteMember(alice) + .title("Updated title") + .build() + + assertEquals( + listOf( + "Alice joined the group.", + "Alice is now an admin.", + "The group name has changed to \"" + StringUtil.isolateBidi("Updated title") + "\".", + "Alice is no longer in the group." + ), + describeChange(change) + ) + } + + // Group state without a change record + @Test + fun you_created_a_group_change_not_found() { + val group = newGroupBy(you, 0) + .build() + + assertEquals("You joined the group.", describeNewGroup(group)) + } + + @Test + fun you_created_a_group() { + val group = newGroupBy(you, 0) + .build() + + val change = ChangeBuilder.changeBy(you) + .addMember(alice) + .addMember(you) + .addMember(bob) + .title("New title") + .build() + + assertEquals("You created the group.", describeNewGroup(group, change)) + } + + @Test + fun alice_created_a_group_change_not_found() { + val group = newGroupBy(alice, 0) + .member(you) + .build() + + assertEquals("You joined the group.", describeNewGroup(group)) + } + + @Test + fun alice_created_a_group() { + val group = newGroupBy(alice, 0) + .member(you) + .build() + + val change = ChangeBuilder.changeBy(alice) + .addMember(you) + .addMember(alice) + .addMember(bob) + .title("New title") + .build() + + assertEquals("Alice added you to the group.", describeNewGroup(group, change)) + } + + @Test + fun alice_created_a_group_above_zero() { + val group = newGroupBy(alice, 1) + .member(you) + .build() + + assertEquals("You joined the group.", describeNewGroup(group)) + } + + @Test + fun you_were_invited_to_a_group() { + val group = newGroupBy(alice, 0) + .invite(bob, you) + .build() + + assertEquals("Bob invited you to the group.", describeNewGroup(group)) + } + + @Test + fun describe_a_group_you_are_not_in() { + val group = newGroupBy(alice, 1) + .build() + + assertEquals("Group updated.", describeNewGroup(group)) + } + + @Test + fun makeRecipientsClickable_onePlaceholder() { + val id = RecipientId.from(1) + + val result = GroupsV2UpdateMessageProducer.makeRecipientsClickable( + /* context = */ + ApplicationProvider.getApplicationContext(), + /* template = */ + GroupsV2UpdateMessageProducer.makePlaceholder(id), + /* recipientIds = */ + listOf(id), + /* clickHandler = */ + null + ) + + assertEquals("Alice", result.toString()) + } + + @Test + fun makeRecipientsClickable_twoPlaceholders_sameRecipient() { + val id = RecipientId.from(1) + val placeholder = GroupsV2UpdateMessageProducer.makePlaceholder(id) + + val result = GroupsV2UpdateMessageProducer.makeRecipientsClickable( + /* context = */ + ApplicationProvider.getApplicationContext(), + /* template = */ + "$placeholder $placeholder", + /* recipientIds = */ + listOf(id), + /* clickHandler = */ + null + ) + + assertEquals("Alice Alice", result.toString()) + } + + @Test + fun makeRecipientsClickable_twoPlaceholders_differentRecipient() { + val id1 = RecipientId.from(1) + val id2 = RecipientId.from(2) + + val placeholder1 = GroupsV2UpdateMessageProducer.makePlaceholder(id1) + val placeholder2 = GroupsV2UpdateMessageProducer.makePlaceholder(id2) + + val result = GroupsV2UpdateMessageProducer.makeRecipientsClickable( + /* context = */ + ApplicationProvider.getApplicationContext(), + /* template = */ + "$placeholder1 $placeholder2", + /* recipientIds = */ + listOf(id1, id2), + /* clickHandler = */ + null + ) + + assertEquals("Alice Bob", result.toString()) + } + + @Test + fun makeRecipientsClickable_complicated() { + val id1 = RecipientId.from(1) + val id2 = RecipientId.from(2) + + val placeholder1 = GroupsV2UpdateMessageProducer.makePlaceholder(id1) + val placeholder2 = GroupsV2UpdateMessageProducer.makePlaceholder(id2) + + val result = GroupsV2UpdateMessageProducer.makeRecipientsClickable( + /* context = */ + ApplicationProvider.getApplicationContext(), + /* template = */ + "$placeholder1 said hello to $placeholder2, and $placeholder2 said hello back to $placeholder1.", + /* recipientIds = */ + listOf(id1, id2), + /* clickHandler = */ + null + ) + + assertEquals("Alice said hello to Bob, and Bob said hello back to Alice.", result.toString()) + } + + private fun describeConvertedNewGroup(groupState: DecryptedGroup, groupChange: DecryptedGroupChange): String { + val update = translateDecryptedChangeNewGroup( + selfIds, + DecryptedGroupV2Context.Builder() + .change(groupChange) + .groupState(groupState) + .build() + ) + + return producer.describeChanges(update.updates).single().spannable.toString() + } + + private fun describeConvertedChange(previousGroupState: DecryptedGroup?, change: DecryptedGroupChange): List { + val update = translateDecryptedChangeUpdate( + selfIds, + DecryptedGroupV2Context.Builder() + .change(change) + .previousGroupState(previousGroupState) + .build() + ) + + return producer.describeChanges(update.updates) + .map { it.spannable } + .map { it.toString() } + .toList() + } + + private fun describeChange(change: DecryptedGroupChange): List { + return describeChange(null, change) + } + + private fun describeChange( + previousGroupState: DecryptedGroup?, + change: DecryptedGroupChange + ): List { + val convertedChange = describeConvertedChange(previousGroupState, change) + val describedChange = producer.describeChanges(previousGroupState, change) + .map { it.spannable } + .map { it.toString() } + .toList() + assertEquals(describedChange.size, convertedChange.size) + + val convertedIterator = convertedChange.listIterator() + val describedIterator = describedChange.listIterator() + + while (convertedIterator.hasNext()) { + assertEquals(describedIterator.next(), convertedIterator.next()) + } + return describedChange + } + + private fun describeNewGroup(group: DecryptedGroup, groupChange: DecryptedGroupChange = DecryptedGroupChange()): String { + val newGroupString = producer.describeNewGroup(group, groupChange).spannable.toString() + val convertedGroupString = describeConvertedNewGroup(group, groupChange) + + assertEquals(newGroupString, convertedGroupString) + + return newGroupString + } + + private fun assertSingleChangeMentioning(change: DecryptedGroupChange, expectedMentions: List) { + val changes = producer.describeChanges(null, change) + + val description = changes.single() + assertEquals(expectedMentions, description.mentioned) + + if (expectedMentions.isEmpty()) { + assertTrue(description.isStringStatic) + } else { + assertFalse(description.isStringStatic) + } + } + + private class GroupStateBuilder(foundingMember: ACI, revision: Int) { + private val builder = DecryptedGroup.Builder() + .revision(revision) + .members(listOf(DecryptedMember.Builder().aciBytes(foundingMember.toByteString()).build())) + + fun invite(inviter: ACI, invitee: ServiceId): GroupStateBuilder { + builder.pendingMembers(builder.pendingMembers.plus(DecryptedPendingMember.Builder().serviceIdBytes(invitee.toByteString()).addedByAci(inviter.toByteString()).build())) + return this + } + + fun member(member: ACI): GroupStateBuilder { + builder.members(builder.members.plus(DecryptedMember.Builder().aciBytes(member.toByteString()).build())) + return this + } + + fun build(): DecryptedGroup { + return builder.build() + } + } + + companion object { + private fun recipientWithName(id: RecipientId, name: String): Recipient { + return mockk { + every { this@mockk.id } returns id + every { getDisplayName(any()) } returns name + } + } + + private fun newGroupBy(foundingMember: ACI, revision: Int): GroupStateBuilder { + return GroupStateBuilder(foundingMember, revision) + } + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/JobMigratorTest.java b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/JobMigratorTest.java deleted file mode 100644 index d713666152..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/JobMigratorTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager; - -import androidx.annotation.NonNull; - -import org.junit.BeforeClass; -import org.junit.Test; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec; -import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; - -import kotlin.jvm.functions.Function1; - -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class JobMigratorTest { - - @BeforeClass - public static void init() { - Log.initialize(mock(Log.Logger.class)); - } - - @Test(expected = AssertionError.class) - public void JobMigrator_crashWhenTooFewMigrations() { - new JobMigrator(1, 2, Collections.emptyList()); - } - - @Test(expected = AssertionError.class) - public void JobMigrator_crashWhenTooManyMigrations() { - new JobMigrator(1, 2, Arrays.asList(new EmptyMigration(2), new EmptyMigration(3))); - } - - @Test(expected = AssertionError.class) - public void JobMigrator_crashWhenSkippingMigrations() { - new JobMigrator(1, 3, Arrays.asList(new EmptyMigration(2), new EmptyMigration(4))); - } - - @Test - public void JobMigrator_properInitialization() { - new JobMigrator(1, 3, Arrays.asList(new EmptyMigration(2), new EmptyMigration(3))); - } - - @Test - public void migrate_callsAppropriateMigrations_fullSet() { - JobMigration migration1 = spy(new EmptyMigration(2)); - JobMigration migration2 = spy(new EmptyMigration(3)); - - JobMigrator subject = new JobMigrator(1, 3, Arrays.asList(migration1, migration2)); - int version = subject.migrate(simpleJobStorage()); - - assertEquals(3, version); - verify(migration1).migrate(any()); - verify(migration2).migrate(any()); - } - - @Test - public void migrate_callsAppropriateMigrations_subset() { - JobMigration migration1 = spy(new EmptyMigration(2)); - JobMigration migration2 = spy(new EmptyMigration(3)); - - JobMigrator subject = new JobMigrator(2, 3, Arrays.asList(migration1, migration2)); - int version = subject.migrate(simpleJobStorage()); - - assertEquals(3, version); - verify(migration1, never()).migrate(any()); - verify(migration2).migrate(any()); - } - - @Test - public void migrate_callsAppropriateMigrations_none() { - JobMigration migration1 = spy(new EmptyMigration(2)); - JobMigration migration2 = spy(new EmptyMigration(3)); - - JobMigrator subject = new JobMigrator(3, 3, Arrays.asList(migration1, migration2)); - int version = subject.migrate(simpleJobStorage()); - - assertEquals(3, version); - verify(migration1, never()).migrate(any()); - verify(migration2, never()).migrate(any()); - } - - private static JobStorage simpleJobStorage() { - JobStorage jobStorage = mock(JobStorage.class); - JobSpec job = new JobSpec("1", "f1", null, 1, 1, 1, 1, 1, 1, null, null, false, false, 0, 0); - - when(jobStorage.debugGetJobSpecs(anyInt())).thenReturn(new ArrayList<>(Collections.singletonList(job))); - doAnswer(invocation -> { - Function1 transformer = invocation.getArgument(0); - return transformer.invoke(job); - }).when(jobStorage).transformJobs(any()); - - return jobStorage; - } - - private static class EmptyMigration extends JobMigration { - - protected EmptyMigration(int endVersion) { - super(endVersion); - } - - @Override - public @NonNull JobData migrate(@NonNull JobData jobData) { - return jobData; - } - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/JobMigratorTest.kt b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/JobMigratorTest.kt new file mode 100644 index 0000000000..b4b9e4bfa5 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/JobMigratorTest.kt @@ -0,0 +1,130 @@ +package org.thoughtcrime.securesms.jobmanager + +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.BeforeClass +import org.junit.Test +import org.signal.core.util.logging.Log.initialize +import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec +import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage +import org.thoughtcrime.securesms.testutil.EmptyLogger + +class JobMigratorTest { + @Test + fun test_JobMigrator_crashWhenTooFewMigrations() { + val error = assertThrows(AssertionError::class.java) { + JobMigrator(1, 2, emptyList()) + } + assertEquals("You must have a migration for every version!", error.message) + } + + @Test + fun test_JobMigrator_crashWhenTooManyMigrations() { + val error = assertThrows(AssertionError::class.java) { + JobMigrator(1, 2, listOf(EmptyMigration(2), EmptyMigration(3))) + } + assertEquals("You must have a migration for every version!", error.message) + } + + @Test + fun test_JobMigrator_crashWhenSkippingMigrations() { + val error = assertThrows(AssertionError::class.java) { + JobMigrator(1, 3, listOf(EmptyMigration(2), EmptyMigration(4))) + } + assertEquals("Missing migration for version 3!", error.message) + } + + @Test + fun test_JobMigrator_properInitialization() { + JobMigrator(1, 3, listOf(EmptyMigration(2), EmptyMigration(3))) + } + + @Test + fun migrate_callsAppropriateMigrations_fullSet() { + val migration1 = EmptyMigration(2) + val migration2 = EmptyMigration(3) + + val subject = JobMigrator(1, 3, listOf(migration1, migration2)) + val version = subject.migrate(simpleJobStorage()) + + assertEquals(3, version) + assertTrue(migration1.migrated) + assertTrue(migration2.migrated) + } + + @Test + fun migrate_callsAppropriateMigrations_subset() { + val migration1 = EmptyMigration(2) + val migration2 = EmptyMigration(3) + + val subject = JobMigrator(2, 3, listOf(migration1, migration2)) + val version = subject.migrate(simpleJobStorage()) + + assertEquals(3, version) + assertFalse(migration1.migrated) + assertTrue(migration2.migrated) + } + + @Test + fun migrate_callsAppropriateMigrations_none() { + val migration1 = EmptyMigration(2) + val migration2 = EmptyMigration(3) + + val subject = JobMigrator(3, 3, listOf(migration1, migration2)) + val version = subject.migrate(simpleJobStorage()) + + assertEquals(3, version) + assertFalse(migration1.migrated) + assertFalse(migration2.migrated) + } + + private class EmptyMigration(endVersion: Int) : JobMigration(endVersion) { + private var _migrated: Boolean = false + val migrated: Boolean get() = _migrated + + override fun migrate(jobData: JobData): JobData { + _migrated = true + return jobData + } + } + + companion object { + @JvmStatic + @BeforeClass + fun init() { + initialize(EmptyLogger()) + } + + private fun simpleJobStorage(): JobStorage { + val job = JobSpec( + id = "1", + factoryKey = "f1", + queueKey = null, + createTime = 1, + lastRunAttemptTime = 1, + nextBackoffInterval = 1, + runAttempt = 1, + maxAttempts = 1, + lifespan = 1, + serializedData = null, + serializedInputData = null, + isRunning = false, + isMemoryOnly = false, + globalPriority = 0, + queuePriority = 0 + ) + return mockk { + every { debugGetJobSpecs(any()) } returns listOf(job) + every { transformJobs(any()) } answers { + @Suppress("UNCHECKED_CAST") + val transformer = invocation.args.single() as Function1 + transformer(job) + } + } + } + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigrationTest.java b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigrationTest.java deleted file mode 100644 index 3bfbdfcb06..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigrationTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.migrations; - -import org.junit.Rule; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.JobMigration.JobData; -import org.thoughtcrime.securesms.jobs.FailingJob; -import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob; -import org.thoughtcrime.securesms.recipients.Recipient; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; - -public class RecipientIdFollowUpJobMigrationTest { - - @Rule - public MockitoRule rule = MockitoJUnit.rule(); - - @Mock - private MockedStatic recipientMockedStatic; - - @Mock - private MockedStatic jobParametersMockedStatic; - - @Test - public void migrate_sendDeliveryReceiptJob_good() throws Exception { - JobData testData = new JobData("SendDeliveryReceiptJob", null, -1, -1, new JsonJobData.Builder().putString("recipient", "1") - .putLong("message_id", 1) - .putLong("timestamp", 2) - .serialize()); - RecipientIdFollowUpJobMigration subject = new RecipientIdFollowUpJobMigration(); - JobData converted = subject.migrate(testData); - - assertEquals("SendDeliveryReceiptJob", converted.getFactoryKey()); - assertNull(converted.getQueueKey()); - - JsonJobData data = JsonJobData.deserialize(converted.getData()); - assertEquals("1", data.getString("recipient")); - assertEquals(1, data.getLong("message_id")); - assertEquals(2, data.getLong("timestamp")); - - new SendDeliveryReceiptJob.Factory().create(mock(Job.Parameters.class), converted.getData()); - } - - @Test - public void migrate_sendDeliveryReceiptJob_bad() throws Exception { - JobData testData = new JobData("SendDeliveryReceiptJob", null, -1, -1, new JsonJobData.Builder().putString("recipient", "1") - .serialize()); - RecipientIdFollowUpJobMigration subject = new RecipientIdFollowUpJobMigration(); - JobData converted = subject.migrate(testData); - - assertEquals("FailingJob", converted.getFactoryKey()); - assertNull(converted.getQueueKey()); - - new FailingJob.Factory().create(mock(Job.Parameters.class), converted.getData()); - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigrationTest.kt b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigrationTest.kt new file mode 100644 index 0000000000..6899352352 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigrationTest.kt @@ -0,0 +1,57 @@ +package org.thoughtcrime.securesms.jobmanager.migrations + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.JobMigration.JobData +import org.thoughtcrime.securesms.jobmanager.JsonJobData +import org.thoughtcrime.securesms.jobs.FailingJob +import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob + +class RecipientIdFollowUpJobMigrationTest { + @Test + fun migrate_sendDeliveryReceiptJob_good() { + val testData = JobData( + "SendDeliveryReceiptJob", + null, + -1, + -1, + JsonJobData.Builder().putString("recipient", "1") + .putLong("message_id", 1) + .putLong("timestamp", 2) + .serialize() + ) + val subject = RecipientIdFollowUpJobMigration() + val converted = subject.migrate(testData) + + assertEquals("SendDeliveryReceiptJob", converted.factoryKey) + assertNull(converted.queueKey) + + val data = JsonJobData.deserialize(converted.data) + assertEquals("1", data.getString("recipient")) + assertEquals(1, data.getLong("message_id")) + assertEquals(2, data.getLong("timestamp")) + + SendDeliveryReceiptJob.Factory().create(Job.Parameters.Builder().build(), converted.data) + } + + @Test + fun migrate_sendDeliveryReceiptJob_bad() { + val testData = JobData( + "SendDeliveryReceiptJob", + null, + -1, + -1, + JsonJobData.Builder().putString("recipient", "1") + .serialize() + ) + val subject = RecipientIdFollowUpJobMigration() + val converted = subject.migrate(testData) + + assertEquals("FailingJob", converted.factoryKey) + assertNull(converted.queueKey) + + FailingJob.Factory().create(Job.Parameters.Builder().build(), converted.data) + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdJobMigrationTest.java b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdJobMigrationTest.java deleted file mode 100644 index 6c55198046..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdJobMigrationTest.java +++ /dev/null @@ -1,286 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.migrations; - -import android.app.Application; - -import org.junit.Rule; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.JobMigration.JobData; -import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration.NewSerializableSyncMessageId; -import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration.OldSerializableSyncMessageId; -import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; -import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; -import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob; -import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob; -import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob; -import org.thoughtcrime.securesms.jobs.PushGroupSendJob; -import org.thoughtcrime.securesms.jobs.IndividualSendJob; -import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob; -import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.util.JsonUtils; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class RecipientIdJobMigrationTest { - - @Rule - public MockitoRule rule = MockitoJUnit.rule(); - - @Mock - private MockedStatic recipientMockedStatic; - - @Mock - private MockedStatic jobParametersMockStatic; - - @Test - public void migrate_multiDeviceContactUpdateJob() throws Exception { - JobData testData = new JobData("MultiDeviceContactUpdateJob", "MultiDeviceContactUpdateJob", -1, -1, new JsonJobData.Builder().putBoolean("force_sync", false).putString("address", "+16101234567").serialize()); - mockRecipientResolve("+16101234567", 1); - - RecipientIdJobMigration subject = new RecipientIdJobMigration(mock(Application.class)); - JobData converted = subject.migrate(testData); - JsonJobData data = JsonJobData.deserialize(converted.getData()); - - assertEquals("MultiDeviceContactUpdateJob", converted.getFactoryKey()); - assertEquals("MultiDeviceContactUpdateJob", converted.getQueueKey()); - assertFalse(data.getBoolean("force_sync")); - assertFalse(data.hasString("address")); - assertEquals("1", data.getString("recipient")); - - new MultiDeviceContactUpdateJob.Factory().create(mock(Job.Parameters.class), converted.getData()); - } - - @Test - public void migrate_multiDeviceViewOnceOpenJob() throws Exception { - OldSerializableSyncMessageId oldId = new OldSerializableSyncMessageId("+16101234567", 1); - JobData testData = new JobData("MultiDeviceRevealUpdateJob", null, -1, -1, new JsonJobData.Builder().putString("message_id", JsonUtils.toJson(oldId)).serialize()); - mockRecipientResolve("+16101234567", 1); - - RecipientIdJobMigration subject = new RecipientIdJobMigration(mock(Application.class)); - JobData converted = subject.migrate(testData); - JsonJobData data = JsonJobData.deserialize(converted.getData()); - - assertEquals("MultiDeviceRevealUpdateJob", converted.getFactoryKey()); - assertNull(converted.getQueueKey()); - assertEquals(JsonUtils.toJson(new NewSerializableSyncMessageId("1", 1)), data.getString("message_id")); - - new MultiDeviceViewOnceOpenJob.Factory().create(mock(Job.Parameters.class), converted.getData()); - } - - @Test - public void migrate_sendDeliveryReceiptJob() throws Exception { - JobData testData = new JobData("SendDeliveryReceiptJob", null, -1, -1, new JsonJobData.Builder().putString("address", "+16101234567") - .putLong("message_id", 1) - .putLong("timestamp", 2) - .serialize()); - mockRecipientResolve("+16101234567", 1); - - RecipientIdJobMigration subject = new RecipientIdJobMigration(mock(Application.class)); - JobData converted = subject.migrate(testData); - JsonJobData data = JsonJobData.deserialize(converted.getData()); - - assertEquals("SendDeliveryReceiptJob", converted.getFactoryKey()); - assertNull(converted.getQueueKey()); - assertEquals("1", data.getString("recipient")); - assertEquals(1, data.getLong("message_id")); - assertEquals(2, data.getLong("timestamp")); - - new SendDeliveryReceiptJob.Factory().create(mock(Job.Parameters.class), converted.getData()); - } - - @Test - public void migrate_multiDeviceVerifiedUpdateJob() throws Exception { - JobData testData = new JobData("MultiDeviceVerifiedUpdateJob", "__MULTI_DEVICE_VERIFIED_UPDATE__", -1, -1, new JsonJobData.Builder().putString("destination", "+16101234567") - .putString("identity_key", "abcd") - .putInt("verified_status", 1) - .putLong("timestamp", 123) - .serialize()); - mockRecipientResolve("+16101234567", 1); - - RecipientIdJobMigration subject = new RecipientIdJobMigration(mock(Application.class)); - JobData converted = subject.migrate(testData); - JsonJobData data = JsonJobData.deserialize(converted.getData()); - - assertEquals("MultiDeviceVerifiedUpdateJob", converted.getFactoryKey()); - assertEquals("__MULTI_DEVICE_VERIFIED_UPDATE__", converted.getQueueKey()); - assertEquals("abcd", data.getString("identity_key")); - assertEquals(1, data.getInt("verified_status")); - assertEquals(123, data.getLong("timestamp")); - assertEquals("1", data.getString("destination")); - - new MultiDeviceVerifiedUpdateJob.Factory().create(mock(Job.Parameters.class), converted.getData()); - } - - @Test - public void migrate_pushGroupSendJob_null() throws Exception { - JobData testData = new JobData("PushGroupSendJob", "someGroupId", -1, -1, new JsonJobData.Builder().putString("filter_address", null) - .putLong("message_id", 123) - .serialize()); - mockRecipientResolve("someGroupId", 5); - - RecipientIdJobMigration subject = new RecipientIdJobMigration(mock(Application.class)); - JobData converted = subject.migrate(testData); - JsonJobData data = JsonJobData.deserialize(converted.getData()); - - assertEquals("PushGroupSendJob", converted.getFactoryKey()); - assertEquals(RecipientId.from(5).toQueueKey(), converted.getQueueKey()); - assertNull(data.getString("filter_recipient")); - assertFalse(data.hasString("filter_address")); - - new PushGroupSendJob.Factory().create(mock(Job.Parameters.class), converted.getData()); - } - - @Test - public void migrate_pushGroupSendJob_nonNull() throws Exception { - JobData testData = new JobData("PushGroupSendJob", "someGroupId", -1, -1, new JsonJobData.Builder().putString("filter_address", "+16101234567") - .putLong("message_id", 123) - .serialize()); - mockRecipientResolve("+16101234567", 1); - mockRecipientResolve("someGroupId", 5); - - RecipientIdJobMigration subject = new RecipientIdJobMigration(mock(Application.class)); - JobData converted = subject.migrate(testData); - JsonJobData data = JsonJobData.deserialize(converted.getData()); - - assertEquals("PushGroupSendJob", converted.getFactoryKey()); - assertEquals(RecipientId.from(5).toQueueKey(), converted.getQueueKey()); - assertEquals("1", data.getString("filter_recipient")); - assertFalse(data.hasString("filter_address")); - - new PushGroupSendJob.Factory().create(mock(Job.Parameters.class), converted.getData()); - } - - @Test - public void migrate_directoryRefreshJob_null() throws Exception { - JobData testData = new JobData("DirectoryRefreshJob", "DirectoryRefreshJob", -1, -1, new JsonJobData.Builder().putString("address", null).putBoolean("notify_of_new_users", true).serialize()); - - RecipientIdJobMigration subject = new RecipientIdJobMigration(mock(Application.class)); - JobData converted = subject.migrate(testData); - JsonJobData data = JsonJobData.deserialize(converted.getData()); - - assertEquals("DirectoryRefreshJob", converted.getFactoryKey()); - assertEquals("DirectoryRefreshJob", converted.getQueueKey()); - assertNull(data.getString("recipient")); - assertTrue(data.getBoolean("notify_of_new_users")); - assertFalse(data.hasString("address")); - - new DirectoryRefreshJob.Factory().create(mock(Job.Parameters.class), converted.getData()); - } - - @Test - public void migrate_directoryRefreshJob_nonNull() throws Exception { - JobData testData = new JobData("DirectoryRefreshJob", "DirectoryRefreshJob", -1, -1, new JsonJobData.Builder().putString("address", "+16101234567").putBoolean("notify_of_new_users", true).serialize()); - mockRecipientResolve("+16101234567", 1); - - RecipientIdJobMigration subject = new RecipientIdJobMigration(mock(Application.class)); - JobData converted = subject.migrate(testData); - JsonJobData data = JsonJobData.deserialize(converted.getData()); - - assertEquals("DirectoryRefreshJob", converted.getFactoryKey()); - assertEquals("DirectoryRefreshJob", converted.getQueueKey()); - assertTrue(data.getBoolean("notify_of_new_users")); - assertEquals("1", data.getString("recipient")); - assertFalse(data.hasString("address")); - - new DirectoryRefreshJob.Factory().create(mock(Job.Parameters.class), converted.getData()); - } - - @Test - public void migrate_retrieveProfileAvatarJob() throws Exception { - JobData testData = new JobData("RetrieveProfileAvatarJob", "RetrieveProfileAvatarJob+16101234567", -1, -1, new JsonJobData.Builder().putString("address", "+16101234567").putString("profile_avatar", "abc").serialize()); - mockRecipientResolve("+16101234567", 1); - - RecipientIdJobMigration subject = new RecipientIdJobMigration(mock(Application.class)); - JobData converted = subject.migrate(testData); - JsonJobData data = JsonJobData.deserialize(converted.getData()); - - assertEquals("RetrieveProfileAvatarJob", converted.getFactoryKey()); - assertEquals("RetrieveProfileAvatarJob::" + RecipientId.from(1).toQueueKey(), converted.getQueueKey()); - assertEquals("1", data.getString("recipient")); - - - new RetrieveProfileAvatarJob.Factory().create(mock(Job.Parameters.class), converted.getData()); - } - - @Test - public void migrate_multiDeviceReadUpdateJob_empty() throws Exception { - JobData testData = new JobData("MultiDeviceReadUpdateJob", null, -1, -1, new JsonJobData.Builder().putStringArray("message_ids", new String[0]).serialize()); - - RecipientIdJobMigration subject = new RecipientIdJobMigration(mock(Application.class)); - JobData converted = subject.migrate(testData); - JsonJobData data = JsonJobData.deserialize(converted.getData()); - - assertEquals("MultiDeviceReadUpdateJob", converted.getFactoryKey()); - assertNull(converted.getQueueKey()); - assertEquals(0, data.getStringArray("message_ids").length); - - new MultiDeviceReadUpdateJob.Factory().create(mock(Job.Parameters.class), converted.getData()); - } - - @Test - public void migrate_multiDeviceReadUpdateJob_twoIds() throws Exception { - OldSerializableSyncMessageId id1 = new OldSerializableSyncMessageId("+16101234567", 1); - OldSerializableSyncMessageId id2 = new OldSerializableSyncMessageId("+16101112222", 2); - - JobData testData = new JobData("MultiDeviceReadUpdateJob", null, -1, -1, new JsonJobData.Builder().putStringArray("message_ids", new String[]{ JsonUtils.toJson(id1), JsonUtils.toJson(id2) }).serialize()); - mockRecipientResolve("+16101234567", 1); - mockRecipientResolve("+16101112222", 2); - - RecipientIdJobMigration subject = new RecipientIdJobMigration(mock(Application.class)); - JobData converted = subject.migrate(testData); - JsonJobData data = JsonJobData.deserialize(converted.getData()); - - assertEquals("MultiDeviceReadUpdateJob", converted.getFactoryKey()); - assertNull(converted.getQueueKey()); - - String[] updated = data.getStringArray("message_ids"); - assertEquals(2, updated.length); - - assertEquals(JsonUtils.toJson(new NewSerializableSyncMessageId("1", 1)), updated[0]); - assertEquals(JsonUtils.toJson(new NewSerializableSyncMessageId("2", 2)), updated[1]); - - new MultiDeviceReadUpdateJob.Factory().create(mock(Job.Parameters.class), converted.getData()); - } - - @Test - public void migrate_pushMediaSendJob() throws Exception { - JobData testData = new JobData("PushMediaSendJob", "+16101234567", -1, -1, new JsonJobData.Builder().putLong("message_id", 1).serialize()); - mockRecipientResolve("+16101234567", 1); - - RecipientIdJobMigration subject = new RecipientIdJobMigration(mock(Application.class)); - JobData converted = subject.migrate(testData); - JsonJobData data = JsonJobData.deserialize(converted.getData()); - - assertEquals("PushMediaSendJob", converted.getFactoryKey()); - assertEquals(RecipientId.from(1).toQueueKey(), converted.getQueueKey()); - assertEquals(1, data.getLong("message_id")); - - new IndividualSendJob.Factory().create(mock(Job.Parameters.class), converted.getData()); - } - - private void mockRecipientResolve(String address, long recipientId) { - Recipient mockRecipient = mockRecipient(recipientId); - recipientMockedStatic.when(() -> Recipient.external(any(), eq(address))).thenReturn(mockRecipient); - } - - private Recipient mockRecipient(long id) { - Recipient recipient = mock(Recipient.class); - when(recipient.getId()).thenReturn(RecipientId.from(id)); - return recipient; - } - -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdJobMigrationTest.kt b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdJobMigrationTest.kt new file mode 100644 index 0000000000..c3d9692d9c --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdJobMigrationTest.kt @@ -0,0 +1,365 @@ +package org.thoughtcrime.securesms.jobmanager.migrations + +import android.app.Application +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkAll +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.JobMigration.JobData +import org.thoughtcrime.securesms.jobmanager.JsonJobData +import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration.NewSerializableSyncMessageId +import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration.OldSerializableSyncMessageId +import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob +import org.thoughtcrime.securesms.jobs.IndividualSendJob +import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob +import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob +import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob +import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob +import org.thoughtcrime.securesms.jobs.PushGroupSendJob +import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob +import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob +import org.thoughtcrime.securesms.recipients.LiveRecipient +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.util.JsonUtils + +class RecipientIdJobMigrationTest { + @Before + fun setup() { + mockkObject(Recipient) + every { Recipient.live(any()) } returns mockk(relaxed = true) + } + + @After + fun cleanup() { + unmockkAll() + } + + @Test + fun migrate_multiDeviceContactUpdateJob() { + val testData = JobData( + factoryKey = "MultiDeviceContactUpdateJob", + queueKey = "MultiDeviceContactUpdateJob", + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder().putBoolean("force_sync", false).putString("address", "+16101234567").serialize() + ) + mockRecipientResolve("+16101234567", 1) + + val subject = RecipientIdJobMigration(mockk()) + val converted = subject.migrate(testData) + val data = JsonJobData.deserialize(converted.data) + + assertEquals("MultiDeviceContactUpdateJob", converted.factoryKey) + assertEquals("MultiDeviceContactUpdateJob", converted.queueKey) + assertFalse(data.getBoolean("force_sync")) + assertFalse(data.hasString("address")) + assertEquals("1", data.getString("recipient")) + + MultiDeviceContactUpdateJob.Factory().create(Job.Parameters.Builder().build(), converted.data) + } + + @Test + fun migrate_multiDeviceViewOnceOpenJob() { + val oldId = OldSerializableSyncMessageId("+16101234567", 1) + val testData = JobData( + factoryKey = "MultiDeviceRevealUpdateJob", + queueKey = null, + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder().putString("message_id", JsonUtils.toJson(oldId)).serialize() + ) + mockRecipientResolve("+16101234567", 1) + + val subject = RecipientIdJobMigration(mockk()) + val converted = subject.migrate(testData) + val data = JsonJobData.deserialize(converted.data) + + assertEquals("MultiDeviceRevealUpdateJob", converted.factoryKey) + assertNull(converted.queueKey) + assertEquals(JsonUtils.toJson(NewSerializableSyncMessageId("1", 1)), data.getString("message_id")) + + MultiDeviceViewOnceOpenJob.Factory().create(Job.Parameters.Builder().build(), converted.data) + } + + @Test + fun migrate_sendDeliveryReceiptJob() { + val testData = JobData( + factoryKey = "SendDeliveryReceiptJob", + queueKey = null, + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder().putString("address", "+16101234567") + .putLong("message_id", 1) + .putLong("timestamp", 2) + .serialize() + ) + mockRecipientResolve("+16101234567", 1) + + val subject = RecipientIdJobMigration(mockk()) + val converted = subject.migrate(testData) + val data = JsonJobData.deserialize(converted.data) + + assertEquals("SendDeliveryReceiptJob", converted.factoryKey) + assertNull(converted.queueKey) + assertEquals("1", data.getString("recipient")) + assertEquals(1, data.getLong("message_id")) + assertEquals(2, data.getLong("timestamp")) + + SendDeliveryReceiptJob.Factory().create(Job.Parameters.Builder().build(), converted.data) + } + + @Test + fun migrate_multiDeviceVerifiedUpdateJob() { + val testData = JobData( + factoryKey = "MultiDeviceVerifiedUpdateJob", + queueKey = "__MULTI_DEVICE_VERIFIED_UPDATE__", + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder().putString("destination", "+16101234567") + .putString("identity_key", "abcd") + .putInt("verified_status", 1) + .putLong("timestamp", 123) + .serialize() + ) + mockRecipientResolve("+16101234567", 1) + + val subject = RecipientIdJobMigration(mockk()) + val converted = subject.migrate(testData) + val data = JsonJobData.deserialize(converted.data) + + assertEquals("MultiDeviceVerifiedUpdateJob", converted.factoryKey) + assertEquals("__MULTI_DEVICE_VERIFIED_UPDATE__", converted.queueKey) + assertEquals("abcd", data.getString("identity_key")) + assertEquals(1, data.getInt("verified_status")) + assertEquals(123, data.getLong("timestamp")) + assertEquals("1", data.getString("destination")) + + MultiDeviceVerifiedUpdateJob.Factory().create(Job.Parameters.Builder().build(), converted.data) + } + + @Test + fun migrate_pushGroupSendJob_null() { + val testData = JobData( + factoryKey = "PushGroupSendJob", + queueKey = "someGroupId", + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder().putString("filter_address", null) + .putLong("message_id", 123) + .serialize() + ) + mockRecipientResolve("someGroupId", 5) + + val subject = RecipientIdJobMigration(mockk()) + val converted = subject.migrate(testData) + val data = JsonJobData.deserialize(converted.data) + + assertEquals("PushGroupSendJob", converted.factoryKey) + assertEquals(RecipientId.from(5).toQueueKey(), converted.queueKey) + assertNull(data.getString("filter_recipient")) + assertFalse(data.hasString("filter_address")) + + PushGroupSendJob.Factory().create(Job.Parameters.Builder().build(), converted.data) + } + + @Test + fun migrate_pushGroupSendJob_nonNull() { + val testData = JobData( + factoryKey = "PushGroupSendJob", + queueKey = "someGroupId", + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder().putString("filter_address", "+16101234567") + .putLong("message_id", 123) + .serialize() + ) + mockRecipientResolve("+16101234567", 1) + mockRecipientResolve("someGroupId", 5) + + val subject = RecipientIdJobMigration(mockk()) + val converted = subject.migrate(testData) + val data = JsonJobData.deserialize(converted.data) + + assertEquals("PushGroupSendJob", converted.factoryKey) + assertEquals(RecipientId.from(5).toQueueKey(), converted.queueKey) + assertEquals("1", data.getString("filter_recipient")) + assertFalse(data.hasString("filter_address")) + + PushGroupSendJob.Factory().create(Job.Parameters.Builder().build(), converted.data) + } + + @Test + fun migrate_directoryRefreshJob_null() { + val testData = JobData( + factoryKey = "DirectoryRefreshJob", + queueKey = "DirectoryRefreshJob", + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder() + .putString("address", null) + .putBoolean("notify_of_new_users", true) + .serialize() + ) + + val subject = RecipientIdJobMigration(mockk()) + val converted = subject.migrate(testData) + val data = JsonJobData.deserialize(converted.data) + + assertEquals("DirectoryRefreshJob", converted.factoryKey) + assertEquals("DirectoryRefreshJob", converted.queueKey) + assertNull(data.getString("recipient")) + assertTrue(data.getBoolean("notify_of_new_users")) + assertFalse(data.hasString("address")) + + DirectoryRefreshJob.Factory().create(Job.Parameters.Builder().build(), converted.data) + } + + @Test + fun migrate_directoryRefreshJob_nonNull() { + val testData = JobData( + factoryKey = "DirectoryRefreshJob", + queueKey = "DirectoryRefreshJob", + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder() + .putString("address", "+16101234567") + .putBoolean("notify_of_new_users", true) + .serialize() + ) + mockRecipientResolve("+16101234567", 1) + + val subject = RecipientIdJobMigration(mockk()) + val converted = subject.migrate(testData) + val data = JsonJobData.deserialize(converted.data) + + assertEquals("DirectoryRefreshJob", converted.factoryKey) + assertEquals("DirectoryRefreshJob", converted.queueKey) + assertTrue(data.getBoolean("notify_of_new_users")) + assertEquals("1", data.getString("recipient")) + assertFalse(data.hasString("address")) + + DirectoryRefreshJob.Factory().create(Job.Parameters.Builder().build(), converted.data) + } + + @Test + fun migrate_retrieveProfileAvatarJob() { + val testData = JobData( + factoryKey = "RetrieveProfileAvatarJob", + queueKey = "RetrieveProfileAvatarJob+16101234567", + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder() + .putString("address", "+16101234567") + .putString("profile_avatar", "abc") + .serialize() + ) + mockRecipientResolve("+16101234567", 1) + + val subject = RecipientIdJobMigration(mockk()) + val converted = subject.migrate(testData) + val data = JsonJobData.deserialize(converted.data) + + assertEquals("RetrieveProfileAvatarJob", converted.factoryKey) + assertEquals("RetrieveProfileAvatarJob::" + RecipientId.from(1).toQueueKey(), converted.queueKey) + assertEquals("1", data.getString("recipient")) + + RetrieveProfileAvatarJob.Factory().create(Job.Parameters.Builder().build(), converted.data) + } + + @Test + fun migrate_multiDeviceReadUpdateJob_empty() { + val testData = JobData( + factoryKey = "MultiDeviceReadUpdateJob", + queueKey = null, + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder() + .putStringArray("message_ids", arrayOfNulls(0)) + .serialize() + ) + + val subject = RecipientIdJobMigration(mockk()) + val converted = subject.migrate(testData) + val data = JsonJobData.deserialize(converted.data) + + assertEquals("MultiDeviceReadUpdateJob", converted.factoryKey) + assertNull(converted.queueKey) + assertEquals(0, data.getStringArray("message_ids").size) + + MultiDeviceReadUpdateJob.Factory().create(Job.Parameters.Builder().build(), converted.data) + } + + @Test + fun migrate_multiDeviceReadUpdateJob_twoIds() { + val id1 = OldSerializableSyncMessageId("+16101234567", 1) + val id2 = OldSerializableSyncMessageId("+16101112222", 2) + + val testData = JobData( + factoryKey = "MultiDeviceReadUpdateJob", + queueKey = null, + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder() + .putStringArray("message_ids", arrayOf(JsonUtils.toJson(id1), JsonUtils.toJson(id2))) + .serialize() + ) + mockRecipientResolve("+16101234567", 1) + mockRecipientResolve("+16101112222", 2) + + val subject = RecipientIdJobMigration(mockk()) + val converted = subject.migrate(testData) + val data = JsonJobData.deserialize(converted.data) + + assertEquals("MultiDeviceReadUpdateJob", converted.factoryKey) + assertNull(converted.queueKey) + + val updated = data.getStringArray("message_ids") + assertEquals(2, updated.size) + + assertEquals(JsonUtils.toJson(NewSerializableSyncMessageId("1", 1)), updated[0]) + assertEquals(JsonUtils.toJson(NewSerializableSyncMessageId("2", 2)), updated[1]) + + MultiDeviceReadUpdateJob.Factory().create(Job.Parameters.Builder().build(), converted.data) + } + + @Test + fun migrate_pushMediaSendJob() { + val testData = JobData( + factoryKey = "PushMediaSendJob", + queueKey = "+16101234567", + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder().putLong("message_id", 1).serialize() + ) + mockRecipientResolve("+16101234567", 1) + + val subject = RecipientIdJobMigration(mockk()) + val converted = subject.migrate(testData) + val data = JsonJobData.deserialize(converted.data) + + assertEquals("PushMediaSendJob", converted.factoryKey) + assertEquals(RecipientId.from(1).toQueueKey(), converted.queueKey) + assertEquals(1, data.getLong("message_id")) + + IndividualSendJob.Factory().create(Job.Parameters.Builder().build(), converted.data) + } + + private fun mockRecipientResolve(address: String, recipientId: Long) { + every { Recipient.external(any(), address) } returns mockRecipient(recipientId) + } + + private fun mockRecipient(id: Long): Recipient { + return mockk { + every { this@mockk.id } returns RecipientId.from(id) + } + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SendReadReceiptsJobMigrationTest.java b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SendReadReceiptsJobMigrationTest.java deleted file mode 100644 index 96a2fbb535..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SendReadReceiptsJobMigrationTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.migrations; - -import org.junit.Test; -import org.thoughtcrime.securesms.database.MessageTable; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; -import org.thoughtcrime.securesms.jobmanager.JobMigration; -import org.thoughtcrime.securesms.jobs.SendReadReceiptJob; -import org.thoughtcrime.securesms.recipients.RecipientId; - -import java.util.ArrayList; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class SendReadReceiptsJobMigrationTest { - - private final MessageTable mockDatabase = mock(MessageTable.class); - private final SendReadReceiptsJobMigration testSubject = new SendReadReceiptsJobMigration(mockDatabase); - - @Test - public void givenSendReadReceiptJobDataWithoutThreadIdAndThreadIdFound_whenIMigrate_thenIInsertThreadId() { - // GIVEN - SendReadReceiptJob job = new SendReadReceiptJob(1, RecipientId.from(2), new ArrayList<>(), new ArrayList<>()); - JobMigration.JobData jobData = new JobMigration.JobData(job.getFactoryKey(), - "asdf", - -1, - -1, - new JsonJobData.Builder() - .putString("recipient", RecipientId.from(2).serialize()) - .putLongArray("message_ids", new long[]{1, 2, 3, 4, 5}) - .putLong("timestamp", 292837649).serialize()); - when(mockDatabase.getThreadIdForMessage(anyLong())).thenReturn(1234L); - - // WHEN - JobMigration.JobData result = testSubject.migrate(jobData); - JsonJobData data = JsonJobData.deserialize(result.getData()); - - // THEN - assertEquals(1234L, data.getLong("thread")); - assertEquals(RecipientId.from(2).serialize(), data.getString("recipient")); - assertTrue(data.hasLongArray("message_ids")); - assertTrue(data.hasLong("timestamp")); - } - - @Test - public void givenSendReadReceiptJobDataWithoutThreadIdAndThreadIdNotFound_whenIMigrate_thenIGetAFailingJob() { - // GIVEN - SendReadReceiptJob job = new SendReadReceiptJob(1, RecipientId.from(2), new ArrayList<>(), new ArrayList<>()); - JobMigration.JobData jobData = new JobMigration.JobData(job.getFactoryKey(), - "asdf", - -1, - -1, - new JsonJobData.Builder() - .putString("recipient", RecipientId.from(2).serialize()) - .putLongArray("message_ids", new long[]{}) - .putLong("timestamp", 292837649).serialize()); - when(mockDatabase.getThreadIdForMessage(anyLong())).thenReturn(-1L); - - // WHEN - JobMigration.JobData result = testSubject.migrate(jobData); - - // THEN - assertEquals("FailingJob", result.getFactoryKey()); - } - - @Test - public void givenSendReadReceiptJobDataWithThreadId_whenIMigrate_thenIDoNotReplace() { - // GIVEN - SendReadReceiptJob job = new SendReadReceiptJob(1, RecipientId.from(2), new ArrayList<>(), new ArrayList<>()); - JobMigration.JobData jobData = new JobMigration.JobData(job.getFactoryKey(), "asdf", -1, -1, job.serialize()); - - // WHEN - JobMigration.JobData result = testSubject.migrate(jobData); - - // THEN - assertEquals(jobData, result); - } - - @Test - public void givenSomeOtherJobDataWithThreadId_whenIMigrate_thenIDoNotReplace() { - // GIVEN - JobMigration.JobData jobData = new JobMigration.JobData("SomeOtherJob", "asdf", -1, -1, new JsonJobData.Builder().putLong("thread", 1).serialize()); - - // WHEN - JobMigration.JobData result = testSubject.migrate(jobData); - - // THEN - assertEquals(jobData, result); - } - - @Test - public void givenSomeOtherJobDataWithoutThreadId_whenIMigrate_thenIDoNotReplace() { - // GIVEN - JobMigration.JobData jobData = new JobMigration.JobData("SomeOtherJob", "asdf", -1, -1, new JsonJobData.Builder().serialize()); - - // WHEN - JobMigration.JobData result = testSubject.migrate(jobData); - - // THEN - assertEquals(jobData, result); - } - - -} \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SendReadReceiptsJobMigrationTest.kt b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SendReadReceiptsJobMigrationTest.kt new file mode 100644 index 0000000000..754fef32fe --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SendReadReceiptsJobMigrationTest.kt @@ -0,0 +1,104 @@ +package org.thoughtcrime.securesms.jobmanager.migrations + +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.thoughtcrime.securesms.database.MessageTable +import org.thoughtcrime.securesms.jobmanager.JobMigration.JobData +import org.thoughtcrime.securesms.jobmanager.JsonJobData +import org.thoughtcrime.securesms.jobs.SendReadReceiptJob +import org.thoughtcrime.securesms.recipients.RecipientId + +class SendReadReceiptsJobMigrationTest { + private val mockDatabase = mockk() + private val testSubject = SendReadReceiptsJobMigration(mockDatabase) + + @Test + fun givenSendReadReceiptJobDataWithoutThreadIdAndThreadIdFound_whenIMigrate_thenIInsertThreadId() { + // GIVEN + val job = SendReadReceiptJob(1, RecipientId.from(2), ArrayList(), ArrayList()) + val jobData = JobData( + factoryKey = job.factoryKey, + queueKey = "asdf", + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder() + .putString("recipient", RecipientId.from(2).serialize()) + .putLongArray("message_ids", longArrayOf(1, 2, 3, 4, 5)) + .putLong("timestamp", 292837649).serialize() + ) + every { mockDatabase.getThreadIdForMessage(any()) } returns 1234L + + // WHEN + val result = testSubject.migrate(jobData) + val data = JsonJobData.deserialize(result.data) + + // THEN + assertEquals(1234L, data.getLong("thread")) + assertEquals(RecipientId.from(2).serialize(), data.getString("recipient")) + assertTrue(data.hasLongArray("message_ids")) + assertTrue(data.hasLong("timestamp")) + } + + @Test + fun givenSendReadReceiptJobDataWithoutThreadIdAndThreadIdNotFound_whenIMigrate_thenIGetAFailingJob() { + // GIVEN + val job = SendReadReceiptJob(1, RecipientId.from(2), ArrayList(), ArrayList()) + val jobData = JobData( + factoryKey = job.factoryKey, + queueKey = "asdf", + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder() + .putString("recipient", RecipientId.from(2).serialize()) + .putLongArray("message_ids", longArrayOf()) + .putLong("timestamp", 292837649).serialize() + ) + every { mockDatabase.getThreadIdForMessage(any()) } returns -1L + + // WHEN + val result = testSubject.migrate(jobData) + + // THEN + assertEquals("FailingJob", result.factoryKey) + } + + @Test + fun givenSendReadReceiptJobDataWithThreadId_whenIMigrate_thenIDoNotReplace() { + // GIVEN + val job = SendReadReceiptJob(1, RecipientId.from(2), ArrayList(), ArrayList()) + val jobData = JobData(job.factoryKey, "asdf", -1, -1, job.serialize()) + + // WHEN + val result = testSubject.migrate(jobData) + + // THEN + assertEquals(jobData, result) + } + + @Test + fun givenSomeOtherJobDataWithThreadId_whenIMigrate_thenIDoNotReplace() { + // GIVEN + val jobData = JobData("SomeOtherJob", "asdf", -1, -1, JsonJobData.Builder().putLong("thread", 1).serialize()) + + // WHEN + val result = testSubject.migrate(jobData) + + // THEN + assertEquals(jobData, result) + } + + @Test + fun givenSomeOtherJobDataWithoutThreadId_whenIMigrate_thenIDoNotReplace() { + // GIVEN + val jobData = JobData("SomeOtherJob", "asdf", -1, -1, JsonJobData.Builder().serialize()) + + // WHEN + val result = testSubject.migrate(jobData) + + // THEN + assertEquals(jobData, result) + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SenderKeyDistributionSendJobRecipientMigrationTest.java b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SenderKeyDistributionSendJobRecipientMigrationTest.java deleted file mode 100644 index 0ec33c0090..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SenderKeyDistributionSendJobRecipientMigrationTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.migrations; - -import org.junit.Test; -import org.thoughtcrime.securesms.database.GroupTable; -import org.thoughtcrime.securesms.database.model.GroupRecord; -import org.thoughtcrime.securesms.groups.GroupId; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; -import org.thoughtcrime.securesms.jobmanager.JobMigration; -import org.thoughtcrime.securesms.jobs.FailingJob; -import org.thoughtcrime.securesms.jobs.SenderKeyDistributionSendJob; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.util.Util; - -import java.util.Optional; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class SenderKeyDistributionSendJobRecipientMigrationTest { - - private final GroupTable mockDatabase = mock(GroupTable.class); - private final SenderKeyDistributionSendJobRecipientMigration testSubject = new SenderKeyDistributionSendJobRecipientMigration(mockDatabase); - - private static final GroupId GROUP_ID = GroupId.pushOrThrow(Util.getSecretBytes(32)); - - @Test - public void normalMigration() { - // GIVEN - JobMigration.JobData jobData = new JobMigration.JobData(SenderKeyDistributionSendJob.KEY, - "asdf", - -1, - -1, - new JsonJobData.Builder() - .putString("recipient_id", RecipientId.from(1).serialize()) - .putBlobAsString("group_id", GROUP_ID.getDecodedId()) - .serialize()); - - GroupRecord mockGroup = mock(GroupRecord.class); - when(mockGroup.getRecipientId()).thenReturn(RecipientId.from(2)); - when(mockDatabase.getGroup(GROUP_ID)).thenReturn(Optional.of(mockGroup)); - - // WHEN - JobMigration.JobData result = testSubject.migrate(jobData); - JsonJobData data = JsonJobData.deserialize(result.getData()); - - // THEN - assertEquals(RecipientId.from(1).serialize(), data.getString("recipient_id")); - assertEquals(RecipientId.from(2).serialize(), data.getString("thread_recipient_id")); - } - - @Test - public void cannotFindGroup() { - // GIVEN - JobMigration.JobData jobData = new JobMigration.JobData(SenderKeyDistributionSendJob.KEY, - "asdf", - -1, - -1, - new JsonJobData.Builder() - .putString("recipient_id", RecipientId.from(1).serialize()) - .putBlobAsString("group_id", GROUP_ID.getDecodedId()) - .serialize()); - - // WHEN - JobMigration.JobData result = testSubject.migrate(jobData); - - // THEN - assertEquals(FailingJob.KEY, result.getFactoryKey()); - } - - @Test - public void missingGroupId() { - // GIVEN - JobMigration.JobData jobData = new JobMigration.JobData(SenderKeyDistributionSendJob.KEY, - "asdf", - -1, - -1, - new JsonJobData.Builder() - .putString("recipient_id", RecipientId.from(1).serialize()) - .serialize()); - - // WHEN - JobMigration.JobData result = testSubject.migrate(jobData); - - // THEN - assertEquals(FailingJob.KEY, result.getFactoryKey()); - } -} \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SenderKeyDistributionSendJobRecipientMigrationTest.kt b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SenderKeyDistributionSendJobRecipientMigrationTest.kt new file mode 100644 index 0000000000..a30abebf02 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SenderKeyDistributionSendJobRecipientMigrationTest.kt @@ -0,0 +1,94 @@ +package org.thoughtcrime.securesms.jobmanager.migrations + +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Test +import org.thoughtcrime.securesms.database.GroupTable +import org.thoughtcrime.securesms.database.model.GroupRecord +import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.jobmanager.JobMigration.JobData +import org.thoughtcrime.securesms.jobmanager.JsonJobData +import org.thoughtcrime.securesms.jobs.FailingJob +import org.thoughtcrime.securesms.jobs.SenderKeyDistributionSendJob +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.util.Util +import java.util.Optional + +class SenderKeyDistributionSendJobRecipientMigrationTest { + private val mockDatabase = mockk(relaxed = true) + private val testSubject = SenderKeyDistributionSendJobRecipientMigration(mockDatabase) + + @Test + fun normalMigration() { + // GIVEN + val jobData = JobData( + factoryKey = SenderKeyDistributionSendJob.KEY, + queueKey = "asdf", + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder() + .putString("recipient_id", RecipientId.from(1).serialize()) + .putBlobAsString("group_id", GROUP_ID.decodedId) + .serialize() + ) + + val mockGroup = mockk { + every { recipientId } returns RecipientId.from(2) + } + every { mockDatabase.getGroup(GROUP_ID) } returns Optional.of(mockGroup) + + // WHEN + val result = testSubject.migrate(jobData) + val data = JsonJobData.deserialize(result.data) + + // THEN + assertEquals(RecipientId.from(1).serialize(), data.getString("recipient_id")) + assertEquals(RecipientId.from(2).serialize(), data.getString("thread_recipient_id")) + } + + @Test + fun cannotFindGroup() { + // GIVEN + val jobData = JobData( + factoryKey = SenderKeyDistributionSendJob.KEY, + queueKey = "asdf", + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder() + .putString("recipient_id", RecipientId.from(1).serialize()) + .putBlobAsString("group_id", GROUP_ID.decodedId) + .serialize() + ) + + // WHEN + val result = testSubject.migrate(jobData) + + // THEN + assertEquals(FailingJob.KEY, result.factoryKey) + } + + @Test + fun missingGroupId() { + // GIVEN + val jobData = JobData( + factoryKey = SenderKeyDistributionSendJob.KEY, + queueKey = "asdf", + maxAttempts = -1, + lifespan = -1, + data = JsonJobData.Builder() + .putString("recipient_id", RecipientId.from(1).serialize()) + .serialize() + ) + + // WHEN + val result = testSubject.migrate(jobData) + + // THEN + assertEquals(FailingJob.KEY, result.factoryKey) + } + + companion object { + private val GROUP_ID: GroupId = GroupId.pushOrThrow(Util.getSecretBytes(32)) + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/payments/GeographicalRestrictionsTest.java b/app/src/test/java/org/thoughtcrime/securesms/payments/GeographicalRestrictionsTest.java deleted file mode 100644 index a346e8b985..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/payments/GeographicalRestrictionsTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.thoughtcrime.securesms.payments; - -import org.junit.Rule; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.thoughtcrime.securesms.util.RemoteConfig; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.when; - -public final class GeographicalRestrictionsTest { - - @Rule - public MockitoRule rule = MockitoJUnit.rule(); - - @Mock - private MockedStatic remoteConfigMockedStatic; - - @Test - public void e164Allowed_general() { - when(RemoteConfig.paymentsCountryBlocklist()).thenReturn(""); - assertTrue(GeographicalRestrictions.e164Allowed("+15551234567")); - - when(RemoteConfig.paymentsCountryBlocklist()).thenReturn("1"); - assertFalse(GeographicalRestrictions.e164Allowed("+15551234567")); - - when(RemoteConfig.paymentsCountryBlocklist()).thenReturn("1,44"); - assertFalse(GeographicalRestrictions.e164Allowed("+15551234567")); - assertFalse(GeographicalRestrictions.e164Allowed("+445551234567")); - assertTrue(GeographicalRestrictions.e164Allowed("+525551234567")); - - when(RemoteConfig.paymentsCountryBlocklist()).thenReturn("1 234,44"); - assertFalse(GeographicalRestrictions.e164Allowed("+12341234567")); - assertTrue(GeographicalRestrictions.e164Allowed("+15551234567")); - assertTrue(GeographicalRestrictions.e164Allowed("+525551234567")); - assertTrue(GeographicalRestrictions.e164Allowed("+2345551234567")); - } - - @Test - public void e164Allowed_nullNotAllowed() { - assertFalse(GeographicalRestrictions.e164Allowed(null)); - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/payments/GeographicalRestrictionsTest.kt b/app/src/test/java/org/thoughtcrime/securesms/payments/GeographicalRestrictionsTest.kt new file mode 100644 index 0000000000..7c533adf68 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/payments/GeographicalRestrictionsTest.kt @@ -0,0 +1,37 @@ +package org.thoughtcrime.securesms.payments + +import io.mockk.every +import io.mockk.mockkStatic +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.thoughtcrime.securesms.util.RemoteConfig + +class GeographicalRestrictionsTest { + @Test + fun e164Allowed_general() { + mockkStatic(RemoteConfig::class) { + every { RemoteConfig.paymentsCountryBlocklist } returns "" + assertTrue(GeographicalRestrictions.e164Allowed("+15551234567")) + + every { RemoteConfig.paymentsCountryBlocklist } returns "1" + assertFalse(GeographicalRestrictions.e164Allowed("+15551234567")) + + every { RemoteConfig.paymentsCountryBlocklist } returns "1,44" + assertFalse(GeographicalRestrictions.e164Allowed("+15551234567")) + assertFalse(GeographicalRestrictions.e164Allowed("+445551234567")) + assertTrue(GeographicalRestrictions.e164Allowed("+525551234567")) + + every { RemoteConfig.paymentsCountryBlocklist } returns "1 234,44" + assertFalse(GeographicalRestrictions.e164Allowed("+12341234567")) + assertTrue(GeographicalRestrictions.e164Allowed("+15551234567")) + assertTrue(GeographicalRestrictions.e164Allowed("+525551234567")) + assertTrue(GeographicalRestrictions.e164Allowed("+2345551234567")) + } + } + + @Test + fun e164Allowed_nullNotAllowed() { + assertFalse(GeographicalRestrictions.e164Allowed(null)) + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/fcm/PushChallengeRequestTest.java b/app/src/test/java/org/thoughtcrime/securesms/registration/fcm/PushChallengeRequestTest.java deleted file mode 100644 index 8af17977f5..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/fcm/PushChallengeRequestTest.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.registration.fcm; - -import android.app.Application; -import android.os.AsyncTask; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; -import org.whispersystems.signalservice.api.SignalServiceAccountManager; - -import java.io.IOException; -import java.util.Optional; - -import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.hamcrest.Matchers.lessThan; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE, application = Application.class) -public final class PushChallengeRequestTest { - - @Test - public void getPushChallengeBlocking_returns_absent_if_times_out() { - SignalServiceAccountManager signal = mock(SignalServiceAccountManager.class); - - Optional challenge = PushChallengeRequest.getPushChallengeBlocking(signal, "session ID", Optional.of("token"), 50L); - - assertFalse(challenge.isPresent()); - } - - @Test - public void getPushChallengeBlocking_waits_for_specified_period() { - SignalServiceAccountManager signal = mock(SignalServiceAccountManager.class); - - long startTime = System.currentTimeMillis(); - PushChallengeRequest.getPushChallengeBlocking(signal, "session ID", Optional.of("token"), 250L); - long duration = System.currentTimeMillis() - startTime; - - assertThat(duration, greaterThanOrEqualTo(250L)); - } - - @Test - public void getPushChallengeBlocking_completes_fast_if_posted_to_event_bus() throws IOException { - SignalServiceAccountManager signal = mock(SignalServiceAccountManager.class); - doAnswer(invocation -> { - AsyncTask.execute(() -> PushChallengeRequest.postChallengeResponse("CHALLENGE")); - return null; - }).when(signal).requestRegistrationPushChallenge("session ID", "token"); - - long startTime = System.currentTimeMillis(); - Optional challenge = PushChallengeRequest.getPushChallengeBlocking(signal, "session ID", Optional.of("token"), 500L); - long duration = System.currentTimeMillis() - startTime; - - assertThat(duration, lessThan(500L)); - verify(signal).requestRegistrationPushChallenge("session ID", "token"); - verifyNoMoreInteractions(signal); - - assertTrue(challenge.isPresent()); - assertEquals("CHALLENGE", challenge.get()); - } - - @Test - public void getPushChallengeBlocking_returns_fast_if_no_fcm_token_supplied() { - SignalServiceAccountManager signal = mock(SignalServiceAccountManager.class); - - long startTime = System.currentTimeMillis(); - PushChallengeRequest.getPushChallengeBlocking(signal, "session ID", Optional.empty(), 500L); - long duration = System.currentTimeMillis() - startTime; - - assertThat(duration, lessThan(500L)); - } - - @Test - public void getPushChallengeBlocking_returns_absent_if_no_fcm_token_supplied() { - SignalServiceAccountManager signal = mock(SignalServiceAccountManager.class); - - Optional challenge = PushChallengeRequest.getPushChallengeBlocking(signal, "session ID", Optional.empty(), 500L); - - verifyNoInteractions(signal); - assertFalse(challenge.isPresent()); - } - - @Test - public void getPushChallengeBlocking_returns_absent_if_any_IOException_is_thrown() throws IOException { - SignalServiceAccountManager signal = mock(SignalServiceAccountManager.class); - - doThrow(new IOException()).when(signal).requestRegistrationPushChallenge(any(), any()); - - Optional challenge = PushChallengeRequest.getPushChallengeBlocking(signal, "session ID", Optional.of("token"), 500L); - - assertFalse(challenge.isPresent()); - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/fcm/PushChallengeRequestTest.kt b/app/src/test/java/org/thoughtcrime/securesms/registration/fcm/PushChallengeRequestTest.kt new file mode 100644 index 0000000000..ba424fafd3 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/fcm/PushChallengeRequestTest.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.thoughtcrime.securesms.registration.fcm + +import android.app.Application +import android.os.AsyncTask +import io.mockk.called +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.greaterThanOrEqualTo +import org.hamcrest.Matchers.lessThan +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.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.whispersystems.signalservice.api.SignalServiceAccountManager +import java.io.IOException +import java.util.Optional + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) +class PushChallengeRequestTest { + @Test + fun pushChallengeBlocking_returns_absent_if_times_out() { + val signal = mockk(relaxUnitFun = true) + + val challenge = PushChallengeRequest.getPushChallengeBlocking(signal, "session ID", Optional.of("token"), 50L) + + assertFalse(challenge.isPresent) + } + + @Test + fun pushChallengeBlocking_waits_for_specified_period() { + val signal = mockk(relaxUnitFun = true) + + val startTime = System.currentTimeMillis() + PushChallengeRequest.getPushChallengeBlocking(signal, "session ID", Optional.of("token"), 250L) + val duration = System.currentTimeMillis() - startTime + + assertThat(duration, greaterThanOrEqualTo(250L)) + } + + @Test + fun pushChallengeBlocking_completes_fast_if_posted_to_event_bus() { + val signal = mockk { + every { + requestRegistrationPushChallenge("session ID", "token") + } answers { + AsyncTask.execute { PushChallengeRequest.postChallengeResponse("CHALLENGE") } + } + } + + val startTime = System.currentTimeMillis() + val challenge = PushChallengeRequest.getPushChallengeBlocking(signal, "session ID", Optional.of("token"), 500L) + val duration = System.currentTimeMillis() - startTime + + assertThat(duration, lessThan(500L)) + verify { signal.requestRegistrationPushChallenge("session ID", "token") } + confirmVerified(signal) + + assertTrue(challenge.isPresent) + assertEquals("CHALLENGE", challenge.get()) + } + + @Test + fun pushChallengeBlocking_returns_fast_if_no_fcm_token_supplied() { + val signal = mockk() + + val startTime = System.currentTimeMillis() + PushChallengeRequest.getPushChallengeBlocking(signal, "session ID", Optional.empty(), 500L) + val duration = System.currentTimeMillis() - startTime + + assertThat(duration, lessThan(500L)) + } + + @Test + fun pushChallengeBlocking_returns_absent_if_no_fcm_token_supplied() { + val signal = mockk() + + val challenge = PushChallengeRequest.getPushChallengeBlocking(signal, "session ID", Optional.empty(), 500L) + + verify { signal wasNot called } + assertFalse(challenge.isPresent) + } + + @Test + fun pushChallengeBlocking_returns_absent_if_any_IOException_is_thrown() { + val signal = mockk { + every { requestRegistrationPushChallenge(any(), any()) } throws IOException() + } + + val challenge = PushChallengeRequest.getPushChallengeBlocking(signal, "session ID", Optional.of("token"), 500L) + + assertFalse(challenge.isPresent) + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashKbsDataTest.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashKbsDataTest.java deleted file mode 100644 index 5457bdfceb..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashKbsDataTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.thoughtcrime.securesms.registration.v2; - -import org.junit.Test; -import org.signal.core.util.StreamUtil; -import org.signal.libsignal.svr2.PinHash; -import org.thoughtcrime.securesms.registration.testdata.KbsTestVector; -import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; -import org.whispersystems.signalservice.api.kbs.KbsData; -import org.whispersystems.signalservice.api.kbs.MasterKey; -import org.whispersystems.signalservice.api.kbs.PinHashUtil; -import org.whispersystems.signalservice.internal.util.JsonUtil; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.thoughtcrime.securesms.testutil.SecureRandomTestUtil.mockRandom; - -public final class PinHashKbsDataTest { - - @Test - public void vectors_createNewKbsData() throws IOException { - for (KbsTestVector vector : getKbsTestVectorList()) { - PinHash pinHash = fromArgon2Hash(vector.getArgon2Hash()); - - KbsData kbsData = PinHashUtil.createNewKbsData(pinHash, MasterKey.createNew(mockRandom(vector.getMasterKey()))); - - assertArrayEquals(vector.getMasterKey(), kbsData.getMasterKey().serialize()); - assertArrayEquals(vector.getIvAndCipher(), kbsData.getCipherText()); - assertArrayEquals(vector.getKbsAccessKey(), kbsData.getKbsAccessKey()); - assertEquals(vector.getRegistrationLock(), kbsData.getMasterKey().deriveRegistrationLock()); - } - } - - @Test - public void vectors_decryptKbsDataIVCipherText() throws IOException, InvalidCiphertextException { - for (KbsTestVector vector : getKbsTestVectorList()) { - PinHash hashedPin = fromArgon2Hash(vector.getArgon2Hash()); - - KbsData kbsData = PinHashUtil.decryptSvrDataIVCipherText(hashedPin, vector.getIvAndCipher()); - - assertArrayEquals(vector.getMasterKey(), kbsData.getMasterKey().serialize()); - assertArrayEquals(vector.getIvAndCipher(), kbsData.getCipherText()); - assertArrayEquals(vector.getKbsAccessKey(), kbsData.getKbsAccessKey()); - assertEquals(vector.getRegistrationLock(), kbsData.getMasterKey().deriveRegistrationLock()); - } - } - - private static KbsTestVector[] getKbsTestVectorList() throws IOException { - try (InputStream resourceAsStream = ClassLoader.getSystemClassLoader().getResourceAsStream("data/kbs_vectors.json")) { - - KbsTestVector[] data = JsonUtil.fromJson(StreamUtil.readFullyAsString(resourceAsStream), KbsTestVector[].class); - - assertTrue(data.length > 0); - - return data; - } - } - - public static PinHash fromArgon2Hash(byte[] argon2Hash64) { - if (argon2Hash64.length != 64) throw new AssertionError(); - - byte[] K = Arrays.copyOfRange(argon2Hash64, 0, 32); - byte[] kbsAccessKey = Arrays.copyOfRange(argon2Hash64, 32, 64); - - PinHash mocked = mock(PinHash.class); - when(mocked.encryptionKey()).thenReturn(K); - when(mocked.accessKey()).thenReturn(kbsAccessKey); - - return mocked; - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashKbsDataTest.kt b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashKbsDataTest.kt new file mode 100644 index 0000000000..96f4783998 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashKbsDataTest.kt @@ -0,0 +1,72 @@ +package org.thoughtcrime.securesms.registration.v2 + +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.signal.core.util.StreamUtil +import org.signal.libsignal.svr2.PinHash +import org.thoughtcrime.securesms.registration.v2.testdata.KbsTestVector +import org.thoughtcrime.securesms.testutil.SecureRandomTestUtil +import org.whispersystems.signalservice.api.kbs.MasterKey +import org.whispersystems.signalservice.api.kbs.PinHashUtil.createNewKbsData +import org.whispersystems.signalservice.api.kbs.PinHashUtil.decryptSvrDataIVCipherText +import org.whispersystems.signalservice.internal.util.JsonUtil + +class PinHashKbsDataTest { + @Test + fun vectors_createNewKbsData() { + for (vector in kbsTestVectorList) { + val pinHash = fromArgon2Hash(vector.argon2Hash) + + val kbsData = createNewKbsData(pinHash, MasterKey.createNew(SecureRandomTestUtil.mockRandom(vector.masterKey))) + + assertArrayEquals(vector.masterKey, kbsData.masterKey.serialize()) + assertArrayEquals(vector.ivAndCipher, kbsData.cipherText) + assertArrayEquals(vector.kbsAccessKey, kbsData.kbsAccessKey) + assertEquals(vector.registrationLock, kbsData.masterKey.deriveRegistrationLock()) + } + } + + @Test + fun vectors_decryptKbsDataIVCipherText() { + for (vector in kbsTestVectorList) { + val hashedPin = fromArgon2Hash(vector.argon2Hash) + + val kbsData = decryptSvrDataIVCipherText(hashedPin, vector.ivAndCipher) + + assertArrayEquals(vector.masterKey, kbsData.masterKey.serialize()) + assertArrayEquals(vector.ivAndCipher, kbsData.cipherText) + assertArrayEquals(vector.kbsAccessKey, kbsData.kbsAccessKey) + assertEquals(vector.registrationLock, kbsData.masterKey.deriveRegistrationLock()) + } + } + + companion object { + private val kbsTestVectorList: Array + get() { + ClassLoader.getSystemClassLoader().getResourceAsStream("data/kbs_vectors.json").use { resourceAsStream -> + val data: Array = JsonUtil.fromJson( + StreamUtil.readFullyAsString(resourceAsStream), + Array::class.java + ) + assertTrue(data.isNotEmpty()) + return data + } + } + + fun fromArgon2Hash(argon2Hash64: ByteArray): PinHash { + if (argon2Hash64.size != 64) throw AssertionError() + + val k = argon2Hash64.copyOfRange(0, 32) + val kbsAccessKey = argon2Hash64.copyOfRange(32, 64) + + return mockk { + every { encryptionKey() } returns k + every { accessKey() } returns kbsAccessKey + } + } + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/KbsTestVector.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/KbsTestVector.java deleted file mode 100644 index 4e5826fcca..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/KbsTestVector.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.thoughtcrime.securesms.registration.testdata; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -import org.thoughtcrime.securesms.testutil.HexDeserializer; - -public final class KbsTestVector { - - @JsonProperty("backup_id") - @JsonDeserialize(using = HexDeserializer.class) - private byte[] backupId; - - @JsonProperty("argon2_hash") - @JsonDeserialize(using = HexDeserializer.class) - private byte[] argon2Hash; - - @JsonProperty("pin") - private String pin; - - @JsonProperty("registration_lock") - private String registrationLock; - - @JsonProperty("master_key") - @JsonDeserialize(using = HexDeserializer.class) - private byte[] masterKey; - - @JsonProperty("kbs_access_key") - @JsonDeserialize(using = HexDeserializer.class) - private byte[] kbsAccessKey; - - @JsonProperty("iv_and_cipher") - @JsonDeserialize(using = HexDeserializer.class) - private byte[] ivAndCipher; - - public byte[] getBackupId() { - return backupId; - } - - public byte[] getArgon2Hash() { - return argon2Hash; - } - - public String getPin() { - return pin; - } - - public String getRegistrationLock() { - return registrationLock; - } - - public byte[] getMasterKey() { - return masterKey; - } - - public byte[] getKbsAccessKey() { - return kbsAccessKey; - } - - public byte[] getIvAndCipher() { - return ivAndCipher; - } -} \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/KbsTestVector.kt b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/KbsTestVector.kt new file mode 100644 index 0000000000..de1f646180 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/KbsTestVector.kt @@ -0,0 +1,62 @@ +package org.thoughtcrime.securesms.registration.v2.testdata + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import org.thoughtcrime.securesms.testutil.HexDeserializer + +data class KbsTestVector( + @JsonProperty("backup_id") + @JsonDeserialize(using = HexDeserializer::class) + val backupId: ByteArray, + + @JsonProperty("argon2_hash") + @JsonDeserialize(using = HexDeserializer::class) + val argon2Hash: ByteArray, + + @JsonProperty("pin") + val pin: String? = null, + + @JsonProperty("registration_lock") + val registrationLock: String? = null, + + @JsonProperty("master_key") + @JsonDeserialize(using = HexDeserializer::class) + val masterKey: ByteArray, + + @JsonProperty("kbs_access_key") + @JsonDeserialize(using = HexDeserializer::class) + val kbsAccessKey: ByteArray, + + @JsonProperty("iv_and_cipher") + @JsonDeserialize(using = HexDeserializer::class) + val ivAndCipher: ByteArray +) { + // equals() and hashCode() are still recommended on data class because of ByteArray usage + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as KbsTestVector + + if (!backupId.contentEquals(other.backupId)) return false + if (!argon2Hash.contentEquals(other.argon2Hash)) return false + if (pin != other.pin) return false + if (registrationLock != other.registrationLock) return false + if (!masterKey.contentEquals(other.masterKey)) return false + if (!kbsAccessKey.contentEquals(other.kbsAccessKey)) return false + if (!ivAndCipher.contentEquals(other.ivAndCipher)) return false + + return true + } + + override fun hashCode(): Int { + var result = backupId.contentHashCode() + result = 31 * result + argon2Hash.contentHashCode() + result = 31 * result + (pin?.hashCode() ?: 0) + result = 31 * result + (registrationLock?.hashCode() ?: 0) + result = 31 * result + masterKey.contentHashCode() + result = 31 * result + kbsAccessKey.contentHashCode() + result = 31 * result + ivAndCipher.contentHashCode() + return result + } +}