package com.apollographql.apollo; import com.apollographql.android.impl.normalizer.EpisodeHeroName; import com.apollographql.android.impl.normalizer.HeroAndFriendsNamesWithIDs; import com.apollographql.android.impl.normalizer.type.Episode; import com.apollographql.apollo.api.Response; import com.apollographql.apollo.cache.normalized.CacheControl; import com.apollographql.apollo.cache.normalized.lru.EvictionPolicy; import com.apollographql.apollo.cache.normalized.lru.LruNormalizedCacheFactory; import com.apollographql.apollo.exception.ApolloException; import junit.framework.Assert; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.io.IOException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.annotation.Nonnull; import okhttp3.OkHttpClient; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import static com.google.common.truth.Truth.assertThat; public class ApolloWatcherTest { private ApolloClient apolloClient; private MockWebServer server; private static final int TIME_OUT_SECONDS = 3; @Before public void setUp() throws IOException { server = new MockWebServer(); server.start(); OkHttpClient okHttpClient = new OkHttpClient.Builder().build(); apolloClient = ApolloClient.builder() .serverUrl(server.url("/")) .okHttpClient(okHttpClient) .normalizedCache(new LruNormalizedCacheFactory(EvictionPolicy.NO_EVICTION), new IdFieldCacheKeyResolver()) .build(); } @After public void tearDown() { try { server.shutdown(); } catch (IOException ignored) { } } @Test public void testQueryWatcherUpdated_SameQuery_DifferentResults() throws IOException, InterruptedException, TimeoutException, ApolloException { final NamedCountDownLatch firstResponseLatch = new NamedCountDownLatch("firstResponseLatch", 1); final NamedCountDownLatch secondResponseLatch = new NamedCountDownLatch("secondResponseLatch", 2); EpisodeHeroName query = EpisodeHeroName.builder().episode(Episode.EMPIRE).build(); server.enqueue(mockResponse("EpisodeHeroNameResponseWithId.json")); ApolloQueryWatcher<EpisodeHeroName.Data> watcher = apolloClient.query(query).watcher(); watcher.enqueueAndWatch( new ApolloCall.Callback<EpisodeHeroName.Data>() { @Override public void onResponse(@Nonnull Response<EpisodeHeroName.Data> response) { if (secondResponseLatch.getCount() == 2) { assertThat(response.data().hero().name()).isEqualTo("R2-D2"); } else if (secondResponseLatch.getCount() == 1) { assertThat(response.data().hero().name()).isEqualTo("Artoo"); } firstResponseLatch.countDown(); secondResponseLatch.countDown(); } @Override public void onFailure(@Nonnull ApolloException e) { Assert.fail(e.getMessage()); } }); firstResponseLatch.awaitOrThrowWithTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS); // Another newer call gets updated information server.enqueue(mockResponse("EpisodeHeroNameResponseNameChange.json")); apolloClient.query(query).cacheControl(CacheControl.NETWORK_ONLY).execute(); secondResponseLatch.awaitOrThrowWithTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS); watcher.cancel(); } @Test public void testQueryWatcherNotUpdated_SameQuery_SameResults() throws IOException, InterruptedException, TimeoutException { final NamedCountDownLatch firstResponseLatch = new NamedCountDownLatch("firstResponseLatch", 1); final NamedCountDownLatch secondResponseLatch = new NamedCountDownLatch("secondResponseLatch", 2); EpisodeHeroName query = EpisodeHeroName.builder().episode(Episode.EMPIRE).build(); server.enqueue(mockResponse("EpisodeHeroNameResponseWithId.json")); ApolloQueryWatcher<EpisodeHeroName.Data> watcher = apolloClient.query(query).watcher(); watcher.enqueueAndWatch( new ApolloCall.Callback<EpisodeHeroName.Data>() { @Override public void onResponse(@Nonnull Response<EpisodeHeroName.Data> response) { assertThat(response.data().hero().name()).isEqualTo("R2-D2"); firstResponseLatch.countDown(); secondResponseLatch.countDown(); if (secondResponseLatch.getCount() == 0) { Assert.fail("Received two callbacks, although data should not have changed."); } } @Override public void onFailure(@Nonnull ApolloException e) { Assert.fail(e.getMessage()); } }); firstResponseLatch.awaitOrThrowWithTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS); server.enqueue(mockResponse("EpisodeHeroNameResponseWithId.json")); apolloClient.query(query).cacheControl(CacheControl.NETWORK_ONLY).enqueue(null); // Wait 3 seconds to make sure no double callback. // Successful if timeout _is_ reached secondResponseLatch.await(TIME_OUT_SECONDS, TimeUnit.SECONDS); watcher.cancel(); } @Test public void testQueryWatcherUpdated_DifferentQuery_DifferentResults() throws IOException, InterruptedException, TimeoutException, ApolloException { final NamedCountDownLatch firstResponseLatch = new NamedCountDownLatch("firstResponseLatch", 1); final NamedCountDownLatch secondResponseLatch = new NamedCountDownLatch("secondResponseLatch", 2); server.enqueue(mockResponse("EpisodeHeroNameResponseWithId.json")); EpisodeHeroName query = EpisodeHeroName.builder().episode(Episode.EMPIRE).build(); ApolloQueryWatcher<EpisodeHeroName.Data> watcher = apolloClient.query(query).watcher(); watcher.enqueueAndWatch( new ApolloCall.Callback<EpisodeHeroName.Data>() { @Override public void onResponse(@Nonnull Response<EpisodeHeroName.Data> response) { if (secondResponseLatch.getCount() == 2) { assertThat(response.data().hero().name()).isEqualTo("R2-D2"); } else if (secondResponseLatch.getCount() == 1) { assertThat(response.data().hero().name()).isEqualTo("Artoo"); } firstResponseLatch.countDown(); secondResponseLatch.countDown(); } @Override public void onFailure(@Nonnull ApolloException e) { Assert.fail(e.getMessage()); } }); firstResponseLatch.awaitOrThrowWithTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS); HeroAndFriendsNamesWithIDs friendsQuery = HeroAndFriendsNamesWithIDs.builder().episode(Episode.NEWHOPE).build(); server.enqueue(mockResponse("HeroAndFriendsNameWithIdsNameChange.json")); apolloClient.query(friendsQuery).cacheControl(CacheControl.NETWORK_ONLY).execute(); secondResponseLatch.awaitOrThrowWithTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS); watcher.cancel(); } @Test public void testQueryWatcherNotUpdated_DifferentQueries() throws IOException, InterruptedException, TimeoutException { final NamedCountDownLatch firstResponseLatch = new NamedCountDownLatch("firstResponseLatch", 1); final NamedCountDownLatch secondResponseLatch = new NamedCountDownLatch("secondResponseLatch", 2); server.enqueue(mockResponse("EpisodeHeroNameResponseWithId.json")); EpisodeHeroName query = EpisodeHeroName.builder().episode(Episode.EMPIRE).build(); ApolloQueryWatcher<EpisodeHeroName.Data> watcher = apolloClient.query(query).watcher(); watcher.enqueueAndWatch( new ApolloCall.Callback<EpisodeHeroName.Data>() { @Override public void onResponse(@Nonnull Response<EpisodeHeroName.Data> response) { assertThat(response.data().hero().name()).isEqualTo("R2-D2"); firstResponseLatch.countDown(); secondResponseLatch.countDown(); if (secondResponseLatch.getCount() == 0) { Assert.fail("Received two callbacks, although data should not have changed."); } } @Override public void onFailure(@Nonnull ApolloException e) { Assert.fail(e.getMessage()); } }); firstResponseLatch.awaitOrThrowWithTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS); HeroAndFriendsNamesWithIDs friendsQuery = HeroAndFriendsNamesWithIDs.builder().episode(Episode.NEWHOPE).build(); server.enqueue(mockResponse("HeroAndFriendsNameWithIdsResponse.json")); apolloClient.query(friendsQuery).cacheControl(CacheControl.NETWORK_ONLY).enqueue(null); // Wait 3 seconds to make sure no double callback. // Successful if timeout _is_ reached secondResponseLatch.await(TIME_OUT_SECONDS, TimeUnit.SECONDS); watcher.cancel(); } @Test public void testRefetchCacheControl() throws IOException, InterruptedException, TimeoutException { server.enqueue(mockResponse("EpisodeHeroNameResponseWithId.json")); EpisodeHeroName query = EpisodeHeroName.builder().episode(Episode.EMPIRE).build(); final NamedCountDownLatch firstResponseLatch = new NamedCountDownLatch("firstResponseLatch", 1); final NamedCountDownLatch secondResponseLatch = new NamedCountDownLatch("secondResponseLatch", 2); ApolloQueryWatcher<EpisodeHeroName.Data> watcher = apolloClient.query(query).watcher(); watcher.refetchCacheControl(CacheControl.NETWORK_ONLY) //Force network instead of CACHE_FIRST default .enqueueAndWatch( new ApolloCall.Callback<EpisodeHeroName.Data>() { @Override public void onResponse(@Nonnull Response<EpisodeHeroName.Data> response) { if (secondResponseLatch.getCount() == 2) { assertThat(response.data().hero().name()).isEqualTo("R2-D2"); } else if (secondResponseLatch.getCount() == 1) { assertThat(response.data().hero().name()).isEqualTo("ArTwo"); } else { Assert.fail("Unknown hero name: " + response.data().hero().name()); } firstResponseLatch.countDown(); secondResponseLatch.countDown(); } @Override public void onFailure(@Nonnull ApolloException e) { Assert.fail(e.getCause().getMessage()); } }); firstResponseLatch.awaitOrThrowWithTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS); //A different call gets updated information. server.enqueue(mockResponse("EpisodeHeroNameResponseNameChange.json")); //To verify that the updated response comes from server use a different name change // -- this is for the refetch server.enqueue(mockResponse("EpisodeHeroNameResponseNameChangeTwo.json")); apolloClient.query(query).cacheControl(CacheControl.NETWORK_ONLY).enqueue(null); secondResponseLatch.awaitOrThrowWithTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS); watcher.cancel(); } @Test public void testQueryWatcherUpdated_SameQuery_DifferentResults_cacheOnly() throws IOException, InterruptedException, TimeoutException { final NamedCountDownLatch cacheWarmUpLatch = new NamedCountDownLatch("cacheWarmUpLatch", 1); EpisodeHeroName query = EpisodeHeroName.builder().episode(Episode.EMPIRE).build(); server.enqueue(mockResponse("EpisodeHeroNameResponseWithId.json")); apolloClient.query(query).enqueue(new ApolloCall.Callback<EpisodeHeroName.Data>() { @Override public void onResponse(@Nonnull Response<EpisodeHeroName.Data> response) { cacheWarmUpLatch.countDown(); } @Override public void onFailure(@Nonnull ApolloException e) { Assert.fail(e.getMessage()); cacheWarmUpLatch.countDown(); } }); cacheWarmUpLatch.awaitOrThrowWithTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS); //Cache is now "warm" with response data final NamedCountDownLatch firstResponseLatch = new NamedCountDownLatch("firstResponseLatch", 1); final NamedCountDownLatch secondResponseLatch = new NamedCountDownLatch("secondResponseLatch", 2); ApolloQueryWatcher<EpisodeHeroName.Data> watcher = apolloClient.query(query) .cacheControl(CacheControl.CACHE_ONLY) .watcher(); watcher.enqueueAndWatch( new ApolloCall.Callback<EpisodeHeroName.Data>() { @Override public void onResponse(@Nonnull Response<EpisodeHeroName.Data> response) { if (secondResponseLatch.getCount() == 2) { assertThat(response.data().hero().name()).isEqualTo("R2-D2"); } else if (secondResponseLatch.getCount() == 1) { assertThat(response.data().hero().name()).isEqualTo("Artoo"); } firstResponseLatch.countDown(); secondResponseLatch.countDown(); } @Override public void onFailure(@Nonnull ApolloException e) { Assert.fail(e.getMessage()); } }); firstResponseLatch.awaitOrThrowWithTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS); //Another newer call gets updated information server.enqueue(mockResponse("EpisodeHeroNameResponseNameChange.json")); apolloClient.query(query).cacheControl(CacheControl.NETWORK_ONLY).enqueue(null); secondResponseLatch.awaitOrThrowWithTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS); watcher.cancel(); } @Test public void testQueryWatcherNotCalled_WhenCanceled() throws IOException, TimeoutException, InterruptedException, ApolloException { final NamedCountDownLatch firstResponseLatch = new NamedCountDownLatch("firstResponseLatch", 1); final NamedCountDownLatch secondResponseLatch = new NamedCountDownLatch("secondResponseLatch", 2); EpisodeHeroName query = EpisodeHeroName.builder().episode(Episode.EMPIRE).build(); server.enqueue(mockResponse("EpisodeHeroNameResponseWithId.json")); ApolloQueryWatcher<EpisodeHeroName.Data> watcher = apolloClient.query(query).watcher(); watcher.enqueueAndWatch( new ApolloCall.Callback<EpisodeHeroName.Data>() { @Override public void onResponse(@Nonnull Response<EpisodeHeroName.Data> response) { assertThat(response.data().hero().name()).isEqualTo("R2-D2"); firstResponseLatch.countDown(); secondResponseLatch.countDown(); if (secondResponseLatch.getCount() == 0) { Assert.fail("Received two callbacks, although query watcher has already been canceled"); } } @Override public void onFailure(@Nonnull ApolloException e) { Assert.fail(e.getMessage()); } }); firstResponseLatch.awaitOrThrowWithTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS); server.enqueue(mockResponse("EpisodeHeroNameResponseNameChange.json")); watcher.cancel(); apolloClient.query(query).cacheControl(CacheControl.NETWORK_ONLY).execute(); //Wait for 3 seconds to check that callback is not called twice. //Test is successful if timeout is reached. secondResponseLatch.await(TIME_OUT_SECONDS, TimeUnit.SECONDS); } private MockResponse mockResponse(String fileName) throws IOException { return new MockResponse().setChunkedBody(Utils.readFileToString(getClass(), "/" + fileName), 32); } }