dnssd
Version:
Bonjour/Avahi-like service discovery in pure JavaScript
404 lines (307 loc) • 11.8 kB
JavaScript
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 ServiceType = require(dir + '/ServiceType');
const ResourceRecord = require(dir + '/ResourceRecord');
const Fake = require('../Fake');
const Browser = rewire(dir + '/Browser');
describe('Browser', function() {
const intf = new Fake.NetworkInterface();
const query = new Fake.Query();
const resolver = new Fake.ServiceResolver();
// change the networkInterfaces dependency within Browser.js so all new
// browsers have: browser._interface = intf
const NetworkInterfaceMock = {get: sinon.stub().returns(intf)};
const QueryConstructor = sinon.stub().returns(query);
const ServiceResolverConstructor = sinon.stub().returns(resolver);
Browser.__set__('NetworkInterface', NetworkInterfaceMock);
Browser.__set__('ServiceResolver', ServiceResolverConstructor);
Browser.__set__('Query', QueryConstructor);
beforeEach(function() {
intf.reset();
query.reset();
resolver.reset();
ServiceResolverConstructor.reset();
});
describe('#constructor()', function() {
it('should be ok if new keyword missing', function() {
expect(Browser('_http._tcp')).to.be.instanceof(Browser);
});
it('should accept service param as a ServiceType (no throw)', function() {
new Browser(new ServiceType('_http._tcp'));
});
it('should accept service param as an object (no throw)', function() {
new Browser({name: '_http', protocol: '_tcp'});
});
it('should accept service param as a string (no throw)', function() {
new Browser('_http._tcp');
});
it('should accept service param as an array (no throw)', function() {
new Browser(['_http', '_tcp']);
});
it('should be ok with service enumerators (no throw)', function() {
new Browser('_services._dns-sd._udp');
});
it('should throw on invalid service types', function() {
expect(() => new Browser('gunna throw')).to.throw(Error);
});
it('should throw on multiple subtypes', function() {
expect(() => new Browser(['_http', '_tcp', 'sub1', 'sub2'])).to.throw(Error);
});
});
describe('#start()', function() {
it('should return this', function() {
const browser = new Browser('_http._tcp');
sinon.stub(browser, '_startQuery');
expect(browser.start()).to.equal(browser);
});
it('should bind interface & start queries', function(done) {
const browser = new Browser('_http._tcp');
sinon.stub(browser, '_startQuery', () => {
expect(intf.bind).to.have.been.called;
done();
});
browser.start();
});
it('should return early if already started', function() {
const browser = new Browser('_http._tcp');
sinon.stub(browser, '_startQuery');
browser.start();
browser.start(); // <-- does nothing
// wait for promises
setTimeout(() => expect(browser._startQuery).to.have.been.calledOnce, 10);
});
it('should run _onError on startup errors', function(done) {
const browser = new Browser('_http._tcp');
sinon.stub(browser, '_startQuery').returns(Promise.reject());
browser.on('error', () => done());
browser.start();
});
});
describe('#stop()', function() {
it('should remove listeners, stop resolvers, queries, & interfaces', function() {
const browser = new Browser('_http._tcp');
const resolver_1 = new Fake.ServiceResolver();
const resolver_2 = new Fake.ServiceResolver();
browser._resolvers['mock entry #1'] = resolver_1;
browser._resolvers['mock entry #2'] = resolver_2;
browser.stop();
expect(resolver_1.stop).to.have.been.called;
expect(resolver_2.stop).to.have.been.called;
expect(intf.stopUsing).to.have.been.called;
expect(browser.list()).to.be.empty;
});
});
describe('#list()', function() {
it('should return services that are currently active', function() {
const browser = new Browser('_http._tcp');
const service = {};
const resolved = new Fake.ServiceResolver();
resolved.isResolved.returns(false);
resolved.service.returns(service);
const unresolved = new Fake.ServiceResolver();
resolved.isResolved.returns(true);
resolved.service.returns({});
browser._resolvers['resolved service'] = resolved;
browser._resolvers['unresolved service'] = unresolved;
expect(browser.list()).to.eql([service]);
});
it('should return services types that are currently active', function() {
const browser = new Browser('_services._dns-sd._udp');
const recordName = '_http._tcp.local.';
browser._serviceTypes[recordName] = {name: 'http', protocol: 'tcp'};
expect(browser.list()).to.eql([{name: 'http', protocol: 'tcp'}]);
});
});
describe('#_onError()', function() {
it('should call stop and emit the error', function(done) {
const browser = new Browser('_http._tcp');
sinon.stub(browser, 'stop');
browser.on('error', () => {
expect(browser.stop).to.have.been.called;
done();
});
browser.start();
intf.emit('error', new Error());
});
});
describe('#_startQuery()', function() {
it('should query for individual services', function() {
const browser = new Browser('_http._tcp');
browser._startQuery();
expect(query.add).to.have.been.calledWithMatch({
name: '_http._tcp.local.',
qtype: 12,
});
});
it('should query for service subtypes', function() {
const browser = new Browser('_http._tcp,subtype');
browser._startQuery();
expect(query.add).to.have.been.calledWithMatch({
name: 'subtype._sub._http._tcp.local.',
qtype: 12,
});
});
it('should query for available service types', function() {
const browser = new Browser('_services._dns-sd._udp');
browser._startQuery();
expect(query.add).to.have.been.calledWithMatch({
name: '_services._dns-sd._udp.local.',
qtype: 12,
});
});
});
describe('#_addServiceType()', function() {
const PTR = new ResourceRecord.PTR({
name: '_services._dns-sd._udp.local.',
PTRDName: '_http._tcp.local.',
});
it('should add new service types', function(done) {
const browser = new Browser('_services._dns-sd._udp')
.on('serviceUp', (type) => {
expect(browser.list()).to.not.be.empty;
expect(browser.list()[0]).to.eql({name: 'http', protocol: 'tcp'});
expect(type).eql({name: 'http', protocol: 'tcp'});
done();
})
.start();
// wait for promises to resolve first
setTimeout(() => query.emit('answer', PTR), 10);
});
it('should ignore PTRs with TTL=0', function(done) {
const goodbye = PTR.clone();
goodbye.ttl = 0;
const browser = new Browser('_services._dns-sd._udp')
.on('serviceUp', () => { throw new Error('bad!'); })
.start();
// wait for promises to resolve first
setTimeout(() => query.emit('answer', goodbye), 10);
setTimeout(() => {
expect(browser.list()).to.be.empty;
done();
}, 10);
});
it('should answer that have already been found', function(done) {
const browser = new Browser('_services._dns-sd._udp')
.start();
// wait for promises to resolve first
setTimeout(() => query.emit('answer', PTR), 10);
setTimeout(() => query.emit('answer', PTR), 10); // <-- ignored
setTimeout(() => {
expect(browser.list()).to.have.lengthOf(1);
done();
}, 10);
});
it('should do nothing if already stopped', function(done) {
const browser = new Browser('_services._dns-sd._udp');
browser.start();
browser.stop();
// wait for promises to resolve first
setTimeout(() => query.emit('answer', PTR), 10); // <-- ignored
setTimeout(() => {
expect(browser.list()).to.be.empty;
done();
}, 10);
});
});
describe('#_addService()', function() {
const PTR = new ResourceRecord.PTR({
name: '_http._tcp.local.',
PTRDName: 'Instance._http._tcp.local',
});
it('should only emit instance names with resolve = false', function(done) {
new Browser('_http._tcp', {resolve: false})
.on('serviceUp', (name) => {
expect(name).to.equal('Instance');
done();
})
.start();
// wait for promises to resolve first
setTimeout(() => query.emit('answer', PTR), 10);
});
it('should not maintain resovers if maintain = false', function(done) {
new Browser('_http._tcp', {maintain: false})
.on('serviceUp', () => {
expect(resolver.stop).to.have.been.called;
done();
})
.start();
// wait for promises to resolve first
setTimeout(() => query.emit('answer', PTR), 10);
setTimeout(() => resolver.emit('resolved'), 10);
});
it('should emit services when they are resolved/change/down', function(done) {
let obj;
new Browser('_http._tcp')
.on('serviceUp', (service) => {
expect(service).to.be.an.object;
obj = service;
resolver.emit('updated');
})
.on('serviceChanged', (service) => {
expect(service).to.equal(obj);
resolver.emit('down');
})
.on('serviceDown', (service) => {
expect(service).to.equal(obj);
done();
})
.start();
// wait for promises to resolve first
setTimeout(() => query.emit('answer', PTR), 10);
setTimeout(() => resolver.emit('resolved'), 10);
});
it('should not emit serviceDown if service has never resovled', function(done) {
const browser = new Browser('_http._tcp')
.on('serviceUp', () => { throw new Error('bad'); })
.start();
// wait for promises to resolve first
setTimeout(() => query.emit('answer', PTR), 10);
setTimeout(() => resolver.emit('down'), 10);
setTimeout(() => {
expect(browser.list()).to.be.empty;
done();
}, 10);
});
it('should ignore already known instance answers', function(done) {
const browser = new Browser('_http._tcp');
// done x2 would throw:
browser.on('serviceUp', () => {
expect(ServiceResolverConstructor).to.have.been.calledOnce;
done();
});
browser.start();
// wait for promises to resolve first
setTimeout(() => query.emit('answer', PTR), 10);
setTimeout(() => query.emit('answer', PTR), 10);
setTimeout(() => resolver.emit('resolved'), 10);
});
it('should ignore answers with TTL=0', function(done) {
const goodbye = PTR.clone();
goodbye.ttl = 0;
const browser = new Browser('_http._tcp');
browser.start();
// wait for promises to resolve first
setTimeout(() => query.emit('answer', goodbye), 10);
setTimeout(() => {
expect(ServiceResolverConstructor).to.not.have.been.called;
done();
}, 10);
});
it('should do nothing if already stopped', function(done) {
const browser = new Browser('_http._tcp');
browser.start();
browser.stop();
// wait for promises to resolve first
setTimeout(() => query.emit('answer', PTR), 10);
setTimeout(() => {
expect(ServiceResolverConstructor).to.not.have.been.called;
done();
}, 10);
});
});
});