Consolidated Supervisor API tests into clearer files

Change-type: patch
Signed-off-by: Miguel Casqueira <miguel@balena.io>
This commit is contained in:
Miguel Casqueira 2020-11-18 13:24:18 -05:00
parent f6ba69144b
commit 733a2c5dc0
4 changed files with 540 additions and 494 deletions

View File

@ -1,79 +1,29 @@
import { expect } from 'chai';
import { spy, stub, SinonStub } from 'sinon';
import { spy } from 'sinon';
import * as supertest from 'supertest';
import mockedAPI = require('./lib/mocked-device-api');
import * as apiBinder from '../src/api-binder';
import * as deviceState from '../src/device-state';
import Log from '../src/lib/supervisor-console';
import SupervisorAPI from '../src/supervisor-api';
import sampleResponses = require('./data/device-api-responses.json');
import mockedAPI = require('./lib/mocked-device-api');
import * as applicationManager from '../src/compose/application-manager';
import { InstancedAppState } from '../src/types/state';
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 db from '../src/db';
import * as config from '../src/config';
import { Service } from '../src/compose/service';
import { Image } from '../src/compose/images';
import { cloudApiKey } from '../src/lib/api-keys';
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', () => {
let api: SupervisorAPI;
let healthCheckStubs: SinonStub[];
const request = supertest(`http://127.0.0.1:${mockedOptions.listenPort}`);
before(async () => {
await apiBinder.initialized;
await deviceState.initialized;
// Stub health checks so we can modify them whenever needed
healthCheckStubs = [
stub(apiBinder, 'healthcheck'),
stub(deviceState, 'healthcheck'),
];
// The mockedAPI contains stubs that might create unexpected results
// See the module to know what has been stubbed
api = await mockedAPI.create();
@ -94,12 +44,23 @@ describe('SupervisorAPI', () => {
throw e;
}
}
// Restore healthcheck stubs
healthCheckStubs.forEach((hc) => hc.restore);
// Remove any test data generated
await mockedAPI.cleanUp();
});
describe('/ping', () => {
it('responds with OK (without auth)', async () => {
await request.get('/ping').set('Accept', 'application/json').expect(200);
});
it('responds with OK (with auth)', async () => {
await request
.get('/ping')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(200);
});
});
describe('API Key Scope', () => {
it('should generate a key which is scoped for a single application', async () => {
// single app scoped key...
@ -198,350 +159,6 @@ describe('SupervisorAPI', () => {
});
});
describe('/ping', () => {
it('responds with OK (without auth)', async () => {
await request.get('/ping').set('Accept', 'application/json').expect(200);
});
it('responds with OK (with auth)', async () => {
await request
.get('/ping')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(200);
});
});
describe('V1 endpoints', () => {
describe('GET /v1/healthy', () => {
it('returns OK because all checks pass', async () => {
// Make all healthChecks pass
healthCheckStubs.forEach((hc) => hc.resolves(true));
await request
.get('/v1/healthy')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V1.GET['/healthy'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V1.GET['/healthy'].body,
);
expect(response.text).to.deep.equal(
sampleResponses.V1.GET['/healthy'].text,
);
});
});
it('Fails because some checks did not pass', async () => {
// Make one of the healthChecks fail
healthCheckStubs[0].resolves(false);
await request
.get('/v1/healthy')
.set('Accept', 'application/json')
.expect(sampleResponses.V1.GET['/healthy [2]'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V1.GET['/healthy [2]'].body,
);
expect(response.text).to.deep.equal(
sampleResponses.V1.GET['/healthy [2]'].text,
);
});
});
});
before(() => {
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(() => {
(applicationManager.executeStep as SinonStub).restore();
(applicationManager.getCurrentApps as SinonStub).restore();
});
// TODO: add tests for V1 endpoints
describe('GET /v1/apps/:appId', () => {
it('returns information about a specific application', async () => {
await request
.get('/v1/apps/2')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V1.GET['/apps/2'].statusCode)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V1.GET['/apps/2'].body,
);
});
});
});
describe('POST /v1/apps/:appId/stop', () => {
it('stops a SPECIFIC application and returns a containerId', async () => {
await request
.post('/v1/apps/2/stop')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V1.GET['/apps/2/stop'].statusCode)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V1.GET['/apps/2/stop'].body,
);
});
});
});
describe('GET /v1/device', () => {
it('returns MAC address', async () => {
const response = await request
.get('/v1/device')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(200);
expect(response.body).to.have.property('mac_address').that.is.not.empty;
});
});
});
describe('V2 endpoints', () => {
let serviceManagerMock: SinonStub;
let imagesMock: SinonStub;
before(async () => {
serviceManagerMock = stub(serviceManager, 'getAll').resolves([]);
imagesMock = stub(images, 'getStatus').resolves([]);
});
after(async () => {
serviceManagerMock.restore();
imagesMock.restore();
});
describe('GET /v2/device/vpn', () => {
it('returns information about VPN connection', async () => {
await request
.get('/v2/device/vpn')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect('Content-Type', /json/)
.expect(sampleResponses.V2.GET['/device/vpn'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/device/vpn'].body,
);
});
});
});
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 ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V2.GET['/applications/1/state'].statusCode)
.expect('Content-Type', /json/)
.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 ${apiKeys.cloudApiKey}`)
.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 ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V2.GET['/applications/9000/state'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/applications/9000/state'].body,
);
});
});
describe('Scoped API Keys', () => {
it('returns 409 because app is out of scope of the key', async () => {
const apiKey = await apiKeys.generateScopedKey(3, 1);
await request
.get('/v2/applications/2/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKey}`)
.expect(409);
});
});
});
describe('GET /v2/state/status', () => {
it('should return scoped application', async () => {
// 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 })]);
// Make request and evaluate response
await request
.get('/v2/state/status')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${appScopedKey}`)
.expect('Content-Type', /json/)
.expect(
sampleResponses.V2.GET['/state/status?desc=single_application']
.statusCode,
)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/state/status?desc=single_application']
.body,
);
});
});
it('should return no application info due to lack of scope', async () => {
// 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 })]);
// Make request and evaluate response
await request
.get('/v2/state/status')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${appScopedKey}`)
.expect('Content-Type', /json/)
.expect(
sampleResponses.V2.GET['/state/status?desc=no_applications']
.statusCode,
)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/state/status?desc=no_applications'].body,
);
});
});
it('should return success when device has no applications', async () => {
// Create scoped key for any application
const appScopedKey = await apiKeys.generateScopedKey(1658654, 1658654);
// Setup device conditions
serviceManagerMock.resolves([]);
imagesMock.resolves([]);
// Make request and evaluate response
await request
.get('/v2/state/status')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${appScopedKey}`)
.expect('Content-Type', /json/)
.expect(
sampleResponses.V2.GET['/state/status?desc=no_applications']
.statusCode,
)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/state/status?desc=no_applications'].body,
);
});
});
it('should only return 1 application when N > 1 applications on device', async () => {
// Create scoped key for application
const appScopedKey = await apiKeys.generateScopedKey(1658654, 640681);
// Setup device conditions
serviceManagerMock.resolves([
mockService({ appId: 1658654 }),
mockService({ appId: 222222 }),
]);
imagesMock.resolves([
mockImage({ appId: 1658654 }),
mockImage({ appId: 222222 }),
]);
// Make request and evaluate response
await request
.get('/v2/state/status')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${appScopedKey}`)
.expect('Content-Type', /json/)
.expect(
sampleResponses.V2.GET['/state/status?desc=single_application']
.statusCode,
)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/state/status?desc=single_application']
.body,
);
});
});
it('should only return 1 application when in LOCAL MODE (no auth)', async () => {
// Activate localmode
await config.set({ localMode: true });
// Setup device conditions
serviceManagerMock.resolves([
mockService({ appId: 1658654 }),
mockService({ appId: 222222 }),
]);
imagesMock.resolves([
mockImage({ appId: 1658654 }),
mockImage({ appId: 222222 }),
]);
// Make request and evaluate response
await request
.get('/v2/state/status')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(
sampleResponses.V2.GET['/state/status?desc=single_application']
.statusCode,
)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/state/status?desc=single_application']
.body,
);
});
// Deactivate localmode
await config.set({ localMode: false });
});
});
// TODO: add tests for rest of V2 endpoints
});
describe('State change logging', () => {
before(() => {
// Spy on functions we will be testing
@ -599,4 +216,67 @@ describe('SupervisorAPI', () => {
expect(Log.error.lastCall?.lastArg).to.equal('Stopped Supervisor API');
});
});
describe('Authentication', () => {
const INVALID_SECRET = 'bad_api_secret';
it('finds no apiKey and rejects', async () => {
return request.post('/v1/blink').expect(401);
});
it('finds apiKey from query', async () => {
return request.post(`/v1/blink?apikey=${cloudApiKey}`).expect(200);
});
it('finds apiKey from Authorization header (ApiKey scheme)', async () => {
return request
.post('/v1/blink')
.set('Authorization', `ApiKey ${cloudApiKey}`)
.expect(200);
});
it('finds apiKey from Authorization header (Bearer scheme)', async () => {
return request
.post('/v1/blink')
.set('Authorization', `Bearer ${cloudApiKey}`)
.expect(200);
});
it('finds apiKey from Authorization header (case insensitive)', async () => {
const randomCases = [
'Bearer',
'bearer',
'BEARER',
'BeAReR',
'ApiKey',
'apikey',
'APIKEY',
'ApIKeY',
];
for (const scheme of randomCases) {
return request
.post('/v1/blink')
.set('Authorization', `${scheme} ${cloudApiKey}`)
.expect(200);
}
});
it('rejects invalid apiKey from query', async () => {
return request.post(`/v1/blink?apikey=${INVALID_SECRET}`).expect(401);
});
it('rejects invalid apiKey from Authorization header (ApiKey scheme)', async () => {
return request
.post('/v1/blink')
.set('Authorization', `ApiKey ${INVALID_SECRET}`)
.expect(401);
});
it('rejects invalid apiKey from Authorization header (Bearer scheme)', async () => {
return request
.post('/v1/blink')
.set('Authorization', `Bearer ${INVALID_SECRET}`)
.expect(401);
});
});
});

View File

@ -1,95 +0,0 @@
import * as supertest from 'supertest';
import SupervisorAPI from '../src/supervisor-api';
import mockedAPI = require('./lib/mocked-device-api');
import { cloudApiKey } from '../src/lib/api-keys';
const mockedOptions = {
listenPort: 12345,
timeout: 30000,
};
const INVALID_SECRET = 'bad_api_secret';
describe('SupervisorAPI authentication', () => {
let api: SupervisorAPI;
const request = supertest(`http://127.0.0.1:${mockedOptions.listenPort}`);
before(async () => {
// Create test API
api = await mockedAPI.create();
// Start test API
return api.listen(mockedOptions.listenPort, mockedOptions.timeout);
});
after(async () => {
try {
await api.stop();
} catch (e) {
if (e.message !== 'Server is not running.') {
throw e;
}
}
// Remove any test data generated
await mockedAPI.cleanUp();
});
it('finds no apiKey and rejects', async () => {
return request.post('/v1/blink').expect(401);
});
it('finds apiKey from query', async () => {
return request.post(`/v1/blink?apikey=${cloudApiKey}`).expect(200);
});
it('finds apiKey from Authorization header (ApiKey scheme)', async () => {
return request
.post('/v1/blink')
.set('Authorization', `ApiKey ${cloudApiKey}`)
.expect(200);
});
it('finds apiKey from Authorization header (Bearer scheme)', async () => {
return request
.post('/v1/blink')
.set('Authorization', `Bearer ${cloudApiKey}`)
.expect(200);
});
it('finds apiKey from Authorization header (case insensitive)', async () => {
const randomCases = [
'Bearer',
'bearer',
'BEARER',
'BeAReR',
'ApiKey',
'apikey',
'APIKEY',
'ApIKeY',
];
for (const scheme of randomCases) {
return request
.post('/v1/blink')
.set('Authorization', `${scheme} ${cloudApiKey}`)
.expect(200);
}
});
it('rejects invalid apiKey from query', async () => {
return request.post(`/v1/blink?apikey=${INVALID_SECRET}`).expect(401);
});
it('rejects invalid apiKey from Authorization header (ApiKey scheme)', async () => {
return request
.post('/v1/blink')
.set('Authorization', `ApiKey ${INVALID_SECRET}`)
.expect(401);
});
it('rejects invalid apiKey from Authorization header (Bearer scheme)', async () => {
return request
.post('/v1/blink')
.set('Authorization', `Bearer ${INVALID_SECRET}`)
.expect(401);
});
});

View File

@ -0,0 +1,162 @@
import { expect } from 'chai';
import { stub, SinonStub } from 'sinon';
import * as supertest from 'supertest';
import sampleResponses = require('./data/device-api-responses.json');
import mockedAPI = require('./lib/mocked-device-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}`);
before(async () => {
await apiBinder.initialized;
await deviceState.initialized;
// Stub health checks so we can modify them whenever needed
healthCheckStubs = [
stub(apiBinder, 'healthcheck'),
stub(deviceState, 'healthcheck'),
];
// 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
await api.listen(mockedOptions.listenPort, 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 () => {
try {
await api.stop();
} catch (e) {
if (e.message !== 'Server is not running.') {
throw e;
}
}
// Restore healthcheck stubs
healthCheckStubs.forEach((hc) => hc.restore);
// Remove any test data generated
await mockedAPI.cleanUp();
(applicationManager.executeStep as SinonStub).restore();
(applicationManager.getCurrentApps as SinonStub).restore();
});
describe('GET /v1/healthy', () => {
it('returns OK because all checks pass', async () => {
// Make all healthChecks pass
healthCheckStubs.forEach((hc) => hc.resolves(true));
await request
.get('/v1/healthy')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V1.GET['/healthy'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V1.GET['/healthy'].body,
);
expect(response.text).to.deep.equal(
sampleResponses.V1.GET['/healthy'].text,
);
});
});
it('Fails because some checks did not pass', async () => {
// Make one of the healthChecks fail
healthCheckStubs[0].resolves(false);
await request
.get('/v1/healthy')
.set('Accept', 'application/json')
.expect(sampleResponses.V1.GET['/healthy [2]'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V1.GET['/healthy [2]'].body,
);
expect(response.text).to.deep.equal(
sampleResponses.V1.GET['/healthy [2]'].text,
);
});
});
});
describe('GET /v1/apps/:appId', () => {
it('returns information about a specific application', async () => {
await request
.get('/v1/apps/2')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V1.GET['/apps/2'].statusCode)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V1.GET['/apps/2'].body,
);
});
});
});
describe('POST /v1/apps/:appId/stop', () => {
it('stops a SPECIFIC application and returns a containerId', async () => {
await request
.post('/v1/apps/2/stop')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V1.GET['/apps/2/stop'].statusCode)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V1.GET['/apps/2/stop'].body,
);
});
});
});
describe('GET /v1/device', () => {
it('returns MAC address', async () => {
const response = await request
.get('/v1/device')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(200);
expect(response.body).to.have.property('mac_address').that.is.not.empty;
});
});
// TODO: add tests for V1 endpoints
});

View File

@ -0,0 +1,299 @@
import { expect } from 'chai';
import { stub, SinonStub } from 'sinon';
import * as supertest from 'supertest';
import sampleResponses = require('./data/device-api-responses.json');
import mockedAPI = require('./lib/mocked-device-api');
import * as apiBinder from '../src/api-binder';
import * as deviceState from '../src/device-state';
import SupervisorAPI from '../src/supervisor-api';
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}`);
before(async () => {
await apiBinder.initialized;
await deviceState.initialized;
// 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
await api.listen(mockedOptions.listenPort, mockedOptions.timeout);
// Create a scoped key
await apiKeys.initialized;
await apiKeys.generateCloudKey();
serviceManagerMock = stub(serviceManager, 'getAll').resolves([]);
imagesMock = stub(images, 'getStatus').resolves([]);
});
after(async () => {
try {
await api.stop();
} catch (e) {
if (e.message !== 'Server is not running.') {
throw e;
}
}
// Remove any test data generated
await mockedAPI.cleanUp();
serviceManagerMock.restore();
imagesMock.restore();
});
describe('GET /v2/device/vpn', () => {
it('returns information about VPN connection', async () => {
await request
.get('/v2/device/vpn')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect('Content-Type', /json/)
.expect(sampleResponses.V2.GET['/device/vpn'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/device/vpn'].body,
);
});
});
});
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 ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V2.GET['/applications/1/state'].statusCode)
.expect('Content-Type', /json/)
.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 ${apiKeys.cloudApiKey}`)
.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 ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V2.GET['/applications/9000/state'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/applications/9000/state'].body,
);
});
});
describe('Scoped API Keys', () => {
it('returns 409 because app is out of scope of the key', async () => {
const apiKey = await apiKeys.generateScopedKey(3, 1);
await request
.get('/v2/applications/2/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKey}`)
.expect(409);
});
});
});
describe('GET /v2/state/status', () => {
it('should return scoped application', async () => {
// 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 })]);
// Make request and evaluate response
await request
.get('/v2/state/status')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${appScopedKey}`)
.expect('Content-Type', /json/)
.expect(
sampleResponses.V2.GET['/state/status?desc=single_application']
.statusCode,
)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/state/status?desc=single_application']
.body,
);
});
});
it('should return no application info due to lack of scope', async () => {
// 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 })]);
// Make request and evaluate response
await request
.get('/v2/state/status')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${appScopedKey}`)
.expect('Content-Type', /json/)
.expect(
sampleResponses.V2.GET['/state/status?desc=no_applications']
.statusCode,
)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/state/status?desc=no_applications'].body,
);
});
});
it('should return success when device has no applications', async () => {
// Create scoped key for any application
const appScopedKey = await apiKeys.generateScopedKey(1658654, 1658654);
// Setup device conditions
serviceManagerMock.resolves([]);
imagesMock.resolves([]);
// Make request and evaluate response
await request
.get('/v2/state/status')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${appScopedKey}`)
.expect('Content-Type', /json/)
.expect(
sampleResponses.V2.GET['/state/status?desc=no_applications']
.statusCode,
)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/state/status?desc=no_applications'].body,
);
});
});
it('should only return 1 application when N > 1 applications on device', async () => {
// Create scoped key for application
const appScopedKey = await apiKeys.generateScopedKey(1658654, 640681);
// Setup device conditions
serviceManagerMock.resolves([
mockService({ appId: 1658654 }),
mockService({ appId: 222222 }),
]);
imagesMock.resolves([
mockImage({ appId: 1658654 }),
mockImage({ appId: 222222 }),
]);
// Make request and evaluate response
await request
.get('/v2/state/status')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${appScopedKey}`)
.expect('Content-Type', /json/)
.expect(
sampleResponses.V2.GET['/state/status?desc=single_application']
.statusCode,
)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/state/status?desc=single_application']
.body,
);
});
});
it('should only return 1 application when in LOCAL MODE (no auth)', async () => {
// Activate localmode
await config.set({ localMode: true });
// Setup device conditions
serviceManagerMock.resolves([
mockService({ appId: 1658654 }),
mockService({ appId: 222222 }),
]);
imagesMock.resolves([
mockImage({ appId: 1658654 }),
mockImage({ appId: 222222 }),
]);
// Make request and evaluate response
await request
.get('/v2/state/status')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(
sampleResponses.V2.GET['/state/status?desc=single_application']
.statusCode,
)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/state/status?desc=single_application']
.body,
);
});
// Deactivate localmode
await config.set({ localMode: false });
});
});
// TODO: add tests for rest of V2 endpoints
});