dnssd
Version:
Bonjour/Avahi-like service discovery in pure JavaScript
331 lines (233 loc) • 10.2 kB
JavaScript
const _ = require('lodash');
const chai = require('chai');
const expect = chai.expect;
const sinon = require('sinon');
const sinonChai = require('sinon-chai');
chai.use(sinonChai);
const dir = process['test-dir'] || '../../src';
const ResourceRecord = require(dir + '/ResourceRecord');
const ExpiringRecordCollection = require(dir + '/ExpiringRecordCollection');
describe('ExpiringRecordCollection', function() {
// SRV_1 & SRV_2 are related (same name, type) and will have the same namehash
// PTR is a shared, non-unique record type
const SRV_1 = new ResourceRecord.SRV({name: 'SRV', target: 'something', ttl: 10});
const SRV_2 = new ResourceRecord.SRV({name: 'SRV', target: 'different', ttl: 20});
const TXT = new ResourceRecord.TXT({name: 'TXT', ttl: 10});
const PTR = new ResourceRecord.PTR({name: 'PTR', ttl: 10});
describe('#has()', sinon.test(function() {
const collection = new ExpiringRecordCollection([TXT]);
it('should return true if collection already has record', function() {
expect(collection.has(TXT)).to.be.true;
});
it('should return false if it does not have record', function() {
expect(collection.has(PTR)).to.be.false;
});
}));
describe('#add()', function() {
it('should add record and #_schedule timers', function() {
const collection = new ExpiringRecordCollection();
sinon.stub(collection, '_schedule');
collection.add(PTR);
expect(collection.size).to.equal(1);
expect(collection._schedule).to.have.been.calledOnce;
});
it('should updating existing record', function() {
const collection = new ExpiringRecordCollection();
sinon.stub(collection, '_schedule');
collection.add(PTR);
collection.add(PTR);
expect(collection.size).to.equal(1);
});
it('should setToExpire instead of adding if TTL=0', function() {
const zero = new ResourceRecord.SRV({name: 'TTL=0', ttl: 0});
const collection = new ExpiringRecordCollection();
sinon.stub(collection, 'setToExpire');
collection.add(zero);
expect(collection.size).to.equal(0);
expect(collection.setToExpire).to.have.been.calledOnce;
});
});
describe('#addEach()', function() {
it('should add each record', function() {
const collection = new ExpiringRecordCollection();
collection.addEach([TXT, PTR]);
expect(collection.has(TXT)).to.be.true;
expect(collection.has(PTR)).to.be.true;
});
});
describe('#hasAddedWithin()', function() {
it('should be false if record does not exist yet', function() {
const collection = new ExpiringRecordCollection([PTR]);
expect(collection.hasAddedWithin(TXT, 1)).to.be.false;
});
it('should be true if has been added in range', sinon.test(function() {
const collection = new ExpiringRecordCollection([PTR]);
this.clock.tick(5 * 1000);
expect(collection.hasAddedWithin(PTR, 6)).to.be.true;
}));
it('should be false if hasn\'t been added in range', sinon.test(function() {
const collection = new ExpiringRecordCollection([PTR]);
this.clock.tick(5 * 1000);
expect(collection.hasAddedWithin(PTR, 4)).to.be.false;
}));
});
describe('#get()', function() {
it('should return undefined if record does not exist', function() {
const collection = new ExpiringRecordCollection([PTR]);
expect(collection.get(TXT)).to.be.undefined;
});
it('should return clone of record with adjusted TTL', sinon.test(function() {
const collection = new ExpiringRecordCollection([PTR]);
this.clock.tick(3 * 1000);
const clone = collection.get(PTR);
expect(clone).to.not.equal(PTR);
expect(clone.ttl).to.equal(7);
}));
});
describe('#delete()', function() {
it('should remove record', function() {
const collection = new ExpiringRecordCollection([TXT]);
collection.delete(TXT);
expect(collection.size).to.equal(0);
});
it('should remove record id from related group set', function() {
const collection = new ExpiringRecordCollection([SRV_1, SRV_2]);
collection.delete(SRV_1);
expect(collection.size).to.equal(1);
expect(collection._related[SRV_1.namehash].size).to.equal(1);
});
it('should do nothing if collection does not have record', function() {
const collection = new ExpiringRecordCollection();
collection.delete(SRV_1);
expect(collection.size).to.equal(0);
});
it('should emit "expired" event with record', function(done) {
const collection = new ExpiringRecordCollection([PTR]);
collection.on('expired', (result) => {
expect(result).to.equal(PTR);
done();
});
collection.delete(PTR);
});
});
describe('#clear()', function() {
it('should clear all timers and records', function() {
const collection = new ExpiringRecordCollection([TXT, PTR]);
collection.clear();
expect(collection.size).to.equal(0);
});
});
describe('#setToExpire()', function() {
it('should clear timers and delete in 1s', sinon.test(function() {
const collection = new ExpiringRecordCollection([PTR]);
collection.setToExpire(PTR);
this.clock.tick(1 * 1000);
expect(collection.size).to.equal(0);
}));
it('should do nothing if it does not have the record', sinon.test(function() {
const collection = new ExpiringRecordCollection([PTR]);
collection.setToExpire(TXT);
this.clock.tick(1 * 1000);
expect(collection.size).to.equal(1);
}));
it('should not clear existing delete timers', sinon.test(function() {
const collection = new ExpiringRecordCollection([PTR]);
collection.setToExpire(PTR);
this.clock.tick(0.5 * 1000);
collection.setToExpire(PTR); // should NOT reset timer to 1s
this.clock.tick(0.5 * 1000); // delete should have fired
expect(collection.size).to.equal(0);
}));
});
describe('#flushRelated()', function() {
it('should expire related records added > 1s ago', sinon.test(function() {
const collection = new ExpiringRecordCollection([SRV_1, TXT, PTR]);
sinon.stub(collection, 'setToExpire');
this.clock.tick(2 * 1000);
collection.flushRelated(SRV_2);
expect(collection.setToExpire).to.have.been.calledWith(SRV_1);
}));
it('should not expire records added < 1s ago', sinon.test(function() {
const collection = new ExpiringRecordCollection([SRV_1, TXT, PTR]);
sinon.stub(collection, 'setToExpire');
this.clock.tick(0.5 * 1000);
collection.flushRelated(SRV_2);
expect(collection.setToExpire).to.not.have.been.called;
}));
it('should a record should not flush itself', sinon.test(function() {
const collection = new ExpiringRecordCollection([SRV_1, TXT, PTR]);
sinon.stub(collection, 'setToExpire');
this.clock.tick(2 * 1000);
collection.flushRelated(TXT);
expect(collection.setToExpire).to.not.have.been.called;
}));
it('should *not* flush with non-unique records', sinon.test(function() {
const collection = new ExpiringRecordCollection([SRV_1, TXT, PTR]);
sinon.stub(collection, 'setToExpire');
collection.flushRelated(PTR);
expect(collection.setToExpire).to.not.have.been.called;
}));
});
describe('#toArray()', function() {
it('should return array of its records', function() {
expect((new ExpiringRecordCollection([TXT])).toArray()).to.eql([TXT]);
});
});
describe('#hasConflictWith()', sinon.test(function() {
const collection = new ExpiringRecordCollection([SRV_1]);
it('should return true if collection has a conflicting record', function() {
expect(collection.hasConflictWith(SRV_2)).to.be.true;
});
it('should return false if collection has no conflicting records', function() {
expect(collection.hasConflictWith(TXT)).to.be.false;
});
it('should not let a record to conflict with itself', function() {
expect(collection.hasConflictWith(SRV_1)).to.be.false;
});
it('should always return false when given non-unique a record', function() {
expect(collection.hasConflictWith(PTR)).to.be.false;
});
}));
describe('#_getRelatedRecords()', function() {
it('should return an array of records with the same name', function() {
const collection = new ExpiringRecordCollection([PTR]);
expect(collection._getRelatedRecords(PTR.namehash)).to.eql([PTR]);
});
it('should return an empty array if no related records exist', function() {
const collection = new ExpiringRecordCollection([PTR]);
expect(collection._getRelatedRecords('???')).to.eql([]);
});
});
describe('#_filterTTL()', function() {
it('should only return records with TTLs > cutoff', sinon.test(function() {
const collection = new ExpiringRecordCollection([SRV_1]);
this.clock.tick(8 * 1000);
const results = collection._filterTTL([SRV_1, SRV_2], 0.50);
expect(results).to.have.lengthOf(1);
expect(results[0].hash).to.equal(SRV_2.hash);
}));
it('should return an array of clones', function() {
const collection = new ExpiringRecordCollection([SRV_1]);
const results = collection._filterTTL([SRV_1], 0.50);
expect(results).to.not.equal([SRV_1]);
});
it('should subtract elapsed TTL for records', sinon.test(function() {
const collection = new ExpiringRecordCollection([SRV_1]);
this.clock.tick(4 * 1000);
const results = collection._filterTTL([SRV_1], 0.50);
expect(results).to.have.lengthOf(1);
expect(results[0].ttl).to.equal(6);
}));
});
describe('#_schedule()', function() {
it('should schedule expiration and reissue timers', sinon.test(function() {
const collection = new ExpiringRecordCollection([PTR]);
sinon.stub(collection, 'emit');
this.clock.tick(10 * 1000);
expect(collection.emit).to.have.been
.callCount(5)
.calledWith('reissue', PTR)
.calledWith('expired', PTR);
}));
});
});