diff --git a/src/main/java/io/split/android/client/storage/rbs/Clearer.java b/src/main/java/io/split/android/client/storage/rbs/Clearer.java new file mode 100644 index 000000000..1e0ab4a9d --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/Clearer.java @@ -0,0 +1,29 @@ +package io.split.android.client.storage.rbs; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.general.GeneralInfoStorage; +import io.split.android.client.utils.logger.Logger; + +class Clearer implements Runnable { + + private final RuleBasedSegmentDao mDao; + private final GeneralInfoStorage mGeneralInfoStorage; + + public Clearer(RuleBasedSegmentDao dao, GeneralInfoStorage generalInfoStorage) { + mDao = checkNotNull(dao); + mGeneralInfoStorage = checkNotNull(generalInfoStorage); + } + + @Override + public void run() { + try { + mDao.deleteAll(); + mGeneralInfoStorage.setRbsChangeNumber(-1); + } catch (Exception e) { + Logger.e("Error clearing RBS: " + e.getLocalizedMessage()); + throw e; + } + } +} diff --git a/src/main/java/io/split/android/client/storage/rbs/PersistentRuleBasedSegmentStorage.java b/src/main/java/io/split/android/client/storage/rbs/PersistentRuleBasedSegmentStorage.java index c5329bf07..e2bd35634 100644 --- a/src/main/java/io/split/android/client/storage/rbs/PersistentRuleBasedSegmentStorage.java +++ b/src/main/java/io/split/android/client/storage/rbs/PersistentRuleBasedSegmentStorage.java @@ -11,4 +11,9 @@ public interface PersistentRuleBasedSegmentStorage { void update(Set toAdd, Set toRemove, long changeNumber); void clear(); + + interface Provider { + + PersistentRuleBasedSegmentStorage get(); + } } diff --git a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentSnapshot.java b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentSnapshot.java index fdb618bcd..69f050398 100644 --- a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentSnapshot.java +++ b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentSnapshot.java @@ -1,21 +1,25 @@ package io.split.android.client.storage.rbs; -import java.util.Set; +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.NonNull; + +import java.util.Map; import io.split.android.client.dtos.RuleBasedSegment; public class RuleBasedSegmentSnapshot { - private final Set mSegments; + private final Map mSegments; private final long mChangeNumber; - public RuleBasedSegmentSnapshot(Set segments, long changeNumber) { - mSegments = segments; + public RuleBasedSegmentSnapshot(@NonNull Map segments, long changeNumber) { + mSegments = checkNotNull(segments); mChangeNumber = changeNumber; } - public Set getSegments() { + public Map getSegments() { return mSegments; } diff --git a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImpl.java b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImpl.java index 013092227..80b5759b3 100644 --- a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImpl.java +++ b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImpl.java @@ -6,6 +6,7 @@ import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; +import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -77,13 +78,11 @@ public boolean contains(@NonNull Set segmentNames) { @WorkerThread @Override - public void loadLocal() { + public synchronized void loadLocal() { RuleBasedSegmentSnapshot snapshot = mPersistentStorage.getSnapshot(); - Set segments = snapshot.getSegments(); + Map segments = snapshot.getSegments(); mChangeNumber = snapshot.getChangeNumber(); - for (RuleBasedSegment segment : segments) { - mInMemorySegments.put(segment.getName(), segment); - } + mInMemorySegments.putAll(segments); } @WorkerThread diff --git a/src/main/java/io/split/android/client/storage/rbs/SnapshotLoader.java b/src/main/java/io/split/android/client/storage/rbs/SnapshotLoader.java new file mode 100644 index 000000000..d56a54a66 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/SnapshotLoader.java @@ -0,0 +1,68 @@ +package io.split.android.client.storage.rbs; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentEntity; +import io.split.android.client.storage.general.GeneralInfoStorage; +import io.split.android.client.utils.Json; +import io.split.android.client.utils.logger.Logger; + +final class SnapshotLoader implements Callable { + + private final RuleBasedSegmentDao mDao; + private final SplitCipher mCipher; + private final GeneralInfoStorage mGeneralInfoStorage; + + SnapshotLoader(RuleBasedSegmentDao dao, SplitCipher cipher, GeneralInfoStorage generalInfoStorage) { + mDao = checkNotNull(dao); + mCipher = checkNotNull(cipher); + mGeneralInfoStorage = checkNotNull(generalInfoStorage); + } + + @Override + public RuleBasedSegmentSnapshot call() { + try { + long changeNumber = mGeneralInfoStorage.getFlagsChangeNumber(); + List entities = mDao.getAll(); + Map segments = convertToDTOs(entities); + + return new RuleBasedSegmentSnapshot(segments, changeNumber); + } catch (Exception e) { + Logger.e("Error loading RBS from persistent storage", e.getLocalizedMessage()); + throw e; + } + } + + @NonNull + private Map convertToDTOs(@Nullable List entities) { + Map segments = new HashMap<>(); + if (entities != null) { + for (RuleBasedSegmentEntity entity : entities) { + String name = mCipher.decrypt(entity.getName()); + String body = mCipher.decrypt(entity.getBody()); + if (name == null || body == null) { + continue; + } + + try { + RuleBasedSegment ruleBasedSegment = Json.fromJson(body, RuleBasedSegment.class); + segments.put(name, ruleBasedSegment); + } catch (Exception e) { + Logger.e("Error parsing RBS with name " + name + ": " + e.getLocalizedMessage()); + } + } + } + return segments; + } +} diff --git a/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorage.java b/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorage.java new file mode 100644 index 000000000..63d09f09b --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorage.java @@ -0,0 +1,43 @@ +package io.split.android.client.storage.rbs; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import java.util.Set; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.general.GeneralInfoStorage; + +class SqLitePersistentRuleBasedSegmentStorage implements PersistentRuleBasedSegmentStorage { + + private final RuleBasedSegmentDao mDao; + private final SplitRoomDatabase mDatabase; + private final GeneralInfoStorage mGeneralInfoStorage; + private final SplitCipher mCipher; + + public SqLitePersistentRuleBasedSegmentStorage(SplitCipher cipher, + SplitRoomDatabase database, + GeneralInfoStorage generalInfoStorage) { + mCipher = checkNotNull(cipher); + mDatabase = checkNotNull(database); + mDao = database.ruleBasedSegmentDao(); + mGeneralInfoStorage = checkNotNull(generalInfoStorage); + } + + @Override + public RuleBasedSegmentSnapshot getSnapshot() { + return mDatabase.runInTransaction(new SnapshotLoader(mDao, mCipher, mGeneralInfoStorage)); + } + + @Override + public void update(Set toAdd, Set toRemove, long changeNumber) { + mDatabase.runInTransaction(new Updater(mCipher, mDao, mGeneralInfoStorage, toAdd, toRemove, changeNumber)); + } + + @Override + public void clear() { + mDatabase.runInTransaction(new Clearer(mDao, mGeneralInfoStorage)); + } +} diff --git a/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageProvider.java b/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageProvider.java new file mode 100644 index 000000000..eb5a279b6 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageProvider.java @@ -0,0 +1,19 @@ +package io.split.android.client.storage.rbs; + +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.storage.general.GeneralInfoStorage; + +public class SqLitePersistentRuleBasedSegmentStorageProvider implements PersistentRuleBasedSegmentStorage.Provider { + + private final SqLitePersistentRuleBasedSegmentStorage mPersistentStorage; + + public SqLitePersistentRuleBasedSegmentStorageProvider(SplitCipher cipher, SplitRoomDatabase database, GeneralInfoStorage generalInfoStorage) { + mPersistentStorage = new SqLitePersistentRuleBasedSegmentStorage(cipher, database, generalInfoStorage); + } + + @Override + public PersistentRuleBasedSegmentStorage get() { + return mPersistentStorage; + } +} diff --git a/src/main/java/io/split/android/client/storage/rbs/Updater.java b/src/main/java/io/split/android/client/storage/rbs/Updater.java new file mode 100644 index 000000000..12e5c3fb2 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/Updater.java @@ -0,0 +1,84 @@ +package io.split.android.client.storage.rbs; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentEntity; +import io.split.android.client.storage.general.GeneralInfoStorage; +import io.split.android.client.utils.Json; +import io.split.android.client.utils.logger.Logger; + +final class Updater implements Runnable { + + @NonNull + private final SplitCipher mCipher; + @NonNull + private final GeneralInfoStorage mGeneralInfoStorage; + @NonNull + private final RuleBasedSegmentDao mDao; + @NonNull + private final Set mToAdd; + @NonNull + private final Set mToRemove; + private final long mChangeNumber; + + Updater(@NonNull SplitCipher cipher, + @NonNull RuleBasedSegmentDao dao, + @NonNull GeneralInfoStorage generalInfoStorage, + @NonNull Set toAdd, + @NonNull Set toRemove, + long changeNumber) { + mCipher = checkNotNull(cipher); + mDao = checkNotNull(dao); + mGeneralInfoStorage = checkNotNull(generalInfoStorage); + mToAdd = checkNotNull(toAdd); + mToRemove = checkNotNull(toRemove); + mChangeNumber = changeNumber; + } + + @Override + public void run() { + try { + List toDelete = new ArrayList<>(); + for (RuleBasedSegment segment : mToRemove) { + String encryptedName = mCipher.encrypt(segment.getName()); + if (encryptedName != null) { + toDelete.add(encryptedName); + } + } + + List toAdd = new ArrayList<>(); + for (RuleBasedSegment segment : mToAdd) { + if (segment == null) { + continue; + } + + try { + String name = mCipher.encrypt(segment.getName()); + String body = mCipher.encrypt(Json.toJson(segment)); + if (name == null || body == null) { + continue; + } + toAdd.add(new RuleBasedSegmentEntity(name, body, System.currentTimeMillis())); + } catch (Exception e) { + Logger.e("Error parsing RBS with name " + segment.getName() + ": " + e.getLocalizedMessage()); + } + } + + mDao.delete(toDelete); + mDao.insert(toAdd); + mGeneralInfoStorage.setRbsChangeNumber(mChangeNumber); + } catch (Exception e) { + Logger.e("Error updating RBS: " + e.getLocalizedMessage()); + throw e; + } + } +} diff --git a/src/sharedTest/java/helper/TestingHelper.java b/src/sharedTest/java/helper/TestingHelper.java index 2d70138d2..af190706d 100644 --- a/src/sharedTest/java/helper/TestingHelper.java +++ b/src/sharedTest/java/helper/TestingHelper.java @@ -1,5 +1,8 @@ package helper; +import static java.lang.Thread.sleep; + +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; @@ -12,8 +15,6 @@ import io.split.android.client.events.SplitEventTask; import io.split.android.client.utils.logger.Logger; -import static java.lang.Thread.sleep; - public class TestingHelper { public static final String COUNTERS_REFRESH_RATE_SECS_NAME = "COUNTERS_REFRESH_RATE_SECS"; @@ -120,4 +121,14 @@ public static KeyImpression newImpression(String feature, String key) { impression.time = 100; return impression; } + + public static Object getFieldValue(Object object, String fieldName) { + try { + Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(object); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException("Error accessing field: " + fieldName, e); + } + } } diff --git a/src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImplTest.java b/src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImplTest.java index 5af577c6a..d19b02225 100644 --- a/src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImplTest.java +++ b/src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImplTest.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.Map; import java.util.Set; import io.split.android.client.dtos.Excluded; @@ -159,7 +160,7 @@ public void updateReturnsTrueWhenThereWereAddedSegments() { @Test public void loadLocalGetsSnapshotFromPersistentStorage() { - when(mPersistentStorage.getSnapshot()).thenReturn(new RuleBasedSegmentSnapshot(Set.of(), 1)); + when(mPersistentStorage.getSnapshot()).thenReturn(new RuleBasedSegmentSnapshot(Map.of(), 1)); storage.loadLocal(); verify(mPersistentStorage).getSnapshot(); @@ -167,7 +168,7 @@ public void loadLocalGetsSnapshotFromPersistentStorage() { @Test public void loadLocalPopulatesValues() { - RuleBasedSegmentSnapshot snapshot = new RuleBasedSegmentSnapshot(Set.of(createRuleBasedSegment("segment1")), + RuleBasedSegmentSnapshot snapshot = new RuleBasedSegmentSnapshot(Map.of("segment1", createRuleBasedSegment("segment1")), 1); when(mPersistentStorage.getSnapshot()).thenReturn(snapshot); @@ -192,7 +193,7 @@ public void clearCallsClearOnPersistentStorage() { verify(mPersistentStorage).clear(); } - private static RuleBasedSegment createRuleBasedSegment(String name) { + static RuleBasedSegment createRuleBasedSegment(String name) { return new RuleBasedSegment(name, "user", 1, diff --git a/src/test/java/io/split/android/client/storage/rbs/SnapshotLoaderTest.java b/src/test/java/io/split/android/client/storage/rbs/SnapshotLoaderTest.java new file mode 100644 index 000000000..50bc36d81 --- /dev/null +++ b/src/test/java/io/split/android/client/storage/rbs/SnapshotLoaderTest.java @@ -0,0 +1,118 @@ +package io.split.android.client.storage.rbs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentEntity; +import io.split.android.client.storage.general.GeneralInfoStorage; + +public class SnapshotLoaderTest { + + private RuleBasedSegmentDao mDao; + private SplitCipher mCipher; + private GeneralInfoStorage mGeneralInfoStorage; + private SnapshotLoader mSnapshotLoader; + + @Before + public void setUp() { + mDao = mock(RuleBasedSegmentDao.class); + mCipher = mock(SplitCipher.class); + mGeneralInfoStorage = mock(GeneralInfoStorage.class); + mSnapshotLoader = new SnapshotLoader(mDao, mCipher, mGeneralInfoStorage); + } + + @Test + public void callReturnsCorrectSnapshotWithDecryptedSegments() throws Exception { + long expectedChangeNumber = 123L; + when(mGeneralInfoStorage.getFlagsChangeNumber()).thenReturn(expectedChangeNumber); + + RuleBasedSegmentEntity entity1 = new RuleBasedSegmentEntity("segment1", "encryptedBody1", System.currentTimeMillis()); + RuleBasedSegmentEntity entity2 = new RuleBasedSegmentEntity("segment2", "encryptedBody2", System.currentTimeMillis()); + List entities = Arrays.asList(entity1, entity2); + when(mDao.getAll()).thenReturn(entities); + + when(mCipher.decrypt("segment1")).thenReturn("segment1"); + when(mCipher.decrypt("segment2")).thenReturn("segment2"); + when(mCipher.decrypt("encryptedBody1")).thenAnswer(invocation -> "{ \"name\": \"segment1\", \"trafficTypeName\": \"user\", \"changeNumber\": 1 }"); + when(mCipher.decrypt("encryptedBody2")).thenAnswer(invocation -> "{ \"name\": \"segment2\", \"trafficTypeName\": \"user\", \"changeNumber\": 2 }"); + + RuleBasedSegmentSnapshot result = mSnapshotLoader.call(); + + assertNotNull(result); + assertEquals(expectedChangeNumber, result.getChangeNumber()); + + Map segments = result.getSegments(); + assertEquals(2, segments.size()); + RuleBasedSegment rbs1 = segments.get("segment1"); + assertNotNull(rbs1); + assertEquals("segment1", rbs1.getName()); + assertEquals("user", rbs1.getTrafficTypeName()); + assertEquals(1, rbs1.getChangeNumber()); + + RuleBasedSegment rbs2 = segments.get("segment2"); + assertNotNull(rbs2); + assertEquals("segment2", rbs2.getName()); + assertEquals("user", rbs2.getTrafficTypeName()); + assertEquals(2, rbs2.getChangeNumber()); + } + + @Test + public void callGetsChangeNumberFromGeneralInfoStorage() { + mSnapshotLoader.call(); + + verify(mGeneralInfoStorage).getFlagsChangeNumber(); + } + + @Test + public void callGetsAllSegmentsFromDao() { + mSnapshotLoader.call(); + + verify(mDao).getAll(); + } + + @Test + public void callDecryptsNameAndBodyFromEntity() { + when(mDao.getAll()).thenReturn(Arrays.asList( + new RuleBasedSegmentEntity("segment1", "encryptedBody1", System.currentTimeMillis()), + new RuleBasedSegmentEntity("segment2", "encryptedBody2", System.currentTimeMillis()))); + + mSnapshotLoader.call(); + + verify(mCipher).decrypt("segment1"); + verify(mCipher).decrypt("segment2"); + verify(mCipher).decrypt("encryptedBody1"); + verify(mCipher).decrypt("encryptedBody2"); + } + + @Test + public void constructorThrowsNullPointerExceptionWhenDaoIsNull() { + assertThrows(NullPointerException.class, + () -> new SnapshotLoader(null, mCipher, mGeneralInfoStorage)); + } + + @Test + public void constructorThrowsNullPointerExceptionWhenCipherIsNull() { + assertThrows(NullPointerException.class, + () -> new SnapshotLoader(mDao, null, mGeneralInfoStorage)); + } + + @Test + public void constructorThrowsNullPointerExceptionWhenGeneralInfoStorageIsNull() { + assertThrows(NullPointerException.class, + () -> new SnapshotLoader(mDao, mCipher, null)); + } +} diff --git a/src/test/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageTest.java b/src/test/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageTest.java new file mode 100644 index 000000000..13c646e0c --- /dev/null +++ b/src/test/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageTest.java @@ -0,0 +1,107 @@ +package io.split.android.client.storage.rbs; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static helper.TestingHelper.getFieldValue; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.util.HashSet; +import java.util.Set; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.general.GeneralInfoStorage; + +public class SqLitePersistentRuleBasedSegmentStorageTest { + + private SplitCipher mCipher; + private SplitRoomDatabase mDatabase; + private RuleBasedSegmentDao mDao; + private GeneralInfoStorage mGeneralInfoStorage; + + private SqLitePersistentRuleBasedSegmentStorage storage; + + @Before + public void setUp() { + mCipher = mock(SplitCipher.class); + mDatabase = mock(SplitRoomDatabase.class); + mDao = mock(RuleBasedSegmentDao.class); + mGeneralInfoStorage = mock(GeneralInfoStorage.class); + when(mDatabase.ruleBasedSegmentDao()).thenReturn(mDao); + + storage = new SqLitePersistentRuleBasedSegmentStorage(mCipher, mDatabase, mGeneralInfoStorage); + } + + @Test + public void getSnapshotBuildsAndRunsSnapshotLoaderInstanceInTransaction() { + RuleBasedSegmentSnapshot expectedSnapshot = mock(RuleBasedSegmentSnapshot.class); + when(mDatabase.runInTransaction((SnapshotLoader) any())).thenReturn(expectedSnapshot); + + RuleBasedSegmentSnapshot result = storage.getSnapshot(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SnapshotLoader.class); + verify(mDatabase).runInTransaction(captor.capture()); + SnapshotLoader snapshotLoader = captor.getValue(); + assertSame(mDao, getFieldValue(snapshotLoader, "mDao")); + assertSame(mCipher, getFieldValue(snapshotLoader, "mCipher")); + assertSame(mGeneralInfoStorage, getFieldValue(snapshotLoader, "mGeneralInfoStorage")); + assertSame(expectedSnapshot, result); + } + + @Test + public void updateBuildsAndRunsUpdaterInstanceInTransaction() { + Set toAdd = new HashSet<>(); + Set toRemove = new HashSet<>(); + long changeNumber = 123L; + + storage.update(toAdd, toRemove, changeNumber); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Updater.class); + verify(mDatabase).runInTransaction(captor.capture()); + Updater updater = captor.getValue(); + assertSame(mCipher, getFieldValue(updater, "mCipher")); + assertSame(mDao, getFieldValue(updater, "mDao")); + assertSame(mGeneralInfoStorage, getFieldValue(updater, "mGeneralInfoStorage")); + assertSame(toAdd, getFieldValue(updater, "mToAdd")); + assertSame(toRemove, getFieldValue(updater, "mToRemove")); + assertSame(changeNumber, getFieldValue(updater, "mChangeNumber")); + } + + @Test + public void clearBuildsAndRunsClearerInstanceInTransaction() { + storage.clear(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Clearer.class); + verify(mDatabase).runInTransaction(captor.capture()); + Clearer clearer = captor.getValue(); + assertSame(mDao, getFieldValue(clearer, "mDao")); + assertSame(mGeneralInfoStorage, getFieldValue(clearer, "mGeneralInfoStorage")); + } + + @Test + public void cipherCannotBeNull() { + assertThrows(NullPointerException.class, + () -> new SqLitePersistentRuleBasedSegmentStorage(null, mDatabase, mGeneralInfoStorage)); + } + + @Test + public void databaseCannotBeNull() { + assertThrows(NullPointerException.class, + () -> new SqLitePersistentRuleBasedSegmentStorage(mCipher, null, mGeneralInfoStorage)); + } + + @Test + public void generalInfoStorageCannotBeNull() { + assertThrows(NullPointerException.class, + () -> new SqLitePersistentRuleBasedSegmentStorage(mCipher, mDatabase, null)); + } +} diff --git a/src/test/java/io/split/android/client/storage/rbs/SqLiteRuleBasedSegmentsPersistentStorageProviderTest.java b/src/test/java/io/split/android/client/storage/rbs/SqLiteRuleBasedSegmentsPersistentStorageProviderTest.java new file mode 100644 index 000000000..a6cd2dee8 --- /dev/null +++ b/src/test/java/io/split/android/client/storage/rbs/SqLiteRuleBasedSegmentsPersistentStorageProviderTest.java @@ -0,0 +1,22 @@ +package io.split.android.client.storage.rbs; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import org.junit.Test; + +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.storage.general.GeneralInfoStorage; + +public class SqLiteRuleBasedSegmentsPersistentStorageProviderTest { + + @Test + public void providesSqLiteImplementation() { + PersistentRuleBasedSegmentStorage.Provider provider = + new SqLitePersistentRuleBasedSegmentStorageProvider(mock(SplitCipher.class), mock(SplitRoomDatabase.class), mock(GeneralInfoStorage.class)); + PersistentRuleBasedSegmentStorage persistentRuleBasedSegmentStorage = provider.get(); + + assertTrue(persistentRuleBasedSegmentStorage instanceof SqLitePersistentRuleBasedSegmentStorage); + } +} diff --git a/src/test/java/io/split/android/client/storage/rbs/UpdaterTest.java b/src/test/java/io/split/android/client/storage/rbs/UpdaterTest.java new file mode 100644 index 000000000..edb779f98 --- /dev/null +++ b/src/test/java/io/split/android/client/storage/rbs/UpdaterTest.java @@ -0,0 +1,125 @@ +package io.split.android.client.storage.rbs; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static io.split.android.client.storage.rbs.RuleBasedSegmentStorageImplTest.createRuleBasedSegment; + +import androidx.annotation.NonNull; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatcher; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentEntity; +import io.split.android.client.storage.general.GeneralInfoStorage; + +public class UpdaterTest { + + private SplitCipher mCipher; + private RuleBasedSegmentDao mDao; + private GeneralInfoStorage mGeneralInfoStorage; + private Updater mUpdater; + + @Before + public void setUp() { + mCipher = mock(SplitCipher.class); + mDao = mock(RuleBasedSegmentDao.class); + mGeneralInfoStorage = mock(GeneralInfoStorage.class); + } + + @Test + public void runEncryptsRemovedSegmentNamesBeforeSendingToDao() { + Set toRemove = Set.of( + createRuleBasedSegment("segment1"), createRuleBasedSegment("segment2")); + when(mCipher.encrypt(any())).thenAnswer(invocation -> "encrypted_" + invocation.getArgument(0)); + mUpdater = createUpdater(Collections.emptySet(), toRemove, 10); + + mUpdater.run(); + + verify(mCipher).encrypt("segment1"); + verify(mCipher).encrypt("segment2"); + verify(mDao).delete(argThat(new ArgumentMatcher>() { + @Override + public boolean matches(List argument) { + return argument.size() == 2 && + argument.contains("encrypted_segment1") && + argument.contains("encrypted_segment2"); + } + })); + } + + @Test + public void runEncryptsAddedSegmentNamesBeforeSendingToDao() { + Set toAdd = Set.of( + createRuleBasedSegment("segment1"), createRuleBasedSegment("segment2")); + when(mCipher.encrypt(any())).thenAnswer(invocation -> "encrypted_" + invocation.getArgument(0)); + mUpdater = createUpdater(toAdd, Collections.emptySet(), 10); + + mUpdater.run(); + + verify(mCipher).encrypt("segment1"); + verify(mCipher).encrypt("segment2"); + verify(mDao).insert(argThat(new ArgumentMatcher>() { + @Override + public boolean matches(List argument) { + argument.sort(Comparator.comparing(RuleBasedSegmentEntity::getName)); + RuleBasedSegmentEntity ruleBasedSegmentEntity = argument.get(0); + RuleBasedSegmentEntity ruleBasedSegmentEntity1 = argument.get(1); + return argument.size() == 2 && + ruleBasedSegmentEntity.getName().equals("encrypted_segment1") && + ruleBasedSegmentEntity1.getName().equals("encrypted_segment2") && + ruleBasedSegmentEntity.getBody().startsWith("encrypted_") && + ruleBasedSegmentEntity1.getBody().startsWith("encrypted_"); + } + })); + } + + @Test + public void runUpdatesChangeNumber() { + mUpdater = createUpdater(Collections.emptySet(), Collections.emptySet(), 10); + + mUpdater.run(); + + verify(mGeneralInfoStorage).setRbsChangeNumber(10); + } + + @Test + public void runDoesNotUpdateSegmentIfEncryptedNameIsNull() { + Set toAdd = Set.of( + createRuleBasedSegment("segment1"), createRuleBasedSegment("segment2")); + Set toRemove = Set.of( + createRuleBasedSegment("segment3"), createRuleBasedSegment("segment4")); + when(mCipher.encrypt(anyString())).thenReturn(null); + when(mCipher.encrypt(argThat(argument -> argument.contains("segment1")))).thenReturn("encrypted_segment1"); + when(mCipher.encrypt("segment3")).thenReturn("encrypted_segment3"); + mUpdater = createUpdater(toAdd, toRemove, 10); + + mUpdater.run(); + + verify(mCipher).encrypt("segment1"); + verify(mCipher).encrypt("segment2"); + verify(mCipher).encrypt("segment3"); + verify(mCipher).encrypt("segment4"); + verify(mDao).delete(argThat(argument -> argument.size() == 1 && + argument.get(0).equals("encrypted_segment3"))); + verify(mDao).insert(argThat((ArgumentMatcher>) argument -> argument.size() == 1 && + argument.get(0).getName().equals("encrypted_segment1"))); + } + + @NonNull + private Updater createUpdater(Set toAdd, Set toRemove, long changeNumber) { + return new Updater(mCipher, mDao, mGeneralInfoStorage, toAdd, toRemove, changeNumber); + } +}