/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.stetho.okhttp;
import android.net.Uri;
import android.os.Build;
import com.facebook.stetho.inspector.network.DecompressionHelper;
import com.facebook.stetho.inspector.network.NetworkEventReporter;
import com.facebook.stetho.inspector.network.NetworkEventReporterImpl;
import com.facebook.stetho.inspector.network.ResponseHandler;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
import okio.Buffer;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.rule.PowerMockRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import javax.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.GZIPOutputStream;
import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
@Config(emulateSdk = Build.VERSION_CODES.JELLY_BEAN)
@RunWith(RobolectricTestRunner.class)
@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*", "javax.net.ssl.*" })
@PrepareForTest(NetworkEventReporterImpl.class)
public class StethoInterceptorTest {
@Rule
public PowerMockRule rule = new PowerMockRule();
private NetworkEventReporter mMockEventReporter;
private StethoInterceptor mInterceptor;
private OkHttpClient mClientWithInterceptor;
@Before
public void setUp() {
PowerMockito.mockStatic(NetworkEventReporterImpl.class);
mMockEventReporter = Mockito.mock(NetworkEventReporter.class);
Mockito.when(mMockEventReporter.isEnabled()).thenReturn(true);
PowerMockito.when(NetworkEventReporterImpl.get()).thenReturn(mMockEventReporter);
mInterceptor = new StethoInterceptor();
mClientWithInterceptor = new OkHttpClient();
mClientWithInterceptor.networkInterceptors().add(mInterceptor);
}
@Test
public void testHappyPath() throws IOException {
InOrder inOrder = Mockito.inOrder(mMockEventReporter);
hookAlmostRealRequestWillBeSent(mMockEventReporter);
ByteArrayOutputStream capturedOutput =
hookAlmostRealInterpretResponseStream(mMockEventReporter);
Uri requestUri = Uri.parse("http://www.facebook.com/nowhere");
String requestText = "Test input";
Request request = new Request.Builder()
.url(requestUri.toString())
.method(
"POST",
RequestBody.create(MediaType.parse("text/plain"), requestText))
.build();
String originalBodyData = "Success!";
Response reply = new Response.Builder()
.request(request)
.protocol(Protocol.HTTP_1_1)
.code(200)
.body(ResponseBody.create(MediaType.parse("text/plain"), originalBodyData))
.build();
Response filteredResponse =
mInterceptor.intercept(
new SimpleTestChain(request, reply, null));
inOrder.verify(mMockEventReporter).isEnabled();
inOrder.verify(mMockEventReporter)
.requestWillBeSent(any(NetworkEventReporter.InspectorRequest.class));
inOrder.verify(mMockEventReporter)
.dataSent(
anyString(),
eq(requestText.length()),
eq(requestText.length()));
inOrder.verify(mMockEventReporter)
.responseHeadersReceived(any(NetworkEventReporter.InspectorResponse.class));
String filteredResponseString = filteredResponse.body().string();
String interceptedOutput = capturedOutput.toString();
inOrder.verify(mMockEventReporter).dataReceived(anyString(), anyInt(), anyInt());
inOrder.verify(mMockEventReporter).responseReadFinished(anyString());
assertEquals(originalBodyData, filteredResponseString);
assertEquals(originalBodyData, interceptedOutput);
inOrder.verifyNoMoreInteractions();
}
@Test
public void testWithRequestCompression() throws IOException {
AtomicReference<NetworkEventReporter.InspectorRequest> capturedRequest =
hookAlmostRealRequestWillBeSent(mMockEventReporter);
MockWebServer server = new MockWebServer();
server.start();
server.enqueue(new MockResponse()
.setBody("Success!"));
final byte[] decompressed = "Request text".getBytes();
final byte[] compressed = compress(decompressed);
assertNotEquals(
"Bogus test: decompressed and compressed lengths match",
compressed.length, decompressed.length);
RequestBody compressedBody = RequestBody.create(
MediaType.parse("text/plain"),
compress(decompressed));
Request request = new Request.Builder()
.url(server.getUrl("/"))
.addHeader("Content-Encoding", "gzip")
.post(compressedBody)
.build();
Response response = mClientWithInterceptor.newCall(request).execute();
// Force a read to complete the flow.
response.body().string();
assertArrayEquals(decompressed, capturedRequest.get().body());
Mockito.verify(mMockEventReporter)
.dataSent(
anyString(),
eq(decompressed.length),
eq(compressed.length));
server.shutdown();
}
@Test
public void testWithResponseCompression() throws IOException {
ByteArrayOutputStream capturedOutput = hookAlmostRealInterpretResponseStream(mMockEventReporter);
byte[] uncompressedData = repeat(".", 1024).getBytes();
byte[] compressedData = compress(uncompressedData);
MockWebServer server = new MockWebServer();
server.start();
server.enqueue(new MockResponse()
.setBody(new Buffer().write(compressedData))
.addHeader("Content-Encoding: gzip"));
Request request = new Request.Builder()
.url(server.url("/"))
.build();
Response response = mClientWithInterceptor.newCall(request).execute();
// Verify that the final output and the caller both saw the uncompressed stream.
assertArrayEquals(uncompressedData, response.body().bytes());
assertArrayEquals(uncompressedData, capturedOutput.toByteArray());
// And verify that the StethoInterceptor was able to see both.
Mockito.verify(mMockEventReporter)
.dataReceived(
anyString(),
eq(compressedData.length),
eq(uncompressedData.length));
server.shutdown();
}
private static String repeat(String s, int reps) {
StringBuilder b = new StringBuilder(s.length() * reps);
while (reps-- > 0) {
b.append(s);
}
return b.toString();
}
private static byte[] compress(byte[] data) throws IOException {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
GZIPOutputStream out = new GZIPOutputStream(buf);
out.write(data);
out.close();
return buf.toByteArray();
}
private static AtomicReference<NetworkEventReporter.InspectorRequest>
hookAlmostRealRequestWillBeSent(
final NetworkEventReporter mockEventReporter) {
final AtomicReference<NetworkEventReporter.InspectorRequest> capturedRequest =
new AtomicReference<>(null);
Mockito.doAnswer(
new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
NetworkEventReporter.InspectorRequest request =
(NetworkEventReporter.InspectorRequest)args[0];
capturedRequest.set(request);
// Access the body, causing the body helper to perform decompression...
request.body();
return null;
}
})
.when(mockEventReporter)
.requestWillBeSent(
any(NetworkEventReporter.InspectorRequest.class));
return capturedRequest;
}
/**
* Provide a suitably "real" implementation of
* {@link NetworkEventReporter#interpretResponseStream} for our mock to test that
* events are properly delegated.
*/
private static ByteArrayOutputStream hookAlmostRealInterpretResponseStream(
final NetworkEventReporter mockEventReporter) {
final ByteArrayOutputStream capturedOutput = new ByteArrayOutputStream();
Mockito.when(
mockEventReporter.interpretResponseStream(
anyString(),
anyString(),
anyString(),
any(InputStream.class),
any(ResponseHandler.class)))
.thenAnswer(
new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock invocationOnMock) throws Throwable {
Object[] args = invocationOnMock.getArguments();
String requestId = (String)args[0];
String contentEncoding = (String)args[2];
InputStream responseStream = (InputStream)args[3];
ResponseHandler responseHandler = (ResponseHandler)args[4];
return DecompressionHelper.teeInputWithDecompression(
null /* networkPeerManager */,
requestId,
responseStream,
capturedOutput,
contentEncoding,
responseHandler);
}
});
return capturedOutput;
}
private static class SimpleTestChain implements Interceptor.Chain {
private final Request mRequest;
private final Response mResponse;
@Nullable private final Connection mConnection;
public SimpleTestChain(Request request, Response response, @Nullable Connection connection) {
mRequest = request;
mResponse = response;
mConnection = connection;
}
@Override
public Request request() {
return mRequest;
}
@Override
public Response proceed(Request request) throws IOException {
if (mRequest != request) {
throw new IllegalArgumentException(
"Expected " + System.identityHashCode(mRequest) +
"; got " + System.identityHashCode(request));
}
return mResponse;
}
@Override
public Connection connection() {
return mConnection;
}
}
}