1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-01 00:08:27 +01:00

Merge pull request #168 from Unleash/ava

Ava
This commit is contained in:
Sveinung Røsaker 2016-11-13 16:16:02 +01:00 committed by GitHub
commit 41fc1d6b0c
30 changed files with 851 additions and 838 deletions

1
.gitignore vendored
View File

@ -39,3 +39,4 @@ unleash-server.tar.gz
jsconfig.json
typings
.vscode
.nyc_output

View File

@ -8,8 +8,8 @@ before_script:
- psql -c 'create database unleash_test;' -U postgres
script:
- npm install
- npm run test
- npm run test:coverage
after_success:
- npm run test:coverage-report
notifications:
slack:

View File

@ -1,6 +1,6 @@
# unleash
__Warning: We are in the process of splitting up unleash into multiple packages in this repository, if you want to test the previous package see [previous tag](https://github.com/finn-no/unleash/tree/v1.0.0-alpha.2) __
__Warning: We are in the process of splitting up unleash into multiple packages in this repository, if you want to test the previous package see [previous tag](https://github.com/unleash/unleash/tree/v1.0.0-alpha.2) __
[![Build Status](https://travis-ci.org/Unleash/unleash.svg?branch=master)](https://travis-ci.org/Unleash/unleash)
[![Coverage Status](https://coveralls.io/repos/github/Unleash/unleash/badge.svg?branch=master)](https://coveralls.io/github/Unleash/unleash?branch=master)
@ -12,11 +12,11 @@ __Warning: We are in the process of splitting up unleash into multiple packages
This repo contains the unleash-server, which contains the admin UI and a place to ask for the status of features. In order to make use of unleash you will also need a client implementation.
Known client implementations:
- [unleash-client-java](https://github.com/finn-no/unleash-client-java)
- [unleash-client-node](https://github.com/finn-no/unleash-client-node)
- [unleash-client-java](https://github.com/unleash/unleash-client-java)
- [unleash-client-node](https://github.com/unleash/unleash-client-node)
## Project details
- [Project Roadmap](https://github.com/finn-no/unleash/wiki/Roadmap)
- [Project Roadmap](https://github.com/unleash/unleash/wiki/Roadmap)
## Run with docker
We have set up docker-compose to start postgres and the unleash server together. This makes it really fast to start up

3
app.js
View File

@ -9,6 +9,7 @@ const log4js = require('log4js');
const logger = require('./lib/logger');
const routes = require('./lib/routes');
const path = require('path');
const errorHandler = require('errorhandler');
module.exports = function (config) {
const app = express();
@ -47,7 +48,7 @@ module.exports = function (config) {
app.use(baseUriPath, router);
if (process.env.NODE_ENV !== 'production') {
app.use(require('errorhandler')());
app.use(errorHandler());
}
return app;

View File

@ -2,12 +2,12 @@
const knex = require('knex');
module.exports.createDb = function (databaseConnection, schema = 'public') {
module.exports.createDb = function ({ databaseUri, poolMin = 2, poolMax = 20, databaseSchema = 'public' }) {
const db = knex({
client: 'pg',
connection: databaseConnection,
pool: { min: 2, max: 20 },
searchPath: schema,
connection: databaseUri,
pool: { min: poolMin, max: poolMax },
searchPath: databaseSchema,
});
return db;

View File

@ -9,7 +9,7 @@ const ClientMetricsStore = require('./client-metrics-store');
const ClientStrategyStore = require('./client-strategy-store');
module.exports.createStores = (config) => {
const db = createDb(config.databaseUri, config.databaseSchema);
const db = createDb(config);
const eventStore = new EventStore(db);
return {

View File

@ -1,13 +1,12 @@
'use strict';
const DBMigrate = require('db-migrate');
const path = require('path');
const { getInstance } = require('db-migrate');
const parseDbUrl = require('parse-database-url');
function migrateDb (dbUrl, schema = "public") {
const custom = parseDbUrl(dbUrl);
custom.schema = schema;
const dbmigrate = DBMigrate.getInstance(true, {
function migrateDb ({ databaseUri, databaseSchema = 'public' }) {
const custom = parseDbUrl(databaseUri);
custom.schema = databaseSchema;
const dbmigrate = getInstance(true, {
cwd: __dirname,
config: { custom },
env: 'custom' }
@ -15,4 +14,4 @@ function migrateDb (dbUrl, schema = "public") {
return dbmigrate.up();
}
module.exports = migrateDb;
module.exports = migrateDb;

View File

@ -19,10 +19,10 @@
],
"repository": {
"type": "git",
"url": "ssh://git@github.com:finn-no/unleash.git"
"url": "ssh://git@github.com:unleash/unleash.git"
},
"bugs": {
"url": "https://github.com/finn-no/unleash/issues"
"url": "https://github.com/unleash/unleash/issues"
},
"engines": {
"node": "6"
@ -39,14 +39,13 @@
"start:dev:pg-chain": "export DATABASE_URL=postgres://$PGUSER:$PGPASSWORD@localhost:$PGPORT/postgres ; db-migrate up && npm run start:dev",
"db-migrate": "db-migrate up",
"db-migrate:down": "db-migrate down",
"test": "export PORT=4243 ; mocha --recursive test",
"test:unit": "mocha test/unit/**/*.js ",
"test": "PORT=4243 ava **/**test.js",
"test:docker": "./scripts/docker-postgres.sh",
"test:watch": "mocha --watch test test/*",
"test:watch": "npm run test -- --watch",
"test:pg-virtualenv": "pg_virtualenv npm run test:pg-virtualenv-chai",
"test:pg-virtualenv-chain": "export TEST_DATABASE_URL=postgres://$PGUSER:$PGPASSWORD@localhost:$PGPORT/postgres ; npm run db-migrate-testdb && npm test",
"test:coverage": "istanbul cover ./node_modules/mocha/bin/_mocha test --report lcovonly -- -R spec --recursive",
"test:coverage-report": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage"
"test:coverage": "nyc npm run test",
"test:coverage-report": "nyc report --reporter=text-lcov | coveralls"
},
"dependencies": {
"body-parser": "1.15.2",
@ -70,13 +69,13 @@
"yallist": "^2.0.0"
},
"devDependencies": {
"@types/node": "^6.0.46",
"ava": "^0.16.0",
"coveralls": "^2.11.14",
"istanbul": "^0.4.5",
"mocha": "^3.0.2",
"mocha-lcov-reporter": "1.2.0",
"coveralls": "^2.11.15",
"nyc": "^8.4.0",
"sinon": "^1.17.5",
"supertest": "^2.0.0",
"superagent": "^2.3.0",
"supertest": "^2.0.1",
"supervisor": "^0.11.0",
"unleash-frontend": "1.0.0-alpha.2"
}

View File

@ -3,6 +3,7 @@
const logger = require('./lib/logger');
const migrator = require('./migrator');
const { createStores } = require('./lib/db');
const getApp = require('./app');
const DEFAULT_OPTIONS = {
databaseUri: process.env.DATABASE_URL || 'postgres://unleash_user:passord@localhost:5432/unleash',
@ -21,7 +22,7 @@ function createApp (options) {
stores,
};
const app = require('./app')(config);
const app = getApp(config);
const server = app.listen(app.get('port'), () => {
logger.info(`Unleash started on ${app.get('port')}`);
});

View File

@ -1,6 +0,0 @@
{
"env": {
"browser": true,
"mocha": true
}
}

View File

@ -1,27 +1,27 @@
'use strict';
const specHelper = require('./util/test-helper');
let request;
const test = require('ava');
const { setupApp } = require('./util/test-helper');
const logger = require('../../lib/logger');
describe('The event api', () => {
beforeEach(done => {
specHelper.setupApp().then((app) => {
request = app.request;
done();
});
});
it('returns events', done => {
request
.get('/api/events')
.expect('Content-Type', /json/)
.expect(200, done);
});
it('returns events given a name', done => {
request
.get('/api/events/myname')
.expect('Content-Type', /json/)
.expect(200, done);
});
test.beforeEach(() => {
logger.setLevel('FATAL');
});
test.serial('returns events', async (t) => {
const { request, destroy } = await setupApp('event_api_serial');
return request
.get('/api/events')
.expect('Content-Type', /json/)
.expect(200)
.then(destroy);
});
test.serial('returns events given a name', async (t) => {
const { request, destroy } = await setupApp('event_api_serial');
return request
.get('/api/events/myname')
.expect('Content-Type', /json/)
.expect(200)
.then(destroy);
});

View File

@ -1,176 +1,190 @@
'use strict';
const { test } = require('ava');
const { setupApp } = require('./util/test-helper');
const logger = require('../../lib/logger');
const assert = require('assert');
const specHelper = require('./util/test-helper');
const stringify = function (o) {
return JSON.stringify(o, null, ' ');
};
let request;
describe('The features api', () => {
beforeEach(done => {
specHelper.setupApp().then((app) => {
request = app.request;
done();
});
});
it('returns three feature toggles', done => {
request
.get('/features')
.expect('Content-Type', /json/)
.expect(200)
.end((err, res) => {
assert(res.body.features.length === 3, `expected 3 features, got ${stringify(res.body)}`);
done();
});
});
it('gets a feature by name', done => {
request
.get('/features/featureX')
.expect('Content-Type', /json/)
.expect(200, done);
});
it('cant get feature that dose not exist', done => {
logger.setLevel('FATAL');
request
.get('/features/myfeature')
.expect('Content-Type', /json/)
.expect(404, done);
});
it('creates new feature toggle', done => {
request
.post('/features')
.send({ name: 'com.test.feature', enabled: false })
.set('Content-Type', 'application/json')
.expect(201, done);
});
it('creates new feature toggle with createdBy', done => {
logger.setLevel('FATAL');
request
.post('/features')
.send({ name: 'com.test.Username', enabled: false })
.set('Cookie', ['username=ivaosthu'])
.set('Content-Type', 'application/json')
.end(() => {
request
.get('/api/events')
.end((err, res) => {
assert.equal(res.body.events[0].createdBy, 'ivaosthu');
done();
});
});
});
it('require new feature toggle to have a name', done => {
logger.setLevel('FATAL');
request
.post('/features')
.send({ name: '' })
.set('Content-Type', 'application/json')
.expect(400, done);
});
it('can not change status of feature toggle that does not exist', done => {
logger.setLevel('FATAL');
request
.put('/features/should-not-exist')
.send({ name: 'should-not-exist', enabled: false })
.set('Content-Type', 'application/json')
.expect(404, done);
});
it('can change status of feature toggle that does exist', done => {
logger.setLevel('FATAL');
request
.put('/features/featureY')
.send({ name: 'featureY', enabled: true })
.set('Content-Type', 'application/json')
.expect(200, done);
});
it('archives a feature by name', done => {
request
.delete('/features/featureX')
.expect(200, done);
});
it('can not archive unknown feature', done => {
request
.delete('/features/featureUnknown')
.expect(404, done);
});
it('refuses to create a feature with an existing name', done => {
request
.post('/features')
.send({ name: 'featureX' })
.set('Content-Type', 'application/json')
.expect(403, done);
});
it('refuses to validate a feature with an existing name', done => {
request
.post('/features-validate')
.send({ name: 'featureX' })
.set('Content-Type', 'application/json')
.expect(403, done);
});
describe('new strategies api', () => {
it('automatically map existing strategy to strategies array', (done) => {
request
.get('/features/featureY')
.expect('Content-Type', /json/)
.end((err, res) => {
assert.equal(res.body.strategies.length, 1, 'expected strategy added to strategies');
assert.equal(res.body.strategy, res.body.strategies[0].name);
assert.deepEqual(res.body.parameters, res.body.strategies[0].parameters);
done();
});
});
it('can add two strategies to a feature toggle', (done) => {
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, done);
});
it('should not be allowed to post both strategy and strategies', (done) => {
logger.setLevel('FATAL');
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, done);
});
});
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 })
.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 })
.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 })
.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);
});

View File

@ -1,45 +1,40 @@
'use strict';
const assert = require('assert');
const specHelper = require('./util/test-helper');
const stringify = function (o) {
return JSON.stringify(o, null, ' ');
};
const test = require('ava');
const { setupApp } = require('./util/test-helper');
const logger = require('../../lib/logger');
let request;
describe('The archive features api', () => {
beforeEach(done => {
specHelper.setupApp().then((app) => {
request = app.request;
done();
});
});
it('returns three archived toggles', done => {
request
.get('/api/archive/features')
.expect('Content-Type', /json/)
.expect(200)
.end((err, res) => {
assert(res.body.features.length === 3, `expected 3 features, got ${stringify(res.body)}`);
done();
});
});
it('revives a feature by name', done => {
request
.post('/api/archive/revive')
.send({ name: 'featureArchivedX' })
.set('Content-Type', 'application/json')
.expect(200, done);
});
it('must set name when reviving toggle', done => {
request
.post('/api/archive/revive')
.send({ name: '' })
.expect(400, done);
});
test.beforeEach(() => {
logger.setLevel('FATAL');
});
test.serial('returns three archived toggles', async t => {
const { request, destroy } = await setupApp('archive_serial');
return request
.get('/api/archive/features')
.expect('Content-Type', /json/)
.expect(200)
.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');
return request
.post('/api/archive/revive')
.send({ name: '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')
.send({ name: '' })
.expect(400)
.then(destroy);
});

View File

@ -1,64 +1,65 @@
'use strict';
const test = require('ava');
const { setupApp } = require('./util/test-helper');
const logger = require('../../lib/logger');
const specHelper = require('./util/test-helper');
const assert = require('assert');
let request;
describe('The metrics api', () => {
beforeEach(done => {
specHelper.setupApp().then((app) => {
request = app.request;
done();
});
});
it('should register client', (done) => {
request
.post('/api/client/register')
.send({
appName: 'demo',
instanceId: 'test',
strategies: ['default'],
started: Date.now(),
interval: 10
})
.expect(202, done);
});
it('should accept client metrics', (done) => {
request
.post('/api/client/metrics')
.send({
appName: 'demo',
instanceId: '1',
bucket: {
start: Date.now(),
stop: Date.now(),
toggles: {}
}
})
.expect(202, done)
});
it('should get client strategies', done => {
request
.get('/api/client/strategies')
.expect('Content-Type', /json/)
.end((err, res) => {
assert(res.status, 200);
assert(res.body.length === 1, `expected 1 registerd client, got ${res.body}`);
done();
});;
});
it('should get client instances', done => {
request
.get('/api/client/instances')
.expect('Content-Type', /json/)
.end((err, res) => {
assert(res.status, 200);
assert(res.body.length === 1, `expected 1 registerd client, got ${res.body}`);
done();
});;
});
test.beforeEach(() => {
logger.setLevel('FATAL');
});
test.serial('should register client', async (t) => {
const { request, destroy } = await setupApp('metrics_serial');
return request
.post('/api/client/register')
.send({
appName: 'demo',
instanceId: 'test',
strategies: ['default'],
started: Date.now(),
interval: 10
})
.expect(202)
.then(destroy);
});
test.serial('should accept client metrics', async t => {
const { request, destroy } = await setupApp('metrics_serial');
return request
.post('/api/client/metrics')
.send({
appName: 'demo',
instanceId: '1',
bucket: {
start: Date.now(),
stop: Date.now(),
toggles: {}
}
})
.expect(202)
.then(destroy);
});
test.serial('should get client strategies', async t => {
const { request, destroy } = await setupApp('metrics_serial');
return request
.get('/api/client/strategies')
.expect('Content-Type', /json/)
.expect((res) => {
t.true(res.status === 200);
t.true(res.body.length === 1);
})
.then(destroy);
});
test.serial('should get client instances', async t => {
const { request, destroy } = await setupApp('metrics_serial');
return request
.get('/api/client/instances')
.expect('Content-Type', /json/)
.expect((res) => {
t.true(res.status === 200);
t.true(res.body.length === 1);
})
.then(destroy);
});

View File

@ -1,23 +1,18 @@
'use strict';
const specHelper = require('./util/test-helper');
const test = require('ava');
const { setupApp } = require('./util/test-helper');
const logger = require('../../lib/logger');
let request;
describe('The routes', () => {
beforeEach(done => {
specHelper.setupApp().then((app) => {
request = app.request;
done();
});
});
describe('healthcheck', () => {
it('returns health good', done => {
request.get('/health')
.expect('Content-Type', /json/)
.expect(200)
.expect('{"health":"GOOD"}', done);
});
});
test.beforeEach(() => {
logger.setLevel('FATAL');
});
test('returns health good', async (t) => {
const { request, destroy } = await setupApp('health');
return request.get('/health')
.expect('Content-Type', /json/)
.expect(200)
.expect('{"health":"GOOD"}')
.then(destroy);
});

View File

@ -1,70 +1,81 @@
'use strict';
const specHelper = require('./util/test-helper');
let request;
const test = require('ava');
const { setupApp } = require('./util/test-helper');
const logger = require('../../lib/logger');
describe('The strategy api', () => {
beforeEach(done => {
specHelper.setupApp().then((app) => {
request = app.request;
done();
});
});
it('gets all strategies', done => {
request
.get('/api/strategies')
.expect('Content-Type', /json/)
.expect(200, done);
});
it('gets a strategy by name', done => {
request
.get('/api/strategies/default')
.expect('Content-Type', /json/)
.expect(200, done);
});
it('cant get a strategy by name that dose not exist', done => {
request
.get('/api/strategies/mystrategy')
.expect('Content-Type', /json/)
.expect(404, done);
});
it('creates a new strategy', done => {
request
.post('/api/strategies')
.send({ name: 'myCustomStrategy', description: 'Best strategy ever.' })
.set('Content-Type', 'application/json')
.expect(201, done);
});
it('requires new strategies to have a name', done => {
request
.post('/api/strategies')
.send({ name: '' })
.set('Content-Type', 'application/json')
.expect(400, done);
});
it('refuses to create a strategy with an existing name', done => {
request
.post('/api/strategies')
.send({ name: 'default' })
.set('Content-Type', 'application/json')
.expect(403, done);
});
it('deletes a new strategy', done => {
request
.delete('/api/strategies/usersWithEmail')
.expect(200, done);
});
it('can\'t delete a strategy that dose not exist', done => {
request
.delete('/api/strategies/unknown')
.expect(404, done);
});
test.beforeEach(() => {
logger.setLevel('FATAL');
});
test.serial('gets all strategies', async (t) => {
const { request, destroy } = await setupApp('strategy_api_serial');
return request
.get('/api/strategies')
.expect('Content-Type', /json/)
.expect(200)
.then(destroy);
});
test.serial('gets a strategy by name', async (t) => {
const { request, destroy } = await setupApp('strategy_api_serial');
return request
.get('/api/strategies/default')
.expect('Content-Type', /json/)
.expect(200)
.then(destroy);
});
test.serial('cant get a strategy by name that dose not exist', async (t) => {
const { request, destroy } = await setupApp('strategy_api_serial');
return request
.get('/api/strategies/mystrategy')
.expect('Content-Type', /json/)
.expect(404)
.then(destroy);
});
test.serial('creates a new strategy', async (t) => {
const { request, destroy } = await setupApp('strategy_api_serial');
return request
.post('/api/strategies')
.send({ name: 'myCustomStrategy', description: 'Best strategy ever.' })
.set('Content-Type', 'application/json')
.expect(201)
.then(destroy);
});
test.serial('requires new strategies to have a name', async (t) => {
const { request, destroy } = await setupApp('strategy_api_serial');
return request
.post('/api/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) => {
const { request, destroy } = await setupApp('strategy_api_serial');
return request
.post('/api/strategies')
.send({ name: 'default' })
.set('Content-Type', 'application/json')
.expect(403)
.then(destroy);
});
test.serial('deletes a new strategy', async (t) => {
const { request, destroy } = await setupApp('strategy_api_serial');
return request
.delete('/api/strategies/usersWithEmail')
.expect(200)
.then(destroy);
});
test.serial('can\'t delete a strategy that dose not exist', async (t) => {
const { request, destroy } = await setupApp('strategy_api_serial', false);
return request
.delete('/api/strategies/unknown')
.expect(404);
});

View File

@ -2,28 +2,39 @@
process.env.NODE_ENV = 'test';
let supertest = require('supertest');
const options = {
databaseUri: require('./database-config').getDatabaseUri(),
databaseSchema: 'test'
};
const supertest = require('supertest');
const migrator = require('../../../migrator');
const { createStores } = require('../../../lib/db');
const { createDb } = require('../../../lib/db/db-pool');
const _app = require('../../../app');
// because of migrator bug
delete process.env.DATABASE_URL;
const db = require('../../../lib/db/db-pool').createDb(options.databaseUri);
function createApp (databaseSchema = 'test') {
const options = {
databaseUri: require('./database-config').getDatabaseUri(),
databaseSchema,
minPool: 0,
maxPool: 0,
};
const db = createDb({ databaseUri: options.databaseUri, minPool: 0, maxPool: 0 });
const createApp = db.raw(`DROP SCHEMA IF EXISTS ${options.databaseSchema} CASCADE; CREATE SCHEMA ${options.databaseSchema}`)
.then(() => migrator(options.databaseUri, options.databaseSchema))
.then(() => {
const stores = createStores(options);
const app = require('../../../app')({stores});
return { stores, request: supertest(app) };
});
return db.raw(`CREATE SCHEMA IF NOT EXISTS ${options.databaseSchema}`)
.then(() => migrator(options))
.then(() => {
db.destroy();
const stores = createStores(options);
const app = _app({ stores });
return {
stores,
request: supertest(app),
destroy () {
return stores.db.destroy();
},
};
});
}
function createStrategies (stores) {
return [
@ -49,7 +60,7 @@ function createClientStrategy (stores) {
instanceId: 'test-1',
strategies: ['default'],
started: Date.now(),
interval: 10
interval: 10,
},
].map(client => stores.clientStrategyStore.insert(client));
}
@ -61,7 +72,7 @@ function createClientInstance (stores) {
instanceId: 'test-1',
strategies: ['default'],
started: Date.now(),
interval: 10
interval: 10,
},
].map(client => stores.clientInstanceStore.insert(client));
}
@ -132,10 +143,10 @@ function createFeatures (stores) {
function resetDatabase (stores) {
return Promise.all([
stores.db('strategies').del(),
stores.db('strategies').del(),
stores.db('features').del(),
stores.db('client_strategies').del(),
stores.db('client_instances').del()
stores.db('client_instances').del(),
]);
}
@ -144,15 +155,15 @@ function setupDatabase (stores) {
createStrategies(stores)
.concat(createFeatures(stores)
.concat(createClientInstance(stores))
.concat(createClientStrategy(stores))))
.concat(createClientStrategy(stores))));
}
module.exports = {
setupApp () {
return createApp.then((app) => {
setupApp (name) {
return createApp(name).then((app) => {
return resetDatabase(app.stores)
.then(() => setupDatabase(app.stores))
.then(() => app);
.then(() => setupDatabase(app.stores))
.then(() => app);
});
}
},
};

View File

@ -1,99 +1,103 @@
'use strict';
const test = require('ava');
const eventDiffer = require('../../lib/event-differ');
const eventType = require('../../lib/event-type');
const assert = require('assert');
const eventType = require('../../lib/event-type');
const logger = require('../../lib/logger');
describe('eventDiffer', () => {
it('fails if events include an unknown event type', () => {
const events = [
{ type: eventType.featureCreated, data: {} },
{ type: 'unknown-type', data: {} },
];
test.beforeEach(() => {
logger.setLevel('FATAL');
});
assert.throws(() => {
eventDiffer.addDiffs(events);
});
});
it('diffs a feature-update event', () => {
const feature = 'foo';
const desc = 'bar';
const events = [
{
type: eventType.featureUpdated,
data: { name: feature, description: desc, strategy: 'default', enabled: true, parameters: { value: 2 } },
},
{
type: eventType.featureCreated,
data: { name: feature, description: desc, strategy: 'default', enabled: false, parameters: { value: 1 } },
},
];
test('fails if events include an unknown event type', t => {
const events = [
{ type: eventType.featureCreated, data: {} },
{ type: 'unknown-type', data: {} },
];
t.throws(() => {
eventDiffer.addDiffs(events);
assert.deepEqual(events[0].diffs, [
{ kind: 'E', path: ['enabled'], lhs: false, rhs: true },
{ kind: 'E', path: ['parameters', 'value'], lhs: 1, rhs: 2 },
]);
assert.strictEqual(events[1].diffs, null);
});
it('diffs only against features with the same name', () => {
const events = [
{
type: eventType.featureUpdated,
data: { name: 'bar', description: 'desc', strategy: 'default', enabled: true, parameters: {} },
},
{
type: eventType.featureUpdated,
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: false, parameters: {} },
},
{
type: eventType.featureCreated,
data: { name: 'bar', description: 'desc', strategy: 'default', enabled: false, parameters: {} },
},
{
type: eventType.featureCreated,
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {} },
},
];
eventDiffer.addDiffs(events);
assert.strictEqual(events[0].diffs[0].rhs, true);
assert.strictEqual(events[1].diffs[0].rhs, false);
assert.strictEqual(events[2].diffs, null);
assert.strictEqual(events[3].diffs, null);
});
it('sets an empty array of diffs if nothing was changed', () => {
const events = [
{
type: eventType.featureUpdated,
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {} },
},
{
type: eventType.featureCreated,
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {} },
},
];
eventDiffer.addDiffs(events);
assert.deepEqual(events[0].diffs, []);
});
it('sets diffs to null if there was nothing to diff against', () => {
const events = [
{
type: eventType.featureUpdated,
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {} },
},
];
eventDiffer.addDiffs(events);
assert.strictEqual(events[0].diffs, null);
});
});
test('diffs a feature-update event', t => {
const feature = 'foo';
const desc = 'bar';
const events = [
{
type: eventType.featureUpdated,
data: { name: feature, description: desc, strategy: 'default', enabled: true, parameters: { value: 2 } },
},
{
type: eventType.featureCreated,
data: { name: feature, description: desc, strategy: 'default', enabled: false, parameters: { value: 1 } },
},
];
eventDiffer.addDiffs(events);
t.deepEqual(events[0].diffs, [
{ kind: 'E', path: ['enabled'], lhs: false, rhs: true },
{ kind: 'E', path: ['parameters', 'value'], lhs: 1, rhs: 2 },
]);
t.true(events[1].diffs === null);
});
test('diffs only against features with the same name', t => {
const events = [
{
type: eventType.featureUpdated,
data: { name: 'bar', description: 'desc', strategy: 'default', enabled: true, parameters: {} },
},
{
type: eventType.featureUpdated,
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: false, parameters: {} },
},
{
type: eventType.featureCreated,
data: { name: 'bar', description: 'desc', strategy: 'default', enabled: false, parameters: {} },
},
{
type: eventType.featureCreated,
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {} },
},
];
eventDiffer.addDiffs(events);
t.true(events[0].diffs[0].rhs === true);
t.true(events[1].diffs[0].rhs === false);
t.true(events[2].diffs === null);
t.true(events[3].diffs === null);
});
test('sets an empty array of diffs if nothing was changed', t => {
const events = [
{
type: eventType.featureUpdated,
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {} },
},
{
type: eventType.featureCreated,
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {} },
},
];
eventDiffer.addDiffs(events);
t.deepEqual(events[0].diffs, []);
});
test('sets diffs to null if there was nothing to diff against', t => {
const events = [
{
type: eventType.featureUpdated,
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {} },
},
];
eventDiffer.addDiffs(events);
t.true(events[0].diffs === null);
});

View File

@ -1,66 +1,63 @@
'use strict';
const assert = require('assert');
const test = require('ava');
const mapper = require('../../../lib/helper/legacy-feature-mapper');
describe('legacy-feature-mapper', () => {
it('adds old fields to feature', () => {
const feature = {
name: 'test',
enabled: 0,
strategies: [{
name: 'default',
parameters: {
val: 'bar',
},
}],
};
const mappedFeature = mapper.addOldFields(feature);
assert.equal(mappedFeature.name, feature.name);
assert.equal(mappedFeature.enabled, feature.enabled);
assert.equal(mappedFeature.strategy, feature.strategies[0].name);
assert.notEqual(mappedFeature.parameters, feature.strategies[0].parameters);
assert.deepEqual(mappedFeature.parameters, feature.strategies[0].parameters);
});
it('transforms fields to new format', () => {
const feature = {
name: 'test',
enabled: 0,
strategy: 'default',
test('adds old fields to feature', t => {
const feature = {
name: 'test',
enabled: 0,
strategies: [{
name: 'default',
parameters: {
val: 'bar',
},
};
}],
};
const mappedFeature = mapper.toNewFormat(feature);
const mappedFeature = mapper.addOldFields(feature);
assert.equal(mappedFeature.name, feature.name);
assert.equal(mappedFeature.enabled, feature.enabled);
assert.equal(mappedFeature.strategies.length, 1);
assert.equal(mappedFeature.strategies[0].name, feature.strategy);
assert.deepEqual(mappedFeature.strategies[0].parameters, feature.parameters);
assert(mappedFeature.strategy === undefined);
assert(mappedFeature.parameters === undefined);
});
it('should not transform if it already is the new format', () => {
const feature = {
name: 'test',
enabled: 0,
strategies: [{
name: 'default',
parameters: {
val: 'bar',
},
}],
};
const mappedFeature = mapper.toNewFormat(feature);
assert.equal(mappedFeature, feature);
});
t.true(mappedFeature.name === feature.name);
t.true(mappedFeature.enabled === feature.enabled);
t.true(mappedFeature.strategy === feature.strategies[0].name);
t.true(mappedFeature.parameters !== feature.strategies[0].parameters);
t.deepEqual(mappedFeature.parameters, feature.strategies[0].parameters);
});
test('transforms fields to new format', t => {
const feature = {
name: 'test',
enabled: 0,
strategy: 'default',
parameters: {
val: 'bar',
},
};
const mappedFeature = mapper.toNewFormat(feature);
t.true(mappedFeature.name === feature.name);
t.true(mappedFeature.enabled === feature.enabled);
t.true(mappedFeature.strategies.length === 1);
t.true(mappedFeature.strategies[0].name === feature.strategy);
t.deepEqual(mappedFeature.strategies[0].parameters, feature.parameters);
t.true(mappedFeature.strategy === undefined);
t.true(mappedFeature.parameters === undefined);
});
test('should not transform if it already is the new format', t => {
const feature = {
name: 'test',
enabled: 0,
strategies: [{
name: 'default',
parameters: {
val: 'bar',
},
}],
};
const mappedFeature = mapper.toNewFormat(feature);
t.true(mappedFeature === feature);
});

View File

@ -1,61 +1,63 @@
'use strict';
const test = require('ava');
const store = require('./mocks/store');
const supertest = require('supertest');
const assert = require('assert');
const logger = require('../../../lib/logger');
let request;
let featureToggleStore;
describe('Unit: The features api', () => {
beforeEach(done => {
const stores = store.createStores();
const app = require('../../../app')({
baseUriPath: '',
stores: stores,
});
featureToggleStore = stores.featureToggleStore;
request = supertest(app);
done();
});
it('should get empty getFeatures', (done) => {
request
.get('/features')
.expect('Content-Type', /json/)
.expect(200)
.end((err, res) => {
assert(res.body.features.length === 0);
done();
});
});
it('should get one getFeature', (done) => {
featureToggleStore.addFeature( { name: 'test', strategies: [{ name: 'default' }] } );
request
.get('/features')
.expect('Content-Type', /json/)
.expect(200)
.end((err, res) => {
assert(res.body.features.length === 1);
done();
});
});
it('should add version numbers for /features', (done) => {
featureToggleStore.addFeature( { name: 'test', strategies: [{ name: 'default' }] } );
request
.get('/features')
.expect('Content-Type', /json/)
.expect(200)
.end((err, res) => {
assert.equal(res.body.version, 1);
done();
});
});
test.beforeEach(() => {
logger.setLevel('FATAL');
});
function getSetup () {
const base = `/random${Math.round(Math.random() * 1000)}`;
const stores = store.createStores();
const app = require('../../../app')({
baseUriPath: base,
stores,
});
return {
base,
featureToggleStore: stores.featureToggleStore,
request: supertest(app),
};
}
test('should get empty getFeatures', t => {
const { request, base } = getSetup();
return request
.get(`${base}/features`)
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
t.true(res.body.features.length === 0);
});
});
test('should get one getFeature', t => {
const { request, featureToggleStore, base } = getSetup();
featureToggleStore.addFeature({ name: 'test_', strategies: [{ name: 'default_' }] });
return request
.get(`${base}/features`)
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
t.true(res.body.features.length === 1);
});
});
test('should add version numbers for /features', t => {
const { request, featureToggleStore, base } = getSetup();
featureToggleStore.addFeature({ name: 'test2', strategies: [{ name: 'default' }] });
return request
.get(`${base}/features`)
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
t.true(res.body.version === 1);
});
});

View File

@ -1,58 +0,0 @@
'use strict';
const store = require('./mocks/store');
const supertest = require('supertest');
const assert = require('assert');
const sinon = require('sinon');
let request;
let db;
describe('Unit: The health cheack api', () => {
beforeEach(done => {
const stores = store.createStores();
db = stores.db;
const app = require('../../../app')({
baseUriPath: '',
stores: stores,
});
request = supertest(app);
done();
});
it('should give 500 when db is failing', (done) => {
db.select = () => {
return {
from: () => Promise.reject(new Error('db error'))
}
}
request
.get('/health')
.expect(500)
.end((err, res) => {
assert.equal(res.status, 500)
assert.equal(res.body.health, 'BAD');
done();
});
});
it('should give 200 when db is not failing', (done) => {
request
.get('/health')
.expect(200, done)
});
it('should give health=GOOD when db is not failing', (done) => {
request
.get('/health')
.expect(200)
.end((err, res) => {
assert.equal(res.status, 200)
assert.equal(res.body.health, 'GOOD');
done();
});
});
});

View File

@ -0,0 +1,59 @@
'use strict';
const test = require('ava');
const store = require('./mocks/store');
const supertest = require('supertest');
const logger = require('../../../lib/logger');
test.beforeEach(() => {
logger.setLevel('FATAL');
});
function getSetup () {
const stores = store.createStores();
const db = stores.db;
const app = require('../../../app')({
baseUriPath: '',
stores,
});
return {
db,
request: supertest(app),
};
}
test('should give 500 when db is failing', t => {
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');
});
});
test('should give 200 when db is not failing', () => {
const { request } = getSetup();
return request
.get('/health')
.expect(200);
});
test('should give health=GOOD when db is not failing', t => {
const { request } = getSetup();
return request
.get('/health')
.expect(200)
.expect((res) => {
t.true(res.status === 200);
t.true(res.body.health === 'GOOD');
});
});

View File

@ -1,68 +1,74 @@
'use strict';
const test = require('ava');
const store = require('./mocks/store');
const supertest = require('supertest');
const assert = require('assert');
const logger = require('../../../lib/logger');
let request;
describe('Unit: The metrics api', () => {
beforeEach(done => {
const stores = store.createStores();
const app = require('../../../app')({
baseUriPath: '',
stores: stores,
});
request = supertest(app);
done();
});
it('should register client', (done) => {
request
.post('/api/client/register')
.send({
appName: 'demo',
instanceId: 'test',
strategies: ['default'],
started: Date.now(),
interval: 10
})
.expect(202, done);
});
it('should require appName field', (done) => {
request
.post('/api/client/register')
.expect(400, done)
});
it('should require strategies field', (done) => {
request
.post('/api/client/register')
.send({
appName: 'demo',
instanceId: 'test',
//strategies: ['default'],
started: Date.now(),
interval: 10
})
.expect(400, done)
});
it('should accept client metrics', (done) => {
request
.post('/api/client/metrics')
.send({
appName: 'demo',
instanceId: '1',
bucket: {
start: Date.now(),
stop: Date.now(),
toggles: {}
}
})
.expect(202, done)
});
test.beforeEach(() => {
logger.setLevel('FATAL');
});
function getSetup () {
const stores = store.createStores();
const app = require('../../../app')({
baseUriPath: '',
stores,
});
return {
request: supertest(app),
};
}
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 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);
});

View File

@ -1,6 +1,5 @@
'use strict';
module.exports = {
reset: () => {},
module.exports = () => ({
insert: () => Promise.resolve(),
};
});

View File

@ -1,6 +1,5 @@
'use strict';
module.exports = {
reset: () => {},
module.exports = () => ({
insert: () => Promise.resolve(),
};
});

View File

@ -1,11 +1,10 @@
'use strict';
const _features = [];
module.exports = {
getFeatures: () => Promise.resolve(_features),
addFeature: (feature) => _features.push(feature),
reset: () => {
_features.lengyh = 0;
},
module.exports = () => {
const _features = [];
return {
getFeatures: () => Promise.resolve(_features),
addFeature: (feature) => _features.push(feature),
};
};

View File

@ -1,7 +1,6 @@
'use strict';
module.exports = {
reset: () => {},
module.exports = () => ({
getMetricsLastHour: () => Promise.resolve([]),
insert: () => Promise.resolve(),
};
});

View File

@ -1,12 +1,13 @@
'use strict';
const _strategies = [{ name: 'default', parameters: {} }];
module.exports = {
getStrategies: () => Promise.resolve(_strategies),
addStrategy: (strat) => _strategies.push(strat),
reset: () => {
_strategies.length = 0;
},
module.exports = () => {
const _strategies = [{ name: 'default', parameters: {} }];
return {
getStrategies: () => Promise.resolve(_strategies),
addStrategy: (strat) => _strategies.push(strat),
};
};

View File

@ -1,4 +1,4 @@
const sinon = require('sinon');
'use strict';
const clientMetricsStore = require('./fake-metrics-store');
const clientStrategyStore = require('./fake-client-strategy-store');
@ -11,27 +11,18 @@ const strategyStore = require('./fake-strategies-store');
module.exports = {
createStores: () => {
const db = {
select: () => {
return {
from: () => Promise.resolve()
}
}
}
clientMetricsStore.reset();
clientStrategyStore.reset();
clientInstanceStore.reset();
featureToggleStore.reset();
strategyStore.reset();
select: () => ({
from: () => Promise.resolve(),
}),
};
return {
db,
clientMetricsStore,
clientStrategyStore,
clientInstanceStore,
featureToggleStore,
strategyStore,
}
}
};
clientMetricsStore: clientMetricsStore(),
clientStrategyStore: clientStrategyStore(),
clientInstanceStore: clientInstanceStore(),
featureToggleStore: featureToggleStore(),
strategyStore: strategyStore(),
};
},
};

View File

@ -1,35 +1,28 @@
'use strict';
const test = require('ava');
const store = require('./mocks/store');
const supertest = require('supertest');
const assert = require('assert');
const sinon = require('sinon');
const logger = require('../../../lib/logger');
let request;
let strategyStore;
describe('Unit: The strategies api', () => {
beforeEach(done => {
const stores = store.createStores();
const app = require('../../../app')({
baseUriPath: '',
stores: stores,
});
strategyStore = stores.strategyStore;
request = supertest(app);
done();
});
it('should add version numbers for /stategies', (done) => {
request
.get('/api/strategies')
.expect('Content-Type', /json/)
.expect(200)
.end((err, res) => {
assert.equal(res.body.version, 1);
done();
});
});
test.beforeEach(() => {
logger.setLevel('FATAL');
});
test('should add version numbers for /stategies', t => {
const stores = store.createStores();
const app = require('../../../app')({
baseUriPath: '',
stores,
});
const request = supertest(app);
return request
.get('/api/strategies')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
t.true(res.body.version === 1);
});
});