mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-31 01:16:01 +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
|
||||
const apiRouter = express.Router(); // eslint-disable-line new-cap
|
||||
routes.createAPI(apiRouter, config);
|
||||
app.use(`${baseUriPath}/api/`, apiRouter);
|
||||
|
||||
// Setup deprecated routes
|
||||
const router = express.Router(); // eslint-disable-line new-cap
|
||||
routes.createLegacy(router, config);
|
||||
app.use(baseUriPath, router);
|
||||
const middleware = routes.router(config);
|
||||
if (!middleware) {
|
||||
throw new Error('Routes invalid');
|
||||
}
|
||||
app.use(`${baseUriPath}/`, middleware);
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
app.use(errorHandler());
|
||||
|
@ -1,11 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('ava');
|
||||
const { test } = require('ava');
|
||||
const express = require('express');
|
||||
const proxyquire = require('proxyquire');
|
||||
const getApp = proxyquire('./app', {
|
||||
'./routes': {
|
||||
createAPI: () => {},
|
||||
createLegacy: () => {},
|
||||
router: () => express.Router(),
|
||||
},
|
||||
});
|
||||
|
||||
|
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';
|
||||
|
||||
const eventDiffer = require('../event-differ');
|
||||
const { Router } = require('express');
|
||||
|
||||
const eventDiffer = require('../../event-differ');
|
||||
const version = 1;
|
||||
|
||||
module.exports = function (app, config) {
|
||||
module.exports.router = function(config) {
|
||||
const { eventStore } = config.stores;
|
||||
const router = Router();
|
||||
|
||||
app.get('/events', (req, res) => {
|
||||
router.get('/', (req, res) => {
|
||||
eventStore.getEvents().then(events => {
|
||||
eventDiffer.addDiffs(events);
|
||||
res.json({ version, events });
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/events/:name', (req, res) => {
|
||||
router.get('/:name', (req, res) => {
|
||||
const toggleName = req.params.name;
|
||||
eventStore.getEventsFilterByName(toggleName).then(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,19 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('ava');
|
||||
const store = require('./fixtures/store');
|
||||
const { test } = require('ava');
|
||||
const store = require('./../../../test/fixtures/store');
|
||||
const supertest = require('supertest');
|
||||
const logger = require('../../../lib/logger');
|
||||
const getApp = require('../../../lib/app');
|
||||
const logger = require('../../logger');
|
||||
const getApp = require('../../app');
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const eventBus = new EventEmitter();
|
||||
|
||||
test.beforeEach(() => {
|
||||
test.beforeEach(() => {
|
||||
logger.setLevel('FATAL');
|
||||
});
|
||||
|
||||
function getSetup () {
|
||||
function getSetup() {
|
||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||
const stores = store.createStores();
|
||||
const app = getApp({
|
||||
@ -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();
|
||||
return request
|
||||
.get(`${base}/features`)
|
||||
.get(`${base}/api/admin/features`)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
.expect(res => {
|
||||
t.true(res.body.features.length === 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('should get one getFeature', t => {
|
||||
t.plan(1);
|
||||
const { request, featureToggleStore, base } = getSetup();
|
||||
featureToggleStore.addFeature({ name: 'test_', strategies: [{ name: 'default_' }] });
|
||||
featureToggleStore.addFeature({
|
||||
name: 'test_',
|
||||
strategies: [{ name: 'default_' }],
|
||||
});
|
||||
|
||||
return request
|
||||
.get(`${base}/features`)
|
||||
.get(`${base}/api/admin/features`)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
.expect(res => {
|
||||
t.true(res.body.features.length === 1);
|
||||
});
|
||||
});
|
||||
|
||||
test('should add version numbers for /features', t => {
|
||||
t.plan(1);
|
||||
const { request, featureToggleStore, base } = getSetup();
|
||||
featureToggleStore.addFeature({ name: 'test2', strategies: [{ name: 'default' }] });
|
||||
featureToggleStore.addFeature({
|
||||
name: 'test2',
|
||||
strategies: [{ name: 'default' }],
|
||||
});
|
||||
|
||||
return request
|
||||
.get(`${base}/features`)
|
||||
.get(`${base}/api/admin/features`)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
.expect(res => {
|
||||
t.true(res.body.version === 1);
|
||||
});
|
||||
});
|
||||
|
||||
test('should require at least one strategy when creating a feature toggle', t => {
|
||||
t.plan(0);
|
||||
const { request, base } = getSetup();
|
||||
|
||||
return request
|
||||
.post(`${base}/features`)
|
||||
.post(`${base}/api/admin/features`)
|
||||
.send({ name: 'sample.missing.strategy' })
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(400)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
test('should require at least one strategy when updating a feature toggle', t => {
|
||||
t.plan(0);
|
||||
const { request, featureToggleStore, base } = getSetup();
|
||||
featureToggleStore.addFeature({ name: 'ts', strategies: [{ name: 'default' }] });
|
||||
featureToggleStore.addFeature({
|
||||
name: 'ts',
|
||||
strategies: [{ name: 'default' }],
|
||||
});
|
||||
|
||||
return request
|
||||
.put(`${base}/features/ts`)
|
||||
.put(`${base}/api/admin/features/ts`)
|
||||
.send({ name: 'ts' })
|
||||
.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';
|
||||
|
||||
const logger = require('../logger');
|
||||
const logger = require('../../logger');
|
||||
|
||||
const catchLogAndSendErrorResponse = (err, res) => {
|
||||
logger.error(err);
|
@ -3,18 +3,16 @@
|
||||
const joi = require('joi');
|
||||
|
||||
const strategySchema = joi.object().keys({
|
||||
name: joi.string()
|
||||
.regex(/^[a-zA-Z0-9\\.\\-]{3,100}$/)
|
||||
.required(),
|
||||
name: joi.string().regex(/^[a-zA-Z0-9\\.\\-]{3,100}$/).required(),
|
||||
description: joi.string(),
|
||||
parameters: joi.array()
|
||||
.required()
|
||||
.items(joi.object().keys({
|
||||
parameters: joi.array().required().items(
|
||||
joi.object().keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().required(),
|
||||
description: joi.string().allow(''),
|
||||
required: joi.boolean(),
|
||||
})),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
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,14 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('ava');
|
||||
const store = require('./fixtures/store');
|
||||
const { test } = require('ava');
|
||||
const store = require('./../../../test/fixtures/store');
|
||||
const supertest = require('supertest');
|
||||
const getApp = require('../../../lib/app');
|
||||
const getApp = require('../../app');
|
||||
const logger = require('../../logger');
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const eventBus = new EventEmitter();
|
||||
|
||||
function getSetup () {
|
||||
function getSetup() {
|
||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||
const stores = store.createStores();
|
||||
const app = getApp({
|
||||
@ -24,89 +25,101 @@ function getSetup () {
|
||||
};
|
||||
}
|
||||
|
||||
test.beforeEach(() => {
|
||||
logger.setLevel('FATAL');
|
||||
});
|
||||
|
||||
test('should add version numbers for /stategies', t => {
|
||||
t.plan(1);
|
||||
const { request, base } = getSetup();
|
||||
|
||||
return request
|
||||
.get(`${base}/api/strategies`)
|
||||
.get(`${base}/api/admin/strategies`)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
.expect(res => {
|
||||
t.true(res.body.version === 1);
|
||||
});
|
||||
});
|
||||
|
||||
test('should require a name when creating a new stratey', t => {
|
||||
t.plan(1);
|
||||
const { request, base } = getSetup();
|
||||
|
||||
return request
|
||||
.post(`${base}/api/strategies`)
|
||||
.post(`${base}/api/admin/strategies`)
|
||||
.send({})
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
.expect(res => {
|
||||
t.true(res.body.name === 'ValidationError');
|
||||
});
|
||||
});
|
||||
|
||||
test('should require parameters array when creating a new stratey', t => {
|
||||
t.plan(1);
|
||||
const { request, base } = getSetup();
|
||||
|
||||
return request
|
||||
.post(`${base}/api/strategies`)
|
||||
.post(`${base}/api/admin/strategies`)
|
||||
.send({ name: 'TestStrat' })
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
.expect(res => {
|
||||
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();
|
||||
|
||||
return request
|
||||
.post(`${base}/api/strategies`)
|
||||
.post(`${base}/api/admin/strategies`)
|
||||
.send({ name: 'TestStrat', parameters: [] })
|
||||
.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();
|
||||
strategyStore.addStrategy({ name: 'Testing', parameters: [] });
|
||||
|
||||
return request
|
||||
.post(`${base}/api/strategies`)
|
||||
.post(`${base}/api/admin/strategies`)
|
||||
.send({ name: 'Testing', parameters: [] })
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
test('should update strategy', () => {
|
||||
test('should update strategy', t => {
|
||||
t.plan(0);
|
||||
const name = 'AnotherStrat';
|
||||
const { request, base, strategyStore } = getSetup();
|
||||
strategyStore.addStrategy({ name, parameters: [] });
|
||||
|
||||
return request
|
||||
.put(`${base}/api/strategies/${name}`)
|
||||
.put(`${base}/api/admin/strategies/${name}`)
|
||||
.send({ name, parameters: [], description: 'added' })
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
test('should not update uknown strategy', () => {
|
||||
test('should not update uknown strategy', t => {
|
||||
t.plan(0);
|
||||
const name = 'UnknownStrat';
|
||||
const { request, base } = getSetup();
|
||||
|
||||
return request
|
||||
.put(`${base}/api/strategies/${name}`)
|
||||
.put(`${base}/api/admin/strategies/${name}`)
|
||||
.send({ name, parameters: [], description: 'added' })
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
test('should validate format when updating strategy', () => {
|
||||
test('should validate format when updating strategy', t => {
|
||||
t.plan(0);
|
||||
const name = 'AnotherStrat';
|
||||
const { request, base, strategyStore } = getSetup();
|
||||
strategyStore.addStrategy({ name, parameters: [] });
|
||||
|
||||
return request
|
||||
.put(`${base}/api/strategies/${name}`)
|
||||
.send({ })
|
||||
.put(`${base}/api/admin/strategies/${name}`)
|
||||
.send({})
|
||||
.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';
|
||||
|
||||
const { Router } = require('express');
|
||||
const prometheusRegister = require('prom-client/lib/register');
|
||||
|
||||
module.exports = function (app, config) {
|
||||
exports.router = config => {
|
||||
const router = Router();
|
||||
|
||||
if (config.serverMetrics) {
|
||||
app.get('/internal-backstage/prometheus', (req, res) => {
|
||||
router.get('/prometheus', (req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end(prometheusRegister.metrics());
|
||||
});
|
||||
}
|
||||
|
||||
return router;
|
||||
};
|
||||
|
@ -1,19 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('ava');
|
||||
const store = require('./fixtures/store');
|
||||
const { test } = require('ava');
|
||||
const store = require('./../../test/fixtures/store');
|
||||
const supertest = require('supertest');
|
||||
const logger = require('../../../lib/logger');
|
||||
const getApp = require('../../../lib/app');
|
||||
const logger = require('../logger');
|
||||
const getApp = require('../app');
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const eventBus = new EventEmitter();
|
||||
|
||||
test.beforeEach(() => {
|
||||
test.beforeEach(() => {
|
||||
logger.setLevel('FATAL');
|
||||
});
|
||||
|
||||
test('should use enable prometheus', t => {
|
||||
t.plan(0);
|
||||
const stores = store.createStores();
|
||||
const app = getApp({
|
||||
baseUriPath: '',
|
||||
@ -27,5 +28,5 @@ test('should use enable prometheus', t => {
|
||||
return request
|
||||
.get('/internal-backstage/prometheus')
|
||||
.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';
|
||||
|
||||
const test = require('ava');
|
||||
const store = require('./fixtures/store');
|
||||
const { test } = require('ava');
|
||||
const store = require('./../../../test/fixtures/store');
|
||||
const supertest = require('supertest');
|
||||
const logger = require('../../../lib/logger');
|
||||
const getApp = require('../../../lib/app');
|
||||
const logger = require('../../logger');
|
||||
const getApp = require('../../app');
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const eventBus = new EventEmitter();
|
||||
@ -13,7 +13,7 @@ test.beforeEach(() => {
|
||||
logger.setLevel('FATAL');
|
||||
});
|
||||
|
||||
function getSetup () {
|
||||
function getSetup() {
|
||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||
const stores = store.createStores();
|
||||
const app = getApp({
|
||||
@ -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();
|
||||
return request
|
||||
.get(`${base}/api/`)
|
||||
.get(`${base}/api/client/features`)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
t.true(res.body.links['feature-toggles'].uri === '/api/features');
|
||||
.expect(res => {
|
||||
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({
|
||||
appName: joi.string().required(),
|
||||
instanceId: joi.string().required(),
|
||||
bucket: joi.object().required()
|
||||
.keys({
|
||||
bucket: joi.object().required().keys({
|
||||
start: joi.date().required(),
|
||||
stop: joi.date().required(),
|
||||
toggles: joi.object(),
|
||||
@ -16,9 +15,7 @@ const clientMetricsSchema = joi.object().keys({
|
||||
const clientRegisterSchema = joi.object().keys({
|
||||
appName: joi.string().required(),
|
||||
instanceId: joi.string().required(),
|
||||
strategies: joi.array()
|
||||
.required()
|
||||
.items(joi.string(), joi.any().strip()),
|
||||
strategies: joi.array().required().items(joi.string(), joi.any().strip()),
|
||||
started: joi.date().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';
|
||||
|
||||
const logger = require('../logger');
|
||||
const { Router } = require('express');
|
||||
|
||||
module.exports = function (app, config) {
|
||||
app.get('/health', (req, res) => {
|
||||
config.stores.db.select(1)
|
||||
exports.router = function(config) {
|
||||
const router = Router();
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
config.stores.db
|
||||
.select(1)
|
||||
.from('features')
|
||||
.then(() => res.json({ health: 'GOOD' }))
|
||||
.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' });
|
||||
});
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
|
@ -1,20 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('ava');
|
||||
const store = require('./fixtures/store');
|
||||
const { test } = require('ava');
|
||||
const store = require('./../../test/fixtures/store');
|
||||
const supertest = require('supertest');
|
||||
const logger = require('../../../lib/logger');
|
||||
const getApp = require('../../../lib/app');
|
||||
const logger = require('../logger');
|
||||
const getApp = require('../app');
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const eventBus = new EventEmitter();
|
||||
|
||||
test.beforeEach(() => {
|
||||
test.beforeEach(() => {
|
||||
logger.setLevel('FATAL');
|
||||
});
|
||||
|
||||
|
||||
function getSetup () {
|
||||
function getSetup() {
|
||||
const stores = store.createStores();
|
||||
const db = stores.db;
|
||||
const app = getApp({
|
||||
@ -30,34 +29,28 @@ function getSetup () {
|
||||
}
|
||||
|
||||
test('should give 500 when db is failing', t => {
|
||||
t.plan(2);
|
||||
const { request, db } = getSetup();
|
||||
db.select = () => ({
|
||||
from: () => Promise.reject(new Error('db error')),
|
||||
});
|
||||
return request
|
||||
.get('/health')
|
||||
.expect(500)
|
||||
.expect((res) => {
|
||||
t.true(res.status === 500);
|
||||
t.true(res.body.health === 'BAD');
|
||||
});
|
||||
return request.get('/health').expect(500).expect(res => {
|
||||
t.true(res.status === 500);
|
||||
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();
|
||||
return request
|
||||
.get('/health')
|
||||
.expect(200);
|
||||
return request.get('/health').expect(200);
|
||||
});
|
||||
|
||||
test('should give health=GOOD when db is not failing', t => {
|
||||
t.plan(2);
|
||||
const { request } = getSetup();
|
||||
return request
|
||||
.get('/health')
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
t.true(res.status === 200);
|
||||
t.true(res.body.health === 'GOOD');
|
||||
});
|
||||
return request.get('/health').expect(200).expect(res => {
|
||||
t.true(res.status === 200);
|
||||
t.true(res.body.health === 'GOOD');
|
||||
});
|
||||
});
|
||||
|
@ -1,18 +1,44 @@
|
||||
'use strict';
|
||||
|
||||
const { Router } = require('express');
|
||||
|
||||
exports.createAPI = function (router, config) {
|
||||
require('./api')(router, config);
|
||||
require('./event')(router, config);
|
||||
require('./feature')(router, config);
|
||||
require('./feature-archive')(router, config);
|
||||
require('./strategy')(router, config);
|
||||
require('./health-check')(router, config);
|
||||
require('./metrics')(router, config);
|
||||
};
|
||||
const adminApi = require('./admin-api');
|
||||
const clientApi = require('./client-api');
|
||||
const clientFeatures = require('./client-api/feature.js');
|
||||
|
||||
exports.createLegacy = function (router, config) {
|
||||
require('./feature')(router, config);
|
||||
require('./health-check')(router, config);
|
||||
require('./backstage')(router, config);
|
||||
const health = require('./health-check');
|
||||
const backstage = require('./backstage.js');
|
||||
|
||||
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 proxyquire = require('proxyquire');
|
||||
const express = require('express');
|
||||
|
||||
const getApp = proxyquire('./app', {
|
||||
'./routes': {
|
||||
createAPI: () => {},
|
||||
createLegacy: () => {},
|
||||
router: () => express.Router(),
|
||||
},
|
||||
});
|
||||
|
||||
@ -35,6 +35,7 @@ const serverImpl = proxyquire('./server-impl', {
|
||||
test('should call preHook', async t => {
|
||||
let called = 0;
|
||||
await serverImpl.start({
|
||||
port: 0,
|
||||
preHook: () => {
|
||||
called++;
|
||||
},
|
||||
@ -44,8 +45,11 @@ test('should call preHook', async t => {
|
||||
|
||||
test('should call preRouterHook', async t => {
|
||||
let called = 0;
|
||||
await serverImpl.start({ preRouterHook: () => {
|
||||
called++;
|
||||
} });
|
||||
await serverImpl.start({
|
||||
port: 0,
|
||||
preRouterHook: () => {
|
||||
called++;
|
||||
},
|
||||
});
|
||||
t.true(called === 1);
|
||||
});
|
||||
|
10
package.json
10
package.json
@ -39,7 +39,7 @@
|
||||
"db-migrate": "db-migrate",
|
||||
"lint": "eslint lib",
|
||||
"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:watch": "npm run test -- --watch",
|
||||
"test:pg-virtualenv": "pg_virtualenv npm run test:pg-virtualenv-chai",
|
||||
@ -81,11 +81,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^7.0.5",
|
||||
"ava": "^0.18.2",
|
||||
"ava": "^0.19.1",
|
||||
"coveralls": "^2.11.16",
|
||||
"eslint": "^3.16.1",
|
||||
"eslint-config-finn": "^1.0.0-beta.1",
|
||||
"eslint": "^4.0.0",
|
||||
"eslint-config-finn": "^1.0.2",
|
||||
"eslint-config-finn-prettier": "^2.0.0",
|
||||
"nyc": "^10.1.2",
|
||||
"prettier": "^1.4.4",
|
||||
"proxyquire": "^1.7.11",
|
||||
"sinon": "^1.17.7",
|
||||
"superagent": "^3.5.0",
|
||||
|
@ -1,26 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('ava');
|
||||
const { setupApp } = require('./helpers/test-helper');
|
||||
const logger = require('../../lib/logger');
|
||||
const { test } = require('ava');
|
||||
const { setupApp } = require('./../../helpers/test-helper');
|
||||
const logger = require('../../../../lib/logger');
|
||||
|
||||
test.beforeEach(() => {
|
||||
test.beforeEach(() => {
|
||||
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');
|
||||
return request
|
||||
.get('/api/events')
|
||||
.get('/api/admin/events')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.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');
|
||||
return request
|
||||
.get('/api/events/myname')
|
||||
.get('/api/admin/events/myname')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.then(destroy);
|
@ -1,38 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('ava');
|
||||
const { setupApp } = require('./helpers/test-helper');
|
||||
const logger = require('../../lib/logger');
|
||||
const { test } = require('ava');
|
||||
const { setupApp } = require('./../../helpers/test-helper');
|
||||
const logger = require('../../../../lib/logger');
|
||||
|
||||
test.beforeEach(() => {
|
||||
test.beforeEach(() => {
|
||||
logger.setLevel('FATAL');
|
||||
});
|
||||
|
||||
test.serial('returns three archived toggles', async t => {
|
||||
t.plan(1);
|
||||
const { request, destroy } = await setupApp('archive_serial');
|
||||
return request
|
||||
.get('/api/archive/features')
|
||||
.get('/api/admin/archive/features')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
.expect(res => {
|
||||
t.true(res.body.features.length === 3);
|
||||
})
|
||||
.then(destroy);
|
||||
});
|
||||
|
||||
test.serial('revives a feature by name', async t => {
|
||||
const { request, destroy } = await setupApp('archive_serial');
|
||||
t.plan(0);
|
||||
const { request, destroy } = await setupApp('archive_serial');
|
||||
return request
|
||||
.post('/api/archive/revive/featureArchivedX')
|
||||
.post('/api/admin/archive/revive/featureArchivedX')
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.then(destroy);
|
||||
});
|
||||
|
||||
test.serial('must set name when reviving toggle', async t => {
|
||||
const { request, destroy } = await setupApp('archive_serial');
|
||||
return request
|
||||
.post('/api/archive/revive/')
|
||||
.expect(404)
|
||||
.then(destroy);
|
||||
t.plan(0);
|
||||
const { request, destroy } = await setupApp('archive_serial');
|
||||
return request.post('/api/admin/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,14 +1,16 @@
|
||||
'use strict';
|
||||
const test = require('ava');
|
||||
const { setupApp } = require('./helpers/test-helper');
|
||||
const logger = require('../../lib/logger');
|
||||
|
||||
test.beforeEach(() => {
|
||||
const { test } = require('ava');
|
||||
const { setupApp } = require('./../../helpers/test-helper');
|
||||
const logger = require('../../../../lib/logger');
|
||||
|
||||
test.beforeEach(() => {
|
||||
logger.setLevel('FATAL');
|
||||
});
|
||||
|
||||
test.serial('should register client', async (t) => {
|
||||
const { request, destroy } = await setupApp('metrics_serial');
|
||||
test.serial('should register client', async t => {
|
||||
t.plan(0);
|
||||
const { request, destroy } = await setupApp('metrics_serial');
|
||||
return request
|
||||
.post('/api/client/register')
|
||||
.send({
|
||||
@ -16,35 +18,39 @@ test.serial('should register client', async (t) => {
|
||||
instanceId: 'test',
|
||||
strategies: ['default'],
|
||||
started: Date.now(),
|
||||
interval: 10
|
||||
interval: 10,
|
||||
})
|
||||
.expect(202)
|
||||
.then(destroy);
|
||||
});
|
||||
|
||||
test.serial('should allow client to register multiple times', async (t) => {
|
||||
const { request, destroy } = await setupApp('metrics_serial');
|
||||
test.serial('should allow client to register multiple times', async t => {
|
||||
t.plan(0);
|
||||
const { request, destroy } = await setupApp('metrics_serial');
|
||||
const clientRegistration = {
|
||||
appName: 'multipleRegistration',
|
||||
instanceId: 'test',
|
||||
strategies: ['default', 'another'],
|
||||
started: Date.now(),
|
||||
interval: 10
|
||||
appName: 'multipleRegistration',
|
||||
instanceId: 'test',
|
||||
strategies: ['default', 'another'],
|
||||
started: Date.now(),
|
||||
interval: 10,
|
||||
};
|
||||
|
||||
return request
|
||||
.post('/api/client/register')
|
||||
.send(clientRegistration)
|
||||
.expect(202)
|
||||
.then(() => request
|
||||
.post('/api/client/register')
|
||||
.send(clientRegistration)
|
||||
.expect(202))
|
||||
.then(() =>
|
||||
request
|
||||
.post('/api/client/register')
|
||||
.send(clientRegistration)
|
||||
.expect(202)
|
||||
)
|
||||
.then(destroy);
|
||||
});
|
||||
|
||||
test.serial('should accept client metrics', async t => {
|
||||
const { request, destroy } = await setupApp('metrics_serial');
|
||||
t.plan(0);
|
||||
const { request, destroy } = await setupApp('metrics_serial');
|
||||
return request
|
||||
.post('/api/client/metrics')
|
||||
.send({
|
||||
@ -53,19 +59,20 @@ test.serial('should accept client metrics', async t => {
|
||||
bucket: {
|
||||
start: Date.now(),
|
||||
stop: Date.now(),
|
||||
toggles: {}
|
||||
}
|
||||
toggles: {},
|
||||
},
|
||||
})
|
||||
.expect(202)
|
||||
.then(destroy);
|
||||
});
|
||||
|
||||
test.serial('should get application details', async t => {
|
||||
const { request, destroy } = await setupApp('metrics_serial');
|
||||
t.plan(3);
|
||||
const { request, destroy } = await setupApp('metrics_serial');
|
||||
return request
|
||||
.get('/api/client/applications/demo-app-1')
|
||||
.get('/api/admin/metrics/applications/demo-app-1')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect((res) => {
|
||||
.expect(res => {
|
||||
t.true(res.status === 200);
|
||||
t.true(res.body.appName === 'demo-app-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 => {
|
||||
const { request, destroy } = await setupApp('metrics_serial');
|
||||
t.plan(2);
|
||||
const { request, destroy } = await setupApp('metrics_serial');
|
||||
return request
|
||||
.get('/api/client/applications')
|
||||
.get('/api/admin/metrics/applications')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect((res) => {
|
||||
.expect(res => {
|
||||
t.true(res.status === 200);
|
||||
t.true(res.body.applications.length === 2);
|
||||
})
|
@ -1,102 +1,124 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('ava');
|
||||
const { setupApp } = require('./helpers/test-helper');
|
||||
const logger = require('../../lib/logger');
|
||||
const { test } = require('ava');
|
||||
const { setupApp } = require('./../../helpers/test-helper');
|
||||
const logger = require('../../../../lib/logger');
|
||||
|
||||
test.beforeEach(() => {
|
||||
test.beforeEach(() => {
|
||||
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');
|
||||
return request
|
||||
.get('/api/strategies')
|
||||
.get('/api/admin/strategies')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
t.true(res.body.strategies.length === 2, 'expected to have two strategies');
|
||||
.expect(res => {
|
||||
t.true(
|
||||
res.body.strategies.length === 2,
|
||||
'expected to have two strategies'
|
||||
);
|
||||
})
|
||||
.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');
|
||||
return request
|
||||
.get('/api/strategies/default')
|
||||
.get('/api/admin/strategies/default')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.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');
|
||||
return request
|
||||
.get('/api/strategies/mystrategy')
|
||||
.get('/api/admin/strategies/mystrategy')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(404)
|
||||
.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');
|
||||
return request
|
||||
.post('/api/strategies')
|
||||
.send({ name: 'myCustomStrategy', description: 'Best strategy ever.', parameters: [] })
|
||||
.post('/api/admin/strategies')
|
||||
.send({
|
||||
name: 'myCustomStrategy',
|
||||
description: 'Best strategy ever.',
|
||||
parameters: [],
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201)
|
||||
.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');
|
||||
return request
|
||||
.post('/api/strategies')
|
||||
.post('/api/admin/strategies')
|
||||
.send({ name: '' })
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(400)
|
||||
.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');
|
||||
return request
|
||||
.post('/api/strategies')
|
||||
.post('/api/admin/strategies')
|
||||
.send({ name: 'default', parameters: [] })
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(403)
|
||||
.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');
|
||||
return request
|
||||
.delete('/api/strategies/usersWithEmail')
|
||||
.delete('/api/admin/strategies/usersWithEmail')
|
||||
.expect(200)
|
||||
.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);
|
||||
return request
|
||||
.delete('/api/strategies/unknown')
|
||||
.expect(404);
|
||||
.delete('/api/admin/strategies/unknown')
|
||||
.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');
|
||||
return request
|
||||
.put('/api/strategies/default')
|
||||
.send({ name: 'default', description: 'Default is the best!', parameters: [] })
|
||||
.put('/api/admin/strategies/default')
|
||||
.send({
|
||||
name: 'default',
|
||||
description: 'Default is the best!',
|
||||
parameters: [],
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.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');
|
||||
return request
|
||||
.put('/api/strategies/unknown')
|
||||
.put('/api/admin/strategies/unknown')
|
||||
.send({ name: 'unkown', parameters: [] })
|
||||
.set('Content-Type', 'application/json')
|
||||
.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,16 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('ava');
|
||||
const { test } = require('ava');
|
||||
const { setupApp } = require('./helpers/test-helper');
|
||||
const logger = require('../../lib/logger');
|
||||
|
||||
test.beforeEach(() => {
|
||||
test.beforeEach(() => {
|
||||
logger.setLevel('FATAL');
|
||||
});
|
||||
|
||||
test('returns health good', async (t) => {
|
||||
test('returns health good', async t => {
|
||||
t.plan(0);
|
||||
const { request, destroy } = await setupApp('health');
|
||||
return request.get('/health')
|
||||
return request
|
||||
.get('/health')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect('{"health":"GOOD"}')
|
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
function getDatabaseUrl () {
|
||||
function getDatabaseUrl() {
|
||||
if (process.env.TEST_DATABASE_URL) {
|
||||
return process.env.TEST_DATABASE_URL;
|
||||
} else {
|
||||
|
@ -18,16 +18,21 @@ delete process.env.DATABASE_URL;
|
||||
const { EventEmitter } = require('events');
|
||||
const eventBus = new EventEmitter();
|
||||
|
||||
function createApp (databaseSchema = 'test') {
|
||||
function createApp(databaseSchema = 'test') {
|
||||
const options = {
|
||||
databaseUrl: require('./database-config').getDatabaseUrl(),
|
||||
databaseSchema,
|
||||
minPool: 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(() => {
|
||||
db.destroy();
|
||||
@ -36,14 +41,14 @@ function createApp (databaseSchema = 'test') {
|
||||
return {
|
||||
stores,
|
||||
request: supertest(app),
|
||||
destroy () {
|
||||
destroy() {
|
||||
return stores.db.destroy();
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function createStrategies (stores) {
|
||||
function createStrategies(stores) {
|
||||
return [
|
||||
{
|
||||
name: 'default',
|
||||
@ -52,15 +57,14 @@ function createStrategies (stores) {
|
||||
},
|
||||
{
|
||||
name: 'usersWithEmail',
|
||||
description: 'Active for users defined in the comma-separated emails-parameter.',
|
||||
parameters: [
|
||||
{ name: 'emails', type: 'string' },
|
||||
],
|
||||
description:
|
||||
'Active for users defined in the comma-separated emails-parameter.',
|
||||
parameters: [{ name: 'emails', type: 'string' }],
|
||||
},
|
||||
].map(strategy => stores.strategyStore._createStrategy(strategy));
|
||||
}
|
||||
|
||||
function createApplications (stores) {
|
||||
function createApplications(stores) {
|
||||
return [
|
||||
{
|
||||
appName: 'demo-app-1',
|
||||
@ -74,7 +78,7 @@ function createApplications (stores) {
|
||||
].map(client => stores.clientApplicationsStore.upsert(client));
|
||||
}
|
||||
|
||||
function createClientInstance (stores) {
|
||||
function createClientInstance(stores) {
|
||||
return [
|
||||
{
|
||||
appName: 'demo-app-1',
|
||||
@ -93,7 +97,7 @@ function createClientInstance (stores) {
|
||||
].map(client => stores.clientInstanceStore.insert(client));
|
||||
}
|
||||
|
||||
function createFeatures (stores) {
|
||||
function createFeatures(stores) {
|
||||
return [
|
||||
{
|
||||
name: 'featureX',
|
||||
@ -105,23 +109,27 @@ function createFeatures (stores) {
|
||||
name: 'featureY',
|
||||
description: 'soon to be the #1 feature',
|
||||
enabled: false,
|
||||
strategies: [{
|
||||
name: 'baz',
|
||||
parameters: {
|
||||
foo: 'bar',
|
||||
strategies: [
|
||||
{
|
||||
name: 'baz',
|
||||
parameters: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
}],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'featureZ',
|
||||
description: 'terrible feature',
|
||||
enabled: true,
|
||||
strategies: [{
|
||||
name: 'baz',
|
||||
parameters: {
|
||||
foo: 'rab',
|
||||
strategies: [
|
||||
{
|
||||
name: 'baz',
|
||||
parameters: {
|
||||
foo: 'rab',
|
||||
},
|
||||
},
|
||||
}],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'featureArchivedX',
|
||||
@ -135,29 +143,33 @@ function createFeatures (stores) {
|
||||
description: 'soon to be the #1 feature',
|
||||
enabled: false,
|
||||
archived: true,
|
||||
strategies: [{
|
||||
name: 'baz',
|
||||
parameters: {
|
||||
foo: 'bar',
|
||||
strategies: [
|
||||
{
|
||||
name: 'baz',
|
||||
parameters: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
}],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'featureArchivedZ',
|
||||
description: 'terrible feature',
|
||||
enabled: true,
|
||||
archived: true,
|
||||
strategies: [{
|
||||
name: 'baz',
|
||||
parameters: {
|
||||
foo: 'rab',
|
||||
strategies: [
|
||||
{
|
||||
name: 'baz',
|
||||
parameters: {
|
||||
foo: 'rab',
|
||||
},
|
||||
},
|
||||
}],
|
||||
],
|
||||
},
|
||||
].map(feature => stores.featureToggleStore._createFeature(feature));
|
||||
}
|
||||
|
||||
function resetDatabase (stores) {
|
||||
function resetDatabase(stores) {
|
||||
return Promise.all([
|
||||
stores.db('strategies').del(),
|
||||
stores.db('features').del(),
|
||||
@ -166,20 +178,22 @@ function resetDatabase (stores) {
|
||||
]);
|
||||
}
|
||||
|
||||
function setupDatabase (stores) {
|
||||
function setupDatabase(stores) {
|
||||
return Promise.all(
|
||||
createStrategies(stores)
|
||||
.concat(createFeatures(stores)
|
||||
.concat(createClientInstance(stores))
|
||||
.concat(createApplications(stores))));
|
||||
createStrategies(stores).concat(
|
||||
createFeatures(stores)
|
||||
.concat(createClientInstance(stores))
|
||||
.concat(createApplications(stores))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setupApp (name) {
|
||||
return createApp(name).then((app) => {
|
||||
return resetDatabase(app.stores)
|
||||
setupApp(name) {
|
||||
return createApp(name).then(app =>
|
||||
resetDatabase(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';
|
||||
|
||||
|
||||
module.exports = () => {
|
||||
module.exports = () => {
|
||||
const _features = [];
|
||||
return {
|
||||
getFeature: (name) => {
|
||||
getFeature: name => {
|
||||
const toggle = _features.find(f => f.name === name);
|
||||
if (toggle) {
|
||||
return Promise.resolve(toggle);
|
||||
@ -13,6 +12,6 @@ module.exports = () => {
|
||||
}
|
||||
},
|
||||
getFeatures: () => Promise.resolve(_features),
|
||||
addFeature: (feature) => _features.push(feature),
|
||||
addFeature: feature => _features.push(feature),
|
||||
};
|
||||
};
|
@ -3,12 +3,12 @@
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
class FakeMetricsStore extends EventEmitter {
|
||||
getMetricsLastHour () {
|
||||
getMetricsLastHour() {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
insert () {
|
||||
insert() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FakeMetricsStore;
|
||||
module.exports = FakeMetricsStore;
|
@ -1,15 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const NotFoundError = require('../../../../lib/error/notfound-error');
|
||||
|
||||
|
||||
const NotFoundError = require('../../lib/error/notfound-error');
|
||||
|
||||
module.exports = () => {
|
||||
const _strategies = [{ name: 'default', parameters: {} }];
|
||||
|
||||
return {
|
||||
getStrategies: () => Promise.resolve(_strategies),
|
||||
getStrategy: (name) => {
|
||||
getStrategy: name => {
|
||||
const strategy = _strategies.find(s => s.name === name);
|
||||
if (strategy) {
|
||||
return Promise.resolve(strategy);
|
||||
@ -17,6 +15,6 @@ module.exports = () => {
|
||||
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 strategyStore = require('./fake-strategies-store');
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
createStores: () => {
|
||||
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