mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-02-20 17:33:18 +00:00
Merge pull request #1533 from balena-io/1530-fix-missing-app-issues
Fix issues with devices associated with inaccessible applications.
This commit is contained in:
commit
29cc75598f
@ -53,7 +53,8 @@ exports.list =
|
||||
.tap (devices) ->
|
||||
devices = _.map devices, (device) ->
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid)
|
||||
device.application_name = device.belongs_to__application[0].app_name
|
||||
device.application_name =
|
||||
if device.belongs_to__application?[0] then device.belongs_to__application[0].app_name else 'N/a'
|
||||
device.uuid = device.uuid.slice(0, 7)
|
||||
return device
|
||||
|
||||
@ -93,7 +94,8 @@ exports.info =
|
||||
balena.models.device.getStatus(device).then (status) ->
|
||||
device.status = status
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid)
|
||||
device.application_name = device.belongs_to__application[0].app_name
|
||||
device.application_name =
|
||||
if device.belongs_to__application?[0] then device.belongs_to__application[0].app_name else 'N/a'
|
||||
device.commit = device.is_on__commit
|
||||
|
||||
console.log visuals.table.vertical device, [
|
||||
@ -345,6 +347,8 @@ exports.move =
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
balena.models.device.get(params.uuid, expandForAppName).then (device) ->
|
||||
device.application_name =
|
||||
if device.belongs_to__application?[0] then device.belongs_to__application[0].app_name else 'N/a'
|
||||
return options.application if options.application
|
||||
|
||||
return Promise.all([
|
||||
@ -359,7 +363,7 @@ exports.move =
|
||||
return patterns.selectApplication (application) ->
|
||||
return _.every [
|
||||
_.some(compatibleDeviceTypes, (dt) -> dt.slug == application.device_type)
|
||||
device.belongs_to__application[0].app_name isnt application.app_name
|
||||
device.application_name isnt application.app_name
|
||||
]
|
||||
.tap (application) ->
|
||||
return balena.models.device.move(params.uuid, application)
|
||||
|
@ -213,6 +213,22 @@ export class BalenaAPIMock {
|
||||
// (Also, nock should automatically throw an error, but also not happening)
|
||||
// For now, the console.error is sufficient (will fail the test)
|
||||
}
|
||||
|
||||
public debug() {
|
||||
const scope = this.scope;
|
||||
let mocks = scope.pendingMocks();
|
||||
console.error(`pending mocks ${mocks.length}: ${mocks}`);
|
||||
|
||||
this.scope.on('request', function(_req, _interceptor, _body) {
|
||||
console.log(`>> REQUEST:` + _req.path);
|
||||
mocks = scope.pendingMocks();
|
||||
console.error(`pending mocks ${mocks.length}: ${mocks}`);
|
||||
});
|
||||
|
||||
this.scope.on('replied', function(_req) {
|
||||
console.log(`<< REPLIED:` + _req.path);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const appServiceVarsByService: { [key: string]: any } = {
|
||||
|
62
tests/commands/device/device-move.spec.ts
Normal file
62
tests/commands/device/device-move.spec.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { expect } from 'chai';
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
|
||||
const HELP_RESPONSE = `
|
||||
Usage: device move <uuid>
|
||||
|
||||
Use this command to move a device to another application you own.
|
||||
|
||||
If you omit the application, you'll get asked for it interactively.
|
||||
|
||||
Examples:
|
||||
|
||||
\t$ balena device move 7cf02a6
|
||||
\t$ balena device move 7cf02a6 --application MyNewApp
|
||||
|
||||
Options:
|
||||
|
||||
--application, -a, --app <application> application name
|
||||
`;
|
||||
|
||||
describe('balena device move', function() {
|
||||
let api: BalenaAPIMock;
|
||||
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Check all expected api calls have been made and clean up.
|
||||
api.done();
|
||||
});
|
||||
|
||||
it('should print help text with the -h flag', async () => {
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
|
||||
const { out, err } = await runCommand('device move -h');
|
||||
|
||||
expect(cleanOutput(out)).to.deep.equal(cleanOutput([HELP_RESPONSE]));
|
||||
|
||||
expect(err).to.eql([]);
|
||||
});
|
||||
|
||||
it.skip('should error if uuid not provided', async () => {
|
||||
// TODO: Figure out how to test for expected errors with current setup
|
||||
// including exit codes if possible.
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
|
||||
const { out, err } = await runCommand('device move');
|
||||
const errLines = cleanOutput(err);
|
||||
|
||||
expect(errLines[0]).to.equal('Missing uuid');
|
||||
expect(out).to.eql([]);
|
||||
});
|
||||
|
||||
// TODO: Test to add once nock matching issues resolved:
|
||||
// - 'should perform device move if application name provided'
|
||||
// - 'should start interactive selection of application name if none provided'
|
||||
// - 'correctly handles devices with missing application'
|
||||
});
|
64
tests/commands/device/device.api-response.json
Normal file
64
tests/commands/device/device.api-response.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"d": [
|
||||
{
|
||||
"belongs_to__application": [
|
||||
{
|
||||
"app_name": "test app",
|
||||
"__metadata": {}
|
||||
}
|
||||
],
|
||||
"id": 1747415,
|
||||
"belongs_to__user": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/user(46272)"
|
||||
},
|
||||
"__id": 46272
|
||||
},
|
||||
"is_managed_by__device": null,
|
||||
"actor": 4180757,
|
||||
"should_be_running__release": null,
|
||||
"device_name": "sparkling-wood",
|
||||
"device_type": "raspberrypi4-64",
|
||||
"uuid": "fda508c8583011b8466c26abdd5159f2",
|
||||
"is_on__commit": "18756d3386c25a044db66b89e0409804",
|
||||
"note": null,
|
||||
"local_id": null,
|
||||
"status": "Idle",
|
||||
"is_online": false,
|
||||
"last_connectivity_event": "2019-11-23T00:26:35.074Z",
|
||||
"is_connected_to_vpn": false,
|
||||
"last_vpn_event": "2019-11-23T00:26:35.074Z",
|
||||
"ip_address": "192.168.0.112",
|
||||
"vpn_address": null,
|
||||
"public_address": "89.186.29.129",
|
||||
"os_version": "balenaOS 2.44.0+rev3",
|
||||
"os_variant": "dev",
|
||||
"supervisor_version": "10.3.7",
|
||||
"should_be_managed_by__supervisor_release": null,
|
||||
"is_managed_by__service_instance": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/service_instance(124111)"
|
||||
},
|
||||
"__id": 124111
|
||||
},
|
||||
"provisioning_progress": null,
|
||||
"provisioning_state": "",
|
||||
"download_progress": null,
|
||||
"is_web_accessible": false,
|
||||
"longitude": "22.5853",
|
||||
"latitude": "51.2712",
|
||||
"location": "Lublin, Lublin, Poland",
|
||||
"custom_longitude": "",
|
||||
"custom_latitude": "",
|
||||
"logs_channel": null,
|
||||
"is_locked_until__date": null,
|
||||
"is_accessible_by_support_until__date": null,
|
||||
"created_at": "2019-11-18T12:27:37.423Z",
|
||||
"is_active": true,
|
||||
"api_heartbeat_state": "offline",
|
||||
"__metadata": {
|
||||
"uri": "/resin/device(@id)?@id=1747415"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
60
tests/commands/device/device.api-response.missing-app.json
Normal file
60
tests/commands/device/device.api-response.missing-app.json
Normal file
@ -0,0 +1,60 @@
|
||||
{
|
||||
"d": [
|
||||
{
|
||||
"belongs_to__application": [
|
||||
],
|
||||
"id": 1747415,
|
||||
"belongs_to__user": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/user(46272)"
|
||||
},
|
||||
"__id": 46272
|
||||
},
|
||||
"is_managed_by__device": null,
|
||||
"actor": 4180757,
|
||||
"should_be_running__release": null,
|
||||
"device_name": "sparkling-wood",
|
||||
"device_type": "raspberrypi4-64",
|
||||
"uuid": "fda508c8583011b8466c26abdd5159f2",
|
||||
"is_on__commit": "18756d3386c25a044db66b89e0409804",
|
||||
"note": null,
|
||||
"local_id": null,
|
||||
"status": "Idle",
|
||||
"is_online": false,
|
||||
"last_connectivity_event": "2019-11-23T00:26:35.074Z",
|
||||
"is_connected_to_vpn": false,
|
||||
"last_vpn_event": "2019-11-23T00:26:35.074Z",
|
||||
"ip_address": "192.168.0.112",
|
||||
"vpn_address": null,
|
||||
"public_address": "89.186.29.129",
|
||||
"os_version": "balenaOS 2.44.0+rev3",
|
||||
"os_variant": "dev",
|
||||
"supervisor_version": "10.3.7",
|
||||
"should_be_managed_by__supervisor_release": null,
|
||||
"is_managed_by__service_instance": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/service_instance(124111)"
|
||||
},
|
||||
"__id": 124111
|
||||
},
|
||||
"provisioning_progress": null,
|
||||
"provisioning_state": "",
|
||||
"download_progress": null,
|
||||
"is_web_accessible": false,
|
||||
"longitude": "22.5853",
|
||||
"latitude": "51.2712",
|
||||
"location": "Lublin, Lublin, Poland",
|
||||
"custom_longitude": "",
|
||||
"custom_latitude": "",
|
||||
"logs_channel": null,
|
||||
"is_locked_until__date": null,
|
||||
"is_accessible_by_support_until__date": null,
|
||||
"created_at": "2019-11-18T12:27:37.423Z",
|
||||
"is_active": true,
|
||||
"api_heartbeat_state": "offline",
|
||||
"__metadata": {
|
||||
"uri": "/resin/device(@id)?@id=1747415"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
94
tests/commands/device/device.spec.ts
Normal file
94
tests/commands/device/device.spec.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { expect } from 'chai';
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
|
||||
const HELP_RESPONSE = `
|
||||
Usage: device <uuid>
|
||||
|
||||
Use this command to show information about a single device.
|
||||
|
||||
Examples:
|
||||
|
||||
\t$ balena device 7cf02a6
|
||||
`;
|
||||
|
||||
describe('balena device', function() {
|
||||
let api: BalenaAPIMock;
|
||||
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Check all expected api calls have been made and clean up.
|
||||
api.done();
|
||||
});
|
||||
|
||||
it('should print help text with the -h flag', async () => {
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
|
||||
const { out, err } = await runCommand('device -h');
|
||||
|
||||
expect(cleanOutput(out)).to.deep.equal(cleanOutput([HELP_RESPONSE]));
|
||||
|
||||
expect(err).to.eql([]);
|
||||
});
|
||||
|
||||
it.skip('should error if uuid not provided', async () => {
|
||||
// TODO: Figure out how to test for expected errors with current setup
|
||||
// including exit codes if possible.
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
|
||||
const { out, err } = await runCommand('device');
|
||||
const errLines = cleanOutput(err);
|
||||
|
||||
expect(errLines[0]).to.equal('Missing uuid');
|
||||
expect(out).to.eql([]);
|
||||
});
|
||||
|
||||
it('should list device details for provided uuid', async () => {
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
|
||||
api.scope
|
||||
.get(/^\/v5\/device/)
|
||||
.replyWithFile(200, __dirname + '/device.api-response.json', {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
const { out, err } = await runCommand('device 27fda508c');
|
||||
|
||||
const lines = cleanOutput(out);
|
||||
|
||||
expect(lines).to.have.lengthOf(13);
|
||||
expect(lines[0]).to.equal('== SPARKLING WOOD');
|
||||
expect(lines[6].split(':')[1].trim()).to.equal('test app');
|
||||
|
||||
expect(err).to.eql([]);
|
||||
});
|
||||
|
||||
it('correctly handles devices with missing application', async () => {
|
||||
// Devices with missing applications will have application name set to `N/a`.
|
||||
// e.g. When user has a device associated with app that user is no longer a collaborator of.
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
|
||||
api.scope
|
||||
.get(/^\/v5\/device/)
|
||||
.replyWithFile(200, __dirname + '/device.api-response.missing-app.json', {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
const { out, err } = await runCommand('device 27fda508c');
|
||||
|
||||
const lines = cleanOutput(out);
|
||||
|
||||
expect(lines).to.have.lengthOf(13);
|
||||
expect(lines[0]).to.equal('== SPARKLING WOOD');
|
||||
expect(lines[6].split(':')[1].trim()).to.equal('N/a');
|
||||
|
||||
expect(err).to.eql([]);
|
||||
});
|
||||
});
|
120
tests/commands/device/devices.api-response.json
Normal file
120
tests/commands/device/devices.api-response.json
Normal file
@ -0,0 +1,120 @@
|
||||
{
|
||||
"d": [
|
||||
{
|
||||
"belongs_to__application": [
|
||||
{
|
||||
"app_name": "test app",
|
||||
"__metadata": {}
|
||||
}
|
||||
],
|
||||
"id": 1747415,
|
||||
"belongs_to__user": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/user(46272)"
|
||||
},
|
||||
"__id": 46272
|
||||
},
|
||||
"is_managed_by__device": null,
|
||||
"actor": 4180757,
|
||||
"should_be_running__release": null,
|
||||
"device_name": "sparkling-wood",
|
||||
"device_type": "raspberrypi4-64",
|
||||
"uuid": "fda508c8583011b8466c26abdd5159f2",
|
||||
"is_on__commit": "18756d3386c25a044db66b89e0409804",
|
||||
"note": null,
|
||||
"local_id": null,
|
||||
"status": "Idle",
|
||||
"is_online": false,
|
||||
"last_connectivity_event": "2019-11-23T00:26:35.074Z",
|
||||
"is_connected_to_vpn": false,
|
||||
"last_vpn_event": "2019-11-23T00:26:35.074Z",
|
||||
"ip_address": "192.168.0.112",
|
||||
"vpn_address": null,
|
||||
"public_address": "89.186.29.129",
|
||||
"os_version": "balenaOS 2.44.0+rev3",
|
||||
"os_variant": "dev",
|
||||
"supervisor_version": "10.3.7",
|
||||
"should_be_managed_by__supervisor_release": null,
|
||||
"is_managed_by__service_instance": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/service_instance(124111)"
|
||||
},
|
||||
"__id": 124111
|
||||
},
|
||||
"provisioning_progress": null,
|
||||
"provisioning_state": "",
|
||||
"download_progress": null,
|
||||
"is_web_accessible": false,
|
||||
"longitude": "22.5853",
|
||||
"latitude": "51.2712",
|
||||
"location": "Lublin, Lublin, Poland",
|
||||
"custom_longitude": "",
|
||||
"custom_latitude": "",
|
||||
"logs_channel": null,
|
||||
"is_locked_until__date": null,
|
||||
"is_accessible_by_support_until__date": null,
|
||||
"created_at": "2019-11-18T12:27:37.423Z",
|
||||
"is_active": true,
|
||||
"api_heartbeat_state": "offline",
|
||||
"__metadata": {
|
||||
"uri": "/resin/device(@id)?@id=1747415"
|
||||
}
|
||||
},
|
||||
{
|
||||
"belongs_to__application": [
|
||||
],
|
||||
"id": 1747416,
|
||||
"belongs_to__user": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/user(46272)"
|
||||
},
|
||||
"__id": 46272
|
||||
},
|
||||
"is_managed_by__device": null,
|
||||
"actor": 4180757,
|
||||
"should_be_running__release": null,
|
||||
"device_name": "dashing-spruce",
|
||||
"device_type": "raspberrypi4-64",
|
||||
"uuid": "fda508c8583011b8466c26abdd5159f3",
|
||||
"is_on__commit": "18756d3386c25a044db66b89e0409804",
|
||||
"note": null,
|
||||
"local_id": null,
|
||||
"status": "Idle",
|
||||
"is_online": false,
|
||||
"last_connectivity_event": "2019-11-23T00:26:35.074Z",
|
||||
"is_connected_to_vpn": false,
|
||||
"last_vpn_event": "2019-11-23T00:26:35.074Z",
|
||||
"ip_address": "192.168.0.112",
|
||||
"vpn_address": null,
|
||||
"public_address": "89.186.29.129",
|
||||
"os_version": "balenaOS 2.44.0+rev3",
|
||||
"os_variant": "dev",
|
||||
"supervisor_version": "10.3.7",
|
||||
"should_be_managed_by__supervisor_release": null,
|
||||
"is_managed_by__service_instance": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/service_instance(124111)"
|
||||
},
|
||||
"__id": 124111
|
||||
},
|
||||
"provisioning_progress": null,
|
||||
"provisioning_state": "",
|
||||
"download_progress": null,
|
||||
"is_web_accessible": false,
|
||||
"longitude": "22.5853",
|
||||
"latitude": "51.2712",
|
||||
"location": "Lublin, Lublin, Poland",
|
||||
"custom_longitude": "",
|
||||
"custom_latitude": "",
|
||||
"logs_channel": null,
|
||||
"is_locked_until__date": null,
|
||||
"is_accessible_by_support_until__date": null,
|
||||
"created_at": "2019-11-18T12:27:37.423Z",
|
||||
"is_active": true,
|
||||
"api_heartbeat_state": "offline",
|
||||
"__metadata": {
|
||||
"uri": "/resin/device(@id)?@id=1747415"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
77
tests/commands/device/devices.spec.ts
Normal file
77
tests/commands/device/devices.spec.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { expect } from 'chai';
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
|
||||
const HELP_RESPONSE = `
|
||||
Usage: devices
|
||||
|
||||
Use this command to list all devices that belong to you.
|
||||
|
||||
You can filter the devices by application by using the \`--application\` option.
|
||||
|
||||
Examples:
|
||||
|
||||
\t$ balena devices
|
||||
\t$ balena devices --application MyApp
|
||||
\t$ balena devices --app MyApp
|
||||
\t$ balena devices -a MyApp
|
||||
|
||||
Options:
|
||||
|
||||
--application, -a, --app <application> application name
|
||||
`;
|
||||
|
||||
describe('balena devices', function() {
|
||||
let api: BalenaAPIMock;
|
||||
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Check all expected api calls have been made and clean up.
|
||||
api.done();
|
||||
});
|
||||
|
||||
it('should print help text with the -h flag', async () => {
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
|
||||
const { out, err } = await runCommand('devices -h');
|
||||
|
||||
expect(cleanOutput(out)).to.deep.equal(cleanOutput([HELP_RESPONSE]));
|
||||
|
||||
expect(err).to.eql([]);
|
||||
});
|
||||
|
||||
it('should list devices from own and collaborator apps', async () => {
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
|
||||
api.scope
|
||||
.get(
|
||||
'/v5/device?$orderby=device_name%20asc&$expand=belongs_to__application($select=app_name)',
|
||||
)
|
||||
.replyWithFile(200, __dirname + '/devices.api-response.json', {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
const { out, err } = await runCommand('devices');
|
||||
|
||||
const lines = cleanOutput(out);
|
||||
|
||||
expect(lines[0].replace(/ +/g, ' ')).to.equal(
|
||||
'ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS ' +
|
||||
'IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL',
|
||||
);
|
||||
expect(lines).to.have.lengthOf.at.least(2);
|
||||
|
||||
expect(lines.some(l => l.includes('test app'))).to.be.true;
|
||||
|
||||
// Devices with missing applications will have application name set to `N/a`.
|
||||
// e.g. When user has a device associated with app that user is no longer a collaborator of.
|
||||
expect(lines.some(l => l.includes('N/a'))).to.be.true;
|
||||
|
||||
expect(err).to.eql([]);
|
||||
});
|
||||
});
|
@ -39,9 +39,10 @@ describe('balena devices supported', function() {
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
|
||||
// TODO: Using the alias api.expect here causes route /config/vars to be called unexpectedly - why?
|
||||
api.scope
|
||||
.get('/device-types/v1')
|
||||
.replyWithFile(200, __dirname + '/supported.api-response.json', {
|
||||
.replyWithFile(200, __dirname + '/device-types.api-response.json', {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
|
@ -24,7 +24,9 @@ import * as balenaCLI from '../build/app';
|
||||
import { configureBluebird, setMaxListeners } from '../build/app-common';
|
||||
|
||||
configureBluebird();
|
||||
setMaxListeners(25); // it appears that 'nock' adds a bunch of listeners - bug?
|
||||
setMaxListeners(35); // it appears that 'nock' adds a bunch of listeners - bug?
|
||||
// SL: Looks like it's not nock causing this, as have seen the problem triggered from help.spec,
|
||||
// which is not using nock. Perhaps mocha/chai? (unlikely), or something in the CLI?
|
||||
|
||||
export const runCommand = async (cmd: string) => {
|
||||
const preArgs = [process.argv[0], path.join(process.cwd(), 'bin', 'balena')];
|
||||
|
Loading…
x
Reference in New Issue
Block a user