package com.getbase.android.db.provider; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.google.common.collect.Iterables; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowContentProviderOperation; import android.content.ContentProviderClient; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentValues; import android.content.OperationApplicationException; import android.net.Uri; import android.net.Uri.Builder; import android.os.RemoteException; import android.provider.BaseColumns; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) public class BatcherTest { @Test public void shouldWorkFineWithValueBackReferences() throws Exception { final Insert firstInsert = ProviderAction.insert(Uri.EMPTY); final Insert secondInsert = ProviderAction.insert(Uri.EMPTY); final ArrayList<ContentProviderOperation> operations = Batcher.begin() .append(firstInsert) .append(secondInsert) .append(ProviderAction.insert(Uri.EMPTY)) .withValueBackReference(firstInsert, BaseColumns._ID) .withValueBackReference(secondInsert, "contact_id") .operations(); assertThat(operations).hasSize(3); final ContentProviderOperation lastOperation = operations.get(2); ShadowContentProviderOperation shadowOperation = Robolectric.shadowOf(lastOperation); final ContentValues backRefs = shadowOperation.getValuesBackReferences(); assertThat(backRefs.get("_id")).isEqualTo(0); assertThat(backRefs.get("contact_id")).isEqualTo(1); } @Test public void shouldResolveSelectionBackReferences() throws Exception { final Insert firstInsert = ProviderAction.insert(Uri.EMPTY); final Insert secondInsert = ProviderAction.insert(Uri.EMPTY); final ArrayList<ContentProviderOperation> operations = Batcher.begin() .append(firstInsert) .append(secondInsert) .append(ProviderAction.update(Uri.EMPTY).value("test", 1L).where(BaseColumns._ID + "=? AND contact_id=?")) .withSelectionBackReference(firstInsert, 0) .withSelectionBackReference(secondInsert, 1) .operations(); assertThat(operations).hasSize(3); final ContentProviderOperation lastOperation = operations.get(2); ShadowContentProviderOperation shadowOperation = Robolectric.shadowOf(lastOperation); final Map<Integer, Integer> backRefs = shadowOperation.getSelectionArgsBackReferences(); assertThat(backRefs).containsEntry(0, 0); assertThat(backRefs).containsEntry(1, 1); } @Test public void shouldGenerateProperListOfContentProviderOperations() throws Exception { final ArrayList<ContentProviderOperation> operations = Batcher.begin() .append(ProviderAction.insert(createFakeUri("first"))) .append(ProviderAction.insert(createFakeUri("second"))) .append(ProviderAction.update(createFakeUri("third")).value("test", 1L)) .operations(); assertThat(operations).hasSize(3); operationAssert(operations.get(0), createFakeUri("first"), ShadowContentProviderOperation.TYPE_INSERT); operationAssert(operations.get(1), createFakeUri("second"), ShadowContentProviderOperation.TYPE_INSERT); operationAssert(operations.get(2), createFakeUri("third"), ShadowContentProviderOperation.TYPE_UPDATE); } @Test public void shouldTakeCareAboutContentValuesInBatch() throws Exception { ContentValues values = new ContentValues(); values.put("test1", 1L); values.put("test2", "blah"); final ArrayList<ContentProviderOperation> operations = Batcher.begin() .append(ProviderAction.insert(createFakeUri("first")).values(values)) .operations(); final ShadowContentProviderOperation contentProviderOperation = Robolectric.shadowOf(operations.get(0)); assertThat(contentProviderOperation.getContentValues()).isEqualTo(values); } @Test public void shouldGenerateEmptyOperations() throws Exception { assertThat(Batcher.begin().operations()).isEmpty(); } @Test public void shouldMapToProperInsertEvenIfTheyHaveIdenticalState() throws Exception { final Insert first = ProviderAction.insert(createFakeUri("only")); final Insert second = ProviderAction.insert(createFakeUri("only")); final ArrayList<ContentProviderOperation> operations = Batcher.begin() .append(first) .append(second) .append(ProviderAction.insert(createFakeUri("only"))).withValueBackReference(first, "column") .operations(); assertThat(operations).hasSize(3); final ShadowContentProviderOperation shadowOperation = Robolectric.shadowOf(Iterables.getLast(operations)); final ContentValues backRefs = shadowOperation.getValuesBackReferences(); assertThat(backRefs.get("column")).isEqualTo(0); } @Test(expected = IllegalStateException.class) public void shouldThrowAnExceptionIfRequestingForPreviousWhenItsDuplicated() throws Exception { final Insert first = ProviderAction.insert(createFakeUri("only")); Batcher.begin() .append(first) .append(first) .append(ProviderAction.insert(createFakeUri("only"))).withValueBackReference(first, "column") .operations(); } @Test(expected = IllegalStateException.class) public void shouldThrowAnExceptionInCaseReferencedInsertDoesNotExistInBatcher() throws Exception { final Insert first = ProviderAction.insert(createFakeUri("only")); Batcher.begin() .append(ProviderAction.insert(createFakeUri("only"))).withValueBackReference(first, "column") .operations(); } @Test(expected = IllegalStateException.class) public void shouldThrowAnExceptionIfPreviousInsertForSelectionBackReferenceIsDuplicated() throws Exception { final Insert first = ProviderAction.insert(createFakeUri("only")); Batcher.begin() .append(first) .append(first) .append(ProviderAction.delete(createFakeUri("only")).where(BaseColumns._ID + "=?")).withSelectionBackReference(first, 0) .operations(); } @Test(expected = IllegalStateException.class) public void shouldThrowAnExceptionPreviousInsertForSelectionBackReferenceWasNotAddedToBatcher() throws Exception { final Insert first = ProviderAction.insert(createFakeUri("only")); Batcher.begin() .append(ProviderAction.delete(createFakeUri("only")).where(BaseColumns._ID + "=?")).withSelectionBackReference(first, 0) .operations(); } @Test public void shouldResolveValueBackReferencesForAllConvertiblesWithinIterable() throws Exception { final Insert first = ProviderAction.insert(createFakeUri("fake")); final Insert second = ProviderAction.insert(createFakeUri("second")); final int copies = 5; final List<ConvertibleToOperation> firstDependants = Collections.<ConvertibleToOperation>nCopies(copies, ProviderAction.insert(createFakeUri("another"))); final List<ConvertibleToOperation> secondDependants = Collections.<ConvertibleToOperation>nCopies(copies, ProviderAction.insert(createFakeUri("yetAnother"))); final ArrayList<ContentProviderOperation> operations = Batcher.begin() .append(first) .append(second) .append(firstDependants) .withValueBackReference(first, "parent_id") .withValueBackReference(second, "another_parent_id") .append(secondDependants) .withValueBackReference(second, "parent_id") .withValueBackReference(first, "another_parent_id") .operations(); assertThat(operations).hasSize(copies * 2 + 2); for (ContentProviderOperation contentProviderOperation : operations.subList(2, 2 + copies)) { final ShadowContentProviderOperation shadowOperation = Robolectric.shadowOf(contentProviderOperation); final ContentValues refs = shadowOperation.getValuesBackReferences(); assertThat(refs.get("parent_id")).isEqualTo(0); assertThat(refs.get("another_parent_id")).isEqualTo(1); } for (ContentProviderOperation contentProviderOperation : operations.subList(2 + copies, operations.size())) { final ShadowContentProviderOperation shadowOperation = Robolectric.shadowOf(contentProviderOperation); final ContentValues refs = shadowOperation.getValuesBackReferences(); assertThat(refs.get("parent_id")).isEqualTo(1); assertThat(refs.get("another_parent_id")).isEqualTo(0); } } @Test(expected = RuntimeException.class) public void shouldThrowRuntimeExceptionIfRemoteExceptionOccurInResolver() throws Exception { throwAnExceptionInsideResolversApplyBatch(OperationApplicationException.class); } @Test(expected = RuntimeException.class) public void shouldThrowRuntimeExceptionIfOperationApplicationExceptionOccurInResolver() throws Exception { throwAnExceptionInsideResolversApplyBatch(OperationApplicationException.class); } @Test(expected = SecurityException.class) public void ifExceptionThrownFromApplyBatchIsNotCheckedThenJustThrowItInResolver() throws Exception { throwAnExceptionInsideResolversApplyBatch(SecurityException.class); } @Test(expected = RuntimeException.class) public void shouldThrowRuntimeExceptionIfRemoteExceptionOccurInProviderClient() throws Exception { throwAnExceptionInsideClientsApplyBatch(OperationApplicationException.class); } @Test(expected = RuntimeException.class) public void shouldThrowRuntimeExceptionIfOperationApplicationExceptionOccurInProviderClient() throws Exception { throwAnExceptionInsideClientsApplyBatch(OperationApplicationException.class); } @Test(expected = SecurityException.class) public void ifExceptionThrownFromApplyBatchIsNotCheckedThenJustThrowItInProviderClient() throws Exception { throwAnExceptionInsideClientsApplyBatch(SecurityException.class); } @Test public void shouldDecorateOperationsUrisIfSpecified() throws Exception { final ArrayList<ContentProviderOperation> operations = Batcher.begin() .append(ProviderAction.insert(createFakeUri("first"))) .append(ProviderAction.update(createFakeUri("second")).value("test", 1L)) .append(ProviderAction.delete(createFakeUri("third"))) .decorateUrisWith(new UriDecorator() { @Override public Uri decorate(Uri uri) { return Uri.withAppendedPath(uri, "boom"); } }) .operations(); assertThat(operations).hasSize(3); operationAssert(operations.get(0), createFakeUri("first", "boom"), ShadowContentProviderOperation.TYPE_INSERT); operationAssert(operations.get(1), createFakeUri("second", "boom"), ShadowContentProviderOperation.TYPE_UPDATE); operationAssert(operations.get(2), createFakeUri("third", "boom"), ShadowContentProviderOperation.TYPE_DELETE); } @SuppressWarnings("unchecked") private void throwAnExceptionInsideResolversApplyBatch(Class<? extends Exception> applyBatchException) throws RemoteException, OperationApplicationException { final ContentResolver resolver = mock(ContentResolver.class); when(resolver.applyBatch(anyString(), any(ArrayList.class))).thenThrow(applyBatchException); Batcher.begin() .append(ProviderAction.insert(createFakeUri("fake"))) .applyBatchOrThrow("com.fakedomain.base", resolver); } @SuppressWarnings("unchecked") private void throwAnExceptionInsideClientsApplyBatch(Class<? extends Exception> applyBatchException) throws RemoteException, OperationApplicationException { final ContentProviderClient client = mock(ContentProviderClient.class); when(client.applyBatch(any(ArrayList.class))).thenThrow(applyBatchException); Batcher.begin() .append(ProviderAction.insert(createFakeUri("fake"))) .applyBatchOrThrow(client); } private static Uri createFakeUri(String... pathSegments) { Builder builder = Uri.parse("content://com.fakedomain.base").buildUpon(); for (String path : pathSegments) { builder.appendPath(path); } return builder.build(); } private static void operationAssert(ContentProviderOperation operation, Uri uri, int type) { final ShadowContentProviderOperation shadowOperation = Robolectric.shadowOf(operation); assertThat(operation.getUri()).isEqualTo(uri); assertThat(shadowOperation.getType()).isEqualTo(type); } }