(Client) Network Response Mocking Logic for Android Clients (Original PR) (#488)

Summary:
Add logic on client side

# How it works (from the code)
1. Server side sends request url and method to response data and headers to client side
   1.1. This will happen every time server update **any** mock response (add, edit, and remove)
2. Client stores those in map
3. For every network request,
   3.1. Check if there is a matching url and method
   3.2. If so, create a new response with the data and headers and drop the request
   3.3. If not, proceed and send the request and wait for a response

`addNetworkInterceptor` is changed to `addInterceptor` to allow short-circuit and proceed without fetching anything. More info can be found at https://square.github.io/okhttp/interceptors/

Note:
- This is an original PR.
- The content below is from original PR

Add network response mocking for Network plugin. See discussion [here](https://github.com/facebook/flipper/issues/475)

## Changelog
- Add Network response mocking, currently support Android clients only
- Change the Android example app to use `addInterceptor()` instead of `addNetworkInterceptor()`
Pull Request resolved: https://github.com/facebook/flipper/pull/488

Test Plan:
{F231673798}

![60549983-187ce800-9d59-11e9-8f7a-4b1b6402653d](https://user-images.githubusercontent.com/410850/61124971-0c242800-a4db-11e9-8e11-8a0a45bbb621.gif)

- Connect an Android device
- Tap on Network plugin
- Click on the Mock button
- Click on Add Route button, and specify the URL
- Edit the mock data in the text area
- Optionally, click the Headers tab to edit the headers data
- Click close button to close the dialog
- Send some network data in your application. You should be able to see the mock data appears in the Network table in those rows highlighted in yellow

Reviewed By: passy

Differential Revision: D16580291

Pulled By: cekkaewnumchai

fbshipit-source-id: fc391f5e7efebc6f51a72b00d16263e009e1fdb0
This commit is contained in:
Qichuan (Sean) ZHANG
2020-03-23 21:57:08 -07:00
committed by Facebook GitHub Bot
parent 4ea1497387
commit d27e45d7bb
2 changed files with 147 additions and 4 deletions

View File

@@ -7,23 +7,37 @@
package com.facebook.flipper.plugins.network;
import android.text.TextUtils;
import android.util.Pair;
import com.facebook.flipper.core.FlipperArray;
import com.facebook.flipper.core.FlipperConnection;
import com.facebook.flipper.core.FlipperObject;
import com.facebook.flipper.core.FlipperReceiver;
import com.facebook.flipper.core.FlipperResponder;
import com.facebook.flipper.plugins.common.BufferingFlipperPlugin;
import com.facebook.flipper.plugins.network.NetworkReporter.RequestInfo;
import com.facebook.flipper.plugins.network.NetworkReporter.ResponseInfo;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.annotation.Nullable;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSource;
public class FlipperOkhttpInterceptor implements Interceptor {
public class FlipperOkhttpInterceptor
implements Interceptor, BufferingFlipperPlugin.MockResponseConnectionListener {
// By default, limit body size (request or response) reporting to 100KB to avoid OOM
private static final long DEFAULT_MAX_BODY_BYTES = 100 * 1024;
@@ -32,6 +46,16 @@ public class FlipperOkhttpInterceptor implements Interceptor {
public @Nullable NetworkFlipperPlugin plugin;
private static class PartialRequestInfo extends Pair<String, String> {
PartialRequestInfo(String url, String method) {
super(url, method);
}
}
// pair of request url and method
private Map<PartialRequestInfo, ResponseInfo> mMockResponseMap = new HashMap<>(0);
private boolean mIsMockResponseSupported = false;
public FlipperOkhttpInterceptor() {
this.plugin = null;
}
@@ -46,14 +70,41 @@ public class FlipperOkhttpInterceptor implements Interceptor {
this.maxBodyBytes = maxBodyBytes;
}
/**
* To support mock response, addIntercept must be used (instead of addNetworkIntercept) to allow
* short circuit: https://square.github.io/okhttp/interceptors/ *
*/
public FlipperOkhttpInterceptor(NetworkFlipperPlugin plugin, boolean isMockResponseSupported) {
this.plugin = plugin;
mIsMockResponseSupported = isMockResponseSupported;
if (isMockResponseSupported) {
this.plugin.setConnectionListener(this);
}
}
public FlipperOkhttpInterceptor(
NetworkFlipperPlugin plugin, long maxBodyBytes, boolean isMockResponseSupported) {
this.plugin = plugin;
this.maxBodyBytes = maxBodyBytes;
mIsMockResponseSupported = isMockResponseSupported;
if (isMockResponseSupported) {
this.plugin.setConnectionListener(this);
}
}
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
String identifier = UUID.randomUUID().toString();
plugin.reportRequest(convertRequest(request, identifier));
Response response = chain.proceed(request);
// Check if there is a mock response
Response mockResponse = mIsMockResponseSupported ? getMockResponse(request) : null;
Response response = mockResponse != null ? mockResponse : chain.proceed(request);
ResponseBody body = response.body();
ResponseInfo responseInfo = convertResponse(response, body, identifier);
responseInfo.isMock = mockResponse != null;
plugin.reportResponse(responseInfo);
return response;
}
@@ -104,4 +155,96 @@ public class FlipperOkhttpInterceptor implements Interceptor {
}
return list;
}
private void registerMockResponse(PartialRequestInfo partialRequest, ResponseInfo response) {
if (!mMockResponseMap.containsKey(partialRequest)) {
mMockResponseMap.put(partialRequest, response);
}
}
@Nullable
private Response getMockResponse(Request request) {
final String url = request.url().toString();
final String method = request.method();
final PartialRequestInfo partialRequest = new PartialRequestInfo(url, method);
if (!mMockResponseMap.containsKey(partialRequest)) {
return null;
}
ResponseInfo mockResponse = mMockResponseMap.get(partialRequest);
if (mockResponse == null) {
return null;
}
final Response.Builder builder = new Response.Builder();
builder
.request(request)
.protocol(Protocol.HTTP_1_1)
.code(mockResponse.statusCode)
.message(mockResponse.statusReason)
.receivedResponseAtMillis(System.currentTimeMillis())
.body(ResponseBody.create(MediaType.parse("application/text"), mockResponse.body));
if (mockResponse.headers != null && !mockResponse.headers.isEmpty()) {
for (final NetworkReporter.Header header : mockResponse.headers) {
if (!TextUtils.isEmpty(header.name) && !TextUtils.isEmpty(header.value)) {
builder.header(header.name, header.value);
}
}
}
return builder.build();
}
@Nullable
private ResponseInfo convertFlipperObjectRouteToResponseInfo(FlipperObject route) {
final String data = route.getString("data");
final String requestUrl = route.getString("requestUrl");
final String method = route.getString("method");
FlipperArray headersArray = route.getArray("headers");
if (TextUtils.isEmpty(requestUrl) || TextUtils.isEmpty(method)) {
return null;
}
final ResponseInfo mockResponse = new ResponseInfo();
mockResponse.body = data.getBytes();
mockResponse.statusCode = HttpURLConnection.HTTP_OK;
mockResponse.statusReason = "OK";
if (headersArray != null) {
final List<NetworkReporter.Header> headers = new ArrayList<>();
for (int j = 0; j < headersArray.length(); j++) {
final FlipperObject header = headersArray.getObject(j);
headers.add(new NetworkReporter.Header(header.getString("key"), header.getString("value")));
}
mockResponse.headers = headers;
}
return mockResponse;
}
@Override
public void onConnect(FlipperConnection connection) {
connection.receive(
"mockResponses",
new FlipperReceiver() {
@Override
public void onReceive(FlipperObject params, FlipperResponder responder) throws Exception {
FlipperArray array = params.getArray("routes");
mMockResponseMap.clear();
for (int i = 0; i < array.length(); i++) {
final FlipperObject route = array.getObject(i);
final String requestUrl = route.getString("requestUrl");
final String method = route.getString("method");
ResponseInfo mockResponse = convertFlipperObjectRouteToResponseInfo(route);
if (mockResponse != null) {
registerMockResponse(new PartialRequestInfo(requestUrl, method), mockResponse);
}
}
responder.success();
}
});
}
@Override
public void onDisconnect() {
mMockResponseMap.clear();
}
}