Implement JS flipper client
Summary: Standardize WS implementation for JS environments. Why do we need a separate server implementation for browsers? Browser targets cannot authenticate via the default certificate exchange flow. We need a dedicated client for them that works over an insecure channel (without the cert exchange). Major changes: 1. Renamed `flipper-js-client-sdk` to `js-flipper` for consistency with `react-native-flipper` 2. Updated `js-flipper` implementation to match our other existing clients Documentation will be updated in a separate subsequent PR. https://fb.quip.com/2mboA0xbgoxl Reviewed By: mweststrate Differential Revision: D31688105 fbshipit-source-id: 418aa80e0fd86361c089cf54b0d44a8b4f748efa
This commit is contained in:
committed by
Facebook GitHub Bot
parent
2be631ea4d
commit
9a47f41056
170
js/js-flipper/src/__tests__/client.spec.ts
Normal file
170
js/js-flipper/src/__tests__/client.spec.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 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 {AddressInfo} from 'net';
|
||||
import {WebSocketServer, WebSocket} from 'ws';
|
||||
|
||||
import {FlipperClient, FlipperWebSocket} from '../client';
|
||||
import {RECONNECT_TIMEOUT} from '../consts';
|
||||
import {WSMessageAccumulator} from './utils';
|
||||
|
||||
describe('client', () => {
|
||||
let port: number;
|
||||
let wsServer: WebSocketServer;
|
||||
let client: FlipperClient;
|
||||
|
||||
let allowConnection = true;
|
||||
const verifyClient = jest.fn().mockImplementation(() => allowConnection);
|
||||
beforeEach(() => {
|
||||
allowConnection = true;
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
wsServer = new WebSocketServer({
|
||||
port: 0,
|
||||
verifyClient,
|
||||
});
|
||||
await new Promise((resolve) => wsServer.on('listening', resolve));
|
||||
port = (wsServer.address() as AddressInfo).port;
|
||||
client = new FlipperClient();
|
||||
// TODO: Figure out why we need to convert ot unknown first
|
||||
client.websocketFactory = (url) =>
|
||||
new WebSocket(url) as unknown as FlipperWebSocket;
|
||||
client.urlBase = `localhost:${port}`;
|
||||
});
|
||||
afterEach(async () => {
|
||||
client.stop();
|
||||
await new Promise((resolve) => wsServer.close(resolve));
|
||||
});
|
||||
|
||||
describe('message handling', () => {
|
||||
describe('getPlugins', () => {
|
||||
it('returns a list of registered plugins', async () => {
|
||||
const serverReceivedMessages = new WSMessageAccumulator();
|
||||
wsServer.on('connection', (ws) => {
|
||||
ws.send(JSON.stringify({method: 'getPlugins', id: 0}));
|
||||
ws.on('message', (message) =>
|
||||
serverReceivedMessages.add(message.toString()),
|
||||
);
|
||||
});
|
||||
|
||||
client.addPlugin({
|
||||
getId: () => '42',
|
||||
onConnect: () => undefined,
|
||||
onDisconnect: () => undefined,
|
||||
});
|
||||
|
||||
await client.start();
|
||||
|
||||
const expectedGetPluginsResponse = {
|
||||
id: 0,
|
||||
success: {
|
||||
plugins: ['42'],
|
||||
},
|
||||
};
|
||||
const actualGetPluginsReponse = await serverReceivedMessages.newMessage;
|
||||
expect(actualGetPluginsReponse).toBe(
|
||||
JSON.stringify(expectedGetPluginsResponse),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('onError is called if message handling has failed, connection is closed, client reconnects', async () => {
|
||||
const onError = jest.fn();
|
||||
client.onError = onError;
|
||||
|
||||
let resolveFirstConnectionPromise: () => void;
|
||||
const firstConnectionPromise = new Promise<void>((resolve) => {
|
||||
resolveFirstConnectionPromise = resolve;
|
||||
});
|
||||
wsServer.on('connection', (ws) => {
|
||||
resolveFirstConnectionPromise();
|
||||
// Send a malformed message to cause a failure
|
||||
ws.send('{{{');
|
||||
});
|
||||
|
||||
// Capturing a moment when the client received an error
|
||||
const receivedErrorPromise = new Promise<void>((resolve) =>
|
||||
onError.mockImplementationOnce((e) => {
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
|
||||
await client.start();
|
||||
|
||||
// Capturing a moment when the client was closed because of the error
|
||||
const closedPromise = new Promise<void>((resolve) => {
|
||||
const originalOnclose = (client as any).ws.onclose;
|
||||
(client as any).ws.onclose = (data: unknown) => {
|
||||
originalOnclose(data);
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
|
||||
await receivedErrorPromise;
|
||||
expect(onError).toBeCalledTimes(1);
|
||||
|
||||
// Make sure that the connection went through
|
||||
await firstConnectionPromise;
|
||||
|
||||
wsServer.removeAllListeners('connection');
|
||||
|
||||
let resolveSecondConnectionPromise: () => void;
|
||||
const secondConnectionPromise = new Promise<void>((resolve) => {
|
||||
resolveSecondConnectionPromise = resolve;
|
||||
});
|
||||
wsServer.on('connection', () => {
|
||||
resolveSecondConnectionPromise();
|
||||
});
|
||||
|
||||
// Make sure the current client is closed
|
||||
// When it closes, it schedules a reconnection
|
||||
await closedPromise;
|
||||
|
||||
// Now, once the reconnection is scheduled, we can advance timers to do the actual reconnection
|
||||
jest.advanceTimersByTime(RECONNECT_TIMEOUT);
|
||||
|
||||
// Make sure that the client reconnects
|
||||
await secondConnectionPromise;
|
||||
});
|
||||
});
|
||||
|
||||
describe('connection', () => {
|
||||
it('onError is called if connection has failed, it is called every time Flipper fails to reconnect', async () => {
|
||||
allowConnection = false;
|
||||
|
||||
const onError = jest.fn();
|
||||
client.onError = onError;
|
||||
|
||||
expect(onError).toBeCalledTimes(0);
|
||||
client.start();
|
||||
|
||||
// Expect connection request to fail
|
||||
await new Promise((resolve) => onError.mockImplementationOnce(resolve));
|
||||
expect(onError).toBeCalledTimes(1);
|
||||
// Checking that the request went through to the server
|
||||
expect(verifyClient).toBeCalledTimes(1);
|
||||
|
||||
// Exepect reconnection attempts to fail
|
||||
for (let i = 2; i < 10; i++) {
|
||||
jest.advanceTimersByTime(RECONNECT_TIMEOUT);
|
||||
await new Promise((resolve) => onError.mockImplementationOnce(resolve));
|
||||
expect(onError).toBeCalledTimes(i);
|
||||
expect(verifyClient).toBeCalledTimes(i);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user