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);