balena-supervisor/test/21-supervisor-api.spec.ts
Rich Bayliss 1b91ef3405
state: Report device MAC address to the API
When reporting device information, send the MAC address of any
interfaces on the system. Also expose in the Supervisor API at
the route GET /v1/device.

Change-type: patch
Signed-off-by: Rich Bayliss <rich@balena.io>
2020-06-22 10:41:06 +01:00

269 lines
7.5 KiB
TypeScript

import { expect } from 'chai';
import { spy, stub, SinonStub } from 'sinon';
import * as supertest from 'supertest';
import APIBinder from '../src/api-binder';
import DeviceState from '../src/device-state';
import Log from '../src/lib/supervisor-console';
import * as images from '../src/compose/images';
import SupervisorAPI from '../src/supervisor-api';
import sampleResponses = require('./data/device-api-responses.json');
import mockedAPI = require('./lib/mocked-device-api');
const mockedOptions = {
listenPort: 54321,
timeout: 30000,
};
const VALID_SECRET = mockedAPI.STUBBED_VALUES.config.apiSecret;
const ALLOWED_INTERFACES = ['lo']; // Only need loopback since this is for testing
describe('SupervisorAPI', () => {
let api: SupervisorAPI;
let healthCheckStubs: SinonStub[];
const request = supertest(`http://127.0.0.1:${mockedOptions.listenPort}`);
const originalGetStatus = images.getStatus;
before(async () => {
// Stub health checks so we can modify them whenever needed
healthCheckStubs = [
stub(APIBinder.prototype, 'healthcheck'),
stub(DeviceState.prototype, 'healthcheck'),
];
// The mockedAPI contains stubs that might create unexpected results
// See the module to know what has been stubbed
api = await mockedAPI.create();
// @ts-expect-error assigning to a RO property
images.getStatus = () => Promise.resolve([]);
// Start test API
return api.listen(
ALLOWED_INTERFACES,
mockedOptions.listenPort,
mockedOptions.timeout,
);
});
after(async () => {
try {
await api.stop();
} catch (e) {
if (e.message !== 'Server is not running.') {
throw e;
}
}
// Restore healthcheck stubs
healthCheckStubs.forEach((hc) => hc.restore);
// Remove any test data generated
await mockedAPI.cleanUp();
// @ts-expect-error assigning to a RO property
images.getStatus = originalGetStatus;
});
describe('/ping', () => {
it('responds with OK (without auth)', async () => {
await request.get('/ping').set('Accept', 'application/json').expect(200);
});
it('responds with OK (with auth)', async () => {
await request
.get('/ping')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.expect(200);
});
});
describe('V1 endpoints', () => {
describe('GET /v1/healthy', () => {
it('returns OK because all checks pass', async () => {
// Make all healthChecks pass
healthCheckStubs.forEach((hc) => hc.resolves(true));
await request
.get('/v1/healthy')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.expect(sampleResponses.V1.GET['/healthy'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V1.GET['/healthy'].body,
);
expect(response.text).to.deep.equal(
sampleResponses.V1.GET['/healthy'].text,
);
});
});
it('Fails because some checks did not pass', async () => {
// Make one of the healthChecks fail
healthCheckStubs[0].resolves(false);
await request
.get('/v1/healthy')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.expect(sampleResponses.V1.GET['/healthy [2]'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V1.GET['/healthy [2]'].body,
);
expect(response.text).to.deep.equal(
sampleResponses.V1.GET['/healthy [2]'].text,
);
});
});
});
// TODO: add tests for V1 endpoints
describe('GET /v1/device', () => {
it('returns MAC address', async () => {
const response = await request
.get('/v1/device')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.expect(200);
expect(response.body).to.have.property('mac_address').that.is.not.empty;
});
});
});
describe('V2 endpoints', () => {
describe('GET /v2/device/vpn', () => {
it('returns information about VPN connection', async () => {
await request
.get('/v2/device/vpn')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.expect('Content-Type', /json/)
.expect(sampleResponses.V2.GET['/device/vpn'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/device/vpn'].body,
);
});
});
});
describe('GET /v2/applications/:appId/state', () => {
it('returns information about a SPECIFIC application', async () => {
await request
.get('/v2/applications/1/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.expect('Content-Type', /json/)
.expect(sampleResponses.V2.GET['/applications/1/state'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/applications/1/state'].body,
);
});
});
it('returns 400 for invalid appId', async () => {
await request
.get('/v2/applications/123invalid/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.expect('Content-Type', /json/)
.expect(
sampleResponses.V2.GET['/applications/123invalid/state'].statusCode,
)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/applications/123invalid/state'].body,
);
});
});
it('returns 409 because app does not exist', async () => {
await request
.get('/v2/applications/9000/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.expect('Content-Type', /json/)
.expect(sampleResponses.V2.GET['/applications/9000/state'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/applications/9000/state'].body,
);
});
});
});
// TODO: add tests for rest of V2 endpoints
});
describe('State change logging', () => {
before(() => {
// Spy on functions we will be testing
spy(Log, 'info');
spy(Log, 'error');
});
beforeEach(async () => {
// Start each case with API stopped
try {
await api.stop();
} catch (e) {
if (e.message !== 'Server is not running.') {
throw e;
}
}
});
after(async () => {
// @ts-ignore
Log.info.restore();
// @ts-ignore
Log.error.restore();
// Resume API for other test suites
return api.listen(
ALLOWED_INTERFACES,
mockedOptions.listenPort,
mockedOptions.timeout,
);
});
it('logs successful start', async () => {
// Start API
await api.listen(
ALLOWED_INTERFACES,
mockedOptions.listenPort,
mockedOptions.timeout,
);
// Check if success start was logged
// @ts-ignore
expect(Log.info.lastCall?.lastArg).to.equal(
`Supervisor API successfully started on port ${mockedOptions.listenPort}`,
);
});
it('logs shutdown', async () => {
// Start API
await api.listen(
ALLOWED_INTERFACES,
mockedOptions.listenPort,
mockedOptions.timeout,
);
// Stop API
await api.stop();
// Check if stopped with info was logged
// @ts-ignore
expect(Log.info.lastCall?.lastArg).to.equal('Stopped Supervisor API');
});
it('logs errored shutdown', async () => {
// Start API
await api.listen(
ALLOWED_INTERFACES,
mockedOptions.listenPort,
mockedOptions.timeout,
);
// Stop API with error
await api.stop({ errored: true });
// Check if stopped with error was logged
// @ts-ignore
expect(Log.error.lastCall?.lastArg).to.equal('Stopped Supervisor API');
});
});
});