diff options
13 files changed, 387 insertions, 31 deletions
diff --git a/src/main/java/com/android/volley/CacheDispatcher.java b/src/main/java/com/android/volley/CacheDispatcher.java index 4ea8a0b..f616285 100644 --- a/src/main/java/com/android/volley/CacheDispatcher.java +++ b/src/main/java/com/android/volley/CacheDispatcher.java @@ -98,8 +98,12 @@ public class CacheDispatcher extends Thread { } catch (InterruptedException e) { // We may have been interrupted because it was time to quit. if (mQuit) { + Thread.currentThread().interrupt(); return; } + VolleyLog.e( + "Ignoring spurious interrupt of CacheDispatcher thread; " + + "use quit() to terminate it"); } } } diff --git a/src/main/java/com/android/volley/NetworkDispatcher.java b/src/main/java/com/android/volley/NetworkDispatcher.java index 6e47465..327afed 100644 --- a/src/main/java/com/android/volley/NetworkDispatcher.java +++ b/src/main/java/com/android/volley/NetworkDispatcher.java @@ -91,8 +91,12 @@ public class NetworkDispatcher extends Thread { } catch (InterruptedException e) { // We may have been interrupted because it was time to quit. if (mQuit) { + Thread.currentThread().interrupt(); return; } + VolleyLog.e( + "Ignoring spurious interrupt of NetworkDispatcher thread; " + + "use quit() to terminate it"); } } } diff --git a/src/main/java/com/android/volley/Request.java b/src/main/java/com/android/volley/Request.java index c958088..cd7290a 100644 --- a/src/main/java/com/android/volley/Request.java +++ b/src/main/java/com/android/volley/Request.java @@ -22,6 +22,7 @@ import android.os.Handler; import android.os.Looper; import android.support.annotation.CallSuper; import android.support.annotation.GuardedBy; +import android.support.annotation.Nullable; import android.text.TextUtils; import com.android.volley.VolleyLog.MarkerLog; import java.io.UnsupportedEncodingException; @@ -81,6 +82,7 @@ public abstract class Request<T> implements Comparable<Request<T>> { private final Object mLock = new Object(); /** Listener interface for errors. */ + @Nullable @GuardedBy("mLock") private Response.ErrorListener mErrorListener; @@ -91,6 +93,7 @@ public abstract class Request<T> implements Comparable<Request<T>> { private RequestQueue mRequestQueue; /** Whether or not responses to this request should be cached. */ + // TODO(#190): Turn this off by default for anything other than GET requests. private boolean mShouldCache = true; /** Whether or not this request has been canceled. */ @@ -139,7 +142,7 @@ public abstract class Request<T> implements Comparable<Request<T>> { * responses is provided by subclasses, who have a better idea of how to deliver an * already-parsed response. */ - public Request(int method, String url, Response.ErrorListener listener) { + public Request(int method, String url, @Nullable Response.ErrorListener listener) { mMethod = method; mUrl = url; mErrorListener = listener; @@ -174,6 +177,7 @@ public abstract class Request<T> implements Comparable<Request<T>> { } /** @return this request's {@link com.android.volley.Response.ErrorListener}. */ + @Nullable public Response.ErrorListener getErrorListener() { synchronized (mLock) { return mErrorListener; @@ -283,7 +287,18 @@ public abstract class Request<T> implements Comparable<Request<T>> { /** Returns the cache key for this request. By default, this is the URL. */ public String getCacheKey() { - return getUrl(); + String url = getUrl(); + // If this is a GET request, just use the URL as the key. + // For callers using DEPRECATED_GET_OR_POST, we assume the method is GET, which matches + // legacy behavior where all methods had the same cache key. We can't determine which method + // will be used because doing so requires calling getPostBody() which is expensive and may + // throw AuthFailureError. + // TODO(#190): Remove support for non-GET methods. + int method = getMethod(); + if (method == Method.GET || method == Method.DEPRECATED_GET_OR_POST) { + return url; + } + return Integer.toString(method) + '-' + url; } /** @@ -458,6 +473,14 @@ public abstract class Request<T> implements Comparable<Request<T>> { StringBuilder encodedParams = new StringBuilder(); try { for (Map.Entry<String, String> entry : params.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) { + throw new IllegalArgumentException( + String.format( + "Request#getParams() or Request#getPostParams() returned a map " + + "containing a null key or value: (%s, %s). All keys " + + "and values must be non-null.", + entry.getKey(), entry.getValue())); + } encodedParams.append(URLEncoder.encode(entry.getKey(), paramsEncoding)); encodedParams.append('='); encodedParams.append(URLEncoder.encode(entry.getValue(), paramsEncoding)); @@ -523,7 +546,7 @@ public abstract class Request<T> implements Comparable<Request<T>> { * remaining, this will cause delivery of a {@link TimeoutError} error. */ public final int getTimeoutMs() { - return mRetryPolicy.getCurrentTimeout(); + return getRetryPolicy().getCurrentTimeout(); } /** Returns the retry policy that should be used for this request. */ diff --git a/src/main/java/com/android/volley/RetryPolicy.java b/src/main/java/com/android/volley/RetryPolicy.java index aa6af43..3ef26de 100644 --- a/src/main/java/com/android/volley/RetryPolicy.java +++ b/src/main/java/com/android/volley/RetryPolicy.java @@ -16,7 +16,27 @@ package com.android.volley; -/** Retry policy for a request. */ +/** + * Retry policy for a request. + * + * <p>A retry policy can control two parameters: + * + * <ul> + * <li>The number of tries. This can be a simple counter or more complex logic based on the type + * of error passed to {@link #retry(VolleyError)}, although {@link #getCurrentRetryCount()} + * should always return the current retry count for logging purposes. + * <li>The request timeout for each try, via {@link #getCurrentTimeout()}. In the common case that + * a request times out before the response has been received from the server, retrying again + * with a longer timeout can increase the likelihood of success (at the expense of causing the + * user to wait longer, especially if the request still fails). + * </ul> + * + * <p>Note that currently, retries triggered by a retry policy are attempted immediately in sequence + * with no delay between them (although the time between tries may increase if the requests are + * timing out and {@link #getCurrentTimeout()} is returning increasing values). + * + * <p>By default, Volley uses {@link DefaultRetryPolicy}. + */ public interface RetryPolicy { /** Returns the current timeout (used for logging). */ diff --git a/src/main/java/com/android/volley/toolbox/HttpClientStack.java b/src/main/java/com/android/volley/toolbox/HttpClientStack.java index be0918a..1e9e4b0 100644 --- a/src/main/java/com/android/volley/toolbox/HttpClientStack.java +++ b/src/main/java/com/android/volley/toolbox/HttpClientStack.java @@ -58,7 +58,7 @@ public class HttpClientStack implements HttpStack { mClient = client; } - private static void addHeaders(HttpUriRequest httpRequest, Map<String, String> headers) { + private static void setHeaders(HttpUriRequest httpRequest, Map<String, String> headers) { for (String key : headers.keySet()) { httpRequest.setHeader(key, headers.get(key)); } @@ -77,8 +77,10 @@ public class HttpClientStack implements HttpStack { public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders) throws IOException, AuthFailureError { HttpUriRequest httpRequest = createHttpRequest(request, additionalHeaders); - addHeaders(httpRequest, additionalHeaders); - addHeaders(httpRequest, request.getHeaders()); + setHeaders(httpRequest, additionalHeaders); + // Request.getHeaders() takes precedence over the given additional (cache) headers) and any + // headers set by createHttpRequest (like the Content-Type header). + setHeaders(httpRequest, request.getHeaders()); onPrepareRequest(httpRequest); HttpParams httpParams = httpRequest.getParams(); int timeoutMs = request.getTimeoutMs(); diff --git a/src/main/java/com/android/volley/toolbox/HurlStack.java b/src/main/java/com/android/volley/toolbox/HurlStack.java index dd73759..5af18ef 100644 --- a/src/main/java/com/android/volley/toolbox/HurlStack.java +++ b/src/main/java/com/android/volley/toolbox/HurlStack.java @@ -74,8 +74,9 @@ public class HurlStack extends BaseHttpStack { throws IOException, AuthFailureError { String url = request.getUrl(); HashMap<String, String> map = new HashMap<>(); - map.putAll(request.getHeaders()); map.putAll(additionalHeaders); + // Request.getHeaders() takes precedence over the given additional (cache) headers). + map.putAll(request.getHeaders()); if (mUrlRewriter != null) { String rewritten = mUrlRewriter.rewriteUrl(url); if (rewritten == null) { @@ -88,7 +89,7 @@ public class HurlStack extends BaseHttpStack { boolean keepConnectionOpen = false; try { for (String headerName : map.keySet()) { - connection.addRequestProperty(headerName, map.get(headerName)); + connection.setRequestProperty(headerName, map.get(headerName)); } setConnectionParametersForRequest(connection, request); // Initialize HttpResponse with data from the HttpURLConnection. @@ -219,6 +220,8 @@ public class HurlStack extends BaseHttpStack { return connection; } + // NOTE: Any request headers added here (via setRequestProperty or addRequestProperty) should be + // checked against the existing properties in the connection and not overridden if already set. @SuppressWarnings("deprecation") /* package */ static void setConnectionParametersForRequest( HttpURLConnection connection, Request<?> request) throws IOException, AuthFailureError { @@ -276,13 +279,16 @@ public class HurlStack extends BaseHttpStack { } private static void addBody(HttpURLConnection connection, Request<?> request, byte[] body) - throws IOException, AuthFailureError { + throws IOException { // Prepare output. There is no need to set Content-Length explicitly, // since this is handled by HttpURLConnection using the size of the prepared // output stream. connection.setDoOutput(true); - connection.addRequestProperty( - HttpHeaderParser.HEADER_CONTENT_TYPE, request.getBodyContentType()); + // Set the content-type unless it was already set (by Request#getHeaders). + if (!connection.getRequestProperties().containsKey(HttpHeaderParser.HEADER_CONTENT_TYPE)) { + connection.setRequestProperty( + HttpHeaderParser.HEADER_CONTENT_TYPE, request.getBodyContentType()); + } DataOutputStream out = new DataOutputStream(connection.getOutputStream()); out.write(body); out.close(); diff --git a/src/main/java/com/android/volley/toolbox/ImageRequest.java b/src/main/java/com/android/volley/toolbox/ImageRequest.java index c804267..59e468f 100644 --- a/src/main/java/com/android/volley/toolbox/ImageRequest.java +++ b/src/main/java/com/android/volley/toolbox/ImageRequest.java @@ -20,6 +20,7 @@ import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.support.annotation.GuardedBy; +import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.widget.ImageView.ScaleType; import com.android.volley.DefaultRetryPolicy; @@ -44,6 +45,7 @@ public class ImageRequest extends Request<Bitmap> { private final Object mLock = new Object(); @GuardedBy("mLock") + @Nullable private Response.Listener<Bitmap> mListener; private final Config mDecodeConfig; @@ -76,7 +78,7 @@ public class ImageRequest extends Request<Bitmap> { int maxHeight, ScaleType scaleType, Config decodeConfig, - Response.ErrorListener errorListener) { + @Nullable Response.ErrorListener errorListener) { super(Method.GET, url, errorListener); setRetryPolicy( new DefaultRetryPolicy( diff --git a/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java b/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java index 757c7f9..1abaec7 100644 --- a/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java +++ b/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java @@ -16,6 +16,7 @@ package com.android.volley.toolbox; +import android.support.annotation.Nullable; import com.android.volley.NetworkResponse; import com.android.volley.ParseError; import com.android.volley.Response; @@ -35,7 +36,8 @@ public class JsonArrayRequest extends JsonRequest<JSONArray> { * @param listener Listener to receive the JSON response * @param errorListener Error listener, or null to ignore errors. */ - public JsonArrayRequest(String url, Listener<JSONArray> listener, ErrorListener errorListener) { + public JsonArrayRequest( + String url, Listener<JSONArray> listener, @Nullable ErrorListener errorListener) { super(Method.GET, url, null, listener, errorListener); } @@ -44,17 +46,17 @@ public class JsonArrayRequest extends JsonRequest<JSONArray> { * * @param method the HTTP method to use * @param url URL to fetch the JSON from - * @param jsonRequest A {@link JSONArray} to post with the request. Null is allowed and - * indicates no parameters will be posted along with request. + * @param jsonRequest A {@link JSONArray} to post with the request. Null indicates no parameters + * will be posted along with request. * @param listener Listener to receive the JSON response * @param errorListener Error listener, or null to ignore errors. */ public JsonArrayRequest( int method, String url, - JSONArray jsonRequest, + @Nullable JSONArray jsonRequest, Listener<JSONArray> listener, - ErrorListener errorListener) { + @Nullable ErrorListener errorListener) { super( method, url, diff --git a/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java b/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java index e9dc3d7..cee5efe 100644 --- a/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java +++ b/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java @@ -16,6 +16,7 @@ package com.android.volley.toolbox; +import android.support.annotation.Nullable; import com.android.volley.NetworkResponse; import com.android.volley.ParseError; import com.android.volley.Response; @@ -36,17 +37,17 @@ public class JsonObjectRequest extends JsonRequest<JSONObject> { * * @param method the HTTP method to use * @param url URL to fetch the JSON from - * @param jsonRequest A {@link JSONObject} to post with the request. Null is allowed and - * indicates no parameters will be posted along with request. + * @param jsonRequest A {@link JSONObject} to post with the request. Null indicates no + * parameters will be posted along with request. * @param listener Listener to receive the JSON response * @param errorListener Error listener, or null to ignore errors. */ public JsonObjectRequest( int method, String url, - JSONObject jsonRequest, + @Nullable JSONObject jsonRequest, Listener<JSONObject> listener, - ErrorListener errorListener) { + @Nullable ErrorListener errorListener) { super( method, url, @@ -63,9 +64,9 @@ public class JsonObjectRequest extends JsonRequest<JSONObject> { */ public JsonObjectRequest( String url, - JSONObject jsonRequest, + @Nullable JSONObject jsonRequest, Listener<JSONObject> listener, - ErrorListener errorListener) { + @Nullable ErrorListener errorListener) { this( jsonRequest == null ? Method.GET : Method.POST, url, diff --git a/src/main/java/com/android/volley/toolbox/JsonRequest.java b/src/main/java/com/android/volley/toolbox/JsonRequest.java index fd395dd..c00d3db 100644 --- a/src/main/java/com/android/volley/toolbox/JsonRequest.java +++ b/src/main/java/com/android/volley/toolbox/JsonRequest.java @@ -17,6 +17,7 @@ package com.android.volley.toolbox; import android.support.annotation.GuardedBy; +import android.support.annotation.Nullable; import com.android.volley.NetworkResponse; import com.android.volley.Request; import com.android.volley.Response; @@ -42,10 +43,11 @@ public abstract class JsonRequest<T> extends Request<T> { /** Lock to guard mListener as it is cleared on cancel() and read on delivery. */ private final Object mLock = new Object(); + @Nullable @GuardedBy("mLock") private Listener<T> mListener; - private final String mRequestBody; + @Nullable private final String mRequestBody; /** * Deprecated constructor for a JsonRequest which defaults to GET unless {@link #getPostBody()} @@ -62,9 +64,9 @@ public abstract class JsonRequest<T> extends Request<T> { public JsonRequest( int method, String url, - String requestBody, + @Nullable String requestBody, Listener<T> listener, - ErrorListener errorListener) { + @Nullable ErrorListener errorListener) { super(method, url, errorListener); mListener = listener; mRequestBody = requestBody; diff --git a/src/main/java/com/android/volley/toolbox/StringRequest.java b/src/main/java/com/android/volley/toolbox/StringRequest.java index 0fbab14..c4c89b5 100644 --- a/src/main/java/com/android/volley/toolbox/StringRequest.java +++ b/src/main/java/com/android/volley/toolbox/StringRequest.java @@ -17,6 +17,7 @@ package com.android.volley.toolbox; import android.support.annotation.GuardedBy; +import android.support.annotation.Nullable; import com.android.volley.NetworkResponse; import com.android.volley.Request; import com.android.volley.Response; @@ -30,6 +31,7 @@ public class StringRequest extends Request<String> { /** Lock to guard mListener as it is cleared on cancel() and read on delivery. */ private final Object mLock = new Object(); + @Nullable @GuardedBy("mLock") private Listener<String> mListener; @@ -42,7 +44,10 @@ public class StringRequest extends Request<String> { * @param errorListener Error listener, or null to ignore errors */ public StringRequest( - int method, String url, Listener<String> listener, ErrorListener errorListener) { + int method, + String url, + Listener<String> listener, + @Nullable ErrorListener errorListener) { super(method, url, errorListener); mListener = listener; } @@ -54,7 +59,8 @@ public class StringRequest extends Request<String> { * @param listener Listener to receive the String response * @param errorListener Error listener, or null to ignore errors */ - public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) { + public StringRequest( + String url, Listener<String> listener, @Nullable ErrorListener errorListener) { this(Method.GET, url, listener, errorListener); } diff --git a/src/test/java/com/android/volley/RequestTest.java b/src/test/java/com/android/volley/RequestTest.java index e2dd655..382d9da 100644 --- a/src/test/java/com/android/volley/RequestTest.java +++ b/src/test/java/com/android/volley/RequestTest.java @@ -20,7 +20,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import com.android.volley.Request.Method; import com.android.volley.Request.Priority; +import java.util.Collections; +import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -84,9 +87,30 @@ public class RequestTest { 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<Object> { - public UrlParseRequest(String url) { - super(Request.Method.GET, url, null); + UrlParseRequest(String url) { + this(Method.GET, url); + } + + UrlParseRequest(int method, String url) { + super(method, url, null); } @Override @@ -97,4 +121,72 @@ public class RequestTest { return null; } } + + @Test + public void nullKeyInPostParams() throws Exception { + Request<Object> request = + new Request<Object>(Method.POST, "url", null) { + @Override + protected void deliverResponse(Object response) {} + + @Override + protected Response<Object> parseNetworkResponse(NetworkResponse response) { + return null; + } + + @Override + protected Map<String, String> getParams() { + return Collections.singletonMap(null, "value"); + } + + @Override + protected Map<String, String> 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<Object> request = + new Request<Object>(Method.POST, "url", null) { + @Override + protected void deliverResponse(Object response) {} + + @Override + protected Response<Object> parseNetworkResponse(NetworkResponse response) { + return null; + } + + @Override + protected Map<String, String> getParams() { + return Collections.singletonMap("key", null); + } + + @Override + protected Map<String, String> getPostParams() { + return Collections.singletonMap("key", null); + } + }; + try { + request.getBody(); + } catch (IllegalArgumentException e) { + // expected + } + try { + request.getPostBody(); + } catch (IllegalArgumentException e) { + // expected + } + } } diff --git a/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java b/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java new file mode 100644 index 0000000..6794af8 --- /dev/null +++ b/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<String, String> outputHeaderMap) { + doAnswer( + new Answer<Void>() { + @Override + public Void answer(InvocationOnMock invocation) { + outputHeaderMap.put( + invocation.<String>getArgument(0), + invocation.<String>getArgument(1)); + return null; + } + }) + .when(mMockConnection) + .setRequestProperty(anyString(), anyString()); + doAnswer( + new Answer<Map<String, List<String>>>() { + @Override + public Map<String, List<String>> answer( + InvocationOnMock invocation) { + Map<String, List<String>> result = new HashMap<>(); + for (Map.Entry<String, String> 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<String, String> outputHeaderMap) { + try { + doAnswer( + new Answer<Void>() { + @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<String, String> additionalHeaders = new HashMap<>(); + additionalHeaders.put("A", "AddlA"); + additionalHeaders.put("B", "AddlB"); + + Map<String, String> 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<String, String> combinedHeaders = new HashMap<>(); + testCase.setOutputHeaderMap(combinedHeaders); + + testCase.getStack().performRequest(mMockRequest, additionalHeaders); + + Map<String, String> 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<String, String> outputHeaderMap); + } +} |