mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-22 02:16:43 +00:00
Migrate API tests to unit/integration
This excludes route tests or refactoring. Also, created tests for API middleware. Change-type: patch Signed-off-by: Christina Ying Wang <christina@balena.io>
This commit is contained in:
parent
636d623151
commit
532e75a77e
135
test/integration/device-api/api-keys.spec.ts
Normal file
135
test/integration/device-api/api-keys.spec.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import * as express from 'express';
|
||||
import * as request from 'supertest';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import * as config from '~/src/config';
|
||||
import * as testDb from '~/src/db';
|
||||
import * as deviceApi from '~/src/device-api';
|
||||
import * as middleware from '~/src/device-api/middleware';
|
||||
import { AuthorizedRequest } from '~/src/device-api/api-keys';
|
||||
|
||||
describe('device-api/api-keys', () => {
|
||||
let app: express.Application;
|
||||
|
||||
before(async () => {
|
||||
await config.initialized();
|
||||
app = express();
|
||||
app.use(middleware.auth);
|
||||
app.get('/test/:appId', (req: AuthorizedRequest, res) => {
|
||||
if (req.auth.isScoped({ apps: [parseInt(req.params.appId, 10)] })) {
|
||||
res.sendStatus(200);
|
||||
} else {
|
||||
res.sendStatus(401);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Delete all scoped API keys between calls to prevent leaking tests
|
||||
await testDb.models('apiSecret').whereNot({ appId: 0 }).del();
|
||||
});
|
||||
|
||||
it('should generate a key which is scoped for a single application', async () => {
|
||||
const appOneKey = await deviceApi.generateScopedKey(111, 'one');
|
||||
const appTwoKey = await deviceApi.generateScopedKey(222, 'two');
|
||||
|
||||
await request(app)
|
||||
.get('/test/111')
|
||||
.set('Authorization', `Bearer ${appOneKey}`)
|
||||
.expect(200);
|
||||
|
||||
await request(app)
|
||||
.get('/test/222')
|
||||
.set('Authorization', `Bearer ${appTwoKey}`)
|
||||
.expect(200);
|
||||
|
||||
await request(app)
|
||||
.get('/test/222')
|
||||
.set('Authorization', `Bearer ${appOneKey}`)
|
||||
.expect(401);
|
||||
|
||||
await request(app)
|
||||
.get('/test/111')
|
||||
.set('Authorization', `Bearer ${appTwoKey}`)
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it('should generate a key which is scoped for multiple applications', async () => {
|
||||
const multiAppKey = await deviceApi.generateScopedKey(111, 'three', {
|
||||
scopes: [111, 222].map((appId) => ({ type: 'app', appId })),
|
||||
});
|
||||
|
||||
await request(app)
|
||||
.get('/test/111')
|
||||
.set('Authorization', `Bearer ${multiAppKey}`)
|
||||
.expect(200);
|
||||
|
||||
await request(app)
|
||||
.get('/test/222')
|
||||
.set('Authorization', `Bearer ${multiAppKey}`)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should generate a key which is scoped for all applications', async () => {
|
||||
await request(app)
|
||||
.get('/test/111')
|
||||
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||
.expect(200);
|
||||
|
||||
await request(app)
|
||||
.get('/test/222')
|
||||
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should have a cached lookup of key scopes', async () => {
|
||||
const globalScopes = await deviceApi.getScopesForKey(
|
||||
await deviceApi.getGlobalApiKey(),
|
||||
);
|
||||
|
||||
const key = 'my-new-key';
|
||||
await testDb
|
||||
.models('apiSecret')
|
||||
.where({ key: await deviceApi.getGlobalApiKey() })
|
||||
.update({ key });
|
||||
|
||||
// Key has been changed, but cache should retain the old key
|
||||
expect(
|
||||
await deviceApi.getScopesForKey(await deviceApi.getGlobalApiKey()),
|
||||
).to.deep.equal(globalScopes);
|
||||
|
||||
// Bust the cache and generate a new global API key
|
||||
const refreshedKey = await deviceApi.refreshKey(
|
||||
await deviceApi.getGlobalApiKey(),
|
||||
);
|
||||
|
||||
// Key that we changed in db is no longer valid
|
||||
expect(await deviceApi.getScopesForKey(key)).to.be.null;
|
||||
|
||||
// Refreshed key should have the global scopes
|
||||
expect(await deviceApi.getScopesForKey(refreshedKey)).to.deep.equal(
|
||||
globalScopes,
|
||||
);
|
||||
});
|
||||
|
||||
it('should regenerate a key and invalidate the old one', async () => {
|
||||
const appScopedKey = await deviceApi.generateScopedKey(111, 'four');
|
||||
|
||||
await request(app)
|
||||
.get('/test/111')
|
||||
.set('Authorization', `Bearer ${appScopedKey}`)
|
||||
.expect(200);
|
||||
|
||||
const newScopedKey = await deviceApi.refreshKey(appScopedKey);
|
||||
|
||||
await request(app)
|
||||
.get('/test/111')
|
||||
.set('Authorization', `Bearer ${appScopedKey}`)
|
||||
.expect(401);
|
||||
|
||||
await request(app)
|
||||
.get('/test/111')
|
||||
.set('Authorization', `Bearer ${newScopedKey}`)
|
||||
.expect(200);
|
||||
});
|
||||
});
|
29
test/integration/device-api/index.spec.ts
Normal file
29
test/integration/device-api/index.spec.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import * as express from 'express';
|
||||
import * as request from 'supertest';
|
||||
|
||||
import * as deviceApi from '~/src/device-api';
|
||||
|
||||
describe('device-api/index', () => {
|
||||
let api: express.Application;
|
||||
|
||||
before(async () => {
|
||||
api = new deviceApi.SupervisorAPI({
|
||||
routers: [],
|
||||
healthchecks: [],
|
||||
// @ts-expect-error
|
||||
}).api;
|
||||
// Express app set in SupervisorAPI is private here
|
||||
// but we need to access it for supertest
|
||||
});
|
||||
|
||||
describe('/ping', () => {
|
||||
it('responds with 200 regardless of auth', async () => {
|
||||
await request(api).get('/ping').expect(200);
|
||||
|
||||
await request(api)
|
||||
.get('/ping')
|
||||
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
});
|
102
test/integration/device-api/middleware.spec.ts
Normal file
102
test/integration/device-api/middleware.spec.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import * as express from 'express';
|
||||
import * as request from 'supertest';
|
||||
|
||||
import * as config from '~/src/config';
|
||||
import * as testDb from '~/src/db';
|
||||
import * as deviceApi from '~/src/device-api';
|
||||
import * as middleware from '~/src/device-api/middleware';
|
||||
|
||||
describe('device-api/middleware', () => {
|
||||
let app: express.Application;
|
||||
|
||||
before(async () => {
|
||||
await config.initialized();
|
||||
});
|
||||
|
||||
describe('auth', () => {
|
||||
const INVALID_KEY = 'bad_api_secret';
|
||||
|
||||
before(() => {
|
||||
app = express();
|
||||
app.use(middleware.auth);
|
||||
app.get('/', (_req, res) => res.sendStatus(200));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Delete all API keys between calls to prevent leaking tests
|
||||
await testDb.models('apiSecret').del();
|
||||
// Reset local mode to default
|
||||
await config.set({ localMode: false });
|
||||
});
|
||||
|
||||
it('responds with 401 if no API key', async () => {
|
||||
await request(app).get('/').expect(401);
|
||||
});
|
||||
|
||||
it('validates API key from request query', async () => {
|
||||
await request(app)
|
||||
.get(`/?apikey=${await deviceApi.getGlobalApiKey()}`)
|
||||
.expect(200);
|
||||
|
||||
await request(app).get(`/?apikey=${INVALID_KEY}`).expect(401);
|
||||
|
||||
// Should not accept case insensitive scheme
|
||||
const cases = ['ApiKey', 'apiKey', 'APIKEY', 'ApIKeY'];
|
||||
for (const query of cases) {
|
||||
await request(app)
|
||||
.get(`/?${query}=${await deviceApi.getGlobalApiKey()}`)
|
||||
.expect(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('validates API key from Authorization header with ApiKey scheme', async () => {
|
||||
// Should accept case insensitive scheme
|
||||
const cases = ['ApiKey', 'apikey', 'APIKEY', 'ApIKeY'];
|
||||
for (const scheme of cases) {
|
||||
await request(app)
|
||||
.get('/')
|
||||
.set(
|
||||
'Authorization',
|
||||
`${scheme} ${await deviceApi.getGlobalApiKey()}`,
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
await request(app)
|
||||
.get('/')
|
||||
.set('Authorization', `${scheme} ${INVALID_KEY}`)
|
||||
.expect(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('finds API key from Authorization header with Bearer scheme', async () => {
|
||||
// Should accept case insensitive scheme
|
||||
const cases: string[] = ['Bearer', 'bearer', 'BEARER', 'BeAReR'];
|
||||
for (const scheme of cases) {
|
||||
await request(app)
|
||||
.get('/')
|
||||
.set(
|
||||
'Authorization',
|
||||
`${scheme} ${await deviceApi.getGlobalApiKey()}`,
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
await request(app)
|
||||
.get('/')
|
||||
.set('Authorization', `${scheme} ${INVALID_KEY}`)
|
||||
.expect(401);
|
||||
}
|
||||
});
|
||||
|
||||
it("doesn't validate auth in local mode", async () => {
|
||||
const testRequest = async (code: number) =>
|
||||
await request(app)
|
||||
.get('/')
|
||||
.set('Authorization', `Bearer ${INVALID_KEY}`)
|
||||
.expect(code);
|
||||
|
||||
await testRequest(401);
|
||||
await config.set({ localMode: true });
|
||||
await testRequest(200);
|
||||
});
|
||||
});
|
||||
});
|
@ -51,7 +51,6 @@ describe('device-state', () => {
|
||||
).to.not.be.rejected;
|
||||
await loadTargetFromFile(appsJson);
|
||||
const targetState = await deviceState.getTarget();
|
||||
// console.log('TARGET', JSON.stringify(targetState, null, 2));
|
||||
await expect(
|
||||
fs.access(appsJsonBackup(appsJson)),
|
||||
'apps.json.preloaded is created after loading the target',
|
||||
|
@ -6,7 +6,7 @@ import { testfs } from 'mocha-pod';
|
||||
import { expect } from 'chai';
|
||||
import Log from '~/lib/supervisor-console';
|
||||
import * as network from '~/src/network';
|
||||
import * as constants from '~/src/lib/constants';
|
||||
import * as constants from '~/lib/constants';
|
||||
|
||||
describe('network', () => {
|
||||
it('checks VPN connection status', async () => {
|
||||
|
@ -5,7 +5,7 @@ import * as _ from 'lodash';
|
||||
import * as apiBinder from '~/src/api-binder';
|
||||
import * as applicationManager from '~/src/compose/application-manager';
|
||||
import * as deviceState from '~/src/device-state';
|
||||
import * as constants from '~/src/lib/constants';
|
||||
import * as constants from '~/lib/constants';
|
||||
import { docker } from '~/lib/docker-utils';
|
||||
import { Supervisor } from '~/src/supervisor';
|
||||
|
||||
|
@ -1,288 +0,0 @@
|
||||
import { expect } from 'chai';
|
||||
import { spy } from 'sinon';
|
||||
import * as supertest from 'supertest';
|
||||
|
||||
import mockedAPI = require('~/test-lib/mocked-device-api');
|
||||
import * as apiBinder from '~/src/api-binder';
|
||||
import * as deviceState from '~/src/device-state';
|
||||
import Log from '~/lib/supervisor-console';
|
||||
import * as db from '~/src/db';
|
||||
import SupervisorAPI from '~/src/device-api';
|
||||
import * as deviceApi from '~/src/device-api';
|
||||
|
||||
const mockedOptions = {
|
||||
listenPort: 54321,
|
||||
timeout: 30000,
|
||||
};
|
||||
|
||||
describe('SupervisorAPI', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
try {
|
||||
await api.stop();
|
||||
} catch (e: any) {
|
||||
if (e.message !== 'Server is not running.') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
// 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 ${await deviceApi.getGlobalApiKey()}`)
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('API Key Scope', () => {
|
||||
it('should generate a key which is scoped for a single application', async () => {
|
||||
// single app scoped key...
|
||||
const appScopedKey = await deviceApi.generateScopedKey(1, 'main');
|
||||
|
||||
await request
|
||||
.get('/v2/applications/1/state')
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', `Bearer ${appScopedKey}`)
|
||||
.expect(200);
|
||||
});
|
||||
it('should generate a key which is scoped for multiple applications', async () => {
|
||||
// multi-app scoped key...
|
||||
const multiAppScopedKey = await deviceApi.generateScopedKey(1, 'other', {
|
||||
scopes: [1, 2].map((appId) => {
|
||||
return { type: 'app', appId };
|
||||
}),
|
||||
});
|
||||
|
||||
await request
|
||||
.get('/v2/applications/1/state')
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', `Bearer ${multiAppScopedKey}`)
|
||||
.expect(200);
|
||||
|
||||
await request
|
||||
.get('/v2/applications/2/state')
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', `Bearer ${multiAppScopedKey}`)
|
||||
.expect(200);
|
||||
});
|
||||
it('should generate a key which is scoped for all applications', async () => {
|
||||
await request
|
||||
.get('/v2/applications/1/state')
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||
.expect(200);
|
||||
|
||||
await request
|
||||
.get('/v2/applications/2/state')
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||
.expect(200);
|
||||
});
|
||||
it('should have a cached lookup of the key scopes to save DB loading', async () => {
|
||||
const scopes = await deviceApi.getScopesForKey(
|
||||
await deviceApi.getGlobalApiKey(),
|
||||
);
|
||||
|
||||
const key = 'not-a-normal-key';
|
||||
await db.initialized();
|
||||
await db
|
||||
.models('apiSecret')
|
||||
.update({
|
||||
key,
|
||||
})
|
||||
.where({
|
||||
key: await deviceApi.getGlobalApiKey(),
|
||||
});
|
||||
|
||||
// the key we had is now gone, but the cache should return values
|
||||
const cachedScopes = await deviceApi.getScopesForKey(
|
||||
await deviceApi.getGlobalApiKey(),
|
||||
);
|
||||
expect(cachedScopes).to.deep.equal(scopes);
|
||||
|
||||
// this should bust the cache...
|
||||
await deviceApi.refreshKey(await deviceApi.getGlobalApiKey());
|
||||
|
||||
// the key we changed should be gone now, and the new key should have the cloud scopes
|
||||
const missingScopes = await deviceApi.getScopesForKey(key);
|
||||
const freshScopes = await deviceApi.getScopesForKey(
|
||||
await deviceApi.getGlobalApiKey(),
|
||||
);
|
||||
|
||||
expect(missingScopes).to.be.null;
|
||||
expect(freshScopes).to.deep.equal(scopes);
|
||||
});
|
||||
it('should regenerate a key and invalidate the old one', async () => {
|
||||
// single app scoped key...
|
||||
const appScopedKey = await deviceApi.generateScopedKey(1, 'main');
|
||||
|
||||
await request
|
||||
.get('/v2/applications/1/state')
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', `Bearer ${appScopedKey}`)
|
||||
.expect(200);
|
||||
|
||||
const newScopedKey = await deviceApi.refreshKey(appScopedKey);
|
||||
|
||||
await request
|
||||
.get('/v2/applications/1/state')
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', `Bearer ${appScopedKey}`)
|
||||
.expect(401);
|
||||
|
||||
await request
|
||||
.get('/v2/applications/1/state')
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', `Bearer ${newScopedKey}`)
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('State change logging', () => {
|
||||
before(() => {
|
||||
// Spy on functions we will be testing
|
||||
spy(Log, 'info');
|
||||
spy(Log, 'error');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Start each case with API stopped
|
||||
try {
|
||||
await api.stop();
|
||||
} catch (e: any) {
|
||||
if (e.message !== 'Server is not running.') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
// @ts-expect-error
|
||||
Log.info.restore();
|
||||
// @ts-expect-error
|
||||
Log.error.restore();
|
||||
// Resume API for other test suites
|
||||
return api.listen(mockedOptions.listenPort, mockedOptions.timeout);
|
||||
});
|
||||
|
||||
it('logs successful start', async () => {
|
||||
// Start API
|
||||
await api.listen(mockedOptions.listenPort, mockedOptions.timeout);
|
||||
// Check if success start was logged
|
||||
// @ts-expect-error
|
||||
expect(Log.info.lastCall?.lastArg).to.equal(
|
||||
`Supervisor API successfully started on port ${mockedOptions.listenPort}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('logs shutdown', async () => {
|
||||
// Start API
|
||||
await api.listen(mockedOptions.listenPort, mockedOptions.timeout);
|
||||
// Stop API
|
||||
await api.stop();
|
||||
// Check if stopped with info was logged
|
||||
// @ts-expect-error
|
||||
expect(Log.info.lastCall?.lastArg).to.equal('Stopped Supervisor API');
|
||||
});
|
||||
|
||||
it('logs errored shutdown', async () => {
|
||||
// Start API
|
||||
await api.listen(mockedOptions.listenPort, mockedOptions.timeout);
|
||||
// Stop API with error
|
||||
await api.stop({ errored: true });
|
||||
// Check if stopped with error was logged
|
||||
// @ts-expect-error
|
||||
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=${await deviceApi.getGlobalApiKey()}`)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('finds apiKey from Authorization header (ApiKey scheme)', async () => {
|
||||
return request
|
||||
.post('/v1/blink')
|
||||
.set('Authorization', `ApiKey ${await deviceApi.getGlobalApiKey()}`)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('finds apiKey from Authorization header (Bearer scheme)', async () => {
|
||||
return request
|
||||
.post('/v1/blink')
|
||||
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||
.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} ${await deviceApi.getGlobalApiKey()}`,
|
||||
)
|
||||
.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);
|
||||
});
|
||||
});
|
||||
});
|
@ -3,7 +3,7 @@
|
||||
import { stub, SinonStub } from 'sinon';
|
||||
import * as dbus from 'dbus';
|
||||
import { Error as DBusError, DBusInterface } from 'dbus';
|
||||
import { initialized } from '~/src/lib/dbus';
|
||||
import { initialized } from '~/lib/dbus';
|
||||
|
||||
let getBusStub: SinonStub;
|
||||
|
||||
|
@ -17,6 +17,7 @@ export const mochaHooks = {
|
||||
sinon.stub(log, 'success');
|
||||
sinon.stub(log, 'event');
|
||||
sinon.stub(log, 'error');
|
||||
sinon.stub(log, 'api');
|
||||
},
|
||||
|
||||
afterEach() {
|
||||
@ -26,6 +27,7 @@ export const mochaHooks = {
|
||||
(log.success as sinon.SinonStub).reset();
|
||||
(log.event as sinon.SinonStub).reset();
|
||||
(log.error as sinon.SinonStub).reset();
|
||||
(log.api as sinon.SinonStub).reset();
|
||||
},
|
||||
|
||||
afterAll() {
|
||||
@ -35,5 +37,6 @@ export const mochaHooks = {
|
||||
(log.success as sinon.SinonStub).restore();
|
||||
(log.event as sinon.SinonStub).restore();
|
||||
(log.error as sinon.SinonStub).restore();
|
||||
(log.api as sinon.SinonStub).restore();
|
||||
},
|
||||
};
|
||||
|
51
test/unit/device-api/middleware.spec.ts
Normal file
51
test/unit/device-api/middleware.spec.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import * as express from 'express';
|
||||
import * as request from 'supertest';
|
||||
import { SinonStub } from 'sinon';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import * as middleware from '~/src/device-api/middleware';
|
||||
import { UpdatesLockedError } from '~/lib/errors';
|
||||
import log from '~/lib/supervisor-console';
|
||||
|
||||
describe('device-api/middleware', () => {
|
||||
let app: express.Application;
|
||||
|
||||
describe('errors', () => {
|
||||
before(() => {
|
||||
app = express();
|
||||
app.get('/locked', (_req, _res) => {
|
||||
throw new UpdatesLockedError();
|
||||
});
|
||||
app.get('/errored', (_req, _res) => {
|
||||
throw new Error();
|
||||
});
|
||||
app.use(middleware.errors);
|
||||
});
|
||||
|
||||
it('responds with 423 if updates are locked', async () => {
|
||||
await request(app).get('/locked').expect(423);
|
||||
});
|
||||
|
||||
it('responds with 503 if any other error', async () => {
|
||||
await request(app).get('/errored').expect(503);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logging', () => {
|
||||
before(() => {
|
||||
app = express();
|
||||
app.use(middleware.logging);
|
||||
app.get('/', (_req, res) => res.sendStatus(200));
|
||||
app.post('/', (_req, res) => res.sendStatus(304));
|
||||
(log.api as SinonStub).reset();
|
||||
});
|
||||
|
||||
it('logs API request methods and status codes', async () => {
|
||||
await request(app).get('/');
|
||||
expect((log.api as SinonStub).lastCall?.firstArg).to.match(/get.*200/i);
|
||||
|
||||
await request(app).post('/');
|
||||
expect((log.api as SinonStub).lastCall?.firstArg).to.match(/post.*304/i);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user