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:
M. Casqueira 2020-05-19 13:04:27 -04:00 committed by GitHub
commit 34fff5ef2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 214 additions and 18 deletions

View File

@ -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);
}, },
); );

View File

@ -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
}); });

View File

@ -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

View File

@ -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": {}

View File

@ -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 };