/* * node-rdkafka - Node.js wrapper for RdKafka C/C++ library * * Copyright (c) 2016 Blizzard Entertainment * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE.txt file for details. */ var ProducerStream = require('../lib/producer-stream'); var t = require('assert'); var Readable = require('stream').Readable; var Emitter = require('events'); var fakeClient; module.exports = { 'ProducerStream stream': { 'beforeEach': function() { fakeClient = new Emitter(); fakeClient._isConnected = true; fakeClient._isConnecting = false; fakeClient.isConnected = function() { return true; }; fakeClient.connect = function(opts, cb) { setImmediate(function() { this.emit('ready'); }.bind(this)); return this; }; fakeClient.disconnect = function(cb) { setImmediate(function() { this.emit('disconnected'); }.bind(this)); return this; }; fakeClient.poll = function() { return this; }; fakeClient.setPollInterval = function() { return this; }; }, 'exports a stream class': function() { t.equal(typeof(ProducerStream), 'function'); }, 'in buffer mode': { 'requires a topic be provided when running in buffer mode': function() { t.throws(function() { var x = new ProducerStream(fakeClient, {}); }); }, 'can be instantiated': function() { t.equal(typeof new ProducerStream(fakeClient, { topic: 'topic' }), 'object'); }, 'does not run connect if the client is already connected': function(cb) { fakeClient.connect = function() { t.fail('Should not run connect if the client is already connected'); }; var stream = new ProducerStream(fakeClient, { topic: 'topic' }); setTimeout(cb, 10); }, 'does run connect if the client is not already connected': function(cb) { fakeClient._isConnected = false; fakeClient.isConnected = function() { return false; }; fakeClient.once('ready', cb); var stream = new ProducerStream(fakeClient, { topic: 'topic' }); }, 'forwards connectOptions to client options when provided': function(cb) { var testClientOptions = { timeout: 3000 }; fakeClient._isConnected = false; fakeClient.isConnected = function() { return false; }; var fakeConnect = fakeClient.connect; fakeClient.connect = function(opts, callback) { t.deepEqual(opts, testClientOptions); cb(); }; var stream = new ProducerStream(fakeClient, { topic: 'topic', connectOptions: testClientOptions }); }, 'automatically disconnects when autoclose is not provided': function(cb) { fakeClient.once('disconnected', cb); var stream = new ProducerStream(fakeClient, { topic: 'topic' }); stream.end(); }, 'does not automatically disconnect when autoclose is set to false': function(done) { fakeClient.once('disconnected', function() { t.fail('Should not run disconnect'); }); var stream = new ProducerStream(fakeClient, { topic: 'topic', autoClose: false }); stream.end(); setTimeout(done, 10); }, 'properly reads off the fake client': function(done) { var message; fakeClient.produce = function(topic, partition, message, key) { t.equal('topic', topic); t.equal(message.toString(), 'Awesome'); t.equal(Buffer.isBuffer(message), true); done(); }; var stream = new ProducerStream(fakeClient, { topic: 'topic' }); stream.on('error', function(err) { t.fail(err); }); stream.write(Buffer.from('Awesome')); }, 'passes a topic string if options are not provided': function(done) { var message; fakeClient.produce = function(topic, partition, message, key) { t.equal('topic', topic); t.equal(message.toString(), 'Awesome'); t.equal(Buffer.isBuffer(message), true); done(); }; var stream = new ProducerStream(fakeClient, { topic: 'topic' }); stream.on('error', function(err) { t.fail(err); }); stream.write(Buffer.from('Awesome')); }, 'properly handles queue errors': function(done) { var message; var first = true; fakeClient.produce = function(topic, partition, message, key) { t.equal('topic', topic); t.equal(message.toString(), 'Awesome'); t.equal(Buffer.isBuffer(message), true); if (first) { first = false; var err = new Error('Queue full'); err.code = -184; throw err; } else { done(); } }; var stream = new ProducerStream(fakeClient, { topic: 'topic' }); stream.on('error', function(err) { t.fail(err); }); stream.write(Buffer.from('Awesome')); }, 'errors out when a non-queue related error occurs': function(done) { fakeClient.produce = function(topic, partition, message, key) { var err = new Error('ERR_MSG_SIZE_TOO_LARGE '); err.code = 10; throw err; }; fakeClient.on('disconnected', function() { done(); }); var stream = new ProducerStream(fakeClient, { topic: 'topic' }); stream.on('error', function(err) { t.equal(err.code, 10, 'Error was unexpected'); // This is good }); stream.write(Buffer.from('Awesome')); }, 'errors out when a non-queue related error occurs but does not disconnect if autoclose is false': function(done) { fakeClient.produce = function(topic, partition, message, key) { var err = new Error('ERR_MSG_SIZE_TOO_LARGE '); err.code = 10; throw err; }; fakeClient.on('disconnected', function() { t.fail('Should not try to disconnect'); }); var stream = new ProducerStream(fakeClient, { topic: 'topic', autoClose: false }); stream.on('error', function(err) { t.equal(err.code, 10, 'Error was unexpected'); // This is good }); stream.write(Buffer.from('Awesome')); setTimeout(done, 10); }, 'properly reads more than one message in order': function(done) { var message; var currentMessage = 0; fakeClient.produce = function(topic, partition, message, key) { currentMessage++; t.equal('topic', topic); t.equal(message.toString(), 'Awesome' + currentMessage); t.equal(Buffer.isBuffer(message), true); if (currentMessage === 2) { done(); } }; var stream = new ProducerStream(fakeClient, { topic: 'topic' }); stream.on('error', function(err) { t.fail(err); }); stream.write(Buffer.from('Awesome1')); stream.write(Buffer.from('Awesome2')); }, 'can be piped into a readable': function(done) { var message; var currentMessage = 0; var iteration = 0; var readable = new Readable({ read: function(size) { iteration++; if (iteration > 1) { } else { this.push('Awesome1'); this.push('Awesome2'); } } }); fakeClient.produce = function(topic, partition, message, key) { currentMessage++; t.equal('topic', topic); t.equal(message.toString(), 'Awesome' + currentMessage); t.equal(Buffer.isBuffer(message), true); if (currentMessage === 2) { done(); } }; var stream = new ProducerStream(fakeClient, { topic: 'topic' }); stream.on('error', function(err) { t.fail(err); }); readable.pipe(stream); }, 'can drain buffered chunks': function(done) { var message; var currentMessage = 0; fakeClient.produce = function(topic, partition, message, key) { currentMessage++; t.equal('topic', topic); t.equal(message.toString(), 'Awesome' + currentMessage); t.equal(Buffer.isBuffer(message), true); if (currentMessage === 3) { done(); } }; var stream = new ProducerStream(fakeClient, { topic: 'topic' }); stream.on('error', function(err) { t.fail(err); }); fakeClient._isConnected = false; fakeClient._isConnecting = true; fakeClient.isConnected = function() { return false; }; stream.write(Buffer.from('Awesome1')); stream.write(Buffer.from('Awesome2')); stream.write(Buffer.from('Awesome3')); fakeClient._isConnected = true; fakeClient._isConnecting = false; fakeClient.isConnected = function() { return true; }; fakeClient.connect(); }, }, 'in objectMode': { 'can be instantiated': function() { t.equal(typeof new ProducerStream(fakeClient, { objectMode: true }), 'object'); }, 'properly produces message objects': function(done) { var _timestamp = Date.now(); var _opaque = { foo: 'bar' }; var _headers = { header: 'header value' }; fakeClient.produce = function(topic, partition, message, key, timestamp, opaque, headers) { t.equal('topic', topic); t.equal(message.toString(), 'Awesome'); t.equal(Buffer.isBuffer(message), true); t.equal(partition, 10); t.equal(key, 'key'); t.deepEqual(_opaque, opaque); t.deepEqual(_timestamp, timestamp); t.deepEqual(_headers, headers); done(); }; var stream = new ProducerStream(fakeClient, { objectMode: true }); stream.on('error', function(err) { t.fail(err); }); stream.write({ topic: 'topic', value: Buffer.from('Awesome'), partition: 10, key: 'key', timestamp: _timestamp, opaque: _opaque, headers: _headers }); }, 'properly handles queue errors': function(done) { var message; var first = true; fakeClient.produce = function(topic, partition, message, key) { t.equal('topic', topic); t.equal(message.toString(), 'Awesome'); t.equal(Buffer.isBuffer(message), true); t.equal(partition, 10); t.equal(key, 'key'); if (first) { first = false; var err = new Error('Queue full'); err.code = -184; throw err; } else { done(); } }; var stream = new ProducerStream(fakeClient, { objectMode: true }); stream.on('error', function(err) { t.fail(err); }); stream.write({ topic: 'topic', value: Buffer.from('Awesome'), partition: 10, key: 'key' }); }, 'errors out when a non-queue related error occurs': function(done) { fakeClient.produce = function(topic, partition, message, key) { var err = new Error('ERR_MSG_SIZE_TOO_LARGE '); err.code = 10; throw err; }; fakeClient.on('disconnected', function() { done(); }); var stream = new ProducerStream(fakeClient, { objectMode: true }); stream.on('error', function(err) { t.equal(err.code, 10, 'Error was unexpected'); // This is good }); stream.write(Buffer.from('Awesome')); }, 'errors out when a non-queue related error occurs but does not disconnect if autoclose is false': function(done) { fakeClient.produce = function(topic, partition, message, key) { var err = new Error('ERR_MSG_SIZE_TOO_LARGE '); err.code = 10; throw err; }; fakeClient.on('disconnected', function() { t.fail('Should not try to disconnect'); }); var stream = new ProducerStream(fakeClient, { objectMode: true, autoClose: false }); stream.on('error', function(err) { t.equal(err.code, 10, 'Error was unexpected'); // This is good }); stream.write({ value: Buffer.from('Awesome'), topic: 'topic' }); setTimeout(done, 10); }, 'properly reads more than one message in order': function(done) { var message; var currentMessage = 0; fakeClient.produce = function(topic, partition, message, key) { currentMessage++; t.equal('topic', topic); t.equal(message.toString(), 'Awesome' + currentMessage); t.equal(Buffer.isBuffer(message), true); if (currentMessage === 2) { done(); } }; var stream = new ProducerStream(fakeClient, { objectMode: true }); stream.on('error', function(err) { t.fail(err); }); stream.write({ value: Buffer.from('Awesome1'), topic: 'topic' }); stream.write({ value: Buffer.from('Awesome2'), topic: 'topic' }); }, 'can be piped into a readable': function(done) { var message; var currentMessage = 0; var iteration = 0; var readable = new Readable({ objectMode: true, read: function(size) { iteration++; if (iteration > 1) { } else { this.push({ topic: 'topic', value: Buffer.from('Awesome1') }); this.push({ topic: 'topic', value: Buffer.from('Awesome2') }); } } }); fakeClient.produce = function(topic, partition, message, key) { currentMessage++; t.equal('topic', topic); t.equal(message.toString(), 'Awesome' + currentMessage); t.equal(Buffer.isBuffer(message), true); if (currentMessage === 2) { done(); } }; var stream = new ProducerStream(fakeClient, { objectMode: true }); stream.on('error', function(err) { t.fail(err); }); readable.pipe(stream); }, 'can drain buffered messages': function(done) { var message; var currentMessage = 0; fakeClient.produce = function(topic, partition, message, key) { currentMessage++; t.equal('topic', topic); t.equal(message.toString(), 'Awesome' + currentMessage); t.equal(Buffer.isBuffer(message), true); if (currentMessage === 3) { done(); } }; var stream = new ProducerStream(fakeClient, { objectMode: true }); stream.on('error', function(err) { t.fail(err); }); fakeClient._isConnected = false; fakeClient._isConnecting = true; fakeClient.isConnected = function() { return false; }; stream.write({ value: Buffer.from('Awesome1'), topic: 'topic' }); stream.write({ value: Buffer.from('Awesome2'), topic: 'topic' }); stream.write({ value: Buffer.from('Awesome3'), topic: 'topic' }); fakeClient._isConnected = true; fakeClient._isConnecting = false; fakeClient.isConnected = function() { return true; }; fakeClient.connect(); }, 'properly handles queue errors while draining': function(done) { var message; var currentMessage = 0; fakeClient.produce = function(topic, partition, message, key) { currentMessage++; if (currentMessage === 3) { var err = new Error('Queue full'); err.code = -184; throw err; } else if (currentMessage === 4) { done(); } }; var stream = new ProducerStream(fakeClient, { objectMode: true }); stream.on('error', function(err) { t.fail(err); }); fakeClient._isConnected = false; fakeClient._isConnecting = true; fakeClient.isConnected = function() { return false; }; stream.write({ value: Buffer.from('Awesome1'), topic: 'topic' }); stream.write({ value: Buffer.from('Awesome2'), topic: 'topic' }); stream.write({ value: Buffer.from('Awesome3'), topic: 'topic' }); stream.write({ value: Buffer.from('Awesome4'), topic: 'topic' }); fakeClient._isConnected = true; fakeClient._isConnecting = false; fakeClient.isConnected = function() { return true; }; fakeClient.connect(); }, 'errors out for non-queue related errors while draining': function (done) { var currentMessage = 0; fakeClient.produce = function(topic, partition, message, key) { currentMessage++; if (currentMessage === 3) { var err = new Error('ERR_MSG_SIZE_TOO_LARGE '); err.code = 10; throw err; } }; fakeClient.on('disconnected', function() { done(); }); var stream = new ProducerStream(fakeClient, { objectMode: true }); stream.on('error', function(err) { t.equal(err.code, 10, 'Error was unexpected'); // This is good }); fakeClient._isConnected = false; fakeClient._isConnecting = true; fakeClient.isConnected = function() { return false; }; stream.write({ value: Buffer.from('Awesome1'), topic: 'topic' }); stream.write({ value: Buffer.from('Awesome2'), topic: 'topic' }); stream.write({ value: Buffer.from('Awesome3'), topic: 'topic' }); stream.write({ value: Buffer.from('Awesome4'), topic: 'topic' }); fakeClient._isConnected = true; fakeClient._isConnecting = false; fakeClient.isConnected = function() { return true; }; fakeClient.connect(); }, } } };