Added test case for /v1/restart API

Change-type: patch
Signed-off-by: Miguel Casqueira <miguel@balena.io>
This commit is contained in:
Miguel Casqueira 2020-11-18 20:37:34 -05:00
parent 733a2c5dc0
commit 7a4473f65b
8 changed files with 335 additions and 90 deletions

View File

@ -1,25 +1,23 @@
import * as _ from 'lodash';
import { expect } from 'chai';
import { stub, SinonStub } from 'sinon';
import * as supertest from 'supertest';
import sampleResponses = require('./data/device-api-responses.json');
import * as appMock from './lib/application-state-mock';
import * as mockedDockerode from './lib/mocked-dockerode';
import mockedAPI = require('./lib/mocked-device-api');
import sampleResponses = require('./data/device-api-responses.json');
import SupervisorAPI from '../src/supervisor-api';
import * as apiBinder from '../src/api-binder';
import * as deviceState from '../src/device-state';
import SupervisorAPI from '../src/supervisor-api';
import * as applicationManager from '../src/compose/application-manager';
import { InstancedAppState } from '../src/types/state';
import * as apiKeys from '../src/lib/api-keys';
const mockedOptions = {
listenPort: 54321,
timeout: 30000,
};
describe('SupervisorAPI [V1 Endpoints]', () => {
let api: SupervisorAPI;
let healthCheckStubs: SinonStub[];
const request = supertest(`http://127.0.0.1:${mockedOptions.listenPort}`);
const request = supertest(
`http://127.0.0.1:${mockedAPI.mockedOptions.listenPort}`,
);
before(async () => {
await apiBinder.initialized;
@ -36,30 +34,14 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
api = await mockedAPI.create();
// Start test API
await api.listen(mockedOptions.listenPort, mockedOptions.timeout);
await api.listen(
mockedAPI.mockedOptions.listenPort,
mockedAPI.mockedOptions.timeout,
);
// Create a scoped key
await apiKeys.initialized;
await apiKeys.generateCloudKey();
const appState = {
[sampleResponses.V1.GET['/apps/2'].body.appId]: {
...sampleResponses.V1.GET['/apps/2'].body,
services: [
{
...sampleResponses.V1.GET['/apps/2'].body,
serviceId: 1,
serviceName: 'main',
config: {},
},
],
},
};
stub(applicationManager, 'getCurrentApps').resolves(
(appState as unknown) as InstancedAppState,
);
stub(applicationManager, 'executeStep').resolves();
});
after(async () => {
@ -74,8 +56,89 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
healthCheckStubs.forEach((hc) => hc.restore);
// Remove any test data generated
await mockedAPI.cleanUp();
(applicationManager.executeStep as SinonStub).restore();
(applicationManager.getCurrentApps as SinonStub).restore();
});
beforeEach(() => {
// Sane defaults
appMock.mockSupervisorNetwork(true);
appMock.mockManagers([], [], []);
appMock.mockImages([], false, []);
});
afterEach(() => {
appMock.unmockAll();
// Clear Dockerode actions recorded for each test
mockedDockerode.resetHistory();
});
describe('POST /v1/restart', () => {
it('restarts application', async () => {
const ID_TO_RESTART = 2;
// single app scoped key...
const appScopedKey = await apiKeys.generateScopedKey(
ID_TO_RESTART,
640681,
);
const service = mockedAPI.mockService({
appId: ID_TO_RESTART,
serviceId: 640681,
});
const image = mockedAPI.mockImage({
appId: ID_TO_RESTART,
serviceId: 640681,
});
const images = [image];
const containers = [service];
// Setup device conditions
appMock.mockManagers([service], [], []);
appMock.mockImages([], false, images);
// Perform the test with our specially crafted data
await mockedDockerode.testWithData({ containers, images }, async () => {
// Perform test
await request
.post('/v1/restart')
.send({ appId: ID_TO_RESTART })
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${appScopedKey}`)
.expect(sampleResponses.V1.POST['/restart'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V1.POST['/restart'].body,
);
expect(response.text).to.deep.equal(
sampleResponses.V1.POST['/restart'].text,
);
});
// Check that mockedDockerode contains 1 stop and start action
const removeSteps = _(mockedDockerode.actions)
.pickBy({ name: 'stop' })
.map()
.value();
expect(removeSteps).to.have.lengthOf(1);
const startSteps = _(mockedDockerode.actions)
.pickBy({ name: 'start' })
.map()
.value();
expect(startSteps).to.have.lengthOf(1);
});
});
it('validates request body parameters', async () => {
await request
.post('/v1/restart')
.send({ thing: '' })
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V1.POST['/restart [Invalid Body]'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V1.POST['/restart [Invalid Body]'].body,
);
expect(response.text).to.deep.equal(
sampleResponses.V1.POST['/restart [Invalid Body]'].text,
);
});
});
});
describe('GET /v1/healthy', () => {

View File

@ -11,52 +11,14 @@ import * as serviceManager from '../src/compose/service-manager';
import * as images from '../src/compose/images';
import * as apiKeys from '../src/lib/api-keys';
import * as config from '../src/config';
import { Service } from '../src/compose/service';
import { Image } from '../src/compose/images';
const mockedOptions = {
listenPort: 54321,
timeout: 30000,
};
const mockService = (overrides?: Partial<Service>) => {
return {
...{
appId: 1658654,
status: 'Running',
serviceName: 'main',
imageId: 2885946,
serviceId: 640681,
containerId:
'f93d386599d1b36e71272d46ad69770cff333842db04e2e4c64dda7b54da07c6',
createdAt: '2020-11-13T20:29:44.143Z',
},
...overrides,
} as Service;
};
const mockImage = (overrides?: Partial<Image>) => {
return {
...{
name:
'registry2.balena-cloud.com/v2/e2bf6410ffc30850e96f5071cdd1dca8@sha256:e2e87a8139b8fc14510095b210ad652d7d5badcc64fdc686cbf749d399fba15e',
appId: 1658654,
serviceName: 'main',
imageId: 2885946,
dockerImageId:
'sha256:4502983d72e2c72bc292effad1b15b49576da3801356f47fd275ba274d409c1a',
status: 'Downloaded',
downloadProgress: null,
},
...overrides,
} as Image;
};
describe('SupervisorAPI [V2 Endpoints]', () => {
let serviceManagerMock: SinonStub;
let imagesMock: SinonStub;
let api: SupervisorAPI;
const request = supertest(`http://127.0.0.1:${mockedOptions.listenPort}`);
const request = supertest(
`http://127.0.0.1:${mockedAPI.mockedOptions.listenPort}`,
);
before(async () => {
await apiBinder.initialized;
@ -67,7 +29,10 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
api = await mockedAPI.create();
// Start test API
await api.listen(mockedOptions.listenPort, mockedOptions.timeout);
await api.listen(
mockedAPI.mockedOptions.listenPort,
mockedAPI.mockedOptions.timeout,
);
// Create a scoped key
await apiKeys.initialized;
@ -167,8 +132,8 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
// Create scoped key for application
const appScopedKey = await apiKeys.generateScopedKey(1658654, 640681);
// Setup device conditions
serviceManagerMock.resolves([mockService({ appId: 1658654 })]);
imagesMock.resolves([mockImage({ appId: 1658654 })]);
serviceManagerMock.resolves([mockedAPI.mockService({ appId: 1658654 })]);
imagesMock.resolves([mockedAPI.mockImage({ appId: 1658654 })]);
// Make request and evaluate response
await request
.get('/v2/state/status')
@ -191,8 +156,8 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
// Create scoped key for wrong application
const appScopedKey = await apiKeys.generateScopedKey(1, 1);
// Setup device conditions
serviceManagerMock.resolves([mockService({ appId: 1658654 })]);
imagesMock.resolves([mockImage({ appId: 1658654 })]);
serviceManagerMock.resolves([mockedAPI.mockService({ appId: 1658654 })]);
imagesMock.resolves([mockedAPI.mockImage({ appId: 1658654 })]);
// Make request and evaluate response
await request
.get('/v2/state/status')
@ -238,12 +203,12 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
const appScopedKey = await apiKeys.generateScopedKey(1658654, 640681);
// Setup device conditions
serviceManagerMock.resolves([
mockService({ appId: 1658654 }),
mockService({ appId: 222222 }),
mockedAPI.mockService({ appId: 1658654 }),
mockedAPI.mockService({ appId: 222222 }),
]);
imagesMock.resolves([
mockImage({ appId: 1658654 }),
mockImage({ appId: 222222 }),
mockedAPI.mockImage({ appId: 1658654 }),
mockedAPI.mockImage({ appId: 222222 }),
]);
// Make request and evaluate response
await request
@ -268,12 +233,12 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
await config.set({ localMode: true });
// Setup device conditions
serviceManagerMock.resolves([
mockService({ appId: 1658654 }),
mockService({ appId: 222222 }),
mockedAPI.mockService({ appId: 1658654 }),
mockedAPI.mockService({ appId: 222222 }),
]);
imagesMock.resolves([
mockImage({ appId: 1658654 }),
mockImage({ appId: 222222 }),
mockedAPI.mockImage({ appId: 1658654 }),
mockedAPI.mockImage({ appId: 222222 }),
]);
// Make request and evaluate response
await request

View File

@ -27,6 +27,18 @@
"containerId": "abc123"
}
}
},
"POST": {
"/restart": {
"statusCode": 200,
"body": {},
"text": "OK"
},
"/restart [Invalid Body]": {
"statusCode": 400,
"body": {},
"text": "Missing app id"
}
}
},
"V2": {

View File

@ -1,3 +1,12 @@
PRETTY_NAME="balenaOS 2.0.6 (fake)"
VARIANT_ID="dev"
ID="balena-os"
NAME="balenaOS"
VERSION="2.0.6"
VERSION_ID="2.0.6+rev1"
PRETTY_NAME="balenaOS 2.0.6+rev1"
MACHINE="raspberrypi4-64"
VARIANT="Development"
VARIANT_ID="dev"
META_BALENA_VERSION="2.0.6"
RESIN_BOARD_REV="b57b01a"
META_RESIN_REV="ef55525"
SLUG="raspberrypi4-64"

View File

@ -0,0 +1,102 @@
{
"slug": "raspberrypi4-64",
"version": 1,
"aliases": [
"raspberrypi4-64"
],
"name": "Raspberry Pi 4",
"arch": "aarch64",
"state": "RELEASED",
"private": false,
"instructions": [
"Write the OS file you downloaded to your SD card. We recommend using <a href=\"http://www.etcher.io/\">Etcher</a>.",
"Insert the freshly burnt SD card into the Raspberry Pi 4.",
"Connect your Raspberry Pi 4 to the internet, then power it up."
],
"gettingStartedLink": {
"windows": "https://www.balena.io/docs/learn/getting-started/raspberrypi4/nodejs/",
"osx": "https://www.balena.io/docs/learn/getting-started/raspberrypi4/nodejs/",
"linux": "https://www.balena.io/docs/learn/getting-started/raspberrypi4/nodejs/"
},
"supportsBlink": true,
"options": [
{
"isGroup": true,
"name": "network",
"message": "Network",
"options": [
{
"message": "Network Connection",
"name": "network",
"type": "list",
"choices": [
"ethernet",
"wifi"
]
},
{
"message": "Wifi SSID",
"name": "wifiSsid",
"type": "text",
"when": {
"network": "wifi"
}
},
{
"message": "Wifi Passphrase",
"name": "wifiKey",
"type": "password",
"when": {
"network": "wifi"
}
}
]
},
{
"isGroup": true,
"isCollapsible": true,
"collapsed": true,
"name": "advanced",
"message": "Advanced",
"options": [
{
"message": "Check for updates every X minutes",
"name": "appUpdatePollInterval",
"type": "number",
"min": 10,
"default": 10
}
]
}
],
"yocto": {
"machine": "raspberrypi4-64",
"image": "resin-image",
"fstype": "resinos-img",
"version": "yocto-warrior",
"deployArtifact": "resin-image-raspberrypi4-64.resinos-img",
"compressed": true
},
"configuration": {
"config": {
"partition": {
"primary": 1
},
"path": "/config.json"
}
},
"initialization": {
"options": [
{
"message": "Select a drive",
"type": "drive",
"name": "drive"
}
],
"operations": [
{
"command": "burn"
}
]
}
}

View File

@ -22,6 +22,15 @@ export function mockManagers(svcs: Service[], vols: Volume[], nets: Network[]) {
networkManager.getAll = async () => nets;
// @ts-expect-error Assigning to a RO property
serviceManager.getAll = async () => {
// Filter services that are being removed
svcs = svcs.filter((s) => s.status !== 'removing');
// Update Installing containers to Running
svcs = svcs.map((s) => {
if (s.status === 'Installing') {
s.status = 'Running';
}
return s;
});
console.log('Calling the mock', svcs);
return svcs;
};
@ -43,7 +52,6 @@ export function mockImages(
) {
// @ts-expect-error Assigning to a RO property
imageManager.getDownloadingImageIds = () => {
console.log('CALLED');
return downloading;
};
// @ts-expect-error Assigning to a RO property

View File

@ -14,8 +14,11 @@ import { createV2Api } from '../../src/device-api/v2';
import * as apiBinder from '../../src/api-binder';
import * as deviceState from '../../src/device-state';
import SupervisorAPI from '../../src/supervisor-api';
import { Service } from '../../src/compose/service';
import { Image } from '../../src/compose/images';
const DB_PATH = './test/data/supervisor-api.sqlite';
// Holds all values used for stubbing
const STUBBED_VALUES = {
commits: {
@ -54,6 +57,49 @@ const STUBBED_VALUES = {
volumes: [],
};
// Useful for creating mock services in the ServiceManager
const mockService = (overrides?: Partial<Service>) => {
return {
...{
appId: 1658654,
status: 'Running',
serviceName: 'main',
imageId: 2885946,
serviceId: 640681,
containerId:
'f93d386599d1b36e71272d46ad69770cff333842db04e2e4c64dda7b54da07c6',
createdAt: '2020-11-13T20:29:44.143Z',
config: {
labels: {},
},
},
...overrides,
} as Service;
};
// Useful for creating mock images that are returned from Images.getStatus
const mockImage = (overrides?: Partial<Image>) => {
return {
...{
name:
'registry2.balena-cloud.com/v2/e2bf6410ffc30850e96f5071cdd1dca8@sha256:e2e87a8139b8fc14510095b210ad652d7d5badcc64fdc686cbf749d399fba15e',
appId: 1658654,
serviceName: 'main',
imageId: 2885946,
dockerImageId:
'sha256:4502983d72e2c72bc292effad1b15b49576da3801356f47fd275ba274d409c1a',
status: 'Downloaded',
downloadProgress: null,
},
...overrides,
} as Image;
};
const mockedOptions = {
listenPort: 54321,
timeout: 30000,
};
/**
* THIS MOCKED API CONTAINS STUBS THAT MIGHT CAUSE UNEXPECTED RESULTS
* IF YOU WANT TO ADD/MODIFY STUBS THAT INVOLVE API OPERATIONS
@ -156,4 +202,11 @@ function restoreStubs() {
serviceManager.getAllByAppId = originalSvcGetAppId;
}
export = { create, cleanUp, STUBBED_VALUES };
export = {
create,
cleanUp,
STUBBED_VALUES,
mockService,
mockImage,
mockedOptions,
};

View File

@ -75,10 +75,43 @@ export function registerOverride<
export interface TestData {
networks: Dictionary<any>;
images: Dictionary<any>;
containers: Dictionary<any>;
}
function createMockedDockerode(data: TestData) {
const mockedDockerode = dockerode.prototype;
mockedDockerode.getContainer = (id: string) => {
addAction('getContainer', { id });
return {
start: async () => {
addAction('start', {});
data.containers = data.containers.map((c: any) => {
if (c.containerId === id) {
c.status = 'Installing';
}
return c;
});
},
stop: async () => {
addAction('stop', {});
data.containers = data.containers.map((c: any) => {
if (c.containerId === id) {
c.status = 'Stopping';
}
return c;
});
},
remove: async () => {
addAction('remove', {});
data.containers = data.containers.map((c: any) => {
if (c.containerId === id) {
c.status = 'removing';
}
return c;
});
},
} as dockerode.Container;
};
mockedDockerode.getNetwork = (id: string) => {
addAction('getNetwork', { id });
return {