mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-01-19 11:16:34 +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(
|
router.get(
|
||||||
'/v2/applications/:appId/state',
|
'/v2/applications/:appId/state',
|
||||||
(_req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response) => {
|
||||||
// Get all services and their statuses, and return it
|
// Check application ID provided is valid
|
||||||
applications
|
const appId = checkInt(req.params.appId);
|
||||||
.getStatus()
|
if (!appId) {
|
||||||
.then((apps) => {
|
return res.status(400).json({
|
||||||
res.status(200).json(apps);
|
status: 'failed',
|
||||||
})
|
message: `Invalid application ID: ${req.params.appId}`,
|
||||||
.catch(next);
|
});
|
||||||
|
}
|
||||||
|
// 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,
|
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
|
const ALLOWED_INTERFACES = ['lo']; // Only need loopback since this is for testing
|
||||||
|
|
||||||
describe('SupervisorAPI', () => {
|
describe('SupervisorAPI', () => {
|
||||||
@ -20,7 +20,8 @@ describe('SupervisorAPI', () => {
|
|||||||
const request = supertest(`http://127.0.0.1:${mockedOptions.listenPort}`);
|
const request = supertest(`http://127.0.0.1:${mockedOptions.listenPort}`);
|
||||||
|
|
||||||
before(async () => {
|
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();
|
api = await mockedAPI.create();
|
||||||
// Start test API
|
// Start test API
|
||||||
return api.listen(
|
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
|
// TODO: add tests for rest of V2 endpoints
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ const mockedOptions = {
|
|||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
};
|
};
|
||||||
|
|
||||||
const VALID_SECRET = mockedAPI.DEFAULT_SECRET;
|
const VALID_SECRET = mockedAPI.STUBBED_VALUES.config.apiSecret;
|
||||||
const INVALID_SECRET = 'bad_api_secret';
|
const INVALID_SECRET = 'bad_api_secret';
|
||||||
const ALLOWED_INTERFACES = ['lo']; // Only need loopback since this is for testing
|
const ALLOWED_INTERFACES = ['lo']; // Only need loopback since this is for testing
|
||||||
|
|
||||||
|
@ -11,6 +11,43 @@
|
|||||||
"connected": false
|
"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": {}
|
"POST": {}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { fs } from 'mz';
|
import { fs } from 'mz';
|
||||||
|
import { stub } from 'sinon';
|
||||||
|
|
||||||
import { ApplicationManager } from '../../src/application-manager';
|
import { ApplicationManager } from '../../src/application-manager';
|
||||||
import Config from '../../src/config';
|
import Config from '../../src/config';
|
||||||
@ -9,13 +10,66 @@ import { createV2Api } from '../../src/device-api/v2';
|
|||||||
import DeviceState from '../../src/device-state';
|
import DeviceState from '../../src/device-state';
|
||||||
import EventTracker from '../../src/event-tracker';
|
import EventTracker from '../../src/event-tracker';
|
||||||
import SupervisorAPI from '../../src/supervisor-api';
|
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 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> {
|
async function create(): Promise<SupervisorAPI> {
|
||||||
// Get SupervisorAPI construct options
|
// Get SupervisorAPI construct options
|
||||||
const { db, config, eventTracker, deviceState } = await createAPIOpts();
|
const { db, config, eventTracker, deviceState } = await createAPIOpts();
|
||||||
|
// Stub functions
|
||||||
|
setupStubs();
|
||||||
// Create ApplicationManager
|
// Create ApplicationManager
|
||||||
const appManager = new ApplicationManager({
|
const appManager = new ApplicationManager({
|
||||||
db,
|
db,
|
||||||
@ -43,6 +97,8 @@ async function cleanUp(): Promise<void> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* noop */
|
/* noop */
|
||||||
}
|
}
|
||||||
|
// Restore created SinonStubs
|
||||||
|
return restoreStubs();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createAPIOpts(): Promise<SupervisorAPIOpts> {
|
async function createAPIOpts(): Promise<SupervisorAPIOpts> {
|
||||||
@ -53,11 +109,8 @@ async function createAPIOpts(): Promise<SupervisorAPIOpts> {
|
|||||||
await db.init();
|
await db.init();
|
||||||
// Create config
|
// Create config
|
||||||
const mockedConfig = new Config({ db });
|
const mockedConfig = new Config({ db });
|
||||||
// Set testing secret
|
// Initialize and set values for mocked Config
|
||||||
await mockedConfig.set({
|
await initConfig(mockedConfig);
|
||||||
apiSecret: DEFAULT_SECRET,
|
|
||||||
});
|
|
||||||
await mockedConfig.init();
|
|
||||||
// Create EventTracker
|
// Create EventTracker
|
||||||
const tracker = new EventTracker();
|
const tracker = new EventTracker();
|
||||||
// Create deviceState
|
// 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 {
|
function buildRoutes(appManager: ApplicationManager): Router {
|
||||||
// Create new Router
|
// Create new Router
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@ -87,6 +153,24 @@ function buildRoutes(appManager: ApplicationManager): Router {
|
|||||||
return 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 {
|
interface SupervisorAPIOpts {
|
||||||
db: Database;
|
db: Database;
|
||||||
config: Config;
|
config: Config;
|
||||||
@ -94,4 +178,4 @@ interface SupervisorAPIOpts {
|
|||||||
deviceState: DeviceState;
|
deviceState: DeviceState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export = { create, cleanUp, DEFAULT_SECRET };
|
export = { create, cleanUp, STUBBED_VALUES };
|
||||||
|
Loading…
Reference in New Issue
Block a user