Skip to content

Commit 500259c

Browse files
authored
Tests for simperium/client (#82)
Unit test coverage for simperium/client
1 parent d45669e commit 500259c

File tree

6 files changed

+257
-19
lines changed

6 files changed

+257
-19
lines changed

src/simperium/client.js

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,7 @@ import Channel from './channel'
55
import defaultGhostStoreProvider from './ghost/default'
66
import defaultObjectStoreProvider from './storage/default'
77

8-
var WebSocketClient;
9-
if ( typeof window !== 'undefined' && window.WebSocket ) {
10-
WebSocketClient = window.WebSocket;
11-
} else {
12-
WebSocketClient = require( 'websocket' ).w3cwebsocket;
13-
}
14-
15-
module.exports = Client;
16-
module.exports.Bucket = Bucket;
17-
module.exports.Channel = Channel;
8+
export { Bucket, Channel };
189

1910
/**
2011
* @function
@@ -30,6 +21,12 @@ module.exports.Channel = Channel;
3021
* @returns {GhostStore} - the ghost store instance to be used by the bucket
3122
*/
3223

24+
/**
25+
* @function
26+
* @name websocketClientProvider
27+
* @param {String} - The url to open the socket connection with
28+
* @returns {Object} a WebSocket client
29+
3330
/**
3431
* A Client is the main interface to Simperium.
3532
*
@@ -40,14 +37,16 @@ module.exports.Channel = Channel;
4037
* - factory function for creating ghost store instances
4138
* @param {bucketStoreProvider} [options.objectStoreProvider=defaultObjectStoreProvider]
4239
* - factory function for creating object store instances
43-
* @param {number} [heartbeatInterval=4] - heartbeat interval for maintaining connection status with Simperium.com
40+
* @param {number} [options.heartbeatInterval=4] - heartbeat interval for maintaining connection status with Simperium.com
41+
* @param {websocketClientProvider} [options.websocketClientProvider] - WebSocket transport, if not provided tries to use window.WebSocket
4442
*/
45-
function Client( appId, accessToken, options ) {
43+
export function Client( appId, accessToken, options ) {
4644
options = options || {};
4745

4846
options.ghostStoreProvider = options.ghostStoreProvider || defaultGhostStoreProvider;
4947
options.objectStoreProvider = options.objectStoreProvider || defaultObjectStoreProvider;
5048
options.hearbeatInterval = options.heartbeatInterval || 4;
49+
options.websocketClientProvider = options.websocketClientProvider || defaultWebsocketClientProvider;
5150

5251
this.accessToken = accessToken;
5352
this.open = false;
@@ -116,10 +115,9 @@ Client.prototype.onHeartbeat = function( message ) {
116115
Client.prototype.onConnect = function() {
117116
this.open = true;
118117

119-
this.emit( 'connect' );
120-
121118
this.heartbeat.start();
122119
this.reconnectionTimer.reset();
120+
this.emit( 'connect' );
123121
};
124122

125123
Client.prototype.onReconnect = function( attempt ) {
@@ -172,6 +170,7 @@ Client.prototype.send = function( data ) {
172170
this.socket.send( data );
173171
} catch ( e ) {
174172
// failed to send, probably not connected
173+
this.emit( 'error', e );
175174
}
176175
};
177176

@@ -181,7 +180,7 @@ Client.prototype.sendChannelMessage = function( id, message ) {
181180

182181
Client.prototype.connect = function() {
183182
this.reconnect = true;
184-
this.socket = new WebSocketClient( this.options.url );
183+
this.socket = this.options.websocketClientProvider( this.options.url );
185184

186185
this.socket.onopen = this.onConnect.bind( this );
187186
this.socket.onmessage = this.onMessage.bind( this );
@@ -199,6 +198,7 @@ Client.prototype.disconnect = function() {
199198
Client.prototype.end = function() {
200199
this.reconnect = false;
201200
this.reconnectionTimer.stop();
201+
this.heartbeat.stop();
202202
this.disconnect();
203203
};
204204

@@ -215,7 +215,7 @@ Client.prototype.setAccessToken = function( token ) {
215215
this.connect();
216216
};
217217

218-
function Heartbeat( seconds, onBeat ) {
218+
export function Heartbeat( seconds, onBeat ) {
219219
this.count = 0;
220220
this.seconds = seconds;
221221
EventEmitter.call( this );
@@ -246,6 +246,7 @@ Heartbeat.prototype.tick = function( count ) {
246246

247247
Heartbeat.prototype.start = function() {
248248
this.stop();
249+
clearTimeout( this.timer );
249250
this.timer = setTimeout( this.onBeat.bind( this ), this.seconds * 1000 );
250251
};
251252

@@ -254,7 +255,7 @@ Heartbeat.prototype.stop = function() {
254255
clearTimeout( this.timeout );
255256
};
256257

257-
function ReconnectionTimer( interval, onTripped ) {
258+
export function ReconnectionTimer( interval, onTripped ) {
258259
EventEmitter.call( this );
259260

260261
this.started = false;
@@ -290,3 +291,13 @@ ReconnectionTimer.prototype.reset = ReconnectionTimer.prototype.stop = function(
290291
this.started = false;
291292
clearTimeout( this.timer );
292293
};
294+
295+
function defaultWebsocketClientProvider( url ) {
296+
let WebSocketClient;
297+
if ( typeof window !== 'undefined' && window.WebSocket ) {
298+
WebSocketClient = window.WebSocket;
299+
} else {
300+
WebSocketClient = require( 'websocket' ).w3cwebsocket;
301+
}
302+
return new WebSocketClient( url );
303+
}

src/simperium/index.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,25 @@ import Client from './client'
22
import Auth from './auth'
33
import * as util from './util'
44

5-
export default function( appId, token, options ) {
6-
return new Client( appId, token, options );
5+
/**
6+
* A Client is the main interface to Simperium.
7+
*
8+
* @param {String} appId - Simperium application id
9+
* @param {String} token - User access token
10+
* @param {Object} options - configuration options for the client
11+
* @param {ghostStoreProvider} [options.ghostStoreProvider=defaultGhostStoreProvider]
12+
* - factory function for creating ghost store instances
13+
* @param {bucketStoreProvider} [options.objectStoreProvider=defaultObjectStoreProvider]
14+
* - factory function for creating object store instances
15+
* @param {number} [options.heartbeatInterval=4] - heartbeat interval for maintaining connection status with Simperium.com
16+
* @param {websocketClientProvider} [options.websocketClientProvider] - WebSocket transport, if not provided tries to use window.WebSocket
17+
* @returns {Object} Simperium client.
18+
*/
19+
export default function createClient( appId, token, options ) {
20+
// Attaching an noop error listener. The behavior is to not
21+
// throw any runtime errors. This is something worth changing
22+
// but applications should be made aware when that is the case.
23+
return new Client( appId, token, options ).on( 'error', () => {} );
724
}
825

926
export { Auth, Client, util }

test/simperium/auth_test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import https from 'https'
33
import { equal, deepEqual } from 'assert'
44
import { EventEmitter } from 'events'
55

6+
const originalRequest = https.request;
67
const stub = ( respond ) => {
78
https.request = ( options, handler ) => {
89
const req = new EventEmitter()
@@ -21,6 +22,11 @@ const stubResponse = ( data ) => stub( ( body, handler ) => {
2122
describe( 'Auth', () => {
2223
let auth;
2324

25+
after( () => {
26+
// Unstub it
27+
https.request = originalRequest;
28+
} );
29+
2430
beforeEach( () => {
2531
auth = buildAuth( 'token', 'secret' );
2632
} );

test/simperium/client_test.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { Client } from '../../src/simperium/client';
2+
import * as events from 'events';
3+
import { equal, ok } from 'assert';
4+
5+
class MockWebSocket extends events.EventEmitter {
6+
constructor( ...args ) {
7+
super( ...args );
8+
this.outbox = [];
9+
}
10+
11+
close() {
12+
// noop
13+
}
14+
15+
send( msg ) {
16+
this.emit( 'send', msg );
17+
this.outbox.push( msg );
18+
}
19+
}
20+
21+
describe( 'Client', () => {
22+
let socket, client;
23+
24+
const websocketClientProvider = ( url ) => {
25+
socket = new MockWebSocket( url );
26+
return socket;
27+
}
28+
29+
beforeEach( () => {
30+
client = new Client( 'MOCK_ID', 'MOCK_TOKEN', {
31+
websocketClientProvider
32+
} );
33+
} )
34+
35+
afterEach( () => {
36+
client.end();
37+
} );
38+
39+
it( 'connects', ( done ) => {
40+
client.once( 'connect', () => {
41+
client.end();
42+
done();
43+
} );
44+
45+
socket.onopen()
46+
} );
47+
48+
it( 'emits unauthorized when unauthorized', ( done ) => {
49+
client.once( 'unauthorized', () => done() );
50+
51+
client.onUnauthorized();
52+
} );
53+
54+
it( 'ticks heartbeat on message', () => {
55+
client.onMessage( { data: 'h:5' } );
56+
equal( 5, client.heartbeat.count );
57+
} );
58+
59+
it( 'emits error when fails to send message', ( done ) => {
60+
const expected = new Error( 'nope' );
61+
socket.send = () => {
62+
throw expected;
63+
}
64+
65+
client.once( 'error', ( error ) => {
66+
equal( expected, error );
67+
done();
68+
} )
69+
70+
client.send();
71+
} )
72+
73+
it( 'sends heartbeat', () => {
74+
socket.once( 'send', ( msg ) => {
75+
equal( 'h:5', msg )
76+
} );
77+
78+
client.sendHeartbeat( 5 );
79+
} )
80+
81+
it( 'setAccessToken emits access-token and connects', ( done ) => {
82+
client.once( 'access-token', ( token ) => {
83+
client.once( 'connect', () => {
84+
done();
85+
} )
86+
87+
equal( 'mock-token', token );
88+
socket.onopen();
89+
} );
90+
91+
client.setAccessToken( 'mock-token' );
92+
} );
93+
94+
it( 'parses channel specific message', ( done ) => {
95+
client.once( 'channel:500', ( msg ) => {
96+
equal( 'c:blah', msg );
97+
done();
98+
} );
99+
client.parseMessage( { data: '500:c:blah' } );
100+
} );
101+
102+
it( 'creates bucket and sends init message', ( done ) => {
103+
client.on( 'error', done );
104+
105+
client.bucket( 'ships' );
106+
107+
socket.once( 'send', ( msg ) => {
108+
const preamble = '0:init:';
109+
110+
equal( msg.slice( 0, preamble.length ), '0:init:' );
111+
112+
const payload = JSON.parse( msg.slice( preamble.length ) );
113+
114+
equal( '1.1', payload.api );
115+
equal( '0.0.1', payload.version );
116+
equal( 'MOCK_ID', payload.app_id );
117+
equal( 'MOCK_TOKEN', payload.token );
118+
equal( 'node-simperium', payload.library );
119+
equal( 'ships', payload.name );
120+
ok(
121+
( /^node-[-a-z0-9]{1,}$/ ).test( payload.clientid ),
122+
`Invalid clientid ${ payload.clientid }`
123+
);
124+
125+
done();
126+
} );
127+
128+
// We expect the client to attempt to authorize a bucket
129+
// once the socket connects
130+
socket.onopen();
131+
} );
132+
} );
133+

test/simperium/heartbeat_test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Heartbeat } from '../../src/simperium/client';
2+
3+
import { equal } from 'assert';
4+
5+
describe( 'Heartbeat', () => {
6+
it( 'timeouts', ( done ) => {
7+
// If the heartbeat does not "tick" within
8+
// the given time, it will emit a `timeout`
9+
// This indicates that the client has not received
10+
// a heartbeat message from simperium
11+
const heartbeat = new Heartbeat( 0.03 );
12+
13+
heartbeat.once( 'timeout', () => {
14+
heartbeat.stop();
15+
done();
16+
} );
17+
18+
heartbeat.start();
19+
} );
20+
21+
it( 'ticks', ( done ) => {
22+
// When the heartbeat is "ticked" by a simperium
23+
// network message it increments its counter and
24+
// emits a 'beat'
25+
const heartbeat = new Heartbeat( 0.03, ( count ) => {
26+
equal( count, 501 );
27+
done();
28+
heartbeat.stop();
29+
} );
30+
31+
heartbeat.start();
32+
heartbeat.tick( 500 )
33+
} )
34+
} );
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ReconnectionTimer } from '../../src/simperium/client';
2+
3+
import { equal } from 'assert';
4+
5+
describe( 'ReconnectionTimer', () => {
6+
it( 'increments the interval', ( done ) => {
7+
let current = 0;
8+
const timer = new ReconnectionTimer( () => 30, ( attempt ) => {
9+
equal( current, attempt )
10+
current += 1;
11+
12+
if ( current === 3 ) {
13+
timer.stop();
14+
done();
15+
} else {
16+
timer.start();
17+
}
18+
} );
19+
20+
timer.start();
21+
} );
22+
23+
it( 'uses a default interval', () => {
24+
const timer = new ReconnectionTimer();
25+
equal( 1000, timer.interval() );
26+
} );
27+
28+
it( 'restart resets the interval and starts the timer', ( done ) => {
29+
const timer = new ReconnectionTimer( () => 0.3, ( attempt ) => {
30+
equal( 0, attempt );
31+
done();
32+
} );
33+
34+
timer.attempt = 1000;
35+
timer.restart();
36+
} )
37+
} );

0 commit comments

Comments
 (0)