mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-03-21 03:25:46 +00:00
Added test case for /v1/restart API
Change-type: patch Signed-off-by: Miguel Casqueira <miguel@balena.io>
This commit is contained in:
parent
733a2c5dc0
commit
7a4473f65b
@ -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', () => {
|
||||
|
@ -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
|
||||
|
@ -27,6 +27,18 @@
|
||||
"containerId": "abc123"
|
||||
}
|
||||
}
|
||||
},
|
||||
"POST": {
|
||||
"/restart": {
|
||||
"statusCode": 200,
|
||||
"body": {},
|
||||
"text": "OK"
|
||||
},
|
||||
"/restart [Invalid Body]": {
|
||||
"statusCode": 400,
|
||||
"body": {},
|
||||
"text": "Missing app id"
|
||||
}
|
||||
}
|
||||
},
|
||||
"V2": {
|
||||
|
@ -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"
|
102
test/data/mnt/boot/device-type.json
Normal file
102
test/data/mnt/boot/device-type.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user