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
171 lines
5.2 KiB
TypeScript
171 lines
5.2 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 {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);
|
|
}
|
|
});
|
|
});
|
|
});
|