particle-api-js
Version:
Particle API Client
263 lines (203 loc) • 9.04 kB
JavaScript
const { sinon, expect } = require('./test-setup');
const http = require('http');
const { EventEmitter } = require('events');
const EventStream = require('../src/EventStream');
describe('EventStream', () => {
afterEach(() => {
sinon.restore();
});
function makeRequest() {
const fakeRequest = new EventEmitter();
fakeRequest.end = sinon.spy();
fakeRequest.setTimeout = sinon.spy();
return fakeRequest;
}
function makeResponse(statusCode) {
const fakeResponse = new EventEmitter();
fakeResponse.statusCode = statusCode;
return fakeResponse;
}
describe('constructor', () => {
it('creates an EventStream objects', () => {
const eventStream = new EventStream('uri', 'token');
expect(eventStream).to.own.include({ uri: 'uri', token: 'token' });
});
});
describe('connect', () => {
it('successfully connects to http', () => {
sinon.useFakeTimers({ shouldAdvanceTime: true });
const fakeRequest = makeRequest();
sinon.stub(http, 'request').callsFake(() => {
setImmediate(() => {
const fakeResponse = makeResponse(200);
fakeRequest.emit('response', fakeResponse);
});
return fakeRequest;
});
const eventStream = new EventStream('http://hostname:8080/path', 'token');
return eventStream.connect().then(() => {
expect(http.request).to.have.been.calledWith({
hostname: 'hostname',
protocol: 'http:',
path: '/path?nonce=0',
headers: {
'Authorization': 'Bearer token'
},
method: 'get',
port: 8080,
mode: 'prefer-streaming'
});
});
});
it('returns http errors on connect', () => {
sinon.useFakeTimers({ shouldAdvanceTime: true });
const fakeRequest = makeRequest();
sinon.stub(http, 'request').callsFake(() => {
setImmediate(() => {
const fakeResponse = makeResponse(500);
fakeRequest.emit('response', fakeResponse);
setImmediate(() => {
fakeResponse.emit('data', '{"error":"unknown"}');
fakeResponse.emit('end');
});
});
return fakeRequest;
});
const eventStream = new EventStream('http://hostname:8080/path', 'token');
return eventStream.connect().then(() => {
throw new Error('expected to throw error');
}, (reason) => {
expect(reason).to.eql({
statusCode: 500,
errorDescription: 'HTTP error 500 from http://hostname:8080/path',
body: {
error: 'unknown'
}
});
});
});
});
describe('parse', () => {
let eventStream;
beforeEach(() => {
eventStream = new EventStream();
sinon.stub(eventStream, 'parseEventStreamLine');
});
it('accumulates date into the buffer before parsing line', () => {
eventStream.parse('wo');
eventStream.parse('rd');
expect(eventStream.buf).to.eql('word');
expect(eventStream.parseEventStreamLine).not.to.have.been.called;
});
it('parses a line ending with \\n', () => {
const line = 'field: value\n';
eventStream.parse(line);
expect(eventStream.parseEventStreamLine).to.have.been.calledWith(0, line.indexOf(':'), line.indexOf('\n'));
});
it('parses 2 lines ending with \\n', () => {
const line1 = 'field: value\n';
const line2 = 'field2: value2\n';
eventStream.parse(line1 + line2);
expect(eventStream.parseEventStreamLine).to.have.been.calledWith(0, line1.indexOf(':'), line1.indexOf('\n'));
expect(eventStream.parseEventStreamLine).to.have.been.calledWith(line1.length, line2.indexOf(':'), line2.indexOf('\n'));
});
it('parses a line ending with \\r\\n', () => {
const line = 'field: value\r\n';
eventStream.parse(line);
expect(eventStream.parseEventStreamLine).to.have.been.calledWith(0, line.indexOf(':'), line.indexOf('\r'));
});
it('parses 2 lines ending with \\r\\n', () => {
const line1 = 'field: value\r\n';
const line2 = 'field2: value2\r\n';
eventStream.parse(line1 + line2);
expect(eventStream.parseEventStreamLine).to.have.been.calledWith(0, line1.indexOf(':'), line1.indexOf('\r'));
expect(eventStream.parseEventStreamLine).to.have.been.calledWith(line1.length, line2.indexOf(':'), line2.indexOf('\r'));
});
it('clears buffer after parsing full lines', () => {
eventStream.parse('field: value\n');
expect(eventStream.buf).to.eql('');
});
it('keeps partial lines in buffer after parsing full lines', () => {
eventStream.parse('field: value\nfield2');
expect(eventStream.buf).to.eql('field2');
});
});
describe('parseEventStreamLine', () => {
let eventStream;
beforeEach(() => {
eventStream = new EventStream();
});
it('ignores comments', () => {
// comments starts with : at column 0
const line = ':ok\n';
eventStream.buf = line;
eventStream.parseEventStreamLine(0, line.indexOf(':'), line.indexOf('\n'));
expect(eventStream.event).not.to.be.ok;
expect(eventStream.data).to.be.eql('');
});
it('saves event name', () => {
const line = 'event: testevent\n';
eventStream.buf = line;
eventStream.parseEventStreamLine(0, line.indexOf(':'), line.indexOf('\n'));
expect(eventStream.event).to.be.true;
expect(eventStream.eventName).to.eql('testevent');
});
it('saves event data', () => {
const line = 'data: {"data":"test"}\n';
eventStream.buf = line;
eventStream.parseEventStreamLine(0, line.indexOf(':'), line.indexOf('\n'));
expect(eventStream.data).to.eql('{"data":"test"}\n');
});
it('saves event name and data on separate lines', () => {
const lines = ['event: testevent\n', 'data: {"data":"test"}\n'];
for (let line of lines) {
eventStream.buf = line;
eventStream.parseEventStreamLine(0, line.indexOf(':'), line.indexOf('\n'));
}
expect(eventStream.event).to.be.true;
expect(eventStream.eventName).to.eql('testevent');
expect(eventStream.data).to.eql('{"data":"test"}\n');
});
it('emits event on blank line after saving event name and data', () => {
const handler = sinon.spy();
eventStream.event = true;
eventStream.eventName = 'testevent';
eventStream.data = '{"data":"test"}\n';
eventStream.on('event', handler);
eventStream.parseEventStreamLine(0, -1, 0);
expect(handler).to.have.been.calledWith({
name: 'testevent',
data: 'test'
});
});
it('emits error if the event handler crashes', () => {
const errorHandler = sinon.spy();
eventStream.event = true;
eventStream.eventName = 'testevent';
eventStream.data = '{"data":"test"}\n';
eventStream.on('error', errorHandler);
eventStream.on('event', () => {
throw new Error('failed!');
});
eventStream.parseEventStreamLine(0, -1, 0);
expect(errorHandler).to.have.been.called;
});
it('clears the event after emitting it', () => {
eventStream.event = true;
eventStream.eventName = 'testevent';
eventStream.data = '{"data":"test"}\n';
eventStream.parseEventStreamLine(0, -1, 0);
expect(eventStream.event).to.be.false;
expect(eventStream.eventName).to.be.undefined;
expect(eventStream.data).to.eql('');
});
it('ignores multiple blank lines in succession', () => {
const handler = sinon.spy();
eventStream.on('event', handler);
eventStream.parseEventStreamLine(0, -1, 0);
eventStream.parseEventStreamLine(0, -1, 0);
eventStream.parseEventStreamLine(0, -1, 0);
expect(handler).not.to.have.been.called;
});
});
});