@button/divvy-client
Version:
NodeJS client to the Divvy quota service.
350 lines (277 loc) • 8.75 kB
JavaScript
;
const assert = require('assert');
const net = require('net');
const sinon = require('sinon');
const carrier = require('carrier');
const Errors = require('../src/errors');
const Client = require('../src/client');
const Bluebird = require('bluebird');
describe('src/client', () => {
describe('general tests', () => {
// Fake server.
var server;
// Test client.
var client;
// String the server should received. Set in tests.
var expectedClientMessage;
// String the server should send back. Set in tests.
var mockServerResponse;
// The server's client connection.
var clientConnection;
beforeEach((done) => {
// Create a server on port 0 (ephemeral / randomly-selected port)
server = net.createServer((conn) => {
clientConnection = conn;
carrier.carry(conn, (line) => {
assert.equal(expectedClientMessage, line);
conn.write(mockServerResponse + '\n');
});
});
// Once the server is bound, connect a client to it then finish.
server.on('listening', () => {
client = new Client('', server.address().port, {
autoReconnect: false,
throttleConnect: false
});
client.on('connected', done);
client.connect();
});
server.listen(0);
});
it('parses an allowed response', (done) => {
expectedClientMessage = 'HIT "name"="test" "path"="123"';
mockServerResponse = 'OK true 1 2';
client.hit({ name: 'test', path: '123'}).then((response) => {
assert.deepEqual(response, {
isAllowed: true,
currentCredit: 1,
nextResetSeconds: 2
});
done();
}).catch(done);
});
it('sorts HIT operation keys', (done) => {
expectedClientMessage = 'HIT "name"="test" "path"="123"';
mockServerResponse = 'OK true 1 2';
client.hit({ path: '123', name: 'test' }).then((response) => {
assert.deepEqual(response, {
isAllowed: true,
currentCredit: 1,
nextResetSeconds: 2
});
done();
}).catch(done);
});
it('parses a not-allowed response', (done) => {
expectedClientMessage = 'HIT "name"="test" "path"="123"';
mockServerResponse = 'OK false 0 0';
client.hit({ name: 'test', path: '123'}).then((response) => {
assert.deepEqual(response, {
isAllowed: false,
currentCredit: 0,
nextResetSeconds: 0
});
done();
}).catch(done);
});
it('emits an event when disconnected', (done) => {
client.on('disconnected', () => {
assert.equal(false, client.connected);
done();
});
setTimeout(() => {
clientConnection.destroy();
}, 10);
});
it('client automatically connects if needed', (done) => {
client.close();
// Create a new client and never explicitly call connect()
const myClient = new Client('', server.address().port);
expectedClientMessage = 'HIT "name"="test" "path"="123"';
mockServerResponse = 'OK true 1 2';
myClient.hit({ path: '123', name: 'test' }).then((response) => {
assert.deepEqual(response, {
isAllowed: true,
currentCredit: 1,
nextResetSeconds: 2
});
assert(myClient.connected);
done();
}).catch(done);
});
});
describe('timeout tests', () => {
// TODO(mikey): These tests should use a fake clock.
// Fake server.
var server;
// Test client.
var client;
beforeEach((done) => {
// A server that always succeeds, but delays 100ms.
server = net.createServer((conn) => {
carrier.carry(conn, () => {
setTimeout(() => {
conn.write('OK true 1 2\n');
}, 50);
});
});
server.on('listening', () => {
client = new Client('', server.address().port);
client.on('connected', done);
client.connect();
});
server.listen(0);
});
it('processes timeouts correctly', (done) => {
const promises = [
client.hit({}, 50).catch((err) => err),
client.hit({}, 60).catch((err) => err),
client.hit({}, 25).catch((err) => err),
];
Bluebird.all(promises).then((results) => {
assert(results[0] instanceof Errors.TimeoutError);
assert(!(results[1] instanceof Error));
assert.deepEqual({ isAllowed: true, currentCredit: 1, nextResetSeconds: 2 }, results[1]);
assert(results[2] instanceof Errors.TimeoutError);
done();
}).catch(done);
});
});
describe('autoreconnect tests', () => {
// Fake server.
var server;
var serverPort;
var clock;
beforeEach((done) => {
// A server that tells us when a client has connected.
server = net.createServer(function(conn) {
this.emit('client-connected', conn);
});
server.on('listening', () => {
serverPort = server.address().port;
done();
});
server.listen(0);
clock = sinon.useFakeTimers();
});
afterEach(() => {
clock.restore();
});
it('reconnects automatically', (done) => {
var reconnectCount = 0;
const client = new Client('', serverPort, {
autoReconnect: true,
throttleConnect: false
});
// Client will keep reconnecting.
server.on('client-connected', (conn) => {
reconnectCount++;
if (reconnectCount === 10) {
done();
return;
}
conn.destroy();
});
client.connect();
});
it('throttles reconnect', (done) => {
const client = new Client('', serverPort, {
autoReconnect: true,
throttleConnect: true
});
// Client will keep reconnecting.
server.on('client-connected', (conn) => {
conn.destroy();
});
client.once('disconnected', () => {
assert(!client.connected);
clock.tick(1000);
client.once('connected', () => {
assert(client.connected);
done();
});
});
client.connect();
});
});
describe('maxPendingRequests tests', () => {
// Fake server.
var server;
var serverPort;
beforeEach((done) => {
server = net.createServer(function(conn) {
this.emit('client-connected', conn);
carrier.carry(conn, () => {
setTimeout(() => {
conn.write('OK true 1 2\n');
}, 100);
});
});
server.on('listening', () => {
serverPort = server.address().port;
done();
});
server.listen(0);
});
it('rejects after 3 pending requests', (done) => {
const client = new Client('', serverPort, {
autoReconnect: true,
maxPendingRequests: 3
});
const promises = [
client.hit({}).catch((err) => err),
client.hit({}).catch((err) => err),
client.hit({}).catch((err) => err),
client.hit({}).catch((err) => err),
];
Bluebird.all(promises).then((results) => {
assert(!(results[0] instanceof Error));
assert(!(results[1] instanceof Error));
assert(!(results[2] instanceof Error));
assert(results[3] instanceof Errors.BacklogError);
done();
}).catch(done);
});
// Test is flaky on travis :-(
xit('rejects in-flight requests when disconnected', (done) => {
const client = new Client('', serverPort, {
autoReconnect: false,
maxPendingRequests: 3
});
server.once('client-connected', (conn) => {
conn.destroy();
});
client.once('connected', () => {
const promises = [
client.hit({}).catch((err) => err),
client.hit({}).catch((err) => err)
];
Bluebird.all(promises).then((results) => {
assert(results[0] instanceof Errors.DisconnectedError);
assert(results[1] instanceof Errors.DisconnectedError);
done();
}).catch(done);
});
client.connect();
});
});
describe('Client.Stub', function() {
beforeEach(function() {
this.client = new Client.Stub();
});
it('is a no-op hit', function(done) {
this.client.hit({}).then(quota => {
assert.equal(quota.isAllowed, true);
assert.equal(quota.currentCredit, 0);
assert.equal(quota.nextResetSeconds, 0);
done();
});
});
it('does not error when calling connect', function() {
this.client.connect();
});
it('does not error when calling close', function() {
this.client.close();
});
});
});