Convert more tests to kotlin.

Resolves #13829
This commit is contained in:
Jameson Williams 2024-12-06 23:45:13 -06:00 committed by Greyson Parrelli
parent 574d6c51ab
commit c2aceb2bd1
12 changed files with 965 additions and 1038 deletions

View file

@ -1,184 +0,0 @@
package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientCreator;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class CallParticipantListUpdateTest {
@Test
public void givenEmptySets_thenExpectNoChanges() {
// GIVEN
Set<CallParticipantListUpdate.Wrapper> added = Collections.emptySet();
Set<CallParticipantListUpdate.Wrapper> removed = Collections.emptySet();
CallParticipantListUpdate update = new CallParticipantListUpdate(added, removed);
// THEN
assertTrue(update.hasNoChanges());
assertFalse(update.hasSingleChange());
}
@Test
public void givenOneEmptySet_thenExpectMultipleChanges() {
// GIVEN
Set<CallParticipantListUpdate.Wrapper> added = new HashSet<>(Arrays.asList(createWrappers(1, 2, 3)));
Set<CallParticipantListUpdate.Wrapper> removed = Collections.emptySet();
CallParticipantListUpdate update = new CallParticipantListUpdate(added, removed);
// THEN
assertFalse(update.hasNoChanges());
assertFalse(update.hasSingleChange());
}
@Test
public void givenNoEmptySets_thenExpectMultipleChanges() {
// GIVEN
Set<CallParticipantListUpdate.Wrapper> added = new HashSet<>(Arrays.asList(createWrappers(1, 2, 3)));
Set<CallParticipantListUpdate.Wrapper> removed = new HashSet<>(Arrays.asList(createWrappers(4, 5, 6)));
CallParticipantListUpdate update = new CallParticipantListUpdate(added, removed);
// THEN
assertFalse(update.hasNoChanges());
assertFalse(update.hasSingleChange());
}
@Test
public void givenOneSetWithSingleItemAndAnEmptySet_thenExpectSingleChange() {
// GIVEN
Set<CallParticipantListUpdate.Wrapper> added = new HashSet<>(Arrays.asList(createWrappers(1)));
Set<CallParticipantListUpdate.Wrapper> removed = Collections.emptySet();
CallParticipantListUpdate update = new CallParticipantListUpdate(added, removed);
// THEN
assertFalse(update.hasNoChanges());
assertTrue(update.hasSingleChange());
}
@Test
public void whenFirstListIsAdded_thenIExpectAnUpdateWithAllItemsFromListAdded() {
// GIVEN
List<CallParticipant> newList = createParticipants(1, 2, 3, 4, 5);
// WHEN
CallParticipantListUpdate update = CallParticipantListUpdate.computeDeltaUpdate(Collections.emptyList(), newList);
// THEN
assertFalse(update.hasNoChanges());
assertTrue(update.getRemoved().isEmpty());
assertThat(update.getAdded(), Matchers.containsInAnyOrder(createWrappers(1, 2, 3, 4, 5)));
}
@Test
public void whenSameListIsAddedTwiceInARowWithinTimeout_thenIExpectAnEmptyUpdate() {
// GIVEN
List<CallParticipant> newList = createParticipants(1, 2, 3, 4, 5);
// WHEN
CallParticipantListUpdate update = CallParticipantListUpdate.computeDeltaUpdate(newList, newList);
// THEN
assertTrue(update.hasNoChanges());
}
@Test
public void whenPlaceholdersAreUsed_thenIExpectAnEmptyUpdate() {
// GIVEN
List<CallParticipant> newList = createPlaceholderParticipants(1, 2, 3, 4, 5);
// WHEN
CallParticipantListUpdate update = CallParticipantListUpdate.computeDeltaUpdate(Collections.emptyList(), newList);
// THEN
assertTrue(update.hasNoChanges());
}
@Test
public void whenNewListIsAdded_thenIExpectAReducedUpdate() {
// GIVEN
List<CallParticipant> list1 = createParticipants(1, 2, 3, 4, 5);
List<CallParticipant> list2 = createParticipants(2, 3, 4, 5, 6);
// WHEN
CallParticipantListUpdate update = CallParticipantListUpdate.computeDeltaUpdate(list1, list2);
// THEN
assertFalse(update.hasNoChanges());
assertThat(update.getAdded(), Matchers.containsInAnyOrder(createWrappers(6)));
assertThat(update.getRemoved(), Matchers.containsInAnyOrder(createWrappers(1)));
}
@Test
public void whenRecipientExistsMultipleTimes_thenIExpectOneInstancePrimaryAndOthersSecondary() {
// GIVEN
List<CallParticipant> list = createParticipants(new long[]{1, 1, 1}, new long[]{1, 2, 3});
// WHEN
CallParticipantListUpdate update = CallParticipantListUpdate.computeDeltaUpdate(Collections.emptyList(), list);
// THEN
List<Boolean> isPrimaryList = Stream.of(update.getAdded()).map(wrapper -> wrapper.getCallParticipant().isPrimary()).toList();
assertThat(isPrimaryList, Matchers.containsInAnyOrder(true, false, false));
}
static CallParticipantListUpdate.Wrapper[] createWrappers(long ... recipientIds) {
CallParticipantListUpdate.Wrapper[] ids = new CallParticipantListUpdate.Wrapper[recipientIds.length];
Set<Long> primaries = new HashSet<>();
for (int i = 0; i < recipientIds.length; i++) {
CallParticipant participant = createParticipant(recipientIds[i], recipientIds[i], primaries.contains(recipientIds[i]) ? CallParticipant.DeviceOrdinal.SECONDARY : CallParticipant.DeviceOrdinal.PRIMARY);
ids[i] = CallParticipantListUpdate.createWrapper(participant);
}
return ids;
}
private static List<CallParticipant> createPlaceholderParticipants(long ... recipientIds) {
long[] deMuxIds = new long[recipientIds.length];
Arrays.fill(deMuxIds, -1);
return createParticipants(recipientIds, deMuxIds);
}
private static List<CallParticipant> createParticipants(long ... recipientIds) {
return createParticipants(recipientIds, recipientIds);
}
private static List<CallParticipant> createParticipants(long[] recipientIds, long[] placeholderIds) {
List<CallParticipant> participants = new ArrayList<>(recipientIds.length);
Set<Long> primaries = new HashSet<>();
for (int i = 0; i < recipientIds.length; i++) {
participants.add(createParticipant(recipientIds[i], placeholderIds[i], primaries.contains(recipientIds[i]) ? CallParticipant.DeviceOrdinal.SECONDARY : CallParticipant.DeviceOrdinal.PRIMARY));
primaries.add(recipientIds[i]);
}
return participants;
}
private static CallParticipant createParticipant(long recipientId, long deMuxId, @NonNull CallParticipant.DeviceOrdinal deviceOrdinal) {
Recipient recipient = RecipientCreator.forId(RecipientId.from(recipientId), true);
return CallParticipant.createRemote(new CallParticipantId(deMuxId, recipient.getId()), recipient, null, new BroadcastVideoSink(), false, false, false, CallParticipant.HAND_LOWERED, -1, false, 0, false, deviceOrdinal);
}
}

View file

@ -0,0 +1,186 @@
package org.thoughtcrime.securesms.components.webrtc
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.containsInAnyOrder
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.thoughtcrime.securesms.events.CallParticipant
import org.thoughtcrime.securesms.events.CallParticipant.Companion.createRemote
import org.thoughtcrime.securesms.events.CallParticipant.DeviceOrdinal
import org.thoughtcrime.securesms.events.CallParticipantId
import org.thoughtcrime.securesms.recipients.RecipientCreator.forId
import org.thoughtcrime.securesms.recipients.RecipientId
class CallParticipantListUpdateTest {
@Test
fun givenEmptySets_thenExpectNoChanges() {
// GIVEN
val added = emptySet<CallParticipantListUpdate.Wrapper>()
val removed = emptySet<CallParticipantListUpdate.Wrapper>()
val update = CallParticipantListUpdate(added, removed)
// THEN
assertTrue(update.hasNoChanges())
assertFalse(update.hasSingleChange())
}
@Test
fun givenOneEmptySet_thenExpectMultipleChanges() {
// GIVEN
val added = createWrappers(1, 2, 3).toSet()
val removed = emptySet<CallParticipantListUpdate.Wrapper>()
val update = CallParticipantListUpdate(added, removed)
// THEN
assertFalse(update.hasNoChanges())
assertFalse(update.hasSingleChange())
}
@Test
fun givenNoEmptySets_thenExpectMultipleChanges() {
// GIVEN
val added = createWrappers(1, 2, 3).toSet()
val removed = createWrappers(4, 5, 6).toSet()
val update = CallParticipantListUpdate(added, removed)
// THEN
assertFalse(update.hasNoChanges())
assertFalse(update.hasSingleChange())
}
@Test
fun givenOneSetWithSingleItemAndAnEmptySet_thenExpectSingleChange() {
// GIVEN
val added = createWrappers(1).toSet()
val removed = emptySet<CallParticipantListUpdate.Wrapper>()
val update = CallParticipantListUpdate(added, removed)
// THEN
assertFalse(update.hasNoChanges())
assertTrue(update.hasSingleChange())
}
@Test
fun whenFirstListIsAdded_thenIExpectAnUpdateWithAllItemsFromListAdded() {
// GIVEN
val newList = createParticipants(1, 2, 3, 4, 5)
// WHEN
val update = CallParticipantListUpdate.computeDeltaUpdate(emptyList(), newList)
// THEN
assertFalse(update.hasNoChanges())
assertTrue(update.removed.isEmpty())
assertThat(update.added, containsInAnyOrder(*createWrappers(1, 2, 3, 4, 5)))
}
@Test
fun whenSameListIsAddedTwiceInARowWithinTimeout_thenIExpectAnEmptyUpdate() {
// GIVEN
val newList = createParticipants(1, 2, 3, 4, 5)
// WHEN
val update = CallParticipantListUpdate.computeDeltaUpdate(newList, newList)
// THEN
assertTrue(update.hasNoChanges())
}
@Test
fun whenPlaceholdersAreUsed_thenIExpectAnEmptyUpdate() {
// GIVEN
val newList = createPlaceholderParticipants(1, 2, 3, 4, 5)
// WHEN
val update = CallParticipantListUpdate.computeDeltaUpdate(emptyList(), newList)
// THEN
assertTrue(update.hasNoChanges())
}
@Test
fun whenNewListIsAdded_thenIExpectAReducedUpdate() {
// GIVEN
val list1 = createParticipants(1, 2, 3, 4, 5)
val list2 = createParticipants(2, 3, 4, 5, 6)
// WHEN
val update = CallParticipantListUpdate.computeDeltaUpdate(list1, list2)
// THEN
assertFalse(update.hasNoChanges())
assertThat(update.added, containsInAnyOrder(*createWrappers(6)))
assertThat(update.removed, containsInAnyOrder(*createWrappers(1)))
}
@Test
fun whenRecipientExistsMultipleTimes_thenIExpectOneInstancePrimaryAndOthersSecondary() {
// GIVEN
val list = createParticipants(longArrayOf(1, 1, 1), longArrayOf(1, 2, 3))
// WHEN
val update = CallParticipantListUpdate.computeDeltaUpdate(emptyList(), list)
// THEN
val isPrimaryList = update.added.map { it.callParticipant.isPrimary }.toList()
assertThat(isPrimaryList, containsInAnyOrder(true, false, false))
}
companion object {
internal fun createWrappers(vararg recipientIds: Long): Array<CallParticipantListUpdate.Wrapper?> {
val ids = arrayOfNulls<CallParticipantListUpdate.Wrapper>(recipientIds.size)
for (i in recipientIds.indices) {
val participant = createParticipant(recipientIds[i], recipientIds[i], DeviceOrdinal.PRIMARY)
ids[i] = CallParticipantListUpdate.createWrapper(participant)
}
return ids
}
private fun createPlaceholderParticipants(
@Suppress("SameParameterValue") vararg recipientIds: Long
): List<CallParticipant> {
val deMuxIds = LongArray(recipientIds.size) { -1 }
return createParticipants(recipientIds, deMuxIds)
}
private fun createParticipants(vararg recipientIds: Long): List<CallParticipant> {
return createParticipants(recipientIds, recipientIds)
}
private fun createParticipants(recipientIds: LongArray, placeholderIds: LongArray): List<CallParticipant> {
val participants = mutableListOf<CallParticipant>()
val primaries = mutableSetOf<Long>()
for (i in recipientIds.indices) {
participants.add(createParticipant(recipientIds[i], placeholderIds[i], if (primaries.contains(recipientIds[i])) DeviceOrdinal.SECONDARY else DeviceOrdinal.PRIMARY))
primaries.add(recipientIds[i])
}
return participants
}
private fun createParticipant(recipientId: Long, deMuxId: Long, deviceOrdinal: DeviceOrdinal): CallParticipant {
val recipient = forId(RecipientId.from(recipientId), true)
return createRemote(
callParticipantId = CallParticipantId(deMuxId, recipient.id),
recipient = recipient,
identityKey = null,
renderer = BroadcastVideoSink(),
isForwardingVideo = false,
audioEnabled = false,
videoEnabled = false,
handRaisedTimestamp = CallParticipant.HAND_LOWERED,
lastSpoke = -1,
mediaKeysReceived = false,
addedToCallTime = 0,
isScreenSharing = false,
deviceOrdinal = deviceOrdinal
)
}
}
}

View file

@ -1,314 +0,0 @@
package org.thoughtcrime.securesms.conversationlist;
import android.app.Application;
import android.database.Cursor;
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.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord;
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter;
import org.thoughtcrime.securesms.conversationlist.model.ConversationReader;
import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.ThreadTable;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.util.RemoteConfig;
import java.util.ArrayList;
import java.util.HashSet;
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.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, application = Application.class)
public class UnarchivedConversationListDataSourceTest {
@Rule
public MockitoRule rule = MockitoJUnit.rule();
@Mock
private MockedStatic<AppDependencies> applicationDependenciesMockedStatic;
@Mock
private MockedStatic<SignalDatabase> signalDatabaseMockedStatic;
@Mock
private MockedStatic<RemoteConfig> remoteConfigMockedStatic;
private ConversationListDataSource.UnarchivedConversationListDataSource testSubject;
private ChatFolderRecord allChatsFolder;
private ThreadTable threadTable;
@Before
public void setUp() {
threadTable = mock(ThreadTable.class);
when(SignalDatabase.threads()).thenReturn(threadTable);
when(AppDependencies.getDatabaseObserver()).thenReturn(mock(DatabaseObserver.class));
when(RemoteConfig.getInlinePinnedChats()).thenReturn(true);
allChatsFolder = setupAllChatsFolder();
testSubject = new ConversationListDataSource.UnarchivedConversationListDataSource(allChatsFolder, ConversationFilter.OFF, false);
}
@Test
public void givenNoConversations_whenIGetTotalCount_thenIExpectZero() {
// WHEN
int result = testSubject.getTotalCount();
// THEN
assertEquals(0, result);
assertFalse(testSubject.hasConversationFilterFooter());
assertFalse(testSubject.hasArchivedFooter());
}
@Test
public void givenArchivedConversations_whenIGetTotalCount_thenIExpectOne() {
// GIVEN
when(threadTable.getArchivedConversationListCount(ConversationFilter.OFF)).thenReturn(12);
// WHEN
int result = testSubject.getTotalCount();
// THEN
assertEquals(1, result);
assertFalse(testSubject.hasConversationFilterFooter());
assertTrue(testSubject.hasArchivedFooter());
}
@Test
public void givenSinglePinnedAndArchivedConversations_whenIGetTotalCount_thenIExpectTwo() {
// GIVEN
when(threadTable.getPinnedConversationListCount(ConversationFilter.OFF, allChatsFolder)).thenReturn(1);
when(threadTable.getUnarchivedConversationListCount(ConversationFilter.OFF, allChatsFolder)).thenReturn(1);
when(threadTable.getArchivedConversationListCount(ConversationFilter.OFF)).thenReturn(12);
// WHEN
int result = testSubject.getTotalCount();
// THEN
assertEquals(2, result);
assertFalse(testSubject.hasConversationFilterFooter());
assertTrue(testSubject.hasArchivedFooter());
}
@Test
public void givenSingleUnpinnedAndArchivedConversations_whenIGetTotalCount_thenIExpectTwo() {
// GIVEN
when(threadTable.getUnarchivedConversationListCount(ConversationFilter.OFF, allChatsFolder)).thenReturn(1);
when(threadTable.getArchivedConversationListCount(ConversationFilter.OFF)).thenReturn(12);
// WHEN
int result = testSubject.getTotalCount();
// THEN
assertEquals(2, result);
assertFalse(testSubject.hasConversationFilterFooter());
assertTrue(testSubject.hasArchivedFooter());
}
@Test
public void givenSinglePinnedAndSingleUnpinned_whenIGetTotalCount_thenIExpectTwo() {
// GIVEN
when(threadTable.getPinnedConversationListCount(ConversationFilter.OFF, allChatsFolder)).thenReturn(1);
when(threadTable.getUnarchivedConversationListCount(ConversationFilter.OFF, allChatsFolder)).thenReturn(2);
// WHEN
int result = testSubject.getTotalCount();
// THEN
assertEquals(2, result);
assertFalse(testSubject.hasConversationFilterFooter());
assertFalse(testSubject.hasArchivedFooter());
}
@Test
public void givenNoConversations_whenIGetCursor_thenIExpectAnEmptyCursor() {
// GIVEN
setupThreadDatabaseCursors(0, 0);
// WHEN
Cursor cursor = testSubject.getCursor(0, 100);
// THEN
verify(threadTable).getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 100, allChatsFolder);
verify(threadTable).getUnarchivedConversationList(ConversationFilter.OFF, false, 0, 100, allChatsFolder);
assertEquals(0, cursor.getCount());
}
@Test
public void givenArchivedConversations_whenIGetCursor_thenIExpectOne() {
// GIVEN
setupThreadDatabaseCursors(0, 0);
when(threadTable.getArchivedConversationListCount(ConversationFilter.OFF)).thenReturn(12);
testSubject.getTotalCount();
// WHEN
Cursor cursor = testSubject.getCursor(0, 100);
// THEN
verify(threadTable).getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 100, allChatsFolder);
verify(threadTable).getUnarchivedConversationList(ConversationFilter.OFF, false, 0, 100, allChatsFolder);
assertEquals(1, cursor.getCount());
}
@Test
public void givenSinglePinnedAndArchivedConversations_whenIGetCursor_thenIExpectTwo() {
// GIVEN
setupThreadDatabaseCursors(1, 0);
when(threadTable.getPinnedConversationListCount(ConversationFilter.OFF, allChatsFolder)).thenReturn(1);
when(threadTable.getUnarchivedConversationListCount(ConversationFilter.OFF, allChatsFolder)).thenReturn(1);
when(threadTable.getArchivedConversationListCount(ConversationFilter.OFF)).thenReturn(12);
testSubject.getTotalCount();
// WHEN
Cursor cursor = testSubject.getCursor(0, 100);
// THEN
verify(threadTable).getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 100, allChatsFolder);
verify(threadTable).getUnarchivedConversationList(ConversationFilter.OFF, false, 0, 99, allChatsFolder);
assertEquals(2, cursor.getCount());
}
@Test
public void givenSingleUnpinnedAndArchivedConversations_whenIGetCursor_thenIExpectTwo() {
// GIVEN
setupThreadDatabaseCursors(0, 1);
when(threadTable.getUnarchivedConversationListCount(ConversationFilter.OFF, allChatsFolder)).thenReturn(1);
when(threadTable.getArchivedConversationListCount(ConversationFilter.OFF)).thenReturn(12);
testSubject.getTotalCount();
// WHEN
Cursor cursor = testSubject.getCursor(0, 100);
// THEN
verify(threadTable).getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 100, allChatsFolder);
verify(threadTable).getUnarchivedConversationList(ConversationFilter.OFF, false, 0, 100, allChatsFolder);
assertEquals(2, cursor.getCount());
}
@Test
public void givenSinglePinnedAndSingleUnpinned_whenIGetCursor_thenIExpectTwo() {
// GIVEN
setupThreadDatabaseCursors(1, 1);
when(threadTable.getPinnedConversationListCount(ConversationFilter.OFF, allChatsFolder)).thenReturn(1);
when(threadTable.getUnarchivedConversationListCount(ConversationFilter.OFF, allChatsFolder)).thenReturn(2);
testSubject.getTotalCount();
// WHEN
Cursor cursor = testSubject.getCursor(0, 100);
// THEN
verify(threadTable).getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 100, allChatsFolder);
verify(threadTable).getUnarchivedConversationList(ConversationFilter.OFF, false, 0, 99, allChatsFolder);
assertEquals(2, cursor.getCount());
}
@Test
public void givenLoadingSecondPage_whenIGetCursor_thenIExpectProperOffsetAndCursorCount() {
// GIVEN
setupThreadDatabaseCursors(0, 100);
when(threadTable.getPinnedConversationListCount(ConversationFilter.OFF, allChatsFolder)).thenReturn(4);
when(threadTable.getUnarchivedConversationListCount(ConversationFilter.OFF, allChatsFolder)).thenReturn(104);
testSubject.getTotalCount();
// WHEN
Cursor cursor = testSubject.getCursor(50, 100);
// THEN
verify(threadTable).getUnarchivedConversationList(ConversationFilter.OFF, true, 50, 100, allChatsFolder);
verify(threadTable).getUnarchivedConversationList(ConversationFilter.OFF, false, 46, 100, allChatsFolder);
assertEquals(100, cursor.getCount());
}
@Test
public void givenHasArchivedAndLoadingLastPage_whenIGetCursor_thenIExpectProperOffsetAndCursorCount() {
// GIVEN
setupThreadDatabaseCursors(0, 99);
when(threadTable.getPinnedConversationListCount(ConversationFilter.OFF, allChatsFolder)).thenReturn(4);
when(threadTable.getUnarchivedConversationListCount(ConversationFilter.OFF, allChatsFolder)).thenReturn(103);
when(threadTable.getArchivedConversationListCount(ConversationFilter.OFF)).thenReturn(12);
testSubject.getTotalCount();
// WHEN
Cursor cursor = testSubject.getCursor(50, 100);
// THEN
verify(threadTable).getUnarchivedConversationList(ConversationFilter.OFF, true, 50, 100, allChatsFolder);
verify(threadTable).getUnarchivedConversationList(ConversationFilter.OFF, false, 46, 100, allChatsFolder);
assertEquals(100, cursor.getCount());
cursor.moveToLast();
assertEquals(0, cursor.getColumnIndex(ConversationReader.HEADER_COLUMN[0]));
}
@Test
public void givenHasNoArchivedAndIsFiltered_whenIGetCursor_thenIExpectConversationFilterFooter() {
// GIVEN
ConversationListDataSource.UnarchivedConversationListDataSource testSubject = new ConversationListDataSource.UnarchivedConversationListDataSource(allChatsFolder, ConversationFilter.UNREAD, false);
setupThreadDatabaseCursors(0, 3);
when(threadTable.getPinnedConversationListCount(ConversationFilter.UNREAD, allChatsFolder)).thenReturn(0);
when(threadTable.getUnarchivedConversationListCount(ConversationFilter.UNREAD, allChatsFolder)).thenReturn(3);
when(threadTable.getArchivedConversationListCount(ConversationFilter.UNREAD)).thenReturn(0);
testSubject.getTotalCount();
// WHEN
Cursor cursor = testSubject.getCursor(0, 5);
// THEN
assertEquals(4, cursor.getCount());
assertTrue(testSubject.hasConversationFilterFooter());
cursor.moveToLast();
assertEquals(0, cursor.getColumnIndex(ConversationReader.HEADER_COLUMN[0]));
}
private void setupThreadDatabaseCursors(int pinned, int unpinned) {
Cursor pinnedCursor = mock(Cursor.class);
when(pinnedCursor.getCount()).thenReturn(pinned);
Cursor unpinnedCursor = mock(Cursor.class);
when(unpinnedCursor.getCount()).thenReturn(unpinned);
when(threadTable.getUnarchivedConversationList(any(), eq(true), anyLong(), anyLong(), any())).thenReturn(pinnedCursor);
when(threadTable.getUnarchivedConversationList(any(), eq(false), anyLong(), anyLong(), any())).thenReturn(unpinnedCursor);
}
private ChatFolderRecord setupAllChatsFolder() {
return new ChatFolderRecord(
1,
"",
-1,
new ArrayList<>(),
new ArrayList<>(),
new HashSet<>(),
new HashSet<>(),
false,
false,
false,
false,
false,
ChatFolderRecord.FolderType.ALL,
0
);
}
}

View file

@ -0,0 +1,305 @@
package org.thoughtcrime.securesms.conversationlist
import android.app.Application
import android.database.Cursor
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkAll
import io.mockk.verify
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
import org.thoughtcrime.securesms.conversationlist.ConversationListDataSource.UnarchivedConversationListDataSource
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
import org.thoughtcrime.securesms.conversationlist.model.ConversationReader
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule
import org.thoughtcrime.securesms.util.RemoteConfig
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
class UnarchivedConversationListDataSourceTest {
private lateinit var testSubject: UnarchivedConversationListDataSource
private lateinit var allChatsFolder: ChatFolderRecord
private lateinit var threadTable: ThreadTable
@get:Rule
val appDependencies = MockAppDependenciesRule()
@Before
fun setUp() {
threadTable = mockk<ThreadTable>(relaxed = true)
mockkStatic(RemoteConfig::class)
every { RemoteConfig.init() } just runs
every { RemoteConfig.inlinePinnedChats } returns true
mockkObject(SignalDatabase)
every { SignalDatabase.threads } returns threadTable
allChatsFolder = setupAllChatsFolder()
testSubject = UnarchivedConversationListDataSource(allChatsFolder, ConversationFilter.OFF, false)
}
@After
fun cleanup() {
unmockkAll()
}
@Test
fun givenNoConversations_whenIGetTotalCount_thenIExpectZero() {
// WHEN
val result = testSubject.totalCount
// THEN
assertEquals(0, result)
assertFalse(testSubject.hasConversationFilterFooter())
assertFalse(testSubject.hasArchivedFooter())
}
@Test
fun givenArchivedConversations_whenIGetTotalCount_thenIExpectOne() {
// GIVEN
every { threadTable.getArchivedConversationListCount(ConversationFilter.OFF) } returns 12
// WHEN
val result = testSubject.totalCount
// THEN
assertEquals(1, result)
assertFalse(testSubject.hasConversationFilterFooter())
assertTrue(testSubject.hasArchivedFooter())
}
@Test
fun givenSinglePinnedAndArchivedConversations_whenIGetTotalCount_thenIExpectTwo() {
// GIVEN
every { threadTable.getPinnedConversationListCount(ConversationFilter.OFF, allChatsFolder) } returns 1
every { threadTable.getUnarchivedConversationListCount(ConversationFilter.OFF, allChatsFolder) } returns 1
every { threadTable.getArchivedConversationListCount(ConversationFilter.OFF) } returns 12
// WHEN
val result = testSubject.totalCount
// THEN
assertEquals(2, result)
assertFalse(testSubject.hasConversationFilterFooter())
assertTrue(testSubject.hasArchivedFooter())
}
@Test
fun givenSingleUnpinnedAndArchivedConversations_whenIGetTotalCount_thenIExpectTwo() {
// GIVEN
every { threadTable.getUnarchivedConversationListCount(ConversationFilter.OFF, allChatsFolder) } returns 1
every { threadTable.getArchivedConversationListCount(ConversationFilter.OFF) } returns 12
// WHEN
val result = testSubject.totalCount
// THEN
assertEquals(2, result)
assertFalse(testSubject.hasConversationFilterFooter())
assertTrue(testSubject.hasArchivedFooter())
}
@Test
fun givenSinglePinnedAndSingleUnpinned_whenIGetTotalCount_thenIExpectTwo() {
// GIVEN
every { threadTable.getPinnedConversationListCount(ConversationFilter.OFF, allChatsFolder) } returns 1
every { threadTable.getUnarchivedConversationListCount(ConversationFilter.OFF, allChatsFolder) } returns 2
// WHEN
val result = testSubject.totalCount
// THEN
assertEquals(2, result)
assertFalse(testSubject.hasConversationFilterFooter())
assertFalse(testSubject.hasArchivedFooter())
}
@Test
fun givenNoConversations_whenIGetCursor_thenIExpectAnEmptyCursor() {
// GIVEN
setupThreadDatabaseCursors(0, 0)
// WHEN
val cursor = testSubject.getCursor(0, 100)
// THEN
verify { threadTable.getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 100, allChatsFolder) }
verify { threadTable.getUnarchivedConversationList(ConversationFilter.OFF, false, 0, 100, allChatsFolder) }
assertEquals(0, cursor.count)
}
@Test
fun givenArchivedConversations_whenIGetCursor_thenIExpectOne() {
// GIVEN
setupThreadDatabaseCursors(0, 0)
every { threadTable.getArchivedConversationListCount(ConversationFilter.OFF) } returns 12
testSubject.totalCount
// WHEN
val cursor = testSubject.getCursor(0, 100)
// THEN
verify { threadTable.getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 100, allChatsFolder) }
verify { threadTable.getUnarchivedConversationList(ConversationFilter.OFF, false, 0, 100, allChatsFolder) }
assertEquals(1, cursor.count)
}
@Test
fun givenSinglePinnedAndArchivedConversations_whenIGetCursor_thenIExpectTwo() {
// GIVEN
setupThreadDatabaseCursors(1, 0)
every { threadTable.getPinnedConversationListCount(ConversationFilter.OFF, allChatsFolder) } returns 1
every { threadTable.getUnarchivedConversationListCount(ConversationFilter.OFF, allChatsFolder) } returns 1
every { threadTable.getArchivedConversationListCount(ConversationFilter.OFF) } returns 12
testSubject.totalCount
// WHEN
val cursor = testSubject.getCursor(0, 100)
// THEN
verify { threadTable.getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 100, allChatsFolder) }
verify { threadTable.getUnarchivedConversationList(ConversationFilter.OFF, false, 0, 99, allChatsFolder) }
assertEquals(2, cursor.count)
}
@Test
fun givenSingleUnpinnedAndArchivedConversations_whenIGetCursor_thenIExpectTwo() {
// GIVEN
setupThreadDatabaseCursors(0, 1)
every { threadTable.getUnarchivedConversationListCount(ConversationFilter.OFF, allChatsFolder) } returns 1
every { threadTable.getArchivedConversationListCount(ConversationFilter.OFF) } returns 12
testSubject.totalCount
// WHEN
val cursor = testSubject.getCursor(0, 100)
// THEN
verify { threadTable.getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 100, allChatsFolder) }
verify { threadTable.getUnarchivedConversationList(ConversationFilter.OFF, false, 0, 100, allChatsFolder) }
assertEquals(2, cursor.count)
}
@Test
fun givenSinglePinnedAndSingleUnpinned_whenIGetCursor_thenIExpectTwo() {
// GIVEN
setupThreadDatabaseCursors(1, 1)
every { threadTable.getPinnedConversationListCount(ConversationFilter.OFF, allChatsFolder) } returns 1
every { threadTable.getUnarchivedConversationListCount(ConversationFilter.OFF, allChatsFolder) } returns 2
testSubject.totalCount
// WHEN
val cursor = testSubject.getCursor(0, 100)
// THEN
verify { threadTable.getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 100, allChatsFolder) }
verify { threadTable.getUnarchivedConversationList(ConversationFilter.OFF, false, 0, 99, allChatsFolder) }
assertEquals(2, cursor.count)
}
@Test
fun givenLoadingSecondPage_whenIGetCursor_thenIExpectProperOffsetAndCursorCount() {
// GIVEN
setupThreadDatabaseCursors(0, 100)
every { threadTable.getPinnedConversationListCount(ConversationFilter.OFF, allChatsFolder) } returns 4
every { threadTable.getUnarchivedConversationListCount(ConversationFilter.OFF, allChatsFolder) } returns 104
testSubject.totalCount
// WHEN
val cursor = testSubject.getCursor(50, 100)
// THEN
verify { threadTable.getUnarchivedConversationList(ConversationFilter.OFF, true, 50, 100, allChatsFolder) }
verify { threadTable.getUnarchivedConversationList(ConversationFilter.OFF, false, 46, 100, allChatsFolder) }
assertEquals(100, cursor.count)
}
@Test
fun givenHasArchivedAndLoadingLastPage_whenIGetCursor_thenIExpectProperOffsetAndCursorCount() {
// GIVEN
setupThreadDatabaseCursors(0, 99)
every { threadTable.getPinnedConversationListCount(ConversationFilter.OFF, allChatsFolder) } returns 4
every { threadTable.getUnarchivedConversationListCount(ConversationFilter.OFF, allChatsFolder) } returns 103
every { threadTable.getArchivedConversationListCount(ConversationFilter.OFF) } returns 12
testSubject.totalCount
// WHEN
val cursor = testSubject.getCursor(50, 100)
// THEN
verify { threadTable.getUnarchivedConversationList(ConversationFilter.OFF, true, 50, 100, allChatsFolder) }
verify { threadTable.getUnarchivedConversationList(ConversationFilter.OFF, false, 46, 100, allChatsFolder) }
assertEquals(100, cursor.count)
cursor.moveToLast()
assertEquals(0, cursor.getColumnIndex(ConversationReader.HEADER_COLUMN[0]))
}
@Test
fun givenHasNoArchivedAndIsFiltered_whenIGetCursor_thenIExpectConversationFilterFooter() {
// GIVEN
val testSubject = UnarchivedConversationListDataSource(allChatsFolder, ConversationFilter.UNREAD, false)
setupThreadDatabaseCursors(0, 3)
every { threadTable.getPinnedConversationListCount(ConversationFilter.UNREAD, allChatsFolder) } returns 0
every { threadTable.getUnarchivedConversationListCount(ConversationFilter.UNREAD, allChatsFolder) } returns 3
every { threadTable.getArchivedConversationListCount(ConversationFilter.UNREAD) } returns 0
testSubject.totalCount
// WHEN
val cursor = testSubject.getCursor(0, 5)
// THEN
assertEquals(4, cursor.count)
assertTrue(testSubject.hasConversationFilterFooter())
cursor.moveToLast()
assertEquals(0, cursor.getColumnIndex(ConversationReader.HEADER_COLUMN[0]))
}
private fun setupThreadDatabaseCursors(pinned: Int, unpinned: Int) {
every {
threadTable.getUnarchivedConversationList(any(), true, any(), any(), any())
} returns mockk<Cursor>(relaxed = true) {
every { count } returns pinned
}
every {
threadTable.getUnarchivedConversationList(any(), false, any(), any(), any())
} returns mockk<Cursor>(relaxed = true) {
every { count } returns unpinned
}
}
private fun setupAllChatsFolder(): ChatFolderRecord {
return ChatFolderRecord(
id = 1,
name = "",
position = -1,
includedChats = emptyList(),
excludedChats = emptyList(),
includedRecipients = emptySet(),
excludedRecipients = emptySet(),
showUnread = false,
showMutedChats = false,
showIndividualChats = false,
showGroupChats = false,
isMuted = false,
folderType = ChatFolderRecord.FolderType.ALL,
unreadCount = 0
)
}
}

View file

@ -1,35 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.app.Application;
import org.junit.Test;
import org.thoughtcrime.securesms.jobmanager.Job;
import java.util.Map;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public final class JobManagerFactoriesTest {
@Test
public void PushContentReceiveJob_is_retired() {
Map<String, Job.Factory> factories = JobManagerFactories.getJobFactories(mock(Application.class));
assertTrue(factories.get("PushContentReceiveJob") instanceof FailingJob.Factory);
}
@Test
public void AttachmentUploadJob_is_retired() {
Map<String, Job.Factory> factories = JobManagerFactories.getJobFactories(mock(Application.class));
assertTrue(factories.get("AttachmentUploadJob") instanceof FailingJob.Factory);
}
@Test
public void MmsSendJob_is_retired() {
Map<String, Job.Factory> factories = JobManagerFactories.getJobFactories(mock(Application.class));
assertTrue(factories.get("MmsSendJob") instanceof FailingJob.Factory);
}
}

View file

@ -0,0 +1,29 @@
package org.thoughtcrime.securesms.jobs
import android.app.Application
import io.mockk.mockk
import org.junit.Assert.assertTrue
import org.junit.Test
class JobManagerFactoriesTest {
@Test
fun test_PushContentReceiveJob_is_retired() {
val factories = JobManagerFactories.getJobFactories(mockk<Application>())
assertTrue(factories["PushContentReceiveJob"] is FailingJob.Factory)
}
@Test
fun test_AttachmentUploadJob_is_retired() {
val factories = JobManagerFactories.getJobFactories(mockk<Application>())
assertTrue(factories["AttachmentUploadJob"] is FailingJob.Factory)
}
@Test
fun test_MmsSendJob_is_retired() {
val factories = JobManagerFactories.getJobFactories(mockk<Application>())
assertTrue(factories["MmsSendJob"] is FailingJob.Factory)
}
}

View file

@ -1,275 +0,0 @@
package org.thoughtcrime.securesms.recipients;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.testutil.LogRecorder;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.util.Optional;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public final class RecipientIdCacheTest {
private static final int TEST_CACHE_LIMIT = 5;
private RecipientIdCache recipientIdCache;
private LogRecorder logRecorder;
@Before
public void setup() {
recipientIdCache = new RecipientIdCache(TEST_CACHE_LIMIT);
logRecorder = new LogRecorder();
Log.initialize(logRecorder);
}
@Test
public void empty_access_by_nulls() {
RecipientId recipientId = recipientIdCache.get(null, null);
assertNull(recipientId);
}
@Test
public void empty_access_by_uuid() {
RecipientId recipientId = recipientIdCache.get(ACI.from(UUID.randomUUID()), null);
assertNull(recipientId);
}
@Test
public void empty_access_by_e164() {
RecipientId recipientId = recipientIdCache.get(null, "+155512345");
assertNull(recipientId);
}
@Test
public void cache_hit_by_uuid() {
RecipientId recipientId1 = recipientId();
ServiceId sid1 = ACI.from(UUID.randomUUID());
recipientIdCache.put(recipient(recipientId1, sid1, null));
RecipientId recipientId = recipientIdCache.get(sid1, null);
assertEquals(recipientId1, recipientId);
}
@Test
public void cache_miss_by_uuid() {
RecipientId recipientId1 = recipientId();
ServiceId sid1 = ACI.from(UUID.randomUUID());
ServiceId sid2 = ACI.from(UUID.randomUUID());
recipientIdCache.put(recipient(recipientId1, sid1, null));
RecipientId recipientId = recipientIdCache.get(sid2, null);
assertNull(recipientId);
}
@Test
public void cache_hit_by_uuid_e164_not_supplied_on_get() {
RecipientId recipientId1 = recipientId();
ServiceId sid1 = ACI.from(UUID.randomUUID());
recipientIdCache.put(recipient(recipientId1, sid1, "+15551234567"));
RecipientId recipientId = recipientIdCache.get(sid1, null);
assertEquals(recipientId1, recipientId);
}
@Test
public void cache_miss_by_uuid_e164_not_supplied_on_put() {
RecipientId recipientId1 = recipientId();
ServiceId sid1 = ACI.from(UUID.randomUUID());
recipientIdCache.put(recipient(recipientId1, sid1, null));
RecipientId recipientId = recipientIdCache.get(sid1, "+15551234567");
assertNull(recipientId);
}
@Test
public void cache_hit_by_e164() {
RecipientId recipientId1 = recipientId();
String e164 = "+1555123456";
recipientIdCache.put(recipient(recipientId1, null, e164));
RecipientId recipientId = recipientIdCache.get(null, e164);
assertEquals(recipientId1, recipientId);
}
@Test
public void cache_miss_by_e164() {
RecipientId recipientId1 = recipientId();
String e164a = "+1555123456";
String e164b = "+1555123457";
recipientIdCache.put(recipient(recipientId1, null, e164a));
RecipientId recipientId = recipientIdCache.get(null, e164b);
assertNull(recipientId);
}
@Test
public void cache_hit_by_e164_uuid_not_supplied_on_get() {
RecipientId recipientId1 = recipientId();
ServiceId sid1 = ACI.from(UUID.randomUUID());
recipientIdCache.put(recipient(recipientId1, sid1, "+15551234567"));
RecipientId recipientId = recipientIdCache.get(null, "+15551234567");
assertEquals(recipientId1, recipientId);
}
@Test
public void cache_miss_by_e164_uuid_not_supplied_on_put() {
RecipientId recipientId1 = recipientId();
ServiceId sid1 = ACI.from(UUID.randomUUID());
String e164 = "+1555123456";
recipientIdCache.put(recipient(recipientId1, null, e164));
RecipientId recipientId = recipientIdCache.get(sid1, e164);
assertNull(recipientId);
}
@Test
public void cache_hit_by_both() {
RecipientId recipientId1 = recipientId();
ServiceId sid1 = ACI.from(UUID.randomUUID());
String e164 = "+1555123456";
recipientIdCache.put(recipient(recipientId1, sid1, e164));
RecipientId recipientId = recipientIdCache.get(sid1, e164);
assertEquals(recipientId1, recipientId);
}
@Test
public void full_recipient_id_learned_by_two_puts() {
RecipientId recipientId1 = recipientId();
ServiceId sid1 = ACI.from(UUID.randomUUID());
String e164 = "+1555123456";
recipientIdCache.put(recipient(recipientId1, sid1, null));
recipientIdCache.put(recipient(recipientId1, null, e164));
RecipientId recipientId = recipientIdCache.get(sid1, e164);
assertEquals(recipientId1, recipientId);
}
@Test
public void if_cache_state_disagrees_returns_null() {
RecipientId recipientId1 = recipientId();
RecipientId recipientId2 = recipientId();
ServiceId sid = ACI.from(UUID.randomUUID());
String e164 = "+1555123456";
recipientIdCache.put(recipient(recipientId1, null, e164));
recipientIdCache.put(recipient(recipientId2, sid, null));
RecipientId recipientId = recipientIdCache.get(sid, e164);
assertNull(recipientId);
assertEquals(1, logRecorder.getWarnings().size());
assertEquals("Seen invalid RecipientIdCacheState", logRecorder.getWarnings().get(0).getMessage());
}
@Test
public void after_invalid_cache_hit_entries_are_cleared_up() {
RecipientId recipientId1 = recipientId();
RecipientId recipientId2 = recipientId();
ServiceId sid = ACI.from(UUID.randomUUID());
String e164 = "+1555123456";
recipientIdCache.put(recipient(recipientId1, null, e164));
recipientIdCache.put(recipient(recipientId2, sid, null));
recipientIdCache.get(sid, e164);
assertNull(recipientIdCache.get(sid, null));
assertNull(recipientIdCache.get(null, e164));
}
@Test
public void multiple_entries() {
RecipientId recipientId1 = recipientId();
RecipientId recipientId2 = recipientId();
ServiceId sid1 = ACI.from(UUID.randomUUID());
ServiceId sid2 = ACI.from(UUID.randomUUID());
recipientIdCache.put(recipient(recipientId1, sid1, null));
recipientIdCache.put(recipient(recipientId2, sid2, null));
assertEquals(recipientId1, recipientIdCache.get(sid1, null));
assertEquals(recipientId2, recipientIdCache.get(sid2, null));
}
@Test
public void drops_oldest_when_reaches_cache_limit() {
RecipientId recipientId1 = recipientId();
ServiceId sid1 = ACI.from(UUID.randomUUID());
recipientIdCache.put(recipient(recipientId1, sid1, null));
for (int i = 0; i < TEST_CACHE_LIMIT; i++) {
recipientIdCache.put(recipient(recipientId(), ACI.from(UUID.randomUUID()), null));
}
assertNull(recipientIdCache.get(sid1, null));
}
@Test
public void remains_in_cache_when_used_before_reaching_cache_limit() {
RecipientId recipientId1 = recipientId();
ServiceId sid1 = ACI.from(UUID.randomUUID());
recipientIdCache.put(recipient(recipientId1, sid1, null));
for (int i = 0; i < TEST_CACHE_LIMIT - 1; i++) {
recipientIdCache.put(recipient(recipientId(), ACI.from(UUID.randomUUID()), null));
}
assertEquals(recipientId1, recipientIdCache.get(sid1, null));
recipientIdCache.put(recipient(recipientId(), ACI.from(UUID.randomUUID()), null));
assertEquals(recipientId1, recipientIdCache.get(sid1, null));
}
private static @NonNull RecipientId recipientId() {
return mock(RecipientId.class);
}
private static @NonNull Recipient recipient(RecipientId recipientId, @Nullable ServiceId serviceId, @Nullable String e164) {
Recipient mock = mock(Recipient.class);
when(mock.getId()).thenReturn(recipientId);
when(mock.getServiceId()).thenReturn(Optional.ofNullable(serviceId));
when(mock.getE164()).thenReturn(Optional.ofNullable(e164));
return mock;
}
}

View file

@ -0,0 +1,264 @@
package org.thoughtcrime.securesms.recipients
import io.mockk.every
import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.testutil.LogRecorder
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.Optional
import java.util.UUID
class RecipientIdCacheTest {
private val recipientIdCache = RecipientIdCache(TEST_CACHE_LIMIT)
private val logRecorder = LogRecorder().apply { Log.initialize(this) }
@Test
fun empty_access_by_nulls() {
val recipientId = recipientIdCache[null, null]
assertNull(recipientId)
}
@Test
fun empty_access_by_uuid() {
val recipientId = recipientIdCache[randomAci(), null]
assertNull(recipientId)
}
@Test
fun empty_access_by_e164() {
val recipientId = recipientIdCache[null, "+155512345"]
assertNull(recipientId)
}
@Test
fun cache_hit_by_uuid() {
val recipientId1 = recipientId()
val sid1 = randomAci()
recipientIdCache.put(recipient(recipientId1, sid1, null))
val recipientId = recipientIdCache[sid1, null]
assertEquals(recipientId1, recipientId)
}
@Test
fun cache_miss_by_uuid() {
val recipientId1 = recipientId()
val sid1 = randomAci()
val sid2 = randomAci()
recipientIdCache.put(recipient(recipientId1, sid1, null))
val recipientId = recipientIdCache[sid2, null]
assertNull(recipientId)
}
@Test
fun cache_hit_by_uuid_e164_not_supplied_on_get() {
val recipientId1 = recipientId()
val sid1 = randomAci()
recipientIdCache.put(recipient(recipientId1, sid1, "+15551234567"))
val recipientId = recipientIdCache[sid1, null]
assertEquals(recipientId1, recipientId)
}
@Test
fun cache_miss_by_uuid_e164_not_supplied_on_put() {
val recipientId1 = recipientId()
val sid1 = randomAci()
recipientIdCache.put(recipient(recipientId1, sid1, null))
val recipientId = recipientIdCache[sid1, "+15551234567"]
assertNull(recipientId)
}
@Test
fun cache_hit_by_e164() {
val recipientId1 = recipientId()
val e164 = "+1555123456"
recipientIdCache.put(recipient(recipientId1, null, e164))
val recipientId = recipientIdCache[null, e164]
assertEquals(recipientId1, recipientId)
}
@Test
fun cache_miss_by_e164() {
val recipientId1 = recipientId()
val e164a = "+1555123456"
val e164b = "+1555123457"
recipientIdCache.put(recipient(recipientId1, null, e164a))
val recipientId = recipientIdCache[null, e164b]
assertNull(recipientId)
}
@Test
fun cache_hit_by_e164_uuid_not_supplied_on_get() {
val recipientId1 = recipientId()
val sid1 = randomAci()
recipientIdCache.put(recipient(recipientId1, sid1, "+15551234567"))
val recipientId = recipientIdCache[null, "+15551234567"]
assertEquals(recipientId1, recipientId)
}
@Test
fun cache_miss_by_e164_uuid_not_supplied_on_put() {
val recipientId1 = recipientId()
val sid1 = randomAci()
val e164 = "+1555123456"
recipientIdCache.put(recipient(recipientId1, null, e164))
val recipientId = recipientIdCache[sid1, e164]
assertNull(recipientId)
}
@Test
fun cache_hit_by_both() {
val recipientId1 = recipientId()
val sid1 = randomAci()
val e164 = "+1555123456"
recipientIdCache.put(recipient(recipientId1, sid1, e164))
val recipientId = recipientIdCache[sid1, e164]
assertEquals(recipientId1, recipientId)
}
@Test
fun full_recipient_id_learned_by_two_puts() {
val recipientId1 = recipientId()
val sid1 = randomAci()
val e164 = "+1555123456"
recipientIdCache.put(recipient(recipientId1, sid1, null))
recipientIdCache.put(recipient(recipientId1, null, e164))
val recipientId = recipientIdCache[sid1, e164]
assertEquals(recipientId1, recipientId)
}
@Test
fun if_cache_state_disagrees_returns_null() {
val recipientId1 = recipientId()
val recipientId2 = recipientId()
val sid = randomAci()
val e164 = "+1555123456"
recipientIdCache.put(recipient(recipientId1, null, e164))
recipientIdCache.put(recipient(recipientId2, sid, null))
val recipientId = recipientIdCache[sid, e164]
assertNull(recipientId)
assertEquals(1, logRecorder.warnings.size)
assertEquals("Seen invalid RecipientIdCacheState", logRecorder.warnings.single().message)
}
@Test
fun after_invalid_cache_hit_entries_are_cleared_up() {
val recipientId1 = recipientId()
val recipientId2 = recipientId()
val sid = randomAci()
val e164 = "+1555123456"
recipientIdCache.put(recipient(recipientId1, null, e164))
recipientIdCache.put(recipient(recipientId2, sid, null))
recipientIdCache[sid, e164]
assertNull(recipientIdCache[sid, null])
assertNull(recipientIdCache[null, e164])
}
@Test
fun multiple_entries() {
val recipientId1 = recipientId()
val recipientId2 = recipientId()
val sid1 = randomAci()
val sid2 = randomAci()
recipientIdCache.put(recipient(recipientId1, sid1, null))
recipientIdCache.put(recipient(recipientId2, sid2, null))
assertEquals(recipientId1, recipientIdCache[sid1, null])
assertEquals(recipientId2, recipientIdCache[sid2, null])
}
@Test
fun drops_oldest_when_reaches_cache_limit() {
val recipientId1 = recipientId()
val sid1 = randomAci()
recipientIdCache.put(recipient(recipientId1, sid1, null))
for (i in 0 until TEST_CACHE_LIMIT) {
recipientIdCache.put(recipient(recipientId(), randomAci(), null))
}
assertNull(recipientIdCache[sid1, null])
}
@Test
fun remains_in_cache_when_used_before_reaching_cache_limit() {
val recipientId1 = recipientId()
val sid1 = randomAci()
recipientIdCache.put(recipient(recipientId1, sid1, null))
for (i in 0 until TEST_CACHE_LIMIT - 1) {
recipientIdCache.put(recipient(recipientId(), randomAci(), null))
}
assertEquals(recipientId1, recipientIdCache[sid1, null])
recipientIdCache.put(recipient(recipientId(), randomAci(), null))
assertEquals(recipientId1, recipientIdCache[sid1, null])
}
companion object {
private const val TEST_CACHE_LIMIT = 5
private fun recipientId(): RecipientId {
return mockk<RecipientId>()
}
private fun randomAci(): ServiceId {
return ServiceId.ACI.from(UUID.randomUUID())
}
private fun recipient(recipientId: RecipientId, serviceId: ServiceId?, e164: String?): Recipient {
return mockk<Recipient> {
every { this@mockk.id } returns recipientId
every { this@mockk.serviceId } returns Optional.ofNullable(serviceId)
every { this@mockk.e164 } returns Optional.ofNullable(e164)
}
}
}
}

View file

@ -1,182 +0,0 @@
package org.thoughtcrime.securesms.storage;
import com.annimon.stream.Stream;
import org.junit.Before;
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.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.IdDifferenceResult;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
import org.whispersystems.signalservice.api.storage.SignalRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import okio.ByteString;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.thoughtcrime.securesms.testutil.TestHelpers.assertContentsEqual;
import static org.thoughtcrime.securesms.testutil.TestHelpers.byteArray;
import static org.thoughtcrime.securesms.testutil.TestHelpers.byteListOf;
public final class StorageSyncHelperTest {
private static final ACI ACI_A = ACI.parseOrThrow("ebef429e-695e-4f51-bcc4-526a60ac68c7");
private static final ACI ACI_SELF = ACI.parseOrThrow("1b2a2ca5-fc9e-4656-8c9f-22cc349ed3af");
private static final String E164_A = "+16108675309";
private static final String E164_SELF = "+16105555555";
private static final Recipient SELF = mock(Recipient.class);
static {
when(SELF.getServiceId()).thenReturn(Optional.of(ACI_SELF));
when(SELF.getE164()).thenReturn(Optional.of(E164_SELF));
when(SELF.resolve()).thenReturn(SELF);
}
@Rule
public MockitoRule rule = MockitoJUnit.rule();
@Mock
private MockedStatic<Recipient> recipientMockedStatic;
@Mock
private MockedStatic<RemoteConfig> remoteConfigMockedStatic;
@Before
public void setup() {
recipientMockedStatic.when(Recipient::self).thenReturn(SELF);
Log.initialize(new Log.Logger[0]);
StorageSyncHelper.setTestKeyGenerator(null);
}
@Test
public void findIdDifference_allOverlap() {
IdDifferenceResult result = StorageSyncHelper.findIdDifference(keyListOf(1, 2, 3), keyListOf(1, 2, 3));
assertTrue(result.localOnlyIds.isEmpty());
assertTrue(result.remoteOnlyIds.isEmpty());
assertFalse(result.getHasTypeMismatches());
}
@Test
public void findIdDifference_noOverlap() {
IdDifferenceResult result = StorageSyncHelper.findIdDifference(keyListOf(1, 2, 3), keyListOf(4, 5, 6));
assertContentsEqual(keyListOf(1, 2, 3), result.remoteOnlyIds);
assertContentsEqual(keyListOf(4, 5, 6), result.localOnlyIds);
assertFalse(result.getHasTypeMismatches());
}
@Test
public void findIdDifference_someOverlap() {
IdDifferenceResult result = StorageSyncHelper.findIdDifference(keyListOf(1, 2, 3), keyListOf(2, 3, 4));
assertContentsEqual(keyListOf(1), result.remoteOnlyIds);
assertContentsEqual(keyListOf(4), result.localOnlyIds);
assertFalse(result.getHasTypeMismatches());
}
@Test
public void findIdDifference_typeMismatch_allOverlap() {
IdDifferenceResult result = StorageSyncHelper.findIdDifference(keyListOf(new HashMap<Integer, Integer>() {{
put(100, 1);
put(200, 2);
}}),
keyListOf(new HashMap<Integer, Integer>() {{
put(100, 1);
put(200, 1);
}}));
assertTrue(result.localOnlyIds.isEmpty());
assertTrue(result.remoteOnlyIds.isEmpty());
assertTrue(result.getHasTypeMismatches());
}
@Test
public void findIdDifference_typeMismatch_someOverlap() {
IdDifferenceResult result = StorageSyncHelper.findIdDifference(keyListOf(new HashMap<Integer, Integer>() {{
put(100, 1);
put(200, 2);
put(300, 1);
}}),
keyListOf(new HashMap<Integer, Integer>() {{
put(100, 1);
put(200, 1);
put(400, 1);
}}));
assertContentsEqual(Arrays.asList(StorageId.forType(byteArray(300), 1)), result.remoteOnlyIds);
assertContentsEqual(Arrays.asList(StorageId.forType(byteArray(400), 1)), result.localOnlyIds);
assertTrue(result.getHasTypeMismatches());
}
@Test
public void ContactUpdate_equals_sameProfileKeys() {
byte[] profileKey = new byte[32];
byte[] profileKeyCopy = profileKey.clone();
ContactRecord contactA = contactBuilder(ACI_A, E164_A, "a").profileKey(ByteString.of(profileKey)).build();
ContactRecord contactB = contactBuilder(ACI_A, E164_A, "a").profileKey(ByteString.of(profileKeyCopy)).build();
SignalContactRecord signalContactA = new SignalContactRecord(StorageId.forContact(byteArray(1)), contactA);
SignalContactRecord signalContactB = new SignalContactRecord(StorageId.forContact(byteArray(1)), contactB);
assertEquals(signalContactA, signalContactB);
assertEquals(signalContactA.hashCode(), signalContactB.hashCode());
assertFalse(StorageSyncHelper.profileKeyChanged(update(signalContactA, signalContactB)));
}
@Test
public void ContactUpdate_equals_differentProfileKeys() {
byte[] profileKey = new byte[32];
byte[] profileKeyCopy = profileKey.clone();
profileKeyCopy[0] = 1;
ContactRecord contactA = contactBuilder(ACI_A, E164_A, "a").profileKey(ByteString.of(profileKey)).build();
ContactRecord contactB = contactBuilder(ACI_A, E164_A, "a").profileKey(ByteString.of(profileKeyCopy)).build();
SignalContactRecord signalContactA = new SignalContactRecord(StorageId.forContact(byteArray(1)), contactA);
SignalContactRecord signalContactB = new SignalContactRecord(StorageId.forContact(byteArray(1)), contactB);
assertNotEquals(signalContactA, signalContactB);
assertNotEquals(signalContactA.hashCode(), signalContactB.hashCode());
assertTrue(StorageSyncHelper.profileKeyChanged(update(signalContactA, signalContactB)));
}
private static ContactRecord.Builder contactBuilder(ACI aci, String e164, String profileName) {
return new ContactRecord.Builder()
.aci(aci.toString())
.e164(e164)
.givenName(profileName);
}
private static <E extends SignalRecord<?>> StorageRecordUpdate<E> update(E oldRecord, E newRecord) {
return new StorageRecordUpdate<>(oldRecord, newRecord);
}
private static List<StorageId> keyListOf(int... vals) {
return Stream.of(byteListOf(vals)).map(b -> StorageId.forType(b, 1)).toList();
}
private static List<StorageId> keyListOf(Map<Integer, Integer> vals) {
return Stream.of(vals).map(e -> StorageId.forType(byteArray(e.getKey()), e.getValue())).toList();
}
}

View file

@ -0,0 +1,150 @@
package org.thoughtcrime.securesms.storage
import okio.ByteString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.thoughtcrime.securesms.storage.StorageSyncHelper.findIdDifference
import org.thoughtcrime.securesms.storage.StorageSyncHelper.profileKeyChanged
import org.thoughtcrime.securesms.testutil.TestHelpers
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.ACI.Companion.parseOrThrow
import org.whispersystems.signalservice.api.storage.SignalContactRecord
import org.whispersystems.signalservice.api.storage.SignalRecord
import org.whispersystems.signalservice.api.storage.StorageId
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
class StorageSyncHelperTest {
@Test
fun findIdDifference_allOverlap() {
val result = findIdDifference(keyListOf(1, 2, 3), keyListOf(1, 2, 3))
assertTrue(result.localOnlyIds.isEmpty())
assertTrue(result.remoteOnlyIds.isEmpty())
assertFalse(result.hasTypeMismatches)
}
@Test
fun findIdDifference_noOverlap() {
val result = findIdDifference(keyListOf(1, 2, 3), keyListOf(4, 5, 6))
TestHelpers.assertContentsEqual(keyListOf(1, 2, 3), result.remoteOnlyIds)
TestHelpers.assertContentsEqual(keyListOf(4, 5, 6), result.localOnlyIds)
assertFalse(result.hasTypeMismatches)
}
@Test
fun findIdDifference_someOverlap() {
val result = findIdDifference(keyListOf(1, 2, 3), keyListOf(2, 3, 4))
TestHelpers.assertContentsEqual(keyListOf(1), result.remoteOnlyIds)
TestHelpers.assertContentsEqual(keyListOf(4), result.localOnlyIds)
assertFalse(result.hasTypeMismatches)
}
@Test
fun findIdDifference_typeMismatch_allOverlap() {
val result = findIdDifference(
keyListOf(
mapOf(
100 to 1,
200 to 2
)
),
keyListOf(
mapOf(
100 to 1,
200 to 1
)
)
)
assertTrue(result.localOnlyIds.isEmpty())
assertTrue(result.remoteOnlyIds.isEmpty())
assertTrue(result.hasTypeMismatches)
}
@Test
fun findIdDifference_typeMismatch_someOverlap() {
val result = findIdDifference(
keyListOf(
mapOf(
100 to 1,
200 to 2,
300 to 1
)
),
keyListOf(
mapOf(
100 to 1,
200 to 1,
400 to 1
)
)
)
TestHelpers.assertContentsEqual(listOf(StorageId.forType(TestHelpers.byteArray(300), 1)), result.remoteOnlyIds)
TestHelpers.assertContentsEqual(listOf(StorageId.forType(TestHelpers.byteArray(400), 1)), result.localOnlyIds)
assertTrue(result.hasTypeMismatches)
}
@Test
fun test_ContactUpdate_equals_sameProfileKeys() {
val profileKey = ByteArray(32)
val profileKeyCopy = profileKey.clone()
val contactA = contactBuilder(ACI_A, E164_A, "a").profileKey(ByteString.of(*profileKey)).build()
val contactB = contactBuilder(ACI_A, E164_A, "a").profileKey(ByteString.of(*profileKeyCopy)).build()
val signalContactA = SignalContactRecord(StorageId.forContact(TestHelpers.byteArray(1)), contactA)
val signalContactB = SignalContactRecord(StorageId.forContact(TestHelpers.byteArray(1)), contactB)
assertEquals(signalContactA, signalContactB)
assertEquals(signalContactA.hashCode(), signalContactB.hashCode())
assertFalse(profileKeyChanged(update(signalContactA, signalContactB)))
}
@Test
fun test_ContactUpdate_equals_differentProfileKeys() {
val profileKey = ByteArray(32)
val profileKeyCopy = profileKey.clone()
profileKeyCopy[0] = 1
val contactA = contactBuilder(ACI_A, E164_A, "a").profileKey(ByteString.of(*profileKey)).build()
val contactB = contactBuilder(ACI_A, E164_A, "a").profileKey(ByteString.of(*profileKeyCopy)).build()
val signalContactA = SignalContactRecord(StorageId.forContact(TestHelpers.byteArray(1)), contactA)
val signalContactB = SignalContactRecord(StorageId.forContact(TestHelpers.byteArray(1)), contactB)
assertNotEquals(signalContactA, signalContactB)
assertNotEquals(signalContactA.hashCode(), signalContactB.hashCode())
assertTrue(profileKeyChanged(update(signalContactA, signalContactB)))
}
companion object {
private val ACI_A = parseOrThrow("ebef429e-695e-4f51-bcc4-526a60ac68c7")
private const val E164_A = "+16108675309"
@Suppress("SameParameterValue")
private fun contactBuilder(aci: ACI, e164: String, profileName: String): ContactRecord.Builder {
return ContactRecord.Builder()
.aci(aci.toString())
.e164(e164)
.givenName(profileName)
}
private fun <E : SignalRecord<*>> update(oldRecord: E, newRecord: E): StorageRecordUpdate<E> {
return StorageRecordUpdate(oldRecord, newRecord)
}
private fun keyListOf(vararg vals: Int): List<StorageId> {
return TestHelpers.byteListOf(*vals).map { StorageId.forType(it, 1) }.toList()
}
private fun keyListOf(vals: Map<Int, Int>): List<StorageId> {
return vals.map { StorageId.forType(TestHelpers.byteArray(it.key), it.value) }.toList()
}
}
}

View file

@ -1,48 +0,0 @@
package org.thoughtcrime.securesms.testutil;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.security.SecureRandom;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
public final class SecureRandomTestUtil {
private SecureRandomTestUtil() {
}
/**
* Creates a {@link SecureRandom} that returns exactly the {@param returnValue} the first time
* its {@link SecureRandom#nextBytes(byte[])}} method is called.
* <p>
* Any attempt to call with the incorrect length, or a second time will fail.
*/
public static SecureRandom mockRandom(byte[] returnValue) {
SecureRandom mock = mock(SecureRandom.class);
ArgumentCaptor<byte[]> argument = ArgumentCaptor.forClass(byte[].class);
doAnswer(new Answer<Void>() {
private int count;
@Override
public Void answer(InvocationOnMock invocation) {
assertEquals("SecureRandom Mock: nextBytes only expected to be called once", 1, ++count);
byte[] output = argument.getValue();
assertEquals("SecureRandom Mock: nextBytes byte[] length requested does not match byte[] setup", returnValue.length, output.length);
System.arraycopy(returnValue, 0, output, 0, returnValue.length);
return null;
}
}).when(mock).nextBytes(argument.capture());
return mock;
}
}

View file

@ -0,0 +1,31 @@
package org.thoughtcrime.securesms.testutil
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import org.junit.Assert.assertEquals
import java.security.SecureRandom
object SecureRandomTestUtil {
/**
* Creates a [SecureRandom] that returns exactly the {@param returnValue} the first time
* its [SecureRandom.nextBytes]} method is called.
*
* Any attempt to call with the incorrect length, or a second time will fail.
*/
@JvmStatic
fun mockRandom(returnValue: ByteArray): SecureRandom {
return mockk<SecureRandom> {
var count = 0
val slot = slot<ByteArray>()
every {
nextBytes(capture(slot))
} answers {
assertEquals("SecureRandom Mock: nextBytes only expected to be called once", 1, ++count)
val output = slot.captured
assertEquals("SecureRandom Mock: nextBytes byte[] length requested does not match byte[] setup", returnValue.size, output.size)
System.arraycopy(returnValue, 0, output, 0, returnValue.size)
}
}
}
}