mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-01-19 03:06:27 +00:00
Merge pull request #1329 from balena-io/1294-returning-app-state
Improved handling of invalid appId in V2 state endpoint
This commit is contained in:
commit
34fff5ef2d
@ -220,14 +220,41 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||
|
||||
router.get(
|
||||
'/v2/applications/:appId/state',
|
||||
(_req: Request, res: Response, next: NextFunction) => {
|
||||
// Get all services and their statuses, and return it
|
||||
applications
|
||||
.getStatus()
|
||||
.then((apps) => {
|
||||
res.status(200).json(apps);
|
||||
})
|
||||
.catch(next);
|
||||
async (req: Request, res: Response) => {
|
||||
// Check application ID provided is valid
|
||||
const appId = checkInt(req.params.appId);
|
||||
if (!appId) {
|
||||
return res.status(400).json({
|
||||
status: 'failed',
|
||||
message: `Invalid application ID: ${req.params.appId}`,
|
||||
});
|
||||
}
|
||||
// Query device for all applications
|
||||
let apps: any;
|
||||
try {
|
||||
apps = await applications.getStatus();
|
||||
} catch (e) {
|
||||
log.error(e.message);
|
||||
return res.status(500).json({
|
||||
status: 'failed',
|
||||
message: `Unable to retrieve state for application ID: ${appId}`,
|
||||
});
|
||||
}
|
||||
// Check if the application exists
|
||||
if (!(appId in apps.local)) {
|
||||
return res.status(409).json({
|
||||
status: 'failed',
|
||||
message: `Application ID does not exist: ${appId}`,
|
||||
});
|
||||
}
|
||||
// Filter applications we do not want
|
||||
for (const app in apps.local) {
|
||||
if (app !== appId.toString()) {
|
||||
delete apps.local[app];
|
||||
}
|
||||
}
|
||||
// Return filtered applications
|
||||
return res.status(200).json(apps);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -12,7 +12,7 @@ const mockedOptions = {
|
||||
timeout: 30000,
|
||||
};
|
||||
|
||||
const VALID_SECRET = mockedAPI.DEFAULT_SECRET;
|
||||
const VALID_SECRET = mockedAPI.STUBBED_VALUES.config.apiSecret;
|
||||
const ALLOWED_INTERFACES = ['lo']; // Only need loopback since this is for testing
|
||||
|
||||
describe('SupervisorAPI', () => {
|
||||
@ -20,7 +20,8 @@ describe('SupervisorAPI', () => {
|
||||
const request = supertest(`http://127.0.0.1:${mockedOptions.listenPort}`);
|
||||
|
||||
before(async () => {
|
||||
// Create test API
|
||||
// The mockedAPI contains stubs that might create unexpected results
|
||||
// See the module to know what has been stubbed
|
||||
api = await mockedAPI.create();
|
||||
// Start test API
|
||||
return api.listen(
|
||||
@ -75,6 +76,53 @@ describe('SupervisorAPI', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
|
@ -8,7 +8,7 @@ const mockedOptions = {
|
||||
timeout: 30000,
|
||||
};
|
||||
|
||||
const VALID_SECRET = mockedAPI.DEFAULT_SECRET;
|
||||
const VALID_SECRET = mockedAPI.STUBBED_VALUES.config.apiSecret;
|
||||
const INVALID_SECRET = 'bad_api_secret';
|
||||
const ALLOWED_INTERFACES = ['lo']; // Only need loopback since this is for testing
|
||||
|
||||
|
@ -11,6 +11,43 @@
|
||||
"connected": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"/applications/1/state": {
|
||||
"statusCode": 200,
|
||||
"body": {
|
||||
"local": {
|
||||
"1": {
|
||||
"services": {
|
||||
"1111": {
|
||||
"status": "Running",
|
||||
"releaseId": 99999,
|
||||
"download_progress": null
|
||||
},
|
||||
"2222": {
|
||||
"status": "Running",
|
||||
"releaseId": 99999,
|
||||
"download_progress": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependent": {},
|
||||
"commit": "7fc9c5bea8e361acd49886fe6cc1e1cd"
|
||||
}
|
||||
},
|
||||
"/applications/9000/state": {
|
||||
"statusCode": 409,
|
||||
"body": {
|
||||
"status": "failed",
|
||||
"message": "Application ID does not exist: 9000"
|
||||
}
|
||||
},
|
||||
"/applications/123invalid/state": {
|
||||
"statusCode": 400,
|
||||
"body": {
|
||||
"status": "failed",
|
||||
"message": "Invalid application ID: 123invalid"
|
||||
}
|
||||
}
|
||||
},
|
||||
"POST": {}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import { fs } from 'mz';
|
||||
import { stub } from 'sinon';
|
||||
|
||||
import { ApplicationManager } from '../../src/application-manager';
|
||||
import Config from '../../src/config';
|
||||
@ -9,13 +10,66 @@ import { createV2Api } from '../../src/device-api/v2';
|
||||
import DeviceState from '../../src/device-state';
|
||||
import EventTracker from '../../src/event-tracker';
|
||||
import SupervisorAPI from '../../src/supervisor-api';
|
||||
import { Images } from '../../src/compose/images';
|
||||
import { ServiceManager } from '../../src/compose/service-manager';
|
||||
import { NetworkManager } from '../../src/compose/network-manager';
|
||||
import { VolumeManager } from '../../src/compose/volume-manager';
|
||||
|
||||
const DB_PATH = './test/data/supervisor-api.sqlite';
|
||||
const DEFAULT_SECRET = 'secure_api_secret';
|
||||
// Holds all values used for stubbing
|
||||
const STUBBED_VALUES = {
|
||||
config: {
|
||||
apiSecret: 'secure_api_secret',
|
||||
currentCommit: '7fc9c5bea8e361acd49886fe6cc1e1cd',
|
||||
},
|
||||
services: [
|
||||
{
|
||||
appId: 1,
|
||||
imageId: 1111,
|
||||
status: 'Running',
|
||||
releaseId: 99999,
|
||||
createdAt: new Date('2020-04-25T04:15:23.111Z'),
|
||||
serviceName: 'main',
|
||||
},
|
||||
{
|
||||
appId: 1,
|
||||
imageId: 2222,
|
||||
status: 'Running',
|
||||
releaseId: 99999,
|
||||
createdAt: new Date('2020-04-25T04:15:23.111Z'),
|
||||
serviceName: 'redis',
|
||||
},
|
||||
{
|
||||
appId: 2,
|
||||
imageId: 3333,
|
||||
status: 'Running',
|
||||
releaseId: 77777,
|
||||
createdAt: new Date('2020-05-15T19:33:06.088Z'),
|
||||
serviceName: 'main',
|
||||
},
|
||||
],
|
||||
images: [],
|
||||
networks: [],
|
||||
volumes: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* THIS MOCKED API CONTAINS STUBS THAT MIGHT CAUSE UNEXPECTED RESULTS
|
||||
* IF YOU WANT TO ADD/MODIFY STUBS THAT INVOLVE API OPERATIONS
|
||||
* AND MULTIPLE TEST CASES WILL USE THEM THEN ADD THEM HERE
|
||||
* OTHERWISE YOU CAN ADD STUBS ON A PER TEST CASE BASIS
|
||||
*
|
||||
* EXAMPLE: We stub ApplicationManager so there is atleast 1 running app
|
||||
*
|
||||
* You can see all the stubbed values convientiely in STUBBED_VALUES.
|
||||
*
|
||||
*/
|
||||
|
||||
async function create(): Promise<SupervisorAPI> {
|
||||
// Get SupervisorAPI construct options
|
||||
const { db, config, eventTracker, deviceState } = await createAPIOpts();
|
||||
// Stub functions
|
||||
setupStubs();
|
||||
// Create ApplicationManager
|
||||
const appManager = new ApplicationManager({
|
||||
db,
|
||||
@ -43,6 +97,8 @@ async function cleanUp(): Promise<void> {
|
||||
} catch (e) {
|
||||
/* noop */
|
||||
}
|
||||
// Restore created SinonStubs
|
||||
return restoreStubs();
|
||||
}
|
||||
|
||||
async function createAPIOpts(): Promise<SupervisorAPIOpts> {
|
||||
@ -53,11 +109,8 @@ async function createAPIOpts(): Promise<SupervisorAPIOpts> {
|
||||
await db.init();
|
||||
// Create config
|
||||
const mockedConfig = new Config({ db });
|
||||
// Set testing secret
|
||||
await mockedConfig.set({
|
||||
apiSecret: DEFAULT_SECRET,
|
||||
});
|
||||
await mockedConfig.init();
|
||||
// Initialize and set values for mocked Config
|
||||
await initConfig(mockedConfig);
|
||||
// Create EventTracker
|
||||
const tracker = new EventTracker();
|
||||
// Create deviceState
|
||||
@ -76,6 +129,19 @@ async function createAPIOpts(): Promise<SupervisorAPIOpts> {
|
||||
};
|
||||
}
|
||||
|
||||
async function initConfig(config: Config): Promise<void> {
|
||||
// Set testing secret
|
||||
await config.set({
|
||||
apiSecret: STUBBED_VALUES.config.apiSecret,
|
||||
});
|
||||
// Set a currentCommit
|
||||
await config.set({
|
||||
currentCommit: STUBBED_VALUES.config.currentCommit,
|
||||
});
|
||||
// Initialize this config
|
||||
return config.init();
|
||||
}
|
||||
|
||||
function buildRoutes(appManager: ApplicationManager): Router {
|
||||
// Create new Router
|
||||
const router = Router();
|
||||
@ -87,6 +153,24 @@ function buildRoutes(appManager: ApplicationManager): Router {
|
||||
return router;
|
||||
}
|
||||
|
||||
function setupStubs() {
|
||||
stub(ServiceManager.prototype, 'getStatus').resolves(STUBBED_VALUES.services);
|
||||
stub(Images.prototype, 'getStatus').resolves(STUBBED_VALUES.images);
|
||||
stub(NetworkManager.prototype, 'getAllByAppId').resolves(
|
||||
STUBBED_VALUES.networks,
|
||||
);
|
||||
stub(VolumeManager.prototype, 'getAllByAppId').resolves(
|
||||
STUBBED_VALUES.volumes,
|
||||
);
|
||||
}
|
||||
|
||||
function restoreStubs() {
|
||||
(ServiceManager.prototype as any).getStatus.restore();
|
||||
(Images.prototype as any).getStatus.restore();
|
||||
(NetworkManager.prototype as any).getAllByAppId.restore();
|
||||
(VolumeManager.prototype as any).getAllByAppId.restore();
|
||||
}
|
||||
|
||||
interface SupervisorAPIOpts {
|
||||
db: Database;
|
||||
config: Config;
|
||||
@ -94,4 +178,4 @@ interface SupervisorAPIOpts {
|
||||
deviceState: DeviceState;
|
||||
}
|
||||
|
||||
export = { create, cleanUp, DEFAULT_SECRET };
|
||||
export = { create, cleanUp, STUBBED_VALUES };
|
||||
|
Loading…
Reference in New Issue
Block a user