UNPKG

@stoprocent/noble

Version:

A Node.js BLE (Bluetooth Low Energy) central library.

1,498 lines (1,159 loc) 66.3 kB
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