From d27e45d7bb7f12d40c5c80aca1162c78a3d38357 Mon Sep 17 00:00:00 2001 From: "Qichuan (Sean) ZHANG" Date: Mon, 23 Mar 2020 21:57:08 -0700 Subject: [PATCH] (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 --- .../network/FlipperOkhttpInterceptor.java | 147 +++++++++++++++++- .../flipper/sample/FlipperInitializer.java | 4 +- 2 files changed, 147 insertions(+), 4 deletions(-) diff --git a/android/plugins/network/src/main/java/com/facebook/flipper/plugins/network/FlipperOkhttpInterceptor.java b/android/plugins/network/src/main/java/com/facebook/flipper/plugins/network/FlipperOkhttpInterceptor.java index 935f5512f..4e73f6a28 100644 --- a/android/plugins/network/src/main/java/com/facebook/flipper/plugins/network/FlipperOkhttpInterceptor.java +++ b/android/plugins/network/src/main/java/com/facebook/flipper/plugins/network/FlipperOkhttpInterceptor.java @@ -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 { + PartialRequestInfo(String url, String method) { + super(url, method); + } + } + + // pair of request url and method + private Map 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 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(); + } } diff --git a/android/sample/src/debug/java/com/facebook/flipper/sample/FlipperInitializer.java b/android/sample/src/debug/java/com/facebook/flipper/sample/FlipperInitializer.java index a65056996..d7806c28a 100644 --- a/android/sample/src/debug/java/com/facebook/flipper/sample/FlipperInitializer.java +++ b/android/sample/src/debug/java/com/facebook/flipper/sample/FlipperInitializer.java @@ -36,7 +36,7 @@ public final class FlipperInitializer { final DescriptorMapping descriptorMapping = DescriptorMapping.withDefaults(); final NetworkFlipperPlugin networkPlugin = new NetworkFlipperPlugin(); - final FlipperOkhttpInterceptor interceptor = new FlipperOkhttpInterceptor(networkPlugin); + final FlipperOkhttpInterceptor interceptor = new FlipperOkhttpInterceptor(networkPlugin, true); // Normally, you would want to make this dependent on a BuildConfig flag, but // for this demo application we can safely assume that you always want to debug. @@ -60,7 +60,7 @@ public final class FlipperInitializer { final OkHttpClient okHttpClient = new OkHttpClient.Builder() - .addNetworkInterceptor(interceptor) + .addInterceptor(interceptor) .connectTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.MINUTES)