diff --git a/lib/app-capitano.coffee b/lib/app-capitano.coffee index ec547a8d..91872370 100644 --- a/lib/app-capitano.coffee +++ b/lib/app-capitano.coffee @@ -146,7 +146,7 @@ capitano.command(actions.join.join) capitano.command(actions.leave.leave) exports.run = (argv) -> - cli = capitano.parse(argv) + cli = capitano.parse(argv.slice(2)) runCommand = -> capitanoExecuteAsync = Promise.promisify(capitano.execute) if cli.global?.help diff --git a/lib/app.ts b/lib/app.ts index 7ea9381d..fc75a655 100644 --- a/lib/app.ts +++ b/lib/app.ts @@ -57,24 +57,25 @@ function routeCliFramework(argv: string[], options: AppOptions): void { } const [isOclif, isTopic] = isOclifCommand(cmdSlice); + if (isOclif) { + let oclifArgs = cmdSlice; if (isTopic) { // convert space-separated commands to oclif's topic:command syntax - argv = [ - argv[0], - argv[1], - cmdSlice[0] + ':' + cmdSlice[1], - ...cmdSlice.slice(2), - ]; - } else { - argv = [argv[0], argv[1], ...cmdSlice]; + oclifArgs = [cmdSlice[0] + ':' + cmdSlice[1], ...cmdSlice.slice(2)]; } if (process.env.DEBUG) { - console.log(`[debug] new argv=[${argv}] length=${argv.length}`); + console.log( + `[debug] new argv=[${[ + argv[0], + argv[1], + ...oclifArgs, + ]}] length=${oclifArgs.length + 2}`, + ); } - return require('./app-oclif').run(cmdSlice, options); + return require('./app-oclif').run(oclifArgs, options); } else { - return require('./app-capitano').run(cmdSlice); + return require('./app-capitano').run(argv); } } diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 61de4e53..ff4f2a27 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -575,6 +575,15 @@ "@types/node": "*" } }, + "@types/nock": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@types/nock/-/nock-10.0.3.tgz", + "integrity": "sha512-OthuN+2FuzfZO3yONJ/QVjKmLEuRagS9TV9lEId+WHL9KhftYG+/2z+pxlr0UgVVXSpVD8woie/3fzQn8ft/Ow==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "10.14.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.5.tgz", @@ -12390,6 +12399,23 @@ } } }, + "nock": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz", + "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==", + "dev": true, + "requires": { + "chai": "^4.1.2", + "debug": "^4.1.0", + "deep-equal": "^1.0.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.5", + "mkdirp": "^0.5.0", + "propagate": "^1.0.0", + "qs": "^6.5.1", + "semver": "^5.5.0" + } + }, "node-abi": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.9.0.tgz", @@ -13466,6 +13492,12 @@ "resolved": "https://registry.npmjs.org/promise-memoize/-/promise-memoize-1.2.1.tgz", "integrity": "sha1-cflVSpic9r+Jh7Q6UhMtXHkxJlc=" }, + "propagate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", + "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", + "dev": true + }, "proper-lockfile": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-1.2.0.tgz", diff --git a/package.json b/package.json index db2eef41..1265ff9a 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "@types/mocha": "^5.2.7", "@types/mz": "0.0.32", "@types/net-keepalive": "^0.4.0", + "@types/nock": "^10.0.3", "@types/node": "10.14.5", "@types/prettyjson": "0.0.28", "@types/raven": "2.5.1", @@ -118,6 +119,7 @@ "gulp-inline-source": "^2.1.0", "gulp-shell": "^0.5.2", "mocha": "^6.2.0", + "nock": "^10.0.6", "pkg": "^4.4.0", "prettier": "1.17.0", "publish-release": "^1.6.0", diff --git a/tests/commands/env/add.spec.ts b/tests/commands/env/add.spec.ts new file mode 100644 index 00000000..0f383759 --- /dev/null +++ b/tests/commands/env/add.spec.ts @@ -0,0 +1,29 @@ +import * as chai from 'chai'; +import { balenaAPIMock, runCommand } from '../../helpers'; + +describe('balena env add', function() { + it('should successfully add an environment variable', async () => { + const deviceId = 'f63fd7d7812c34c4c14ae023fdff05f5'; + const mock = balenaAPIMock(); + mock + .get(/device/) + .reply(201, { + d: [ + { + id: 1031543, + __metadata: { uri: '/resin/device(@id)?@id=1031543' }, + }, + ], + }) + .post(/device_environment_variable/) + .reply(200, 'OK'); + + const { out, err } = await runCommand(`env add TEST 1 -d ${deviceId}`); + + chai.expect(out.join('')).to.equal(''); + chai.expect(err.join('')).to.equal(''); + + // @ts-ignore + mock.remove(); + }); +}); diff --git a/tests/commands/env/rm.spec.ts b/tests/commands/env/rm.spec.ts new file mode 100644 index 00000000..322fafc5 --- /dev/null +++ b/tests/commands/env/rm.spec.ts @@ -0,0 +1,17 @@ +import * as chai from 'chai'; +import { balenaAPIMock, runCommand } from '../../helpers'; + +describe('balena env rm', function() { + it('should successfully delete an environment variable', async () => { + const mock = balenaAPIMock(); + mock.delete(/device_environment_variable/).reply(200, 'OK'); + + const { out, err } = await runCommand('env rm 144690 -d -y'); + + chai.expect(out.join('')).to.equal(''); + chai.expect(err.join('')).to.equal(''); + + // @ts-ignore + mock.remove(); + }); +}); diff --git a/tests/commands/help.spec.ts b/tests/commands/help.spec.ts new file mode 100644 index 00000000..6fb32a36 --- /dev/null +++ b/tests/commands/help.spec.ts @@ -0,0 +1,137 @@ +import * as chai from 'chai'; +import * as _ from 'lodash'; +import { runCommand } from '../helpers'; + +const SIMPLE_HELP = ` +Usage: balena [COMMAND] [OPTIONS] + +If you need help, or just want to say hi, don't hesitate in reaching out +through our discussion and support forums at https://forums.balena.io + +For bug reports or feature requests, have a look at the GitHub issues or +create a new one at: https://github.com/balena-io/balena-cli/issues/ + +Primary commands: + + help [command...] show help + login login to balena + push Start a remote build on the balena cloud build servers or a local mode device + logs show device logs + ssh [serviceName] SSH into the host or application container of a device + apps list all applications + app list a single application + devices list all devices + device list a single device + tunnel Tunnel local ports to your balenaOS device + preload preload an app on a disk image (or Edison zip archive) + build [source] Build a single image or a multicontainer project locally + deploy [image] Deploy a single image or a multicontainer project to a balena application + join [deviceIp] Promote a local device running balenaOS to join an application on a balena server + leave [deviceIp] Detach a local device from its balena application + scan Scan for balenaOS devices in your local network + +`; + +const ADDITIONAL_HELP = ` +Additional commands: + + api-key generate Generate a new API key with the given name + app create create an application + app restart restart an application + app rm remove an application + config generate generate a config.json file + config inject inject a device configuration file + config read read a device configuration + config reconfigure reconfigure a provisioned device + config write write a device configuration + device identify identify a device with a UUID + device init initialise a device with balenaOS + device move move a device to another application + device os-update Start a Host OS update for a device + device public-url gets the public URL of a device + device public-url disable disable public URL for a device + device public-url enable enable public URL for a device + device public-url status Returns true if public URL is enabled for a device + device reboot restart a device + device register register a device + device rename [newName] rename a balena device + device rm remove a device + device shutdown shutdown a device + devices supported list all supported devices + env add name [value] add an environment or config variable to an application or device + env rename rename an environment variable + env rm id remove an environment variable from an application or device + envs list all environment variables + key list a single ssh key + key add [path] add a SSH key to balena + key rm remove a ssh key + keys list all ssh keys + local configure (Re)configure a balenaOS drive or image + local flash Flash an image to a drive + logout logout from balena + note <|note> set a device note + os build-config build the OS config and save it to the JSON file + os configure configure an os image + os download download an unconfigured os image + os initialize initialize an os image + os versions show the available balenaOS versions for the given device type + settings print current settings + tag rm remove a resource tag + tag set [value] set a resource tag + tags list all resource tags + util available-drives list available drives + version display version information for the balena CLI and/or Node.js + whoami get current username and email address + +`; + +const GLOBAL_OPTIONS = ` + Global Options: + + --help, -h + --version, -v +`; + +const cleanOutput = (output: string[] | string) => { + return _(_.castArray(output)) + .map(log => { + return log.split('\n').map(line => { + return line.trim(); + }); + }) + .flatten() + .compact() + .value(); +}; + +describe('balena help', function() { + it('should print simple help text', async () => { + const { out, err } = await runCommand('help'); + + chai + .expect(cleanOutput(out)) + .to.deep.equal( + cleanOutput([ + SIMPLE_HELP, + 'Run `balena help --verbose` to list additional commands', + GLOBAL_OPTIONS, + ]), + ); + + chai.expect(err.join('')).to.equal(''); + }); + + it('should print additional commands with the -v flag', async () => { + const { out, err } = await runCommand('help -v'); + + chai + .expect(cleanOutput(out)) + .to.deep.equal( + cleanOutput([SIMPLE_HELP, ADDITIONAL_HELP, GLOBAL_OPTIONS]), + ); + + chai.expect(err.join('')).to.equal(''); + + chai.expect(err.join('')).to.equal(''); + }); +}); diff --git a/tests/version.spec.ts b/tests/commands/version.spec.ts similarity index 96% rename from tests/version.spec.ts rename to tests/commands/version.spec.ts index 20630946..55cab1d2 100644 --- a/tests/version.spec.ts +++ b/tests/commands/version.spec.ts @@ -1,6 +1,6 @@ import * as chai from 'chai'; import * as fs from 'fs'; -import { runCommand } from './helpers'; +import { runCommand } from '../helpers'; const packageJSON = JSON.parse(fs.readFileSync('./package.json', 'utf8')); const nodeVersion = process.version.startsWith('v') diff --git a/tests/helpers.ts b/tests/helpers.ts index 92652fd2..94d57915 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -1,3 +1,4 @@ +import * as nock from 'nock'; import * as path from 'path'; import * as balenaCLI from '../build/app'; @@ -21,7 +22,12 @@ export const runCommand = async (cmd: string) => { // @ts-ignore process.stderr.write = (log: string) => { // Skip over debug messages - if (!log.startsWith('[debug]')) { + if ( + !log.startsWith('[debug]') && + // TODO stop this warning message from appearing when running + // sdk.setSharedOptions multiple times in the same process + !log.startsWith('Shared SDK options') + ) { err.push(log); } oldStdErr(log); @@ -46,3 +52,17 @@ export const runCommand = async (cmd: string) => { throw err; } }; + +export const balenaAPIMock = () => { + return nock(/./) + .get('/config/vars') + .reply(200, { + reservedNames: [], + reservedNamespaces: [], + invalidRegex: '/^d|W/', + whiteListedNames: [], + whiteListedNamespaces: [], + blackListedNames: [], + configVarSchema: [], + }); +};