Skip to content

Commit

Permalink
Encrypt flag names for deletion (#705)
Browse files Browse the repository at this point in the history
  • Loading branch information
gthea authored Sep 23, 2024
1 parent ef87d30 commit 88a593c
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class SqLitePersistentSplitsStorage implements PersistentSplitsStorage {
private final SplitListTransformer<SplitEntity, Split> mEntityToSplitTransformer;
private final SplitListTransformer<Split, SplitEntity> mSplitToEntityTransformer;
private final SplitRoomDatabase mDatabase;
private final SplitCipher mCipher;

public SqLitePersistentSplitsStorage(@NonNull SplitRoomDatabase database, @NonNull SplitCipher splitCipher) {
this(database, new SplitParallelTaskExecutorFactoryImpl(), splitCipher);
Expand All @@ -32,18 +33,21 @@ public SqLitePersistentSplitsStorage(@NonNull SplitRoomDatabase database, @NonNu
@VisibleForTesting
public SqLitePersistentSplitsStorage(@NonNull SplitRoomDatabase database,
@NonNull SplitListTransformer<SplitEntity, Split> entityToSplitTransformer,
@NonNull SplitListTransformer<Split, SplitEntity> splitToEntityTransformer) {
@NonNull SplitListTransformer<Split, SplitEntity> splitToEntityTransformer,
@NonNull SplitCipher cipher) {
mDatabase = checkNotNull(database);
mEntityToSplitTransformer = checkNotNull(entityToSplitTransformer);
mSplitToEntityTransformer = checkNotNull(splitToEntityTransformer);
mCipher = checkNotNull(cipher);
}

private SqLitePersistentSplitsStorage(@NonNull SplitRoomDatabase database,
@NonNull SplitParallelTaskExecutorFactory executorFactory,
@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
Expand Down Expand Up @@ -104,8 +108,13 @@ public void updateFlagsSpec(String flagsSpec) {

@Override
public void delete(List<String> splitNames) {
List<String> 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<List<String>> deleteChunk = partition(splitNames, SQL_PARAM_BIND_SIZE);
List<List<String>> deleteChunk = partition(encryptedNames, SQL_PARAM_BIND_SIZE);
for (List<String> splits : deleteChunk) {
mDatabase.splitDao().delete(splits);
}
Expand Down Expand Up @@ -152,7 +161,7 @@ private List<String> splitNameList(List<Split> splits) {
return names;
}
for (Split split : splits) {
names.add(split.name);
names.add(mCipher.encrypt(split.name));
}
return names;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -31,13 +40,31 @@ public class SqLitePersistentSplitsStorageTest {
private SplitListTransformer<Split, SplitEntity> 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<Void>) 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<String> encryptionAnswer) {
when(mCipher.encrypt(any())).thenAnswer(encryptionAnswer);
mStorage = new SqLitePersistentSplitsStorage(mDatabase, mEntityToSplitTransformer, mSplitToSplitEntityTransformer, mCipher);
}

@After
public void tearDown() throws Exception {
mAutoCloseable.close();
}

@Test
Expand Down Expand Up @@ -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<Split> activeSplits = Collections.emptyList();
List<Split> 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<String>) 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<Split> activeSplits = new ArrayList<>();
List<Split> 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<String>) 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<String>) 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<GeneralInfoEntity>() {
@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<GeneralInfoEntity>() {
@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<SplitEntity> getMockEntities() {
ArrayList<SplitEntity> entities = new ArrayList<>();
String jsonTemplate = "{\"name\":\"%s\", \"changeNumber\": %d}";
Expand Down

0 comments on commit 88a593c

Please sign in to comment.