From 212e7004acfdce76c900fd97070e2e5e8476be20 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Wed, 7 Jul 2021 14:01:06 -0700 Subject: Import of Volley from GitHub to AOSP. Android.bp has been updated to account for the new source directory structure. - 0dc50bcfd021c204a9e6c9e7e6befbdfa1027247 Refactor Volley into a multi-module project. (#418) by Jeff Davidson - 763c86b0bc9f66a8bb499f6a8b7fd3bdc87621a8 Remove new constructors from JsonRequests which are break... by Jeff Davidson - 8d1b1a59e7cd1b1d3c6d8686f8831cea08f80d1f Add @NonNull annotations to Volley (#413) by Kamal Faraj - 5ba41f8670413973f587e435598f9f1724fa26e9 Allow sending any JSON with JsonArrayRequest & JsonObject... by Paul Smith - 784cdd755392a6080e5eb0bf94bd7bf4ea31cf17 Update SNAPSHOT version after 1.2.0 release by Jeff Davidson - 0d6497bab417a5f78b3c8e03ea157ada0fbfbc5d Add developers stanza to Volley POM. (#400) by Jeff Davidson - 36274bf515a699ae5a7fe3d321206d1b803226d8 API cleanup for Async Volley stack ahead of 1.2.0 release... by Jeff Davidson - 03f0144843fcf9ebafe512647c1c588975429452 Update environment variable name for snapshot pushes. (#3... by Jeff Davidson - 3bd1975652687d2baa1b11a7f02b135edede8523 Publish SNAPSHOT builds to OSSRH instead of OJO. (#397) by Jeff Davidson - 0e0c3d9cfa694f8f1400a9e9abc4bc11761fdb52 Invoke RetryPolicy#retry in the blocking executor. (#393) by Jeff Davidson - b51831a48f06ad28f627c3624e5edb41598a2bf8 Use a consistent timebase when evaluating soft/hard TTLs.... by Jeff Davidson - cd0839113b100f163df1ebd04ce6d5b9e36e9863 Migrate from Travis CI to GitHub Actions. (#381) by Jeff Davidson - bdc0e393199ebf9e67c4e29e665252818eed4639 Clean up cache initialization in AsyncRequestQueue. (#380) by Jeff Davidson - 1c0ade36edde15d02844b40351ab6f80c63b71b3 Actually allow applications to provide custom executors. by Jeff Davidson GitOrigin-RevId: 0dc50bcfd021c204a9e6c9e7e6befbdfa1027247 Change-Id: I4b8e4098ad5c349cb83efc867273fac1d3582a34 --- .../com/android/volley/AsyncRequestQueueTest.java | 200 +++++++ .../com/android/volley/CacheDispatcherTest.java | 276 +++++++++ .../com/android/volley/NetworkDispatcherTest.java | 146 +++++ .../com/android/volley/NetworkResponseTest.java | 61 ++ .../volley/RequestQueueIntegrationTest.java | 197 +++++++ .../java/com/android/volley/RequestQueueTest.java | 129 ++++ .../test/java/com/android/volley/RequestTest.java | 232 ++++++++ .../com/android/volley/ResponseDeliveryTest.java | 71 +++ .../com/android/volley/mock/MockAsyncStack.java | 86 +++ .../com/android/volley/mock/MockHttpStack.java | 80 +++ .../java/com/android/volley/mock/MockRequest.java | 99 ++++ .../com/android/volley/mock/ShadowSystemClock.java | 27 + .../volley/toolbox/AdaptedHttpStackTest.java | 128 ++++ .../volley/toolbox/AndroidAuthenticatorTest.java | 111 ++++ .../android/volley/toolbox/BaseHttpStackTest.java | 104 ++++ .../volley/toolbox/BasicAsyncNetworkTest.java | 508 ++++++++++++++++ .../android/volley/toolbox/BasicNetworkTest.java | 384 ++++++++++++ .../android/volley/toolbox/ByteArrayPoolTest.java | 78 +++ .../java/com/android/volley/toolbox/CacheTest.java | 39 ++ .../android/volley/toolbox/DiskBasedCacheTest.java | 646 +++++++++++++++++++++ .../volley/toolbox/HttpClientStackTest.java | 156 +++++ .../volley/toolbox/HttpHeaderParserTest.java | 317 ++++++++++ .../volley/toolbox/HttpStackConformanceTest.java | 192 ++++++ .../com/android/volley/toolbox/HurlStackTest.java | 337 +++++++++++ .../android/volley/toolbox/ImageLoaderTest.java | 121 ++++ .../android/volley/toolbox/ImageRequestTest.java | 194 +++++++ .../volley/toolbox/JsonRequestCharsetTest.java | 119 ++++ .../android/volley/toolbox/JsonRequestTest.java | 73 +++ .../volley/toolbox/NetworkImageViewTest.java | 101 ++++ .../toolbox/PoolingByteArrayOutputStreamTest.java | 81 +++ .../android/volley/toolbox/RequestFutureTest.java | 35 ++ .../android/volley/toolbox/RequestQueueTest.java | 51 ++ .../com/android/volley/toolbox/RequestTest.java | 77 +++ .../com/android/volley/toolbox/ResponseTest.java | 55 ++ .../android/volley/toolbox/StringRequestTest.java | 42 ++ .../com/android/volley/utils/CacheTestUtils.java | 89 +++ .../volley/utils/ImmediateResponseDelivery.java | 37 ++ 37 files changed, 5679 insertions(+) create mode 100644 core/src/test/java/com/android/volley/AsyncRequestQueueTest.java create mode 100644 core/src/test/java/com/android/volley/CacheDispatcherTest.java create mode 100644 core/src/test/java/com/android/volley/NetworkDispatcherTest.java create mode 100644 core/src/test/java/com/android/volley/NetworkResponseTest.java create mode 100644 core/src/test/java/com/android/volley/RequestQueueIntegrationTest.java create mode 100644 core/src/test/java/com/android/volley/RequestQueueTest.java create mode 100644 core/src/test/java/com/android/volley/RequestTest.java create mode 100644 core/src/test/java/com/android/volley/ResponseDeliveryTest.java create mode 100644 core/src/test/java/com/android/volley/mock/MockAsyncStack.java create mode 100644 core/src/test/java/com/android/volley/mock/MockHttpStack.java create mode 100644 core/src/test/java/com/android/volley/mock/MockRequest.java create mode 100644 core/src/test/java/com/android/volley/mock/ShadowSystemClock.java create mode 100644 core/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/CacheTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/HurlStackTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/ImageRequestTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/JsonRequestTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/RequestFutureTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/RequestQueueTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/RequestTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/ResponseTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/StringRequestTest.java create mode 100644 core/src/test/java/com/android/volley/utils/CacheTestUtils.java create mode 100644 core/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java (limited to 'core/src/test/java/com') diff --git a/core/src/test/java/com/android/volley/AsyncRequestQueueTest.java b/core/src/test/java/com/android/volley/AsyncRequestQueueTest.java new file mode 100644 index 0000000..aef4f01 --- /dev/null +++ b/core/src/test/java/com/android/volley/AsyncRequestQueueTest.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.android.volley; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.volley.AsyncCache.OnGetCompleteCallback; +import com.android.volley.AsyncCache.OnWriteCompleteCallback; +import com.android.volley.mock.ShadowSystemClock; +import com.android.volley.toolbox.NoAsyncCache; +import com.android.volley.toolbox.StringRequest; +import com.android.volley.utils.ImmediateResponseDelivery; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Unit tests for AsyncRequestQueue, with all dependencies mocked out */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowSystemClock.class}) +public class AsyncRequestQueueTest { + + @Mock private AsyncNetwork mMockNetwork; + @Mock private ScheduledExecutorService mMockScheduledExecutor; + private final ResponseDelivery mDelivery = new ImmediateResponseDelivery(); + private AsyncRequestQueue queue; + + @Before + public void setUp() throws Exception { + initMocks(this); + queue = createRequestQueue(new NoAsyncCache()); + } + + @Test + public void cancelAll_onlyCorrectTag() throws Exception { + queue.start(); + Object tagA = new Object(); + Object tagB = new Object(); + StringRequest req1 = mock(StringRequest.class); + when(req1.getTag()).thenReturn(tagA); + StringRequest req2 = mock(StringRequest.class); + when(req2.getTag()).thenReturn(tagB); + StringRequest req3 = mock(StringRequest.class); + when(req3.getTag()).thenReturn(tagA); + StringRequest req4 = mock(StringRequest.class); + when(req4.getTag()).thenReturn(tagA); + + queue.add(req1); // A + queue.add(req2); // B + queue.add(req3); // A + queue.cancelAll(tagA); + queue.add(req4); // A + + verify(req1).cancel(); // A cancelled + verify(req3).cancel(); // A cancelled + verify(req2, never()).cancel(); // B not cancelled + verify(req4, never()).cancel(); // A added after cancel not cancelled + queue.stop(); + } + + @Test + public void add_notifiesListener() throws Exception { + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + queue.start(); + queue.addRequestEventListener(listener); + StringRequest req = mock(StringRequest.class); + + queue.add(req); + + verify(listener).onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_QUEUED); + verifyNoMoreInteractions(listener); + queue.stop(); + } + + @Test + public void finish_notifiesListener() throws Exception { + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + queue.start(); + queue.addRequestEventListener(listener); + StringRequest req = mock(StringRequest.class); + + queue.finish(req); + + verify(listener).onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_FINISHED); + verifyNoMoreInteractions(listener); + queue.stop(); + } + + @Test + public void sendRequestEvent_notifiesListener() throws Exception { + StringRequest req = mock(StringRequest.class); + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + queue.start(); + queue.addRequestEventListener(listener); + + queue.sendRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + + verify(listener) + .onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + verifyNoMoreInteractions(listener); + queue.stop(); + } + + @Test + public void removeRequestEventListener_removesListener() throws Exception { + StringRequest req = mock(StringRequest.class); + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + queue.start(); + queue.addRequestEventListener(listener); + queue.removeRequestEventListener(listener); + + queue.sendRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + + verifyNoMoreInteractions(listener); + queue.stop(); + } + + @Test + public void requestsQueuedBeforeCacheInitialization_asyncCache() { + // Create a new queue with a mock cache in order to verify the initialization. + AsyncCache mockAsyncCache = mock(AsyncCache.class); + AsyncRequestQueue queue = createRequestQueue(mockAsyncCache); + queue.start(); + + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(OnWriteCompleteCallback.class); + verify(mockAsyncCache).initialize(callbackCaptor.capture()); + + StringRequest req = mock(StringRequest.class); + req.setShouldCache(true); + when(req.getCacheKey()).thenReturn("cache-key"); + queue.add(req); + + // Cache should not be read before initialization completes. + verify(mockAsyncCache, never()).get(anyString(), any(OnGetCompleteCallback.class)); + + callbackCaptor.getValue().onWriteComplete(); + + // Once the write completes, the request should be kicked off (in the form of a cache + // lookup). + verify(mockAsyncCache).get(eq("cache-key"), any(OnGetCompleteCallback.class)); + + queue.stop(); + } + + private AsyncRequestQueue createRequestQueue(AsyncCache asyncCache) { + return new AsyncRequestQueue.Builder(mMockNetwork) + .setResponseDelivery(mDelivery) + .setAsyncCache(asyncCache) + .setExecutorFactory( + new AsyncRequestQueue.ExecutorFactory() { + @Override + public ExecutorService createNonBlockingExecutor( + BlockingQueue taskQueue) { + return MoreExecutors.newDirectExecutorService(); + } + + @Override + public ExecutorService createBlockingExecutor( + BlockingQueue taskQueue) { + return MoreExecutors.newDirectExecutorService(); + } + + @Override + public ScheduledExecutorService createNonBlockingScheduledExecutor() { + return mMockScheduledExecutor; + } + }) + .build(); + } +} diff --git a/core/src/test/java/com/android/volley/CacheDispatcherTest.java b/core/src/test/java/com/android/volley/CacheDispatcherTest.java new file mode 100644 index 0000000..aef6785 --- /dev/null +++ b/core/src/test/java/com/android/volley/CacheDispatcherTest.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.android.volley; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.volley.toolbox.StringRequest; +import com.android.volley.utils.CacheTestUtils; +import java.util.concurrent.BlockingQueue; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +@SuppressWarnings("rawtypes") +public class CacheDispatcherTest { + private CacheDispatcher mDispatcher; + private @Mock BlockingQueue> mCacheQueue; + private @Mock BlockingQueue> mNetworkQueue; + private @Mock Cache mCache; + private @Mock ResponseDelivery mDelivery; + private @Mock Network mNetwork; + private StringRequest mRequest; + + @Before + public void setUp() throws Exception { + initMocks(this); + + mRequest = new StringRequest(Request.Method.GET, "http://foo", null, null); + mDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery); + } + + private static class WaitForever implements Answer { + @Override + public Object answer(InvocationOnMock invocationOnMock) throws Throwable { + Thread.sleep(Long.MAX_VALUE); + return null; + } + } + + @Test + public void runStopsOnQuit() throws Exception { + when(mCacheQueue.take()).then(new WaitForever()); + mDispatcher.start(); + mDispatcher.quit(); + mDispatcher.join(1000); + } + + private static void verifyNoResponse(ResponseDelivery delivery) { + verify(delivery, never()).postResponse(any(Request.class), any(Response.class)); + verify(delivery, never()) + .postResponse(any(Request.class), any(Response.class), any(Runnable.class)); + verify(delivery, never()).postError(any(Request.class), any(VolleyError.class)); + } + + // A cancelled request should not be processed at all. + @Test + public void cancelledRequest() throws Exception { + mRequest.cancel(); + mDispatcher.processRequest(mRequest); + verify(mCache, never()).get(anyString()); + verifyNoResponse(mDelivery); + } + + // A cache miss does not post a response and puts the request on the network queue. + @Test + public void cacheMiss() throws Exception { + mDispatcher.processRequest(mRequest); + verifyNoResponse(mDelivery); + verify(mNetworkQueue).put(mRequest); + assertNull(mRequest.getCacheEntry()); + } + + // A non-expired cache hit posts a response and does not queue to the network. + @Test + public void nonExpiredCacheHit() throws Exception { + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false); + when(mCache.get(anyString())).thenReturn(entry); + mDispatcher.processRequest(mRequest); + verify(mDelivery).postResponse(any(Request.class), any(Response.class)); + verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class)); + } + + // A soft-expired cache hit posts a response and queues to the network. + @Test + public void softExpiredCacheHit() throws Exception { + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true); + when(mCache.get(anyString())).thenReturn(entry); + mDispatcher.processRequest(mRequest); + + // Soft expiration needs to use the deferred Runnable variant of postResponse, + // so make sure it gets to run. + ArgumentCaptor runnable = ArgumentCaptor.forClass(Runnable.class); + verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture()); + runnable.getValue().run(); + // This way we can verify the behavior of the Runnable as well. + verify(mNetworkQueue).put(mRequest); + assertSame(entry, mRequest.getCacheEntry()); + + verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class)); + } + + // An expired cache hit does not post a response and queues to the network. + @Test + public void expiredCacheHit() throws Exception { + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, true, true); + when(mCache.get(anyString())).thenReturn(entry); + mDispatcher.processRequest(mRequest); + verifyNoResponse(mDelivery); + verify(mNetworkQueue).put(mRequest); + assertSame(entry, mRequest.getCacheEntry()); + } + + // An fresh cache hit with parse error, does not post a response and queues to the network. + @Test + public void freshCacheHit_parseError() throws Exception { + Request request = mock(Request.class); + when(request.parseNetworkResponse(any(NetworkResponse.class))) + .thenReturn(Response.error(new ParseError())); + when(request.getCacheKey()).thenReturn("cache/key"); + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false); + when(mCache.get(anyString())).thenReturn(entry); + + mDispatcher.processRequest(request); + + verifyNoResponse(mDelivery); + verify(mNetworkQueue).put(request); + assertNull(request.getCacheEntry()); + verify(mCache).invalidate("cache/key", true); + verify(request).addMarker("cache-parsing-failed"); + } + + @Test + public void duplicateCacheMiss() throws Exception { + StringRequest secondRequest = + new StringRequest(Request.Method.GET, "http://foo", null, null); + mRequest.setSequence(1); + secondRequest.setSequence(2); + mDispatcher.processRequest(mRequest); + mDispatcher.processRequest(secondRequest); + verify(mNetworkQueue).put(mRequest); + verifyNoResponse(mDelivery); + } + + @Test + public void tripleCacheMiss_networkErrorOnFirst() throws Exception { + StringRequest secondRequest = + new StringRequest(Request.Method.GET, "http://foo", null, null); + StringRequest thirdRequest = + new StringRequest(Request.Method.GET, "http://foo", null, null); + mRequest.setSequence(1); + secondRequest.setSequence(2); + thirdRequest.setSequence(3); + mDispatcher.processRequest(mRequest); + mDispatcher.processRequest(secondRequest); + mDispatcher.processRequest(thirdRequest); + + verify(mNetworkQueue).put(mRequest); + verifyNoResponse(mDelivery); + + ((Request) mRequest).notifyListenerResponseNotUsable(); + // Second request should now be in network queue. + verify(mNetworkQueue).put(secondRequest); + // Another unusable response, third request should now be added. + ((Request) secondRequest).notifyListenerResponseNotUsable(); + verify(mNetworkQueue).put(thirdRequest); + } + + @Test + public void duplicateSoftExpiredCacheHit_failedRequest() throws Exception { + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true); + when(mCache.get(anyString())).thenReturn(entry); + + StringRequest secondRequest = + new StringRequest(Request.Method.GET, "http://foo", null, null); + mRequest.setSequence(1); + secondRequest.setSequence(2); + + mDispatcher.processRequest(mRequest); + mDispatcher.processRequest(secondRequest); + + // Soft expiration needs to use the deferred Runnable variant of postResponse, + // so make sure it gets to run. + ArgumentCaptor runnable = ArgumentCaptor.forClass(Runnable.class); + verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture()); + runnable.getValue().run(); + // This way we can verify the behavior of the Runnable as well. + + verify(mNetworkQueue).put(mRequest); + verify(mDelivery) + .postResponse(any(Request.class), any(Response.class), any(Runnable.class)); + + ((Request) mRequest).notifyListenerResponseNotUsable(); + // Second request should now be in network queue. + verify(mNetworkQueue).put(secondRequest); + } + + @Test + public void duplicateSoftExpiredCacheHit_successfulRequest() throws Exception { + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true); + when(mCache.get(anyString())).thenReturn(entry); + + StringRequest secondRequest = + new StringRequest(Request.Method.GET, "http://foo", null, null); + mRequest.setSequence(1); + secondRequest.setSequence(2); + + mDispatcher.processRequest(mRequest); + mDispatcher.processRequest(secondRequest); + + // Soft expiration needs to use the deferred Runnable variant of postResponse, + // so make sure it gets to run. + ArgumentCaptor runnable = ArgumentCaptor.forClass(Runnable.class); + verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture()); + runnable.getValue().run(); + // This way we can verify the behavior of the Runnable as well. + + verify(mNetworkQueue).put(mRequest); + verify(mDelivery) + .postResponse(any(Request.class), any(Response.class), any(Runnable.class)); + + ((Request) mRequest).notifyListenerResponseReceived(Response.success(null, entry)); + // Second request should have delivered response. + verify(mNetworkQueue, never()).put(secondRequest); + verify(mDelivery) + .postResponse(any(Request.class), any(Response.class), any(Runnable.class)); + } + + @Test + public void processRequestNotifiesListener() throws Exception { + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + RequestQueue queue = new RequestQueue(mCache, mNetwork, 0, mDelivery); + queue.addRequestEventListener(listener); + mRequest.setRequestQueue(queue); + + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false); + when(mCache.get(anyString())).thenReturn(entry); + mDispatcher.processRequest(mRequest); + + InOrder inOrder = inOrder(listener); + inOrder.verify(listener) + .onRequestEvent(mRequest, RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_STARTED); + inOrder.verify(listener) + .onRequestEvent(mRequest, RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED); + inOrder.verifyNoMoreInteractions(); + } +} diff --git a/core/src/test/java/com/android/volley/NetworkDispatcherTest.java b/core/src/test/java/com/android/volley/NetworkDispatcherTest.java new file mode 100644 index 0000000..74dfe8a --- /dev/null +++ b/core/src/test/java/com/android/volley/NetworkDispatcherTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.android.volley; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.volley.toolbox.NoCache; +import com.android.volley.toolbox.StringRequest; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.BlockingQueue; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class NetworkDispatcherTest { + private NetworkDispatcher mDispatcher; + private @Mock ResponseDelivery mDelivery; + private @Mock BlockingQueue> mNetworkQueue; + private @Mock Network mNetwork; + private @Mock Cache mCache; + private StringRequest mRequest; + + private static final byte[] CANNED_DATA = + "Ceci n'est pas une vraie reponse".getBytes(StandardCharsets.UTF_8); + + @Before + public void setUp() throws Exception { + initMocks(this); + mRequest = new StringRequest(Request.Method.GET, "http://foo", null, null); + mDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery); + } + + @Test + public void successPostsResponse() throws Exception { + when(mNetwork.performRequest(any(Request.class))) + .thenReturn(new NetworkResponse(CANNED_DATA)); + mDispatcher.processRequest(mRequest); + + ArgumentCaptor response = ArgumentCaptor.forClass(Response.class); + verify(mDelivery).postResponse(any(Request.class), response.capture()); + assertTrue(response.getValue().isSuccess()); + assertEquals(response.getValue().result, new String(CANNED_DATA, StandardCharsets.UTF_8)); + + verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class)); + } + + @Test + public void successNotifiesListener() throws Exception { + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + RequestQueue queue = new RequestQueue(new NoCache(), mNetwork, 0, mDelivery); + queue.addRequestEventListener(listener); + mRequest.setRequestQueue(queue); + + when(mNetwork.performRequest(any(Request.class))) + .thenReturn(new NetworkResponse(CANNED_DATA)); + + mDispatcher.processRequest(mRequest); + + InOrder inOrder = inOrder(listener); + inOrder.verify(listener) + .onRequestEvent( + mRequest, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + inOrder.verify(listener) + .onRequestEvent( + mRequest, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void exceptionPostsError() throws Exception { + when(mNetwork.performRequest(any(Request.class))).thenThrow(new ServerError()); + mDispatcher.processRequest(mRequest); + + verify(mDelivery).postError(any(Request.class), any(VolleyError.class)); + verify(mDelivery, never()).postResponse(any(Request.class), any(Response.class)); + } + + @Test + public void exceptionNotifiesListener() throws Exception { + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + RequestQueue queue = new RequestQueue(new NoCache(), mNetwork, 0, mDelivery); + queue.addRequestEventListener(listener); + mRequest.setRequestQueue(queue); + + when(mNetwork.performRequest(any(Request.class))).thenThrow(new ServerError()); + + mDispatcher.processRequest(mRequest); + + InOrder inOrder = inOrder(listener); + inOrder.verify(listener) + .onRequestEvent( + mRequest, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + inOrder.verify(listener) + .onRequestEvent( + mRequest, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void shouldCacheFalse() throws Exception { + mRequest.setShouldCache(false); + mDispatcher.processRequest(mRequest); + verify(mCache, never()).put(anyString(), any(Cache.Entry.class)); + } + + @Test + public void shouldCacheTrue() throws Exception { + when(mNetwork.performRequest(any(Request.class))) + .thenReturn(new NetworkResponse(CANNED_DATA)); + mRequest.setShouldCache(true); + mDispatcher.processRequest(mRequest); + ArgumentCaptor entry = ArgumentCaptor.forClass(Cache.Entry.class); + verify(mCache).put(eq(mRequest.getCacheKey()), entry.capture()); + assertTrue(Arrays.equals(entry.getValue().data, CANNED_DATA)); + } +} diff --git a/core/src/test/java/com/android/volley/NetworkResponseTest.java b/core/src/test/java/com/android/volley/NetworkResponseTest.java new file mode 100644 index 0000000..70210da --- /dev/null +++ b/core/src/test/java/com/android/volley/NetworkResponseTest.java @@ -0,0 +1,61 @@ +package com.android.volley; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class NetworkResponseTest { + + @SuppressWarnings("deprecation") + @Test + public void mapToList() { + Map headers = new HashMap<>(); + headers.put("key1", "value1"); + headers.put("key2", "value2"); + + NetworkResponse resp = new NetworkResponse(200, null, headers, false); + + List
expectedHeaders = new ArrayList<>(); + expectedHeaders.add(new Header("key1", "value1")); + expectedHeaders.add(new Header("key2", "value2")); + + assertThat(expectedHeaders, containsInAnyOrder(resp.allHeaders.toArray(new Header[0]))); + } + + @Test + public void listToMap() { + List
headers = new ArrayList<>(); + headers.add(new Header("key1", "value1")); + // Later values should be preferred. + headers.add(new Header("key2", "ignoredvalue")); + headers.add(new Header("key2", "value2")); + + NetworkResponse resp = new NetworkResponse(200, null, false, 0L, headers); + + Map expectedHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + expectedHeaders.put("key1", "value1"); + expectedHeaders.put("key2", "value2"); + + assertEquals(expectedHeaders, resp.headers); + } + + @SuppressWarnings("deprecation") + @Test + public void nullValuesDontCrash() { + new NetworkResponse(null); + new NetworkResponse(null, null); + new NetworkResponse(200, null, null, false); + new NetworkResponse(200, null, null, false, 0L); + new NetworkResponse(200, null, false, 0L, null); + } +} diff --git a/core/src/test/java/com/android/volley/RequestQueueIntegrationTest.java b/core/src/test/java/com/android/volley/RequestQueueIntegrationTest.java new file mode 100644 index 0000000..a2bfbc6 --- /dev/null +++ b/core/src/test/java/com/android/volley/RequestQueueIntegrationTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.android.volley; + +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.volley.Request.Priority; +import com.android.volley.RequestQueue.RequestFinishedListener; +import com.android.volley.mock.MockRequest; +import com.android.volley.mock.ShadowSystemClock; +import com.android.volley.toolbox.NoCache; +import com.android.volley.utils.ImmediateResponseDelivery; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Integration tests for {@link RequestQueue} that verify its behavior in conjunction with real + * dispatcher, queues and Requests. + * + *

The Network is mocked out. + */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowSystemClock.class}) +public class RequestQueueIntegrationTest { + + private ResponseDelivery mDelivery; + @Mock private Network mMockNetwork; + @Mock private RequestFinishedListener mMockListener; + @Mock private RequestFinishedListener mMockListener2; + + @Before + public void setUp() throws Exception { + mDelivery = new ImmediateResponseDelivery(); + initMocks(this); + } + + @Test + public void add_requestProcessedInCorrectOrder() throws Exception { + // Enqueue 2 requests with different cache keys, and different priorities. The second, + // higher priority request takes 20ms. + // Assert that the first request is only handled after the first one has been parsed and + // delivered. + MockRequest lowerPriorityReq = new MockRequest(); + MockRequest higherPriorityReq = new MockRequest(); + lowerPriorityReq.setCacheKey("1"); + higherPriorityReq.setCacheKey("2"); + lowerPriorityReq.setPriority(Priority.LOW); + higherPriorityReq.setPriority(Priority.HIGH); + + Answer delayAnswer = + new Answer() { + @Override + public NetworkResponse answer(InvocationOnMock invocationOnMock) + throws Throwable { + Thread.sleep(20); + return mock(NetworkResponse.class); + } + }; + // delay only for higher request + when(mMockNetwork.performRequest(higherPriorityReq)).thenAnswer(delayAnswer); + when(mMockNetwork.performRequest(lowerPriorityReq)).thenReturn(mock(NetworkResponse.class)); + + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery); + queue.addRequestFinishedListener(mMockListener); + queue.add(lowerPriorityReq); + queue.add(higherPriorityReq); + queue.start(); + + InOrder inOrder = inOrder(mMockListener); + // verify higherPriorityReq goes through first + inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(higherPriorityReq); + // verify lowerPriorityReq goes last + inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(lowerPriorityReq); + + queue.stop(); + } + + /** Asserts that requests with same cache key are processed in order. */ + @Test + public void add_dedupeByCacheKey() throws Exception { + // Enqueue 2 requests with the same cache key. The first request takes 20ms. Assert that the + // second request is only handled after the first one has been parsed and delivered. + MockRequest req1 = new MockRequest(); + MockRequest req2 = new MockRequest(); + Answer delayAnswer = + new Answer() { + @Override + public NetworkResponse answer(InvocationOnMock invocationOnMock) + throws Throwable { + Thread.sleep(20); + return mock(NetworkResponse.class); + } + }; + // delay only for first + when(mMockNetwork.performRequest(req1)).thenAnswer(delayAnswer); + when(mMockNetwork.performRequest(req2)).thenReturn(mock(NetworkResponse.class)); + + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 3, mDelivery); + queue.addRequestFinishedListener(mMockListener); + queue.add(req1); + queue.add(req2); + queue.start(); + + InOrder inOrder = inOrder(mMockListener); + // verify req1 goes through first + inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(req1); + // verify req2 goes last + inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(req2); + + queue.stop(); + } + + /** Verify RequestFinishedListeners are informed when requests are canceled. */ + @Test + public void add_requestFinishedListenerCanceled() throws Exception { + MockRequest request = new MockRequest(); + Answer delayAnswer = + new Answer() { + @Override + public NetworkResponse answer(InvocationOnMock invocationOnMock) + throws Throwable { + Thread.sleep(200); + return mock(NetworkResponse.class); + } + }; + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery); + + when(mMockNetwork.performRequest(request)).thenAnswer(delayAnswer); + + queue.addRequestFinishedListener(mMockListener); + queue.start(); + queue.add(request); + + request.cancel(); + verify(mMockListener, timeout(10000)).onRequestFinished(request); + queue.stop(); + } + + /** Verify RequestFinishedListeners are informed when requests are successfully delivered. */ + @Test + public void add_requestFinishedListenerSuccess() throws Exception { + MockRequest request = new MockRequest(); + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery); + + queue.addRequestFinishedListener(mMockListener); + queue.addRequestFinishedListener(mMockListener2); + queue.start(); + queue.add(request); + + verify(mMockListener, timeout(10000)).onRequestFinished(request); + verify(mMockListener2, timeout(10000)).onRequestFinished(request); + + queue.stop(); + } + + /** Verify RequestFinishedListeners are informed when request errors. */ + @Test + public void add_requestFinishedListenerError() throws Exception { + MockRequest request = new MockRequest(); + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery); + + when(mMockNetwork.performRequest(request)).thenThrow(new VolleyError()); + + queue.addRequestFinishedListener(mMockListener); + queue.start(); + queue.add(request); + + verify(mMockListener, timeout(10000)).onRequestFinished(request); + queue.stop(); + } +} diff --git a/core/src/test/java/com/android/volley/RequestQueueTest.java b/core/src/test/java/com/android/volley/RequestQueueTest.java new file mode 100644 index 0000000..ba9b0f8 --- /dev/null +++ b/core/src/test/java/com/android/volley/RequestQueueTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.android.volley; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.volley.mock.ShadowSystemClock; +import com.android.volley.toolbox.NoCache; +import com.android.volley.toolbox.StringRequest; +import com.android.volley.utils.ImmediateResponseDelivery; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Unit tests for RequestQueue, with all dependencies mocked out */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowSystemClock.class}) +public class RequestQueueTest { + + private ResponseDelivery mDelivery; + @Mock private Network mMockNetwork; + + @Before + public void setUp() throws Exception { + mDelivery = new ImmediateResponseDelivery(); + initMocks(this); + } + + @Test + public void cancelAll_onlyCorrectTag() throws Exception { + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery); + Object tagA = new Object(); + Object tagB = new Object(); + StringRequest req1 = mock(StringRequest.class); + when(req1.getTag()).thenReturn(tagA); + StringRequest req2 = mock(StringRequest.class); + when(req2.getTag()).thenReturn(tagB); + StringRequest req3 = mock(StringRequest.class); + when(req3.getTag()).thenReturn(tagA); + StringRequest req4 = mock(StringRequest.class); + when(req4.getTag()).thenReturn(tagA); + + queue.add(req1); // A + queue.add(req2); // B + queue.add(req3); // A + queue.cancelAll(tagA); + queue.add(req4); // A + + verify(req1).cancel(); // A cancelled + verify(req3).cancel(); // A cancelled + verify(req2, never()).cancel(); // B not cancelled + verify(req4, never()).cancel(); // A added after cancel not cancelled + } + + @Test + public void add_notifiesListener() throws Exception { + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery); + queue.addRequestEventListener(listener); + StringRequest req = mock(StringRequest.class); + + queue.add(req); + + verify(listener).onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_QUEUED); + verifyNoMoreInteractions(listener); + } + + @Test + public void finish_notifiesListener() throws Exception { + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery); + queue.addRequestEventListener(listener); + StringRequest req = mock(StringRequest.class); + + queue.finish(req); + + verify(listener).onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_FINISHED); + verifyNoMoreInteractions(listener); + } + + @Test + public void sendRequestEvent_notifiesListener() throws Exception { + StringRequest req = mock(StringRequest.class); + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery); + queue.addRequestEventListener(listener); + + queue.sendRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + + verify(listener) + .onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + verifyNoMoreInteractions(listener); + } + + @Test + public void removeRequestEventListener_removesListener() throws Exception { + StringRequest req = mock(StringRequest.class); + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery); + queue.addRequestEventListener(listener); + queue.removeRequestEventListener(listener); + + queue.sendRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + + verifyNoMoreInteractions(listener); + } +} diff --git a/core/src/test/java/com/android/volley/RequestTest.java b/core/src/test/java/com/android/volley/RequestTest.java new file mode 100644 index 0000000..cced39f --- /dev/null +++ b/core/src/test/java/com/android/volley/RequestTest.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.android.volley; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.volley.Request.Method; +import com.android.volley.Request.Priority; +import com.android.volley.toolbox.NoCache; +import java.util.Collections; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class RequestTest { + private @Mock ResponseDelivery mDelivery; + private @Mock Network mNetwork; + + @Before + public void setUp() throws Exception { + initMocks(this); + } + + @Test + public void compareTo() { + int sequence = 0; + TestRequest low = new TestRequest(Priority.LOW); + low.setSequence(sequence++); + TestRequest low2 = new TestRequest(Priority.LOW); + low2.setSequence(sequence++); + TestRequest high = new TestRequest(Priority.HIGH); + high.setSequence(sequence++); + TestRequest immediate = new TestRequest(Priority.IMMEDIATE); + immediate.setSequence(sequence++); + + // "Low" should sort higher because it's really processing order. + assertTrue(low.compareTo(high) > 0); + assertTrue(high.compareTo(low) < 0); + assertTrue(low.compareTo(low2) < 0); + assertTrue(low.compareTo(immediate) > 0); + assertTrue(immediate.compareTo(high) < 0); + } + + private static class TestRequest extends Request { + private Priority mPriority = Priority.NORMAL; + + public TestRequest(Priority priority) { + super(Request.Method.GET, "", null); + mPriority = priority; + } + + @Override + public Priority getPriority() { + return mPriority; + } + + @Override + protected void deliverResponse(Object response) {} + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + } + + @Test + public void urlParsing() { + UrlParseRequest nullUrl = new UrlParseRequest(null); + assertEquals(0, nullUrl.getTrafficStatsTag()); + UrlParseRequest emptyUrl = new UrlParseRequest(""); + assertEquals(0, emptyUrl.getTrafficStatsTag()); + UrlParseRequest noHost = new UrlParseRequest("http:///"); + assertEquals(0, noHost.getTrafficStatsTag()); + UrlParseRequest badProtocol = new UrlParseRequest("bad:http://foo"); + assertEquals(0, badProtocol.getTrafficStatsTag()); + UrlParseRequest goodProtocol = new UrlParseRequest("http://foo"); + assertFalse(0 == goodProtocol.getTrafficStatsTag()); + } + + @Test + public void getCacheKey() { + assertEquals( + "http://example.com", + new UrlParseRequest(Method.GET, "http://example.com").getCacheKey()); + assertEquals( + "http://example.com", + new UrlParseRequest(Method.DEPRECATED_GET_OR_POST, "http://example.com") + .getCacheKey()); + assertEquals( + "1-http://example.com", + new UrlParseRequest(Method.POST, "http://example.com").getCacheKey()); + assertEquals( + "2-http://example.com", + new UrlParseRequest(Method.PUT, "http://example.com").getCacheKey()); + } + + private static class UrlParseRequest extends Request { + UrlParseRequest(String url) { + this(Method.GET, url); + } + + UrlParseRequest(int method, String url) { + super(method, url, null); + } + + @Override + protected void deliverResponse(Object response) {} + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + } + + @Test + public void nullKeyInPostParams() throws Exception { + Request request = + new Request(Method.POST, "url", null) { + @Override + protected void deliverResponse(Object response) {} + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + + @Override + protected Map getParams() { + return Collections.singletonMap(null, "value"); + } + + @Override + protected Map getPostParams() { + return Collections.singletonMap(null, "value"); + } + }; + try { + request.getBody(); + } catch (IllegalArgumentException e) { + // expected + } + try { + request.getPostBody(); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void nullValueInPostParams() throws Exception { + Request request = + new Request(Method.POST, "url", null) { + @Override + protected void deliverResponse(Object response) {} + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + + @Override + protected Map getParams() { + return Collections.singletonMap("key", null); + } + + @Override + protected Map getPostParams() { + return Collections.singletonMap("key", null); + } + }; + try { + request.getBody(); + } catch (IllegalArgumentException e) { + // expected + } + try { + request.getPostBody(); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void sendEvent_notifiesListeners() throws Exception { + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + RequestQueue queue = new RequestQueue(new NoCache(), mNetwork, 0, mDelivery); + queue.addRequestEventListener(listener); + + Request request = + new Request(Method.POST, "url", null) { + @Override + protected void deliverResponse(Object response) {} + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + }; + request.setRequestQueue(queue); + + request.sendEvent(RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + + verify(listener) + .onRequestEvent( + request, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + verifyNoMoreInteractions(listener); + } +} diff --git a/core/src/test/java/com/android/volley/ResponseDeliveryTest.java b/core/src/test/java/com/android/volley/ResponseDeliveryTest.java new file mode 100644 index 0000000..6e71c3b --- /dev/null +++ b/core/src/test/java/com/android/volley/ResponseDeliveryTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.android.volley; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.android.volley.mock.MockRequest; +import com.android.volley.utils.CacheTestUtils; +import com.android.volley.utils.ImmediateResponseDelivery; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ResponseDeliveryTest { + + private ExecutorDelivery mDelivery; + private MockRequest mRequest; + private Response mSuccessResponse; + + @Before + public void setUp() throws Exception { + // Make the delivery just run its posted responses immediately. + mDelivery = new ImmediateResponseDelivery(); + mRequest = new MockRequest(); + mRequest.setSequence(1); + byte[] data = new byte[16]; + Cache.Entry cacheEntry = CacheTestUtils.makeRandomCacheEntry(data); + mSuccessResponse = Response.success(data, cacheEntry); + } + + @Test + public void postResponseCallsDeliverResponse() { + mDelivery.postResponse(mRequest, mSuccessResponse); + assertTrue(mRequest.deliverResponse_called); + assertFalse(mRequest.deliverError_called); + } + + @Test + public void postResponseSuppressesCanceled() { + mRequest.cancel(); + mDelivery.postResponse(mRequest, mSuccessResponse); + assertFalse(mRequest.deliverResponse_called); + assertFalse(mRequest.deliverError_called); + } + + @Test + public void postErrorCallsDeliverError() { + Response errorResponse = Response.error(new ServerError()); + + mDelivery.postResponse(mRequest, errorResponse); + assertTrue(mRequest.deliverError_called); + assertFalse(mRequest.deliverResponse_called); + } +} diff --git a/core/src/test/java/com/android/volley/mock/MockAsyncStack.java b/core/src/test/java/com/android/volley/mock/MockAsyncStack.java new file mode 100644 index 0000000..5ea8343 --- /dev/null +++ b/core/src/test/java/com/android/volley/mock/MockAsyncStack.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * 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 com.android.volley.mock; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.toolbox.AsyncHttpStack; +import com.android.volley.toolbox.HttpResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class MockAsyncStack extends AsyncHttpStack { + + private HttpResponse mResponseToReturn; + + private IOException mExceptionToThrow; + + private String mLastUrl; + + private Map mLastHeaders; + + private byte[] mLastPostBody; + + public String getLastUrl() { + return mLastUrl; + } + + public Map getLastHeaders() { + return mLastHeaders; + } + + public byte[] getLastPostBody() { + return mLastPostBody; + } + + public void setResponseToReturn(HttpResponse response) { + mResponseToReturn = response; + } + + public void setExceptionToThrow(IOException exception) { + mExceptionToThrow = exception; + } + + @Override + public void executeRequest( + Request request, Map additionalHeaders, OnRequestComplete callback) { + if (mExceptionToThrow != null) { + callback.onError(mExceptionToThrow); + return; + } + mLastUrl = request.getUrl(); + mLastHeaders = new HashMap<>(); + try { + if (request.getHeaders() != null) { + mLastHeaders.putAll(request.getHeaders()); + } + } catch (AuthFailureError authFailureError) { + callback.onAuthError(authFailureError); + return; + } + if (additionalHeaders != null) { + mLastHeaders.putAll(additionalHeaders); + } + try { + mLastPostBody = request.getBody(); + } catch (AuthFailureError e) { + mLastPostBody = null; + } + callback.onSuccess(mResponseToReturn); + } +} diff --git a/core/src/test/java/com/android/volley/mock/MockHttpStack.java b/core/src/test/java/com/android/volley/mock/MockHttpStack.java new file mode 100644 index 0000000..b86e7a0 --- /dev/null +++ b/core/src/test/java/com/android/volley/mock/MockHttpStack.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.android.volley.mock; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.toolbox.BaseHttpStack; +import com.android.volley.toolbox.HttpResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class MockHttpStack extends BaseHttpStack { + + private HttpResponse mResponseToReturn; + + private IOException mExceptionToThrow; + + private String mLastUrl; + + private Map mLastHeaders; + + private byte[] mLastPostBody; + + public String getLastUrl() { + return mLastUrl; + } + + public Map getLastHeaders() { + return mLastHeaders; + } + + public byte[] getLastPostBody() { + return mLastPostBody; + } + + public void setResponseToReturn(HttpResponse response) { + mResponseToReturn = response; + } + + public void setExceptionToThrow(IOException exception) { + mExceptionToThrow = exception; + } + + @Override + public HttpResponse executeRequest(Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + if (mExceptionToThrow != null) { + throw mExceptionToThrow; + } + mLastUrl = request.getUrl(); + mLastHeaders = new HashMap<>(); + if (request.getHeaders() != null) { + mLastHeaders.putAll(request.getHeaders()); + } + if (additionalHeaders != null) { + mLastHeaders.putAll(additionalHeaders); + } + try { + mLastPostBody = request.getBody(); + } catch (AuthFailureError e) { + mLastPostBody = null; + } + return mResponseToReturn; + } +} diff --git a/core/src/test/java/com/android/volley/mock/MockRequest.java b/core/src/test/java/com/android/volley/mock/MockRequest.java new file mode 100644 index 0000000..6fc26b4 --- /dev/null +++ b/core/src/test/java/com/android/volley/mock/MockRequest.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.android.volley.mock; + +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.Response.ErrorListener; +import com.android.volley.VolleyError; +import com.android.volley.utils.CacheTestUtils; +import java.util.HashMap; +import java.util.Map; + +public class MockRequest extends Request { + public MockRequest() { + super(Request.Method.GET, "http://foo.com", null); + } + + public MockRequest(String url, ErrorListener listener) { + super(Request.Method.GET, url, listener); + } + + private Map mPostParams = new HashMap(); + + public void setPostParams(Map postParams) { + mPostParams = postParams; + } + + @Override + public Map getPostParams() { + return mPostParams; + } + + private String mCacheKey = super.getCacheKey(); + + public void setCacheKey(String cacheKey) { + mCacheKey = cacheKey; + } + + @Override + public String getCacheKey() { + return mCacheKey; + } + + public boolean deliverResponse_called = false; + public boolean parseResponse_called = false; + + @Override + protected void deliverResponse(byte[] response) { + deliverResponse_called = true; + } + + public boolean deliverError_called = false; + + @Override + public void deliverError(VolleyError error) { + super.deliverError(error); + deliverError_called = true; + } + + public boolean cancel_called = false; + + @Override + public void cancel() { + cancel_called = true; + super.cancel(); + } + + private Priority mPriority = super.getPriority(); + + public void setPriority(Priority priority) { + mPriority = priority; + } + + @Override + public Priority getPriority() { + return mPriority; + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + parseResponse_called = true; + return Response.success(response.data, CacheTestUtils.makeRandomCacheEntry(response.data)); + } +} diff --git a/core/src/test/java/com/android/volley/mock/ShadowSystemClock.java b/core/src/test/java/com/android/volley/mock/ShadowSystemClock.java new file mode 100644 index 0000000..6d75d4b --- /dev/null +++ b/core/src/test/java/com/android/volley/mock/ShadowSystemClock.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.android.volley.mock; + +import android.os.SystemClock; +import org.robolectric.annotation.Implements; + +@Implements(value = SystemClock.class, callThroughByDefault = true) +public class ShadowSystemClock { + public static long elapsedRealtime() { + return 0; + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java b/core/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java new file mode 100644 index 0000000..dbd6535 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java @@ -0,0 +1,128 @@ +package com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.when; + +import com.android.volley.Header; +import com.android.volley.Request; +import com.android.volley.mock.TestRequest; +import java.io.IOException; +import java.io.InputStream; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.message.BasicHeader; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class AdaptedHttpStackTest { + private static final Request REQUEST = new TestRequest.Get(); + private static final Map ADDITIONAL_HEADERS = Collections.emptyMap(); + + @Mock private HttpStack mHttpStack; + @Mock private HttpResponse mHttpResponse; + @Mock private StatusLine mStatusLine; + @Mock private HttpEntity mHttpEntity; + @Mock private InputStream mContent; + + private AdaptedHttpStack mAdaptedHttpStack; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mAdaptedHttpStack = new AdaptedHttpStack(mHttpStack); + when(mHttpResponse.getStatusLine()).thenReturn(mStatusLine); + } + + @Test(expected = SocketTimeoutException.class) + public void requestTimeout() throws Exception { + when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)) + .thenThrow(new ConnectTimeoutException()); + + mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); + } + + @Test + public void emptyResponse() throws Exception { + when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse); + when(mStatusLine.getStatusCode()).thenReturn(12345); + when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]); + + com.android.volley.toolbox.HttpResponse response = + mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); + + assertEquals(12345, response.getStatusCode()); + assertEquals(Collections.emptyList(), response.getHeaders()); + assertNull(response.getContent()); + } + + @Test + public void nonEmptyResponse() throws Exception { + when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse); + when(mStatusLine.getStatusCode()).thenReturn(12345); + when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]); + when(mHttpResponse.getEntity()).thenReturn(mHttpEntity); + when(mHttpEntity.getContentLength()).thenReturn((long) Integer.MAX_VALUE); + when(mHttpEntity.getContent()).thenReturn(mContent); + + com.android.volley.toolbox.HttpResponse response = + mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); + + assertEquals(12345, response.getStatusCode()); + assertEquals(Collections.emptyList(), response.getHeaders()); + assertEquals(Integer.MAX_VALUE, response.getContentLength()); + assertSame(mContent, response.getContent()); + } + + @Test(expected = IOException.class) + public void responseTooBig() throws Exception { + when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse); + when(mStatusLine.getStatusCode()).thenReturn(12345); + when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]); + when(mHttpResponse.getEntity()).thenReturn(mHttpEntity); + when(mHttpEntity.getContentLength()).thenReturn(Integer.MAX_VALUE + 1L); + when(mHttpEntity.getContent()).thenReturn(mContent); + + mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); + } + + @Test + public void responseWithHeaders() throws Exception { + when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse); + when(mStatusLine.getStatusCode()).thenReturn(12345); + when(mHttpResponse.getAllHeaders()) + .thenReturn( + new org.apache.http.Header[] { + new BasicHeader("header1", "value1_B"), + new BasicHeader("header3", "value3"), + new BasicHeader("HEADER2", "value2"), + new BasicHeader("header1", "value1_A") + }); + + com.android.volley.toolbox.HttpResponse response = + mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); + + assertEquals(12345, response.getStatusCode()); + assertNull(response.getContent()); + + List
expectedHeaders = new ArrayList<>(); + expectedHeaders.add(new Header("header1", "value1_B")); + expectedHeaders.add(new Header("header3", "value3")); + expectedHeaders.add(new Header("HEADER2", "value2")); + expectedHeaders.add(new Header("header1", "value1_A")); + assertEquals(expectedHeaders, response.getHeaders()); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java b/core/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java new file mode 100644 index 0000000..982eda2 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import com.android.volley.AuthFailureError; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class AndroidAuthenticatorTest { + @Mock private AccountManager mAccountManager; + @Mock private AccountManagerFuture mFuture; + private Account mAccount; + private AndroidAuthenticator mAuthenticator; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mAccount = new Account("coolperson", "cooltype"); + mAuthenticator = new AndroidAuthenticator(mAccountManager, mAccount, "cooltype", false); + } + + @Test(expected = AuthFailureError.class) + public void failedGetAuthToken() throws Exception { + when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)) + .thenReturn(mFuture); + when(mFuture.getResult()).thenThrow(new AuthenticatorException("sadness!")); + mAuthenticator.getAuthToken(); + } + + @Test(expected = AuthFailureError.class) + public void resultContainsIntent() throws Exception { + Intent intent = new Intent(); + Bundle bundle = new Bundle(); + bundle.putParcelable(AccountManager.KEY_INTENT, intent); + when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)) + .thenReturn(mFuture); + when(mFuture.getResult()).thenReturn(bundle); + when(mFuture.isDone()).thenReturn(true); + when(mFuture.isCancelled()).thenReturn(false); + mAuthenticator.getAuthToken(); + } + + @Test(expected = AuthFailureError.class) + public void missingAuthToken() throws Exception { + Bundle bundle = new Bundle(); + when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)) + .thenReturn(mFuture); + when(mFuture.getResult()).thenReturn(bundle); + when(mFuture.isDone()).thenReturn(true); + when(mFuture.isCancelled()).thenReturn(false); + mAuthenticator.getAuthToken(); + } + + @Test + public void invalidateAuthToken() throws Exception { + mAuthenticator.invalidateAuthToken("monkey"); + verify(mAccountManager).invalidateAuthToken("cooltype", "monkey"); + } + + @Test + public void goodToken() throws Exception { + Bundle bundle = new Bundle(); + bundle.putString(AccountManager.KEY_AUTHTOKEN, "monkey"); + when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)) + .thenReturn(mFuture); + when(mFuture.getResult()).thenReturn(bundle); + when(mFuture.isDone()).thenReturn(true); + when(mFuture.isCancelled()).thenReturn(false); + Assert.assertEquals("monkey", mAuthenticator.getAuthToken()); + } + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + Context context = mock(Context.class); + new AndroidAuthenticator(context, mAccount, "cooltype"); + new AndroidAuthenticator(context, mAccount, "cooltype", true); + Assert.assertSame(mAccount, mAuthenticator.getAccount()); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java b/core/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java new file mode 100644 index 0000000..1049ad0 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java @@ -0,0 +1,104 @@ +package com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import com.android.volley.AuthFailureError; +import com.android.volley.Header; +import com.android.volley.Request; +import com.android.volley.mock.TestRequest; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class BaseHttpStackTest { + private static final Request REQUEST = new TestRequest.Get(); + private static final Map ADDITIONAL_HEADERS = Collections.emptyMap(); + + @Mock private InputStream mContent; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void legacyRequestWithoutBody() throws Exception { + BaseHttpStack stack = + new BaseHttpStack() { + @Override + public HttpResponse executeRequest( + Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + assertSame(REQUEST, request); + assertSame(ADDITIONAL_HEADERS, additionalHeaders); + return new HttpResponse(12345, Collections.
emptyList()); + } + }; + org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS); + assertEquals(12345, resp.getStatusLine().getStatusCode()); + assertEquals(0, resp.getAllHeaders().length); + assertNull(resp.getEntity()); + } + + @Test + public void legacyResponseWithBody() throws Exception { + BaseHttpStack stack = + new BaseHttpStack() { + @Override + public HttpResponse executeRequest( + Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + assertSame(REQUEST, request); + assertSame(ADDITIONAL_HEADERS, additionalHeaders); + return new HttpResponse( + 12345, Collections.
emptyList(), 555, mContent); + } + }; + org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS); + assertEquals(12345, resp.getStatusLine().getStatusCode()); + assertEquals(0, resp.getAllHeaders().length); + assertEquals(555L, resp.getEntity().getContentLength()); + assertSame(mContent, resp.getEntity().getContent()); + } + + @Test + public void legacyResponseHeaders() throws Exception { + BaseHttpStack stack = + new BaseHttpStack() { + @Override + public HttpResponse executeRequest( + Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + assertSame(REQUEST, request); + assertSame(ADDITIONAL_HEADERS, additionalHeaders); + List
headers = new ArrayList<>(); + headers.add(new Header("HeaderA", "ValueA")); + headers.add(new Header("HeaderB", "ValueB_1")); + headers.add(new Header("HeaderB", "ValueB_2")); + return new HttpResponse(12345, headers); + } + }; + org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS); + assertEquals(12345, resp.getStatusLine().getStatusCode()); + assertEquals(3, resp.getAllHeaders().length); + assertEquals("HeaderA", resp.getAllHeaders()[0].getName()); + assertEquals("ValueA", resp.getAllHeaders()[0].getValue()); + assertEquals("HeaderB", resp.getAllHeaders()[1].getName()); + assertEquals("ValueB_1", resp.getAllHeaders()[1].getValue()); + assertEquals("HeaderB", resp.getAllHeaders()[2].getName()); + assertEquals("ValueB_2", resp.getAllHeaders()[2].getValue()); + assertNull(resp.getEntity()); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java b/core/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java new file mode 100644 index 0000000..91d4062 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java @@ -0,0 +1,508 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.volley.AsyncNetwork; +import com.android.volley.AuthFailureError; +import com.android.volley.Cache.Entry; +import com.android.volley.Header; +import com.android.volley.NetworkResponse; +import com.android.volley.NoConnectionError; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.RetryPolicy; +import com.android.volley.ServerError; +import com.android.volley.TimeoutError; +import com.android.volley.VolleyError; +import com.android.volley.mock.MockAsyncStack; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class BasicAsyncNetworkTest { + + @Mock private RetryPolicy mMockRetryPolicy; + @Mock private AsyncNetwork.OnRequestComplete mockCallback; + private ExecutorService executor = MoreExecutors.newDirectExecutorService(); + + @Before + public void setUp() throws Exception { + initMocks(this); + } + + @Test + public void headersAndPostParams() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = + new HttpResponse( + 200, + Collections.
emptyList(), + "foobar".getBytes(StandardCharsets.UTF_8)); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + Entry entry = new Entry(); + entry.etag = "foobar"; + entry.lastModified = 1503102002000L; + request.setCacheEntry(entry); + perform(request, httpNetwork).get(); + assertEquals("foo", mockAsyncStack.getLastHeaders().get("requestheader")); + assertEquals("foobar", mockAsyncStack.getLastHeaders().get("If-None-Match")); + assertEquals( + "Sat, 19 Aug 2017 00:20:02 GMT", + mockAsyncStack.getLastHeaders().get("If-Modified-Since")); + assertEquals( + "requestpost=foo&", + new String(mockAsyncStack.getLastPostBody(), StandardCharsets.UTF_8)); + } + + @Test + public void headersAndPostParamsStream() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + ByteArrayInputStream stream = new ByteArrayInputStream("foobar".getBytes("UTF-8")); + HttpResponse fakeResponse = + new HttpResponse(200, Collections.
emptyList(), 6, stream); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + Entry entry = new Entry(); + entry.etag = "foobar"; + entry.lastModified = 1503102002000L; + request.setCacheEntry(entry); + perform(request, httpNetwork).get(); + assertEquals("foo", mockAsyncStack.getLastHeaders().get("requestheader")); + assertEquals("foobar", mockAsyncStack.getLastHeaders().get("If-None-Match")); + assertEquals( + "Sat, 19 Aug 2017 00:20:02 GMT", + mockAsyncStack.getLastHeaders().get("If-Modified-Since")); + assertEquals( + "requestpost=foo&", + new String(mockAsyncStack.getLastPostBody(), StandardCharsets.UTF_8)); + } + + @Test + public void notModified() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + List
headers = new ArrayList<>(); + headers.add(new Header("ServerKeyA", "ServerValueA")); + headers.add(new Header("ServerKeyB", "ServerValueB")); + headers.add(new Header("SharedKey", "ServerValueShared")); + headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + Entry entry = new Entry(); + entry.allResponseHeaders = new ArrayList<>(); + entry.allResponseHeaders.add(new Header("CachedKeyA", "CachedValueA")); + entry.allResponseHeaders.add(new Header("CachedKeyB", "CachedValueB")); + entry.allResponseHeaders.add(new Header("SharedKey", "CachedValueShared")); + entry.allResponseHeaders.add(new Header("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1")); + entry.allResponseHeaders.add(new Header("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2")); + request.setCacheEntry(entry); + httpNetwork.performRequest(request, mockCallback); + NetworkResponse response = perform(request, httpNetwork).get(); + List
expectedHeaders = new ArrayList<>(); + // Should have all server headers + cache headers that didn't show up in server response. + expectedHeaders.add(new Header("ServerKeyA", "ServerValueA")); + expectedHeaders.add(new Header("ServerKeyB", "ServerValueB")); + expectedHeaders.add(new Header("SharedKey", "ServerValueShared")); + expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + expectedHeaders.add(new Header("CachedKeyA", "CachedValueA")); + expectedHeaders.add(new Header("CachedKeyB", "CachedValueB")); + assertThat(expectedHeaders, containsInAnyOrder(response.allHeaders.toArray(new Header[0]))); + } + + @Test + public void notModified_legacyCache() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + List
headers = new ArrayList<>(); + headers.add(new Header("ServerKeyA", "ServerValueA")); + headers.add(new Header("ServerKeyB", "ServerValueB")); + headers.add(new Header("SharedKey", "ServerValueShared")); + headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + Entry entry = new Entry(); + entry.responseHeaders = new HashMap<>(); + entry.responseHeaders.put("CachedKeyA", "CachedValueA"); + entry.responseHeaders.put("CachedKeyB", "CachedValueB"); + entry.responseHeaders.put("SharedKey", "CachedValueShared"); + entry.responseHeaders.put("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1"); + entry.responseHeaders.put("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2"); + request.setCacheEntry(entry); + NetworkResponse response = perform(request, httpNetwork).get(); + List
expectedHeaders = new ArrayList<>(); + // Should have all server headers + cache headers that didn't show up in server response. + expectedHeaders.add(new Header("ServerKeyA", "ServerValueA")); + expectedHeaders.add(new Header("ServerKeyB", "ServerValueB")); + expectedHeaders.add(new Header("SharedKey", "ServerValueShared")); + expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + expectedHeaders.add(new Header("CachedKeyA", "CachedValueA")); + expectedHeaders.add(new Header("CachedKeyB", "CachedValueB")); + assertThat(expectedHeaders, containsInAnyOrder(response.allHeaders.toArray(new Header[0]))); + } + + @Test + public void socketTimeout() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + mockAsyncStack.setExceptionToThrow(new SocketTimeoutException()); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should retry socket timeouts + verify(mMockRetryPolicy).retry(any(TimeoutError.class)); + reset(mMockRetryPolicy, mockCallback); + } + + @Test + public void noConnectionDefault() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + mockAsyncStack.setExceptionToThrow(new IOException()); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should not retry when there is no connection + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy, mockCallback); + } + + @Test + public void noConnectionRetry() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + mockAsyncStack.setExceptionToThrow(new IOException()); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + request.setShouldRetryConnectionErrors(true); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should retry when there is no connection + verify(mMockRetryPolicy).retry(any(NoConnectionError.class)); + reset(mMockRetryPolicy, mockCallback); + } + + @Test + public void noConnectionNoRetry() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + mockAsyncStack.setExceptionToThrow(new IOException()); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + request.setShouldRetryConnectionErrors(false); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should not retry when there is no connection + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy, mockCallback); + } + + @Test + public void unauthorized() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = new HttpResponse(401, Collections.
emptyList()); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should retry in case it's an auth failure. + verify(mMockRetryPolicy).retry(any(AuthFailureError.class)); + reset(mMockRetryPolicy, mockCallback); + } + + @Test(expected = RuntimeException.class) + public void malformedUrlRequest() throws VolleyError, ExecutionException, InterruptedException { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + mockAsyncStack.setExceptionToThrow(new MalformedURLException()); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + perform(request, httpNetwork).get(); + } + + @Test + public void forbidden() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = new HttpResponse(403, Collections.
emptyList()); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should retry in case it's an auth failure. + verify(mMockRetryPolicy).retry(any(AuthFailureError.class)); + reset(mMockRetryPolicy, mockCallback); + } + + @Test + public void redirect() throws Exception { + for (int i = 300; i <= 399; i++) { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + if (i != 304) { + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + } else { + verify(mockCallback, never()).onError(any(VolleyError.class)); + verify(mockCallback, times(1)).onSuccess(any(NetworkResponse.class)); + } + // should not retry 300 responses. + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy, mockCallback); + } + } + + @Test + public void otherClientError() throws Exception { + for (int i = 400; i <= 499; i++) { + if (i == 401 || i == 403) { + // covered above. + continue; + } + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should not retry other 400 errors. + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy, mockCallback); + } + } + + @Test + public void serverError_enableRetries() throws Exception { + for (int i = 500; i <= 599; i++) { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = + new BasicAsyncNetwork.Builder(mockAsyncStack) + .setPool(new ByteArrayPool(4096)) + .build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + request.setShouldRetryServerErrors(true); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should retry all 500 errors + verify(mMockRetryPolicy).retry(any(ServerError.class)); + reset(mMockRetryPolicy, mockCallback); + } + } + + @Test + public void serverError_disableRetries() throws Exception { + for (int i = 500; i <= 599; i++) { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should not retry any 500 error w/ HTTP 500 retries turned off (the default). + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy, mockCallback); + } + } + + @Test + public void notModifiedShortCircuit() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + List
headers = new ArrayList<>(); + headers.add(new Header("ServerKeyA", "ServerValueA")); + headers.add(new Header("ServerKeyB", "ServerValueB")); + headers.add(new Header("SharedKey", "ServerValueShared")); + headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onSuccess(any(NetworkResponse.class)); + verify(mockCallback, never()).onError(any(VolleyError.class)); + reset(mMockRetryPolicy, mockCallback); + } + + @Test + public void performRequestSuccess() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = + new HttpResponse( + 200, + Collections.
emptyList(), + "foobar".getBytes(StandardCharsets.UTF_8)); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + Entry entry = new Entry(); + entry.etag = "foobar"; + entry.lastModified = 1503102002000L; + request.setCacheEntry(entry); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onSuccess(any(NetworkResponse.class)); + verify(mockCallback, never()).onError(any(VolleyError.class)); + reset(mMockRetryPolicy, mockCallback); + } + + @Test(expected = IllegalStateException.class) + public void performRequestNeverSetExecutorTest() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = new HttpResponse(200, Collections.
emptyList()); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + Request request = buildRequest(); + perform(request, httpNetwork).get(); + } + + /** Helper functions */ + private CompletableFuture perform(Request request, AsyncNetwork network) + throws VolleyError { + final CompletableFuture future = new CompletableFuture<>(); + network.performRequest( + request, + new AsyncNetwork.OnRequestComplete() { + @Override + public void onSuccess(NetworkResponse networkResponse) { + future.complete(networkResponse); + } + + @Override + public void onError(VolleyError volleyError) { + future.complete(null); + } + }); + return future; + } + + private static Request buildRequest() { + return new Request(Request.Method.GET, "http://foo", null) { + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + + @Override + protected void deliverResponse(String response) {} + + @Override + public Map getHeaders() { + Map result = new HashMap(); + result.put("requestheader", "foo"); + return result; + } + + @Override + public Map getParams() { + Map result = new HashMap(); + result.put("requestpost", "foo"); + return result; + } + }; + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java b/core/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java new file mode 100644 index 0000000..3630379 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.volley.AuthFailureError; +import com.android.volley.Cache.Entry; +import com.android.volley.Header; +import com.android.volley.NetworkResponse; +import com.android.volley.NoConnectionError; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.RetryPolicy; +import com.android.volley.ServerError; +import com.android.volley.TimeoutError; +import com.android.volley.VolleyError; +import com.android.volley.mock.MockHttpStack; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class BasicNetworkTest { + + @Mock private Request mMockRequest; + @Mock private RetryPolicy mMockRetryPolicy; + + @Before + public void setUp() throws Exception { + initMocks(this); + } + + @Test + public void headersAndPostParams() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + InputStream responseStream = + new ByteArrayInputStream("foobar".getBytes(StandardCharsets.UTF_8)); + HttpResponse fakeResponse = + new HttpResponse(200, Collections.
emptyList(), 6, responseStream); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + Entry entry = new Entry(); + entry.etag = "foobar"; + entry.lastModified = 1503102002000L; + request.setCacheEntry(entry); + httpNetwork.performRequest(request); + assertEquals("foo", mockHttpStack.getLastHeaders().get("requestheader")); + assertEquals("foobar", mockHttpStack.getLastHeaders().get("If-None-Match")); + assertEquals( + "Sat, 19 Aug 2017 00:20:02 GMT", + mockHttpStack.getLastHeaders().get("If-Modified-Since")); + assertEquals( + "requestpost=foo&", + new String(mockHttpStack.getLastPostBody(), StandardCharsets.UTF_8)); + } + + @Test + public void notModified() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + List
headers = new ArrayList<>(); + headers.add(new Header("ServerKeyA", "ServerValueA")); + headers.add(new Header("ServerKeyB", "ServerValueB")); + headers.add(new Header("SharedKey", "ServerValueShared")); + headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + Entry entry = new Entry(); + entry.allResponseHeaders = new ArrayList<>(); + entry.allResponseHeaders.add(new Header("CachedKeyA", "CachedValueA")); + entry.allResponseHeaders.add(new Header("CachedKeyB", "CachedValueB")); + entry.allResponseHeaders.add(new Header("SharedKey", "CachedValueShared")); + entry.allResponseHeaders.add(new Header("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1")); + entry.allResponseHeaders.add(new Header("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2")); + request.setCacheEntry(entry); + NetworkResponse response = httpNetwork.performRequest(request); + List
expectedHeaders = new ArrayList<>(); + // Should have all server headers + cache headers that didn't show up in server response. + expectedHeaders.add(new Header("ServerKeyA", "ServerValueA")); + expectedHeaders.add(new Header("ServerKeyB", "ServerValueB")); + expectedHeaders.add(new Header("SharedKey", "ServerValueShared")); + expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + expectedHeaders.add(new Header("CachedKeyA", "CachedValueA")); + expectedHeaders.add(new Header("CachedKeyB", "CachedValueB")); + assertThat(expectedHeaders, containsInAnyOrder(response.allHeaders.toArray(new Header[0]))); + } + + @Test + public void notModified_legacyCache() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + List
headers = new ArrayList<>(); + headers.add(new Header("ServerKeyA", "ServerValueA")); + headers.add(new Header("ServerKeyB", "ServerValueB")); + headers.add(new Header("SharedKey", "ServerValueShared")); + headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + Entry entry = new Entry(); + entry.responseHeaders = new HashMap<>(); + entry.responseHeaders.put("CachedKeyA", "CachedValueA"); + entry.responseHeaders.put("CachedKeyB", "CachedValueB"); + entry.responseHeaders.put("SharedKey", "CachedValueShared"); + entry.responseHeaders.put("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1"); + entry.responseHeaders.put("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2"); + request.setCacheEntry(entry); + NetworkResponse response = httpNetwork.performRequest(request); + List
expectedHeaders = new ArrayList<>(); + // Should have all server headers + cache headers that didn't show up in server response. + expectedHeaders.add(new Header("ServerKeyA", "ServerValueA")); + expectedHeaders.add(new Header("ServerKeyB", "ServerValueB")); + expectedHeaders.add(new Header("SharedKey", "ServerValueShared")); + expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + expectedHeaders.add(new Header("CachedKeyA", "CachedValueA")); + expectedHeaders.add(new Header("CachedKeyB", "CachedValueB")); + assertThat(expectedHeaders, containsInAnyOrder(response.allHeaders.toArray(new Header[0]))); + } + + @Test + public void socketTimeout() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + mockHttpStack.setExceptionToThrow(new SocketTimeoutException()); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should retry socket timeouts + verify(mMockRetryPolicy).retry(any(TimeoutError.class)); + } + + @Test + public void noConnectionDefault() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + mockHttpStack.setExceptionToThrow(new IOException()); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should not retry when there is no connection + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + } + + @Test + public void noConnectionRetry() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + mockHttpStack.setExceptionToThrow(new IOException()); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + request.setShouldRetryConnectionErrors(true); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should retry when there is no connection + verify(mMockRetryPolicy).retry(any(NoConnectionError.class)); + reset(mMockRetryPolicy); + } + + @Test + public void noConnectionNoRetry() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + mockHttpStack.setExceptionToThrow(new IOException()); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + request.setShouldRetryConnectionErrors(false); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should not retry when there is no connection + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + } + + @Test + public void unauthorized() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + HttpResponse fakeResponse = new HttpResponse(401, Collections.
emptyList()); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should retry in case it's an auth failure. + verify(mMockRetryPolicy).retry(any(AuthFailureError.class)); + } + + @Test + public void forbidden() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + HttpResponse fakeResponse = new HttpResponse(403, Collections.
emptyList()); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should retry in case it's an auth failure. + verify(mMockRetryPolicy).retry(any(AuthFailureError.class)); + } + + @Test + public void redirect() throws Exception { + for (int i = 300; i <= 399; i++) { + MockHttpStack mockHttpStack = new MockHttpStack(); + HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should not retry 300 responses. + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy); + } + } + + @Test + public void otherClientError() throws Exception { + for (int i = 400; i <= 499; i++) { + if (i == 401 || i == 403) { + // covered above. + continue; + } + MockHttpStack mockHttpStack = new MockHttpStack(); + HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should not retry other 400 errors. + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy); + } + } + + @Test + public void serverError_enableRetries() throws Exception { + for (int i = 500; i <= 599; i++) { + MockHttpStack mockHttpStack = new MockHttpStack(); + HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack, new ByteArrayPool(4096)); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + request.setShouldRetryServerErrors(true); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should retry all 500 errors + verify(mMockRetryPolicy).retry(any(ServerError.class)); + reset(mMockRetryPolicy); + } + } + + @Test + public void serverError_disableRetries() throws Exception { + for (int i = 500; i <= 599; i++) { + MockHttpStack mockHttpStack = new MockHttpStack(); + HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should not retry any 500 error w/ HTTP 500 retries turned off (the default). + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy); + } + } + + private static Request buildRequest() { + return new Request(Request.Method.GET, "http://foo", null) { + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + + @Override + protected void deliverResponse(String response) {} + + @Override + public Map getHeaders() { + Map result = new HashMap(); + result.put("requestheader", "foo"); + return result; + } + + @Override + public Map getParams() { + Map result = new HashMap(); + result.put("requestpost", "foo"); + return result; + } + }; + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java b/core/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java new file mode 100644 index 0000000..62da207 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class ByteArrayPoolTest { + @Test + public void reusesBuffer() { + ByteArrayPool pool = new ByteArrayPool(32); + + byte[] buf1 = pool.getBuf(16); + byte[] buf2 = pool.getBuf(16); + + pool.returnBuf(buf1); + pool.returnBuf(buf2); + + byte[] buf3 = pool.getBuf(16); + byte[] buf4 = pool.getBuf(16); + assertTrue(buf3 == buf1 || buf3 == buf2); + assertTrue(buf4 == buf1 || buf4 == buf2); + assertTrue(buf3 != buf4); + } + + @Test + public void obeysSizeLimit() { + ByteArrayPool pool = new ByteArrayPool(32); + + byte[] buf1 = pool.getBuf(16); + byte[] buf2 = pool.getBuf(16); + byte[] buf3 = pool.getBuf(16); + + pool.returnBuf(buf1); + pool.returnBuf(buf2); + pool.returnBuf(buf3); + + byte[] buf4 = pool.getBuf(16); + byte[] buf5 = pool.getBuf(16); + byte[] buf6 = pool.getBuf(16); + + assertTrue(buf4 == buf2 || buf4 == buf3); + assertTrue(buf5 == buf2 || buf5 == buf3); + assertTrue(buf4 != buf5); + assertTrue(buf6 != buf1 && buf6 != buf2 && buf6 != buf3); + } + + @Test + public void returnsBufferWithRightSize() { + ByteArrayPool pool = new ByteArrayPool(32); + + byte[] buf1 = pool.getBuf(16); + pool.returnBuf(buf1); + + byte[] buf2 = pool.getBuf(17); + assertNotSame(buf2, buf1); + + byte[] buf3 = pool.getBuf(15); + assertSame(buf3, buf1); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/CacheTest.java b/core/src/test/java/com/android/volley/toolbox/CacheTest.java new file mode 100644 index 0000000..22dae22 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/CacheTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.junit.Assert.assertNotNull; + +import com.android.volley.Cache; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class CacheTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(Cache.class.getMethod("get", String.class)); + assertNotNull(Cache.class.getMethod("put", String.class, Cache.Entry.class)); + assertNotNull(Cache.class.getMethod("initialize")); + assertNotNull(Cache.class.getMethod("invalidate", String.class, boolean.class)); + assertNotNull(Cache.class.getMethod("remove", String.class)); + assertNotNull(Cache.class.getMethod("clear")); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java b/core/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java new file mode 100644 index 0000000..db6e491 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java @@ -0,0 +1,646 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.emptyArray; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import com.android.volley.Cache; +import com.android.volley.Header; +import com.android.volley.toolbox.DiskBasedCache.CacheHeader; +import com.android.volley.toolbox.DiskBasedCache.CountingInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Random; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 16) +public class DiskBasedCacheTest { + + private static final int MAX_SIZE = 1024 * 1024; + + private Cache cache; + + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Rule public ExpectedException exception = ExpectedException.none(); + + @Before + public void setup() throws IOException { + // Initialize empty cache + cache = new DiskBasedCache(temporaryFolder.getRoot(), MAX_SIZE); + cache.initialize(); + } + + @After + public void teardown() { + cache = null; + } + + @Test + public void testEmptyInitialize() { + assertThat(cache.get("key"), is(nullValue())); + } + + @Test + public void testPutGetZeroBytes() { + Cache.Entry entry = new Cache.Entry(); + entry.data = new byte[0]; + entry.serverDate = 1234567L; + entry.lastModified = 13572468L; + entry.ttl = 9876543L; + entry.softTtl = 8765432L; + entry.etag = "etag"; + entry.responseHeaders = new HashMap<>(); + entry.responseHeaders.put("fruit", "banana"); + entry.responseHeaders.put("color", "yellow"); + cache.put("my-magical-key", entry); + + assertThatEntriesAreEqual(cache.get("my-magical-key"), entry); + assertThat(cache.get("unknown-key"), is(nullValue())); + } + + @Test + public void testPutRemoveGet() { + Cache.Entry entry = randomData(511); + cache.put("key", entry); + + assertThatEntriesAreEqual(cache.get("key"), entry); + + cache.remove("key"); + assertThat(cache.get("key"), is(nullValue())); + assertThat(listCachedFiles(), is(emptyArray())); + } + + @Test + public void testPutClearGet() { + Cache.Entry entry = randomData(511); + cache.put("key", entry); + + assertThatEntriesAreEqual(cache.get("key"), entry); + + cache.clear(); + assertThat(cache.get("key"), is(nullValue())); + assertThat(listCachedFiles(), is(emptyArray())); + } + + @Test + public void testReinitialize() { + Cache.Entry entry = randomData(1023); + cache.put("key", entry); + + Cache copy = new DiskBasedCache(temporaryFolder.getRoot(), MAX_SIZE); + copy.initialize(); + + assertThatEntriesAreEqual(copy.get("key"), entry); + } + + @Test + public void testInvalidate() { + Cache.Entry entry = randomData(32); + entry.softTtl = 8765432L; + entry.ttl = 9876543L; + cache.put("key", entry); + + cache.invalidate("key", false); + entry.softTtl = 0; // expired + assertThatEntriesAreEqual(cache.get("key"), entry); + } + + @Test + public void testInvalidateFullExpire() { + Cache.Entry entry = randomData(32); + entry.softTtl = 8765432L; + entry.ttl = 9876543L; + cache.put("key", entry); + + cache.invalidate("key", true); + entry.softTtl = 0; // expired + entry.ttl = 0; // expired + assertThatEntriesAreEqual(cache.get("key"), entry); + } + + @Test + public void testTooLargeEntry() { + Cache.Entry entry = randomData(MAX_SIZE - getEntrySizeOnDisk("oversize")); + cache.put("oversize", entry); + + assertThat(cache.get("oversize"), is(nullValue())); + } + + @Test + public void testMaxSizeEntry() { + Cache.Entry entry = randomData(MAX_SIZE - getEntrySizeOnDisk("maxsize") - 1); + cache.put("maxsize", entry); + + assertThatEntriesAreEqual(cache.get("maxsize"), entry); + } + + @Test + public void testTrimAtThreshold() { + // Start with the largest possible entry. + Cache.Entry entry = randomData(MAX_SIZE - getEntrySizeOnDisk("maxsize") - 1); + cache.put("maxsize", entry); + + assertThatEntriesAreEqual(cache.get("maxsize"), entry); + + // Now any new entry should cause the first one to be cleared. + entry = randomData(0); + cache.put("bit", entry); + + assertThat(cache.get("goodsize"), is(nullValue())); + assertThatEntriesAreEqual(cache.get("bit"), entry); + } + + @Test + public void testTrimWithMultipleEvictions_underHysteresisThreshold() { + Cache.Entry entry1 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry1") - 1); + cache.put("entry1", entry1); + Cache.Entry entry2 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry2") - 1); + cache.put("entry2", entry2); + Cache.Entry entry3 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry3") - 1); + cache.put("entry3", entry3); + + assertThatEntriesAreEqual(cache.get("entry1"), entry1); + assertThatEntriesAreEqual(cache.get("entry2"), entry2); + assertThatEntriesAreEqual(cache.get("entry3"), entry3); + + Cache.Entry entry = + randomData( + (int) (DiskBasedCache.HYSTERESIS_FACTOR * MAX_SIZE) + - getEntrySizeOnDisk("max")); + cache.put("max", entry); + + assertThat(cache.get("entry1"), is(nullValue())); + assertThat(cache.get("entry2"), is(nullValue())); + assertThat(cache.get("entry3"), is(nullValue())); + assertThatEntriesAreEqual(cache.get("max"), entry); + } + + @Test + public void testTrimWithMultipleEvictions_atHysteresisThreshold() { + Cache.Entry entry1 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry1") - 1); + cache.put("entry1", entry1); + Cache.Entry entry2 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry2") - 1); + cache.put("entry2", entry2); + Cache.Entry entry3 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry3") - 1); + cache.put("entry3", entry3); + + assertThatEntriesAreEqual(cache.get("entry1"), entry1); + assertThatEntriesAreEqual(cache.get("entry2"), entry2); + assertThatEntriesAreEqual(cache.get("entry3"), entry3); + + Cache.Entry entry = + randomData( + (int) (DiskBasedCache.HYSTERESIS_FACTOR * MAX_SIZE) + - getEntrySizeOnDisk("max") + + 1); + cache.put("max", entry); + + assertThat(cache.get("entry1"), is(nullValue())); + assertThat(cache.get("entry2"), is(nullValue())); + assertThat(cache.get("entry3"), is(nullValue())); + assertThat(cache.get("max"), is(nullValue())); + } + + @Test + public void testTrimWithPartialEvictions() { + Cache.Entry entry1 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry1") - 1); + cache.put("entry1", entry1); + Cache.Entry entry2 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry2") - 1); + cache.put("entry2", entry2); + Cache.Entry entry3 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry3") - 1); + cache.put("entry3", entry3); + + assertThatEntriesAreEqual(cache.get("entry1"), entry1); + assertThatEntriesAreEqual(cache.get("entry2"), entry2); + assertThatEntriesAreEqual(cache.get("entry3"), entry3); + + Cache.Entry entry4 = randomData((MAX_SIZE - getEntrySizeOnDisk("entry4") - 1) / 2); + cache.put("entry4", entry4); + + assertThat(cache.get("entry1"), is(nullValue())); + assertThat(cache.get("entry2"), is(nullValue())); + assertThatEntriesAreEqual(cache.get("entry3"), entry3); + assertThatEntriesAreEqual(cache.get("entry4"), entry4); + } + + @Test + public void testLargeEntryDoesntClearCache() { + // Writing a large entry to an empty cache should succeed + Cache.Entry largeEntry = randomData(MAX_SIZE - getEntrySizeOnDisk("largeEntry") - 1); + cache.put("largeEntry", largeEntry); + + assertThatEntriesAreEqual(cache.get("largeEntry"), largeEntry); + + // Reset and fill up ~half the cache. + cache.clear(); + Cache.Entry entry = randomData(MAX_SIZE / 2 - getEntrySizeOnDisk("entry") - 1); + cache.put("entry", entry); + + assertThatEntriesAreEqual(cache.get("entry"), entry); + + // Writing the large entry should no-op, because otherwise the pruning algorithm would clear + // the whole cache, since the large entry is above the hysteresis threshold. + cache.put("largeEntry", largeEntry); + + assertThat(cache.get("largeEntry"), is(nullValue())); + assertThatEntriesAreEqual(cache.get("entry"), entry); + } + + @Test + @SuppressWarnings("TryFinallyCanBeTryWithResources") + public void testGetBadMagic() throws IOException { + // Cache something + Cache.Entry entry = randomData(1023); + cache.put("key", entry); + assertThatEntriesAreEqual(cache.get("key"), entry); + + // Overwrite the magic header + File cacheFolder = temporaryFolder.getRoot(); + File file = cacheFolder.listFiles()[0]; + FileOutputStream fos = new FileOutputStream(file); + try { + DiskBasedCache.writeInt(fos, 0); // overwrite magic + } finally { + //noinspection ThrowFromFinallyBlock + fos.close(); + } + + assertThat(cache.get("key"), is(nullValue())); + assertThat(listCachedFiles(), is(emptyArray())); + } + + @Test + @SuppressWarnings("TryFinallyCanBeTryWithResources") + public void testGetWrongKey() throws IOException { + // Cache something + Cache.Entry entry = randomData(1023); + cache.put("key", entry); + assertThatEntriesAreEqual(cache.get("key"), entry); + + // Access the cached file + File cacheFolder = temporaryFolder.getRoot(); + File file = cacheFolder.listFiles()[0]; + FileOutputStream fos = new FileOutputStream(file); + try { + // Overwrite with a different key + CacheHeader wrongHeader = new CacheHeader("bad", entry); + wrongHeader.writeHeader(fos); + } finally { + //noinspection ThrowFromFinallyBlock + fos.close(); + } + + // key is gone, but file is still there + assertThat(cache.get("key"), is(nullValue())); + assertThat(listCachedFiles(), is(arrayWithSize(1))); + + // Note: file is now a zombie because its key does not map to its name + } + + @Test + public void testStreamToBytesNegativeLength() throws IOException { + byte[] data = new byte[1]; + CountingInputStream cis = + new CountingInputStream(new ByteArrayInputStream(data), data.length); + exception.expect(IOException.class); + DiskBasedCache.streamToBytes(cis, -1); + } + + @Test + public void testStreamToBytesExcessiveLength() throws IOException { + byte[] data = new byte[1]; + CountingInputStream cis = + new CountingInputStream(new ByteArrayInputStream(data), data.length); + exception.expect(IOException.class); + DiskBasedCache.streamToBytes(cis, 2); + } + + @Test + public void testStreamToBytesOverflow() throws IOException { + byte[] data = new byte[0]; + CountingInputStream cis = + new CountingInputStream(new ByteArrayInputStream(data), 0x100000000L); + exception.expect(IOException.class); + DiskBasedCache.streamToBytes(cis, 0x100000000L); // int value is 0 + } + + @Test + public void testReadHeaderListWithNegativeSize() throws IOException { + // If a cached header list is corrupted and begins with a negative size, + // verify that readHeaderList will throw an IOException. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DiskBasedCache.writeInt(baos, -1); // negative size + CountingInputStream cis = + new CountingInputStream( + new ByteArrayInputStream(baos.toByteArray()), Integer.MAX_VALUE); + // Expect IOException due to negative size + exception.expect(IOException.class); + DiskBasedCache.readHeaderList(cis); + } + + @Test + public void testReadHeaderListWithGinormousSize() throws IOException { + // If a cached header list is corrupted and begins with 2GB size, verify + // that readHeaderList will throw EOFException rather than OutOfMemoryError. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DiskBasedCache.writeInt(baos, Integer.MAX_VALUE); // 2GB size + CountingInputStream cis = + new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size()); + // Expect EOFException when end of stream is reached + exception.expect(EOFException.class); + DiskBasedCache.readHeaderList(cis); + } + + @Test + public void testFileIsDeletedWhenWriteHeaderFails() throws IOException { + // Create DataOutputStream that throws IOException + OutputStream mockedOutputStream = spy(OutputStream.class); + doThrow(IOException.class).when(mockedOutputStream).write(anyInt()); + + // Create read-only copy that fails to write anything + DiskBasedCache readonly = spy((DiskBasedCache) cache); + doReturn(mockedOutputStream).when(readonly).createOutputStream(any(File.class)); + + // Attempt to write + readonly.put("key", randomData(1111)); + + // write is called at least once because each linked stream flushes when closed + verify(mockedOutputStream, atLeastOnce()).write(anyInt()); + assertThat(readonly.get("key"), is(nullValue())); + assertThat(listCachedFiles(), is(emptyArray())); + + // Note: original cache will try (without success) to read from file + assertThat(cache.get("key"), is(nullValue())); + } + + @Test + public void testIOExceptionInInitialize() throws IOException { + // Cache a few kilobytes + cache.put("kilobyte", randomData(1024)); + cache.put("kilobyte2", randomData(1024)); + cache.put("kilobyte3", randomData(1024)); + + // Create DataInputStream that throws IOException + InputStream mockedInputStream = spy(InputStream.class); + //noinspection ResultOfMethodCallIgnored + doThrow(IOException.class).when(mockedInputStream).read(); + + // Create broken cache that fails to read anything + DiskBasedCache broken = spy(new DiskBasedCache(temporaryFolder.getRoot())); + doReturn(mockedInputStream).when(broken).createInputStream(any(File.class)); + + // Attempt to initialize + broken.initialize(); + + // Everything is gone + assertThat(broken.get("kilobyte"), is(nullValue())); + assertThat(broken.get("kilobyte2"), is(nullValue())); + assertThat(broken.get("kilobyte3"), is(nullValue())); + assertThat(listCachedFiles(), is(emptyArray())); + + // Verify that original cache can cope with missing files + assertThat(cache.get("kilobyte"), is(nullValue())); + assertThat(cache.get("kilobyte2"), is(nullValue())); + assertThat(cache.get("kilobyte3"), is(nullValue())); + } + + @Test + public void testManyResponseHeaders() { + Cache.Entry entry = new Cache.Entry(); + entry.data = new byte[0]; + entry.responseHeaders = new HashMap<>(); + for (int i = 0; i < 0xFFFF; i++) { + entry.responseHeaders.put(Integer.toString(i), ""); + } + cache.put("key", entry); + } + + @Test + @SuppressWarnings("TryFinallyCanBeTryWithResources") + public void testCountingInputStreamByteCount() throws IOException { + // Write some bytes + ByteArrayOutputStream out = new ByteArrayOutputStream(); + //noinspection ThrowFromFinallyBlock + try { + DiskBasedCache.writeInt(out, 1); + DiskBasedCache.writeLong(out, -1L); + DiskBasedCache.writeString(out, "hamburger"); + } finally { + //noinspection ThrowFromFinallyBlock + out.close(); + } + long bytesWritten = out.size(); + + // Read the bytes and compare the counts + CountingInputStream cis = + new CountingInputStream(new ByteArrayInputStream(out.toByteArray()), bytesWritten); + try { + assertThat(cis.bytesRemaining(), is(bytesWritten)); + assertThat(cis.bytesRead(), is(0L)); + assertThat(DiskBasedCache.readInt(cis), is(1)); + assertThat(DiskBasedCache.readLong(cis), is(-1L)); + assertThat(DiskBasedCache.readString(cis), is("hamburger")); + assertThat(cis.bytesRead(), is(bytesWritten)); + assertThat(cis.bytesRemaining(), is(0L)); + } finally { + //noinspection ThrowFromFinallyBlock + cis.close(); + } + } + + /* Serialization tests */ + + @Test + public void testEmptyReadThrowsEOF() throws IOException { + ByteArrayInputStream empty = new ByteArrayInputStream(new byte[] {}); + exception.expect(EOFException.class); + DiskBasedCache.readInt(empty); + } + + @Test + public void serializeInt() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DiskBasedCache.writeInt(baos, 0); + DiskBasedCache.writeInt(baos, 19791214); + DiskBasedCache.writeInt(baos, -20050711); + DiskBasedCache.writeInt(baos, Integer.MIN_VALUE); + DiskBasedCache.writeInt(baos, Integer.MAX_VALUE); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + assertEquals(DiskBasedCache.readInt(bais), 0); + assertEquals(DiskBasedCache.readInt(bais), 19791214); + assertEquals(DiskBasedCache.readInt(bais), -20050711); + assertEquals(DiskBasedCache.readInt(bais), Integer.MIN_VALUE); + assertEquals(DiskBasedCache.readInt(bais), Integer.MAX_VALUE); + } + + @Test + public void serializeLong() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DiskBasedCache.writeLong(baos, 0); + DiskBasedCache.writeLong(baos, 31337); + DiskBasedCache.writeLong(baos, -4160); + DiskBasedCache.writeLong(baos, 4295032832L); + DiskBasedCache.writeLong(baos, -4314824046L); + DiskBasedCache.writeLong(baos, Long.MIN_VALUE); + DiskBasedCache.writeLong(baos, Long.MAX_VALUE); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + assertEquals(DiskBasedCache.readLong(bais), 0); + assertEquals(DiskBasedCache.readLong(bais), 31337); + assertEquals(DiskBasedCache.readLong(bais), -4160); + assertEquals(DiskBasedCache.readLong(bais), 4295032832L); + assertEquals(DiskBasedCache.readLong(bais), -4314824046L); + assertEquals(DiskBasedCache.readLong(bais), Long.MIN_VALUE); + assertEquals(DiskBasedCache.readLong(bais), Long.MAX_VALUE); + } + + @Test + public void serializeString() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DiskBasedCache.writeString(baos, ""); + DiskBasedCache.writeString(baos, "This is a string."); + DiskBasedCache.writeString(baos, "ファイカス"); + CountingInputStream cis = + new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size()); + assertEquals(DiskBasedCache.readString(cis), ""); + assertEquals(DiskBasedCache.readString(cis), "This is a string."); + assertEquals(DiskBasedCache.readString(cis), "ファイカス"); + } + + @Test + public void serializeHeaders() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + List
empty = new ArrayList<>(); + DiskBasedCache.writeHeaderList(empty, baos); + DiskBasedCache.writeHeaderList(null, baos); + List
twoThings = new ArrayList<>(); + twoThings.add(new Header("first", "thing")); + twoThings.add(new Header("second", "item")); + DiskBasedCache.writeHeaderList(twoThings, baos); + List
emptyKey = new ArrayList<>(); + emptyKey.add(new Header("", "value")); + DiskBasedCache.writeHeaderList(emptyKey, baos); + List
emptyValue = new ArrayList<>(); + emptyValue.add(new Header("key", "")); + DiskBasedCache.writeHeaderList(emptyValue, baos); + List
sameKeys = new ArrayList<>(); + sameKeys.add(new Header("key", "value")); + sameKeys.add(new Header("key", "value2")); + DiskBasedCache.writeHeaderList(sameKeys, baos); + CountingInputStream cis = + new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size()); + assertEquals(DiskBasedCache.readHeaderList(cis), empty); + assertEquals(DiskBasedCache.readHeaderList(cis), empty); // null reads back empty + assertEquals(DiskBasedCache.readHeaderList(cis), twoThings); + assertEquals(DiskBasedCache.readHeaderList(cis), emptyKey); + assertEquals(DiskBasedCache.readHeaderList(cis), emptyValue); + assertEquals(DiskBasedCache.readHeaderList(cis), sameKeys); + } + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(DiskBasedCache.class.getConstructor(File.class, int.class)); + assertNotNull( + DiskBasedCache.class.getConstructor(DiskBasedCache.FileSupplier.class, int.class)); + assertNotNull(DiskBasedCache.class.getConstructor(File.class)); + assertNotNull(DiskBasedCache.class.getConstructor(DiskBasedCache.FileSupplier.class)); + + assertNotNull(DiskBasedCache.class.getMethod("getFileForKey", String.class)); + } + + @Test + public void initializeIfRootDirectoryDeleted() { + temporaryFolder.delete(); + + Cache.Entry entry = randomData(101); + cache.put("key1", entry); + + assertThat(cache.get("key1"), is(nullValue())); + + // confirm that we can now store entries + cache.put("key2", entry); + assertThatEntriesAreEqual(cache.get("key2"), entry); + } + + /* Test helpers */ + + private void assertThatEntriesAreEqual(Cache.Entry actual, Cache.Entry expected) { + assertThat(actual.data, is(equalTo(expected.data))); + assertThat(actual.etag, is(equalTo(expected.etag))); + assertThat(actual.lastModified, is(equalTo(expected.lastModified))); + assertThat(actual.responseHeaders, is(equalTo(expected.responseHeaders))); + assertThat(actual.serverDate, is(equalTo(expected.serverDate))); + assertThat(actual.softTtl, is(equalTo(expected.softTtl))); + assertThat(actual.ttl, is(equalTo(expected.ttl))); + } + + private Cache.Entry randomData(int length) { + Cache.Entry entry = new Cache.Entry(); + byte[] data = new byte[length]; + new Random(42).nextBytes(data); // explicit seed for reproducible results + entry.data = data; + return entry; + } + + private File[] listCachedFiles() { + return temporaryFolder.getRoot().listFiles(); + } + + private int getEntrySizeOnDisk(String key) { + // Header size is: + // 4 bytes for magic int + // 8 + len(key) bytes for key (long length) + // 8 bytes for etag (long length + 0 characters) + // 32 bytes for serverDate, lastModified, ttl, and softTtl longs + // 4 bytes for length of header list int + // == 56 + len(key) bytes total. + return 56 + key.length(); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java b/core/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java new file mode 100644 index 0000000..2a451dc --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.android.volley.Request.Method; +import com.android.volley.mock.TestRequest; +import com.android.volley.toolbox.HttpClientStack.HttpPatch; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpTrace; +import org.apache.http.client.methods.HttpUriRequest; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class HttpClientStackTest { + + @Test + public void createDeprecatedGetRequest() throws Exception { + TestRequest.DeprecatedGet request = new TestRequest.DeprecatedGet(); + assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpGet); + } + + @Test + public void createDeprecatedPostRequest() throws Exception { + TestRequest.DeprecatedPost request = new TestRequest.DeprecatedPost(); + assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPost); + } + + @Test + public void createGetRequest() throws Exception { + TestRequest.Get request = new TestRequest.Get(); + assertEquals(request.getMethod(), Method.GET); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpGet); + } + + @Test + public void createPostRequest() throws Exception { + TestRequest.Post request = new TestRequest.Post(); + assertEquals(request.getMethod(), Method.POST); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPost); + } + + @Test + public void createPostRequestWithBody() throws Exception { + TestRequest.PostWithBody request = new TestRequest.PostWithBody(); + assertEquals(request.getMethod(), Method.POST); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPost); + } + + @Test + public void createPutRequest() throws Exception { + TestRequest.Put request = new TestRequest.Put(); + assertEquals(request.getMethod(), Method.PUT); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPut); + } + + @Test + public void createPutRequestWithBody() throws Exception { + TestRequest.PutWithBody request = new TestRequest.PutWithBody(); + assertEquals(request.getMethod(), Method.PUT); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPut); + } + + @Test + public void createDeleteRequest() throws Exception { + TestRequest.Delete request = new TestRequest.Delete(); + assertEquals(request.getMethod(), Method.DELETE); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpDelete); + } + + @Test + public void createHeadRequest() throws Exception { + TestRequest.Head request = new TestRequest.Head(); + assertEquals(request.getMethod(), Method.HEAD); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpHead); + } + + @Test + public void createOptionsRequest() throws Exception { + TestRequest.Options request = new TestRequest.Options(); + assertEquals(request.getMethod(), Method.OPTIONS); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpOptions); + } + + @Test + public void createTraceRequest() throws Exception { + TestRequest.Trace request = new TestRequest.Trace(); + assertEquals(request.getMethod(), Method.TRACE); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpTrace); + } + + @Test + public void createPatchRequest() throws Exception { + TestRequest.Patch request = new TestRequest.Patch(); + assertEquals(request.getMethod(), Method.PATCH); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPatch); + } + + @Test + public void createPatchRequestWithBody() throws Exception { + TestRequest.PatchWithBody request = new TestRequest.PatchWithBody(); + assertEquals(request.getMethod(), Method.PATCH); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPatch); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java b/core/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java new file mode 100644 index 0000000..7780c3e --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.android.volley.Cache; +import com.android.volley.Header; +import com.android.volley.NetworkResponse; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class HttpHeaderParserTest { + + private static long ONE_MINUTE_MILLIS = 1000L * 60; + private static long ONE_HOUR_MILLIS = 1000L * 60 * 60; + private static long ONE_DAY_MILLIS = ONE_HOUR_MILLIS * 24; + private static long ONE_WEEK_MILLIS = ONE_DAY_MILLIS * 7; + + private NetworkResponse response; + private Map headers; + + @Before + public void setUp() throws Exception { + headers = new HashMap(); + response = new NetworkResponse(0, null, headers, false); + } + + @Test + public void parseCacheHeaders_noHeaders() { + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNull(entry.etag); + assertEquals(0, entry.serverDate); + assertEquals(0, entry.lastModified); + assertEquals(0, entry.ttl); + assertEquals(0, entry.softTtl); + } + + @Test + public void parseCacheHeaders_nullHeaders() { + response = new NetworkResponse(0, null, null, false); + assertNull(HttpHeaderParser.parseCacheHeaders(response)); + } + + @Test + public void parseCacheHeaders_headersSet() { + headers.put("MyCustomHeader", "42"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNotNull(entry.responseHeaders); + assertEquals(1, entry.responseHeaders.size()); + assertEquals("42", entry.responseHeaders.get("MyCustomHeader")); + } + + @Test + public void parseCacheHeaders_etag() { + headers.put("ETag", "Yow!"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertEquals("Yow!", entry.etag); + } + + @Test + public void parseCacheHeaders_normalExpire() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Last-Modified", rfc1123Date(now - ONE_DAY_MILLIS)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(entry.serverDate, now, ONE_MINUTE_MILLIS); + assertEqualsWithin(entry.lastModified, (now - ONE_DAY_MILLIS), ONE_MINUTE_MILLIS); + assertTrue(entry.softTtl >= (now + ONE_HOUR_MILLIS)); + assertTrue(entry.ttl == entry.softTtl); + } + + @Test + public void parseCacheHeaders_expiresInPast() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now - ONE_HOUR_MILLIS)); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(entry.serverDate, now, ONE_MINUTE_MILLIS); + assertEquals(0, entry.ttl); + assertEquals(0, entry.softTtl); + } + + @Test + public void parseCacheHeaders_serverRelative() { + + long now = System.currentTimeMillis(); + // Set "current" date as one hour in the future + headers.put("Date", rfc1123Date(now + ONE_HOUR_MILLIS)); + // TTL four hours in the future, so should be three hours from now + headers.put("Expires", rfc1123Date(now + 4 * ONE_HOUR_MILLIS)); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertEqualsWithin(now + 3 * ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + } + + @Test + public void parseCacheHeaders_cacheControlOverridesExpires() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + headers.put("Cache-Control", "public, max-age=86400"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(now + ONE_DAY_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + } + + @Test + public void testParseCacheHeaders_staleWhileRevalidate() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + + // - max-age (entry.softTtl) indicates that the asset is fresh for 1 day + // - stale-while-revalidate (entry.ttl) indicates that the asset may + // continue to be served stale for up to additional 7 days + headers.put("Cache-Control", "max-age=86400, stale-while-revalidate=604800"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(now + ONE_DAY_MILLIS, entry.softTtl, ONE_MINUTE_MILLIS); + assertEqualsWithin(now + ONE_DAY_MILLIS + ONE_WEEK_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); + } + + @Test + public void parseCacheHeaders_cacheControlNoCache() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + headers.put("Cache-Control", "no-cache"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNull(entry); + } + + @Test + public void parseCacheHeaders_cacheControlMustRevalidateNoMaxAge() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + headers.put("Cache-Control", "must-revalidate"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(now, entry.ttl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + } + + @Test + public void parseCacheHeaders_cacheControlMustRevalidateWithMaxAge() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + headers.put("Cache-Control", "must-revalidate, max-age=3600"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(now + ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + } + + @Test + public void parseCacheHeaders_cacheControlMustRevalidateWithMaxAgeAndStale() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + + // - max-age (entry.softTtl) indicates that the asset is fresh for 1 day + // - stale-while-revalidate (entry.ttl) indicates that the asset may + // continue to be served stale for up to additional 7 days, but this is + // ignored in this case because of the must-revalidate header. + headers.put( + "Cache-Control", "must-revalidate, max-age=86400, stale-while-revalidate=604800"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(now + ONE_DAY_MILLIS, entry.softTtl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + } + + private void assertEqualsWithin(long expected, long value, long fudgeFactor) { + long diff = Math.abs(expected - value); + assertTrue(diff < fudgeFactor); + } + + private static String rfc1123Date(long millis) { + DateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH); + return df.format(new Date(millis)); + } + + // -------------------------- + + @Test + public void parseCharset() { + // Like the ones we usually see + headers.put("Content-Type", "text/plain; charset=utf-8"); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers)); + + // Charset specified, ignore default charset + headers.put("Content-Type", "text/plain; charset=utf-8"); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "ISO-8859-1")); + + // Extra whitespace + headers.put("Content-Type", "text/plain; charset=utf-8 "); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers)); + + // Extra parameters + headers.put("Content-Type", "text/plain; charset=utf-8; frozzle=bar"); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers)); + + // No Content-Type header + headers.clear(); + assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); + + // No Content-Type header, use default charset + headers.clear(); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "utf-8")); + + // Empty value + headers.put("Content-Type", "text/plain; charset="); + assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); + + // None specified + headers.put("Content-Type", "text/plain"); + assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); + + // None charset specified, use default charset + headers.put("Content-Type", "application/json"); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "utf-8")); + + // None specified, extra semicolon + headers.put("Content-Type", "text/plain;"); + assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); + + // No headers, use default charset + assertEquals("utf-8", HttpHeaderParser.parseCharset(null, "utf-8")); + } + + @Test + public void parseCaseInsensitive() { + long now = System.currentTimeMillis(); + + List
headers = new ArrayList<>(); + headers.add(new Header("eTAG", "Yow!")); + headers.add(new Header("DATE", rfc1123Date(now))); + headers.add(new Header("expires", rfc1123Date(now + ONE_HOUR_MILLIS))); + headers.add(new Header("cache-control", "public, max-age=86400")); + headers.add(new Header("content-type", "text/plain")); + + NetworkResponse response = new NetworkResponse(0, null, false, 0, headers); + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertEquals("Yow!", entry.etag); + assertEqualsWithin(now + ONE_DAY_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + assertEquals( + "ISO-8859-1", HttpHeaderParser.parseCharset(HttpHeaderParser.toHeaderMap(headers))); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java b/core/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java new file mode 100644 index 0000000..6794af8 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java @@ -0,0 +1,192 @@ +package com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import com.android.volley.Request; +import com.android.volley.RetryPolicy; +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.http.Header; +import org.apache.http.HttpRequest; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpUriRequest; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; + +/** Tests to validate that HttpStack implementations conform with expected behavior. */ +@RunWith(RobolectricTestRunner.class) +public class HttpStackConformanceTest { + @Mock private RetryPolicy mMockRetryPolicy; + @Mock private Request mMockRequest; + + @Mock private HttpURLConnection mMockConnection; + @Mock private OutputStream mMockOutputStream; + @Spy private HurlStack mHurlStack = new HurlStack(); + + @Mock private HttpClient mMockHttpClient; + private HttpClientStack mHttpClientStack; + + private final TestCase[] mTestCases = + new TestCase[] { + // TestCase for HurlStack. + new TestCase() { + @Override + public HttpStack getStack() { + return mHurlStack; + } + + @Override + public void setOutputHeaderMap(final Map outputHeaderMap) { + doAnswer( + new Answer() { + @Override + public Void answer(InvocationOnMock invocation) { + outputHeaderMap.put( + invocation.getArgument(0), + invocation.getArgument(1)); + return null; + } + }) + .when(mMockConnection) + .setRequestProperty(anyString(), anyString()); + doAnswer( + new Answer>>() { + @Override + public Map> answer( + InvocationOnMock invocation) { + Map> result = new HashMap<>(); + for (Map.Entry entry : + outputHeaderMap.entrySet()) { + result.put( + entry.getKey(), + Collections.singletonList( + entry.getValue())); + } + return result; + } + }) + .when(mMockConnection) + .getRequestProperties(); + } + }, + + // TestCase for HttpClientStack. + new TestCase() { + @Override + public HttpStack getStack() { + return mHttpClientStack; + } + + @Override + public void setOutputHeaderMap(final Map outputHeaderMap) { + try { + doAnswer( + new Answer() { + @Override + public Void answer(InvocationOnMock invocation) + throws Throwable { + HttpRequest request = invocation.getArgument(0); + for (Header header : request.getAllHeaders()) { + if (outputHeaderMap.containsKey( + header.getName())) { + fail( + "Multiple values for header " + + header.getName()); + } + outputHeaderMap.put( + header.getName(), + header.getValue()); + } + return null; + } + }) + .when(mMockHttpClient) + .execute(any(HttpUriRequest.class)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + }; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mHttpClientStack = spy(new HttpClientStack(mMockHttpClient)); + + doReturn(mMockConnection).when(mHurlStack).createConnection(any(URL.class)); + doReturn(mMockOutputStream).when(mMockConnection).getOutputStream(); + when(mMockRequest.getUrl()).thenReturn("http://127.0.0.1"); + when(mMockRequest.getRetryPolicy()).thenReturn(mMockRetryPolicy); + } + + @Test + public void headerPrecedence() throws Exception { + Map additionalHeaders = new HashMap<>(); + additionalHeaders.put("A", "AddlA"); + additionalHeaders.put("B", "AddlB"); + + Map requestHeaders = new HashMap<>(); + requestHeaders.put("A", "RequestA"); + requestHeaders.put("C", "RequestC"); + when(mMockRequest.getHeaders()).thenReturn(requestHeaders); + + when(mMockRequest.getMethod()).thenReturn(Request.Method.POST); + when(mMockRequest.getBody()).thenReturn(new byte[0]); + when(mMockRequest.getBodyContentType()).thenReturn("BodyContentType"); + + for (TestCase testCase : mTestCases) { + // Test once without a Content-Type header in getHeaders(). + Map combinedHeaders = new HashMap<>(); + testCase.setOutputHeaderMap(combinedHeaders); + + testCase.getStack().performRequest(mMockRequest, additionalHeaders); + + Map expectedHeaders = new HashMap<>(); + expectedHeaders.put("A", "RequestA"); + expectedHeaders.put("B", "AddlB"); + expectedHeaders.put("C", "RequestC"); + expectedHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, "BodyContentType"); + + assertEquals(expectedHeaders, combinedHeaders); + + // Reset and test again with a Content-Type header in getHeaders(). + combinedHeaders.clear(); + + requestHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, "RequestContentType"); + expectedHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, "RequestContentType"); + + testCase.getStack().performRequest(mMockRequest, additionalHeaders); + assertEquals(expectedHeaders, combinedHeaders); + + // Clear the Content-Type header for the next TestCase. + requestHeaders.remove(HttpHeaderParser.HEADER_CONTENT_TYPE); + } + } + + private interface TestCase { + HttpStack getStack(); + + void setOutputHeaderMap(Map outputHeaderMap); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/HurlStackTest.java b/core/src/test/java/com/android/volley/toolbox/HurlStackTest.java new file mode 100644 index 0000000..7508244 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/HurlStackTest.java @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.android.volley.Header; +import com.android.volley.Request; +import com.android.volley.Request.Method; +import com.android.volley.mock.TestRequest; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class HurlStackTest { + + @Mock private HttpURLConnection mMockConnection; + private HurlStack mHurlStack; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + when(mMockConnection.getOutputStream()).thenReturn(new ByteArrayOutputStream()); + + mHurlStack = + new HurlStack() { + @Override + protected HttpURLConnection createConnection(URL url) { + return mMockConnection; + } + + @Override + protected InputStream createInputStream( + Request request, HttpURLConnection connection) { + return new MonitoringInputStream( + super.createInputStream(request, connection)); + } + + @Override + protected OutputStream createOutputStream( + Request request, HttpURLConnection connection, int length) + throws IOException { + if (request instanceof MonitoredRequest) { + return new MonitoringOutputStream( + super.createOutputStream(request, connection, length), + (MonitoredRequest) request, + length); + } + return super.createOutputStream(request, connection, length); + } + }; + } + + @Test + public void connectionForDeprecatedGetRequest() throws Exception { + TestRequest.DeprecatedGet request = new TestRequest.DeprecatedGet(); + assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection, never()).setRequestMethod(anyString()); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForDeprecatedPostRequest() throws Exception { + TestRequest.DeprecatedPost request = new TestRequest.DeprecatedPost(); + assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("POST"); + verify(mMockConnection).setDoOutput(true); + } + + @Test + public void connectionForGetRequest() throws Exception { + TestRequest.Get request = new TestRequest.Get(); + assertEquals(request.getMethod(), Method.GET); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("GET"); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForPostRequest() throws Exception { + TestRequest.Post request = new TestRequest.Post(); + assertEquals(request.getMethod(), Method.POST); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("POST"); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForPostWithBodyRequest() throws Exception { + TestRequest.PostWithBody request = new TestRequest.PostWithBody(); + assertEquals(request.getMethod(), Method.POST); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("POST"); + verify(mMockConnection).setDoOutput(true); + } + + @Test + public void connectionForPutRequest() throws Exception { + TestRequest.Put request = new TestRequest.Put(); + assertEquals(request.getMethod(), Method.PUT); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("PUT"); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForPutWithBodyRequest() throws Exception { + TestRequest.PutWithBody request = new TestRequest.PutWithBody(); + assertEquals(request.getMethod(), Method.PUT); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("PUT"); + verify(mMockConnection).setDoOutput(true); + } + + @Test + public void connectionForDeleteRequest() throws Exception { + TestRequest.Delete request = new TestRequest.Delete(); + assertEquals(request.getMethod(), Method.DELETE); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("DELETE"); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForHeadRequest() throws Exception { + TestRequest.Head request = new TestRequest.Head(); + assertEquals(request.getMethod(), Method.HEAD); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("HEAD"); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForOptionsRequest() throws Exception { + TestRequest.Options request = new TestRequest.Options(); + assertEquals(request.getMethod(), Method.OPTIONS); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("OPTIONS"); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForTraceRequest() throws Exception { + TestRequest.Trace request = new TestRequest.Trace(); + assertEquals(request.getMethod(), Method.TRACE); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("TRACE"); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForPatchRequest() throws Exception { + TestRequest.Patch request = new TestRequest.Patch(); + assertEquals(request.getMethod(), Method.PATCH); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("PATCH"); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForPatchWithBodyRequest() throws Exception { + TestRequest.PatchWithBody request = new TestRequest.PatchWithBody(); + assertEquals(request.getMethod(), Method.PATCH); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("PATCH"); + verify(mMockConnection).setDoOutput(true); + } + + @Test + public void executeRequestClosesConnection_connectionError() throws Exception { + when(mMockConnection.getResponseCode()).thenThrow(new SocketTimeoutException()); + try { + mHurlStack.executeRequest( + new TestRequest.Get(), Collections.emptyMap()); + fail("Should have thrown exception"); + } catch (IOException e) { + verify(mMockConnection).disconnect(); + } + } + + @Test + public void executeRequestClosesConnection_invalidResponseCode() throws Exception { + when(mMockConnection.getResponseCode()).thenReturn(-1); + try { + mHurlStack.executeRequest( + new TestRequest.Get(), Collections.emptyMap()); + fail("Should have thrown exception"); + } catch (IOException e) { + verify(mMockConnection).disconnect(); + } + } + + @Test + public void executeRequestClosesConnection_noResponseBody() throws Exception { + when(mMockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_NO_CONTENT); + mHurlStack.executeRequest(new TestRequest.Get(), Collections.emptyMap()); + verify(mMockConnection).disconnect(); + } + + @Test + public void executeRequestClosesConnection_hasResponseBody() throws Exception { + when(mMockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(mMockConnection.getInputStream()) + .thenReturn(new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8))); + HttpResponse response = + mHurlStack.executeRequest( + new TestRequest.Get(), Collections.emptyMap()); + // Shouldn't be disconnected until the stream is consumed. + verify(mMockConnection, never()).disconnect(); + response.getContent().close(); + verify(mMockConnection).disconnect(); + } + + @Test + public void convertHeaders() { + Map> headers = new HashMap<>(); + headers.put(null, Collections.singletonList("Ignored")); + headers.put("HeaderA", Collections.singletonList("ValueA")); + List values = new ArrayList<>(); + values.add("ValueB_1"); + values.add("ValueB_2"); + headers.put("HeaderB", values); + List
result = HurlStack.convertHeaders(headers); + List
expected = new ArrayList<>(); + expected.add(new Header("HeaderA", "ValueA")); + expected.add(new Header("HeaderB", "ValueB_1")); + expected.add(new Header("HeaderB", "ValueB_2")); + assertEquals(expected, result); + } + + @Test + public void interceptResponseStream() throws Exception { + when(mMockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(mMockConnection.getInputStream()) + .thenReturn(new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8))); + HttpResponse response = + mHurlStack.executeRequest( + new TestRequest.Get(), Collections.emptyMap()); + assertTrue(response.getContent() instanceof MonitoringInputStream); + } + + @Test + public void interceptRequestStream() throws Exception { + MonitoredRequest request = new MonitoredRequest(); + mHurlStack.executeRequest(request, Collections.emptyMap()); + assertTrue(request.totalRequestBytes > 0); + assertEquals(request.totalRequestBytes, request.requestBytesRead); + } + + private static class MonitoringInputStream extends FilterInputStream { + private MonitoringInputStream(InputStream in) { + super(in); + } + } + + private static class MonitoringOutputStream extends FilterOutputStream { + private MonitoredRequest request; + + private MonitoringOutputStream(OutputStream out, MonitoredRequest request, int length) { + super(out); + this.request = request; + this.request.totalRequestBytes = length; + } + + @Override + public void write(int b) throws IOException { + this.request.requestBytesRead++; + out.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + this.request.requestBytesRead += len; + out.write(b, off, len); + } + } + + private static class MonitoredRequest extends TestRequest.PostWithBody { + int requestBytesRead = 0; + int totalRequestBytes = 0; + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java b/core/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java new file mode 100644 index 0000000..59a0b1b --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.graphics.Bitmap; +import android.widget.ImageView; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ImageLoaderTest { + private RequestQueue mRequestQueue; + private ImageLoader.ImageCache mImageCache; + private ImageLoader mImageLoader; + + @Before + public void setUp() { + mRequestQueue = mock(RequestQueue.class); + mImageCache = mock(ImageLoader.ImageCache.class); + mImageLoader = new ImageLoader(mRequestQueue, mImageCache); + } + + @Test + public void isCachedChecksCache() throws Exception { + when(mImageCache.getBitmap(anyString())).thenReturn(null); + Assert.assertFalse(mImageLoader.isCached("http://foo", 0, 0)); + } + + @Test + public void getWithCacheHit() throws Exception { + Bitmap bitmap = Bitmap.createBitmap(1, 1, null); + ImageLoader.ImageListener listener = mock(ImageLoader.ImageListener.class); + when(mImageCache.getBitmap(anyString())).thenReturn(bitmap); + ImageLoader.ImageContainer ic = mImageLoader.get("http://foo", listener); + Assert.assertSame(bitmap, ic.getBitmap()); + verify(listener).onResponse(ic, true); + } + + @Test + public void getWithCacheMiss() throws Exception { + when(mImageCache.getBitmap(anyString())).thenReturn(null); + ImageLoader.ImageListener listener = mock(ImageLoader.ImageListener.class); + // Ask for the image to be loaded. + mImageLoader.get("http://foo", listener); + // Second pass to test deduping logic. + mImageLoader.get("http://foo", listener); + // Response callback should be called both times. + verify(listener, times(2)).onResponse(any(ImageLoader.ImageContainer.class), eq(true)); + // But request should be enqueued only once. + verify(mRequestQueue, times(1)).add(Mockito.>any()); + } + + @Test + public void publicMethods() throws Exception { + // Catch API breaking changes. + ImageLoader.getImageListener(null, -1, -1); + mImageLoader.setBatchedResponseDelay(1000); + + assertNotNull( + ImageLoader.class.getConstructor(RequestQueue.class, ImageLoader.ImageCache.class)); + + assertNotNull( + ImageLoader.class.getMethod( + "getImageListener", ImageView.class, int.class, int.class)); + assertNotNull(ImageLoader.class.getMethod("isCached", String.class, int.class, int.class)); + assertNotNull( + ImageLoader.class.getMethod( + "isCached", String.class, int.class, int.class, ImageView.ScaleType.class)); + assertNotNull( + ImageLoader.class.getMethod("get", String.class, ImageLoader.ImageListener.class)); + assertNotNull( + ImageLoader.class.getMethod( + "get", + String.class, + ImageLoader.ImageListener.class, + int.class, + int.class)); + assertNotNull( + ImageLoader.class.getMethod( + "get", + String.class, + ImageLoader.ImageListener.class, + int.class, + int.class, + ImageView.ScaleType.class)); + assertNotNull(ImageLoader.class.getMethod("setBatchedResponseDelay", int.class)); + + assertNotNull( + ImageLoader.ImageListener.class.getMethod( + "onResponse", ImageLoader.ImageContainer.class, boolean.class)); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/ImageRequestTest.java b/core/src/test/java/com/android/volley/toolbox/ImageRequestTest.java new file mode 100644 index 0000000..6b50319 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/ImageRequestTest.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import com.android.volley.NetworkResponse; +import com.android.volley.Response; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowBitmapFactory; + +@RunWith(RobolectricTestRunner.class) +public class ImageRequestTest { + + @Test + public void parseNetworkResponse_resizing() throws Exception { + // This is a horrible hack but Robolectric doesn't have a way to provide + // width and height hints for decodeByteArray. It works because the byte array + // "file:fake" is ASCII encodable and thus the name in Robolectric's fake + // bitmap creator survives as-is, and provideWidthAndHeightHints puts + // "file:" + name in its lookaside map. I write all this because it will + // probably break mysteriously at some point and I feel terrible about your + // having to debug it. + byte[] jpegBytes = "file:fake".getBytes(StandardCharsets.UTF_8); + ShadowBitmapFactory.provideWidthAndHeightHints("fake", 1024, 500); + NetworkResponse jpeg = new NetworkResponse(jpegBytes); + + // Scale the image uniformly (maintain the image's aspect ratio) so that + // both dimensions (width and height) of the image will be equal to or + // less than the corresponding dimension of the view. + ScaleType scalteType = ScaleType.CENTER_INSIDE; + + // Exact sizes + verifyResize(jpeg, 512, 250, scalteType, 512, 250); // exactly half + verifyResize(jpeg, 511, 249, scalteType, 509, 249); // just under half + verifyResize(jpeg, 1080, 500, scalteType, 1024, 500); // larger + verifyResize(jpeg, 500, 500, scalteType, 500, 244); // keep same ratio + + // Specify only width, preserve aspect ratio + verifyResize(jpeg, 512, 0, scalteType, 512, 250); + verifyResize(jpeg, 800, 0, scalteType, 800, 390); + verifyResize(jpeg, 1024, 0, scalteType, 1024, 500); + + // Specify only height, preserve aspect ratio + verifyResize(jpeg, 0, 250, scalteType, 512, 250); + verifyResize(jpeg, 0, 391, scalteType, 800, 391); + verifyResize(jpeg, 0, 500, scalteType, 1024, 500); + + // No resize + verifyResize(jpeg, 0, 0, scalteType, 1024, 500); + + // Scale the image uniformly (maintain the image's aspect ratio) so that + // both dimensions (width and height) of the image will be equal to or + // larger than the corresponding dimension of the view. + scalteType = ScaleType.CENTER_CROP; + + // Exact sizes + verifyResize(jpeg, 512, 250, scalteType, 512, 250); + verifyResize(jpeg, 511, 249, scalteType, 511, 249); + verifyResize(jpeg, 1080, 500, scalteType, 1024, 500); + verifyResize(jpeg, 500, 500, scalteType, 1024, 500); + + // Specify only width + verifyResize(jpeg, 512, 0, scalteType, 512, 250); + verifyResize(jpeg, 800, 0, scalteType, 800, 390); + verifyResize(jpeg, 1024, 0, scalteType, 1024, 500); + + // Specify only height + verifyResize(jpeg, 0, 250, scalteType, 512, 250); + verifyResize(jpeg, 0, 391, scalteType, 800, 391); + verifyResize(jpeg, 0, 500, scalteType, 1024, 500); + + // No resize + verifyResize(jpeg, 0, 0, scalteType, 1024, 500); + + // Scale in X and Y independently, so that src matches dst exactly. This + // may change the aspect ratio of the src. + scalteType = ScaleType.FIT_XY; + + // Exact sizes + verifyResize(jpeg, 512, 250, scalteType, 512, 250); + verifyResize(jpeg, 511, 249, scalteType, 511, 249); + verifyResize(jpeg, 1080, 500, scalteType, 1024, 500); + verifyResize(jpeg, 500, 500, scalteType, 500, 500); + + // Specify only width + verifyResize(jpeg, 512, 0, scalteType, 512, 500); + verifyResize(jpeg, 800, 0, scalteType, 800, 500); + verifyResize(jpeg, 1024, 0, scalteType, 1024, 500); + + // Specify only height + verifyResize(jpeg, 0, 250, scalteType, 1024, 250); + verifyResize(jpeg, 0, 391, scalteType, 1024, 391); + verifyResize(jpeg, 0, 500, scalteType, 1024, 500); + + // No resize + verifyResize(jpeg, 0, 0, scalteType, 1024, 500); + } + + private void verifyResize( + NetworkResponse networkResponse, + int maxWidth, + int maxHeight, + ScaleType scaleType, + int expectedWidth, + int expectedHeight) { + ImageRequest request = + new ImageRequest("", null, maxWidth, maxHeight, scaleType, Config.RGB_565, null); + Response response = request.parseNetworkResponse(networkResponse); + assertNotNull(response); + assertTrue(response.isSuccess()); + Bitmap bitmap = response.result; + assertNotNull(bitmap); + assertEquals(expectedWidth, bitmap.getWidth()); + assertEquals(expectedHeight, bitmap.getHeight()); + } + + @Test + public void findBestSampleSize() { + // desired == actual == 1 + assertEquals(1, ImageRequest.findBestSampleSize(100, 150, 100, 150)); + + // exactly half == 2 + assertEquals(2, ImageRequest.findBestSampleSize(280, 160, 140, 80)); + + // just over half == 1 + assertEquals(1, ImageRequest.findBestSampleSize(1000, 800, 501, 401)); + + // just under 1/4 == 4 + assertEquals(4, ImageRequest.findBestSampleSize(100, 200, 24, 50)); + } + + private static byte[] readInputStream(InputStream in) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int count; + while ((count = in.read(buffer)) != -1) { + bytes.write(buffer, 0, count); + } + in.close(); + return bytes.toByteArray(); + } + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull( + ImageRequest.class.getConstructor( + String.class, + Response.Listener.class, + int.class, + int.class, + Bitmap.Config.class, + Response.ErrorListener.class)); + assertNotNull( + ImageRequest.class.getConstructor( + String.class, + Response.Listener.class, + int.class, + int.class, + ImageView.ScaleType.class, + Bitmap.Config.class, + Response.ErrorListener.class)); + assertEquals(ImageRequest.DEFAULT_IMAGE_TIMEOUT_MS, 1000); + assertEquals(ImageRequest.DEFAULT_IMAGE_MAX_RETRIES, 2); + assertEquals(ImageRequest.DEFAULT_IMAGE_BACKOFF_MULT, 2f, 0); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java b/core/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java new file mode 100644 index 0000000..70bb2ea --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.android.volley.NetworkResponse; +import com.android.volley.Response; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class JsonRequestCharsetTest { + + /** String in Czech - "Retezec v cestine." */ + private static final String TEXT_VALUE = "\u0158et\u011bzec v \u010de\u0161tin\u011b."; + + private static final String TEXT_NAME = "text"; + private static final int TEXT_INDEX = 0; + + /** + * Copyright symbol has different encoding in utf-8 and ISO-8859-1, and it doesn't exists in + * ISO-8859-2 + */ + private static final String COPY_VALUE = "\u00a9"; + + private static final String COPY_NAME = "copyright"; + private static final int COPY_INDEX = 1; + + @Test + public void defaultCharsetJsonObject() throws Exception { + // UTF-8 is default charset for JSON + byte[] data = jsonObjectString().getBytes(Charset.forName("UTF-8")); + NetworkResponse network = new NetworkResponse(data); + JsonObjectRequest objectRequest = new JsonObjectRequest("", null, null, null); + Response objectResponse = objectRequest.parseNetworkResponse(network); + + assertNotNull(objectResponse); + assertTrue(objectResponse.isSuccess()); + assertEquals(TEXT_VALUE, objectResponse.result.getString(TEXT_NAME)); + assertEquals(COPY_VALUE, objectResponse.result.getString(COPY_NAME)); + } + + @Test + public void defaultCharsetJsonArray() throws Exception { + // UTF-8 is default charset for JSON + byte[] data = jsonArrayString().getBytes(Charset.forName("UTF-8")); + NetworkResponse network = new NetworkResponse(data); + JsonArrayRequest arrayRequest = new JsonArrayRequest("", null, null); + Response arrayResponse = arrayRequest.parseNetworkResponse(network); + + assertNotNull(arrayResponse); + assertTrue(arrayResponse.isSuccess()); + assertEquals(TEXT_VALUE, arrayResponse.result.getString(TEXT_INDEX)); + assertEquals(COPY_VALUE, arrayResponse.result.getString(COPY_INDEX)); + } + + @Test + public void specifiedCharsetJsonObject() throws Exception { + byte[] data = jsonObjectString().getBytes(Charset.forName("ISO-8859-1")); + Map headers = new HashMap(); + headers.put("Content-Type", "application/json; charset=iso-8859-1"); + NetworkResponse network = new NetworkResponse(data, headers); + JsonObjectRequest objectRequest = new JsonObjectRequest("", null, null, null); + Response objectResponse = objectRequest.parseNetworkResponse(network); + + assertNotNull(objectResponse); + assertTrue(objectResponse.isSuccess()); + // don't check the text in Czech, ISO-8859-1 doesn't support some Czech characters + assertEquals(COPY_VALUE, objectResponse.result.getString(COPY_NAME)); + } + + @Test + public void specifiedCharsetJsonArray() throws Exception { + byte[] data = jsonArrayString().getBytes(Charset.forName("ISO-8859-2")); + Map headers = new HashMap(); + headers.put("Content-Type", "application/json; charset=iso-8859-2"); + NetworkResponse network = new NetworkResponse(data, headers); + JsonArrayRequest arrayRequest = new JsonArrayRequest("", null, null); + Response arrayResponse = arrayRequest.parseNetworkResponse(network); + + assertNotNull(arrayResponse); + assertTrue(arrayResponse.isSuccess()); + assertEquals(TEXT_VALUE, arrayResponse.result.getString(TEXT_INDEX)); + // don't check the copyright symbol, ISO-8859-2 doesn't have it, but it has Czech characters + } + + private static String jsonObjectString() throws Exception { + JSONObject json = new JSONObject().put(TEXT_NAME, TEXT_VALUE).put(COPY_NAME, COPY_VALUE); + return json.toString(); + } + + private static String jsonArrayString() throws Exception { + JSONArray json = new JSONArray().put(TEXT_INDEX, TEXT_VALUE).put(COPY_INDEX, COPY_VALUE); + return json.toString(); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/JsonRequestTest.java b/core/src/test/java/com/android/volley/toolbox/JsonRequestTest.java new file mode 100644 index 0000000..44c0ad9 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/JsonRequestTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.junit.Assert.assertNotNull; + +import com.android.volley.Response; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class JsonRequestTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull( + JsonRequest.class.getConstructor( + String.class, + String.class, + Response.Listener.class, + Response.ErrorListener.class)); + assertNotNull( + JsonRequest.class.getConstructor( + int.class, + String.class, + String.class, + Response.Listener.class, + Response.ErrorListener.class)); + + assertNotNull( + JsonArrayRequest.class.getConstructor( + String.class, Response.Listener.class, Response.ErrorListener.class)); + assertNotNull( + JsonArrayRequest.class.getConstructor( + int.class, + String.class, + JSONArray.class, + Response.Listener.class, + Response.ErrorListener.class)); + + assertNotNull( + JsonObjectRequest.class.getConstructor( + String.class, + JSONObject.class, + Response.Listener.class, + Response.ErrorListener.class)); + assertNotNull( + JsonObjectRequest.class.getConstructor( + int.class, + String.class, + JSONObject.class, + Response.Listener.class, + Response.ErrorListener.class)); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java b/core/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java new file mode 100644 index 0000000..fd2073e --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.ViewGroup.LayoutParams; +import android.widget.ImageView.ScaleType; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class NetworkImageViewTest { + private NetworkImageView mNIV; + private MockImageLoader mMockImageLoader; + + @Before + public void setUp() throws Exception { + mMockImageLoader = new MockImageLoader(); + mNIV = new NetworkImageView(RuntimeEnvironment.application); + } + + @Test + public void setImageUrl_requestsImage() { + mNIV.setLayoutParams( + new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + mNIV.setImageUrl("http://foo", mMockImageLoader); + assertEquals("http://foo", mMockImageLoader.lastRequestUrl); + assertEquals(0, mMockImageLoader.lastMaxWidth); + assertEquals(0, mMockImageLoader.lastMaxHeight); + } + + // public void testSetImageUrl_setsMaxSize() { + // // TODO: Not sure how to make getWidth() return something from an + // // instrumentation test. Write this test once it's figured out. + // } + + private static class MockImageLoader extends ImageLoader { + public MockImageLoader() { + super(null, null); + } + + public String lastRequestUrl; + public int lastMaxWidth; + public int lastMaxHeight; + + @Override + public ImageContainer get( + String requestUrl, + ImageListener imageListener, + int maxWidth, + int maxHeight, + ScaleType scaleType) { + lastRequestUrl = requestUrl; + lastMaxWidth = maxWidth; + lastMaxHeight = maxHeight; + return null; + } + } + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(NetworkImageView.class.getConstructor(Context.class)); + assertNotNull(NetworkImageView.class.getConstructor(Context.class, AttributeSet.class)); + assertNotNull( + NetworkImageView.class.getConstructor( + Context.class, AttributeSet.class, int.class)); + + assertNotNull( + NetworkImageView.class.getMethod("setImageUrl", String.class, ImageLoader.class)); + assertNotNull(NetworkImageView.class.getMethod("setDefaultImageDrawable", Drawable.class)); + assertNotNull(NetworkImageView.class.getMethod("setDefaultImageBitmap", Bitmap.class)); + assertNotNull(NetworkImageView.class.getMethod("setDefaultImageResId", int.class)); + assertNotNull(NetworkImageView.class.getMethod("setErrorImageDrawable", Drawable.class)); + assertNotNull(NetworkImageView.class.getMethod("setErrorImageBitmap", Bitmap.class)); + assertNotNull(NetworkImageView.class.getMethod("setErrorImageResId", int.class)); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java b/core/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java new file mode 100644 index 0000000..266edcd --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.Arrays; +import org.junit.Test; + +public class PoolingByteArrayOutputStreamTest { + @Test + public void pooledOneBuffer() throws IOException { + ByteArrayPool pool = new ByteArrayPool(32768); + writeOneBuffer(pool); + writeOneBuffer(pool); + writeOneBuffer(pool); + } + + @Test + public void pooledIndividualWrites() throws IOException { + ByteArrayPool pool = new ByteArrayPool(32768); + writeBytesIndividually(pool); + writeBytesIndividually(pool); + writeBytesIndividually(pool); + } + + @Test + public void unpooled() throws IOException { + ByteArrayPool pool = new ByteArrayPool(0); + writeOneBuffer(pool); + writeOneBuffer(pool); + writeOneBuffer(pool); + } + + @Test + public void unpooledIndividualWrites() throws IOException { + ByteArrayPool pool = new ByteArrayPool(0); + writeBytesIndividually(pool); + writeBytesIndividually(pool); + writeBytesIndividually(pool); + } + + private void writeOneBuffer(ByteArrayPool pool) throws IOException { + byte[] data = new byte[16384]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (i & 0xff); + } + PoolingByteArrayOutputStream os = new PoolingByteArrayOutputStream(pool); + os.write(data); + + assertTrue(Arrays.equals(data, os.toByteArray())); + } + + private void writeBytesIndividually(ByteArrayPool pool) { + byte[] data = new byte[16384]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (i & 0xff); + } + PoolingByteArrayOutputStream os = new PoolingByteArrayOutputStream(pool); + for (int i = 0; i < data.length; i++) { + os.write(data[i]); + } + + assertTrue(Arrays.equals(data, os.toByteArray())); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/RequestFutureTest.java b/core/src/test/java/com/android/volley/toolbox/RequestFutureTest.java new file mode 100644 index 0000000..5b5c975 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/RequestFutureTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.junit.Assert.assertNotNull; + +import com.android.volley.Request; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class RequestFutureTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(RequestFuture.class.getMethod("newFuture")); + assertNotNull(RequestFuture.class.getMethod("setRequest", Request.class)); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/RequestQueueTest.java b/core/src/test/java/com/android/volley/toolbox/RequestQueueTest.java new file mode 100644 index 0000000..1899b71 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/RequestQueueTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.junit.Assert.assertNotNull; + +import com.android.volley.Cache; +import com.android.volley.Network; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.ResponseDelivery; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class RequestQueueTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull( + RequestQueue.class.getConstructor( + Cache.class, Network.class, int.class, ResponseDelivery.class)); + assertNotNull(RequestQueue.class.getConstructor(Cache.class, Network.class, int.class)); + assertNotNull(RequestQueue.class.getConstructor(Cache.class, Network.class)); + + assertNotNull(RequestQueue.class.getMethod("start")); + assertNotNull(RequestQueue.class.getMethod("stop")); + assertNotNull(RequestQueue.class.getMethod("getSequenceNumber")); + assertNotNull(RequestQueue.class.getMethod("getCache")); + assertNotNull(RequestQueue.class.getMethod("cancelAll", RequestQueue.RequestFilter.class)); + assertNotNull(RequestQueue.class.getMethod("cancelAll", Object.class)); + assertNotNull(RequestQueue.class.getMethod("add", Request.class)); + assertNotNull(RequestQueue.class.getDeclaredMethod("finish", Request.class)); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/RequestTest.java b/core/src/test/java/com/android/volley/toolbox/RequestTest.java new file mode 100644 index 0000000..0911ad6 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/RequestTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.junit.Assert.assertNotNull; + +import com.android.volley.Cache; +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.RetryPolicy; +import com.android.volley.VolleyError; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class RequestTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull( + Request.class.getConstructor( + int.class, String.class, Response.ErrorListener.class)); + + assertNotNull(Request.class.getMethod("getMethod")); + assertNotNull(Request.class.getMethod("setTag", Object.class)); + assertNotNull(Request.class.getMethod("getTag")); + assertNotNull(Request.class.getMethod("getErrorListener")); + assertNotNull(Request.class.getMethod("getTrafficStatsTag")); + assertNotNull(Request.class.getMethod("setRetryPolicy", RetryPolicy.class)); + assertNotNull(Request.class.getMethod("addMarker", String.class)); + assertNotNull(Request.class.getDeclaredMethod("finish", String.class)); + assertNotNull(Request.class.getMethod("setRequestQueue", RequestQueue.class)); + assertNotNull(Request.class.getMethod("setSequence", int.class)); + assertNotNull(Request.class.getMethod("getSequence")); + assertNotNull(Request.class.getMethod("getUrl")); + assertNotNull(Request.class.getMethod("getCacheKey")); + assertNotNull(Request.class.getMethod("setCacheEntry", Cache.Entry.class)); + assertNotNull(Request.class.getMethod("getCacheEntry")); + assertNotNull(Request.class.getMethod("cancel")); + assertNotNull(Request.class.getMethod("isCanceled")); + assertNotNull(Request.class.getMethod("getHeaders")); + assertNotNull(Request.class.getDeclaredMethod("getParams")); + assertNotNull(Request.class.getDeclaredMethod("getParamsEncoding")); + assertNotNull(Request.class.getMethod("getBodyContentType")); + assertNotNull(Request.class.getMethod("getBody")); + assertNotNull(Request.class.getMethod("setShouldCache", boolean.class)); + assertNotNull(Request.class.getMethod("shouldCache")); + assertNotNull(Request.class.getMethod("getPriority")); + assertNotNull(Request.class.getMethod("getTimeoutMs")); + assertNotNull(Request.class.getMethod("getRetryPolicy")); + assertNotNull(Request.class.getMethod("markDelivered")); + assertNotNull(Request.class.getMethod("hasHadResponseDelivered")); + assertNotNull( + Request.class.getDeclaredMethod("parseNetworkResponse", NetworkResponse.class)); + assertNotNull(Request.class.getDeclaredMethod("parseNetworkError", VolleyError.class)); + assertNotNull(Request.class.getDeclaredMethod("deliverResponse", Object.class)); + assertNotNull(Request.class.getMethod("deliverError", VolleyError.class)); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/ResponseTest.java b/core/src/test/java/com/android/volley/toolbox/ResponseTest.java new file mode 100644 index 0000000..44438fa --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/ResponseTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.junit.Assert.assertNotNull; + +import com.android.volley.Cache; +import com.android.volley.NetworkResponse; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ResponseTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(Response.class.getMethod("success", Object.class, Cache.Entry.class)); + assertNotNull(Response.class.getMethod("error", VolleyError.class)); + assertNotNull(Response.class.getMethod("isSuccess")); + + assertNotNull(Response.Listener.class.getDeclaredMethod("onResponse", Object.class)); + + assertNotNull( + Response.ErrorListener.class.getDeclaredMethod( + "onErrorResponse", VolleyError.class)); + + assertNotNull( + NetworkResponse.class.getConstructor( + int.class, byte[].class, Map.class, boolean.class, long.class)); + assertNotNull( + NetworkResponse.class.getConstructor( + int.class, byte[].class, Map.class, boolean.class)); + assertNotNull(NetworkResponse.class.getConstructor(byte[].class)); + assertNotNull(NetworkResponse.class.getConstructor(byte[].class, Map.class)); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/StringRequestTest.java b/core/src/test/java/com/android/volley/toolbox/StringRequestTest.java new file mode 100644 index 0000000..0ecb06b --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/StringRequestTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.android.volley.toolbox; + +import static org.junit.Assert.assertNotNull; + +import com.android.volley.Response; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class StringRequestTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull( + StringRequest.class.getConstructor( + String.class, Response.Listener.class, Response.ErrorListener.class)); + assertNotNull( + StringRequest.class.getConstructor( + int.class, + String.class, + Response.Listener.class, + Response.ErrorListener.class)); + } +} diff --git a/core/src/test/java/com/android/volley/utils/CacheTestUtils.java b/core/src/test/java/com/android/volley/utils/CacheTestUtils.java new file mode 100644 index 0000000..5980712 --- /dev/null +++ b/core/src/test/java/com/android/volley/utils/CacheTestUtils.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.android.volley.utils; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +import com.android.volley.Cache; +import java.util.Random; + +public class CacheTestUtils { + + /** + * Makes a random cache entry. + * + * @param data Data to use, or null to use random data + * @param isExpired Whether the TTLs should be set such that this entry is expired + * @param needsRefresh Whether the TTLs should be set such that this entry needs refresh + */ + public static Cache.Entry makeRandomCacheEntry( + byte[] data, boolean isExpired, boolean needsRefresh) { + Random random = new Random(); + Cache.Entry entry = new Cache.Entry(); + if (data != null) { + entry.data = data; + } else { + entry.data = new byte[random.nextInt(1024)]; + } + entry.etag = String.valueOf(random.nextLong()); + entry.lastModified = random.nextLong(); + entry.ttl = isExpired ? 0 : Long.MAX_VALUE; + entry.softTtl = needsRefresh ? 0 : Long.MAX_VALUE; + return entry; + } + + /** + * Like {@link #makeRandomCacheEntry(byte[], boolean, boolean)} but defaults to an unexpired + * entry. + */ + public static Cache.Entry makeRandomCacheEntry(byte[] data) { + return makeRandomCacheEntry(data, false, false); + } + + public static void assertThatEntriesAreEqual(Cache.Entry actual, Cache.Entry expected) { + assertNotNull(actual); + assertThat(actual.data, is(equalTo(expected.data))); + assertThat(actual.etag, is(equalTo(expected.etag))); + assertThat(actual.lastModified, is(equalTo(expected.lastModified))); + assertThat(actual.responseHeaders, is(equalTo(expected.responseHeaders))); + assertThat(actual.serverDate, is(equalTo(expected.serverDate))); + assertThat(actual.softTtl, is(equalTo(expected.softTtl))); + assertThat(actual.ttl, is(equalTo(expected.ttl))); + } + + public static Cache.Entry randomData(int length) { + Cache.Entry entry = new Cache.Entry(); + byte[] data = new byte[length]; + new Random(42).nextBytes(data); // explicit seed for reproducible results + entry.data = data; + return entry; + } + + public static int getEntrySizeOnDisk(String key) { + // Header size is: + // 4 bytes for magic int + // 8 + len(key) bytes for key (long length) + // 8 bytes for etag (long length + 0 characters) + // 32 bytes for serverDate, lastModified, ttl, and softTtl longs + // 4 bytes for length of header list int + // == 56 + len(key) bytes total. + return 56 + key.length(); + } +} diff --git a/core/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java b/core/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java new file mode 100644 index 0000000..67e5923 --- /dev/null +++ b/core/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.android.volley.utils; + +import com.android.volley.ExecutorDelivery; +import java.util.concurrent.Executor; + +/** + * A ResponseDelivery for testing that immediately delivers responses instead of posting back to the + * main thread. + */ +public class ImmediateResponseDelivery extends ExecutorDelivery { + + public ImmediateResponseDelivery() { + super( + new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }); + } +} -- cgit v1.2.3