mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-20 06:07:57 +00:00
28c5a44e71
Controlled by BALENA_HOST_FIREWALL_MODE, the firewall can either be 'on' or 'off'. - In the 'off' state, all traffic is allowed. - In the 'on' state, only traffic for the core services provided by Balena is allowed. Change-type: patch Signed-off-by: Rich Bayliss <rich@balena.io>
248 lines
7.3 KiB
TypeScript
248 lines
7.3 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;
|
|
|
|
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(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(mockedOptions.listenPort, mockedOptions.timeout);
|
|
});
|
|
|
|
it('logs successful start', async () => {
|
|
// Start API
|
|
await api.listen(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(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(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');
|
|
});
|
|
});
|
|
});
|