dnssd
Version:
Bonjour/Avahi-like service discovery in pure JavaScript
610 lines (447 loc) • 17.5 kB
JavaScript
const _ = require('lodash');
const chai = require('chai');
const expect = chai.expect;
const rewire = require('rewire');
const sinon = require('sinon');
const sinonChai = require('sinon-chai');
chai.use(sinonChai);
const dir = process['test-dir'] || '../../src';
const Packet = require(dir + '/Packet');
const ResourceRecord = require(dir + '/ResourceRecord');
const QueryRecord = require(dir + '/QueryRecord');
const Fake = require('../Fake');
const NetworkInterface = rewire(dir + '/NetworkInterface');
describe('NetworkInterface', function() {
// interface addresses, same form as os.networkInterfaces() output
const interfaceAddresses = {
'Ethernet':
[ { address: 'fe80::73b6:73b6:73b6:73b6',
family: 'IPv6',
internal: false },
{ address: '169.254.100.175',
family: 'IPv4',
internal: false } ],
'Wi-Fi':
[ { address: 'fe80::7b30:7b30:7b30:7b30',
family: 'IPv6',
internal: false },
{ address: '192.168.1.5',
family: 'IPv4',
internal: false } ],
'Loopback':
[ { address: '::1',
family: 'IPv6',
internal: true },
{ address: '127.0.0.1',
family: 'IPv4',
internal: true } ],
};
const osStub = {networkInterfaces: sinon.stub().returns(interfaceAddresses)};
NetworkInterface.__set__('os', osStub);
beforeEach(function() {
NetworkInterface.__set__('activeInterfaces', {});
});
describe('::get()', function() {
it('should make a new NetworkInterface for `any`', function() {
const intf = NetworkInterface.get();
expect(intf).to.be.instanceof(NetworkInterface);
});
it('should return existing interface', function() {
const intf = NetworkInterface.get();
const copy = NetworkInterface.get();
expect(intf).to.equal(copy); // same object
});
it('should make a new NetworkInterface using a given multicast interface name', function() {
const intf = NetworkInterface.get('Ethernet');
const copy = NetworkInterface.get('Ethernet');
expect(intf).to.be.instanceof(NetworkInterface);
expect(intf).to.equal(copy); // same object
});
it('should make a new NetworkInterface using a given multicast IPv4 address', function() {
const intf = NetworkInterface.get('192.168.1.5');
const copy = NetworkInterface.get('192.168.1.5');
expect(intf).to.be.instanceof(NetworkInterface);
expect(intf).to.equal(copy); // same object
});
it('should throw with a decent error msg on bad input', function() {
const one = NetworkInterface.get.bind(null, 'bad input'); // unknown interface
const two = NetworkInterface.get.bind(null, '111.222.333.444'); // unknown address
expect(one).to.throw();
expect(two).to.throw();
});
});
describe('::getLoopback()', function() {
it('should return the name of the loopback interface, if any', function() {
expect(NetworkInterface.getLoopback()).to.equal('Loopback');
});
});
describe('#constructor()', function() {
it('should init with proper defaults', function() {
const intf = new NetworkInterface();
expect(intf._usingMe).to.equal(0);
expect(intf._isBound).to.equal(false);
expect(intf._sockets).to.be.empty;
});
});
describe('#bind()', function() {
it('should resolve when every socket is bound', function(done) {
const intf = new NetworkInterface();
sinon.stub(intf, '_bindSocket').returns(Promise.resolve());
intf.bind().then(() => {
expect(intf._isBound).to.be.true;
expect(intf._usingMe).to.equal(1);
done();
});
});
it('should reject if binding fails', function(done) {
const intf = new NetworkInterface();
sinon.stub(intf, '_bindSocket').returns(Promise.reject());
intf.bind().catch(() => {
expect(intf._isBound).to.be.false;
expect(intf._usingMe).to.equal(0);
done();
});
});
it('should resolve immediately if already bound', function(done) {
const intf = new NetworkInterface();
sinon.stub(intf, '_bindSocket').returns(Promise.resolve());
// bind twice, 2nd bind should be immediate with no re-bind
intf.bind()
.then(() => {
expect(intf._bindSocket).to.have.callCount(1);
intf.bind();
})
.then(() => {
expect(intf._bindSocket).to.have.callCount(1);
expect(intf._usingMe).to.equal(2);
done();
});
});
it('should prevent concurrent binds, only binding once', function(done) {
const intf = new NetworkInterface();
sinon.stub(intf, '_bindSocket').returns(Promise.resolve());
const onSuccess = _.after(2, () => {
expect(intf._bindSocket).to.have.callCount(1);
expect(intf._usingMe).to.equal(2);
expect(intf._isBound).to.be.true;
done();
});
intf.bind().then(onSuccess);
intf.bind().then(onSuccess);
});
it('should fail on both concurrents if binding fails', function(done) {
const intf = new NetworkInterface();
sinon.stub(intf, '_bindSocket').returns(Promise.reject());
const onFail = _.after(2, () => {
expect(intf._usingMe).to.equal(0);
expect(intf._isBound).to.be.false;
done();
});
intf.bind().catch(onFail);
intf.bind().catch(onFail);
});
});
describe('#_bindSocket()', function() {
const socket = new Fake.Socket();
socket.address.returns({});
const dgram = {createSocket: sinon.stub().returns(socket)};
let revert;
before(function() {
revert = NetworkInterface.__set__('dgram', dgram);
});
after(function() {
revert();
});
beforeEach(function() {
socket.reset();
dgram.createSocket.reset();
});
it('should create IPv4 socket and resolve when bound', function(done) {
const intf = new NetworkInterface();
intf._bindSocket().then(() => {
expect(dgram.createSocket).to.have.been.calledWithMatch({type: 'udp4'});
done();
});
socket.emit('listening');
});
it('should `setMulticastInterface` if needed', function(done) {
const intf = new NetworkInterface('Ethernet', '169.254.100.175');
intf._bindSocket().then(() => {
expect(intf._sockets[0].setMulticastInterface)
.to.have.been.calledWith('169.254.100.175');
done();
});
socket.emit('listening');
});
it('should reject if bind fails', function(done) {
const intf = new NetworkInterface();
sinon.stub(intf, '_onError');
intf._bindSocket().catch(() => {
expect(intf._onError).to.not.have.been.called;
expect(intf._sockets).to.be.empty;
done();
});
socket.emit('error');
});
it('should _onError when socket closes unexpectedly', function(done) {
const intf = new NetworkInterface();
sinon.stub(intf, '_onError', () => done());
intf._bindSocket().then(() => {
socket.emit('close');
});
socket.emit('listening');
});
it('should _onError on socket errors', function(done) {
const intf = new NetworkInterface();
sinon.stub(intf, '_onError', () => done());
intf._bindSocket().then(() => {
socket.emit('error');
});
socket.emit('listening');
});
it('should _onMessage when socket receives a message', function(done) {
const intf = new NetworkInterface();
sinon.stub(intf, '_onMessage', () => done());
intf._bindSocket();
socket.emit('message', 'fake msg', {fake: 'rinfo'});
});
});
describe('#_addToCache()', function() {
it('should add records to cache & flush unique records', function() {
const intf = new NetworkInterface();
const unique = new ResourceRecord.TXT({name: 'TXT'});
const shared = new ResourceRecord.PTR({name: 'PTR'});
const packet = new Packet();
packet.setAnswers([unique]);
packet.setAdditionals([shared]);
sinon.spy(intf.cache, 'add');
sinon.spy(intf.cache, 'flushRelated');
intf._addToCache(packet);
expect(intf.cache.flushRelated).to.have.been
.calledOnce
.calledWith(unique);
expect(intf.cache.add).to.have.been
.calledTwice
.calledWith(unique)
.calledWith(shared);
});
});
describe('#_onMessage()', function() {
const msg = (new Packet()).toBuffer();
const rinfo = {address: '1.1.1.1', port: 5353};
const PacketConstructor = sinon.stub();
let revert;
before(function() {
revert = NetworkInterface.__set__('Packet', PacketConstructor);
});
after(function() {
revert();
});
afterEach(function() {
PacketConstructor.resetBehavior();
});
it('should emit answer event on answer messages', function(done) {
const intf = new NetworkInterface();
const answerPacket = new Packet();
answerPacket.setAnswers([new ResourceRecord.TXT({name: 'TXT'})]);
answerPacket.setResponseBit();
PacketConstructor.returns(answerPacket);
intf.on('answer', (arg) => {
expect(arg).to.equal(answerPacket);
done();
});
intf._onMessage(msg, rinfo);
});
it('should emit probe event on probe messages', function(done) {
const intf = new NetworkInterface();
const probePacket = new Packet();
probePacket.setQuestions([new QueryRecord({name: 'TXT'})]);
probePacket.setAuthorities([new ResourceRecord.TXT({name: 'TXT'})]);
PacketConstructor.returns(probePacket);
intf.on('probe', (arg) => {
expect(arg).to.equal(probePacket);
done();
});
intf._onMessage(msg, rinfo);
});
it('should emit query event on query messages', function(done) {
const intf = new NetworkInterface();
const queryPacket = new Packet();
queryPacket.setQuestions([new QueryRecord({name: 'TXT'})]);
PacketConstructor.returns(queryPacket);
intf.on('query', (arg) => {
expect(arg).to.equal(queryPacket);
done();
});
intf._onMessage(msg, rinfo);
});
it('should skip over packets that are invalid', function() {
const intf = new NetworkInterface();
const invalidPacket = new Packet();
invalidPacket.setQuestions([new QueryRecord({name: 'TXT'})]);
sinon.stub(invalidPacket, 'isValid').returns(false);
PacketConstructor.returns(invalidPacket);
sinon.stub(intf, 'emit');
intf._onMessage(msg, rinfo);
expect(intf.emit).to.not.have.been.called;
});
it('should keep track of previously sent packets when debugging', function() {
const debug = function() {};
debug.isEnabled = true;
debug.verbose = function() {};
debug.verbose.isEnabled = true;
const revertDebug = NetworkInterface.__set__('debug', debug);
PacketConstructor.returns(new Packet());
const intf = new NetworkInterface();
intf._buffers.push((new Packet()).toBuffer());
intf._onMessage(msg, rinfo);
expect(intf._buffers).to.be.empty;
revertDebug();
});
});
describe('#hasRecentlySent()', function() {
it('should be true if recently sent / false if not', sinon.test(function() {
const intf = new NetworkInterface();
const SRV = new ResourceRecord.SRV({name: 'SRV'});
intf._history.add(SRV);
expect(intf.hasRecentlySent(SRV)).to.be.true;
expect(intf.hasRecentlySent(SRV, 5)).to.be.true;
this.clock.tick(10 * 1000);
expect(intf.hasRecentlySent(SRV, 5)).to.be.false;
}));
});
describe('#send()', function() {
const answer = new ResourceRecord.TXT({name: 'Answer Record'});
const question = new QueryRecord({name: 'Question Record'});
const callback = sinon.stub();
const socket = new Fake.Socket();
socket.address.returns({family: 'IPv4'});
const intf = new NetworkInterface();
intf._sockets.push(socket);
beforeEach(function() {
intf._isBound = true;
callback.reset();
socket.reset();
socket.send.resetBehavior();
socket.send.yields();
});
it('should do nothing if not bound yet', function() {
intf._isBound = false;
intf.send(null, null, callback);
expect(callback).to.have.been.called;
expect(socket.send).to.not.have.been.called;
});
it('should do nothing if packet is empty', function() {
intf.send(new Packet(), null, callback);
expect(callback).to.have.been.called;
expect(socket.send).to.not.have.been.called;
});
it('should do nothing if destination is not link local', function() {
const packet = new Packet();
packet.setQuestions([question]);
intf.send(packet, {address: '7.7.7.7'}, callback);
expect(socket.send).to.not.have.been.called;
});
it('should send packet to given destination', function() {
const packet = new Packet();
packet.setQuestions([question]);
const destination = {address: '192.168.1.10', port: 4321};
intf.send(packet, destination, callback);
expect(socket.send.firstCall.args[3]).to.equal(destination.port);
expect(socket.send.firstCall.args[4]).to.equal(destination.address);
expect(callback).to.have.been.called;
});
it('should not send packet to destination on wrong IPv socket', function() {
const packet = new Packet();
packet.setQuestions([question]);
intf.send(packet, {address: '::1'}, callback);
expect(socket.send).to.not.have.been.called;
});
it('should send packet to multicast address', function() {
const packet = new Packet();
packet.setQuestions([question]);
intf.send(packet, null, callback);
expect(socket.send.firstCall.args[3]).to.equal(5353);
expect(socket.send.firstCall.args[4]).to.equal('224.0.0.251');
});
it('should add outgoing answers to interface history', function() {
const packet = new Packet();
packet.setAnswers([answer]);
packet.setResponseBit();
intf.send(packet, null, callback);
expect(intf.hasRecentlySent(answer)).to.be.true;
});
it('should keep track of sent buffers for debugging', function() {
const debug = function() {};
debug.isEnabled = true;
debug.verbose = function() {};
debug.verbose.isEnabled = true;
const revert = NetworkInterface.__set__('debug', debug);
const packet = new Packet();
packet.setQuestions([question]);
intf.send(packet, null, callback);
expect(intf._buffers).to.be.not.empty;
revert();
});
it('should split packet and resend on EMSGSIZE', sinon.test(function() {
const err = new Error();
err.code = 'EMSGSIZE';
socket.send.onFirstCall().yields(err);
const packet = new Packet();
packet.setQuestions([question]);
this.spy(intf, 'send');
intf.send(packet);
expect(intf.send).to.have.callCount(3); // first call + 2 more for each half
}));
it('should _onError for anything else', function(done) {
socket.send.yields(new Error());
const packet = new Packet();
packet.setQuestions([question]);
intf.on('error', () => done());
intf.send(packet);
});
});
describe('#_onError()', function() {
it('should shutdown and emit error', function() {
const intf = new NetworkInterface();
sinon.stub(intf, 'stop');
sinon.stub(intf, 'emit');
const err = new Error();
intf._onError(err);
expect(intf.stop).to.have.been.called;
expect(intf.emit).to.have.been.calledWith('error', err);
});
});
describe('#stopUsing()', function() {
it('should only shutdown when no one is using it anymore', function() {
const intf = new NetworkInterface();
intf._usingMe = 2;
sinon.stub(intf, 'stop');
intf.stopUsing();
expect(intf.stop).to.not.have.been.called;
intf.stopUsing();
expect(intf.stop).to.have.been.called;
});
});
describe('#stop()', function() {
it('should remove all listeners from sockets before closing', function() {
const intf = new NetworkInterface();
const socket = new Fake.Socket();
socket.close = () => {
socket.emit('close');
};
socket.on('close', () => {
throw new Error('Should remove listeners first!');
});
intf._sockets = [socket];
intf.stop();
});
it('should not throw on socket.close() calls', function() {
const intf = new NetworkInterface();
const socket = new Fake.Socket();
socket.close.throws('Already closed!');
intf._sockets = [socket];
intf.stop();
});
});
});