From 0535b1b4fb0efe162280596bd41461c841e33fb5 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Mon, 23 Sep 2024 11:44:15 -0300 Subject: [PATCH] Encrypt flag names for deletion --- .../splits/SqLitePersistentSplitsStorage.java | 17 +- .../SqLitePersistentSplitsStorageTest.java | 167 +++++++++++++++++- 2 files changed, 178 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java b/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java index aa1906feb..14aa16835 100644 --- a/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java +++ b/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java @@ -24,6 +24,7 @@ public class SqLitePersistentSplitsStorage implements PersistentSplitsStorage { private final SplitListTransformer mEntityToSplitTransformer; private final SplitListTransformer mSplitToEntityTransformer; private final SplitRoomDatabase mDatabase; + private final SplitCipher mCipher; public SqLitePersistentSplitsStorage(@NonNull SplitRoomDatabase database, @NonNull SplitCipher splitCipher) { this(database, new SplitParallelTaskExecutorFactoryImpl(), splitCipher); @@ -32,10 +33,12 @@ public SqLitePersistentSplitsStorage(@NonNull SplitRoomDatabase database, @NonNu @VisibleForTesting public SqLitePersistentSplitsStorage(@NonNull SplitRoomDatabase database, @NonNull SplitListTransformer entityToSplitTransformer, - @NonNull SplitListTransformer splitToEntityTransformer) { + @NonNull SplitListTransformer splitToEntityTransformer, + @NonNull SplitCipher cipher) { mDatabase = checkNotNull(database); mEntityToSplitTransformer = checkNotNull(entityToSplitTransformer); mSplitToEntityTransformer = checkNotNull(splitToEntityTransformer); + mCipher = checkNotNull(cipher); } private SqLitePersistentSplitsStorage(@NonNull SplitRoomDatabase database, @@ -43,7 +46,8 @@ private SqLitePersistentSplitsStorage(@NonNull SplitRoomDatabase database, @NonNull SplitCipher splitCipher) { this(database, new SplitEntityToSplitTransformer(executorFactory.createForList(Split.class), splitCipher), - new SplitToSplitEntityTransformer(executorFactory.createForList(SplitEntity.class), splitCipher)); + new SplitToSplitEntityTransformer(executorFactory.createForList(SplitEntity.class), splitCipher), + splitCipher); } @Override @@ -104,8 +108,13 @@ public void updateFlagsSpec(String flagsSpec) { @Override public void delete(List splitNames) { + List encryptedNames = new ArrayList<>(); + for (String splitName : splitNames) { + encryptedNames.add(mCipher.encrypt(splitName)); + } + // This is to avoid an sqlite error if there are many split to delete - List> deleteChunk = partition(splitNames, SQL_PARAM_BIND_SIZE); + List> deleteChunk = partition(encryptedNames, SQL_PARAM_BIND_SIZE); for (List splits : deleteChunk) { mDatabase.splitDao().delete(splits); } @@ -152,7 +161,7 @@ private List splitNameList(List splits) { return names; } for (Split split : splits) { - names.add(split.name); + names.add(mCipher.encrypt(split.name)); } return names; } diff --git a/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java b/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java index 9f6725884..ea28aff03 100644 --- a/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java +++ b/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java @@ -1,20 +1,29 @@ package io.split.android.client.storage.splits; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import io.split.android.client.dtos.Split; +import io.split.android.client.storage.cipher.SplitCipher; import io.split.android.client.storage.db.GeneralInfoDao; import io.split.android.client.storage.db.GeneralInfoEntity; import io.split.android.client.storage.db.SplitDao; @@ -31,13 +40,31 @@ public class SqLitePersistentSplitsStorageTest { private SplitListTransformer mSplitToSplitEntityTransformer; @Mock private SplitDao mSplitDao; + @Mock + private SplitCipher mCipher; private SqLitePersistentSplitsStorage mStorage; + private AutoCloseable mAutoCloseable; @Before public void setUp() { - MockitoAnnotations.openMocks(this); + mAutoCloseable = MockitoAnnotations.openMocks(this); + when(mDatabase.generalInfoDao()).thenReturn(mock(GeneralInfoDao.class)); + when(mDatabase.splitDao()).thenReturn(mSplitDao); + doAnswer((Answer) invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }).when(mDatabase).runInTransaction(any(Runnable.class)); + instantiateStorage(invocation -> invocation.getArgument(0)); + } - mStorage = new SqLitePersistentSplitsStorage(mDatabase, mEntityToSplitTransformer, mSplitToSplitEntityTransformer); + private void instantiateStorage(Answer encryptionAnswer) { + when(mCipher.encrypt(any())).thenAnswer(encryptionAnswer); + mStorage = new SqLitePersistentSplitsStorage(mDatabase, mEntityToSplitTransformer, mSplitToSplitEntityTransformer, mCipher); + } + + @After + public void tearDown() throws Exception { + mAutoCloseable.close(); } @Test @@ -72,6 +99,142 @@ public void getFlagsSpecFetchesValueFromGeneralInfoDao() { assertEquals("2.5", flagsSpec); } + @Test + public void getFlagsSpecReturnsNullWhenItIsNotSet() { + GeneralInfoDao generalInfoDao = mock(GeneralInfoDao.class); + when(mDatabase.generalInfoDao()).thenReturn(generalInfoDao); + when(generalInfoDao.getByName("flagsSpec")).thenReturn(null); + + String flagsSpec = mStorage.getFlagsSpec(); + + assertNull(flagsSpec); + } + + @Test + public void updateRemovesEncryptedSplitNames() { + List activeSplits = Collections.emptyList(); + List archivedSplits = new ArrayList<>(); + long changeNumber = 9999; + long timestamp = 123456789; + + Split split1 = new Split(); + split1.name = "split-1"; + archivedSplits.add(split1); + Split split2 = new Split(); + split2.name = "split-2"; + archivedSplits.add(split2); + Split split3 = new Split(); + split3.name = "split-3"; + archivedSplits.add(split3); + ProcessedSplitChange change = new ProcessedSplitChange(activeSplits, archivedSplits, changeNumber, timestamp); + when(mCipher.encrypt(any())).thenAnswer((Answer) invocation -> invocation.getArgument(0) + "_encrypted"); + + mStorage.update(change); + + verify(mSplitDao).delete(argThat(list -> list.contains("split-1_encrypted") && list.contains("split-2_encrypted") && list.contains("split-3_encrypted") && list.size() == 3)); + } + + @Test + public void updateForSplitChangeUsesTransformer() { + List activeSplits = new ArrayList<>(); + List archivedSplits = Collections.emptyList(); + long changeNumber = 9999; + long timestamp = 123456789; + + Split split1 = new Split(); + split1.name = "split-1"; + activeSplits.add(split1); + Split split2 = new Split(); + split2.name = "split-2"; + activeSplits.add(split2); + Split split3 = new Split(); + split3.name = "split-3"; + activeSplits.add(split3); + ProcessedSplitChange change = new ProcessedSplitChange(activeSplits, archivedSplits, changeNumber, timestamp); + when(mCipher.encrypt(any())).thenAnswer((Answer) invocation -> invocation.getArgument(0) + "_encrypted"); + + mStorage.update(change); + + verify(mSplitToSplitEntityTransformer).transform(activeSplits); + } + + @Test + public void updatingNullSplitChangeDoesNotInteractWithDatabase() { + mStorage.update((ProcessedSplitChange) null); + + verifyNoInteractions(mSplitToSplitEntityTransformer); + verifyNoInteractions(mCipher); + verifyNoInteractions(mDatabase); + } + + @Test + public void deleteRemovesEncryptedSplitNames() { + when(mCipher.encrypt(any())).then((Answer) invocation -> invocation.getArgument(0) + "_encrypted"); + + mStorage.delete(Collections.singletonList("split-1")); + + verify(mSplitDao).delete(Collections.singletonList("split-1_encrypted")); + } + + @Test + public void clearResetsChangeNumberAndRemovesAllFlags() { + mStorage.clear(); + + verify(mDatabase.generalInfoDao()).update(argThat(new ArgumentMatcher() { + @Override + public boolean matches(GeneralInfoEntity argument) { + return argument.getName().equals(GeneralInfoEntity.CHANGE_NUMBER_INFO) && argument.getLongValue() == -1; + } + })); + verify(mDatabase.splitDao()).deleteAll(); + } + + @Test + public void getFilterQueryStringReturnsNullWhenItIsNotSet() { + when(mDatabase.generalInfoDao().getByName(GeneralInfoEntity.SPLITS_FILTER_QUERY_STRING)).thenReturn(null); + + String filterQueryString = mStorage.getFilterQueryString(); + + assertNull(filterQueryString); + } + + @Test + public void getFilterQueryStringReturnsStringValueWhenSet() { + when(mDatabase.generalInfoDao().getByName(GeneralInfoEntity.SPLITS_FILTER_QUERY_STRING)).thenReturn(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_FILTER_QUERY_STRING, "filterQueryString")); + + String filterQueryString = mStorage.getFilterQueryString(); + + assertEquals("filterQueryString", filterQueryString); + } + + @Test + public void updateFilterQueryStringUpdatesItInGeneralInfo() { + GeneralInfoDao generalInfoDao = mock(GeneralInfoDao.class); + when(mDatabase.generalInfoDao()).thenReturn(generalInfoDao); + + mStorage.updateFilterQueryString("filterQueryString"); + + verify(generalInfoDao).update(argThat(new ArgumentMatcher() { + @Override + public boolean matches(GeneralInfoEntity argument) { + return argument.getStringValue().equals("filterQueryString") && argument.getName().equals(GeneralInfoEntity.SPLITS_FILTER_QUERY_STRING); + } + })); + } + + @Test + public void updateSingleSplitUsesTransformer() { + Split split = new Split(); + split.name = "split-1"; + SplitEntity entity = new SplitEntity(); + entity.setName("split-1"); + when(mSplitToSplitEntityTransformer.transform(Collections.singletonList(split))).thenReturn(Collections.singletonList(entity)); + + mStorage.update(split); + + verify(mSplitToSplitEntityTransformer).transform(Collections.singletonList(split)); + } + private List getMockEntities() { ArrayList entities = new ArrayList<>(); String jsonTemplate = "{\"name\":\"%s\", \"changeNumber\": %d}";