From dc65a31a01b53e8081e14018ea221e9484adf1ff Mon Sep 17 00:00:00 2001 From: busticated Date: Sat, 1 Aug 2020 10:38:17 -0700 Subject: [PATCH 1/2] clarify that `product device add` command accepts `deviceID` (vs. `device`) --- src/cli/product.js | 2 +- src/cli/product.test.js | 4 ++-- src/cmd/product.js | 20 ++++++++++---------- test/e2e/product.e2e.js | 10 +++++----- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/cli/product.js b/src/cli/product.js index 5f9fec291..308b24448 100644 --- a/src/cli/product.js +++ b/src/cli/product.js @@ -41,7 +41,7 @@ module.exports = ({ commandProcessor, root }) => { }); commandProcessor.createCommand(device, 'add', 'Adds one or more devices into a Product', { - params: ' [device]', + params: ' [deviceID]', options: { file: { alias: 'f', diff --git a/src/cli/product.test.js b/src/cli/product.test.js index a518acdcd..ac2f794e1 100644 --- a/src/cli/product.test.js +++ b/src/cli/product.test.js @@ -104,7 +104,7 @@ describe('Product Command-Line Interface', () => { it('Parses arguments', () => { const argv = commandProcessor.parse(root, ['product', 'device', 'add', '12345', '5a8ef38cb85f8720edce631a']); expect(argv.clierror).to.equal(undefined); - expect(argv.params).to.eql({ product: '12345', device: '5a8ef38cb85f8720edce631a' }); + expect(argv.params).to.eql({ product: '12345', deviceID: '5a8ef38cb85f8720edce631a' }); }); it('Errors when required arguments are missing', () => { @@ -122,7 +122,7 @@ describe('Product Command-Line Interface', () => { commandProcessor.showHelp((helpText) => { expect(helpText).to.equal([ 'Adds one or more devices into a Product', - 'Usage: particle product device add [options] [device]', + 'Usage: particle product device add [options] [deviceID]', '', 'Options:', ' --file, -f Path to single column .txt file with list of IDs, S/Ns, IMEIs, or ICCIDs of the devices to add [string]', diff --git a/src/cmd/product.js b/src/cmd/product.js index f6faaa259..cd627681f 100644 --- a/src/cmd/product.js +++ b/src/cmd/product.js @@ -14,26 +14,26 @@ module.exports = class ProductCommand extends CLICommandBase { super(...args); } - addDevice({ params: { product, device } }){ - const identifiers = [device]; - const msg = `Adding device ${device} to product ${product}`; + addDevice({ params: { product, deviceID } }){ + const identifiers = [deviceID]; + const msg = `Adding device ${deviceID} to product ${product}`; const upload = uploadProductDevices(product, identifiers); return this.ui.showBusySpinnerUntilResolved(msg, upload) .then(result => this.showDeviceAddResult(result)); } - addDevices({ file, params: { product, device } }){ - if (!device && !file){ + addDevices({ file, params: { product, deviceID } }){ + if (!deviceID && !file){ throw usageError( - '`device` parameter or `--file` option is required' + '`deviceID` parameter or `--file` option is required' ); } - if (device){ - if (!this.isDeviceId(device)){ - return this.showUsageError(`\`device\` parameter must be an id - received: ${device}`); + if (deviceID){ + if (!this.isDeviceId(deviceID)){ + return this.showUsageError(`\`deviceID\` parameter must be an id - received: ${deviceID}`); } - return this.addDevice({ params: { product, device } }); + return this.addDevice({ params: { product, deviceID } }); } const msg = `Adding devices in ${file} to product ${product}`; diff --git a/test/e2e/product.e2e.js b/test/e2e/product.e2e.js index 83b7a3e1b..4e5957c87 100644 --- a/test/e2e/product.e2e.js +++ b/test/e2e/product.e2e.js @@ -283,7 +283,7 @@ describe('Product Commands', () => { const deviceIDsEmptyFilePath = path.join(PATH_TMP_DIR, 'product-device-ids-empty.txt'); const help = [ 'Adds one or more devices into a Product', - 'Usage: particle product device add [options] [device]', + 'Usage: particle product device add [options] [deviceID]', '', 'Global Options:', ' -v, --verbose Increases how much logging to display [count]', @@ -333,20 +333,20 @@ describe('Product Commands', () => { expect(exitCode).to.equal(0); }); - it('Fails to add a single device when `device` param or `--file` flag is not provided', async () => { + it('Fails to add a single device when `deviceID` param or `--file` flag is not provided', async () => { const args = ['product', 'device', 'add', PRODUCT_01_ID]; const { stdout, stderr, exitCode } = await cli.run(args); - expect(stdout).to.include('`device` parameter or `--file` option is required'); + expect(stdout).to.include('`deviceID` parameter or `--file` option is required'); expect(stderr.split(os.EOL)).to.include.members(help); expect(exitCode).to.equal(1); }); - it('Fails to add a single device when `device` param is not an id', async () => { + it('Fails to add a single device when `deviceID` param is not an id', async () => { const args = ['product', 'device', 'add', PRODUCT_01_ID, PRODUCT_01_DEVICE_01_NAME]; const { stdout, stderr, exitCode } = await cli.run(args); - expect(stdout).to.include(`\`device\` parameter must be an id - received: ${PRODUCT_01_DEVICE_01_NAME}`); + expect(stdout).to.include(`\`deviceID\` parameter must be an id - received: ${PRODUCT_01_DEVICE_01_NAME}`); expect(stderr.split(os.EOL)).to.include.members(help); expect(exitCode).to.equal(1); }); From e396c9fedfed36c7a0a7375a53fff0d6d5c5f64f Mon Sep 17 00:00:00 2001 From: busticated Date: Sat, 1 Aug 2020 12:56:31 -0700 Subject: [PATCH 2/2] add `product device remove` command --- src/cli/product.js | 10 +++++ src/cli/product.test.js | 5 ++- src/cmd/product.js | 21 +++++++++++ test/e2e/help.e2e.js | 4 +- test/e2e/product.e2e.js | 83 +++++++++++++++++++++++++++++++++++++---- 5 files changed, 111 insertions(+), 12 deletions(-) diff --git a/src/cli/product.js b/src/cli/product.js index 308b24448..53da2dbfa 100644 --- a/src/cli/product.js +++ b/src/cli/product.js @@ -58,5 +58,15 @@ module.exports = ({ commandProcessor, root }) => { } }); + commandProcessor.createCommand(device, 'remove', 'Removes a device from a Product', { + params: ' ', + examples: { + '$0 $command 12345 0123456789abcdef01234567': 'Remove device id `0123456789abcdef01234567` from product `12345`', + }, + handler: (args) => { + const ProdCmd = require('../cmd/product'); + return new ProdCmd(args).removeDevice(args); + } + }); return product; }; diff --git a/src/cli/product.test.js b/src/cli/product.test.js index ac2f794e1..c5e18bbc7 100644 --- a/src/cli/product.test.js +++ b/src/cli/product.test.js @@ -51,8 +51,9 @@ describe('Product Command-Line Interface', () => { 'Help: particle help product device ', '', 'Commands:', - ' list List all devices that are part of a product', - ' add Adds one or more devices into a Product', + ' list List all devices that are part of a product', + ' add Adds one or more devices into a Product', + ' remove Removes a device from a Product', '' ].join(os.EOL)); }); diff --git a/src/cmd/product.js b/src/cmd/product.js index cd627681f..580735760 100644 --- a/src/cmd/product.js +++ b/src/cmd/product.js @@ -81,6 +81,27 @@ module.exports = class ProductCommand extends CLICommandBase { return this.ui.write(message.join(os.EOL)); } + removeDevice({ params: { product, deviceID } }){ + if (!this.isDeviceId(deviceID)){ + return this.showUsageError(`\`deviceID\` parameter must be an id - received: ${deviceID}`); + } + + const msg = `Removing device ${deviceID} from product ${product}`; + const remove = createAPI() + .removeDevice(deviceID, product) + .catch(error => { + const message = 'Error removing device from product'; + throw createAPIErrorResult({ error, message, json: false }); + }); + + return this.ui.showBusySpinnerUntilResolved(msg, remove) + .then(() => this.showDeviceRemoveResult({ product, deviceID })); + } + + showDeviceRemoveResult({ product, deviceID }){ + return this.ui.write(`Success! Removed device ${deviceID} from product ${product}${os.EOL}`); + } + showDeviceDetail({ json, params: { product, device } }){ const msg = `Fetching device ${device} detail`; const fetchData = createAPI().getDeviceAttributes(device, product); diff --git a/test/e2e/help.e2e.js b/test/e2e/help.e2e.js index a3038b1b0..3a5a74d86 100644 --- a/test/e2e/help.e2e.js +++ b/test/e2e/help.e2e.js @@ -57,8 +57,8 @@ describe('Help & Unknown Command / Argument Handling', () => { 'library', 'list', 'login', 'logout', 'mesh create', 'mesh add', 'mesh remove', 'mesh list', 'mesh info', 'mesh scan', 'mesh', 'monitor', 'nyan', 'preprocess', 'product device list', 'product device add', - 'product device', 'product', 'project create', 'project', 'publish', - 'serial list', 'serial monitor', 'serial identify', 'serial wifi', + 'product device remove', 'product device', 'product', 'project create', + 'project', 'publish', 'serial list', 'serial monitor', 'serial identify', 'serial wifi', 'serial mac', 'serial inspect', 'serial flash', 'serial claim', 'serial', 'setup', 'subscribe', 'token list', 'token revoke', 'token create', 'token', 'udp send', 'udp listen', 'udp', 'update', diff --git a/test/e2e/product.e2e.js b/test/e2e/product.e2e.js index 4e5957c87..dd405f8fd 100644 --- a/test/e2e/product.e2e.js +++ b/test/e2e/product.e2e.js @@ -72,14 +72,14 @@ describe('Product Commands', () => { // TODO (mirande): sometimes entity includes: `pinned_build_target` const detailedDeviceFieldNames = ['cellular', 'connected', 'current_build_target', 'default_build_target', 'denied', - 'desired_firmware_version', 'development', 'firmware_product_id', - 'firmware_updates_enabled', 'firmware_updates_forced', - 'firmware_version', 'functions', 'groups', 'iccid', 'id', 'imei', - 'last_handshake_at', 'last_heard', 'last_iccid', 'last_ip_address', - 'mobile_secret', 'name', 'notes', 'online', 'owner', - 'pinned_build_target', 'platform_id', 'product_id', 'quarantined', - 'serial_number', 'status', 'system_firmware_version', - 'targeted_firmware_release_version', 'variables']; + 'development', 'firmware_product_id', 'firmware_updates_enabled', + 'firmware_updates_forced', 'firmware_version', 'functions', 'groups', + 'iccid', 'id', 'imei', 'last_handshake_at', 'last_heard', + 'last_iccid', 'last_ip_address', 'mobile_secret', 'name', 'notes', + 'online', 'owner', 'pinned_build_target', 'platform_id', + 'product_id', 'quarantined', 'serial_number', 'status', + 'system_firmware_version', 'targeted_firmware_release_version', + 'variables']; it('Lists devices', async () => { const args = ['product', 'device', 'list', PRODUCT_01_ID]; @@ -440,6 +440,73 @@ describe('Product Commands', () => { }); }); + describe('Device Remove Subcommand', () => { + const help = [ + 'Removes a device from a Product', + 'Usage: particle product device remove [options] ', + '', + 'Global Options:', + ' -v, --verbose Increases how much logging to display [count]', + ' -q, --quiet Decreases how much logging to display [count]', + '', + 'Examples:', + ' particle product device remove 12345 0123456789abcdef01234567 Remove device id `0123456789abcdef01234567` from product `12345`' + ]; + + before(async () => { + await cli.setTestProfileAndLogin(); + }); + + after(async () => { + await cli.run(['product', 'device', 'add', PRODUCT_01_ID, PRODUCT_01_DEVICE_01_ID]); + }); + + it('Removes a device', async () => { + const args = ['product', 'device', 'remove', PRODUCT_01_ID, PRODUCT_01_DEVICE_01_ID]; + const { stdout, stderr, exitCode } = await cli.run(args); + + expect(stdout).to.include(`Success! Removed device ${PRODUCT_01_DEVICE_01_ID} from product ${PRODUCT_01_ID}${os.EOL}`); + expect(stderr).to.equal(''); + expect(exitCode).to.equal(0); + }); + + it('Fails to remove a device when `deviceID` param is not provided', async () => { + const args = ['product', 'device', 'remove', PRODUCT_01_ID]; + const { stdout, stderr, exitCode } = await cli.run(args); + + expect(stdout).to.include('Parameter \'deviceID\' is required.'); + expect(stderr.split(os.EOL)).to.include.members(help); + expect(exitCode).to.equal(1); + }); + + it('Fails to remove a device when `deviceID` param is not an id', async () => { + const args = ['product', 'device', 'remove', PRODUCT_01_ID, PRODUCT_01_DEVICE_01_NAME]; + const { stdout, stderr, exitCode } = await cli.run(args); + + expect(stdout).to.include(`\`deviceID\` parameter must be an id - received: ${PRODUCT_01_DEVICE_01_NAME}`); + expect(stderr.split(os.EOL)).to.include.members(help); + expect(exitCode).to.equal(1); + }); + + it('Fails to remove a device when `product` is unknown', async () => { + const args = ['product', 'device', 'remove', 'LOLWUTNOPE', PRODUCT_01_DEVICE_01_ID]; + const { stdout, stderr, exitCode } = await cli.run(args); + + expect(stdout).to.include('HTTP error 404'); + expect(stderr).to.equal(''); + expect(exitCode).to.equal(1); + }); + + it('Fails to remove a device when `device` is unknown', async () => { + const args = ['product', 'device', 'remove', PRODUCT_01_ID, '000000000000000000000001']; + const { stdout, stderr, exitCode } = await cli.run(args); + + expect(stdout).to.include('Error removing device from product: Device not found for this product'); + expect(stderr).to.equal(''); + expect(exitCode).to.equal(1); + }); + }); + function parseAndSortDeviceList(stdout){ const json = JSON.parse(stdout);