mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-23 13:46:45 +02:00
Refactor routes setup, move test files, cleanup legacy
This commit is contained in:
parent
5771bcb1bd
commit
be4852f63a
13
lib/app.js
13
lib/app.js
@ -68,14 +68,11 @@ module.exports = function(config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Setup API routes
|
// Setup API routes
|
||||||
const apiRouter = express.Router(); // eslint-disable-line new-cap
|
const middleware = routes.router(config);
|
||||||
routes.createAPI(apiRouter, config);
|
if (!middleware) {
|
||||||
app.use(`${baseUriPath}/api/`, apiRouter);
|
throw new Error('Routes invalid');
|
||||||
|
}
|
||||||
// Setup deprecated routes
|
app.use(`${baseUriPath}/`, middleware);
|
||||||
const router = express.Router(); // eslint-disable-line new-cap
|
|
||||||
routes.createLegacy(router, config);
|
|
||||||
app.use(baseUriPath, router);
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
app.use(errorHandler());
|
app.use(errorHandler());
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const { test } = require('ava');
|
||||||
|
const express = require('express');
|
||||||
const proxyquire = require('proxyquire');
|
const proxyquire = require('proxyquire');
|
||||||
const getApp = proxyquire('./app', {
|
const getApp = proxyquire('./app', {
|
||||||
'./routes': {
|
'./routes': {
|
||||||
createAPI: () => {},
|
router: () => express.Router(),
|
||||||
createLegacy: () => {},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
0
lib/routes/admin-api/applications.js
Normal file
0
lib/routes/admin-api/applications.js
Normal file
39
lib/routes/admin-api/applications.test.js
Normal file
39
lib/routes/admin-api/applications.test.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { test } = require('ava');
|
||||||
|
const store = require('./../../../test/fixtures/store');
|
||||||
|
const supertest = require('supertest');
|
||||||
|
const logger = require('../../logger');
|
||||||
|
const getApp = require('../../app');
|
||||||
|
|
||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const eventBus = new EventEmitter();
|
||||||
|
|
||||||
|
test.beforeEach(() => {
|
||||||
|
logger.setLevel('FATAL');
|
||||||
|
});
|
||||||
|
|
||||||
|
function getSetup() {
|
||||||
|
const stores = store.createStores();
|
||||||
|
const app = getApp({
|
||||||
|
baseUriPath: '',
|
||||||
|
stores,
|
||||||
|
eventBus,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
request: supertest(app),
|
||||||
|
stores,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should return list of client applications', t => {
|
||||||
|
t.plan(1);
|
||||||
|
const { request } = getSetup();
|
||||||
|
return request
|
||||||
|
.get('/api/admin/metrics/applications')
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.true(res.body.applications.length === 0);
|
||||||
|
});
|
||||||
|
});
|
46
lib/routes/admin-api/archive.js
Normal file
46
lib/routes/admin-api/archive.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Router } = require('express');
|
||||||
|
|
||||||
|
const logger = require('../../logger');
|
||||||
|
const { FEATURE_REVIVED } = require('../../event-type');
|
||||||
|
const ValidationError = require('../../error/validation-error');
|
||||||
|
const validateRequest = require('../../error/validate-request');
|
||||||
|
|
||||||
|
const handleErrors = (req, res, error) => {
|
||||||
|
switch (error.constructor) {
|
||||||
|
case ValidationError:
|
||||||
|
return res.status(400).json(req.validationErrors()).end();
|
||||||
|
default:
|
||||||
|
logger.error('Server failed executing request', error);
|
||||||
|
return res.status(500).end();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.router = function(config) {
|
||||||
|
const { featureToggleStore, eventStore } = config.stores;
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/features', (req, res) => {
|
||||||
|
featureToggleStore.getArchivedFeatures().then(archivedFeatures => {
|
||||||
|
res.json({ features: archivedFeatures });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/revive/:name', (req, res) => {
|
||||||
|
req.checkParams('name', 'Name is required').notEmpty();
|
||||||
|
|
||||||
|
validateRequest(req)
|
||||||
|
.then(() =>
|
||||||
|
eventStore.store({
|
||||||
|
type: FEATURE_REVIVED,
|
||||||
|
createdBy: req.connection.remoteAddress,
|
||||||
|
data: { name: req.params.name },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(() => res.status(200).end())
|
||||||
|
.catch(error => handleErrors(req, res, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
5
lib/routes/admin-api/archive.test.js
Normal file
5
lib/routes/admin-api/archive.test.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { test } = require('ava');
|
||||||
|
|
||||||
|
test.todo('should unit test archive');
|
@ -1,19 +1,22 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const eventDiffer = require('../event-differ');
|
const { Router } = require('express');
|
||||||
|
|
||||||
|
const eventDiffer = require('../../event-differ');
|
||||||
const version = 1;
|
const version = 1;
|
||||||
|
|
||||||
module.exports = function (app, config) {
|
module.exports.router = function(config) {
|
||||||
const { eventStore } = config.stores;
|
const { eventStore } = config.stores;
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
app.get('/events', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
eventStore.getEvents().then(events => {
|
eventStore.getEvents().then(events => {
|
||||||
eventDiffer.addDiffs(events);
|
eventDiffer.addDiffs(events);
|
||||||
res.json({ version, events });
|
res.json({ version, events });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/events/:name', (req, res) => {
|
router.get('/:name', (req, res) => {
|
||||||
const toggleName = req.params.name;
|
const toggleName = req.params.name;
|
||||||
eventStore.getEventsFilterByName(toggleName).then(events => {
|
eventStore.getEventsFilterByName(toggleName).then(events => {
|
||||||
if (events) {
|
if (events) {
|
||||||
@ -27,4 +30,6 @@ module.exports = function (app, config) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
};
|
};
|
39
lib/routes/admin-api/events.test.js
Normal file
39
lib/routes/admin-api/events.test.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { test } = require('ava');
|
||||||
|
|
||||||
|
const store = require('./../../../test/fixtures/store');
|
||||||
|
const supertest = require('supertest');
|
||||||
|
const logger = require('../../logger');
|
||||||
|
const getApp = require('../../app');
|
||||||
|
|
||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const eventBus = new EventEmitter();
|
||||||
|
|
||||||
|
function getSetup() {
|
||||||
|
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||||
|
const stores = store.createStores();
|
||||||
|
const app = getApp({
|
||||||
|
baseUriPath: base,
|
||||||
|
stores,
|
||||||
|
eventBus,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { base, eventStore: stores.eventStore, request: supertest(app) };
|
||||||
|
}
|
||||||
|
|
||||||
|
test.beforeEach(() => {
|
||||||
|
logger.setLevel('FATAL');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get empty events list via admin', t => {
|
||||||
|
t.plan(1);
|
||||||
|
const { request, base } = getSetup();
|
||||||
|
return request
|
||||||
|
.get(`${base}/api/admin/events`)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.true(res.body.events.length === 0);
|
||||||
|
});
|
||||||
|
});
|
196
lib/routes/admin-api/feature.js
Normal file
196
lib/routes/admin-api/feature.js
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Router } = require('express');
|
||||||
|
const joi = require('joi');
|
||||||
|
|
||||||
|
const logger = require('../../logger');
|
||||||
|
const {
|
||||||
|
FEATURE_CREATED,
|
||||||
|
FEATURE_UPDATED,
|
||||||
|
FEATURE_ARCHIVED,
|
||||||
|
} = require('../../event-type');
|
||||||
|
const NameExistsError = require('../../error/name-exists-error');
|
||||||
|
const NotFoundError = require('../../error/notfound-error');
|
||||||
|
const ValidationError = require('../../error/validation-error.js');
|
||||||
|
const validateRequest = require('../../error/validate-request');
|
||||||
|
const extractUser = require('../../extract-user');
|
||||||
|
|
||||||
|
const handleErrors = (req, res, error) => {
|
||||||
|
logger.warn('Error creating or updating feature', error);
|
||||||
|
switch (error.constructor) {
|
||||||
|
case NotFoundError:
|
||||||
|
return res.status(404).end();
|
||||||
|
case NameExistsError:
|
||||||
|
return res
|
||||||
|
.status(403)
|
||||||
|
.json([
|
||||||
|
{
|
||||||
|
msg:
|
||||||
|
'A feature with this name already exists. Try re-activating it from the archive.',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.end();
|
||||||
|
case ValidationError:
|
||||||
|
return res.status(400).json(req.validationErrors()).end();
|
||||||
|
default:
|
||||||
|
logger.error('Server failed executing request', error);
|
||||||
|
return res.status(500).end();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const strategiesSchema = joi.object().keys({
|
||||||
|
name: joi.string().regex(/^[a-zA-Z0-9\\.\\-]{3,100}$/).required(),
|
||||||
|
parameters: joi.object(),
|
||||||
|
});
|
||||||
|
|
||||||
|
function validateStrategy(featureToggle) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (
|
||||||
|
!featureToggle.strategies ||
|
||||||
|
featureToggle.strategies.length === 0
|
||||||
|
) {
|
||||||
|
return reject(
|
||||||
|
new ValidationError('You must define at least one strategy')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
featureToggle.strategies = featureToggle.strategies.map(
|
||||||
|
strategyConfig => {
|
||||||
|
const result = joi.validate(strategyConfig, strategiesSchema);
|
||||||
|
if (result.error) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return resolve(featureToggle);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = 1;
|
||||||
|
|
||||||
|
module.exports.router = function(config) {
|
||||||
|
const { featureToggleStore, eventStore } = config.stores;
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
featureToggleStore
|
||||||
|
.getFeatures()
|
||||||
|
.then(features => res.json({ version, features }));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:featureName', (req, res) => {
|
||||||
|
featureToggleStore
|
||||||
|
.getFeature(req.params.featureName)
|
||||||
|
.then(feature => res.json(feature).end())
|
||||||
|
.catch(() =>
|
||||||
|
res.status(404).json({ error: 'Could not find feature' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function validateUniqueName(req) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
featureToggleStore
|
||||||
|
.getFeature(req.body.name)
|
||||||
|
.then(() =>
|
||||||
|
reject(new NameExistsError('Feature name already exist'))
|
||||||
|
)
|
||||||
|
.catch(() => resolve(req));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post('/validate', (req, res) => {
|
||||||
|
req.checkBody('name', 'Name is required').notEmpty();
|
||||||
|
req
|
||||||
|
.checkBody('name', 'Name must match format ^[0-9a-zA-Z\\.\\-]+$')
|
||||||
|
.matches(/^[0-9a-zA-Z\\.\\-]+$/i);
|
||||||
|
|
||||||
|
validateRequest(req)
|
||||||
|
.then(validateUniqueName)
|
||||||
|
.then(() => res.status(201).end())
|
||||||
|
.catch(error => handleErrors(req, res, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', (req, res) => {
|
||||||
|
req.checkBody('name', 'Name is required').notEmpty();
|
||||||
|
req
|
||||||
|
.checkBody('name', 'Name must match format ^[0-9a-zA-Z\\.\\-]+$')
|
||||||
|
.matches(/^[0-9a-zA-Z\\.\\-]+$/i);
|
||||||
|
const userName = extractUser(req);
|
||||||
|
|
||||||
|
validateRequest(req)
|
||||||
|
.then(validateUniqueName)
|
||||||
|
.then(_req => _req.body)
|
||||||
|
.then(validateStrategy)
|
||||||
|
.then(featureToggle =>
|
||||||
|
eventStore.store({
|
||||||
|
type: FEATURE_CREATED,
|
||||||
|
createdBy: userName,
|
||||||
|
data: featureToggle,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(() => res.status(201).end())
|
||||||
|
.catch(error => handleErrors(req, res, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/:featureName', (req, res) => {
|
||||||
|
const featureName = req.params.featureName;
|
||||||
|
const userName = extractUser(req);
|
||||||
|
const updatedFeature = req.body;
|
||||||
|
|
||||||
|
updatedFeature.name = featureName;
|
||||||
|
|
||||||
|
featureToggleStore
|
||||||
|
.getFeature(featureName)
|
||||||
|
.then(() => validateStrategy(updatedFeature))
|
||||||
|
.then(() =>
|
||||||
|
eventStore.store({
|
||||||
|
type: FEATURE_UPDATED,
|
||||||
|
createdBy: userName,
|
||||||
|
data: updatedFeature,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(() => res.status(200).end())
|
||||||
|
.catch(error => handleErrors(req, res, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/:featureName/toggle', (req, res) => {
|
||||||
|
const featureName = req.params.featureName;
|
||||||
|
const userName = extractUser(req);
|
||||||
|
|
||||||
|
featureToggleStore
|
||||||
|
.getFeature(featureName)
|
||||||
|
.then(feature => {
|
||||||
|
feature.enabled = !feature.enabled;
|
||||||
|
return eventStore.store({
|
||||||
|
type: FEATURE_UPDATED,
|
||||||
|
createdBy: userName,
|
||||||
|
data: feature,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => res.status(200).end())
|
||||||
|
.catch(error => handleErrors(req, res, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/:featureName', (req, res) => {
|
||||||
|
const featureName = req.params.featureName;
|
||||||
|
const userName = extractUser(req);
|
||||||
|
|
||||||
|
featureToggleStore
|
||||||
|
.getFeature(featureName)
|
||||||
|
.then(() =>
|
||||||
|
eventStore.store({
|
||||||
|
type: FEATURE_ARCHIVED,
|
||||||
|
createdBy: userName,
|
||||||
|
data: {
|
||||||
|
name: featureName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(() => res.status(200).end())
|
||||||
|
.catch(error => handleErrors(req, res, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
@ -1,10 +1,10 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const { test } = require('ava');
|
||||||
const store = require('./fixtures/store');
|
const store = require('./../../../test/fixtures/store');
|
||||||
const supertest = require('supertest');
|
const supertest = require('supertest');
|
||||||
const logger = require('../../../lib/logger');
|
const logger = require('../../logger');
|
||||||
const getApp = require('../../../lib/app');
|
const getApp = require('../../app');
|
||||||
|
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
@ -29,60 +29,74 @@ function getSetup () {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('should get empty getFeatures', t => {
|
test('should get empty getFeatures via admin', t => {
|
||||||
|
t.plan(1);
|
||||||
const { request, base } = getSetup();
|
const { request, base } = getSetup();
|
||||||
return request
|
return request
|
||||||
.get(`${base}/features`)
|
.get(`${base}/api/admin/features`)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect(res => {
|
||||||
t.true(res.body.features.length === 0);
|
t.true(res.body.features.length === 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should get one getFeature', t => {
|
test('should get one getFeature', t => {
|
||||||
|
t.plan(1);
|
||||||
const { request, featureToggleStore, base } = getSetup();
|
const { request, featureToggleStore, base } = getSetup();
|
||||||
featureToggleStore.addFeature({ name: 'test_', strategies: [{ name: 'default_' }] });
|
featureToggleStore.addFeature({
|
||||||
|
name: 'test_',
|
||||||
|
strategies: [{ name: 'default_' }],
|
||||||
|
});
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.get(`${base}/features`)
|
.get(`${base}/api/admin/features`)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect(res => {
|
||||||
t.true(res.body.features.length === 1);
|
t.true(res.body.features.length === 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should add version numbers for /features', t => {
|
test('should add version numbers for /features', t => {
|
||||||
|
t.plan(1);
|
||||||
const { request, featureToggleStore, base } = getSetup();
|
const { request, featureToggleStore, base } = getSetup();
|
||||||
featureToggleStore.addFeature({ name: 'test2', strategies: [{ name: 'default' }] });
|
featureToggleStore.addFeature({
|
||||||
|
name: 'test2',
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.get(`${base}/features`)
|
.get(`${base}/api/admin/features`)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect(res => {
|
||||||
t.true(res.body.version === 1);
|
t.true(res.body.version === 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should require at least one strategy when creating a feature toggle', t => {
|
test('should require at least one strategy when creating a feature toggle', t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, base } = getSetup();
|
const { request, base } = getSetup();
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.post(`${base}/features`)
|
.post(`${base}/api/admin/features`)
|
||||||
.send({ name: 'sample.missing.strategy' })
|
.send({ name: 'sample.missing.strategy' })
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(400)
|
.expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should require at least one strategy when updating a feature toggle', t => {
|
test('should require at least one strategy when updating a feature toggle', t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, featureToggleStore, base } = getSetup();
|
const { request, featureToggleStore, base } = getSetup();
|
||||||
featureToggleStore.addFeature({ name: 'ts', strategies: [{ name: 'default' }] });
|
featureToggleStore.addFeature({
|
||||||
|
name: 'ts',
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.put(`${base}/features/ts`)
|
.put(`${base}/api/admin/features/ts`)
|
||||||
.send({ name: 'ts' })
|
.send({ name: 'ts' })
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(400)
|
.expect(400);
|
||||||
});
|
});
|
38
lib/routes/admin-api/index.js
Normal file
38
lib/routes/admin-api/index.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Router } = require('express');
|
||||||
|
|
||||||
|
const features = require('./feature.js');
|
||||||
|
const featureArchive = require('./archive.js');
|
||||||
|
const events = require('./event.js');
|
||||||
|
const strategies = require('./strategy');
|
||||||
|
const metrics = require('./metrics');
|
||||||
|
|
||||||
|
const apiDef = {
|
||||||
|
version: 2,
|
||||||
|
links: {
|
||||||
|
'feature-toggles': { uri: '/api/admin/features' },
|
||||||
|
'feature-archive': { uri: '/api/admin/archive' },
|
||||||
|
strategies: { uri: '/api/admin/strategies' },
|
||||||
|
events: { uri: '/api/admin/events' },
|
||||||
|
metrics: { uri: '/api/admin/metrics' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.apiDef = apiDef;
|
||||||
|
|
||||||
|
exports.router = config => {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
res.json(apiDef);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use('/features', features.router(config));
|
||||||
|
router.use('/archive', featureArchive.router(config));
|
||||||
|
router.use('/strategies', strategies.router(config));
|
||||||
|
router.use('/events', events.router(config));
|
||||||
|
router.use('/metrics', metrics.router(config));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
137
lib/routes/admin-api/metrics.js
Normal file
137
lib/routes/admin-api/metrics.js
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Router } = require('express');
|
||||||
|
|
||||||
|
const logger = require('../../logger');
|
||||||
|
const ClientMetrics = require('../../client-metrics');
|
||||||
|
const { catchLogAndSendErrorResponse } = require('./route-utils');
|
||||||
|
|
||||||
|
exports.router = function(config) {
|
||||||
|
const {
|
||||||
|
clientMetricsStore,
|
||||||
|
clientInstanceStore,
|
||||||
|
clientApplicationsStore,
|
||||||
|
strategyStore,
|
||||||
|
featureToggleStore,
|
||||||
|
} = config.stores;
|
||||||
|
|
||||||
|
const metrics = new ClientMetrics(clientMetricsStore);
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/seen-toggles', (req, res) => {
|
||||||
|
const seenAppToggles = metrics.getAppsWithToggles();
|
||||||
|
res.json(seenAppToggles);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/seen-apps', (req, res) => {
|
||||||
|
const seenApps = metrics.getSeenAppsPerToggle();
|
||||||
|
clientApplicationsStore
|
||||||
|
.getApplications()
|
||||||
|
.then(toLookup)
|
||||||
|
.then(metaData => {
|
||||||
|
Object.keys(seenApps).forEach(key => {
|
||||||
|
seenApps[key] = seenApps[key].map(entry => {
|
||||||
|
if (metaData[entry.appName]) {
|
||||||
|
return Object.assign(
|
||||||
|
{},
|
||||||
|
entry,
|
||||||
|
metaData[entry.appName]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
res.json(seenApps);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/feature-toggles', (req, res) => {
|
||||||
|
res.json(metrics.getTogglesMetrics());
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/feature-toggles/:name', (req, res) => {
|
||||||
|
const name = req.params.name;
|
||||||
|
const data = metrics.getTogglesMetrics();
|
||||||
|
const lastHour = data.lastHour[name] || {};
|
||||||
|
const lastMinute = data.lastMinute[name] || {};
|
||||||
|
res.json({
|
||||||
|
lastHour,
|
||||||
|
lastMinute,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/applications/:appName', (req, res) => {
|
||||||
|
const input = Object.assign({}, req.body, {
|
||||||
|
appName: req.params.appName,
|
||||||
|
});
|
||||||
|
clientApplicationsStore
|
||||||
|
.upsert(input)
|
||||||
|
.then(() => res.status(202).end())
|
||||||
|
.catch(e => {
|
||||||
|
logger.error(e);
|
||||||
|
res.status(500).end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function toLookup(metaData) {
|
||||||
|
return metaData.reduce((result, entry) => {
|
||||||
|
result[entry.appName] = entry;
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get('/applications/', (req, res) => {
|
||||||
|
clientApplicationsStore
|
||||||
|
.getApplications(req.query)
|
||||||
|
.then(applications => res.json({ applications }))
|
||||||
|
.catch(err => catchLogAndSendErrorResponse(err, res));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/applications/:appName', (req, res) => {
|
||||||
|
const appName = req.params.appName;
|
||||||
|
const seenToggles = metrics.getSeenTogglesByAppName(appName);
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
clientApplicationsStore.getApplication(appName),
|
||||||
|
clientInstanceStore.getByAppName(appName),
|
||||||
|
strategyStore.getStrategies(),
|
||||||
|
featureToggleStore.getFeatures(),
|
||||||
|
])
|
||||||
|
.then(([application, instances, strategies, features]) => {
|
||||||
|
const appDetails = {
|
||||||
|
appName: application.appName,
|
||||||
|
createdAt: application.createdAt,
|
||||||
|
description: application.description,
|
||||||
|
url: application.url,
|
||||||
|
color: application.color,
|
||||||
|
icon: application.icon,
|
||||||
|
strategies: application.strategies.map(name => {
|
||||||
|
const found = strategies.find(
|
||||||
|
feature => feature.name === name
|
||||||
|
);
|
||||||
|
if (found) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
return { name, notFound: true };
|
||||||
|
}),
|
||||||
|
instances,
|
||||||
|
seenToggles: seenToggles.map(name => {
|
||||||
|
const found = features.find(
|
||||||
|
feature => feature.name === name
|
||||||
|
);
|
||||||
|
if (found) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
return { name, notFound: true };
|
||||||
|
}),
|
||||||
|
links: {
|
||||||
|
self: `/api/applications/${application.appName}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.json(appDetails);
|
||||||
|
})
|
||||||
|
.catch(err => catchLogAndSendErrorResponse(err, res));
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
100
lib/routes/admin-api/metrics.test.js
Normal file
100
lib/routes/admin-api/metrics.test.js
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { test } = require('ava');
|
||||||
|
const store = require('./../../../test/fixtures/store');
|
||||||
|
const supertest = require('supertest');
|
||||||
|
const logger = require('../../logger');
|
||||||
|
const getApp = require('../../app');
|
||||||
|
|
||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const eventBus = new EventEmitter();
|
||||||
|
|
||||||
|
test.beforeEach(() => {
|
||||||
|
logger.setLevel('FATAL');
|
||||||
|
});
|
||||||
|
|
||||||
|
function getSetup() {
|
||||||
|
const stores = store.createStores();
|
||||||
|
const app = getApp({
|
||||||
|
baseUriPath: '',
|
||||||
|
stores,
|
||||||
|
eventBus,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
request: supertest(app),
|
||||||
|
stores,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should return seen toggles even when there is nothing', t => {
|
||||||
|
t.plan(1);
|
||||||
|
const { request } = getSetup();
|
||||||
|
return request
|
||||||
|
.get('/api/admin/metrics/seen-toggles')
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.true(res.body.length === 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return list of seen-toggles per app', t => {
|
||||||
|
t.plan(3);
|
||||||
|
const { request, stores } = getSetup();
|
||||||
|
const appName = 'asd!23';
|
||||||
|
stores.clientMetricsStore.emit('metrics', {
|
||||||
|
appName,
|
||||||
|
instanceId: 'instanceId',
|
||||||
|
bucket: {
|
||||||
|
start: new Date(),
|
||||||
|
stop: new Date(),
|
||||||
|
toggles: {
|
||||||
|
toggleX: { yes: 123, no: 0 },
|
||||||
|
toggleY: { yes: 123, no: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return request
|
||||||
|
.get('/api/admin/metrics/seen-toggles')
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
const seenAppsWithToggles = res.body;
|
||||||
|
t.true(seenAppsWithToggles.length === 1);
|
||||||
|
t.true(seenAppsWithToggles[0].appName === appName);
|
||||||
|
t.true(seenAppsWithToggles[0].seenToggles.length === 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return feature-toggles metrics even when there is nothing', t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request } = getSetup();
|
||||||
|
return request.get('/api/admin/metrics/feature-toggles').expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return metrics for all toggles', t => {
|
||||||
|
t.plan(2);
|
||||||
|
const { request, stores } = getSetup();
|
||||||
|
const appName = 'asd!23';
|
||||||
|
stores.clientMetricsStore.emit('metrics', {
|
||||||
|
appName,
|
||||||
|
instanceId: 'instanceId',
|
||||||
|
bucket: {
|
||||||
|
start: new Date(),
|
||||||
|
stop: new Date(),
|
||||||
|
toggles: {
|
||||||
|
toggleX: { yes: 123, no: 0 },
|
||||||
|
toggleY: { yes: 123, no: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return request
|
||||||
|
.get('/api/admin/metrics/feature-toggles')
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
const metrics = res.body;
|
||||||
|
t.true(metrics.lastHour !== undefined);
|
||||||
|
t.true(metrics.lastMinute !== undefined);
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const logger = require('../logger');
|
const logger = require('../../logger');
|
||||||
|
|
||||||
const catchLogAndSendErrorResponse = (err, res) => {
|
const catchLogAndSendErrorResponse = (err, res) => {
|
||||||
logger.error(err);
|
logger.error(err);
|
@ -3,18 +3,16 @@
|
|||||||
const joi = require('joi');
|
const joi = require('joi');
|
||||||
|
|
||||||
const strategySchema = joi.object().keys({
|
const strategySchema = joi.object().keys({
|
||||||
name: joi.string()
|
name: joi.string().regex(/^[a-zA-Z0-9\\.\\-]{3,100}$/).required(),
|
||||||
.regex(/^[a-zA-Z0-9\\.\\-]{3,100}$/)
|
|
||||||
.required(),
|
|
||||||
description: joi.string(),
|
description: joi.string(),
|
||||||
parameters: joi.array()
|
parameters: joi.array().required().items(
|
||||||
.required()
|
joi.object().keys({
|
||||||
.items(joi.object().keys({
|
|
||||||
name: joi.string().required(),
|
name: joi.string().required(),
|
||||||
type: joi.string().required(),
|
type: joi.string().required(),
|
||||||
description: joi.string().allow(''),
|
description: joi.string().allow(''),
|
||||||
required: joi.boolean(),
|
required: joi.boolean(),
|
||||||
})),
|
})
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = strategySchema;
|
module.exports = strategySchema;
|
131
lib/routes/admin-api/strategy.js
Normal file
131
lib/routes/admin-api/strategy.js
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Router } = require('express');
|
||||||
|
const joi = require('joi');
|
||||||
|
|
||||||
|
const eventType = require('../../event-type');
|
||||||
|
const logger = require('../../logger');
|
||||||
|
const NameExistsError = require('../../error/name-exists-error');
|
||||||
|
const extractUser = require('../../extract-user');
|
||||||
|
const strategySchema = require('./strategy-schema');
|
||||||
|
const version = 1;
|
||||||
|
|
||||||
|
const handleError = (req, res, error) => {
|
||||||
|
logger.warn('Error creating or updating strategy', error);
|
||||||
|
switch (error.name) {
|
||||||
|
case 'NotFoundError':
|
||||||
|
return res.status(404).end();
|
||||||
|
case 'NameExistsError':
|
||||||
|
return res
|
||||||
|
.status(403)
|
||||||
|
.json([
|
||||||
|
{
|
||||||
|
msg: `A strategy named '${req.body
|
||||||
|
.name}' already exists.`,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.end();
|
||||||
|
case 'ValidationError':
|
||||||
|
return res.status(400).json(error).end();
|
||||||
|
default:
|
||||||
|
logger.error('Could perfom operation', error);
|
||||||
|
return res.status(500).end();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.router = function(config) {
|
||||||
|
const { strategyStore, eventStore } = config.stores;
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
strategyStore.getStrategies().then(strategies => {
|
||||||
|
res.json({ version, strategies });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:name', (req, res) => {
|
||||||
|
strategyStore
|
||||||
|
.getStrategy(req.params.name)
|
||||||
|
.then(strategy => res.json(strategy).end())
|
||||||
|
.catch(() =>
|
||||||
|
res.status(404).json({ error: 'Could not find strategy' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/:name', (req, res) => {
|
||||||
|
const strategyName = req.params.name;
|
||||||
|
|
||||||
|
strategyStore
|
||||||
|
.getStrategy(strategyName)
|
||||||
|
.then(() =>
|
||||||
|
eventStore.store({
|
||||||
|
type: eventType.STRATEGY_DELETED,
|
||||||
|
createdBy: extractUser(req),
|
||||||
|
data: {
|
||||||
|
name: strategyName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(() => res.status(200).end())
|
||||||
|
.catch(error => handleError(req, res, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', (req, res) => {
|
||||||
|
const data = req.body;
|
||||||
|
validateInput(data)
|
||||||
|
.then(validateStrategyName)
|
||||||
|
.then(newStrategy =>
|
||||||
|
eventStore.store({
|
||||||
|
type: eventType.STRATEGY_CREATED,
|
||||||
|
createdBy: extractUser(req),
|
||||||
|
data: newStrategy,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(() => res.status(201).end())
|
||||||
|
.catch(error => handleError(req, res, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/:strategyName', (req, res) => {
|
||||||
|
const strategyName = req.params.strategyName;
|
||||||
|
const updatedStrategy = req.body;
|
||||||
|
|
||||||
|
updatedStrategy.name = strategyName;
|
||||||
|
|
||||||
|
strategyStore
|
||||||
|
.getStrategy(strategyName)
|
||||||
|
.then(() => validateInput(updatedStrategy))
|
||||||
|
.then(() =>
|
||||||
|
eventStore.store({
|
||||||
|
type: eventType.STRATEGY_UPDATED,
|
||||||
|
createdBy: extractUser(req),
|
||||||
|
data: updatedStrategy,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(() => res.status(200).end())
|
||||||
|
.catch(error => handleError(req, res, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
function validateStrategyName(data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
strategyStore
|
||||||
|
.getStrategy(data.name)
|
||||||
|
.then(() =>
|
||||||
|
reject(new NameExistsError('Feature name already exist'))
|
||||||
|
)
|
||||||
|
.catch(() => resolve(data));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateInput(data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
joi.validate(data, strategySchema, (err, cleaned) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
return resolve(cleaned);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
@ -1,9 +1,10 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const { test } = require('ava');
|
||||||
const store = require('./fixtures/store');
|
const store = require('./../../../test/fixtures/store');
|
||||||
const supertest = require('supertest');
|
const supertest = require('supertest');
|
||||||
const getApp = require('../../../lib/app');
|
const getApp = require('../../app');
|
||||||
|
const logger = require('../../logger');
|
||||||
|
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
@ -24,89 +25,101 @@ function getSetup () {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test.beforeEach(() => {
|
||||||
|
logger.setLevel('FATAL');
|
||||||
|
});
|
||||||
|
|
||||||
test('should add version numbers for /stategies', t => {
|
test('should add version numbers for /stategies', t => {
|
||||||
|
t.plan(1);
|
||||||
const { request, base } = getSetup();
|
const { request, base } = getSetup();
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.get(`${base}/api/strategies`)
|
.get(`${base}/api/admin/strategies`)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect(res => {
|
||||||
t.true(res.body.version === 1);
|
t.true(res.body.version === 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should require a name when creating a new stratey', t => {
|
test('should require a name when creating a new stratey', t => {
|
||||||
|
t.plan(1);
|
||||||
const { request, base } = getSetup();
|
const { request, base } = getSetup();
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.post(`${base}/api/strategies`)
|
.post(`${base}/api/admin/strategies`)
|
||||||
.send({})
|
.send({})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
.expect((res) => {
|
.expect(res => {
|
||||||
t.true(res.body.name === 'ValidationError');
|
t.true(res.body.name === 'ValidationError');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should require parameters array when creating a new stratey', t => {
|
test('should require parameters array when creating a new stratey', t => {
|
||||||
|
t.plan(1);
|
||||||
const { request, base } = getSetup();
|
const { request, base } = getSetup();
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.post(`${base}/api/strategies`)
|
.post(`${base}/api/admin/strategies`)
|
||||||
.send({ name: 'TestStrat' })
|
.send({ name: 'TestStrat' })
|
||||||
.expect(400)
|
.expect(400)
|
||||||
.expect((res) => {
|
.expect(res => {
|
||||||
t.true(res.body.name === 'ValidationError');
|
t.true(res.body.name === 'ValidationError');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create a new stratey with empty parameters', () => {
|
test('should create a new stratey with empty parameters', t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, base } = getSetup();
|
const { request, base } = getSetup();
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.post(`${base}/api/strategies`)
|
.post(`${base}/api/admin/strategies`)
|
||||||
.send({ name: 'TestStrat', parameters: [] })
|
.send({ name: 'TestStrat', parameters: [] })
|
||||||
.expect(201);
|
.expect(201);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not be possible to override name', () => {
|
test('should not be possible to override name', t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, base, strategyStore } = getSetup();
|
const { request, base, strategyStore } = getSetup();
|
||||||
strategyStore.addStrategy({ name: 'Testing', parameters: [] });
|
strategyStore.addStrategy({ name: 'Testing', parameters: [] });
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.post(`${base}/api/strategies`)
|
.post(`${base}/api/admin/strategies`)
|
||||||
.send({ name: 'Testing', parameters: [] })
|
.send({ name: 'Testing', parameters: [] })
|
||||||
.expect(403);
|
.expect(403);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should update strategy', () => {
|
test('should update strategy', t => {
|
||||||
|
t.plan(0);
|
||||||
const name = 'AnotherStrat';
|
const name = 'AnotherStrat';
|
||||||
const { request, base, strategyStore } = getSetup();
|
const { request, base, strategyStore } = getSetup();
|
||||||
strategyStore.addStrategy({ name, parameters: [] });
|
strategyStore.addStrategy({ name, parameters: [] });
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.put(`${base}/api/strategies/${name}`)
|
.put(`${base}/api/admin/strategies/${name}`)
|
||||||
.send({ name, parameters: [], description: 'added' })
|
.send({ name, parameters: [], description: 'added' })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not update uknown strategy', () => {
|
test('should not update uknown strategy', t => {
|
||||||
|
t.plan(0);
|
||||||
const name = 'UnknownStrat';
|
const name = 'UnknownStrat';
|
||||||
const { request, base } = getSetup();
|
const { request, base } = getSetup();
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.put(`${base}/api/strategies/${name}`)
|
.put(`${base}/api/admin/strategies/${name}`)
|
||||||
.send({ name, parameters: [], description: 'added' })
|
.send({ name, parameters: [], description: 'added' })
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should validate format when updating strategy', () => {
|
test('should validate format when updating strategy', t => {
|
||||||
|
t.plan(0);
|
||||||
const name = 'AnotherStrat';
|
const name = 'AnotherStrat';
|
||||||
const { request, base, strategyStore } = getSetup();
|
const { request, base, strategyStore } = getSetup();
|
||||||
strategyStore.addStrategy({ name, parameters: [] });
|
strategyStore.addStrategy({ name, parameters: [] });
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.put(`${base}/api/strategies/${name}`)
|
.put(`${base}/api/admin/strategies/${name}`)
|
||||||
.send({})
|
.send({})
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
@ -1,19 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const apiDef = {
|
|
||||||
version: 1,
|
|
||||||
links: {
|
|
||||||
'feature-toggles': { uri: '/api/features' },
|
|
||||||
'strategies': { uri: '/api/strategies' },
|
|
||||||
'events': { uri: '/api/events' },
|
|
||||||
'client-register': { uri: '/api/client/register' },
|
|
||||||
'client-metrics': { uri: '/api/client/register' },
|
|
||||||
'seen-toggles': { uri: '/api/client/seen-toggles' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = (app) => {
|
|
||||||
app.get('/', (req, res) => {
|
|
||||||
res.json(apiDef);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,12 +1,17 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const { Router } = require('express');
|
||||||
const prometheusRegister = require('prom-client/lib/register');
|
const prometheusRegister = require('prom-client/lib/register');
|
||||||
|
|
||||||
module.exports = function (app, config) {
|
exports.router = config => {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
if (config.serverMetrics) {
|
if (config.serverMetrics) {
|
||||||
app.get('/internal-backstage/prometheus', (req, res) => {
|
router.get('/prometheus', (req, res) => {
|
||||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||||
res.end(prometheusRegister.metrics());
|
res.end(prometheusRegister.metrics());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return router;
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const { test } = require('ava');
|
||||||
const store = require('./fixtures/store');
|
const store = require('./../../test/fixtures/store');
|
||||||
const supertest = require('supertest');
|
const supertest = require('supertest');
|
||||||
const logger = require('../../../lib/logger');
|
const logger = require('../logger');
|
||||||
const getApp = require('../../../lib/app');
|
const getApp = require('../app');
|
||||||
|
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
@ -14,6 +14,7 @@ test.beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should use enable prometheus', t => {
|
test('should use enable prometheus', t => {
|
||||||
|
t.plan(0);
|
||||||
const stores = store.createStores();
|
const stores = store.createStores();
|
||||||
const app = getApp({
|
const app = getApp({
|
||||||
baseUriPath: '',
|
baseUriPath: '',
|
||||||
@ -27,5 +28,5 @@ test('should use enable prometheus', t => {
|
|||||||
return request
|
return request
|
||||||
.get('/internal-backstage/prometheus')
|
.get('/internal-backstage/prometheus')
|
||||||
.expect('Content-Type', /text/)
|
.expect('Content-Type', /text/)
|
||||||
.expect(200)
|
.expect(200);
|
||||||
});
|
});
|
18
lib/routes/client-api/feature.js
Normal file
18
lib/routes/client-api/feature.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Router } = require('express');
|
||||||
|
|
||||||
|
const version = 1;
|
||||||
|
|
||||||
|
exports.router = config => {
|
||||||
|
const router = Router();
|
||||||
|
const { featureToggleStore } = config.stores;
|
||||||
|
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
featureToggleStore
|
||||||
|
.getFeatures()
|
||||||
|
.then(features => res.json({ version, features }));
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
@ -1,10 +1,10 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const { test } = require('ava');
|
||||||
const store = require('./fixtures/store');
|
const store = require('./../../../test/fixtures/store');
|
||||||
const supertest = require('supertest');
|
const supertest = require('supertest');
|
||||||
const logger = require('../../../lib/logger');
|
const logger = require('../../logger');
|
||||||
const getApp = require('../../../lib/app');
|
const getApp = require('../../app');
|
||||||
|
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
@ -29,13 +29,14 @@ function getSetup () {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('should get api defintion', t => {
|
test('should get empty getFeatures via client', t => {
|
||||||
|
t.plan(1);
|
||||||
const { request, base } = getSetup();
|
const { request, base } = getSetup();
|
||||||
return request
|
return request
|
||||||
.get(`${base}/api/`)
|
.get(`${base}/api/client/features`)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect(res => {
|
||||||
t.true(res.body.links['feature-toggles'].uri === '/api/features');
|
t.true(res.body.features.length === 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
30
lib/routes/client-api/index.js
Normal file
30
lib/routes/client-api/index.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Router } = require('express');
|
||||||
|
const features = require('./feature.js');
|
||||||
|
const metrics = require('./metrics.js');
|
||||||
|
const register = require('./register.js');
|
||||||
|
|
||||||
|
const apiDef = {
|
||||||
|
version: 2,
|
||||||
|
links: {
|
||||||
|
'feature-toggles': { uri: '/api/client/features' },
|
||||||
|
register: { uri: '/api/client/register' },
|
||||||
|
metrics: { uri: '/api/client/metrics' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.apiDef = apiDef;
|
||||||
|
|
||||||
|
exports.router = config => {
|
||||||
|
const router = Router();
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
res.json(apiDef);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use('/features', features.router(config));
|
||||||
|
router.use('/metrics', metrics.router(config));
|
||||||
|
router.use('/register', register.router(config));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
@ -5,8 +5,7 @@ const joi = require('joi');
|
|||||||
const clientMetricsSchema = joi.object().keys({
|
const clientMetricsSchema = joi.object().keys({
|
||||||
appName: joi.string().required(),
|
appName: joi.string().required(),
|
||||||
instanceId: joi.string().required(),
|
instanceId: joi.string().required(),
|
||||||
bucket: joi.object().required()
|
bucket: joi.object().required().keys({
|
||||||
.keys({
|
|
||||||
start: joi.date().required(),
|
start: joi.date().required(),
|
||||||
stop: joi.date().required(),
|
stop: joi.date().required(),
|
||||||
toggles: joi.object(),
|
toggles: joi.object(),
|
||||||
@ -16,9 +15,7 @@ const clientMetricsSchema = joi.object().keys({
|
|||||||
const clientRegisterSchema = joi.object().keys({
|
const clientRegisterSchema = joi.object().keys({
|
||||||
appName: joi.string().required(),
|
appName: joi.string().required(),
|
||||||
instanceId: joi.string().required(),
|
instanceId: joi.string().required(),
|
||||||
strategies: joi.array()
|
strategies: joi.array().required().items(joi.string(), joi.any().strip()),
|
||||||
.required()
|
|
||||||
.items(joi.string(), joi.any().strip()),
|
|
||||||
started: joi.date().required(),
|
started: joi.date().required(),
|
||||||
interval: joi.number().required(),
|
interval: joi.number().required(),
|
||||||
});
|
});
|
39
lib/routes/client-api/metrics.js
Normal file
39
lib/routes/client-api/metrics.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Router } = require('express');
|
||||||
|
const joi = require('joi');
|
||||||
|
const logger = require('../../logger');
|
||||||
|
|
||||||
|
const { clientMetricsSchema } = require('./metrics-schema');
|
||||||
|
|
||||||
|
exports.router = config => {
|
||||||
|
const { clientMetricsStore, clientInstanceStore } = config.stores;
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post('/', (req, res) => {
|
||||||
|
const data = req.body;
|
||||||
|
const clientIp = req.ip;
|
||||||
|
|
||||||
|
joi.validate(data, clientMetricsSchema, (err, cleaned) => {
|
||||||
|
if (err) {
|
||||||
|
logger.warn('Invalid metrics posted', err);
|
||||||
|
return res.status(400).json(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
clientMetricsStore
|
||||||
|
.insert(cleaned)
|
||||||
|
.then(() =>
|
||||||
|
clientInstanceStore.insert({
|
||||||
|
appName: cleaned.appName,
|
||||||
|
instanceId: cleaned.instanceId,
|
||||||
|
clientIp,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(err => logger.error('failed to store metrics', err));
|
||||||
|
|
||||||
|
res.status(202).end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
54
lib/routes/client-api/metrics.test.js
Normal file
54
lib/routes/client-api/metrics.test.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { test } = require('ava');
|
||||||
|
const store = require('./../../../test/fixtures/store');
|
||||||
|
const supertest = require('supertest');
|
||||||
|
const logger = require('../../logger');
|
||||||
|
const getApp = require('../../app');
|
||||||
|
|
||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const eventBus = new EventEmitter();
|
||||||
|
|
||||||
|
test.beforeEach(() => {
|
||||||
|
logger.setLevel('FATAL');
|
||||||
|
});
|
||||||
|
|
||||||
|
function getSetup() {
|
||||||
|
const stores = store.createStores();
|
||||||
|
const app = getApp({
|
||||||
|
baseUriPath: '',
|
||||||
|
stores,
|
||||||
|
eventBus,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
request: supertest(app),
|
||||||
|
stores,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should validate client metrics', t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request } = getSetup();
|
||||||
|
return request
|
||||||
|
.post('/api/client/metrics')
|
||||||
|
.send({ random: 'blush' })
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should accept client metrics', t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request } = getSetup();
|
||||||
|
return request
|
||||||
|
.post('/api/client/metrics')
|
||||||
|
.send({
|
||||||
|
appName: 'demo',
|
||||||
|
instanceId: '1',
|
||||||
|
bucket: {
|
||||||
|
start: Date.now(),
|
||||||
|
stop: Date.now(),
|
||||||
|
toggles: {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.expect(202);
|
||||||
|
});
|
38
lib/routes/client-api/register.js
Normal file
38
lib/routes/client-api/register.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Router } = require('express');
|
||||||
|
const joi = require('joi');
|
||||||
|
const logger = require('../../logger');
|
||||||
|
|
||||||
|
const { clientRegisterSchema } = require('./metrics-schema');
|
||||||
|
|
||||||
|
exports.router = config => {
|
||||||
|
const { clientInstanceStore, clientApplicationsStore } = config.stores;
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post('/', (req, res) => {
|
||||||
|
const data = req.body;
|
||||||
|
|
||||||
|
joi.validate(data, clientRegisterSchema, (err, clientRegistration) => {
|
||||||
|
if (err) {
|
||||||
|
logger.warn('Invalid client data posted', err);
|
||||||
|
return res.status(400).json(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
clientRegistration.clientIp = req.ip;
|
||||||
|
|
||||||
|
clientApplicationsStore
|
||||||
|
.upsert(clientRegistration)
|
||||||
|
.then(() => clientInstanceStore.insert(clientRegistration))
|
||||||
|
.then(() =>
|
||||||
|
logger.info(`New client registered with
|
||||||
|
appName=${clientRegistration.appName} and instanceId=${clientRegistration.instanceId}`)
|
||||||
|
)
|
||||||
|
.catch(err => logger.error('failed to register client', err));
|
||||||
|
|
||||||
|
res.status(202).end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
64
lib/routes/client-api/register.test.js
Normal file
64
lib/routes/client-api/register.test.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { test } = require('ava');
|
||||||
|
const store = require('./../../../test/fixtures/store');
|
||||||
|
const supertest = require('supertest');
|
||||||
|
const logger = require('../../logger');
|
||||||
|
const getApp = require('../../app');
|
||||||
|
|
||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const eventBus = new EventEmitter();
|
||||||
|
|
||||||
|
test.beforeEach(() => {
|
||||||
|
logger.setLevel('FATAL');
|
||||||
|
});
|
||||||
|
|
||||||
|
function getSetup() {
|
||||||
|
const stores = store.createStores();
|
||||||
|
const app = getApp({
|
||||||
|
baseUriPath: '',
|
||||||
|
stores,
|
||||||
|
eventBus,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
request: supertest(app),
|
||||||
|
stores,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should register client', t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request } = getSetup();
|
||||||
|
return request
|
||||||
|
.post('/api/client/register')
|
||||||
|
.send({
|
||||||
|
appName: 'demo',
|
||||||
|
instanceId: 'test',
|
||||||
|
strategies: ['default'],
|
||||||
|
started: Date.now(),
|
||||||
|
interval: 10,
|
||||||
|
})
|
||||||
|
.expect(202);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should require appName field', t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request } = getSetup();
|
||||||
|
return request.post('/api/client/register').expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should require strategies field', t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request } = getSetup();
|
||||||
|
return request
|
||||||
|
.post('/api/client/register')
|
||||||
|
.send({
|
||||||
|
appName: 'demo',
|
||||||
|
instanceId: 'test',
|
||||||
|
// strategies: ['default'],
|
||||||
|
started: Date.now(),
|
||||||
|
interval: 10,
|
||||||
|
})
|
||||||
|
.expect(400);
|
||||||
|
});
|
@ -1,44 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const logger = require('../logger');
|
|
||||||
const { FEATURE_REVIVED } = require('../event-type');
|
|
||||||
const ValidationError = require('../error/validation-error');
|
|
||||||
const validateRequest = require('../error/validate-request');
|
|
||||||
|
|
||||||
const handleErrors = (req, res, error) => {
|
|
||||||
switch (error.constructor) {
|
|
||||||
case ValidationError:
|
|
||||||
return res
|
|
||||||
.status(400)
|
|
||||||
.json(req.validationErrors())
|
|
||||||
.end();
|
|
||||||
default:
|
|
||||||
logger.error('Server failed executing request', error);
|
|
||||||
return res
|
|
||||||
.status(500)
|
|
||||||
.end();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = function (app, config) {
|
|
||||||
const { featureToggleStore, eventStore } = config.stores;
|
|
||||||
|
|
||||||
app.get('/archive/features', (req, res) => {
|
|
||||||
featureToggleStore.getArchivedFeatures().then(archivedFeatures => {
|
|
||||||
res.json({ features: archivedFeatures });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/archive/revive/:name', (req, res) => {
|
|
||||||
req.checkParams('name', 'Name is required').notEmpty();
|
|
||||||
|
|
||||||
validateRequest(req)
|
|
||||||
.then(() => eventStore.store({
|
|
||||||
type: FEATURE_REVIVED,
|
|
||||||
createdBy: req.connection.remoteAddress,
|
|
||||||
data: { name: req.params.name },
|
|
||||||
}))
|
|
||||||
.then(() => res.status(200).end())
|
|
||||||
.catch(error => handleErrors(req, res, error));
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,178 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const joi = require('joi');
|
|
||||||
const logger = require('../logger');
|
|
||||||
const { FEATURE_CREATED, FEATURE_UPDATED, FEATURE_ARCHIVED } = require('../event-type');
|
|
||||||
const NameExistsError = require('../error/name-exists-error');
|
|
||||||
const NotFoundError = require('../error/notfound-error');
|
|
||||||
const ValidationError = require('../error/validation-error.js');
|
|
||||||
const validateRequest = require('../error/validate-request');
|
|
||||||
const extractUser = require('../extract-user');
|
|
||||||
|
|
||||||
const legacyFeatureMapper = require('../data-helper/legacy-feature-mapper');
|
|
||||||
const version = 1;
|
|
||||||
|
|
||||||
const handleErrors = (req, res, error) => {
|
|
||||||
logger.warn('Error creating or updating feature', error);
|
|
||||||
switch (error.constructor) {
|
|
||||||
case NotFoundError:
|
|
||||||
return res
|
|
||||||
.status(404)
|
|
||||||
.end();
|
|
||||||
case NameExistsError:
|
|
||||||
return res
|
|
||||||
.status(403)
|
|
||||||
.json([{ msg: 'A feature with this name already exists. Try re-activating it from the archive.' }])
|
|
||||||
.end();
|
|
||||||
case ValidationError:
|
|
||||||
return res
|
|
||||||
.status(400)
|
|
||||||
.json(req.validationErrors())
|
|
||||||
.end();
|
|
||||||
default:
|
|
||||||
logger.error('Server failed executing request', error);
|
|
||||||
return res
|
|
||||||
.status(500)
|
|
||||||
.end();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = function (app, config) {
|
|
||||||
const { featureToggleStore, eventStore } = config.stores;
|
|
||||||
|
|
||||||
app.get('/features', (req, res) => {
|
|
||||||
featureToggleStore.getFeatures()
|
|
||||||
.then((features) => features.map(legacyFeatureMapper.addOldFields))
|
|
||||||
.then(features => res.json({ version, features }));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/features/:featureName', (req, res) => {
|
|
||||||
featureToggleStore.getFeature(req.params.featureName)
|
|
||||||
.then(legacyFeatureMapper.addOldFields)
|
|
||||||
.then(feature => res.json(feature).end())
|
|
||||||
.catch(() => res.status(404).json({ error: 'Could not find feature' }));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/features-validate', (req, res) => {
|
|
||||||
req.checkBody('name', 'Name is required').notEmpty();
|
|
||||||
req.checkBody('name', 'Name must match format ^[0-9a-zA-Z\\.\\-]+$').matches(/^[0-9a-zA-Z\\.\\-]+$/i);
|
|
||||||
|
|
||||||
validateRequest(req)
|
|
||||||
.then(validateFormat)
|
|
||||||
.then(validateUniqueName)
|
|
||||||
.then(() => res.status(201).end())
|
|
||||||
.catch(error => handleErrors(req, res, error));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/features', (req, res) => {
|
|
||||||
req.checkBody('name', 'Name is required').notEmpty();
|
|
||||||
req.checkBody('name', 'Name must match format ^[0-9a-zA-Z\\.\\-]+$').matches(/^[0-9a-zA-Z\\.\\-]+$/i);
|
|
||||||
const userName = extractUser(req);
|
|
||||||
|
|
||||||
validateRequest(req)
|
|
||||||
.then(validateFormat)
|
|
||||||
.then(validateUniqueName)
|
|
||||||
.then((_req) => legacyFeatureMapper.toNewFormat(_req.body))
|
|
||||||
.then(validateStrategy)
|
|
||||||
.then((featureToggle) => eventStore.store({
|
|
||||||
type: FEATURE_CREATED,
|
|
||||||
createdBy: userName,
|
|
||||||
data: featureToggle,
|
|
||||||
}))
|
|
||||||
.then(() => res.status(201).end())
|
|
||||||
.catch(error => handleErrors(req, res, error));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.put('/features/:featureName', (req, res) => {
|
|
||||||
const featureName = req.params.featureName;
|
|
||||||
const userName = extractUser(req);
|
|
||||||
const updatedFeature = legacyFeatureMapper.toNewFormat(req.body);
|
|
||||||
|
|
||||||
updatedFeature.name = featureName;
|
|
||||||
|
|
||||||
featureToggleStore.getFeature(featureName)
|
|
||||||
.then(() => validateStrategy(updatedFeature))
|
|
||||||
.then(() => eventStore.store({
|
|
||||||
type: FEATURE_UPDATED,
|
|
||||||
createdBy: userName,
|
|
||||||
data: updatedFeature,
|
|
||||||
}))
|
|
||||||
.then(() => res.status(200).end())
|
|
||||||
.catch(error => handleErrors(req, res, error));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/features/:featureName/toggle', (req, res) => {
|
|
||||||
const featureName = req.params.featureName;
|
|
||||||
const userName = extractUser(req);
|
|
||||||
|
|
||||||
featureToggleStore.getFeature(featureName)
|
|
||||||
.then((feature) => {
|
|
||||||
feature.enabled = !feature.enabled;
|
|
||||||
return eventStore.store({
|
|
||||||
type: FEATURE_UPDATED,
|
|
||||||
createdBy: userName,
|
|
||||||
data: feature,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(() => res.status(200).end())
|
|
||||||
.catch(error => handleErrors(req, res, error));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.delete('/features/:featureName', (req, res) => {
|
|
||||||
const featureName = req.params.featureName;
|
|
||||||
const userName = extractUser(req);
|
|
||||||
|
|
||||||
featureToggleStore.getFeature(featureName)
|
|
||||||
.then(() => eventStore.store({
|
|
||||||
type: FEATURE_ARCHIVED,
|
|
||||||
createdBy: userName,
|
|
||||||
data: {
|
|
||||||
name: featureName,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
.then(() => res.status(200).end())
|
|
||||||
.catch(error => handleErrors(req, res, error));
|
|
||||||
});
|
|
||||||
|
|
||||||
function validateUniqueName (req) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
featureToggleStore.getFeature(req.body.name)
|
|
||||||
.then(() => reject(new NameExistsError('Feature name already exist')))
|
|
||||||
.catch(() => resolve(req));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateFormat (req) {
|
|
||||||
if (req.body.strategy && req.body.strategies) {
|
|
||||||
return Promise.reject(new ValidationError('Cannot use both "strategy" and "strategies".'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const strategiesSchema = joi.object().keys({
|
|
||||||
name: joi.string()
|
|
||||||
.regex(/^[a-zA-Z0-9\\.\\-]{3,100}$/)
|
|
||||||
.required(),
|
|
||||||
parameters: joi.object(),
|
|
||||||
});
|
|
||||||
|
|
||||||
function validateStrategy (featureToggle) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!featureToggle.strategies || featureToggle.strategies.length === 0) {
|
|
||||||
return reject(new ValidationError('You must define at least one strategy'));
|
|
||||||
}
|
|
||||||
|
|
||||||
featureToggle.strategies = featureToggle.strategies.map((strategyConfig) => {
|
|
||||||
const result = joi.validate(strategyConfig, strategiesSchema);
|
|
||||||
if (result.error) {
|
|
||||||
throw result.error;
|
|
||||||
}
|
|
||||||
return result.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return resolve(featureToggle);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,15 +1,24 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
|
const { Router } = require('express');
|
||||||
|
|
||||||
module.exports = function (app, config) {
|
exports.router = function(config) {
|
||||||
app.get('/health', (req, res) => {
|
const router = Router();
|
||||||
config.stores.db.select(1)
|
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
config.stores.db
|
||||||
|
.select(1)
|
||||||
.from('features')
|
.from('features')
|
||||||
.then(() => res.json({ health: 'GOOD' }))
|
.then(() => res.json({ health: 'GOOD' }))
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
logger.error('Could not select from features, error was: ', err);
|
logger.error(
|
||||||
|
'Could not select from features, error was: ',
|
||||||
|
err
|
||||||
|
);
|
||||||
res.status(500).json({ health: 'BAD' });
|
res.status(500).json({ health: 'BAD' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const { test } = require('ava');
|
||||||
const store = require('./fixtures/store');
|
const store = require('./../../test/fixtures/store');
|
||||||
const supertest = require('supertest');
|
const supertest = require('supertest');
|
||||||
const logger = require('../../../lib/logger');
|
const logger = require('../logger');
|
||||||
const getApp = require('../../../lib/app');
|
const getApp = require('../app');
|
||||||
|
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
@ -13,7 +13,6 @@ test.beforeEach(() => {
|
|||||||
logger.setLevel('FATAL');
|
logger.setLevel('FATAL');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function getSetup() {
|
function getSetup() {
|
||||||
const stores = store.createStores();
|
const stores = store.createStores();
|
||||||
const db = stores.db;
|
const db = stores.db;
|
||||||
@ -30,34 +29,28 @@ function getSetup () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test('should give 500 when db is failing', t => {
|
test('should give 500 when db is failing', t => {
|
||||||
|
t.plan(2);
|
||||||
const { request, db } = getSetup();
|
const { request, db } = getSetup();
|
||||||
db.select = () => ({
|
db.select = () => ({
|
||||||
from: () => Promise.reject(new Error('db error')),
|
from: () => Promise.reject(new Error('db error')),
|
||||||
});
|
});
|
||||||
return request
|
return request.get('/health').expect(500).expect(res => {
|
||||||
.get('/health')
|
|
||||||
.expect(500)
|
|
||||||
.expect((res) => {
|
|
||||||
t.true(res.status === 500);
|
t.true(res.status === 500);
|
||||||
t.true(res.body.health === 'BAD');
|
t.true(res.body.health === 'BAD');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should give 200 when db is not failing', () => {
|
test('should give 200 when db is not failing', t => {
|
||||||
|
t.plan(0);
|
||||||
const { request } = getSetup();
|
const { request } = getSetup();
|
||||||
return request
|
return request.get('/health').expect(200);
|
||||||
.get('/health')
|
|
||||||
.expect(200);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should give health=GOOD when db is not failing', t => {
|
test('should give health=GOOD when db is not failing', t => {
|
||||||
|
t.plan(2);
|
||||||
const { request } = getSetup();
|
const { request } = getSetup();
|
||||||
return request
|
return request.get('/health').expect(200).expect(res => {
|
||||||
.get('/health')
|
|
||||||
.expect(200)
|
|
||||||
.expect((res) => {
|
|
||||||
t.true(res.status === 200);
|
t.true(res.status === 200);
|
||||||
t.true(res.body.health === 'GOOD');
|
t.true(res.body.health === 'GOOD');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1,18 +1,44 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const { Router } = require('express');
|
||||||
|
|
||||||
exports.createAPI = function (router, config) {
|
const adminApi = require('./admin-api');
|
||||||
require('./api')(router, config);
|
const clientApi = require('./client-api');
|
||||||
require('./event')(router, config);
|
const clientFeatures = require('./client-api/feature.js');
|
||||||
require('./feature')(router, config);
|
|
||||||
require('./feature-archive')(router, config);
|
|
||||||
require('./strategy')(router, config);
|
|
||||||
require('./health-check')(router, config);
|
|
||||||
require('./metrics')(router, config);
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.createLegacy = function (router, config) {
|
const health = require('./health-check');
|
||||||
require('./feature')(router, config);
|
const backstage = require('./backstage.js');
|
||||||
require('./health-check')(router, config);
|
|
||||||
require('./backstage')(router, config);
|
exports.router = function(config) {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.use('/health', health.router(config));
|
||||||
|
router.use('/internal-backstage', backstage.router(config));
|
||||||
|
|
||||||
|
router.get('/api', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
version: 2,
|
||||||
|
links: {
|
||||||
|
admin: {
|
||||||
|
uri: '/api/admin',
|
||||||
|
links: adminApi.apiDef.links,
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
uri: '/api/client',
|
||||||
|
links: clientApi.apiDef.links,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use('/api/admin', adminApi.router(config));
|
||||||
|
router.use('/api/client', clientApi.router(config));
|
||||||
|
|
||||||
|
// legacy support
|
||||||
|
// $root/features
|
||||||
|
// $root/client/register
|
||||||
|
// $root/client/metrics
|
||||||
|
router.use('/api/features', clientFeatures.router(config));
|
||||||
|
|
||||||
|
return router;
|
||||||
};
|
};
|
||||||
|
91
lib/routes/index.test.js
Normal file
91
lib/routes/index.test.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { test } = require('ava');
|
||||||
|
const store = require('./../../test/fixtures/store');
|
||||||
|
const supertest = require('supertest');
|
||||||
|
const logger = require('../logger');
|
||||||
|
const getApp = require('../app');
|
||||||
|
|
||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const eventBus = new EventEmitter();
|
||||||
|
|
||||||
|
test.beforeEach(() => {
|
||||||
|
logger.setLevel('FATAL');
|
||||||
|
});
|
||||||
|
|
||||||
|
function getSetup() {
|
||||||
|
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||||
|
const stores = store.createStores();
|
||||||
|
const app = getApp({
|
||||||
|
baseUriPath: base,
|
||||||
|
stores,
|
||||||
|
eventBus,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
base,
|
||||||
|
featureToggleStore: stores.featureToggleStore,
|
||||||
|
request: supertest(app),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('api defintion', t => {
|
||||||
|
t.plan(5);
|
||||||
|
const { request, base } = getSetup();
|
||||||
|
return request
|
||||||
|
.get(`${base}/api/`)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.truthy(res.body);
|
||||||
|
const { admin, client } = res.body.links;
|
||||||
|
t.true(admin.uri === '/api/admin');
|
||||||
|
t.true(client.uri === '/api/client');
|
||||||
|
t.true(
|
||||||
|
admin.links['feature-toggles'].uri === '/api/admin/features'
|
||||||
|
);
|
||||||
|
t.true(client.links.metrics.uri === '/api/client/metrics');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('admin api defintion', t => {
|
||||||
|
t.plan(2);
|
||||||
|
const { request, base } = getSetup();
|
||||||
|
return request
|
||||||
|
.get(`${base}/api/admin`)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.truthy(res.body);
|
||||||
|
t.true(
|
||||||
|
res.body.links['feature-toggles'].uri === '/api/admin/features'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('client api defintion', t => {
|
||||||
|
t.plan(2);
|
||||||
|
const { request, base } = getSetup();
|
||||||
|
return request
|
||||||
|
.get(`${base}/api/client`)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.truthy(res.body);
|
||||||
|
t.true(res.body.links.metrics.uri === '/api/client/metrics');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('client legacy features uri', t => {
|
||||||
|
t.plan(3);
|
||||||
|
const { request, base } = getSetup();
|
||||||
|
return request
|
||||||
|
.get(`${base}/api/features`)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.truthy(res.body);
|
||||||
|
t.true(res.body.version === 1);
|
||||||
|
t.deepEqual(res.body.features, []);
|
||||||
|
});
|
||||||
|
});
|
@ -1,170 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const logger = require('../logger');
|
|
||||||
const ClientMetrics = require('../client-metrics');
|
|
||||||
const joi = require('joi');
|
|
||||||
const { clientMetricsSchema, clientRegisterSchema } = require('./metrics-schema');
|
|
||||||
const { catchLogAndSendErrorResponse } = require('./route-utils');
|
|
||||||
|
|
||||||
module.exports = function (app, config) {
|
|
||||||
const {
|
|
||||||
clientMetricsStore,
|
|
||||||
clientInstanceStore,
|
|
||||||
clientApplicationsStore,
|
|
||||||
strategyStore,
|
|
||||||
featureToggleStore,
|
|
||||||
} = config.stores;
|
|
||||||
|
|
||||||
const metrics = new ClientMetrics(clientMetricsStore);
|
|
||||||
|
|
||||||
app.get('/client/seen-toggles', (req, res) => {
|
|
||||||
const seenAppToggles = metrics.getAppsWithToggles();
|
|
||||||
res.json(seenAppToggles);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/client/seen-apps', (req, res) => {
|
|
||||||
const seenApps = metrics.getSeenAppsPerToggle();
|
|
||||||
clientApplicationsStore.getApplications()
|
|
||||||
.then(toLookup)
|
|
||||||
.then(metaData => {
|
|
||||||
Object.keys(seenApps).forEach(key => {
|
|
||||||
seenApps[key] = seenApps[key].map(entry => {
|
|
||||||
if (metaData[entry.appName]) {
|
|
||||||
return Object.assign({}, entry, metaData[entry.appName]);
|
|
||||||
}
|
|
||||||
return entry;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
res.json(seenApps);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/client/metrics/feature-toggles', (req, res) => {
|
|
||||||
res.json(metrics.getTogglesMetrics());
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/client/metrics/feature-toggles/:name', (req, res) => {
|
|
||||||
const name = req.params.name;
|
|
||||||
const data = metrics.getTogglesMetrics();
|
|
||||||
const lastHour = data.lastHour[name] || {};
|
|
||||||
const lastMinute = data.lastMinute[name] || {};
|
|
||||||
res.json({
|
|
||||||
lastHour,
|
|
||||||
lastMinute,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/client/metrics', (req, res) => {
|
|
||||||
const data = req.body;
|
|
||||||
const clientIp = req.ip;
|
|
||||||
|
|
||||||
joi.validate(data, clientMetricsSchema, (err, cleaned) => {
|
|
||||||
if (err) {
|
|
||||||
logger.warn('Invalid metrics posted', err);
|
|
||||||
return res.status(400).json(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
clientMetricsStore
|
|
||||||
.insert(cleaned)
|
|
||||||
.then(() => clientInstanceStore.insert({
|
|
||||||
appName: cleaned.appName,
|
|
||||||
instanceId: cleaned.instanceId,
|
|
||||||
clientIp,
|
|
||||||
}))
|
|
||||||
.catch(err => logger.error('failed to store metrics', err));
|
|
||||||
|
|
||||||
res.status(202).end();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/client/register', (req, res) => {
|
|
||||||
const data = req.body;
|
|
||||||
|
|
||||||
joi.validate(data, clientRegisterSchema, (err, clientRegistration) => {
|
|
||||||
if (err) {
|
|
||||||
logger.warn('Invalid client data posted', err);
|
|
||||||
return res.status(400).json(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
clientRegistration.clientIp = req.ip;
|
|
||||||
|
|
||||||
clientApplicationsStore
|
|
||||||
.upsert(clientRegistration)
|
|
||||||
.then(() => clientInstanceStore.insert(clientRegistration))
|
|
||||||
.then(() => logger.info(`New client registered with
|
|
||||||
appName=${clientRegistration.appName} and instanceId=${clientRegistration.instanceId}`))
|
|
||||||
.catch(err => logger.error('failed to register client', err));
|
|
||||||
|
|
||||||
res.status(202).end();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/client/applications/:appName', (req, res) => {
|
|
||||||
const input = Object.assign({}, req.body, {
|
|
||||||
appName: req.params.appName,
|
|
||||||
});
|
|
||||||
clientApplicationsStore
|
|
||||||
.upsert(input)
|
|
||||||
.then(() => res.status(202).end())
|
|
||||||
.catch((e) => {
|
|
||||||
logger.error(e);
|
|
||||||
res.status(500).end();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function toLookup (metaData) {
|
|
||||||
return metaData.reduce((result, entry) => {
|
|
||||||
result[entry.appName] = entry;
|
|
||||||
return result;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
app.get('/client/applications/', (req, res) => {
|
|
||||||
clientApplicationsStore
|
|
||||||
.getApplications(req.query)
|
|
||||||
.then(applications => res.json({ applications }))
|
|
||||||
.catch(err => catchLogAndSendErrorResponse(err, res));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/client/applications/:appName', (req, res) => {
|
|
||||||
const appName = req.params.appName;
|
|
||||||
const seenToggles = metrics.getSeenTogglesByAppName(appName);
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
clientApplicationsStore.getApplication(appName),
|
|
||||||
clientInstanceStore.getByAppName(appName),
|
|
||||||
strategyStore.getStrategies(),
|
|
||||||
featureToggleStore.getFeatures(),
|
|
||||||
])
|
|
||||||
.then(([application, instances, strategies, features]) => {
|
|
||||||
const appDetails = {
|
|
||||||
appName: application.appName,
|
|
||||||
createdAt: application.createdAt,
|
|
||||||
description: application.description,
|
|
||||||
url: application.url,
|
|
||||||
color: application.color,
|
|
||||||
icon: application.icon,
|
|
||||||
strategies: application.strategies.map(name => {
|
|
||||||
const found = strategies.find((feature) => feature.name === name);
|
|
||||||
if (found) {
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
return { name, notFound: true };
|
|
||||||
}),
|
|
||||||
instances,
|
|
||||||
seenToggles: seenToggles.map(name => {
|
|
||||||
const found = features.find((feature) => feature.name === name);
|
|
||||||
if (found) {
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
return { name, notFound: true };
|
|
||||||
}),
|
|
||||||
links: {
|
|
||||||
self: `/api/client/applications/${application.appName}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
res.json(appDetails);
|
|
||||||
})
|
|
||||||
.catch(err => catchLogAndSendErrorResponse(err, res));
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,114 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const joi = require('joi');
|
|
||||||
const eventType = require('../event-type');
|
|
||||||
const logger = require('../logger');
|
|
||||||
const NameExistsError = require('../error/name-exists-error');
|
|
||||||
const extractUser = require('../extract-user');
|
|
||||||
const strategySchema = require('./strategy-schema');
|
|
||||||
const version = 1;
|
|
||||||
|
|
||||||
const handleError = (req, res, error) => {
|
|
||||||
logger.warn('Error creating or updating strategy', error);
|
|
||||||
switch (error.name) {
|
|
||||||
case 'NotFoundError':
|
|
||||||
return res
|
|
||||||
.status(404)
|
|
||||||
.end();
|
|
||||||
case 'NameExistsError':
|
|
||||||
return res
|
|
||||||
.status(403)
|
|
||||||
.json([{ msg: `A strategy named '${req.body.name}' already exists.` }])
|
|
||||||
.end();
|
|
||||||
case 'ValidationError':
|
|
||||||
return res
|
|
||||||
.status(400)
|
|
||||||
.json(error)
|
|
||||||
.end();
|
|
||||||
default:
|
|
||||||
logger.error('Could perfom operation', error);
|
|
||||||
return res
|
|
||||||
.status(500)
|
|
||||||
.end();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = function (app, config) {
|
|
||||||
const { strategyStore, eventStore } = config.stores;
|
|
||||||
|
|
||||||
app.get('/strategies', (req, res) => {
|
|
||||||
strategyStore.getStrategies().then(strategies => {
|
|
||||||
res.json({ version, strategies });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/strategies/:name', (req, res) => {
|
|
||||||
strategyStore.getStrategy(req.params.name)
|
|
||||||
.then(strategy => res.json(strategy).end())
|
|
||||||
.catch(() => res.status(404).json({ error: 'Could not find strategy' }));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.delete('/strategies/:name', (req, res) => {
|
|
||||||
const strategyName = req.params.name;
|
|
||||||
|
|
||||||
strategyStore.getStrategy(strategyName)
|
|
||||||
.then(() => eventStore.store({
|
|
||||||
type: eventType.STRATEGY_DELETED,
|
|
||||||
createdBy: extractUser(req),
|
|
||||||
data: {
|
|
||||||
name: strategyName,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
.then(() => res.status(200).end())
|
|
||||||
.catch(error => handleError(req, res, error));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/strategies', (req, res) => {
|
|
||||||
const data = req.body;
|
|
||||||
validateInput(data)
|
|
||||||
.then(validateStrategyName)
|
|
||||||
.then((newStrategy) => eventStore.store({
|
|
||||||
type: eventType.STRATEGY_CREATED,
|
|
||||||
createdBy: extractUser(req),
|
|
||||||
data: newStrategy,
|
|
||||||
}))
|
|
||||||
.then(() => res.status(201).end())
|
|
||||||
.catch(error => handleError(req, res, error));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.put('/strategies/:strategyName', (req, res) => {
|
|
||||||
const strategyName = req.params.strategyName;
|
|
||||||
const updatedStrategy = req.body;
|
|
||||||
|
|
||||||
updatedStrategy.name = strategyName;
|
|
||||||
|
|
||||||
strategyStore.getStrategy(strategyName)
|
|
||||||
.then(() => validateInput(updatedStrategy))
|
|
||||||
.then(() => eventStore.store({
|
|
||||||
type: eventType.STRATEGY_UPDATED,
|
|
||||||
createdBy: extractUser(req),
|
|
||||||
data: updatedStrategy,
|
|
||||||
}))
|
|
||||||
.then(() => res.status(200).end())
|
|
||||||
.catch(error => handleError(req, res, error));
|
|
||||||
});
|
|
||||||
|
|
||||||
function validateStrategyName (data) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
strategyStore.getStrategy(data.name)
|
|
||||||
.then(() => reject(new NameExistsError('Feature name already exist')))
|
|
||||||
.catch(() => resolve(data));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateInput (data) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
joi.validate(data, strategySchema, (err, cleaned) => {
|
|
||||||
if (err) {
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
return resolve(cleaned);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
const { test } = require('ava');
|
const { test } = require('ava');
|
||||||
const proxyquire = require('proxyquire');
|
const proxyquire = require('proxyquire');
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
const getApp = proxyquire('./app', {
|
const getApp = proxyquire('./app', {
|
||||||
'./routes': {
|
'./routes': {
|
||||||
createAPI: () => {},
|
router: () => express.Router(),
|
||||||
createLegacy: () => {},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -35,6 +35,7 @@ const serverImpl = proxyquire('./server-impl', {
|
|||||||
test('should call preHook', async t => {
|
test('should call preHook', async t => {
|
||||||
let called = 0;
|
let called = 0;
|
||||||
await serverImpl.start({
|
await serverImpl.start({
|
||||||
|
port: 0,
|
||||||
preHook: () => {
|
preHook: () => {
|
||||||
called++;
|
called++;
|
||||||
},
|
},
|
||||||
@ -44,8 +45,11 @@ test('should call preHook', async t => {
|
|||||||
|
|
||||||
test('should call preRouterHook', async t => {
|
test('should call preRouterHook', async t => {
|
||||||
let called = 0;
|
let called = 0;
|
||||||
await serverImpl.start({ preRouterHook: () => {
|
await serverImpl.start({
|
||||||
|
port: 0,
|
||||||
|
preRouterHook: () => {
|
||||||
called++;
|
called++;
|
||||||
} });
|
},
|
||||||
|
});
|
||||||
t.true(called === 1);
|
t.true(called === 1);
|
||||||
});
|
});
|
||||||
|
10
package.json
10
package.json
@ -39,7 +39,7 @@
|
|||||||
"db-migrate": "db-migrate",
|
"db-migrate": "db-migrate",
|
||||||
"lint": "eslint lib",
|
"lint": "eslint lib",
|
||||||
"pretest": "npm run lint",
|
"pretest": "npm run lint",
|
||||||
"test": "PORT=4243 ava test lib/*.test.js lib/**/*.test.js",
|
"test": "PORT=4243 ava lib/*.test.js lib/**/*.test.js lib/**/**/*.test.js lib/**/**/**/*.test.js test",
|
||||||
"test:docker": "./scripts/docker-postgres.sh",
|
"test:docker": "./scripts/docker-postgres.sh",
|
||||||
"test:watch": "npm run test -- --watch",
|
"test:watch": "npm run test -- --watch",
|
||||||
"test:pg-virtualenv": "pg_virtualenv npm run test:pg-virtualenv-chai",
|
"test:pg-virtualenv": "pg_virtualenv npm run test:pg-virtualenv-chai",
|
||||||
@ -81,11 +81,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^7.0.5",
|
"@types/node": "^7.0.5",
|
||||||
"ava": "^0.18.2",
|
"ava": "^0.19.1",
|
||||||
"coveralls": "^2.11.16",
|
"coveralls": "^2.11.16",
|
||||||
"eslint": "^3.16.1",
|
"eslint": "^4.0.0",
|
||||||
"eslint-config-finn": "^1.0.0-beta.1",
|
"eslint-config-finn": "^1.0.2",
|
||||||
|
"eslint-config-finn-prettier": "^2.0.0",
|
||||||
"nyc": "^10.1.2",
|
"nyc": "^10.1.2",
|
||||||
|
"prettier": "^1.4.4",
|
||||||
"proxyquire": "^1.7.11",
|
"proxyquire": "^1.7.11",
|
||||||
"sinon": "^1.17.7",
|
"sinon": "^1.17.7",
|
||||||
"superagent": "^3.5.0",
|
"superagent": "^3.5.0",
|
||||||
|
@ -1,26 +1,28 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const { test } = require('ava');
|
||||||
const { setupApp } = require('./helpers/test-helper');
|
const { setupApp } = require('./../../helpers/test-helper');
|
||||||
const logger = require('../../lib/logger');
|
const logger = require('../../../../lib/logger');
|
||||||
|
|
||||||
test.beforeEach(() => {
|
test.beforeEach(() => {
|
||||||
logger.setLevel('FATAL');
|
logger.setLevel('FATAL');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('returns events', async (t) => {
|
test.serial('returns events', async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('event_api_serial');
|
const { request, destroy } = await setupApp('event_api_serial');
|
||||||
return request
|
return request
|
||||||
.get('/api/events')
|
.get('/api/admin/events')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(destroy);
|
.then(destroy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('returns events given a name', async (t) => {
|
test.serial('returns events given a name', async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('event_api_serial');
|
const { request, destroy } = await setupApp('event_api_serial');
|
||||||
return request
|
return request
|
||||||
.get('/api/events/myname')
|
.get('/api/admin/events/myname')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(destroy);
|
.then(destroy);
|
@ -1,38 +1,38 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const { test } = require('ava');
|
||||||
const { setupApp } = require('./helpers/test-helper');
|
const { setupApp } = require('./../../helpers/test-helper');
|
||||||
const logger = require('../../lib/logger');
|
const logger = require('../../../../lib/logger');
|
||||||
|
|
||||||
test.beforeEach(() => {
|
test.beforeEach(() => {
|
||||||
logger.setLevel('FATAL');
|
logger.setLevel('FATAL');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('returns three archived toggles', async t => {
|
test.serial('returns three archived toggles', async t => {
|
||||||
|
t.plan(1);
|
||||||
const { request, destroy } = await setupApp('archive_serial');
|
const { request, destroy } = await setupApp('archive_serial');
|
||||||
return request
|
return request
|
||||||
.get('/api/archive/features')
|
.get('/api/admin/archive/features')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect(res => {
|
||||||
t.true(res.body.features.length === 3);
|
t.true(res.body.features.length === 3);
|
||||||
})
|
})
|
||||||
.then(destroy);
|
.then(destroy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('revives a feature by name', async t => {
|
test.serial('revives a feature by name', async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('archive_serial');
|
const { request, destroy } = await setupApp('archive_serial');
|
||||||
return request
|
return request
|
||||||
.post('/api/archive/revive/featureArchivedX')
|
.post('/api/admin/archive/revive/featureArchivedX')
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(destroy);
|
.then(destroy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('must set name when reviving toggle', async t => {
|
test.serial('must set name when reviving toggle', async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('archive_serial');
|
const { request, destroy } = await setupApp('archive_serial');
|
||||||
return request
|
return request.post('/api/admin/archive/revive/').expect(404).then(destroy);
|
||||||
.post('/api/archive/revive/')
|
|
||||||
.expect(404)
|
|
||||||
.then(destroy);
|
|
||||||
});
|
});
|
200
test/e2e/api/admin/feature.e2e.test.js
Normal file
200
test/e2e/api/admin/feature.e2e.test.js
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { test } = require('ava');
|
||||||
|
const { setupApp } = require('./../../helpers/test-helper');
|
||||||
|
const logger = require('../../../../lib/logger');
|
||||||
|
|
||||||
|
test.beforeEach(() => {
|
||||||
|
logger.setLevel('FATAL');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('returns three feature toggles', async t => {
|
||||||
|
const { request, destroy } = await setupApp('feature_api_serial');
|
||||||
|
return request
|
||||||
|
.get('/api/admin/features')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.true(res.body.features.length === 3);
|
||||||
|
})
|
||||||
|
.then(destroy);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('gets a feature by name', async t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request, destroy } = await setupApp('feature_api_serial');
|
||||||
|
return request
|
||||||
|
.get('/api/admin/features/featureX')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.then(destroy);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('cant get feature that dose not exist', async t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request, destroy } = await setupApp('feature_api_serial');
|
||||||
|
return request
|
||||||
|
.get('/api/admin/features/myfeature')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(404)
|
||||||
|
.then(destroy);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('creates new feature toggle', async t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request, destroy } = await setupApp('feature_api_serial');
|
||||||
|
return request
|
||||||
|
.post('/api/admin/features')
|
||||||
|
.send({
|
||||||
|
name: 'com.test.feature',
|
||||||
|
enabled: false,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(201)
|
||||||
|
.then(destroy);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('creates new feature toggle with createdBy', async t => {
|
||||||
|
t.plan(1);
|
||||||
|
const { request, destroy } = await setupApp('feature_api_serial');
|
||||||
|
await request
|
||||||
|
.post('/api/admin/features')
|
||||||
|
.send({
|
||||||
|
name: 'com.test.Username',
|
||||||
|
enabled: false,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
})
|
||||||
|
.set('Cookie', ['username=ivaosthu'])
|
||||||
|
.set('Content-Type', 'application/json');
|
||||||
|
await request
|
||||||
|
.get('/api/admin/events')
|
||||||
|
.expect(res => {
|
||||||
|
t.true(res.body.events[0].createdBy === 'ivaosthu');
|
||||||
|
})
|
||||||
|
.then(destroy);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('require new feature toggle to have a name', async t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request, destroy } = await setupApp('feature_api_serial');
|
||||||
|
return request
|
||||||
|
.post('/api/admin/features')
|
||||||
|
.send({ name: '' })
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(400)
|
||||||
|
.then(destroy);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
'can not change status of feature toggle that does not exist',
|
||||||
|
async t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request, destroy } = await setupApp('feature_api_serial');
|
||||||
|
return request
|
||||||
|
.put('/api/admin/features/should-not-exist')
|
||||||
|
.send({ name: 'should-not-exist', enabled: false })
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(404)
|
||||||
|
.then(destroy);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial('can change status of feature toggle that does exist', async t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request, destroy } = await setupApp('feature_api_serial');
|
||||||
|
return request
|
||||||
|
.put('/api/admin/features/featureY')
|
||||||
|
.send({
|
||||||
|
name: 'featureY',
|
||||||
|
enabled: true,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(200)
|
||||||
|
.then(destroy);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('can not toggle of feature that does not exist', async t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request, destroy } = await setupApp('feature_api_serial');
|
||||||
|
return request
|
||||||
|
.post('/api/admin/features/should-not-exist/toggle')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(404)
|
||||||
|
.then(destroy);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('can toggle a feature that does exist', async t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request, destroy } = await setupApp('feature_api_serial');
|
||||||
|
return request
|
||||||
|
.post('/api/admin/features/featureY/toggle')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(200)
|
||||||
|
.then(destroy);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('archives a feature by name', async t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request, destroy } = await setupApp('feature_api_serial');
|
||||||
|
return request
|
||||||
|
.delete('/api/admin/features/featureX')
|
||||||
|
.expect(200)
|
||||||
|
.then(destroy);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('can not archive unknown feature', async t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request, destroy } = await setupApp('feature_api_serial');
|
||||||
|
return request
|
||||||
|
.delete('/api/admin/features/featureUnknown')
|
||||||
|
.expect(404)
|
||||||
|
.then(destroy);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('refuses to create a feature with an existing name', async t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request, destroy } = await setupApp('feature_api_serial');
|
||||||
|
return request
|
||||||
|
.post('/api/admin/features')
|
||||||
|
.send({ name: 'featureX' })
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(403)
|
||||||
|
.then(destroy);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('refuses to validate a feature with an existing name', async t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request, destroy } = await setupApp('feature_api_serial');
|
||||||
|
return request
|
||||||
|
.post('/api/admin/features/validate')
|
||||||
|
.send({ name: 'featureX' })
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(403)
|
||||||
|
.then(destroy);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
'new strategies api can add two strategies to a feature toggle',
|
||||||
|
async t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { request, destroy } = await setupApp('feature_api_serial');
|
||||||
|
return request
|
||||||
|
.put('/api/admin/features/featureY')
|
||||||
|
.send({
|
||||||
|
name: 'featureY',
|
||||||
|
description: 'soon to be the #14 feature',
|
||||||
|
enabled: false,
|
||||||
|
strategies: [
|
||||||
|
{
|
||||||
|
name: 'baz',
|
||||||
|
parameters: { foo: 'bar' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(200)
|
||||||
|
.then(destroy);
|
||||||
|
}
|
||||||
|
);
|
@ -1,13 +1,15 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
const test = require('ava');
|
|
||||||
const { setupApp } = require('./helpers/test-helper');
|
const { test } = require('ava');
|
||||||
const logger = require('../../lib/logger');
|
const { setupApp } = require('./../../helpers/test-helper');
|
||||||
|
const logger = require('../../../../lib/logger');
|
||||||
|
|
||||||
test.beforeEach(() => {
|
test.beforeEach(() => {
|
||||||
logger.setLevel('FATAL');
|
logger.setLevel('FATAL');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('should register client', async (t) => {
|
test.serial('should register client', async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('metrics_serial');
|
const { request, destroy } = await setupApp('metrics_serial');
|
||||||
return request
|
return request
|
||||||
.post('/api/client/register')
|
.post('/api/client/register')
|
||||||
@ -16,34 +18,38 @@ test.serial('should register client', async (t) => {
|
|||||||
instanceId: 'test',
|
instanceId: 'test',
|
||||||
strategies: ['default'],
|
strategies: ['default'],
|
||||||
started: Date.now(),
|
started: Date.now(),
|
||||||
interval: 10
|
interval: 10,
|
||||||
})
|
})
|
||||||
.expect(202)
|
.expect(202)
|
||||||
.then(destroy);
|
.then(destroy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('should allow client to register multiple times', async (t) => {
|
test.serial('should allow client to register multiple times', async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('metrics_serial');
|
const { request, destroy } = await setupApp('metrics_serial');
|
||||||
const clientRegistration = {
|
const clientRegistration = {
|
||||||
appName: 'multipleRegistration',
|
appName: 'multipleRegistration',
|
||||||
instanceId: 'test',
|
instanceId: 'test',
|
||||||
strategies: ['default', 'another'],
|
strategies: ['default', 'another'],
|
||||||
started: Date.now(),
|
started: Date.now(),
|
||||||
interval: 10
|
interval: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.post('/api/client/register')
|
.post('/api/client/register')
|
||||||
.send(clientRegistration)
|
.send(clientRegistration)
|
||||||
.expect(202)
|
.expect(202)
|
||||||
.then(() => request
|
.then(() =>
|
||||||
|
request
|
||||||
.post('/api/client/register')
|
.post('/api/client/register')
|
||||||
.send(clientRegistration)
|
.send(clientRegistration)
|
||||||
.expect(202))
|
.expect(202)
|
||||||
|
)
|
||||||
.then(destroy);
|
.then(destroy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('should accept client metrics', async t => {
|
test.serial('should accept client metrics', async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('metrics_serial');
|
const { request, destroy } = await setupApp('metrics_serial');
|
||||||
return request
|
return request
|
||||||
.post('/api/client/metrics')
|
.post('/api/client/metrics')
|
||||||
@ -53,19 +59,20 @@ test.serial('should accept client metrics', async t => {
|
|||||||
bucket: {
|
bucket: {
|
||||||
start: Date.now(),
|
start: Date.now(),
|
||||||
stop: Date.now(),
|
stop: Date.now(),
|
||||||
toggles: {}
|
toggles: {},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.expect(202)
|
.expect(202)
|
||||||
.then(destroy);
|
.then(destroy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('should get application details', async t => {
|
test.serial('should get application details', async t => {
|
||||||
|
t.plan(3);
|
||||||
const { request, destroy } = await setupApp('metrics_serial');
|
const { request, destroy } = await setupApp('metrics_serial');
|
||||||
return request
|
return request
|
||||||
.get('/api/client/applications/demo-app-1')
|
.get('/api/admin/metrics/applications/demo-app-1')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect((res) => {
|
.expect(res => {
|
||||||
t.true(res.status === 200);
|
t.true(res.status === 200);
|
||||||
t.true(res.body.appName === 'demo-app-1');
|
t.true(res.body.appName === 'demo-app-1');
|
||||||
t.true(res.body.instances.length === 1);
|
t.true(res.body.instances.length === 1);
|
||||||
@ -74,11 +81,12 @@ test.serial('should get application details', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.serial('should get list of applications', async t => {
|
test.serial('should get list of applications', async t => {
|
||||||
|
t.plan(2);
|
||||||
const { request, destroy } = await setupApp('metrics_serial');
|
const { request, destroy } = await setupApp('metrics_serial');
|
||||||
return request
|
return request
|
||||||
.get('/api/client/applications')
|
.get('/api/admin/metrics/applications')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect((res) => {
|
.expect(res => {
|
||||||
t.true(res.status === 200);
|
t.true(res.status === 200);
|
||||||
t.true(res.body.applications.length === 2);
|
t.true(res.body.applications.length === 2);
|
||||||
})
|
})
|
@ -1,102 +1,124 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const { test } = require('ava');
|
||||||
const { setupApp } = require('./helpers/test-helper');
|
const { setupApp } = require('./../../helpers/test-helper');
|
||||||
const logger = require('../../lib/logger');
|
const logger = require('../../../../lib/logger');
|
||||||
|
|
||||||
test.beforeEach(() => {
|
test.beforeEach(() => {
|
||||||
logger.setLevel('FATAL');
|
logger.setLevel('FATAL');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('gets all strategies', async (t) => {
|
test.serial('gets all strategies', async t => {
|
||||||
|
t.plan(1);
|
||||||
const { request, destroy } = await setupApp('strategy_api_serial');
|
const { request, destroy } = await setupApp('strategy_api_serial');
|
||||||
return request
|
return request
|
||||||
.get('/api/strategies')
|
.get('/api/admin/strategies')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect(res => {
|
||||||
t.true(res.body.strategies.length === 2, 'expected to have two strategies');
|
t.true(
|
||||||
|
res.body.strategies.length === 2,
|
||||||
|
'expected to have two strategies'
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.then(destroy);
|
.then(destroy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('gets a strategy by name', async (t) => {
|
test.serial('gets a strategy by name', async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('strategy_api_serial');
|
const { request, destroy } = await setupApp('strategy_api_serial');
|
||||||
return request
|
return request
|
||||||
.get('/api/strategies/default')
|
.get('/api/admin/strategies/default')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(destroy);
|
.then(destroy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('cant get a strategy by name that dose not exist', async (t) => {
|
test.serial('cant get a strategy by name that dose not exist', async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('strategy_api_serial');
|
const { request, destroy } = await setupApp('strategy_api_serial');
|
||||||
return request
|
return request
|
||||||
.get('/api/strategies/mystrategy')
|
.get('/api/admin/strategies/mystrategy')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(404)
|
.expect(404)
|
||||||
.then(destroy);
|
.then(destroy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('creates a new strategy', async (t) => {
|
test.serial('creates a new strategy', async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('strategy_api_serial');
|
const { request, destroy } = await setupApp('strategy_api_serial');
|
||||||
return request
|
return request
|
||||||
.post('/api/strategies')
|
.post('/api/admin/strategies')
|
||||||
.send({ name: 'myCustomStrategy', description: 'Best strategy ever.', parameters: [] })
|
.send({
|
||||||
|
name: 'myCustomStrategy',
|
||||||
|
description: 'Best strategy ever.',
|
||||||
|
parameters: [],
|
||||||
|
})
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(201)
|
.expect(201)
|
||||||
.then(destroy);
|
.then(destroy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('requires new strategies to have a name', async (t) => {
|
test.serial('requires new strategies to have a name', async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('strategy_api_serial');
|
const { request, destroy } = await setupApp('strategy_api_serial');
|
||||||
return request
|
return request
|
||||||
.post('/api/strategies')
|
.post('/api/admin/strategies')
|
||||||
.send({ name: '' })
|
.send({ name: '' })
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(400)
|
.expect(400)
|
||||||
.then(destroy);
|
.then(destroy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('refuses to create a strategy with an existing name', async (t) => {
|
test.serial('refuses to create a strategy with an existing name', async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('strategy_api_serial');
|
const { request, destroy } = await setupApp('strategy_api_serial');
|
||||||
return request
|
return request
|
||||||
.post('/api/strategies')
|
.post('/api/admin/strategies')
|
||||||
.send({ name: 'default', parameters: [] })
|
.send({ name: 'default', parameters: [] })
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(403)
|
.expect(403)
|
||||||
.then(destroy);
|
.then(destroy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('deletes a new strategy', async (t) => {
|
test.serial('deletes a new strategy', async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('strategy_api_serial');
|
const { request, destroy } = await setupApp('strategy_api_serial');
|
||||||
return request
|
return request
|
||||||
.delete('/api/strategies/usersWithEmail')
|
.delete('/api/admin/strategies/usersWithEmail')
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(destroy);
|
.then(destroy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('can\'t delete a strategy that dose not exist', async (t) => {
|
test.serial("can't delete a strategy that dose not exist", async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('strategy_api_serial', false);
|
const { request, destroy } = await setupApp('strategy_api_serial', false);
|
||||||
return request
|
return request
|
||||||
.delete('/api/strategies/unknown')
|
.delete('/api/admin/strategies/unknown')
|
||||||
.expect(404);
|
.expect(404)
|
||||||
|
.then(destroy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('updates a exiting strategy', async (t) => {
|
test.serial('updates a exiting strategy', async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('strategy_api_serial');
|
const { request, destroy } = await setupApp('strategy_api_serial');
|
||||||
return request
|
return request
|
||||||
.put('/api/strategies/default')
|
.put('/api/admin/strategies/default')
|
||||||
.send({ name: 'default', description: 'Default is the best!', parameters: [] })
|
.send({
|
||||||
|
name: 'default',
|
||||||
|
description: 'Default is the best!',
|
||||||
|
parameters: [],
|
||||||
|
})
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(destroy);
|
.then(destroy);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('cant update a unknown strategy', async (t) => {
|
test.serial('cant update a unknown strategy', async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('strategy_api_serial');
|
const { request, destroy } = await setupApp('strategy_api_serial');
|
||||||
return request
|
return request
|
||||||
.put('/api/strategies/unknown')
|
.put('/api/admin/strategies/unknown')
|
||||||
.send({ name: 'unkown', parameters: [] })
|
.send({ name: 'unkown', parameters: [] })
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(404)
|
.expect(404)
|
5
test/e2e/api/client/feature.e2e.test.js
Normal file
5
test/e2e/api/client/feature.e2e.test.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { test } = require('ava');
|
||||||
|
|
||||||
|
test.todo('e2e client feature');
|
5
test/e2e/api/client/metrics.e2e.test.js
Normal file
5
test/e2e/api/client/metrics.e2e.test.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { test } = require('ava');
|
||||||
|
|
||||||
|
test.todo('e2e client metrics');
|
@ -1,208 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const { test } = require('ava');
|
|
||||||
const { setupApp } = require('./helpers/test-helper');
|
|
||||||
const logger = require('../../lib/logger');
|
|
||||||
|
|
||||||
test.beforeEach(() => {
|
|
||||||
logger.setLevel('FATAL');
|
|
||||||
});
|
|
||||||
|
|
||||||
test.serial('returns three feature toggles', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
return request
|
|
||||||
.get('/features')
|
|
||||||
.expect('Content-Type', /json/)
|
|
||||||
.expect(200)
|
|
||||||
.expect((res) => {
|
|
||||||
t.true(res.body.features.length === 3);
|
|
||||||
})
|
|
||||||
.then(destroy);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.serial('gets a feature by name', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
return request
|
|
||||||
.get('/features/featureX')
|
|
||||||
.expect('Content-Type', /json/)
|
|
||||||
.expect(200)
|
|
||||||
.then(destroy);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.serial('cant get feature that dose not exist', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
logger.setLevel('FATAL');
|
|
||||||
return request
|
|
||||||
.get('/features/myfeature')
|
|
||||||
.expect('Content-Type', /json/)
|
|
||||||
.expect(404)
|
|
||||||
.then(destroy);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.serial('creates new feature toggle', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
return request
|
|
||||||
.post('/features')
|
|
||||||
.send({ name: 'com.test.feature', enabled: false, strategies: [{name: 'default'}] })
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(201)
|
|
||||||
.then(destroy);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.serial('creates new feature toggle with createdBy', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
logger.setLevel('FATAL');
|
|
||||||
request
|
|
||||||
.post('/features')
|
|
||||||
.send({ name: 'com.test.Username', enabled: false, strategies: [{name: 'default'}] })
|
|
||||||
.set('Cookie', ['username=ivaosthu'])
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.end(() => {
|
|
||||||
return request
|
|
||||||
.get('/api/events')
|
|
||||||
.expect((res) => {
|
|
||||||
t.true(res.body.events[0].createdBy === 'ivaosthu');
|
|
||||||
})
|
|
||||||
.then(destroy);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.serial('require new feature toggle to have a name', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
logger.setLevel('FATAL');
|
|
||||||
return request
|
|
||||||
.post('/features')
|
|
||||||
.send({ name: '' })
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(400)
|
|
||||||
.then(destroy);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.serial('can not change status of feature toggle that does not exist', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
logger.setLevel('FATAL');
|
|
||||||
return request
|
|
||||||
.put('/features/should-not-exist')
|
|
||||||
.send({ name: 'should-not-exist', enabled: false })
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(404).then(destroy);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.serial('can change status of feature toggle that does exist', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
logger.setLevel('FATAL');
|
|
||||||
return request
|
|
||||||
.put('/features/featureY')
|
|
||||||
.send({ name: 'featureY', enabled: true, strategies: [{name: 'default'}] })
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(200).then(destroy);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.serial('can not toggle of feature that does not exist', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
logger.setLevel('FATAL');
|
|
||||||
return request
|
|
||||||
.post('/features/should-not-exist/toggle')
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(404).then(destroy);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.serial('can toggle a feature that does exist', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
logger.setLevel('FATAL');
|
|
||||||
return request
|
|
||||||
.post('/features/featureY/toggle')
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(200).then(destroy);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.serial('archives a feature by name', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
return request
|
|
||||||
.delete('/features/featureX')
|
|
||||||
.expect(200).then(destroy);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.serial('can not archive unknown feature', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
return request
|
|
||||||
.delete('/features/featureUnknown')
|
|
||||||
.expect(404).then(destroy);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.serial('refuses to create a feature with an existing name', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
return request
|
|
||||||
.post('/features')
|
|
||||||
.send({ name: 'featureX' })
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(403).then(destroy);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.serial('refuses to validate a feature with an existing name', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
return request
|
|
||||||
.post('/features-validate')
|
|
||||||
.send({ name: 'featureX' })
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(403).then(destroy);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
test.serial('new strategies api automatically map existing strategy to strategies array', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
t.plan(3);
|
|
||||||
return request
|
|
||||||
.get('/features/featureY')
|
|
||||||
.expect('Content-Type', /json/)
|
|
||||||
.expect((res) => {
|
|
||||||
t.true(res.body.strategies.length === 1, 'expected strategy added to strategies');
|
|
||||||
t.true(res.body.strategy === res.body.strategies[0].name);
|
|
||||||
t.deepEqual(res.body.parameters, res.body.strategies[0].parameters);
|
|
||||||
})
|
|
||||||
.then(destroy);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.serial('new strategies api can add two strategies to a feature toggle', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
return request
|
|
||||||
.put('/features/featureY')
|
|
||||||
.send({
|
|
||||||
name: 'featureY',
|
|
||||||
description: 'soon to be the #14 feature',
|
|
||||||
enabled: false,
|
|
||||||
strategies: [
|
|
||||||
{
|
|
||||||
name: 'baz',
|
|
||||||
parameters: { foo: 'bar' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(200)
|
|
||||||
.then(destroy);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.serial('new strategies api should not be allowed to post both strategy and strategies', async t => {
|
|
||||||
const { request, destroy } = await setupApp('feature_api_serial');
|
|
||||||
logger.setLevel('FATAL');
|
|
||||||
return request
|
|
||||||
.post('/features')
|
|
||||||
.send({
|
|
||||||
name: 'featureConfusing',
|
|
||||||
description: 'soon to be the #14 feature',
|
|
||||||
enabled: false,
|
|
||||||
strategy: 'baz',
|
|
||||||
parameters: {},
|
|
||||||
strategies: [
|
|
||||||
{
|
|
||||||
name: 'baz',
|
|
||||||
parameters: { foo: 'bar' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(400)
|
|
||||||
.then(destroy);
|
|
||||||
});
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const { test } = require('ava');
|
||||||
const { setupApp } = require('./helpers/test-helper');
|
const { setupApp } = require('./helpers/test-helper');
|
||||||
const logger = require('../../lib/logger');
|
const logger = require('../../lib/logger');
|
||||||
|
|
||||||
@ -8,9 +8,11 @@ test.beforeEach(() => {
|
|||||||
logger.setLevel('FATAL');
|
logger.setLevel('FATAL');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns health good', async (t) => {
|
test('returns health good', async t => {
|
||||||
|
t.plan(0);
|
||||||
const { request, destroy } = await setupApp('health');
|
const { request, destroy } = await setupApp('health');
|
||||||
return request.get('/health')
|
return request
|
||||||
|
.get('/health')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect('{"health":"GOOD"}')
|
.expect('{"health":"GOOD"}')
|
@ -25,9 +25,14 @@ function createApp (databaseSchema = 'test') {
|
|||||||
minPool: 0,
|
minPool: 0,
|
||||||
maxPool: 0,
|
maxPool: 0,
|
||||||
};
|
};
|
||||||
const db = createDb({ databaseUrl: options.databaseUrl, minPool: 0, maxPool: 0 });
|
const db = createDb({
|
||||||
|
databaseUrl: options.databaseUrl,
|
||||||
|
minPool: 0,
|
||||||
|
maxPool: 0,
|
||||||
|
});
|
||||||
|
|
||||||
return db.raw(`CREATE SCHEMA IF NOT EXISTS ${options.databaseSchema}`)
|
return db
|
||||||
|
.raw(`CREATE SCHEMA IF NOT EXISTS ${options.databaseSchema}`)
|
||||||
.then(() => migrator(options))
|
.then(() => migrator(options))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
db.destroy();
|
db.destroy();
|
||||||
@ -52,10 +57,9 @@ function createStrategies (stores) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'usersWithEmail',
|
name: 'usersWithEmail',
|
||||||
description: 'Active for users defined in the comma-separated emails-parameter.',
|
description:
|
||||||
parameters: [
|
'Active for users defined in the comma-separated emails-parameter.',
|
||||||
{ name: 'emails', type: 'string' },
|
parameters: [{ name: 'emails', type: 'string' }],
|
||||||
],
|
|
||||||
},
|
},
|
||||||
].map(strategy => stores.strategyStore._createStrategy(strategy));
|
].map(strategy => stores.strategyStore._createStrategy(strategy));
|
||||||
}
|
}
|
||||||
@ -105,23 +109,27 @@ function createFeatures (stores) {
|
|||||||
name: 'featureY',
|
name: 'featureY',
|
||||||
description: 'soon to be the #1 feature',
|
description: 'soon to be the #1 feature',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
strategies: [{
|
strategies: [
|
||||||
|
{
|
||||||
name: 'baz',
|
name: 'baz',
|
||||||
parameters: {
|
parameters: {
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
},
|
},
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'featureZ',
|
name: 'featureZ',
|
||||||
description: 'terrible feature',
|
description: 'terrible feature',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
strategies: [{
|
strategies: [
|
||||||
|
{
|
||||||
name: 'baz',
|
name: 'baz',
|
||||||
parameters: {
|
parameters: {
|
||||||
foo: 'rab',
|
foo: 'rab',
|
||||||
},
|
},
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'featureArchivedX',
|
name: 'featureArchivedX',
|
||||||
@ -135,24 +143,28 @@ function createFeatures (stores) {
|
|||||||
description: 'soon to be the #1 feature',
|
description: 'soon to be the #1 feature',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
archived: true,
|
archived: true,
|
||||||
strategies: [{
|
strategies: [
|
||||||
|
{
|
||||||
name: 'baz',
|
name: 'baz',
|
||||||
parameters: {
|
parameters: {
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
},
|
},
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'featureArchivedZ',
|
name: 'featureArchivedZ',
|
||||||
description: 'terrible feature',
|
description: 'terrible feature',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
archived: true,
|
archived: true,
|
||||||
strategies: [{
|
strategies: [
|
||||||
|
{
|
||||||
name: 'baz',
|
name: 'baz',
|
||||||
parameters: {
|
parameters: {
|
||||||
foo: 'rab',
|
foo: 'rab',
|
||||||
},
|
},
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
].map(feature => stores.featureToggleStore._createFeature(feature));
|
].map(feature => stores.featureToggleStore._createFeature(feature));
|
||||||
}
|
}
|
||||||
@ -168,18 +180,20 @@ function resetDatabase (stores) {
|
|||||||
|
|
||||||
function setupDatabase(stores) {
|
function setupDatabase(stores) {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
createStrategies(stores)
|
createStrategies(stores).concat(
|
||||||
.concat(createFeatures(stores)
|
createFeatures(stores)
|
||||||
.concat(createClientInstance(stores))
|
.concat(createClientInstance(stores))
|
||||||
.concat(createApplications(stores))));
|
.concat(createApplications(stores))
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
setupApp(name) {
|
setupApp(name) {
|
||||||
return createApp(name).then((app) => {
|
return createApp(name).then(app =>
|
||||||
return resetDatabase(app.stores)
|
resetDatabase(app.stores)
|
||||||
.then(() => setupDatabase(app.stores))
|
.then(() => setupDatabase(app.stores))
|
||||||
.then(() => app);
|
.then(() => app)
|
||||||
});
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
6
test/fixtures/fake-event-store.js
vendored
Normal file
6
test/fixtures/fake-event-store.js
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = () => ({
|
||||||
|
store: () => Promise.resolve(),
|
||||||
|
getEvents: () => Promise.resolve([]),
|
||||||
|
});
|
@ -1,10 +1,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
const _features = [];
|
const _features = [];
|
||||||
return {
|
return {
|
||||||
getFeature: (name) => {
|
getFeature: name => {
|
||||||
const toggle = _features.find(f => f.name === name);
|
const toggle = _features.find(f => f.name === name);
|
||||||
if (toggle) {
|
if (toggle) {
|
||||||
return Promise.resolve(toggle);
|
return Promise.resolve(toggle);
|
||||||
@ -13,6 +12,6 @@ module.exports = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
getFeatures: () => Promise.resolve(_features),
|
getFeatures: () => Promise.resolve(_features),
|
||||||
addFeature: (feature) => _features.push(feature),
|
addFeature: feature => _features.push(feature),
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -1,15 +1,13 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const NotFoundError = require('../../../../lib/error/notfound-error');
|
const NotFoundError = require('../../lib/error/notfound-error');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
const _strategies = [{ name: 'default', parameters: {} }];
|
const _strategies = [{ name: 'default', parameters: {} }];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getStrategies: () => Promise.resolve(_strategies),
|
getStrategies: () => Promise.resolve(_strategies),
|
||||||
getStrategy: (name) => {
|
getStrategy: name => {
|
||||||
const strategy = _strategies.find(s => s.name === name);
|
const strategy = _strategies.find(s => s.name === name);
|
||||||
if (strategy) {
|
if (strategy) {
|
||||||
return Promise.resolve(strategy);
|
return Promise.resolve(strategy);
|
||||||
@ -17,6 +15,6 @@ module.exports = () => {
|
|||||||
return Promise.reject(new NotFoundError('Not found!'));
|
return Promise.reject(new NotFoundError('Not found!'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addStrategy: (strat) => _strategies.push(strat),
|
addStrategy: strat => _strategies.push(strat),
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -7,8 +7,6 @@ const featureToggleStore = require('./fake-feature-toggle-store');
|
|||||||
const eventStore = require('./fake-event-store');
|
const eventStore = require('./fake-event-store');
|
||||||
const strategyStore = require('./fake-strategies-store');
|
const strategyStore = require('./fake-strategies-store');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createStores: () => {
|
createStores: () => {
|
||||||
const db = {
|
const db = {
|
@ -1,7 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
return {
|
|
||||||
store: () => Promise.resolve(),
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,168 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const test = require('ava');
|
|
||||||
const store = require('./fixtures/store');
|
|
||||||
const supertest = require('supertest');
|
|
||||||
const logger = require('../../../lib/logger');
|
|
||||||
const getApp = require('../../../lib/app');
|
|
||||||
|
|
||||||
const { EventEmitter } = require('events');
|
|
||||||
const eventBus = new EventEmitter();
|
|
||||||
|
|
||||||
test.beforeEach(() => {
|
|
||||||
logger.setLevel('FATAL');
|
|
||||||
});
|
|
||||||
|
|
||||||
function getSetup () {
|
|
||||||
const stores = store.createStores();
|
|
||||||
const app = getApp({
|
|
||||||
baseUriPath: '',
|
|
||||||
stores,
|
|
||||||
eventBus,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
request: supertest(app),
|
|
||||||
stores,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test('should register client', () => {
|
|
||||||
const { request } = getSetup();
|
|
||||||
return request
|
|
||||||
.post('/api/client/register')
|
|
||||||
.send({
|
|
||||||
appName: 'demo',
|
|
||||||
instanceId: 'test',
|
|
||||||
strategies: ['default'],
|
|
||||||
started: Date.now(),
|
|
||||||
interval: 10,
|
|
||||||
})
|
|
||||||
.expect(202);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should require appName field', () => {
|
|
||||||
const { request } = getSetup();
|
|
||||||
return request
|
|
||||||
.post('/api/client/register')
|
|
||||||
.expect(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should require strategies field', () => {
|
|
||||||
const { request } = getSetup();
|
|
||||||
return request
|
|
||||||
.post('/api/client/register')
|
|
||||||
.send({
|
|
||||||
appName: 'demo',
|
|
||||||
instanceId: 'test',
|
|
||||||
// strategies: ['default'],
|
|
||||||
started: Date.now(),
|
|
||||||
interval: 10,
|
|
||||||
})
|
|
||||||
.expect(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should validate client metrics', () => {
|
|
||||||
const { request } = getSetup();
|
|
||||||
return request
|
|
||||||
.post('/api/client/metrics')
|
|
||||||
.send({ random: 'blush' })
|
|
||||||
.expect(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
test('should accept client metrics', () => {
|
|
||||||
const { request } = getSetup();
|
|
||||||
return request
|
|
||||||
.post('/api/client/metrics')
|
|
||||||
.send({
|
|
||||||
appName: 'demo',
|
|
||||||
instanceId: '1',
|
|
||||||
bucket: {
|
|
||||||
start: Date.now(),
|
|
||||||
stop: Date.now(),
|
|
||||||
toggles: {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.expect(202);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return seen toggles even when there is nothing', t => {
|
|
||||||
const { request } = getSetup();
|
|
||||||
return request
|
|
||||||
.get('/api/client/seen-toggles')
|
|
||||||
.expect(200)
|
|
||||||
.expect((res) => {
|
|
||||||
t.true(res.body.length === 0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return list of seen-toggles per app', t => {
|
|
||||||
const { request, stores } = getSetup();
|
|
||||||
const appName = 'asd!23';
|
|
||||||
stores.clientMetricsStore.emit('metrics', {
|
|
||||||
appName,
|
|
||||||
instanceId: 'instanceId',
|
|
||||||
bucket: {
|
|
||||||
start: new Date(),
|
|
||||||
stop: new Date(),
|
|
||||||
toggles: {
|
|
||||||
toggleX: { yes: 123, no: 0 },
|
|
||||||
toggleY: { yes: 123, no: 0 }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return request
|
|
||||||
.get('/api/client/seen-toggles')
|
|
||||||
.expect(200)
|
|
||||||
.expect((res) => {
|
|
||||||
const seenAppsWithToggles = res.body;
|
|
||||||
t.true(seenAppsWithToggles.length === 1);
|
|
||||||
t.true(seenAppsWithToggles[0].appName === appName);
|
|
||||||
t.true(seenAppsWithToggles[0].seenToggles.length === 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return feature-toggles metrics even when there is nothing', t => {
|
|
||||||
const { request } = getSetup();
|
|
||||||
return request
|
|
||||||
.get('/api/client/metrics/feature-toggles')
|
|
||||||
.expect(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return metrics for all toggles', t => {
|
|
||||||
const { request, stores } = getSetup();
|
|
||||||
const appName = 'asd!23';
|
|
||||||
stores.clientMetricsStore.emit('metrics', {
|
|
||||||
appName,
|
|
||||||
instanceId: 'instanceId',
|
|
||||||
bucket: {
|
|
||||||
start: new Date(),
|
|
||||||
stop: new Date(),
|
|
||||||
toggles: {
|
|
||||||
toggleX: { yes: 123, no: 0 },
|
|
||||||
toggleY: { yes: 123, no: 0 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return request
|
|
||||||
.get('/api/client/metrics/feature-toggles')
|
|
||||||
.expect(200)
|
|
||||||
.expect((res) => {
|
|
||||||
const metrics = res.body;
|
|
||||||
t.true(metrics.lastHour !== undefined);
|
|
||||||
t.true(metrics.lastMinute !== undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return list of client applications', t => {
|
|
||||||
const { request } = getSetup();
|
|
||||||
return request
|
|
||||||
.get('/api/client/applications')
|
|
||||||
.expect(200)
|
|
||||||
.expect((res) => {
|
|
||||||
t.true(res.body.applications.length === 0);
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Reference in New Issue
Block a user