package com.zegoggles.smssync.service; import android.content.Context; import android.database.Cursor; import android.database.MatrixCursor; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.store.imap.XOAuth2AuthenticationFailedException; import com.zegoggles.smssync.auth.TokenRefreshException; import com.zegoggles.smssync.auth.TokenRefresher; import com.zegoggles.smssync.contacts.ContactAccessor; import com.zegoggles.smssync.contacts.ContactGroup; import com.zegoggles.smssync.contacts.ContactGroupIds; import com.zegoggles.smssync.mail.BackupImapStore; import com.zegoggles.smssync.mail.ConversionResult; import com.zegoggles.smssync.mail.DataType; import com.zegoggles.smssync.mail.MessageConverter; import com.zegoggles.smssync.preferences.AuthPreferences; import com.zegoggles.smssync.preferences.Preferences; import com.zegoggles.smssync.service.state.BackupState; import com.zegoggles.smssync.service.state.SmsSyncState; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import java.util.EnumSet; import java.util.HashMap; import static com.zegoggles.smssync.mail.DataType.CALLLOG; import static com.zegoggles.smssync.mail.DataType.MMS; import static com.zegoggles.smssync.mail.DataType.SMS; import static com.zegoggles.smssync.service.BackupItemsFetcher.emptyCursor; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.notNull; import static org.mockito.Mockito.anyListOf; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @RunWith(RobolectricTestRunner.class) public class BackupTaskTest { BackupTask task; BackupConfig config; Context context; @Mock BackupImapStore store; @Mock BackupImapStore.BackupFolder folder; @Mock SmsBackupService service; @Mock BackupState state; @Mock BackupItemsFetcher fetcher; @Mock MessageConverter converter; @Mock CalendarSyncer syncer; @Mock AuthPreferences authPreferences; @Mock Preferences preferences; @Mock ContactAccessor accessor; @Mock TokenRefresher tokenRefresher; @Before public void before() { initMocks(this); config = getBackupConfig(EnumSet.of(SMS)); when(service.getApplicationContext()).thenReturn(Robolectric.application); when(service.getState()).thenReturn(state); task = new BackupTask(service, fetcher, converter, syncer, authPreferences, preferences, accessor, tokenRefresher); context = Robolectric.application; } private BackupConfig getBackupConfig(EnumSet<DataType> types) { return new BackupConfig(store, 0, false, 100, new ContactGroup(-1), BackupType.MANUAL, types, false ); } @Test public void shouldAcquireAndReleaseLocksDuringBackup() throws Exception { mockAllFetchEmpty(); task.doInBackground(config); verify(service).acquireLocks(); verify(service).releaseLocks(); verify(service).transition(SmsSyncState.FINISHED_BACKUP, null); } @Test public void shouldVerifyStoreSettings() throws Exception { mockFetch(SMS, 1); when(converter.convertMessages(any(Cursor.class), eq(SMS))).thenReturn(result(SMS, 1)); when(store.getFolder(SMS)).thenReturn(folder); task.doInBackground(config); verify(store).checkSettings(); } @Test public void shouldBackupItems() throws Exception { mockFetch(SMS, 1); when(converter.convertMessages(any(Cursor.class), eq(SMS))).thenReturn(result(SMS, 1)); when(store.getFolder(notNull(DataType.class))).thenReturn(folder); BackupState finalState = task.doInBackground(config); verify(folder).appendMessages(anyListOf(Message.class)); verify(service).transition(SmsSyncState.LOGIN, null); verify(service).transition(SmsSyncState.CALC, null); assertThat(finalState).isNotNull(); assertThat(finalState.isFinished()).isTrue(); assertThat(finalState.currentSyncedItems).isEqualTo(1); assertThat(finalState.itemsToSync).isEqualTo(1); assertThat(finalState.backupType).isEqualTo(config.backupType); } @Test public void shouldBackupMultipleTypes() throws Exception { mockFetch(SMS, 1); mockFetch(MMS, 2); when(store.getFolder(notNull(DataType.class))).thenReturn(folder); when(converter.convertMessages(any(Cursor.class), any(DataType.class))).thenReturn(result(SMS, 1)); BackupState finalState = task.doInBackground(getBackupConfig(EnumSet.of(SMS, MMS))); assertThat(finalState.currentSyncedItems).isEqualTo(3); verify(folder, times(3)).appendMessages(anyListOf(Message.class)); } @Test public void shouldCreateFoldersLazilyOnlyForNeededTypes() throws Exception { mockFetch(SMS, 1); when(converter.convertMessages(any(Cursor.class), eq(SMS))).thenReturn(result(SMS, 1)); when(store.getFolder(notNull(DataType.class))).thenReturn(folder); task.doInBackground(config); verify(store).getFolder(SMS); verify(store, never()).getFolder(MMS); verify(store, never()).getFolder(CALLLOG); } @Test public void shouldCloseImapFolderAfterBackup() throws Exception { mockFetch(SMS, 1); when(converter.convertMessages(any(Cursor.class), eq(SMS))).thenReturn(result(SMS, 1)); when(store.getFolder(notNull(DataType.class))).thenReturn(folder); task.doInBackground(config); verify(store).closeFolders(); } @Test public void shouldCreateNoFoldersIfNoItemsToBackup() throws Exception { mockFetch(SMS, 0); task.doInBackground(config); verifyZeroInteractions(store); } @Test public void shouldSkipItems() throws Exception { when(fetcher.getMostRecentTimestamp(any(DataType.class))).thenReturn(-23L); BackupState finalState = task.doInBackground(new BackupConfig( store, 0, true, 100, new ContactGroup(-1), BackupType.MANUAL, EnumSet.of(SMS), false ) ); assertThat(DataType.SMS.getMaxSyncedDate(context)).isEqualTo(-23); assertThat(DataType.MMS.getMaxSyncedDate(context)).isEqualTo(-1); assertThat(DataType.CALLLOG.getMaxSyncedDate(context)).isEqualTo(-1); assertThat(finalState).isNotNull(); assertThat(finalState.isFinished()).isTrue(); } @Test public void shouldHandleAuthErrorAndTokenCannotBeRefreshed() throws Exception { mockFetch(SMS, 1); when(converter.convertMessages(any(Cursor.class), notNull(DataType.class))).thenReturn(result(SMS, 1)); XOAuth2AuthenticationFailedException exception = mock(XOAuth2AuthenticationFailedException.class); when(exception.getStatus()).thenReturn(400); when(store.getFolder(notNull(DataType.class))).thenThrow(exception); doThrow(new TokenRefreshException("failed")).when(tokenRefresher).refreshOAuth2Token(); task.doInBackground(config); verify(tokenRefresher, times(1)).refreshOAuth2Token(); verify(service).transition(SmsSyncState.ERROR, exception); // make sure locks only get acquired+released once verify(service).acquireLocks(); verify(service).releaseLocks(); } @Test public void shouldHandleAuthErrorAndTokenCouldBeRefreshed() throws Exception { mockFetch(SMS, 1); when(converter.convertMessages(any(Cursor.class), notNull(DataType.class))).thenReturn(result(SMS, 1)); XOAuth2AuthenticationFailedException exception = mock(XOAuth2AuthenticationFailedException.class); when(exception.getStatus()).thenReturn(400); when(store.getFolder(notNull(DataType.class))).thenThrow(exception); when(service.getBackupImapStore()).thenReturn(store); task.doInBackground(config); verify(tokenRefresher).refreshOAuth2Token(); verify(service, times(2)).transition(SmsSyncState.LOGIN, null); verify(service, times(2)).transition(SmsSyncState.CALC, null); verify(service).transition(SmsSyncState.ERROR, exception); // make sure locks only get acquired+released once verify(service).acquireLocks(); verify(service).releaseLocks(); } private ConversionResult result(DataType type, int n) { ConversionResult result = new ConversionResult(type); for (int i = 0; i<n; i++) { result.add(new MimeMessage(), new HashMap<String, String>()); } return result; } private void mockFetch(DataType type, final int n) { when(fetcher.getItemsForDataType(eq(type), any(ContactGroupIds.class), anyInt())).then(new Answer<Object>() { @Override public Object answer(InvocationOnMock invocationOnMock) throws Throwable { return testMessages(n); } }); } private Cursor testMessages(int n) { MatrixCursor cursor = new MatrixCursor(new String[] {"_id"} ); for (int i = 0; i < n; i++) { cursor.addRow(new Object[]{ "12345" }); } return cursor; } private void mockAllFetchEmpty() { when(fetcher.getItemsForDataType(any(DataType.class), any(ContactGroupIds.class), anyInt())).thenReturn(emptyCursor()); } }