@stoprocent/noble
Version:
A Node.js BLE (Bluetooth Low Energy) central library.
1,498 lines (1,159 loc) • 66.3 kB
JavaScript
const should = require('should');
// Mock the required modules before importing the Bindings module
jest.mock('../../../lib/hci-socket/acl-stream', () => {
return jest.fn();
});
jest.mock('../../../lib/hci-socket/gap', () => {
const gapMock = jest.fn();
gapMock.prototype.on = jest.fn();
gapMock.prototype.setScanParameters = jest.fn().mockResolvedValue(null);
gapMock.prototype.startScanning = jest.fn().mockResolvedValue(null);
gapMock.prototype.stopScanning = jest.fn().mockResolvedValue(null);
gapMock.prototype.setAddress = jest.fn().mockResolvedValue(null);
gapMock.prototype.createLeConn = jest.fn().mockResolvedValue(null);
gapMock.prototype.disconnect = jest.fn().mockResolvedValue(null);
gapMock.prototype.reset = jest.fn().mockResolvedValue(null);
return gapMock;
});
jest.mock('../../../lib/hci-socket/gatt', () => {
const gattOnMock = jest.fn();
const gattExchangeMtuMock = jest.fn();
const gattMock = jest.fn().mockImplementation(() => ({
on: gattOnMock,
exchangeMtu: gattExchangeMtuMock
}));
// Make the mocks accessible for assertions
gattMock.onMock = gattOnMock;
gattMock.exchangeMtuMock = gattExchangeMtuMock;
return gattMock;
});
jest.mock('../../../lib/hci-socket/hci', () => {
const createLeConnSpy = jest.fn();
const hciMock = jest.fn();
hciMock.prototype.createLeConn = createLeConnSpy;
hciMock.prototype.on = jest.fn();
hciMock.prototype.init = jest.fn();
hciMock.prototype.setAddress = jest.fn().mockResolvedValue(null);
hciMock.prototype.reset = jest.fn().mockResolvedValue(null);
// Add STATUS_MAPPER to the constructor
hciMock.STATUS_MAPPER = { 1: 'custom mapper' };
// Make createLeConn accessible for later assertions
hciMock.createLeConnSpy = createLeConnSpy;
return hciMock;
});
jest.mock('../../../lib/hci-socket/signaling', () => {
const signalingOnMock = jest.fn();
const signalingMock = jest.fn().mockImplementation(() => ({
on: signalingOnMock
}));
// Make the mock accessible for assertions
signalingMock.onMock = signalingOnMock;
return signalingMock;
});
// Mock 'os' module
jest.mock('os', () => ({
platform: jest.fn().mockReturnValue('linux')
}));
// Import the Bindings module after mocking
const Bindings = require('../../../lib/hci-socket/bindings');
const Gap = require('../../../lib/hci-socket/gap');
const Gatt = require('../../../lib/hci-socket/gatt');
const Hci = require('../../../lib/hci-socket/hci');
const AclStream = require('../../../lib/hci-socket/acl-stream');
const Signaling = require('../../../lib/hci-socket/signaling');
describe('hci-socket bindings', () => {
let bindings;
const options = {};
beforeEach(() => {
// Mock process methods
jest.spyOn(process, 'on').mockImplementation(() => {});
jest.spyOn(process, 'exit').mockImplementation(() => {});
// Mock console.warn - silent implementation
jest.spyOn(console, 'warn').mockImplementation(() => {});
// Reset all mock state
jest.clearAllMocks();
// Mock timing functions
jest.useFakeTimers();
// Create bindings instance and start it
bindings = new Bindings(options);
bindings.start();
});
afterEach(() => {
jest.useRealTimers();
jest.restoreAllMocks(); // This will restore both process and console.warn mocks
});
it('constructor', () => {
should(bindings._state).eql(null);
should(bindings._isScanning).eql(false);
should(bindings._isScanningStarted).eql(false);
should(bindings._addresses).deepEqual({});
should(bindings._addresseTypes).deepEqual({});
should(bindings._connectable).deepEqual({});
should(bindings._isExtended).eql('extended' in options && options.extended);
should(bindings.scannable).deepEqual({});
should(bindings._pendingConnectionUuid).eql(null);
should(bindings._connectionQueue).deepEqual([]);
should(bindings._handles).deepEqual({});
should(bindings._gatts).deepEqual({});
should(bindings._aclStreams).deepEqual({});
should(bindings._signalings).deepEqual({});
});
it('start', () => {
expect(bindings._gap.on).toHaveBeenCalledTimes(4);
expect(bindings._hci.on).toHaveBeenCalledTimes(8);
expect(bindings._hci.init).toHaveBeenCalledTimes(1);
expect(process.on).toHaveBeenCalledTimes(2);
});
describe('onSigInt', () => {
it('should exit', () => {
const sigIntListeners = process.listeners('SIGINT');
bindings._sigIntHandler = sigIntListeners[sigIntListeners.length - 1];
bindings.onSigInt();
expect(process.exit).toHaveBeenCalledWith(1);
expect(process.exit).toHaveBeenCalledTimes(1);
});
it('should not exit', () => {
bindings._sigIntHandler = jest.fn();
bindings.onSigInt();
expect(process.exit).not.toHaveBeenCalled();
});
});
it('setScanParameters', () => {
bindings.setScanParameters('interval', 'window');
expect(bindings._gap.setScanParameters).toHaveBeenCalledTimes(1);
expect(bindings._gap.setScanParameters).toHaveBeenCalledWith('interval', 'window');
});
describe('startScanning', () => {
it('no args, already scanning', () => {
bindings._isScanning = true;
const scanStartSpy = jest.fn();
bindings.on('scanStart', scanStartSpy);
bindings.startScanning();
should(bindings._scanServiceUuids).deepEqual([]);
expect(scanStartSpy).toHaveBeenCalledTimes(1);
expect(bindings._gap.startScanning).not.toHaveBeenCalled();
});
it('no args, not scanning but started', () => {
bindings._isScanning = false;
bindings._isScanningStarted = true;
bindings._gap.startScanning = jest.fn();
bindings.startScanning();
should(bindings._scanServiceUuids).deepEqual([]);
expect(bindings._gap.startScanning).not.toHaveBeenCalled();
});
it('with args, first time', () => {
bindings._isScanning = false;
bindings._isScanningStarted = false;
bindings._gap.startScanning = jest.fn();
bindings.startScanning(['uuid'], true);
should(bindings._scanServiceUuids).deepEqual(['uuid']);
should(bindings._isScanningStarted).true();
expect(bindings._gap.startScanning).toHaveBeenCalledTimes(1);
expect(bindings._gap.startScanning).toHaveBeenCalledWith(true);
});
});
describe('stopScanning', () => {
it('when scanning', () => {
bindings._isScanning = true;
bindings._gap.stopScanning = jest.fn();
bindings.stopScanning();
expect(bindings._gap.stopScanning).toHaveBeenCalledTimes(1);
});
it('when not scanning', () => {
bindings._isScanning = false;
bindings._gap.stopScanning = jest.fn();
const scanStopSpy = jest.fn();
bindings.on('scanStop', scanStopSpy);
bindings.stopScanning();
expect(bindings._gap.stopScanning).toHaveBeenCalledTimes(1);
expect(scanStopSpy).toHaveBeenCalledTimes(1);
});
});
it('setAddress', () => {
bindings.setAddress('test-address');
expect(bindings._hci.setAddress).toHaveBeenCalledTimes(1);
expect(bindings._hci.setAddress).toHaveBeenCalledWith('test-address');
});
describe('connect', () => {
it('missing peripheral, no queue, public address', () => {
bindings._hci.createLeConn = jest.fn();
bindings.connect('112233445566', { addressType: 'public' });
should(bindings._connectionQueue).length(1);
should(bindings._connectionQueue[0].id).eql('112233445566');
should(bindings._connectionQueue[0].params).eql({ addressType: 'public' });
expect(bindings._hci.createLeConn).toHaveBeenCalledTimes(1);
expect(bindings._hci.createLeConn).toHaveBeenCalledWith('11:22:33:44:55:66', 'public', { addressType: 'public' }, true);
});
it('missing peripheral, no queue, random address', () => {
bindings._hci.createLeConn = jest.fn();
bindings.connect('f32233445566', { addressType: 'random' });
should(bindings._connectionQueue).length(1);
should(bindings._connectionQueue[0].id).eql('f32233445566');
should(bindings._connectionQueue[0].params).eql({ addressType: 'random' });
expect(bindings._hci.createLeConn).toHaveBeenCalledTimes(1);
expect(bindings._hci.createLeConn).toHaveBeenCalledWith('f3:22:33:44:55:66', 'random', { addressType: 'random' }, true);
});
it('existing peripheral, no queue', () => {
bindings._hci.createLeConn = jest.fn();
bindings._addresses = {
peripheralUuid: 'address'
};
bindings._addresseTypes = {
peripheralUuid: 'addressType'
};
bindings.connect('peripheralUuid', 'parameters');
should(bindings._connectionQueue).length(1);
should(bindings._connectionQueue[0].id).eql('peripheralUuid');
should(bindings._connectionQueue[0].params).eql('parameters');
expect(bindings._hci.createLeConn).toHaveBeenCalledTimes(1);
expect(bindings._hci.createLeConn).toHaveBeenCalledWith('address', 'addressType', 'parameters', true);
});
it('missing peripheral, with queue', () => {
bindings._pendingConnectionUuid = 'pending-uuid';
bindings.connect('peripheralUuid', 'parameters');
should(bindings._connectionQueue).length(1);
should(bindings._connectionQueue[0].id).eql('peripheralUuid');
should(bindings._connectionQueue[0].params).eql('parameters');
});
});
describe('disconnect', () => {
it('missing handle', () => {
bindings._hci.disconnect = jest.fn();
bindings.disconnect('peripheralUuid');
expect(bindings._hci.disconnect).toHaveBeenCalledTimes(1);
expect(bindings._hci.disconnect).toHaveBeenCalledWith(undefined);
});
it('existing handle', () => {
bindings._handles = {
peripheralUuid: 'handle'
};
bindings._hci.disconnect = jest.fn();
bindings.disconnect('peripheralUuid');
expect(bindings._hci.disconnect).toHaveBeenCalledTimes(1);
expect(bindings._hci.disconnect).toHaveBeenCalledWith('handle');
});
});
describe('cancel', () => {
it('missing handle', () => {
bindings._connectionQueue.push({ id: 'anotherPeripheralUuid' });
bindings._hci.cancelConnect = jest.fn();
bindings.cancelConnect('peripheralUuid');
should(bindings._connectionQueue).size(1);
expect(bindings._hci.cancelConnect).toHaveBeenCalledTimes(1);
expect(bindings._hci.cancelConnect).toHaveBeenCalledWith(undefined);
});
it('existing handle', () => {
bindings._handles = {
peripheralUuid: 'handle'
};
bindings._connectionQueue.push({ id: 'anotherPeripheralUuid' });
bindings._connectionQueue.push({ id: 'peripheralUuid' });
bindings._hci.cancelConnect = jest.fn();
bindings.cancelConnect('peripheralUuid');
should(bindings._connectionQueue).size(1);
expect(bindings._hci.cancelConnect).toHaveBeenCalledTimes(1);
expect(bindings._hci.cancelConnect).toHaveBeenCalledWith('handle');
});
});
describe('updateRssi', () => {
it('missing handle', () => {
bindings._hci.readRssi = jest.fn();
bindings.updateRssi('peripheralUuid');
expect(bindings._hci.readRssi).toHaveBeenCalledTimes(1);
expect(bindings._hci.readRssi).toHaveBeenCalledWith(undefined);
});
it('existing handle', () => {
bindings._handles = {
peripheralUuid: 'handle'
};
bindings._hci.readRssi = jest.fn();
bindings.updateRssi('peripheralUuid');
expect(bindings._hci.readRssi).toHaveBeenCalledTimes(1);
expect(bindings._hci.readRssi).toHaveBeenCalledWith('handle');
});
});
describe('stop', () => {
it('no handles', () => {
bindings._isScanning = true;
bindings._gap.stopScanning = jest.fn();
bindings._hci.reset = jest.fn();
bindings._hci.stop = jest.fn();
bindings._sigIntHandler = jest.fn();
bindings._exitHandler = jest.fn();
bindings.stop();
expect(bindings._gap.stopScanning).toHaveBeenCalledTimes(1);
expect(bindings._hci.reset).not.toHaveBeenCalled(); // Reset is not called in stop()
expect(bindings._hci.stop).toHaveBeenCalledTimes(1);
});
it('with handles', () => {
bindings._isScanning = true;
bindings._gap.stopScanning = jest.fn();
bindings._hci.disconnect = jest.fn();
bindings._hci.reset = jest.fn();
bindings._hci.stop = jest.fn();
bindings._sigIntHandler = jest.fn();
bindings._exitHandler = jest.fn();
bindings._aclStreams = [1, 2, 3];
bindings.stop();
expect(bindings._gap.stopScanning).toHaveBeenCalledTimes(1);
expect(bindings._hci.disconnect).toHaveBeenCalledTimes(3);
});
});
describe('onStateChange', () => {
it('same state', () => {
const stateChange = jest.fn();
bindings._state = 'state';
bindings.on('stateChange', stateChange);
bindings.onStateChange('state');
expect(stateChange).not.toHaveBeenCalled();
});
it('new state', () => {
const stateChange = jest.fn();
bindings._state = 'state';
bindings.on('stateChange', stateChange);
bindings.onStateChange('newState');
expect(stateChange).toHaveBeenCalledTimes(1);
expect(stateChange).toHaveBeenCalledWith('newState');
});
it('unauthorized', () => {
const stateChange = jest.fn();
bindings._state = 'state';
bindings.on('stateChange', stateChange);
jest.spyOn(console, 'log').mockImplementation(() => {});
bindings.onStateChange('unauthorized');
expect(stateChange).toHaveBeenCalledTimes(1);
expect(stateChange).toHaveBeenCalledWith('unauthorized');
expect(console.log).toHaveBeenCalledTimes(3);
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('adapter state unauthorized'));
});
it('unsupported', () => {
const stateChange = jest.fn();
bindings._state = 'state';
bindings.on('stateChange', stateChange);
jest.spyOn(console, 'log').mockImplementation(() => {});
bindings.onStateChange('unsupported');
expect(stateChange).toHaveBeenCalledTimes(1);
expect(stateChange).toHaveBeenCalledWith('unsupported');
expect(console.log).toHaveBeenCalledTimes(3);
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('adapter does not support Bluetooth'));
});
});
it('onAddressChange', () => {
const onAddressChange = jest.fn();
bindings.on('addressChange', onAddressChange);
bindings.onAddressChange('newAddress');
expect(onAddressChange).toHaveBeenCalledTimes(1);
expect(onAddressChange).toHaveBeenCalledWith('newAddress');
});
it('onScanParametersSet', () => {
const onScanParametersSet = jest.fn();
bindings.on('scanParametersSet', onScanParametersSet);
bindings.onScanParametersSet();
expect(onScanParametersSet).toHaveBeenCalledTimes(1);
});
it('onScanStart', () => {
const onScanStart = jest.fn();
bindings.on('scanStart', onScanStart);
bindings.onScanStart('filterDuplicates');
expect(onScanStart).toHaveBeenCalledTimes(1);
expect(onScanStart).toHaveBeenCalledWith('filterDuplicates');
});
it('onScanStop', () => {
const onScanStop = jest.fn();
bindings.on('scanStop', onScanStop);
bindings.onScanStop();
expect(onScanStop).toHaveBeenCalledTimes(1);
});
describe('onDiscover', () => {
it('new device, no scanServiceUuids', () => {
const onDiscover = jest.fn();
bindings.on('discover', onDiscover);
bindings._scanServiceUuids = [];
const status = 'status';
const address = 'address:as:mac';
const addressType = 'addressType';
const connectable = 'connectable';
const advertisement = 'advertisement';
const rssi = 'rssi';
bindings.onDiscover(status, address, addressType, connectable, advertisement, rssi, undefined);
const uuid = 'addressasmac';
should(bindings._addresses).deepEqual({ [uuid]: address });
should(bindings._addresseTypes).deepEqual({ [uuid]: addressType });
should(bindings._connectable).deepEqual({ [uuid]: connectable });
expect(onDiscover).toHaveBeenCalledTimes(1);
expect(onDiscover).toHaveBeenCalledWith(uuid, address, addressType, connectable, advertisement, rssi, undefined);
});
it('new device, with matching scanServiceUuids', () => {
const onDiscover = jest.fn();
bindings.on('discover', onDiscover);
bindings._scanServiceUuids = ['service-uuid'];
const status = 'status';
const address = 'address:as:mac';
const addressType = 'addressType';
const connectable = 'connectable';
const advertisement = {
serviceUuids: ['service-uuid']
};
const rssi = 'rssi';
bindings.onDiscover(status, address, addressType, connectable, advertisement, rssi, undefined);
const uuid = 'addressasmac';
should(bindings._addresses).deepEqual({ [uuid]: address });
should(bindings._addresseTypes).deepEqual({ [uuid]: addressType });
should(bindings._connectable).deepEqual({ [uuid]: connectable });
expect(onDiscover).toHaveBeenCalledTimes(1);
expect(onDiscover).toHaveBeenCalledWith(uuid, address, addressType, connectable, advertisement, rssi, undefined);
});
it('new device, with non-matching scanServiceUuids', () => {
const onDiscover = jest.fn();
bindings.on('discover', onDiscover);
bindings._scanServiceUuids = ['service-uuid'];
const status = 'status';
const address = 'address:as:mac';
const addressType = 'addressType';
const connectable = 'connectable';
const advertisement = {
serviceUuids: ['another-service-uuid']
};
const rssi = 'rssi';
bindings.onDiscover(status, address, addressType, connectable, advertisement, rssi, undefined);
should(bindings._addresses).deepEqual({});
should(bindings._addresseTypes).deepEqual({});
should(bindings._connectable).deepEqual({});
expect(onDiscover).not.toHaveBeenCalled();
});
it('new device, with service data on advertisement', () => {
const onDiscover = jest.fn();
bindings.on('discover', onDiscover);
bindings._scanServiceUuids = ['service-uuid'];
const status = 'status';
const address = 'address:as:mac';
const addressType = 'addressType';
const connectable = 'connectable';
const advertisement = {
serviceData: [{ uuid: 'service-uuid' }]
};
const rssi = 'rssi';
bindings.onDiscover(status, address, addressType, connectable, advertisement, rssi, undefined);
const uuid = 'addressasmac';
should(bindings._addresses).deepEqual({ [uuid]: address });
should(bindings._addresseTypes).deepEqual({ [uuid]: addressType });
should(bindings._connectable).deepEqual({ [uuid]: connectable });
expect(onDiscover).toHaveBeenCalledTimes(1);
expect(onDiscover).toHaveBeenCalledWith(uuid, address, addressType, connectable, advertisement, rssi, undefined);
});
it('new device, non matching service data on advertisement', () => {
const onDiscover = jest.fn();
bindings.on('discover', onDiscover);
bindings._scanServiceUuids = ['service-uuid'];
const status = 'status';
const address = 'address:as:mac';
const addressType = 'addressType';
const connectable = 'connectable';
const advertisement = {
serviceData: [{ uuid: 'another-service-uuid' }]
};
const rssi = 'rssi';
bindings.onDiscover(status, address, addressType, connectable, advertisement, rssi, undefined);
should(bindings._addresses).deepEqual({});
should(bindings._addresseTypes).deepEqual({});
should(bindings._connectable).deepEqual({});
expect(onDiscover).not.toHaveBeenCalled();
});
it('new device, no services on advertisement', () => {
const onDiscover = jest.fn();
bindings.on('discover', onDiscover);
bindings._scanServiceUuids = ['service-uuid'];
const status = 'status';
const address = 'address:as:mac';
const addressType = 'addressType';
const connectable = 'connectable';
const advertisement = {};
const rssi = 'rssi';
bindings.onDiscover(status, address, addressType, connectable, advertisement, rssi, undefined);
should(bindings._addresses).deepEqual({});
should(bindings._addresseTypes).deepEqual({});
should(bindings._connectable).deepEqual({});
expect(onDiscover).not.toHaveBeenCalled();
});
it('new device, undefined _scanServiceUuids', () => {
const onDiscover = jest.fn();
bindings.on('discover', onDiscover);
bindings._scanServiceUuids = undefined;
const status = 'status';
const address = 'address:as:mac';
const addressType = 'addressType';
const connectable = 'connectable';
const advertisement = {
serviceData: [{ uuid: 'service-uuid' }]
};
const rssi = 'rssi';
bindings.onDiscover(status, address, addressType, connectable, advertisement, rssi, undefined);
should(bindings._addresses).deepEqual({});
should(bindings._addresseTypes).deepEqual({});
should(bindings._connectable).deepEqual({});
expect(onDiscover).not.toHaveBeenCalled();
});
});
describe('onLeConnComplete', () => {
it('not on master node', () => {
const status = 1;
const handle = 'handle';
const role = 'not-master';
const addressType = 'addressType';
const address = 'address';
const connectCallback = jest.fn();
bindings.on('connect', connectCallback);
bindings.onLeConnComplete(status, handle, role, addressType, address);
expect(connectCallback).not.toHaveBeenCalled();
});
it('with right status on master node', () => {
const status = 0;
const handle = 'handle';
const role = 0;
const addressType = 'addressType';
const address = 'address:split:by:separator';
const connectCallback = jest.fn();
bindings.on('connect', connectCallback);
bindings.onLeConnComplete(status, handle, role, addressType, address);
jest.advanceTimersByTime(0);
expect(AclStream).toHaveBeenCalledTimes(1);
expect(Gatt).toHaveBeenCalledTimes(1);
expect(Signaling).toHaveBeenCalledTimes(1);
expect(Gatt.onMock).toHaveBeenCalledTimes(17);
expect(Gatt.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Gatt.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Gatt.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Gatt.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Gatt.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Gatt.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Gatt.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Gatt.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Gatt.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Gatt.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Gatt.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Gatt.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Gatt.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Gatt.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Gatt.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Gatt.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Signaling.onMock).toHaveBeenCalledTimes(1);
expect(Signaling.onMock).toHaveBeenCalledWith(expect.any(String), expect.any(Function));
expect(Gatt.exchangeMtuMock).toHaveBeenCalledTimes(1);
expect(connectCallback).toHaveBeenCalledTimes(1);
expect(connectCallback).toHaveBeenCalledWith('addresssplitbyseparator', null);
should(bindings._pendingConnectionUuid).equal(null);
});
it('with invalid status on master node', () => {
const status = 1;
const handle = 'handle';
const role = 0;
const addressType = 'addressType';
const address = 'address:split:by:separator';
const connectCallback = jest.fn();
bindings._connectionQueue.push({ id: 'pending_uuid' });
bindings.on('connect', connectCallback);
bindings.onLeConnComplete(status, handle, role, addressType, address);
expect(AclStream).not.toHaveBeenCalled();
expect(Gatt).not.toHaveBeenCalled();
expect(Signaling).not.toHaveBeenCalled();
expect(connectCallback).toHaveBeenCalledTimes(1);
expect(connectCallback).toHaveBeenCalledWith('pending_uuid', expect.objectContaining({
message: 'custom mapper (0x1)'
}));
should(bindings._connectionQueue).length(0);
});
it('with unmapped status on master node', () => {
const status = 2;
const handle = 'handle';
const role = 0;
const addressType = 'addressType';
const address = 'address:split:by:separator';
const connectCallback = jest.fn();
bindings._connectionQueue.push({ id: 'pending_uuid' });
bindings.on('connect', connectCallback);
bindings.onLeConnComplete(status, handle, role, addressType, address);
expect(AclStream).not.toHaveBeenCalled();
expect(Gatt).not.toHaveBeenCalled();
expect(Signaling).not.toHaveBeenCalled();
expect(connectCallback).toHaveBeenCalledTimes(1);
expect(connectCallback).toHaveBeenCalledWith('pending_uuid', expect.objectContaining({
message: 'HCI Error: Unknown (0x2)'
}));
should(bindings._connectionQueue).length(0);
});
it('with connection queue', () => {
const status = 0;
const handle = 'handle';
const role = 0;
const addressType = 'random';
const address = '11:22:33:44:55:66';
const connectCallback = jest.fn();
bindings._addresses = { 'queuedId_1': '112233445566', 'queuedId_2': '998877665544' };
bindings._addresseTypes = { 'queuedId_1': 'random', 'queuedId_2': 'public' };
bindings.connect('queuedId_1', { addressType: 'random' });
bindings.connect('queuedId_2', { addressType: 'public' });
bindings.on('connect', connectCallback);
bindings.onLeConnComplete(status, handle, role, addressType, address);
expect(connectCallback).toHaveBeenCalledTimes(1);
expect(connectCallback).toHaveBeenCalledWith('112233445566', null);
expect(Hci.createLeConnSpy).toHaveBeenCalledTimes(2);
expect(Hci.createLeConnSpy).toHaveBeenCalledWith('112233445566', 'random', { addressType: 'random' }, true);
expect(Hci.createLeConnSpy).toHaveBeenCalledWith('998877665544', 'public', { addressType: 'public' }, false);
should(bindings._connectionQueue).length(1);
});
it('with longer connection queue', () => {
const status = 0;
const handle = 'handle';
const role = 0;
const addressType = 'random';
const address = '11:22:33:44:55:66';
const connectCallback = jest.fn();
bindings._addresses = { 'queuedId_1': '112233445566', 'queuedId_2': '998877665544', 'queuedId_3': 'aabbccddeeff' };
bindings._addresseTypes = { 'queuedId_1': 'random', 'queuedId_2': 'public', 'queuedId_3': 'random' };
bindings.connect('queuedId_1', { addressType: 'random' });
bindings.connect('queuedId_2', { addressType: 'public' });
bindings.connect('queuedId_3', { addressType: 'random' });
bindings.emit('reset');
bindings.on('connect', connectCallback);
bindings.onLeConnComplete(status, handle, role, addressType, address);
expect(connectCallback).toHaveBeenCalledTimes(1);
expect(connectCallback).toHaveBeenCalledWith('112233445566', null);
expect(Hci.createLeConnSpy).toHaveBeenCalledTimes(2);
expect(Hci.createLeConnSpy).toHaveBeenCalledWith('112233445566', 'random', { addressType: 'random' }, true);
expect(Hci.createLeConnSpy).toHaveBeenCalledWith('998877665544', 'public', { addressType: 'public' }, false);
expect(bindings._connectionQueue).toHaveLength(2);
});
it('with longer connection queue where 2 connections are completed', () => {
const status = 0;
const handle = 'handle';
const role = 0;
const addressType = 'random';
const address = '11:22:33:44:55:66';
const status2 = 0;
const handle2 = 'handle2';
const role2 = 0;
const addressType2 = 'public';
const address2 = '99:88:77:66:55:44';
const connectCallback = jest.fn();
bindings._addresses = { 'queuedId_1': '112233445566', 'queuedId_2': '998877665544', 'queuedId_3': 'aabbccddeeff' };
bindings._addresseTypes = { 'queuedId_1': 'random', 'queuedId_2': 'public', 'queuedId_3': 'random' };
bindings.connect('queuedId_1', { addressType: 'random' });
bindings.connect('queuedId_2', { addressType: 'public' });
bindings.connect('queuedId_3', { addressType: 'random' });
bindings.on('connect', connectCallback);
bindings.onLeConnComplete(status, handle, role, addressType, address);
bindings.onLeConnComplete(status2, handle2, role2, addressType2, address2);
expect(connectCallback).toHaveBeenCalledTimes(2);
expect(connectCallback).toHaveBeenCalledWith('112233445566', null);
expect(connectCallback).toHaveBeenCalledWith('998877665544', null);
expect(Hci.createLeConnSpy).toHaveBeenCalledTimes(3);
expect(Hci.createLeConnSpy).toHaveBeenCalledWith('112233445566', 'random', { addressType: 'random' }, true);
expect(Hci.createLeConnSpy).toHaveBeenCalledWith('998877665544', 'public', { addressType: 'public' }, false);
expect(Hci.createLeConnSpy).toHaveBeenCalledWith('aabbccddeeff', 'random', { addressType: 'random' }, false);
expect(bindings._connectionQueue).toHaveLength(1);
});
});
describe('onDisconnComplete', () => {
it('handle not found', () => {
const disconnectCallback = jest.fn();
bindings.on('disconnect', disconnectCallback);
bindings.onDisconnComplete('missing', 'reason');
expect(disconnectCallback).not.toHaveBeenCalled();
});
it('existing handle', () => {
const disconnectCallback = jest.fn();
const handle = 'handle';
const anotherHandle = 'another_handle';
const uuid = 'uuid';
const anotherUuid = 'another_uuid';
const reason = 'reason';
const gattSpy = {
removeAllListeners: jest.fn()
};
const signalingSpy = {
removeAllListeners: jest.fn()
};
// Init expected
bindings._handles[uuid] = uuid;
bindings._handles[handle] = uuid;
bindings._handles[anotherUuid] = uuid;
bindings._handles[anotherHandle] = anotherUuid;
bindings._aclStreams[handle] = [];
bindings._aclStreams[anotherHandle] = [];
bindings._gatts[handle] = gattSpy;
bindings._gatts[uuid] = gattSpy;
bindings._gatts[anotherHandle] = gattSpy;
bindings._gatts[anotherUuid] = gattSpy;
bindings._signalings[uuid] = signalingSpy;
bindings._signalings[handle] = signalingSpy;
bindings._signalings[anotherUuid] = signalingSpy;
bindings._signalings[anotherHandle] = signalingSpy;
bindings.on('disconnect', disconnectCallback);
bindings.onDisconnComplete(handle, reason);
expect(disconnectCallback).toHaveBeenCalledTimes(1);
expect(disconnectCallback).toHaveBeenCalledWith(uuid, reason);
expect(gattSpy.removeAllListeners).toHaveBeenCalledTimes(1);
expect(signalingSpy.removeAllListeners).toHaveBeenCalledTimes(1);
should(bindings._handles).not.have.keys(uuid, handle);
should(bindings._aclStreams).not.have.keys(handle);
should(bindings._gatts).not.have.keys(uuid, handle);
should(bindings._signalings).not.have.keys(uuid, handle);
});
});
describe('onEncryptChange', () => {
it('missing handle', () => {
const handle = 'handle';
const anotherHandle = 'another_handle';
const encrypt = 'encrypt';
const aclSpy = {
pushEncrypt: jest.fn()
};
bindings._aclStreams[handle] = aclSpy;
bindings.onEncryptChange(anotherHandle, encrypt);
expect(aclSpy.pushEncrypt).not.toHaveBeenCalled();
});
it('existing handle', () => {
const handle = 'handle';
const encrypt = 'encrypt';
const aclSpy = {
pushEncrypt: jest.fn()
};
bindings._aclStreams[handle] = aclSpy;
bindings.onEncryptChange(handle, encrypt);
expect(aclSpy.pushEncrypt).toHaveBeenCalledTimes(1);
expect(aclSpy.pushEncrypt).toHaveBeenCalledWith(encrypt);
});
it('existing handle no encrypt', () => {
const handle = 'handle';
const aclSpy = {
pushEncrypt: jest.fn()
};
bindings._aclStreams[handle] = aclSpy;
bindings.onEncryptChange(handle);
expect(aclSpy.pushEncrypt).toHaveBeenCalledTimes(1);
expect(aclSpy.pushEncrypt).toHaveBeenCalledWith(undefined);
});
});
it('onMtu', () => {
const address = 'this:is:an:address';
const rssi = 'rssi';
const callback = jest.fn();
bindings.on('onMtu', callback);
bindings.onMtu(address, rssi);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('thisisanaddress', rssi);
});
it('onRssiRead', () => {
const handle = 'handle';
const rssi = 'rssi';
const callback = jest.fn();
bindings._handles[handle] = 'binding_handle';
bindings.on('rssiUpdate', callback);
bindings.onRssiRead(handle, rssi);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('binding_handle', rssi);
});
describe('onAclDataPkt', () => {
it('missing handle', () => {
const handle = 'handle';
const anotherHandle = 'another_handle';
const cid = 'cid';
const data = 'data';
const aclSpy = {
push: jest.fn()
};
bindings._aclStreams[handle] = aclSpy;
bindings.onAclDataPkt(anotherHandle, cid, data);
expect(aclSpy.push).not.toHaveBeenCalled();
});
it('existing handle', () => {
const handle = 'handle';
const cid = 'cid';
const data = 'data';
const aclSpy = {
push: jest.fn()
};
bindings._aclStreams[handle] = aclSpy;
bindings.onAclDataPkt(handle, cid, data);
expect(aclSpy.push).toHaveBeenCalledTimes(1);
expect(aclSpy.push).toHaveBeenCalledWith(cid, data);
});
it('existing handle no cid no data', () => {
const handle = 'handle';
const aclSpy = {
push: jest.fn()
};
bindings._aclStreams[handle] = aclSpy;
bindings.onAclDataPkt(handle);
expect(aclSpy.push).toHaveBeenCalledTimes(1);
expect(aclSpy.push).toHaveBeenCalledWith(undefined, undefined);
});
});
describe('addService', () => {
it('missing gatt', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const anotherHandle = 'another_handle';
const service = 'service';
const gatt = {
addService: jest.fn()
};
bindings._handles[peripheralUuid] = anotherHandle;
bindings._gatts[handle] = gatt;
bindings.addService(peripheralUuid, service);
expect(gatt.addService).not.toHaveBeenCalled();
});
it('existing gatt', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const service = 'service';
const gatt = {
addService: jest.fn()
};
bindings._handles[peripheralUuid] = handle;
bindings._gatts[handle] = gatt;
bindings.addService(peripheralUuid, service);
expect(gatt.addService).toHaveBeenCalledTimes(1);
expect(gatt.addService).toHaveBeenCalledWith(service);
});
it('existing gatt no service', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const gatt = {
addService: jest.fn()
};
bindings._handles[peripheralUuid] = handle;
bindings._gatts[handle] = gatt;
bindings.addService(peripheralUuid);
expect(gatt.addService).toHaveBeenCalledTimes(1);
expect(gatt.addService).toHaveBeenCalledWith(undefined);
});
});
describe('discoverServices', () => {
it('missing gatt', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const anotherHandle = 'another_handle';
const uuids = 'uuids';
const gatt = {
discoverServices: jest.fn()
};
bindings._handles[peripheralUuid] = anotherHandle;
bindings._gatts[handle] = gatt;
bindings.discoverServices(peripheralUuid, uuids);
expect(gatt.discoverServices).not.toHaveBeenCalled();
});
it('existing gatt', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const uuids = 'uuids';
const gatt = {
discoverServices: jest.fn()
};
bindings._handles[peripheralUuid] = handle;
bindings._gatts[handle] = gatt;
bindings.discoverServices(peripheralUuid, uuids);
expect(gatt.discoverServices).toHaveBeenCalledTimes(1);
expect(gatt.discoverServices).toHaveBeenCalledWith(uuids);
});
it('existing gatt no uuids', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const gatt = {
discoverServices: jest.fn()
};
bindings._handles[peripheralUuid] = handle;
bindings._gatts[handle] = gatt;
bindings.discoverServices(peripheralUuid);
expect(gatt.discoverServices).toHaveBeenCalledTimes(1);
expect(gatt.discoverServices).toHaveBeenCalledWith([]);
});
});
it('onServicesDiscovered', () => {
const address = 'this:is:an:address';
const serviceUuids = 'serviceUuids';
const callback = jest.fn();
bindings.on('servicesDiscover', callback);
bindings.onServicesDiscovered(address, serviceUuids);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('thisisanaddress', serviceUuids);
});
it('onServicesDiscoveredEX', () => {
const address = 'this:is:an:address';
const serviceUuids = 'serviceUuids';
const callback = jest.fn();
bindings.on('servicesDiscovered', callback);
bindings.onServicesDiscoveredEX(address, serviceUuids);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('thisisanaddress', serviceUuids);
});
describe('discoverIncludedServices', () => {
it('missing gatt', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const anotherHandle = 'another_handle';
const serviceUuid = 'service-uuid';
const serviceUuids = 'serviceUuids';
const gatt = {
discoverIncludedServices: jest.fn()
};
bindings._handles[peripheralUuid] = anotherHandle;
bindings._gatts[handle] = gatt;
bindings.discoverIncludedServices(peripheralUuid, serviceUuid, serviceUuids);
expect(gatt.discoverIncludedServices).not.toHaveBeenCalled();
});
it('existing gatt', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const serviceUuid = 'service-uuid';
const serviceUuids = 'serviceUuids';
const gatt = {
discoverIncludedServices: jest.fn()
};
bindings._handles[peripheralUuid] = handle;
bindings._gatts[handle] = gatt;
bindings.discoverIncludedServices(peripheralUuid, serviceUuid, serviceUuids);
expect(gatt.discoverIncludedServices).toHaveBeenCalledTimes(1);
expect(gatt.discoverIncludedServices).toHaveBeenCalledWith(serviceUuid, serviceUuids);
});
it('existing gatt no uuids', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const serviceUuid = 'service-uuid';
const gatt = {
discoverIncludedServices: jest.fn()
};
bindings._handles[peripheralUuid] = handle;
bindings._gatts[handle] = gatt;
bindings.discoverIncludedServices(peripheralUuid, serviceUuid);
expect(gatt.discoverIncludedServices).toHaveBeenCalledTimes(1);
expect(gatt.discoverIncludedServices).toHaveBeenCalledWith(serviceUuid, []);
});
});
it('onIncludedServicesDiscovered', () => {
const address = 'this:is:an:address';
const serviceUuid = 'serviceUuid';
const includedServiceUuids = 'includedServiceUuids';
const callback = jest.fn();
bindings.on('includedServicesDiscover', callback);
bindings.onIncludedServicesDiscovered(address, serviceUuid, includedServiceUuids);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('thisisanaddress', serviceUuid, includedServiceUuids);
});
describe('addCharacteristics', () => {
it('missing gatt', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const anotherHandle = 'another_handle';
const serviceUuid = 'serviceUuid';
const characteristics = 'characteristics';
const gatt = {
addCharacteristics: jest.fn()
};
bindings._handles[peripheralUuid] = anotherHandle;
bindings._gatts[handle] = gatt;
bindings.addCharacteristics(peripheralUuid, serviceUuid, characteristics);
expect(gatt.addCharacteristics).not.toHaveBeenCalled();
});
it('existing gatt', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const serviceUuid = 'serviceUuid';
const characteristics = 'characteristics';
const gatt = {
addCharacteristics: jest.fn()
};
bindings._handles[peripheralUuid] = handle;
bindings._gatts[handle] = gatt;
bindings.addCharacteristics(peripheralUuid, serviceUuid, characteristics);
expect(gatt.addCharacteristics).toHaveBeenCalledTimes(1);
expect(gatt.addCharacteristics).toHaveBeenCalledWith(serviceUuid, characteristics);
});
it('existing gatt no serviceUuid no characteristics', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const gatt = {
addCharacteristics: jest.fn()
};
bindings._handles[peripheralUuid] = handle;
bindings._gatts[handle] = gatt;
bindings.addCharacteristics(peripheralUuid);
expect(gatt.addCharacteristics).toHaveBeenCalledTimes(1);
expect(gatt.addCharacteristics).toHaveBeenCalledWith(undefined, undefined);
});
});
describe('discoverCharacteristics', () => {
it('missing gatt', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const anotherHandle = 'another_handle';
const serviceUuid = 'serviceUuid';
const characteristicUuids = 'characteristics';
const gatt = {
discoverCharacteristics: jest.fn()
};
bindings._handles[peripheralUuid] = anotherHandle;
bindings._gatts[handle] = gatt;
bindings.discoverCharacteristics(peripheralUuid, serviceUuid, characteristicUuids);
expect(gatt.discoverCharacteristics).not.toHaveBeenCalled();
});
it('existing gatt', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const serviceUuid = 'serviceUuid';
const characteristicUuids = 'characteristics';
const gatt = {
discoverCharacteristics: jest.fn()
};
bindings._handles[peripheralUuid] = handle;
bindings._gatts[handle] = gatt;
bindings.discoverCharacteristics(peripheralUuid, serviceUuid, characteristicUuids);
expect(gatt.discoverCharacteristics).toHaveBeenCalledTimes(1);
expect(gatt.discoverCharacteristics).toHaveBeenCalledWith(serviceUuid, characteristicUuids);
});
it('existing gatt no uuids', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const serviceUuid = 'serviceUuid';
const gatt = {
discoverCharacteristics: jest.fn()
};
bindings._handles[peripheralUuid] = handle;
bindings._gatts[handle] = gatt;
bindings.discoverCharacteristics(peripheralUuid, serviceUuid);
expect(gatt.discoverCharacteristics).toHaveBeenCalledTimes(1);
expect(gatt.discoverCharacteristics).toHaveBeenCalledWith(serviceUuid, []);
});
});
it('onCharacteristicsDiscovered', () => {
const address = 'this:is:an:address';
const serviceUuid = 'serviceUuid';
const characteristics = 'characteristics';
const callback = jest.fn();
bindings.on('characteristicsDiscover', callback);
bindings.onCharacteristicsDiscovered(address, serviceUuid, characteristics);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('thisisanaddress', serviceUuid, characteristics);
});
it('onCharacteristicsDiscoveredEX', () => {
const address = 'this:is:an:address';
const serviceUuid = 'serviceUuid';
const characteristics = 'characteristics';
const callback = jest.fn();
bindings.on('characteristicsDiscovered', callback);
bindings.onCharacteristicsDiscoveredEX(address, serviceUuid, characteristics);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('thisisanaddress', serviceUuid, characteristics);
});
describe('read', () => {
it('missing gatt', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const anotherHandle = 'another_handle';
const serviceUuid = 'serviceUuid';
const characteristicUuid = 'characteristic';
const gatt = {
read: jest.fn()
};
bindings._handles[peripheralUuid] = anotherHandle;
bindings._gatts[handle] = gatt;
bindings.read(peripheralUuid, serviceUuid, characteristicUuid);
expect(gatt.read).not.toHaveBeenCalled();
});
it('existing gatt', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const serviceUuid = 'serviceUuid';
const characteristicUuid = 'characteristics';
const gatt = {
read: jest.fn()
};
bindings._handles[peripheralUuid] = handle;
bindings._gatts[handle] = gatt;
bindings.read(peripheralUuid, serviceUuid, characteristicUuid);
expect(gatt.read).toHaveBeenCalledTimes(1);
expect(gatt.read).toHaveBeenCalledWith(serviceUuid, characteristicUuid);
});
it('existing gatt no uuids', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const gatt = {
read: jest.fn()
};
bindings._handles[peripheralUuid] = handle;
bindings._gatts[handle] = gatt;
bindings.read(peripheralUuid);
expect(gatt.read).toHaveBeenCalledTimes(1);
expect(gatt.read).toHaveBeenCalledWith(undefined, undefined);
});
});
it('onRead', () => {
const address = 'this:is:an:address';
const serviceUuid = 'serviceUuid';
const characteristicUuid = 'characteristics';
const data = 'data';
const callback = jest.fn();
bindings.on('read', callback);
bindings.onRead(address, serviceUuid, characteristicUuid, data);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('thisisanaddress', serviceUuid, characteristicUuid, data, false);
});
describe('write', () => {
it('missing gatt', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const anotherHandle = 'another_handle';
const serviceUuid = 'serviceUuid';
const characteristicUuid = 'characteristic';
const data = 'data';
const withoutResponse = true;
const gatt = {
write: jest.fn()
};
bindings._handles[peripheralUuid] = anotherHandle;
bindings._gatts[handle] = gatt;
bindings.write(peripheralUuid, serviceUuid, characteristicUuid, data, withoutResponse);
expect(gatt.write).not.toHaveBeenCalled();
});
it('existing gatt', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const serviceUuid = 'serviceUuid';
const characteristicUuid = 'characteristics';
const data = 'data';
const withoutResponse = true;
const gatt = {
write: jest.fn()
};
bindings._handles[peripheralUuid] = handle;
bindings._gatts[handle] = gatt;
bindings.write(peripheralUuid, serviceUuid, characteristicUuid, data, withoutResponse);
expect(gatt.write).toHaveBeenCalledTimes(1);
expect(gatt.write).toHaveBeenCalledWith(serviceUuid, characteristicUuid, data, withoutResponse);
});
it('existing gatt no uuids', () => {
const peripheralUuid = 'uuid';
const handle = 'handle';
const gatt = {
write: jest.fn()
};
bindings._handles[peripheralUuid] = handle;
bindings._gatts[handle] = gatt;
bindings.write(peripheralUuid);
expect(gatt.write).toHaveBeenCalledTimes(1);
expect(gatt.write).toHaveBeenCalledWith(undefined, undefined, undefined, undefined);
});
});
it('onWrite', () => {
const address = 'this:is:an:address';
const serviceUuid = 'serviceUuid';
const characteristicUuid = 'characteristics';
const callback = jest.fn();
bindings.on('write', callback);
bindings.onWrite(address, serviceUuid, characteristicUuid);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('thisisanaddress', serviceUuid, characteristicUuid);
});
describe('broadcast', () => {
it('missing gatt', () => {
const peri