Summary: Network Plugin - When creating a mock request from a selected request, the request data is not in the proper format. It is decoded instead of just being copied from the call (which has already been decoded properly). This PR fixes that problem. Below is a screenshot showing the problem (which occurs for all text response data):  ## Changelog Network Plugin - Fix problem with decoding request data for mocks copied from selection Pull Request resolved: https://github.com/facebook/flipper/pull/2340 Test Plan: Using the sample Android app, issue a network request In Flipper, create a mock for the network request by selecting it and using the "Copy Selected Calls" function in the mock Verify that the request data is readable:  Note: Testing was done using the sample app which uses responses with JSON data. I was not able to provide testing for other types of calls, specifically calls that would return binary data. Reviewed By: passy Differential Revision: D28533224 Pulled By: mweststrate fbshipit-source-id: ce11d23ade60843c110286f7a5bbeba91febbcf0
237 lines
7.0 KiB
TypeScript
237 lines
7.0 KiB
TypeScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
*/
|
|
|
|
import fs from 'fs';
|
|
// eslint-disable-next-line
|
|
import electron, {OpenDialogOptions, remote} from 'electron';
|
|
import {Atom, DataTableManager} from 'flipper-plugin';
|
|
import {createContext} from 'react';
|
|
import {Header, Request} from '../types';
|
|
import {bodyAsString, decodeBody} from '../utils';
|
|
import {message} from 'antd';
|
|
|
|
export type Route = {
|
|
requestUrl: string;
|
|
requestMethod: string;
|
|
responseData: string;
|
|
responseHeaders: {[id: string]: Header};
|
|
responseStatus: string;
|
|
enabled: boolean;
|
|
};
|
|
|
|
export type MockRoute = {
|
|
requestUrl: string;
|
|
method: string;
|
|
data: string;
|
|
headers: Header[];
|
|
status: string;
|
|
enabled: boolean;
|
|
};
|
|
|
|
export interface NetworkRouteManager {
|
|
addRoute(): string | undefined;
|
|
modifyRoute(id: string, routeChange: Partial<Route>): void;
|
|
removeRoute(id: string): void;
|
|
enableRoute(id: string): void;
|
|
copySelectedCalls(): void;
|
|
importRoutes(): void;
|
|
exportRoutes(): void;
|
|
clearRoutes(): void;
|
|
}
|
|
|
|
export const nullNetworkRouteManager: NetworkRouteManager = {
|
|
addRoute(): string | undefined {
|
|
return '';
|
|
},
|
|
modifyRoute(_id: string, _routeChange: Partial<Route>) {},
|
|
removeRoute(_id: string) {},
|
|
enableRoute(_id: string) {},
|
|
copySelectedCalls() {},
|
|
importRoutes() {},
|
|
exportRoutes() {},
|
|
clearRoutes() {},
|
|
};
|
|
|
|
export const NetworkRouteContext = createContext<NetworkRouteManager>(
|
|
nullNetworkRouteManager,
|
|
);
|
|
|
|
export function createNetworkManager(
|
|
nextRouteId: Atom<number>,
|
|
routes: Atom<{[id: string]: any}>,
|
|
informClientMockChange: (routes: {[id: string]: any}) => Promise<void>,
|
|
tableManagerRef: React.RefObject<DataTableManager<Request> | undefined>,
|
|
): NetworkRouteManager {
|
|
return {
|
|
addRoute(): string | undefined {
|
|
const newNextRouteId = nextRouteId.get();
|
|
routes.update((draft) => {
|
|
draft[newNextRouteId.toString()] = {
|
|
requestUrl: '',
|
|
requestMethod: 'GET',
|
|
responseData: '',
|
|
responseHeaders: {},
|
|
responseStatus: '200',
|
|
enabled: true,
|
|
};
|
|
});
|
|
nextRouteId.set(newNextRouteId + 1);
|
|
return String(newNextRouteId);
|
|
},
|
|
modifyRoute(id: string, routeChange: Partial<Route>) {
|
|
if (!routes.get().hasOwnProperty(id)) {
|
|
return;
|
|
}
|
|
routes.update((draft) => {
|
|
Object.assign(draft[id], routeChange);
|
|
});
|
|
informClientMockChange(routes.get());
|
|
},
|
|
removeRoute(id: string) {
|
|
if (routes.get().hasOwnProperty(id)) {
|
|
routes.update((draft) => {
|
|
delete draft[id];
|
|
});
|
|
}
|
|
informClientMockChange(routes.get());
|
|
},
|
|
enableRoute(id: string) {
|
|
if (routes.get().hasOwnProperty(id)) {
|
|
routes.update((draft) => {
|
|
draft[id].enabled = !draft[id].enabled;
|
|
});
|
|
}
|
|
informClientMockChange(routes.get());
|
|
},
|
|
copySelectedCalls() {
|
|
tableManagerRef.current?.getSelectedItems().forEach((request) => {
|
|
// convert headers
|
|
const headers: {[id: string]: Header} = {};
|
|
request.responseHeaders?.forEach((e) => {
|
|
headers[e.key] = e;
|
|
});
|
|
|
|
// no need to convert data, already converted when real call was created
|
|
const responseData =
|
|
request && request.responseData ? request.responseData : '';
|
|
|
|
const newNextRouteId = nextRouteId.get();
|
|
routes.update((draft) => {
|
|
draft[newNextRouteId.toString()] = {
|
|
requestUrl: request.url,
|
|
requestMethod: request.method,
|
|
responseData: responseData as string,
|
|
responseHeaders: headers,
|
|
responseStatus: request.status?.toString() ?? '',
|
|
enabled: true,
|
|
};
|
|
});
|
|
nextRouteId.set(newNextRouteId + 1);
|
|
});
|
|
|
|
informClientMockChange(routes.get());
|
|
},
|
|
importRoutes() {
|
|
const options: OpenDialogOptions = {
|
|
properties: ['openFile'],
|
|
filters: [{extensions: ['json'], name: 'Flipper Route Files'}],
|
|
};
|
|
remote.dialog.showOpenDialog(options).then((result) => {
|
|
const filePaths = result.filePaths;
|
|
if (filePaths.length > 0) {
|
|
fs.readFile(filePaths[0], 'utf8', (err, data) => {
|
|
if (err) {
|
|
message.error('Unable to import file');
|
|
return;
|
|
}
|
|
const importedRoutes = JSON.parse(data);
|
|
importedRoutes?.forEach((importedRoute: Route) => {
|
|
if (importedRoute != null) {
|
|
const newNextRouteId = nextRouteId.get();
|
|
routes.update((draft) => {
|
|
draft[newNextRouteId.toString()] = {
|
|
requestUrl: importedRoute.requestUrl,
|
|
requestMethod: importedRoute.requestMethod,
|
|
responseData: importedRoute.responseData as string,
|
|
responseHeaders: importedRoute.responseHeaders,
|
|
responseStatus: importedRoute.responseStatus,
|
|
enabled: true,
|
|
};
|
|
});
|
|
nextRouteId.set(newNextRouteId + 1);
|
|
}
|
|
});
|
|
informClientMockChange(routes.get());
|
|
});
|
|
}
|
|
});
|
|
},
|
|
exportRoutes() {
|
|
remote.dialog
|
|
.showSaveDialog(
|
|
// @ts-ignore This appears to work but isn't allowed by the types
|
|
null,
|
|
{
|
|
title: 'Export Routes',
|
|
defaultPath: 'NetworkPluginRoutesExport.json',
|
|
},
|
|
)
|
|
.then((result: electron.SaveDialogReturnValue) => {
|
|
const file = result.filePath;
|
|
if (!file) {
|
|
return;
|
|
}
|
|
fs.writeFile(
|
|
file,
|
|
JSON.stringify(Object.values(routes.get()), null, 2),
|
|
'utf8',
|
|
(err) => {
|
|
if (err) {
|
|
message.error('Failed to store mock routes: ' + err);
|
|
} else {
|
|
message.info('Successfully exported mock routes');
|
|
}
|
|
},
|
|
);
|
|
});
|
|
},
|
|
clearRoutes() {
|
|
routes.set({});
|
|
informClientMockChange(routes.get());
|
|
},
|
|
};
|
|
}
|
|
|
|
export function computeMockRoutes(routes: {[id: string]: Route}) {
|
|
const existedIdSet: {[id: string]: {[method: string]: boolean}} = {};
|
|
const filteredRoutes: {[id: string]: Route} = Object.entries(routes).reduce(
|
|
(accRoutes, [id, route]) => {
|
|
if (existedIdSet.hasOwnProperty(route.requestUrl)) {
|
|
if (
|
|
existedIdSet[route.requestUrl].hasOwnProperty(route.requestMethod)
|
|
) {
|
|
return accRoutes;
|
|
}
|
|
existedIdSet[route.requestUrl] = {
|
|
...existedIdSet[route.requestUrl],
|
|
[route.requestMethod]: true,
|
|
};
|
|
return Object.assign({[id]: route}, accRoutes);
|
|
} else {
|
|
existedIdSet[route.requestUrl] = {
|
|
[route.requestMethod]: true,
|
|
};
|
|
return Object.assign({[id]: route}, accRoutes);
|
|
}
|
|
},
|
|
{},
|
|
);
|
|
return filteredRoutes;
|
|
}
|