Unit testing Connections.ts (#66)

* Jest, Enzyme test env setup

* unit tests for telemetry state update logic

* remove sinon, superfluous scripts

* Remove console log
This commit is contained in:
YJ
2018-10-05 00:14:53 -07:00
committed by Maciej Hirsz
parent b8ad6249ca
commit 8e16f7c129
7 changed files with 1279 additions and 86 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"presets": ["env", "react"]
}
+25 -1
View File
@@ -4,6 +4,20 @@
"author": "Parity Technologies Ltd. <admin@parity.io>",
"license": "GPL-3.0",
"description": "Polkadot Telemetry frontend",
"jest": {
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"setupFiles": [
"jest-localstorage-mock"
],
"transform": {
"\\.(ts|tsx)$": "ts-jest"
},
"testRegex": "/test/.*\\.(spec).(ts|tsx|js)$"
},
"dependencies": {
"@fnando/sparkline": "maciejhirsz/sparkline",
"polkadot-identicon": "^1.1.0",
@@ -15,7 +29,7 @@
"scripts": {
"start": "react-scripts-ts start",
"build": "react-scripts-ts build",
"test": "react-scripts-ts test --env=jsdom",
"test": "jest --coverage",
"eject": "react-scripts-ts eject"
},
"devDependencies": {
@@ -24,6 +38,16 @@
"@types/react": "^16.3.17",
"@types/react-dom": "^16.0.6",
"@types/react-svg": "^3.0.0",
"babel-jest": "^23.6.0",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"enzyme": "^3.6.0",
"enzyme-adapter-react-16": "^1.5.0",
"jest": "^23.6.0",
"jest-localstorage-mock": "^2.2.0",
"mock-socket": "^8.0.2",
"react-test-renderer": "^16.5.2",
"ts-jest": "^23.10.2",
"typescript": "^2.9.2"
}
}
+68 -64
View File
@@ -81,73 +81,11 @@ export class Connection {
this.socket.send(`subscribe:${chain}`);
}
private bindSocket() {
this.ping();
this.state = this.update({
status: 'online',
nodes: new Map(),
sortedNodes: [],
});
if (this.state.subscribed) {
this.resubscribeTo = this.state.subscribed;
this.state = this.update({ subscribed: null });
}
this.socket.addEventListener('message', this.handleMessages);
this.socket.addEventListener('close', this.handleDisconnect);
this.socket.addEventListener('error', this.handleDisconnect);
}
private ping = () => {
if (this.pingSent) {
this.handleDisconnect();
return;
}
this.pingId += 1;
this.pingSent = timestamp();
this.socket.send(`ping:${this.pingId}`);
this.pingTimeout = setTimeout(this.ping, 30000);
}
private pong(id: number) {
if (!this.pingSent) {
console.error('Received a pong without sending a ping first');
this.handleDisconnect();
return;
}
if (id !== this.pingId) {
console.error('pingId differs');
this.handleDisconnect();
}
const latency = timestamp() - this.pingSent;
this.pingSent = null;
console.log('latency', latency);
}
private clean() {
clearTimeout(this.pingTimeout);
this.pingSent = null;
this.socket.removeEventListener('message', this.handleMessages);
this.socket.removeEventListener('close', this.handleDisconnect);
this.socket.removeEventListener('error', this.handleDisconnect);
}
private handleMessages = (event: MessageEvent) => {
const data = event.data as FeedMessage.Data;
public handleMessages = (messages: FeedMessage.Message[]) => {
const { nodes, chains } = this.state;
let { sortedNodes } = this.state;
messages: for (const message of FeedMessage.deserialize(data)) {
messages: for (const message of messages) {
switch (message.action) {
case Actions.FeedVersion: {
if (message.payload !== VERSION) {
@@ -329,6 +267,72 @@ export class Connection {
this.autoSubscribe();
}
private bindSocket() {
this.ping();
this.state = this.update({
status: 'online',
nodes: new Map()
});
if (this.state.subscribed) {
this.resubscribeTo = this.state.subscribed;
this.state = this.update({ subscribed: null });
}
this.socket.addEventListener('message', this.handleFeedData);
this.socket.addEventListener('close', this.handleDisconnect);
this.socket.addEventListener('error', this.handleDisconnect);
}
private ping = () => {
if (this.pingSent) {
this.handleDisconnect();
return;
}
this.pingId += 1;
this.pingSent = timestamp();
this.socket.send(`ping:${this.pingId}`);
this.pingTimeout = setTimeout(this.ping, 30000);
}
private pong(id: number) {
if (!this.pingSent) {
console.error('Received a pong without sending a ping first');
this.handleDisconnect();
return;
}
if (id !== this.pingId) {
console.error('pingId differs');
this.handleDisconnect();
}
const latency = timestamp() - this.pingSent;
this.pingSent = null;
console.log('latency', latency);
}
private clean() {
clearTimeout(this.pingTimeout);
this.pingSent = null;
this.socket.removeEventListener('message', this.handleFeedData);
this.socket.removeEventListener('close', this.handleDisconnect);
this.socket.removeEventListener('error', this.handleDisconnect);
}
private handleFeedData = (event: MessageEvent) => {
const data = event.data as FeedMessage.Data;
this.handleMessages(FeedMessage.deserialize(data));
}
private autoSubscribe() {
const { subscribed, chains } = this.state;
const { resubscribeTo } = this;
+5 -1
View File
@@ -12,7 +12,11 @@ export class Persistent<Data> {
const stored = window.localStorage.getItem(key) as Maybe<Stringified<Data>>;
if (stored) {
this.value = parse(stored);
try {
this.value = parse(stored);
} catch (err) {
this.value = initial;
}
} else {
this.value = initial;
}
+352
View File
@@ -0,0 +1,352 @@
import * as Enzyme from './enzyme';
const { shallow, mount } = Enzyme;
import { Server } from 'mock-socket';
import { Types, FeedMessage, timestamp, VERSION } from '../../common';
import { Node, Update, State } from '../src/state';
import { Connection } from '../src/Connection';
import { PersistentObject, PersistentSet } from '../src/persist';
const { Actions } = FeedMessage;
describe('Connection.ts', () => {
const fakeWebSocketURL = 'ws://localhost:8080';
const mockServer = new Server(fakeWebSocketURL);
mockServer.on('connection', (socket: any) => {
console.log('Got connection!');
// socket.on('message', (message: any) => {
// console.log('message received by web socket -> ', message);
// });
//
// socket.send('[0,15,8,["BBQ Birch",6],8,["Pistachios",57]]');
});
let state: State;
let connection: Connection;
let handleMessages: any;
let update: any;
const settings: State.Settings = {
location: false,
validator: false,
implementation: false,
peers: false,
txs: false,
cpu: false,
mem: false,
blocknumber: false,
blockhash: false,
blocktime: false,
blockpropagation: false,
blocklasttime: false
}
beforeAll(async () => {
state = {
status: 'offline',
best: 0 as Types.BlockNumber,
blockTimestamp: 0 as Types.Timestamp,
blockAverage: null,
timeDiff: 0 as Types.Milliseconds,
subscribed: null,
chains: new Map(),
nodes: new Map(),
sortedNodes: [],
settings,
pins: new Set()
} as State;
const pins: PersistentSet<Types.NodeName> = new PersistentSet<Types.NodeName>('key', ((p) => {}));
update = jest.fn((changes) => {
// console.log('message passed to the update function -> ', changes);
state = Object.assign({}, state, changes);
// stub update function
return state as Readonly<State>;
}) as Update;
connection = await Connection.create(pins, update);
const before = connection.handleMessages;
connection.handleMessages = jest.fn(connection.handleMessages);
// console.log('handle after', connection.handleMessages === handleMessages, connection.handleMessages === before);
});
afterEach(() => {
// clear stubs and fakes after each test case
// connection.handleMessages.restore();
})
test('handle Feed Version message state update', async () => {
connection.handleMessages([
{
action: 0,
payload: VERSION
}
] as any as FeedMessage.Message[])
expect(update).toHaveBeenCalled();
expect(state.status).toBe('online');
})
test('handle Best Block message state update', () => {
const time = timestamp();
connection.handleMessages([
{
action: 1,
payload: [1, time, 0],
}, {
action: 1,
payload: [1, time, 123456789]
}
] as any as FeedMessage.Message[]);
expect(update).toHaveBeenCalled();
expect(state.blockTimestamp).toBe(time);
expect(state.blockAverage).toBe(123456789);
})
describe('Add and Remove a Node', () => {
const time = timestamp();
test('handle Added Node message state update', () => {
/*
Added Node Message (NodeId, NodeDetails, NodeStats, BlockDetails, Maybe<NodeLocation>)
*/
connection.handleMessages([
{
action: 2,
payload: [1, ['Sample Node', 'Sampling', '1.2.3', '0x123456789012345'], [12, 84, 38, 78], [1, 'aoaiuhsf9o2ih389r', 777, time, 829]],
}
] as any as FeedMessage.Message[]);
expect(update).toHaveBeenCalled();
expect(state.status).toBe('online');
expect(state.nodes).toBeDefined();
const nodes = [];
for (const node of state.nodes.values()) {
nodes.push(node);
}
const firstNode = nodes[0];
expect(firstNode.id).toBe(1);
expect(firstNode.name).toBe('Sample Node');
expect(firstNode.implementation).toBe('Sampling');
expect(firstNode.validator).toBe('0x123456789012345');
expect(firstNode.peers).toBe(12);
expect(firstNode.txs).toBe(84);
expect(firstNode.mem).toBe(38);
expect(firstNode.cpu).toBe(78);
expect(firstNode.height).toBe(1);
expect(firstNode.hash).toBe('aoaiuhsf9o2ih389r');
expect(firstNode.blockTime).toBe(777);
expect(firstNode.blockTimestamp).toBe(time);
expect(firstNode.propagationTime).toBe(829);
})
test('handle Located Node message state update', () => {
/*
Located Node
[NodeId, Latitude, Longitude, City]
*/
connection.handleMessages([
{
action: 0x04,
payload: [1, 30.828, 101.4111, 'Kuala Lumpur']
}
] as any as FeedMessage.Message[]);
expect(update).toHaveBeenCalled();
const nodes = [];
for (const node of state.nodes.values()) {
nodes.push(node);
}
const firstNode = nodes[0];
expect(firstNode.lat).toEqual(30.828)
expect(firstNode.lon).toEqual(101.4111)
expect(firstNode.city).toEqual('Kuala Lumpur')
});
test('handle Time Sync message state update', () => {
connection.handleMessages([
{
action: 0x07,
payload: time + 12345432
}
] as any as FeedMessage.Message[]);
const nodes = [];
for (const node of state.nodes.values()) {
nodes.push(node);
}
const firstNode = nodes[0];
expect(firstNode.blockTimestamp).toBe(time);
})
test('handle Imported Block message state update', () => {
/*
ImportedBlockMessage [NodeId, BlockDetails]
BlockDetails = [BlockNumber, BlockHash, Milliseconds, Timestamp, Maybe<PropagationTime>]
*/
connection.handleMessages([
{
action: 0x05,
payload: ["BBQ Birch", [1, 'ABCDEFGH12345678', 123, time, 48292010]],
}
] as any as FeedMessage.Message[]);
expect(state.sortedNodes).toBeDefined();
const sortedNodes = [];
for (const node of state.sortedNodes) {
sortedNodes.push(node);
}
const firstSortedNode = sortedNodes[0];
expect(firstSortedNode.pinned).toBeFalsy();
expect(firstSortedNode).toMatchObject({
pinned: false,
id: 1,
name: 'Sample Node',
implementation: 'Sampling',
version: '1.2.3',
validator: '0x123456789012345',
peers: 12,
txs: 84,
mem: 38,
cpu: 78,
height: 1,
hash: 'aoaiuhsf9o2ih389r',
blockTime: 777,
blockTimestamp: time,
propagationTime: 829,
lat: 30.828,
lon: 101.4111,
city: 'Kuala Lumpur'
});
});
test('handle Removed Node message state update', () => {
/*
payload: 1
*/
connection.handleMessages([
{
action: 3,
payload: 1
}
] as any as FeedMessage.Message[]);
expect(update).toHaveBeenCalled();
expect(state.nodes.keys()).toMatchObject({});
})
})
describe('Add and Remove a Chain', () => {
test('handle Added Chain message state update', () => {
connection.handleMessages([
{
action: 8,
payload: ["BBQ Birch", 6],
}, {
action: 8,
payload: ["Krumme Lanke", 57]
}
] as any as FeedMessage.Message[]);
expect(update).toHaveBeenCalled();
const chains = [];
for (const chain of state.chains) {
chains.push(chain);
}
const firstChain = chains[0];
const secondChain = chains[1];
expect(chains).toHaveLength(2);
expect(firstChain).toEqual([ 'BBQ Birch', 6 ]);
expect(secondChain).toEqual([ 'Krumme Lanke', 57 ]);
});
test('handle Node Stats message state update', () => {
})
describe('Subscribe and Unsubscribe to Message', () => {
test('handle Subscribed To message state update', () => {
connection.handleMessages([
{
action: 0x0A,
payload: 'BBQ Birch'
}
] as any as FeedMessage.Message[]);
expect(update).toHaveBeenCalled();
expect(state.subscribed).toBe('BBQ Birch');
connection.handleMessages([
{
action: 0x0A,
payload: 'Krumme Lanke'
}
] as any as FeedMessage.Message[]);
expect(update).toHaveBeenCalled();
expect(state.subscribed).toBe('Krumme Lanke');
});
test('handle Unsubscribed From message state update', () => {
connection.handleMessages([
{
action: 0x0B,
payload: 'Krumme Lanke'
}
] as any as FeedMessage.Message[]);
expect(update).toHaveBeenCalled();
expect(state.subscribed).toBeFalsy();
})
})
test('handle Removed Chain message state update', () => {
connection.handleMessages([
{
action: 9,
payload: 'BBQ Birch'
}, {
action: 9,
payload: 'Krumme Lanke'
}
] as any as FeedMessage.Message[]);
expect(update).toHaveBeenCalled();
expect(state.chains.keys()).toMatchObject({});
})
})
});
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2017-2018 @polkadot authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
const Adapter = require('enzyme-adapter-react-16');
const Enzyme = require('enzyme');
Enzyme.configure({
adapter: new Adapter()
});
module.exports = Enzyme;
+814 -20
View File
File diff suppressed because it is too large Load Diff