Files
flipper/js/js-flipper/src/__tests__/client.spec.ts
Andrey Goncharov 9a47f41056 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
2021-10-21 04:28:21 -07:00

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);
}
});
});
});