/*
* Copyright (c) 2016 Ha Duy Trung
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.hidroh.materialistic.data;
import android.accounts.Account;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.widget.ProgressBar;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowContentResolver;
import org.robolectric.shadows.ShadowNetworkInfo;
import org.robolectric.shadows.ShadowNotificationManager;
import org.robolectric.util.ServiceController;
import java.io.IOException;
import io.github.hidroh.materialistic.BuildConfig;
import io.github.hidroh.materialistic.R;
import io.github.hidroh.materialistic.test.TestRunner;
import io.github.hidroh.materialistic.test.shadow.ShadowWebView;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import rx.schedulers.Schedulers;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
@SuppressWarnings("unchecked")
@Config(shadows = {ShadowWebView.class}, sdk = 18)
@RunWith(TestRunner.class)
public class ItemSyncAdapterTest {
private TestItemSyncAdapter adapter;
private SharedPreferences syncPreferences;
private @Captor ArgumentCaptor<Callback<HackerNewsItem>> callbackCapture;
private ReadabilityClient readabilityClient = mock(ReadabilityClient.class);
private ServiceController<ItemSyncService> serviceController;
private ItemSyncService service;
private @Captor ArgumentCaptor<ReadabilityClient.Callback> readabilityCallbackCaptor;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
reset(TestRestServiceFactory.hnRestService);
reset(readabilityClient);
serviceController = Robolectric.buildService(ItemSyncService.class);
service = serviceController.attach().create().get();
setNetworkType(ConnectivityManager.TYPE_WIFI);
PreferenceManager.getDefaultSharedPreferences(service)
.edit()
.putBoolean(service.getString(R.string.pref_saved_item_sync), true)
.putBoolean(service.getString(R.string.pref_offline_comments), true)
.apply();
adapter = new TestItemSyncAdapter(service, new TestRestServiceFactory(), readabilityClient);
syncPreferences = service.getSharedPreferences(
service.getPackageName() +
SyncDelegate.SYNC_PREFERENCES_FILE, Context.MODE_PRIVATE);
}
@Test
public void testSyncDisabled() {
PreferenceManager.getDefaultSharedPreferences(service)
.edit().clear().apply();
SyncDelegate.scheduleSync(service,
new SyncDelegate.JobBuilder(RuntimeEnvironment.application, "1").build());
assertNull(ShadowContentResolver.getStatus(createSyncAccount(),
MaterialisticProvider.PROVIDER_AUTHORITY));
}
@Test
public void testSyncEnabledCached() throws IOException {
HackerNewsItem hnItem = mock(HackerNewsItem.class);
Call<HackerNewsItem> call = mock(Call.class);
when(call.execute()).thenReturn(Response.success(hnItem));
when(TestRestServiceFactory.hnRestService.cachedItem(any())).thenReturn(call);
SyncDelegate.scheduleSync(service,
new SyncDelegate.JobBuilder(RuntimeEnvironment.application, "1").build());
adapter.onPerformSync(mock(Account.class), getLastSyncExtras(), null, null, null);
// cache hit, should not try network or defer
verify(TestRestServiceFactory.hnRestService).cachedItem(any());
verify(TestRestServiceFactory.hnRestService, never()).networkItem(any());
assertThat(syncPreferences.getAll()).isEmpty();
}
@Test
public void testSyncEnabledNonWifi() throws IOException {
Call<HackerNewsItem> call = mock(Call.class);
when(call.execute()).thenThrow(IOException.class);
when(TestRestServiceFactory.hnRestService.cachedItem(any())).thenReturn(call);
setNetworkType(ConnectivityManager.TYPE_MOBILE);
SyncDelegate.scheduleSync(service,
new SyncDelegate.JobBuilder(RuntimeEnvironment.application, "1").build());
adapter.onPerformSync(mock(Account.class), getLastSyncExtras(), null, null, null);
// should defer
assertThat(syncPreferences.getAll()).isNotEmpty();
}
@Test
public void testSyncEnabledAnyConnection() throws IOException {
Call<HackerNewsItem> call = mock(Call.class);
when(call.execute()).thenThrow(IOException.class);
when(TestRestServiceFactory.hnRestService.cachedItem(any())).thenReturn(call);
when(TestRestServiceFactory.hnRestService.networkItem(any())).thenReturn(call);
PreferenceManager.getDefaultSharedPreferences(service)
.edit()
.putString(service.getString(R.string.pref_offline_data),
service.getString(R.string.offline_data_default))
.apply();
setNetworkType(ConnectivityManager.TYPE_MOBILE);
SyncDelegate.scheduleSync(service,
new SyncDelegate.JobBuilder(RuntimeEnvironment.application, "1").build());
adapter.onPerformSync(mock(Account.class), getLastSyncExtras(), null, null, null);
// should try cache, then network
verify(TestRestServiceFactory.hnRestService).cachedItem(any());
verify(TestRestServiceFactory.hnRestService).networkItem(any());
assertThat(syncPreferences.getAll()).isEmpty();
}
@Test
public void testSyncEnabledWifi() throws IOException {
Call<HackerNewsItem> call = mock(Call.class);
when(call.execute()).thenThrow(IOException.class);
when(TestRestServiceFactory.hnRestService.cachedItem(any())).thenReturn(call);
when(TestRestServiceFactory.hnRestService.networkItem(any())).thenReturn(call);
SyncDelegate.scheduleSync(service,
new SyncDelegate.JobBuilder(RuntimeEnvironment.application, "1").build());
adapter.onPerformSync(mock(Account.class), getLastSyncExtras(), null, null, null);
// should try cache before network
verify(TestRestServiceFactory.hnRestService).cachedItem(any());
verify(TestRestServiceFactory.hnRestService).networkItem(any());
assertThat(syncPreferences.getAll()).isEmpty();
// on network response should try children
verify(call).enqueue(callbackCapture.capture());
HackerNewsItem item = mock(HackerNewsItem.class);
when(item.getKids()).thenReturn(new long[]{2L, 3L});
callbackCapture.getValue().onResponse(null, Response.success(item));
verify(TestRestServiceFactory.hnRestService, times(3)).cachedItem(any());
}
@Test
public void testSyncChildrenDisabled() throws IOException {
HackerNewsItem item = mock(HackerNewsItem.class);
when(item.getKids()).thenReturn(new long[]{2L, 3L});
Call<HackerNewsItem> call = mock(Call.class);
when(call.execute()).thenReturn(Response.success(item));
when(TestRestServiceFactory.hnRestService.cachedItem(any())).thenReturn(call);
PreferenceManager.getDefaultSharedPreferences(service)
.edit()
.putBoolean(service.getString(R.string.pref_offline_comments), false)
.apply();
SyncDelegate.scheduleSync(service,
new SyncDelegate.JobBuilder(RuntimeEnvironment.application, "1").build());
adapter.onPerformSync(mock(Account.class), getLastSyncExtras(), null, null, null);
// should not sync children
verify(TestRestServiceFactory.hnRestService).cachedItem(any());
}
@Test
public void testSyncDeferred() throws IOException {
Call<HackerNewsItem> call = mock(Call.class);
when(call.execute()).thenThrow(IOException.class);
when(TestRestServiceFactory.hnRestService.cachedItem(any())).thenReturn(call);
when(TestRestServiceFactory.hnRestService.networkItem(any())).thenReturn(call);
syncPreferences.edit().putBoolean("1", true).putBoolean("2", true).apply();
SyncDelegate.scheduleSync(service,
new SyncDelegate.JobBuilder(RuntimeEnvironment.application, null).build());
adapter.onPerformSync(mock(Account.class), getLastSyncExtras(), null, null, null);
ShadowContentResolver.Status syncStatus = ShadowContentResolver.getStatus(
new Account("Materialistic", BuildConfig.APPLICATION_ID),
MaterialisticProvider.PROVIDER_AUTHORITY);
assertThat(syncStatus.syncRequests).isEqualTo(3); // original + 2 deferred
}
@Test
public void testSyncReadabilityDisabled() throws IOException {
HackerNewsItem item = new TestHnItem(1L) {
@Override
public boolean isStoryType() {
return true;
}
@Override
public String getRawUrl() {
return "http://example.com";
}
};
Call<HackerNewsItem> call = mock(Call.class);
when(call.execute()).thenReturn(Response.success(item));
when(TestRestServiceFactory.hnRestService.cachedItem(any())).thenReturn(call);
PreferenceManager.getDefaultSharedPreferences(service)
.edit()
.putBoolean(service.getString(R.string.pref_offline_readability), false)
.apply();
SyncDelegate.scheduleSync(service,
new SyncDelegate.JobBuilder(RuntimeEnvironment.application, "1").build());
adapter.onPerformSync(mock(Account.class), getLastSyncExtras(), null, null, null);
verify(TestRestServiceFactory.hnRestService).cachedItem(any());
verify(readabilityClient, never()).parse(any(), any(), any());
}
@Test
public void testSyncReadability() throws IOException {
HackerNewsItem item = new TestHnItem(1L) {
@Override
public boolean isStoryType() {
return true;
}
@Override
public String getRawUrl() {
return "http://example.com";
}
};
Call<HackerNewsItem> call = mock(Call.class);
when(call.execute()).thenReturn(Response.success(item));
when(TestRestServiceFactory.hnRestService.cachedItem(any())).thenReturn(call);
SyncDelegate.scheduleSync(service,
new SyncDelegate.JobBuilder(RuntimeEnvironment.application, "1").build());
adapter.onPerformSync(mock(Account.class), getLastSyncExtras(), null, null, null);
verify(TestRestServiceFactory.hnRestService).cachedItem(any());
verify(readabilityClient).parse(any(), eq("http://example.com"), any());
}
@Test
public void testSyncReadabilityNoWifi() throws IOException {
HackerNewsItem item = new TestHnItem(1L) {
@Override
public boolean isStoryType() {
return true;
}
};
Call<HackerNewsItem> call = mock(Call.class);
when(call.execute()).thenReturn(Response.success(item));
when(TestRestServiceFactory.hnRestService.cachedItem(any())).thenReturn(call);
setNetworkType(ConnectivityManager.TYPE_MOBILE);
SyncDelegate.scheduleSync(service,
new SyncDelegate.JobBuilder(RuntimeEnvironment.application, "1").build());
adapter.onPerformSync(mock(Account.class), getLastSyncExtras(), null, null, null);
verify(readabilityClient, never()).parse(any(), any(), any());
}
@Test
public void testSyncReadabilityNotStory() throws IOException {
HackerNewsItem item = new TestHnItem(1L) {
@Override
public boolean isStoryType() {
return false;
}
};
Call<HackerNewsItem> call = mock(Call.class);
when(call.execute()).thenReturn(Response.success(item));
when(TestRestServiceFactory.hnRestService.cachedItem(any())).thenReturn(call);
SyncDelegate.scheduleSync(service,
new SyncDelegate.JobBuilder(RuntimeEnvironment.application, "1").build());
adapter.onPerformSync(mock(Account.class), getLastSyncExtras(), null, null, null);
verify(TestRestServiceFactory.hnRestService).cachedItem(any());
verify(readabilityClient, never()).parse(any(), any(), any());
}
@Test
public void testSyncWebCacheEmptyUrl() {
new FavoriteManager(Schedulers.immediate())
.add(service, new Favorite("1", null, "title", System.currentTimeMillis()));
assertThat(ShadowWebView.getLastGlobalLoadedUrl()).isNullOrEmpty();
}
@Test
public void testSyncWebCache() throws IOException {
ShadowWebView.lastGlobalLoadedUrl = null;
PreferenceManager.getDefaultSharedPreferences(service)
.edit()
.putBoolean(service.getString(R.string.pref_offline_article), true)
.apply();
HackerNewsItem item = new TestHnItem(1L) {
@Override
public boolean isStoryType() {
return true;
}
@Override
public String getUrl() {
return "http://example.com";
}
};
Call<HackerNewsItem> call = mock(Call.class);
when(call.execute()).thenReturn(Response.success(item));
when(TestRestServiceFactory.hnRestService.cachedItem(any())).thenReturn(call);
SyncDelegate.scheduleSync(service,
new SyncDelegate.JobBuilder(RuntimeEnvironment.application, "1").build());
adapter.onPerformSync(mock(Account.class), getLastSyncExtras(), null, null, null);
assertThat(ShadowWebView.getLastGlobalLoadedUrl()).contains("http://example.com");
}
@Test
public void testSyncWebCacheDisabled() throws IOException {
ShadowWebView.lastGlobalLoadedUrl = null;
PreferenceManager.getDefaultSharedPreferences(service)
.edit()
.putBoolean(service.getString(R.string.pref_offline_article), false)
.apply();
HackerNewsItem item = new TestHnItem(1L) {
@Override
public boolean isStoryType() {
return true;
}
@Override
public String getRawUrl() {
return "http://example.com";
}
};
Call<HackerNewsItem> call = mock(Call.class);
when(call.execute()).thenReturn(Response.success(item));
when(TestRestServiceFactory.hnRestService.cachedItem(any())).thenReturn(call);
SyncDelegate.scheduleSync(service,
new SyncDelegate.JobBuilder(RuntimeEnvironment.application, "1").build());
adapter.onPerformSync(mock(Account.class), getLastSyncExtras(), null, null, null);
assertThat(ShadowWebView.getLastGlobalLoadedUrl()).isNullOrEmpty();
}
@Test
public void testNotification() throws IOException {
Call<HackerNewsItem> call = mock(Call.class);
when(call.execute()).thenReturn(Response.success(new TestHnItem(1L) {
@Override
public boolean isStoryType() {
return true;
}
@Override
public String getRawUrl() {
return "http://example.com";
}
@Override
public long[] getKids() {
return new long[]{2L, 3L};
}
}));
when(TestRestServiceFactory.hnRestService.cachedItem(eq("1"))).thenReturn(call);
Call<HackerNewsItem> kid1Call = mock(Call.class);
when(kid1Call.execute()).thenReturn(Response.success(new TestHnItem(2L) {
@Override
public boolean isStoryType() {
return false;
}
}));
when(TestRestServiceFactory.hnRestService.cachedItem(eq("2"))).thenReturn(kid1Call);
Call<HackerNewsItem> kid2Call = mock(Call.class);
when(kid2Call.execute()).thenThrow(IOException.class);
when(TestRestServiceFactory.hnRestService.cachedItem(eq("3"))).thenReturn(kid2Call);
when(TestRestServiceFactory.hnRestService.networkItem(eq("3"))).thenReturn(kid2Call);
PreferenceManager.getDefaultSharedPreferences(service)
.edit()
.putBoolean(service.getString(R.string.pref_offline_notification), true)
.apply();
SyncDelegate.scheduleSync(service,
new SyncDelegate.JobBuilder(RuntimeEnvironment.application, "1").build());
adapter.onPerformSync(mock(Account.class), getLastSyncExtras(), null, null, null);
verify(readabilityClient).parse(any(), eq("http://example.com"),
readabilityCallbackCaptor.capture());
readabilityCallbackCaptor.getValue().onResponse("");
ShadowNotificationManager notificationManager = shadowOf((NotificationManager) service
.getSystemService(Context.NOTIFICATION_SERVICE));
ProgressBar progress = shadowOf(notificationManager.getNotification(1))
.getProgressBar();
assertThat(progress.getProgress()).isEqualTo(3); // self + kid 1 + readability
assertThat(progress.getMax()).isEqualTo(104); // self + 2 kids + readability + web
shadowOf(adapter.syncDelegate.mWebView).getWebChromeClient()
.onProgressChanged(adapter.syncDelegate.mWebView, 100);
verify(kid2Call).enqueue(callbackCapture.capture());
callbackCapture.getValue().onFailure(null, null);
assertThat(notificationManager.getAllNotifications()).isEmpty();
}
@Test
public void testBindService() {
assertNotNull(service.onBind(null));
}
@Test
public void testWifiChange() {
setNetworkType(ConnectivityManager.TYPE_MOBILE);
new ItemSyncWifiReceiver()
.onReceive(service, new Intent(ConnectivityManager.CONNECTIVITY_ACTION));
assertFalse(ShadowContentResolver.isSyncActive(createSyncAccount(),
MaterialisticProvider.PROVIDER_AUTHORITY));
setNetworkType(ConnectivityManager.TYPE_WIFI);
new ItemSyncWifiReceiver().onReceive(service, new Intent());
assertFalse(ShadowContentResolver.isSyncActive(createSyncAccount(),
MaterialisticProvider.PROVIDER_AUTHORITY));
setNetworkType(ConnectivityManager.TYPE_WIFI);
new ItemSyncWifiReceiver()
.onReceive(service, new Intent(ConnectivityManager.CONNECTIVITY_ACTION));
assertTrue(ShadowContentResolver.isSyncActive(createSyncAccount(),
MaterialisticProvider.PROVIDER_AUTHORITY));
}
@After
public void tearDown() {
serviceController.destroy();
}
private void setNetworkType(int type) {
shadowOf((ConnectivityManager) service.getSystemService(Context.CONNECTIVITY_SERVICE))
.setActiveNetworkInfo(ShadowNetworkInfo.newInstance(null, type, 0, true, true));
}
private Bundle getLastSyncExtras() {
return ShadowContentResolver.getStatus(createSyncAccount(),
MaterialisticProvider.PROVIDER_AUTHORITY).syncExtras;
}
@NonNull
private Account createSyncAccount() {
return new Account("Materialistic", BuildConfig.APPLICATION_ID);
}
private static class TestItemSyncAdapter extends ItemSyncAdapter {
SyncDelegate syncDelegate;
TestItemSyncAdapter(Context context, RestServiceFactory factory, ReadabilityClient readabilityClient) {
super(context, factory, readabilityClient);
}
@NonNull
@Override
SyncDelegate createSyncDelegate() {
syncDelegate = super.createSyncDelegate();
return syncDelegate;
}
}
}