Fix Network serialization
Summary: Changelog: [Network] Fix import / export of binary data Introduced proper serialization of binary data when creating a Flipper export. Implements / solves https://github.com/facebook/flipper/issues/2308 Reviewed By: nikoant Differential Revision: D28441021 fbshipit-source-id: 90b524bf2a5d85e373073b50a3ccf2bb29628ee0
This commit is contained in:
committed by
Facebook GitHub Bot
parent
5242a81e94
commit
b947a65c51
@@ -11,7 +11,7 @@ import {TestUtils} from 'flipper-plugin';
|
|||||||
import * as NetworkPlugin from '../index';
|
import * as NetworkPlugin from '../index';
|
||||||
|
|
||||||
test('Can handle custom headers', async () => {
|
test('Can handle custom headers', async () => {
|
||||||
const {instance, sendEvent, act, renderer, exportState} =
|
const {instance, sendEvent, act, renderer, exportStateAsync} =
|
||||||
TestUtils.renderPlugin(NetworkPlugin);
|
TestUtils.renderPlugin(NetworkPlugin);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -122,7 +122,7 @@ test('Can handle custom headers', async () => {
|
|||||||
|
|
||||||
// after import, columns should be visible and restored
|
// after import, columns should be visible and restored
|
||||||
{
|
{
|
||||||
const snapshot = exportState();
|
const snapshot = await exportStateAsync();
|
||||||
// Note: snapshot is set in the previous test
|
// Note: snapshot is set in the previous test
|
||||||
const {instance: instance2, renderer: renderer2} = TestUtils.renderPlugin(
|
const {instance: instance2, renderer: renderer2} = TestUtils.renderPlugin(
|
||||||
NetworkPlugin,
|
NetworkPlugin,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {decodeBody} from '../utils';
|
|||||||
import {ResponseInfo} from '../types';
|
import {ResponseInfo} from '../types';
|
||||||
import {promisify} from 'util';
|
import {promisify} from 'util';
|
||||||
import {readFileSync} from 'fs';
|
import {readFileSync} from 'fs';
|
||||||
|
import {TestUtils} from 'flipper-plugin';
|
||||||
|
import * as NetworkPlugin from '../index';
|
||||||
|
|
||||||
async function createMockResponse(
|
async function createMockResponse(
|
||||||
input: string,
|
input: string,
|
||||||
@@ -136,3 +138,122 @@ describe('network data encoding', () => {
|
|||||||
expect(bodyAsBuffer(response)).toEqual(tinyLogoExpected);
|
expect(bodyAsBuffer(response)).toEqual(tinyLogoExpected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('binary data gets serialized correctly', async () => {
|
||||||
|
const tinyLogoExpected = readFileSync(
|
||||||
|
path.join(__dirname, 'fixtures', 'tiny_logo.png'),
|
||||||
|
);
|
||||||
|
const tinyLogoData = readFileSync(
|
||||||
|
path.join(__dirname, 'fixtures', 'tiny_logo.base64.txt'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
const donatingExpected = readFileSync(
|
||||||
|
path.join(__dirname, 'fixtures', 'donating.md'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
const donatingData = readFileSync(
|
||||||
|
path.join(__dirname, 'fixtures', 'donating.md.utf8.gzip.ios.txt'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
const {instance, sendEvent, exportStateAsync} =
|
||||||
|
TestUtils.startPlugin(NetworkPlugin);
|
||||||
|
sendEvent('newRequest', {
|
||||||
|
id: '0',
|
||||||
|
timestamp: 0,
|
||||||
|
data: donatingData,
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: 'Content-Type',
|
||||||
|
value: 'text/plain',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
method: 'post',
|
||||||
|
url: 'http://www.fbflipper.com',
|
||||||
|
});
|
||||||
|
const response = await createMockResponse(
|
||||||
|
'tiny_logo.android.txt',
|
||||||
|
'image/png',
|
||||||
|
);
|
||||||
|
sendEvent('newResponse', response);
|
||||||
|
|
||||||
|
expect(instance.requests.getById('0')).toMatchObject({
|
||||||
|
requestHeaders: [
|
||||||
|
{
|
||||||
|
key: 'Content-Type',
|
||||||
|
value: 'text/plain',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
requestData: donatingExpected,
|
||||||
|
responseHeaders: [
|
||||||
|
{
|
||||||
|
key: 'Content-Type',
|
||||||
|
value: 'image/png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
responseData: new Uint8Array(tinyLogoExpected),
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = await exportStateAsync();
|
||||||
|
expect(snapshot).toMatchObject({
|
||||||
|
isMockResponseSupported: true,
|
||||||
|
selectedId: undefined,
|
||||||
|
requests2: [
|
||||||
|
{
|
||||||
|
domain: 'www.fbflipper.com/',
|
||||||
|
duration: 0,
|
||||||
|
id: '0',
|
||||||
|
insights: undefined,
|
||||||
|
method: 'post',
|
||||||
|
reason: 'dunno',
|
||||||
|
requestHeaders: [
|
||||||
|
{
|
||||||
|
key: 'Content-Type',
|
||||||
|
value: 'text/plain',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
requestData: donatingExpected, // not encoded
|
||||||
|
responseData: [tinyLogoData.trim()], // wrapped represents base64
|
||||||
|
responseHeaders: [
|
||||||
|
{
|
||||||
|
key: 'Content-Type',
|
||||||
|
value: 'image/png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
responseIsMock: false,
|
||||||
|
responseLength: 24838,
|
||||||
|
status: 200,
|
||||||
|
url: 'http://www.fbflipper.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const {instance: instance2} = TestUtils.startPlugin(NetworkPlugin, {
|
||||||
|
initialState: snapshot,
|
||||||
|
});
|
||||||
|
expect(instance2.requests.getById('0')).toMatchObject({
|
||||||
|
domain: 'www.fbflipper.com/',
|
||||||
|
duration: 0,
|
||||||
|
id: '0',
|
||||||
|
insights: undefined,
|
||||||
|
method: 'post',
|
||||||
|
reason: 'dunno',
|
||||||
|
requestHeaders: [
|
||||||
|
{
|
||||||
|
key: 'Content-Type',
|
||||||
|
value: 'text/plain',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
requestData: donatingExpected,
|
||||||
|
responseData: new Uint8Array(tinyLogoExpected),
|
||||||
|
responseHeaders: [
|
||||||
|
{
|
||||||
|
key: 'Content-Type',
|
||||||
|
value: 'image/png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
responseIsMock: false,
|
||||||
|
responseLength: 24838,
|
||||||
|
status: 200,
|
||||||
|
url: 'http://www.fbflipper.com',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
ResponseFollowupChunk,
|
ResponseFollowupChunk,
|
||||||
AddProtobufEvent,
|
AddProtobufEvent,
|
||||||
PartialResponses,
|
PartialResponses,
|
||||||
|
SerializedRequest,
|
||||||
} from './types';
|
} from './types';
|
||||||
import {ProtobufDefinitionsRepository} from './ProtobufDefinitionsRepository';
|
import {ProtobufDefinitionsRepository} from './ProtobufDefinitionsRepository';
|
||||||
import {
|
import {
|
||||||
@@ -68,6 +69,7 @@ import {
|
|||||||
createNetworkManager,
|
createNetworkManager,
|
||||||
computeMockRoutes,
|
computeMockRoutes,
|
||||||
} from './request-mocking/NetworkRouteManager';
|
} from './request-mocking/NetworkRouteManager';
|
||||||
|
import {Base64} from 'js-base64';
|
||||||
|
|
||||||
const LOCALSTORAGE_MOCK_ROUTE_LIST_KEY = '__NETWORK_CACHED_MOCK_ROUTE_LIST';
|
const LOCALSTORAGE_MOCK_ROUTE_LIST_KEY = '__NETWORK_CACHED_MOCK_ROUTE_LIST';
|
||||||
const LOCALSTORAGE_RESPONSE_BODY_FORMAT_KEY =
|
const LOCALSTORAGE_RESPONSE_BODY_FORMAT_KEY =
|
||||||
@@ -94,6 +96,13 @@ type CustomColumnConfig = {
|
|||||||
type: 'response' | 'request';
|
type: 'response' | 'request';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type StateExport = {
|
||||||
|
requests2: SerializedRequest[];
|
||||||
|
isMockResponseSupported: boolean;
|
||||||
|
selectedId: string | undefined;
|
||||||
|
customColumns: CustomColumnConfig[];
|
||||||
|
};
|
||||||
|
|
||||||
export function plugin(client: PluginClient<Events, Methods>) {
|
export function plugin(client: PluginClient<Events, Methods>) {
|
||||||
const networkRouteManager = createState<NetworkRouteManager>(
|
const networkRouteManager = createState<NetworkRouteManager>(
|
||||||
nullNetworkRouteManager,
|
nullNetworkRouteManager,
|
||||||
@@ -101,30 +110,20 @@ export function plugin(client: PluginClient<Events, Methods>) {
|
|||||||
|
|
||||||
const routes = createState<{[id: string]: Route}>({});
|
const routes = createState<{[id: string]: Route}>({});
|
||||||
const nextRouteId = createState<number>(0);
|
const nextRouteId = createState<number>(0);
|
||||||
const isMockResponseSupported = createState<boolean>(false, {
|
const isMockResponseSupported = createState<boolean>(false);
|
||||||
persist: 'isMockResponseSupported',
|
|
||||||
});
|
|
||||||
const showMockResponseDialog = createState<boolean>(false);
|
const showMockResponseDialog = createState<boolean>(false);
|
||||||
const detailBodyFormat = createState<string>(
|
const detailBodyFormat = createState<string>(
|
||||||
localStorage.getItem(LOCALSTORAGE_RESPONSE_BODY_FORMAT_KEY) || 'parsed',
|
localStorage.getItem(LOCALSTORAGE_RESPONSE_BODY_FORMAT_KEY) || 'parsed',
|
||||||
);
|
);
|
||||||
const requests = createDataSource<Request, 'id'>([], {
|
const requests = createDataSource<Request, 'id'>([], {
|
||||||
key: 'id',
|
key: 'id',
|
||||||
persist: 'requests2',
|
|
||||||
});
|
|
||||||
const selectedId = createState<string | undefined>(undefined, {
|
|
||||||
persist: 'selectedId',
|
|
||||||
});
|
});
|
||||||
|
const selectedId = createState<string | undefined>(undefined);
|
||||||
const tableManagerRef = createRef<undefined | DataTableManager<Request>>();
|
const tableManagerRef = createRef<undefined | DataTableManager<Request>>();
|
||||||
|
|
||||||
const partialResponses = createState<PartialResponses>(
|
const partialResponses = createState<PartialResponses>({});
|
||||||
{},
|
|
||||||
{persist: 'partialResponses'},
|
|
||||||
);
|
|
||||||
|
|
||||||
const customColumns = createState<CustomColumnConfig[]>([], {
|
const customColumns = createState<CustomColumnConfig[]>([]); // Store in local storage as well: T69989583
|
||||||
persist: 'customColumns', // Store in local storage as well: T69989583
|
|
||||||
});
|
|
||||||
const columns = createState<DataTableColumn<Request>[]>(baseColumns); // not persistable
|
const columns = createState<DataTableColumn<Request>[]>(baseColumns); // not persistable
|
||||||
|
|
||||||
client.onDeepLink((payload: unknown) => {
|
client.onDeepLink((payload: unknown) => {
|
||||||
@@ -337,6 +336,61 @@ export function plugin(client: PluginClient<Events, Methods>) {
|
|||||||
customColumns.get().forEach(addDataTableColumnConfig);
|
customColumns.get().forEach(addDataTableColumnConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
client.onExport<StateExport>(async (idler, onStatusMessage) => {
|
||||||
|
const serializedRequests: SerializedRequest[] = [];
|
||||||
|
for (let i = 0; i < requests.size; i++) {
|
||||||
|
const request = requests.get(i);
|
||||||
|
serializedRequests.push({
|
||||||
|
...request,
|
||||||
|
requestTime: request.requestTime.getTime(),
|
||||||
|
responseTime: request.responseTime?.getTime(),
|
||||||
|
requestData:
|
||||||
|
request.requestData instanceof Uint8Array
|
||||||
|
? [Base64.fromUint8Array(request.requestData)]
|
||||||
|
: request.requestData,
|
||||||
|
responseData:
|
||||||
|
request.responseData instanceof Uint8Array
|
||||||
|
? [Base64.fromUint8Array(request.responseData)]
|
||||||
|
: request.responseData,
|
||||||
|
});
|
||||||
|
if (idler.isCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (idler.shouldIdle()) {
|
||||||
|
onStatusMessage(`Serializing request ${i + 1}/${requests.size}`);
|
||||||
|
await idler.idle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isMockResponseSupported: isMockResponseSupported.get(),
|
||||||
|
selectedId: selectedId.get(),
|
||||||
|
requests2: serializedRequests,
|
||||||
|
customColumns: customColumns.get(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
client.onImport<StateExport>((data) => {
|
||||||
|
selectedId.set(data.selectedId);
|
||||||
|
isMockResponseSupported.set(data.isMockResponseSupported);
|
||||||
|
customColumns.set(data.customColumns);
|
||||||
|
data.requests2.forEach((request) => {
|
||||||
|
requests.append({
|
||||||
|
...request,
|
||||||
|
requestTime: new Date(request.requestTime),
|
||||||
|
responseTime:
|
||||||
|
request.responseTime != null
|
||||||
|
? new Date(request.responseTime)
|
||||||
|
: undefined,
|
||||||
|
requestData: Array.isArray(request.requestData)
|
||||||
|
? Base64.toUint8Array(request.requestData[0])
|
||||||
|
: request.requestData,
|
||||||
|
responseData: Array.isArray(request.responseData)
|
||||||
|
? Base64.toUint8Array(request.responseData[0])
|
||||||
|
: request.responseData,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
columns,
|
columns,
|
||||||
routes,
|
routes,
|
||||||
|
|||||||
@@ -35,6 +35,16 @@ export interface Request {
|
|||||||
|
|
||||||
export type Requests = DataSource<Request, 'id', string>;
|
export type Requests = DataSource<Request, 'id', string>;
|
||||||
|
|
||||||
|
export type SerializedRequest = Omit<
|
||||||
|
Request,
|
||||||
|
'requestTime' | 'responseTime' | 'requestData' | 'responseData'
|
||||||
|
> & {
|
||||||
|
requestTime: number;
|
||||||
|
requestData?: string | [string]; // wrapped in Array represents base64 encoded
|
||||||
|
responseTime?: number;
|
||||||
|
responseData?: string | [string]; // wrapped in Array represents base64 encoded
|
||||||
|
};
|
||||||
|
|
||||||
export type RequestInfo = {
|
export type RequestInfo = {
|
||||||
id: RequestId;
|
id: RequestId;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user