particle-cli
Version:
Simple Node commandline application for working with your Particle devices and using the Particle Cloud
529 lines (450 loc) • 15.8 kB
JavaScript
'use strict';
const { expect, sinon } = require('../../test/setup');
const WiFiCommands = require('./wifi');
const usbUtils = require('../cmd/usb-util');
const utilities = require('../lib/utilities');
const fs = require('fs-extra');
const path = require('path');
const { PATH_TMP_DIR } = require('../../test/lib/env');
const os = require('os');
describe('Wifi Commands', () => {
let ui, newSpin, stopSpin;
let openDevice;
beforeEach(() => {
sinon.stub(utilities, 'delay').resolves();
ui = {
stdout: {
write: sinon.stub()
},
stderr: {
write: sinon.stub()
}
};
newSpin = sinon.stub().returns({
start: sinon.stub().callsFake(() => {
return {
setSpinnerTitle: sinon.stub()
};
}),
stop: sinon.stub() });
stopSpin = sinon.stub();
openDevice = {
deviceId: 'deviceId',
isOpen: true,
close: sinon.stub(),
reset: sinon.stub(),
scanWifiNetworks: sinon.stub(),
joinNewWifiNetwork: sinon.stub(),
getDeviceMode: sinon.stub().resolves()
};
usbUtils.getOneUsbDevice = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
describe('_serializeNetworks', () => {
it('returns a list of networks', () => {
const networks = [
{
ssid: 'network1',
security: 'WPA2',
rssi: -50,
channel: 6
},
{
ssid: 'network2',
security: 'NO_SECURITY',
rssi: -70,
channel: 11
}
];
const wifiCommands = new WiFiCommands({ ui });
const result = wifiCommands._serializeNetworks(networks);
expect(result).to.eql([
{
ssid: 'network1',
security: 'WPA2',
signal_level: -50,
channel: '6',
unsecure: false,
mac: ''
},
{
ssid: 'network2',
security: 'NO_SECURITY',
signal_level: -70,
channel: '11',
unsecure: true,
mac: ''
}]);
});
});
describe('_pickNetworkManually', () => {
it('prompts for ssid and password', async () => {
const wifiCommands = new WiFiCommands({ ui });
ui.prompt = sinon.stub();
ui.prompt
.onCall(0).returns({ ssid: 'ssid' })
.onCall(1).returns({ security: 'WPA2_PSK' })
.onCall(2).returns({ password: 'password' })
.onCall(3).returns({ hidden: false });
const result = await wifiCommands._pickNetworkManually();
expect(result).to.eql({ ssid: 'ssid', security: 'WPA2_PSK', password: 'password', hidden: false });
expect(ui.prompt).to.have.callCount(4);
expect(ui.prompt.firstCall).to.have.been.calledWith([{
type: 'input',
name: 'ssid',
message: 'SSID',
validate: sinon.match.func,
filter: sinon.match.func
}]);
expect(ui.prompt.secondCall).to.have.been.calledWith([{
choices: ['NO_SECURITY', 'WEP', 'WPA_PSK', 'WPA2_PSK', 'WPA3_PSK'],
type: 'list',
name: 'security',
message: 'Select the security type for your Wi-Fi network:'
}]);
expect(ui.prompt.thirdCall).to.have.been.calledWith([{
type: 'input',
name: 'password',
message: 'Wi-Fi Password',
validate: sinon.match.func
}]);
expect(ui.prompt.lastCall).to.have.been.calledWith([{
default: false,
message: 'Is this a hidden network?',
type: 'confirm',
name: 'hidden'
}]);
});
});
describe('_filterNetworks', () => {
it('filters out and remove null, undefined and 5GHZ networks', () => {
const networks = [
{
ssid: null,
bssid: 'abc123',
security: 'WPA2',
rssi: -50,
channel: 6
},
{
ssid: 'network1',
bssid: 'def456',
security: 'WPA2',
rssi: -50,
channel: 6
},
{
ssid: 'network2',
bssid: 'ghi789',
security: 'WPA2',
rssi: -50,
channel: 15
},
null,
];
const wifiCommands = new WiFiCommands({ ui });
const result = wifiCommands._filterNetworks(networks);
expect(result).to.eql([
{
ssid: 'network1',
bssid: 'def456',
security: 'WPA2',
rssi: -50,
channel: 6
},
]);
});
});
describe('_deviceScanNetworks', () => {
it('returns a list of networks from a particle device', async () => {
openDevice.scanWifiNetworks.resolves([{
ssid: 'network1',
security: 'WPA2',
rssi: -50,
channel: 6
}]);
const wifiCommands = new WiFiCommands({ ui });
const networks = await wifiCommands._deviceScanNetworks(openDevice);
expect(networks).to.eql([{
ssid: 'network1',
security: 'WPA2',
signal_level: -50,
channel: '6',
unsecure: false,
mac: ''
}]);
});
it('throws an error if fails after retrying', async () => {
openDevice.scanWifiNetworks.rejects(new Error('error'));
let error;
const wifiCommands = new WiFiCommands({ ui });
try {
await wifiCommands._deviceScanNetworks(openDevice, { customRetryCount: 1 });
} catch (_error) {
error = _error;
}
expect(error).to.be.an.instanceOf(Error);
expect(error.message).to.eql(`Unable to scan for Wi-Fi networks: error${os.EOL}`);
expect(utilities.delay).to.have.been.calledWith(1000);
});
it('returns empty if there is no networks', async () => {
openDevice.scanWifiNetworks.resolves([]);
const wifiCommands = new WiFiCommands({ ui });
const networks = await wifiCommands._deviceScanNetworks(openDevice);
expect(networks).to.eql([]);
});
});
describe('_promptToSelectNetwork', () => {
it('prompts to select a network', async () => {
const networks = [
{
ssid: 'network1',
security: 'WPA2_PSK',
signal_level: -50,
channel: 6,
unsecure: true,
mac: ''
}];
const wifiCommands = new WiFiCommands({ ui });
ui.prompt = sinon.stub();
ui.prompt.resolves({ network: 'network1' });
const result = await wifiCommands._promptToSelectNetwork(networks);
expect(result).to.eql({ ssid: 'network1', security: 'WPA2_PSK', password: undefined });
});
it('asks for password if network is not unsecure', async () => {
const networks = [
{
ssid: 'network1',
security: 'WPA2_PSK',
signal_level: -50,
channel: 6,
unsecure: false,
mac: ''
}];
const wifiCommands = new WiFiCommands({ ui });
ui.prompt = sinon.stub();
ui.prompt.onCall(0).resolves({ network: 'network1' });
ui.prompt.onCall(1).resolves({ password: 'password' });
const result = await wifiCommands._promptToSelectNetwork(networks);
expect(result).to.eql({ ssid: 'network1', security: 'WPA2_PSK', password: 'password' });
expect(ui.prompt).to.have.been.calledTwice;
expect(ui.prompt).to.calledWithMatch([{
choices: sinon.match.func,
message: 'Select the Wi-Fi network with which you wish to connect your device:',
name: 'network',
type: 'list',
when: sinon.match.func
}
]);
expect(ui.prompt).to.have.been.calledWith([{
type: 'input',
name: 'password',
message: 'Wi-Fi Password',
validate: sinon.match.func
}]);
});
it('returns rescan if RESCAN_LABEL is selected', async () => {
const networks = [
{
ssid: 'network1',
security: 'WPA2',
signal_level: -50,
channel: 6,
unsecure: true,
mac: ''
}];
const wifiCommands = new WiFiCommands({ ui });
ui.prompt = sinon.stub();
ui.prompt.resolves({ network: '[rescan networks]' });
const result = await wifiCommands._promptToSelectNetwork(networks);
expect(result).to.eql({ ssid: null, rescan: true });
});
});
describe('_getNetworkToConnectFromJson', () => {
beforeEach(async () => {
await fs.ensureDir(path.join(PATH_TMP_DIR, 'networks'));
});
afterEach(async () => {
await fs.remove(path.join(PATH_TMP_DIR, 'networks'));
});
it('returns network from json', async () => {
// create a file with a network
const network = { network: 'my-network', security: 'WPA2_PSK', password: 'my-password' };
const file = path.join(PATH_TMP_DIR, 'networks', 'network.json');
fs.writeJsonSync(file, network);
const wifiCommands = new WiFiCommands({ ui });
wifiCommands.file = file;
const result = await wifiCommands._getNetworkToConnectFromJson();
expect(result).to.eql({ ssid: network.network, security: 'WPA2_PSK', password: network.password, hidden: undefined });
});
it('returns a hidden network from json', async () => {
// create a file with a network
const network = { network: 'my-network', security: 'WPA2_PSK', password: 'my-password', hidden: true };
const file = path.join(PATH_TMP_DIR, 'networks', 'network.json');
fs.writeJsonSync(file, network);
const wifiCommands = new WiFiCommands({ ui });
wifiCommands.file = file;
const result = await wifiCommands._getNetworkToConnectFromJson();
expect(result).to.eql({ ssid: network.network, security: 'WPA2_PSK', password: network.password, hidden: true });
});
it('throws error if file does not exist', async () => {
const fileName = path.join(process.cwd(), 'fake-file');
const wifiCommands = new WiFiCommands({ ui });
wifiCommands.file = fileName;
const expectedErrorMessage = `ENOENT: no such file or directory, open '${fileName}'`;
let error;
try {
await wifiCommands._getNetworkToConnectFromJson();
} catch (e) {
error = e;
}
expect(error).to.be.an.instanceOf(Error);
expect(error.message).to.eql(expectedErrorMessage);
});
it('throws an error in case the file does not contain network property', async () => {
const network = { password: 'my-password' };
const file = path.join(PATH_TMP_DIR, 'networks', 'network.json');
fs.writeJsonSync(file, network);
const wifiCommands = new WiFiCommands({ ui });
wifiCommands.file = file;
let error;
try {
await wifiCommands._getNetworkToConnectFromJson();
} catch (_error) {
error = _error;
}
expect(error.message).to.eql('No network name found in the file');
expect(error.isUsageError).to.eql(true);
});
});
describe('_scanNetworks', () => {
it('returns a list of networks', async () => {
const networks = [
{
ssid: 'network1',
security: 'WPA2',
rssi: -50,
channel: 6
}
];
const wifiCommands = new WiFiCommands({ ui });
wifiCommands._deviceScanNetworks = sinon.stub().resolves(networks);
const result = await wifiCommands._scanNetworks();
expect(result).to.eql(networks);
});
it ('prompts to rescan if there are no networks', async () => {
const wifiCommands = new WiFiCommands({ ui });
wifiCommands._deviceScanNetworks = sinon.stub().resolves([]);
ui.prompt = sinon.stub();
ui.prompt.onCall(0).resolves({ rescan: true });
ui.prompt.onCall(1).resolves({ rescan: false });
const result = await wifiCommands._scanNetworks();
expect(result).to.eql([]);
expect(ui.prompt).to.have.been.calledWith([{
default: true,
message: 'No networks found. Try again?',
type: 'confirm',
name: 'rescan',
}]);
});
});
describe('_getNetworkToConnect', () => {
it('returns network from prompt', async () => {
const networks = [
{
ssid: 'network1',
security: 'WPA2',
signal_level: -50,
channel: 6,
unsecure: true,
mac: ''
}];
const wifiCommands = new WiFiCommands('deviceId', { ui, newSpin, stopSpin });
wifiCommands._promptForScanNetworks = sinon.stub().resolves(true);
wifiCommands._scanNetworks = sinon.stub().resolves(networks);
wifiCommands._promptToSelectNetwork = sinon.stub().resolves({ ssid: 'network1', password: 'password' });
const result = await wifiCommands._getNetworkToConnect();
expect(result).to.eql({ ssid: 'network1', password: 'password' });
});
it('returns network from manual input', async () => {
const wifiCommands = new WiFiCommands('deviceId', { ui, newSpin, stopSpin });
wifiCommands._promptForScanNetworks = sinon.stub().resolves(false);
wifiCommands._pickNetworkManually = sinon.stub().resolves({ ssid: 'network1', password: 'password' });
const result = await wifiCommands._getNetworkToConnect();
expect(result).to.eql({ ssid: 'network1', password: 'password' });
});
});
describe('_getActionStringFromOp', () => {
it('converts an operation name to a verb form', () => {
const wifiCommands = new WiFiCommands('deviceId', { ui, newSpin, stopSpin });
const operationName = 'Adding Wi-Fi network';
const expected = 'add Wi-Fi network';
const result = wifiCommands._getActionStringFromOp(operationName);
expect(result).to.equal(expected);
});
});
describe('_performWifiOperation', () => {
let wifiCommands;
let operationCallback;
let device;
beforeEach(() => {
wifiCommands = new WiFiCommands({ ui });
device = {
isOpen: false,
platformId: 35,
_id: 'deviceId'
};
operationCallback = sinon.stub();
});
it('returns true if operationCallback returns a replyObject', async () => {
const replyObject = { foo: 'bar' };
operationCallback.resolves(replyObject);
const result = await wifiCommands._performWifiOperation(device, 'Test Operation', operationCallback);
expect(result).to.eql(replyObject);
expect(operationCallback).to.have.been.calledOnce;
});
it('throws an error if operationCallback throws an error with message "Not supported"', async () => {
operationCallback.rejects(new Error('Random Error'));
let error;
try {
await wifiCommands._performWifiOperation(device, 'Test Operation', operationCallback);
} catch (e) {
error = e;
}
expect(error).to.be.an.instanceOf(Error);
expect(error.message).to.equal(`Unable to test Operation: Random Error${os.EOL}`);
});
});
describe('_handleDeviceError', () => {
it('returns an error with the appropriate message and helper string for "Invalid state" error', () => {
const wifiCommands = new WiFiCommands({ ui });
const error = { name: 'Invalid state', message: 'Invalid state', cause: false };
const result = wifiCommands._handleDeviceError(error, { action: 'join wi-fi network' });
expect(result).to.be.an.instanceOf(Error);
expect(result.message).to.include('Unable to join wi-fi network: Invalid state');
});
it('returns an error with the appropriate message and helper string for "Not found" error', () => {
const wifiCommands = new WiFiCommands('deviceId', { ui, newSpin, stopSpin });
const error = { name: 'Not found', message: 'Not found', cause: false };
const result = wifiCommands._handleDeviceError(error, { action: 'join wi-fi network' });
expect(result).to.be.an.instanceOf(Error);
expect(result.message).to.equal(`Unable to join wi-fi network: Not found${os.EOL}If you are using a hidden network, please add the hidden network credentials first using 'particle wifi add'.`);
});
it('returns an error with the appropriate message and helper string for "Not supported" error', () => {
const wifiCommands = new WiFiCommands('deviceId', { ui, newSpin, stopSpin });
const error = { name: 'Not supported', message: 'Not supported', cause: false };
const resultError = wifiCommands._handleDeviceError(error, { action: 'join wi-fi network' });
expect(resultError).to.be.an.instanceOf(Error);
expect(resultError.message).to.include(`Unable to join wi-fi network: Not supported${os.EOL}This feature is likely not supported on this firmware version.${os.EOL}Update to device-os 6.2.0 or use 'particle wifi join --help' to join a network.${os.EOL}Alternatively, check 'particle serial wifi'.${os.EOL}`);
});
it('returns an error without a helper string for unknown error messages', () => {
const wifiCommands = new WiFiCommands('deviceId', { ui, newSpin, stopSpin });
const error = { name: 'Unknown error', message: 'Unknown error', cause: false };
const result = wifiCommands._handleDeviceError(error, { action: 'join wi-fi network' });
expect(result).to.be.an.instanceOf(Error);
expect(result.message).to.equal(`Unable to join wi-fi network: Unknown error${os.EOL}`);
});
});
});