mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-07 03:40:16 +00:00
The host-config module exposes the following interfaces: get, patch, and parse. `get` gets host configuration such as redsocks proxy configuration and hostname and returns it in an object of type HostConfiguration. `patch` takes an object of type HostConfiguration or LegacyHostConfiguration and updates the hostname and redsocks proxy configuration, optionally forcing the patch through update locks. `parse` takes a user input of unknown type and parses it into type HostConfiguration or LegacyHostConfiguration for patching, erroring if parse was unsuccessful. LegacyHostConfiguration is a looser typing of the user input which does not validate values of the five known proxy fields of type, ip, port, username, and password. We should stop supporting it in the next major Supervisor API release. Change-type: minor Signed-off-by: Christina Ying Wang <christina@balena.io>
857 lines
26 KiB
TypeScript
857 lines
26 KiB
TypeScript
import { expect } from 'chai';
|
|
import type * as express from 'express';
|
|
import type { SinonStub } from 'sinon';
|
|
import { stub } from 'sinon';
|
|
import request from 'supertest';
|
|
|
|
import * as config from '~/src/config';
|
|
import * as db from '~/src/db';
|
|
import * as hostConfig from '~/src/host-config';
|
|
import type { Service } from '~/src/compose/service';
|
|
import * as apiKeys from '~/lib/api-keys';
|
|
import * as deviceApi from '~/src/device-api';
|
|
import * as actions from '~/src/device-api/actions';
|
|
import * as v1 from '~/src/device-api/v1';
|
|
import {
|
|
UpdatesLockedError,
|
|
NotFoundError,
|
|
BadRequestError,
|
|
} from '~/lib/errors';
|
|
import log from '~/lib/supervisor-console';
|
|
|
|
// All routes that require Authorization are integration tests due to
|
|
// the api-key module relying on the database.
|
|
describe('device-api/v1', () => {
|
|
let api: express.Application;
|
|
|
|
before(async () => {
|
|
await config.initialized();
|
|
|
|
// `api` is a private property on SupervisorAPI but
|
|
// passing it directly to supertest is easier than
|
|
// setting up an API listen port & timeout
|
|
api = new deviceApi.SupervisorAPI({
|
|
routers: [v1.router],
|
|
healthchecks: [],
|
|
// @ts-expect-error extract private variable for testing
|
|
}).api;
|
|
});
|
|
|
|
describe('GET /v1/healthy', () => {
|
|
after(() => {
|
|
api = new deviceApi.SupervisorAPI({
|
|
routers: [v1.router],
|
|
healthchecks: [],
|
|
// @ts-expect-error extract private variable for testing
|
|
}).api;
|
|
});
|
|
|
|
it('responds with 200 because all healthchecks pass', async () => {
|
|
api = new deviceApi.SupervisorAPI({
|
|
routers: [v1.router],
|
|
healthchecks: [stub().resolves(true), stub().resolves(true)],
|
|
// @ts-expect-error extract private variable for testing
|
|
}).api;
|
|
await request(api).get('/v1/healthy').expect(200);
|
|
});
|
|
|
|
it('responds with 500 because some healthchecks did not pass', async () => {
|
|
api = new deviceApi.SupervisorAPI({
|
|
routers: [v1.router],
|
|
healthchecks: [stub().resolves(false), stub().resolves(true)],
|
|
// @ts-expect-error extract private variable for testing
|
|
}).api;
|
|
await request(api).get('/v1/healthy').expect(500);
|
|
});
|
|
});
|
|
|
|
describe('POST /v1/blink', () => {
|
|
// Actions are tested elsewhere so we can stub the dependency here
|
|
before(() => stub(actions, 'identify'));
|
|
after(() => (actions.identify as SinonStub).restore());
|
|
|
|
it('responds with 200', async () => {
|
|
await request(api)
|
|
.post('/v1/blink')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200);
|
|
});
|
|
});
|
|
|
|
describe('POST /v1/regenerate-api-key', () => {
|
|
// Actions are tested elsewhere so we can stub the dependency here
|
|
beforeEach(() => stub(actions, 'regenerateKey'));
|
|
afterEach(() => (actions.regenerateKey as SinonStub).restore());
|
|
|
|
it('responds with 200 and valid new API key', async () => {
|
|
const oldKey = await apiKeys.getGlobalApiKey();
|
|
const newKey = 'my_new_key';
|
|
(actions.regenerateKey as SinonStub).resolves(newKey);
|
|
|
|
await request(api)
|
|
.post('/v1/regenerate-api-key')
|
|
.set('Authorization', `Bearer ${oldKey}`)
|
|
.expect(200)
|
|
.then((response) => {
|
|
expect(response.text).to.match(new RegExp(newKey));
|
|
});
|
|
});
|
|
|
|
it('responds with 503 if regenerate was unsuccessful', async () => {
|
|
const oldKey = await apiKeys.getGlobalApiKey();
|
|
(actions.regenerateKey as SinonStub).throws(new Error());
|
|
|
|
await request(api)
|
|
.post('/v1/regenerate-api-key')
|
|
.set('Authorization', `Bearer ${oldKey}`)
|
|
.expect(503);
|
|
});
|
|
});
|
|
|
|
describe('POST /v1/restart', () => {
|
|
// Actions are tested elsewhere so we can stub the dependency here
|
|
let doRestartStub: SinonStub;
|
|
beforeEach(() => {
|
|
doRestartStub = stub(actions, 'doRestart').resolves();
|
|
});
|
|
afterEach(async () => {
|
|
doRestartStub.restore();
|
|
// Remove all scoped API keys between tests
|
|
await db.models('apiSecret').whereNot({ appId: 0 }).del();
|
|
});
|
|
|
|
it('validates data from request body', async () => {
|
|
// Parses force: false
|
|
await request(api)
|
|
.post('/v1/restart')
|
|
.send({ appId: 1234567, force: false })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200);
|
|
expect(doRestartStub).to.have.been.calledWith(1234567, false);
|
|
doRestartStub.resetHistory();
|
|
|
|
// Parses force: true
|
|
await request(api)
|
|
.post('/v1/restart')
|
|
.send({ appId: 7654321, force: true })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200);
|
|
expect(doRestartStub).to.have.been.calledWith(7654321, true);
|
|
doRestartStub.resetHistory();
|
|
|
|
// Defaults to force: false
|
|
await request(api)
|
|
.post('/v1/restart')
|
|
.send({ appId: 7654321 })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200);
|
|
expect(doRestartStub).to.have.been.calledWith(7654321, false);
|
|
});
|
|
|
|
it('responds with 400 if appId is missing', async () => {
|
|
await request(api)
|
|
.post('/v1/restart')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(400);
|
|
});
|
|
|
|
it("responds with 401 if caller's API key is not in scope of appId", async () => {
|
|
const scopedKey = await apiKeys.generateScopedKey(1234567, 'main');
|
|
await request(api)
|
|
.post('/v1/restart')
|
|
.send({ appId: 7654321 })
|
|
.set('Authorization', `Bearer ${scopedKey}`)
|
|
.expect(401);
|
|
});
|
|
|
|
it('responds with 200 if restart succeeded', async () => {
|
|
await request(api)
|
|
.post('/v1/restart')
|
|
.send({ appId: 1234567 })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200);
|
|
});
|
|
|
|
it('responds with 423 if there are update locks', async () => {
|
|
doRestartStub.throws(new UpdatesLockedError());
|
|
await request(api)
|
|
.post('/v1/restart')
|
|
.send({ appId: 1234567 })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(423);
|
|
});
|
|
|
|
it('responds with 503 for other errors that occur during restart', async () => {
|
|
doRestartStub.throws(new Error());
|
|
await request(api)
|
|
.post('/v1/restart')
|
|
.send({ appId: 1234567 })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(503);
|
|
});
|
|
});
|
|
|
|
describe('POST /v1/purge', () => {
|
|
let doPurgeStub: SinonStub;
|
|
beforeEach(() => {
|
|
doPurgeStub = stub(actions, 'doPurge').resolves();
|
|
});
|
|
afterEach(async () => {
|
|
doPurgeStub.restore();
|
|
// Remove all scoped API keys between tests
|
|
await db.models('apiSecret').whereNot({ appId: 0 }).del();
|
|
});
|
|
|
|
it('validates data from request body', async () => {
|
|
// Parses force: false
|
|
await request(api)
|
|
.post('/v1/purge')
|
|
.send({ appId: 1234567, force: false })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200);
|
|
expect(doPurgeStub).to.have.been.calledWith(1234567, false);
|
|
doPurgeStub.resetHistory();
|
|
|
|
// Parses force: true
|
|
await request(api)
|
|
.post('/v1/purge')
|
|
.send({ appId: 7654321, force: true })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200);
|
|
expect(doPurgeStub).to.have.been.calledWith(7654321, true);
|
|
doPurgeStub.resetHistory();
|
|
|
|
// Defaults to force: false
|
|
await request(api)
|
|
.post('/v1/purge')
|
|
.send({ appId: 7654321 })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200);
|
|
expect(doPurgeStub).to.have.been.calledWith(7654321, false);
|
|
});
|
|
|
|
it('responds with 400 if appId is missing', async () => {
|
|
await request(api)
|
|
.post('/v1/purge')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(400);
|
|
});
|
|
|
|
it("responds with 401 if caller's API key is not in scope of appId", async () => {
|
|
const scopedKey = await apiKeys.generateScopedKey(1234567, 'main');
|
|
await request(api)
|
|
.post('/v1/purge')
|
|
.send({ appId: 7654321 })
|
|
.set('Authorization', `Bearer ${scopedKey}`)
|
|
.expect(401);
|
|
});
|
|
|
|
it('responds with 200 if purge succeeded', async () => {
|
|
await request(api)
|
|
.post('/v1/purge')
|
|
.send({ appId: 1234567 })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200);
|
|
});
|
|
|
|
it('responds with 423 if there are update locks', async () => {
|
|
doPurgeStub.throws(new UpdatesLockedError());
|
|
await request(api)
|
|
.post('/v1/purge')
|
|
.send({ appId: 1234567 })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(423);
|
|
});
|
|
|
|
it('responds with 503 for other errors that occur during purge', async () => {
|
|
doPurgeStub.throws(new Error());
|
|
await request(api)
|
|
.post('/v1/purge')
|
|
.send({ appId: 1234567 })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(503);
|
|
});
|
|
});
|
|
|
|
describe('POST /v1/apps/:appId/stop', () => {
|
|
let executeServiceActionStub: SinonStub;
|
|
let getLegacyServiceStub: SinonStub;
|
|
beforeEach(() => {
|
|
executeServiceActionStub = stub(
|
|
actions,
|
|
'executeServiceAction',
|
|
).resolves();
|
|
getLegacyServiceStub = stub(actions, 'getLegacyService').resolves({
|
|
containerId: 'abcdef',
|
|
} as Service);
|
|
});
|
|
afterEach(async () => {
|
|
executeServiceActionStub.restore();
|
|
getLegacyServiceStub.restore();
|
|
// Remove all scoped API keys between tests
|
|
await db.models('apiSecret').whereNot({ appId: 0 }).del();
|
|
});
|
|
|
|
it('validates data from request body', async () => {
|
|
// Parses force: false
|
|
await request(api)
|
|
.post('/v1/apps/1234567/stop')
|
|
.send({ force: false })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200);
|
|
expect(executeServiceActionStub).to.have.been.calledWith({
|
|
action: 'stop',
|
|
appId: 1234567,
|
|
force: false,
|
|
isLegacy: true,
|
|
});
|
|
executeServiceActionStub.resetHistory();
|
|
|
|
// Parses force: true
|
|
await request(api)
|
|
.post('/v1/apps/7654321/stop')
|
|
.send({ force: true })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200);
|
|
expect(executeServiceActionStub).to.have.been.calledWith({
|
|
action: 'stop',
|
|
appId: 7654321,
|
|
force: true,
|
|
isLegacy: true,
|
|
});
|
|
executeServiceActionStub.resetHistory();
|
|
|
|
// Defaults to force: false
|
|
await request(api)
|
|
.post('/v1/apps/7654321/stop')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200);
|
|
expect(executeServiceActionStub).to.have.been.calledWith({
|
|
action: 'stop',
|
|
appId: 7654321,
|
|
force: false,
|
|
isLegacy: true,
|
|
});
|
|
});
|
|
|
|
it("responds with 401 if caller's API key is not in scope of appId", async () => {
|
|
const scopedKey = await apiKeys.generateScopedKey(1234567, 'main');
|
|
await request(api)
|
|
.post('/v1/apps/7654321/stop')
|
|
.set('Authorization', `Bearer ${scopedKey}`)
|
|
.expect(401);
|
|
});
|
|
|
|
it('responds with 200 and containerId if service stop succeeded if service stop succeeded', async () => {
|
|
await request(api)
|
|
.post('/v1/apps/1234567/stop')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200, { containerId: 'abcdef' });
|
|
});
|
|
|
|
it('responds with 404 if app or service not found', async () => {
|
|
executeServiceActionStub.throws(new NotFoundError());
|
|
await request(api)
|
|
.post('/v1/apps/1234567/stop')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(404);
|
|
});
|
|
|
|
it('responds with 400 if invalid appId or appId corresponds to a multicontainer release', async () => {
|
|
await request(api)
|
|
.post('/v1/apps/badAppId/stop')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(400);
|
|
|
|
executeServiceActionStub.throws(new BadRequestError());
|
|
await request(api)
|
|
.post('/v1/apps/1234567/stop')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(400);
|
|
});
|
|
|
|
it('responds with 423 if there are update locks', async () => {
|
|
executeServiceActionStub.throws(new UpdatesLockedError());
|
|
await request(api)
|
|
.post('/v1/apps/1234567/stop')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(423);
|
|
});
|
|
|
|
it('responds with 503 for other errors that occur during service stop', async () => {
|
|
executeServiceActionStub.throws(new Error());
|
|
await request(api)
|
|
.post('/v1/apps/1234567/stop')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(503);
|
|
});
|
|
});
|
|
|
|
describe('POST /v1/apps/:appId/start', () => {
|
|
let executeServiceActionStub: SinonStub;
|
|
let getLegacyServiceStub: SinonStub;
|
|
beforeEach(() => {
|
|
executeServiceActionStub = stub(
|
|
actions,
|
|
'executeServiceAction',
|
|
).resolves();
|
|
getLegacyServiceStub = stub(actions, 'getLegacyService').resolves({
|
|
containerId: 'abcdef',
|
|
} as Service);
|
|
});
|
|
afterEach(async () => {
|
|
executeServiceActionStub.restore();
|
|
getLegacyServiceStub.restore();
|
|
// Remove all scoped API keys between tests
|
|
await db.models('apiSecret').whereNot({ appId: 0 }).del();
|
|
});
|
|
|
|
it('validates data from request body', async () => {
|
|
// Parses force: false
|
|
await request(api)
|
|
.post('/v1/apps/1234567/start')
|
|
.send({ force: false })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200);
|
|
expect(executeServiceActionStub).to.have.been.calledWith({
|
|
action: 'start',
|
|
appId: 1234567,
|
|
force: false,
|
|
isLegacy: true,
|
|
});
|
|
executeServiceActionStub.resetHistory();
|
|
|
|
// Parses force: true
|
|
await request(api)
|
|
.post('/v1/apps/7654321/start')
|
|
.send({ force: true })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200);
|
|
expect(executeServiceActionStub).to.have.been.calledWith({
|
|
action: 'start',
|
|
appId: 7654321,
|
|
force: true,
|
|
isLegacy: true,
|
|
});
|
|
executeServiceActionStub.resetHistory();
|
|
|
|
// Defaults to force: false
|
|
await request(api)
|
|
.post('/v1/apps/7654321/start')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200);
|
|
expect(executeServiceActionStub).to.have.been.calledWith({
|
|
action: 'start',
|
|
appId: 7654321,
|
|
force: false,
|
|
isLegacy: true,
|
|
});
|
|
});
|
|
|
|
it("responds with 401 if caller's API key is not in scope of appId", async () => {
|
|
const scopedKey = await apiKeys.generateScopedKey(1234567, 'main');
|
|
await request(api)
|
|
.post('/v1/apps/7654321/start')
|
|
.set('Authorization', `Bearer ${scopedKey}`)
|
|
.expect(401);
|
|
});
|
|
|
|
it('responds with 200 and containerId if service start succeeded', async () => {
|
|
await request(api)
|
|
.post('/v1/apps/1234567/start')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200, { containerId: 'abcdef' });
|
|
});
|
|
|
|
it('responds with 404 if app or service not found', async () => {
|
|
executeServiceActionStub.throws(new NotFoundError());
|
|
await request(api)
|
|
.post('/v1/apps/1234567/start')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(404);
|
|
});
|
|
|
|
it('responds with 400 if invalid appId or appId corresponds to a multicontainer release', async () => {
|
|
await request(api)
|
|
.post('/v1/apps/badAppId/start')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(400);
|
|
|
|
executeServiceActionStub.throws(new BadRequestError());
|
|
await request(api)
|
|
.post('/v1/apps/1234567/start')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(400);
|
|
});
|
|
|
|
it('responds with 423 if there are update locks', async () => {
|
|
executeServiceActionStub.throws(new UpdatesLockedError());
|
|
await request(api)
|
|
.post('/v1/apps/1234567/start')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(423);
|
|
});
|
|
|
|
it('responds with 503 for other errors that occur during service start', async () => {
|
|
executeServiceActionStub.throws(new Error());
|
|
await request(api)
|
|
.post('/v1/apps/1234567/start')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(503);
|
|
});
|
|
});
|
|
|
|
describe('POST /v1/reboot', () => {
|
|
let executeDeviceActionStub: SinonStub;
|
|
beforeEach(() => {
|
|
executeDeviceActionStub = stub(actions, 'executeDeviceAction').resolves();
|
|
});
|
|
afterEach(async () => executeDeviceActionStub.restore());
|
|
|
|
it('validates data from request body', async () => {
|
|
// Parses force: false
|
|
await request(api)
|
|
.post('/v1/reboot')
|
|
.send({ force: false })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
|
|
expect(executeDeviceActionStub).to.have.been.calledWith(
|
|
{
|
|
action: 'reboot',
|
|
},
|
|
false,
|
|
);
|
|
executeDeviceActionStub.resetHistory();
|
|
|
|
// Parses force: true
|
|
await request(api)
|
|
.post('/v1/reboot')
|
|
.send({ force: true })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
|
|
expect(executeDeviceActionStub).to.have.been.calledWith(
|
|
{
|
|
action: 'reboot',
|
|
},
|
|
true,
|
|
);
|
|
executeDeviceActionStub.resetHistory();
|
|
|
|
// Defaults to force: false
|
|
await request(api)
|
|
.post('/v1/reboot')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
|
|
expect(executeDeviceActionStub).to.have.been.calledWith(
|
|
{
|
|
action: 'reboot',
|
|
},
|
|
false,
|
|
);
|
|
});
|
|
|
|
it('responds with 202 if request successful', async () => {
|
|
await request(api)
|
|
.post('/v1/reboot')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(202);
|
|
});
|
|
|
|
it('responds with 423 if there are update locks', async () => {
|
|
executeDeviceActionStub.throws(new UpdatesLockedError());
|
|
await request(api)
|
|
.post('/v1/reboot')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(423);
|
|
});
|
|
|
|
it('responds with 500 for other errors that occur during reboot', async () => {
|
|
executeDeviceActionStub.throws(new Error());
|
|
await request(api)
|
|
.post('/v1/reboot')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(500);
|
|
});
|
|
});
|
|
|
|
describe('POST /v1/shutdown', () => {
|
|
let executeDeviceActionStub: SinonStub;
|
|
beforeEach(() => {
|
|
executeDeviceActionStub = stub(actions, 'executeDeviceAction').resolves();
|
|
});
|
|
afterEach(async () => executeDeviceActionStub.restore());
|
|
|
|
it('validates data from request body', async () => {
|
|
// Parses force: false
|
|
await request(api)
|
|
.post('/v1/shutdown')
|
|
.send({ force: false })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
|
|
expect(executeDeviceActionStub).to.have.been.calledWith(
|
|
{
|
|
action: 'shutdown',
|
|
},
|
|
false,
|
|
);
|
|
executeDeviceActionStub.resetHistory();
|
|
|
|
// Parses force: true
|
|
await request(api)
|
|
.post('/v1/shutdown')
|
|
.send({ force: true })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
|
|
expect(executeDeviceActionStub).to.have.been.calledWith(
|
|
{
|
|
action: 'shutdown',
|
|
},
|
|
true,
|
|
);
|
|
executeDeviceActionStub.resetHistory();
|
|
|
|
// Defaults to force: false
|
|
await request(api)
|
|
.post('/v1/shutdown')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
|
|
expect(executeDeviceActionStub).to.have.been.calledWith(
|
|
{
|
|
action: 'shutdown',
|
|
},
|
|
false,
|
|
);
|
|
});
|
|
|
|
it('responds with 202 if request successful', async () => {
|
|
await request(api)
|
|
.post('/v1/shutdown')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(202);
|
|
});
|
|
|
|
it('responds with 423 if there are update locks', async () => {
|
|
executeDeviceActionStub.throws(new UpdatesLockedError());
|
|
await request(api)
|
|
.post('/v1/shutdown')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(423);
|
|
});
|
|
|
|
it('responds with 500 for other errors that occur during shutdown', async () => {
|
|
executeDeviceActionStub.throws(new Error());
|
|
await request(api)
|
|
.post('/v1/shutdown')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(500);
|
|
});
|
|
});
|
|
|
|
describe('POST /v1/update', () => {
|
|
let updateTargetStub: SinonStub;
|
|
beforeEach(() => {
|
|
updateTargetStub = stub(actions, 'updateTarget');
|
|
});
|
|
afterEach(async () => updateTargetStub.restore());
|
|
|
|
it('validates data from request body', async () => {
|
|
// Parses force: false
|
|
await request(api)
|
|
.post('/v1/update')
|
|
.send({ force: false })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
|
|
expect(updateTargetStub.lastCall.firstArg).to.be.false;
|
|
updateTargetStub.resetHistory();
|
|
|
|
// Parses force: true
|
|
await request(api)
|
|
.post('/v1/update')
|
|
.send({ force: true })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
|
|
expect(updateTargetStub.lastCall.firstArg).to.be.true;
|
|
updateTargetStub.resetHistory();
|
|
|
|
// Defaults to force: false
|
|
await request(api)
|
|
.post('/v1/update')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
|
|
expect(updateTargetStub.lastCall.firstArg).to.be.false;
|
|
});
|
|
|
|
it('responds with 204 if update triggered', async () => {
|
|
updateTargetStub.returns(true);
|
|
await request(api)
|
|
.post('/v1/update')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(204);
|
|
});
|
|
|
|
it('responds with 202 if update not triggered', async () => {
|
|
updateTargetStub.returns(false);
|
|
await request(api)
|
|
.post('/v1/update')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(202);
|
|
});
|
|
});
|
|
|
|
describe('GET /v1/apps/:appId', () => {
|
|
let getSingleContainerAppStub: SinonStub;
|
|
beforeEach(() => {
|
|
getSingleContainerAppStub = stub(
|
|
actions,
|
|
'getSingleContainerApp',
|
|
).resolves({} as any);
|
|
});
|
|
afterEach(async () => {
|
|
getSingleContainerAppStub.restore();
|
|
// Remove all scoped API keys between tests
|
|
await db.models('apiSecret').whereNot({ appId: 0 }).del();
|
|
});
|
|
|
|
it('validates data from request body', async () => {
|
|
await request(api)
|
|
.get('/v1/apps/1234567')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
|
|
expect(getSingleContainerAppStub).to.have.been.calledWith(1234567);
|
|
});
|
|
|
|
it('responds with 200 if request successful', async () => {
|
|
await request(api)
|
|
.get('/v1/apps/1234567')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200, {});
|
|
});
|
|
|
|
it('responds with 400 if invalid appId parameter', async () => {
|
|
await request(api)
|
|
.get('/v1/apps/badAppId')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(400);
|
|
});
|
|
|
|
it('responds with 400 if action throws BadRequestError', async () => {
|
|
getSingleContainerAppStub.throws(new BadRequestError());
|
|
await request(api)
|
|
.get('/v1/apps/1234567')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(400);
|
|
});
|
|
|
|
it("responds with 401 if caller's API key is not in scope of appId", async () => {
|
|
const scopedKey = await apiKeys.generateScopedKey(7654321, 'main');
|
|
await request(api)
|
|
.get('/v1/apps/1234567')
|
|
.set('Authorization', `Bearer ${scopedKey}`)
|
|
.expect(401);
|
|
});
|
|
|
|
it('responds with 503 for other errors that occur during request', async () => {
|
|
getSingleContainerAppStub.throws(new Error());
|
|
await request(api)
|
|
.get('/v1/apps/1234567')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(503);
|
|
});
|
|
});
|
|
|
|
describe('GET /v1/device', () => {
|
|
let getLegacyDeviceStateStub: SinonStub;
|
|
beforeEach(() => {
|
|
getLegacyDeviceStateStub = stub(actions, 'getLegacyDeviceState');
|
|
});
|
|
afterEach(() => getLegacyDeviceStateStub.restore());
|
|
|
|
it('responds with 200 and legacy device state', async () => {
|
|
getLegacyDeviceStateStub.resolves({ test_state: 'Success' });
|
|
await request(api)
|
|
.get('/v1/device')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200, { test_state: 'Success' });
|
|
});
|
|
|
|
it('responds with 503 for other errors that occur during request', async () => {
|
|
getLegacyDeviceStateStub.throws(new Error());
|
|
await request(api)
|
|
.get('/v1/device')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(503);
|
|
});
|
|
});
|
|
|
|
describe('GET /v1/device/host-config', () => {
|
|
// Stub external dependencies
|
|
let getHostConfigStub: SinonStub;
|
|
beforeEach(() => {
|
|
getHostConfigStub = stub(hostConfig, 'get');
|
|
});
|
|
afterEach(() => {
|
|
getHostConfigStub.restore();
|
|
});
|
|
|
|
it('responds with 200 and host config', async () => {
|
|
getHostConfigStub.resolves({ network: { hostname: 'deadbeef' } });
|
|
await request(api)
|
|
.get('/v1/device/host-config')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200, { network: { hostname: 'deadbeef' } });
|
|
});
|
|
|
|
it('responds with 503 for other errors that occur during request', async () => {
|
|
getHostConfigStub.throws(new Error());
|
|
await request(api)
|
|
.get('/v1/device/host-config')
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(503);
|
|
});
|
|
});
|
|
|
|
describe('PATCH /v1/device/host-config', () => {
|
|
before(() => stub(actions, 'patchHostConfig'));
|
|
after(() => (actions.patchHostConfig as SinonStub).restore());
|
|
|
|
it('warns on console when sent a malformed patch body', async () => {
|
|
await request(api)
|
|
.patch('/v1/device/host-config')
|
|
.send({})
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200)
|
|
.then(() => {
|
|
expect(log.warn as SinonStub).to.have.been.calledWith(
|
|
"Key 'network' must exist in PATCH body",
|
|
);
|
|
});
|
|
});
|
|
|
|
it('responds with 200 if patch successful', async () => {
|
|
(actions.patchHostConfig as SinonStub).resolves();
|
|
await request(api)
|
|
.patch('/v1/device/host-config')
|
|
.send({ network: { hostname: 'deadbeef' } })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(200);
|
|
});
|
|
|
|
it('responds with 423 for update lock errors', async () => {
|
|
(actions.patchHostConfig as SinonStub).throws(new UpdatesLockedError());
|
|
await request(api)
|
|
.patch('/v1/device/host-config')
|
|
.send({ network: { hostname: 'deadbeef' } })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(423);
|
|
});
|
|
|
|
it('responds with 400 for BadRequestErrors', async () => {
|
|
(actions.patchHostConfig as SinonStub).throws(new BadRequestError());
|
|
await request(api)
|
|
.patch('/v1/device/host-config')
|
|
.send({ network: { hostname: 'deadbeef' } })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(400);
|
|
});
|
|
|
|
it('responds with 503 for other errors that occur during patch', async () => {
|
|
(actions.patchHostConfig as SinonStub).throws(new Error());
|
|
await request(api)
|
|
.patch('/v1/device/host-config')
|
|
.send({ network: { hostname: 'deadbeef' } })
|
|
.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
|
|
.expect(503);
|
|
});
|
|
});
|
|
});
|