mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-01 00:08:27 +01:00
Merge pull request #147 from finn-no/feature/aggregate_strategies_api
Feature/aggregate strategies api
This commit is contained in:
commit
32ddc5680c
285
docs/api-v1.md
Normal file
285
docs/api-v1.md
Normal file
@ -0,0 +1,285 @@
|
||||
# API
|
||||
|
||||
## Feature Toggles
|
||||
|
||||
### Fetching Feature Toggles
|
||||
|
||||
**GET: http://unleash.host.com/features**
|
||||
|
||||
This endpoint is the one all clients should use to fetch all available feature toggles
|
||||
from the _unleash-server_. The response returns all active feature toggles and their
|
||||
current strategy configuration. A feature toggle will have _at least_ one configured strategy.
|
||||
A strategy will have a `name` and `parameters` map.
|
||||
|
||||
> _Note:_ Clients should perfer the `strategies` property.
|
||||
> Legacy properties (`strategy` & `parameters`) will be kept until **version 2** of the format.
|
||||
|
||||
This endpoint should never return anything besides a valid *20X or 304-response*. It will also
|
||||
include a `Etag`-header. The value of this header can be used by clients as the value of
|
||||
the `If-None-Match`-header in the request to prevent a data transfer if the clients already
|
||||
has the latest response locally.
|
||||
|
||||
**Example response:**
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"features": [
|
||||
{
|
||||
"name": "Feature.A",
|
||||
"description": "lorem ipsum",
|
||||
"enabled": false,
|
||||
"strategies": [
|
||||
{
|
||||
"name": "default",
|
||||
"parameters": {}
|
||||
}
|
||||
],
|
||||
"strategy": "default",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"name": "Feature.B",
|
||||
"description": "lorem ipsum",
|
||||
"enabled": true,
|
||||
"strategies": [
|
||||
{
|
||||
"name": "ActiveForUserWithId",
|
||||
"parameters": {
|
||||
"userIdList": "123,221,998"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GradualRolloutRandom",
|
||||
"parameters": {
|
||||
"percentage": "10"
|
||||
}
|
||||
}
|
||||
],
|
||||
"strategy": "ActiveForUserWithId",
|
||||
"parameters": {
|
||||
"userIdList": "123,221,998"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**GET: http://unleash.host.com/features/:featureName**
|
||||
|
||||
Used to fetch details about a specific featureToggle. This is mostly provded to make it easy to
|
||||
debug the API and should not be used by the client implementations.
|
||||
|
||||
> _Notice_: You will not get a version property when fetching a specific feature toggle by name.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Feature.A",
|
||||
"description": "lorem ipsum..",
|
||||
"enabled": false,
|
||||
"strategies": [
|
||||
{
|
||||
"name": "default",
|
||||
"parameters": {}
|
||||
}
|
||||
],
|
||||
"strategy": "default",
|
||||
"parameters": {}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Create a new Feature Toggle
|
||||
|
||||
**POST: http://unleash.host.com/features/**
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Feature.A",
|
||||
"description": "lorem ipsum..",
|
||||
"enabled": false,
|
||||
"strategies": [
|
||||
{
|
||||
"name": "default",
|
||||
"parameters": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Used by the admin-dashboard to create a new feature toggles. The name **must be unique**,
|
||||
otherwise you will get a _403-response_.
|
||||
|
||||
Returns 200-respose if the feature toggle was created successfully.
|
||||
|
||||
### Update a Feature Toggle
|
||||
|
||||
**PUT: http://unleash.host.com/features/:toggleName**
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Feature.A",
|
||||
"description": "lorem ipsum..",
|
||||
"enabled": false,
|
||||
"strategies": [
|
||||
{
|
||||
"name": "default",
|
||||
"parameters": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Used by the admin dashboard to update a feature toggles. The name has to match an
|
||||
existing features toggle.
|
||||
|
||||
Returns 200-respose if the feature toggle was updated successfully.
|
||||
|
||||
### Archive a Feature Toggle
|
||||
|
||||
**DELETE: http://unleash.host.com/features/:toggleName**
|
||||
|
||||
Used to archive a feature toggle. A feature toggle can never be totally be deleted,
|
||||
but can be archived. This is a design decision to make sure that a old feature
|
||||
toggle suddnely reapear by some one else reusing the same name.
|
||||
|
||||
## Archive
|
||||
|
||||
### Fetch archived toggles
|
||||
|
||||
**GET http://unleash.host.com/archive/features**
|
||||
|
||||
Used to fetch list of archived feature toggles
|
||||
|
||||
**Example response:**
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"features": [
|
||||
{
|
||||
"name": "Feature.A",
|
||||
"description": "lorem ipsum",
|
||||
"enabled": false,
|
||||
"strategies": [
|
||||
{
|
||||
"name": "default",
|
||||
"parameters": {}
|
||||
}
|
||||
],
|
||||
"strategy": "default",
|
||||
"parameters": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Revive feature toggle
|
||||
|
||||
**POST http://unleash.host.com//archive/revive**
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Feature.A"
|
||||
}
|
||||
```
|
||||
|
||||
Used to revive a feature toggle.
|
||||
|
||||
|
||||
## Strategies
|
||||
|
||||
### Fetch Strategies
|
||||
**GET: http://unleash.host.com/strategies**
|
||||
|
||||
Used to fetch all defined strategies and their defined paramters.
|
||||
|
||||
**Response**
|
||||
|
||||
```json
|
||||
{
|
||||
version: 1,
|
||||
strategies: [
|
||||
{
|
||||
name: "default",
|
||||
description: "Default on/off strategy.",
|
||||
parametersTemplate: null
|
||||
},
|
||||
{
|
||||
name: "ActiveForUserWithEmail",
|
||||
description: "A comma separated list of email adresses this feature should be active for.",
|
||||
parametersTemplate: {
|
||||
emails: "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Accounts",
|
||||
description: "Enable for user accounts",
|
||||
parametersTemplate: {
|
||||
Accountname: "string"
|
||||
}
|
||||
}
|
||||
]}
|
||||
```
|
||||
|
||||
### Create strategy
|
||||
|
||||
**POST: http://unleash.host.com/strategies**
|
||||
|
||||
**Body**
|
||||
|
||||
```json
|
||||
{
|
||||
name: "ActiveForUserWithEmail",
|
||||
description: "A comma separated list of email adresses this feature should be active for.",
|
||||
parametersTemplate: {
|
||||
emails: "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Used to create a new Strategy. Name must be unique.
|
||||
|
||||
|
||||
|
||||
# Events
|
||||
|
||||
**GET: http://unleash.host.com/events**
|
||||
|
||||
Used to fetch all changes in the unleash system.
|
||||
|
||||
Event types:
|
||||
|
||||
- feature-created
|
||||
- feature-updated
|
||||
- feature-archived
|
||||
- feature-revived
|
||||
- strategy-created
|
||||
- strategy-deleted
|
||||
|
||||
**Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"events":[
|
||||
{
|
||||
"id":454,
|
||||
"type":"feature-updated",
|
||||
"createdBy":"unknown",
|
||||
"createdAt":"2016-08-24T11:22:01.354Z",
|
||||
"data": {
|
||||
"name":"eid.bankid.mobile",
|
||||
"description":"",
|
||||
"strategy":"default",
|
||||
"enabled":true,
|
||||
"parameters":{}
|
||||
},
|
||||
"diffs": [
|
||||
{"kind":"E","path":["enabled"],"lhs":false,"rhs":true}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
71
docs/schema.md
Normal file
71
docs/schema.md
Normal file
@ -0,0 +1,71 @@
|
||||
# Schema
|
||||
|
||||
## Table: _migrations_
|
||||
|
||||
Used by db-migrate module to keep track of migrations.
|
||||
|
||||
| NAME | TYPE | SIZE | NULLABLE | COLUMN_DEF |
|
||||
| ----------- | --------- | ----------- | -------- | -------------------------------------- |
|
||||
| id | serial | 10 | 0 | nextval('migrations_id_seq'::regclass) |
|
||||
| name | varchar | 255 | 0 | (null) |
|
||||
| run_on | timestamp | 29 | 0 | (null) |
|
||||
|
||||
|
||||
|
||||
## Table: _events_
|
||||
| NAME | TYPE | SIZE | NULLABLE | COLUMN_DEF |
|
||||
| ----------- | --------- | ----------- | -------- | ---------------------------------- |
|
||||
| id | serial | 10 | 0 | nextval('events_id_seq'::regclass) |
|
||||
| created_at | timestamp | 29 | 1 | now() |
|
||||
| type | varchar | 255 | 0 | (null) |
|
||||
| created_by | varchar | 255 | 0 | (null) |
|
||||
| data | json | 2147483647 | 1 | (null) |
|
||||
|
||||
|
||||
## Table: _strategies_loc
|
||||
| NAME | TYPE | SIZE | NULLABLE | COLUMN_DEF |
|
||||
| ------------------- | --------- | ----------- | -------- | ---------- |
|
||||
| created_at | timestamp | 29 | 1 | now() |
|
||||
| name | varchar | 255 | 0 | (null) |
|
||||
| description | text | 2147483647 | 1 | (null) |
|
||||
| parameters_template | json | 2147483647 | 1 | (null) |
|
||||
|
||||
|
||||
## Table: _features_
|
||||
|
||||
| **NAME** | **TYPE** | **SIZE** | **NULLABLE** | **COLUMN_DEF** | **COMMENT** |
|
||||
| ------------- | --------- | ----------- | ------------ | -------------- | ----------- |
|
||||
| created_at | timestamp | 29 | 1 | now() | |
|
||||
| name | varchar | 255 | 0 | (null) | |
|
||||
| enabled | int4 | 10 | 1 | 0 | |
|
||||
| description | text | 2147483647 | 1 | (null) | |
|
||||
| archived | int4 | 10 | 1 | 0 | |
|
||||
| parameters | json | 2147483647 | 1 | (null) | deprecated (*) |
|
||||
| strategy_name | varchar | 255 | 1 | (null) | deprecated (*) |
|
||||
| strategies | json | 2147483647 | 1 | (null) | |
|
||||
|
||||
(*) we migrated from `parmaters` and `strategy_name` to `strategies` which should contain an array of these.
|
||||
|
||||
For [aggregate strategies](https://github.com/finn-no/unleash/issues/102) we had the following sql to migrate to the strategies column:
|
||||
|
||||
```sql
|
||||
ALTER TABLE features ADD "strategies" json;
|
||||
|
||||
--populate the strategies column
|
||||
UPDATE features
|
||||
SET strategies = ('[{"name":"'||f.strategy_name||'","parameters":'||f.parameters||'}]')::json
|
||||
FROM features as f
|
||||
WHERE f.name = features.name;
|
||||
```
|
||||
|
||||
In order to migrate back, one can use the following sql (it will loose all, but the first activation strategy):
|
||||
|
||||
```sql
|
||||
UPDATE features
|
||||
SET strategy_name = f.strategies->0->>'name',
|
||||
parameters = f.strategies->0->'parameters'
|
||||
FROM features as f
|
||||
WHERE f.name = features.name;
|
||||
|
||||
ALTER TABLE features DROP COLUMN "strategies";
|
||||
```
|
@ -2,7 +2,7 @@
|
||||
const eventType = require('../eventType');
|
||||
const logger = require('../logger');
|
||||
const NotFoundError = require('../error/NotFoundError');
|
||||
const FEATURE_COLUMNS = ['name', 'description', 'enabled', 'strategy_name', 'parameters'];
|
||||
const FEATURE_COLUMNS = ['name', 'description', 'enabled', 'strategies'];
|
||||
|
||||
module.exports = function (db, eventStore) {
|
||||
eventStore.on(eventType.featureCreated, event => createFeature(event.data));
|
||||
@ -39,18 +39,15 @@ module.exports = function (db, eventStore) {
|
||||
.map(rowToFeature);
|
||||
}
|
||||
|
||||
|
||||
function rowToFeature (row) {
|
||||
if (!row) {
|
||||
throw new NotFoundError('No feature toggle found');
|
||||
}
|
||||
|
||||
return {
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
enabled: row.enabled > 0,
|
||||
strategy: row.strategy_name, // eslint-disable-line
|
||||
parameters: row.parameters,
|
||||
strategies: row.strategies,
|
||||
};
|
||||
}
|
||||
|
||||
@ -60,8 +57,7 @@ module.exports = function (db, eventStore) {
|
||||
description: data.description,
|
||||
enabled: data.enabled ? 1 : 0,
|
||||
archived: data.archived ? 1 : 0,
|
||||
strategy_name: data.strategy, // eslint-disable-line
|
||||
parameters: data.parameters,
|
||||
strategies: JSON.stringify(data.strategies),
|
||||
};
|
||||
}
|
||||
|
||||
|
31
packages/unleash-api/lib/helper/legacy-feature-mapper.js
Normal file
31
packages/unleash-api/lib/helper/legacy-feature-mapper.js
Normal file
@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
function addOldFields (feature) {
|
||||
const modifiedFeature = Object.assign({}, feature);
|
||||
modifiedFeature.strategy = feature.strategies[0].name;
|
||||
modifiedFeature.parameters = Object.assign({}, feature.strategies[0].parameters);
|
||||
return modifiedFeature;
|
||||
}
|
||||
|
||||
function isOldFomrat (feature) {
|
||||
return feature.strategy && !feature.strategies;
|
||||
}
|
||||
|
||||
function toNewFormat (feature) {
|
||||
if (isOldFomrat(feature)) {
|
||||
return {
|
||||
name: feature.name,
|
||||
description: feature.description,
|
||||
enabled: feature.enabled,
|
||||
strategies: [
|
||||
{
|
||||
name: feature.strategy,
|
||||
parameters: Object.assign({}, feature.parameters),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return feature;
|
||||
}
|
||||
|
||||
module.exports = { addOldFields, toNewFormat };
|
@ -1,5 +1,6 @@
|
||||
'use strict';
|
||||
const eventDiffer = require('../eventDiffer');
|
||||
const version = 1;
|
||||
|
||||
module.exports = function (app, config) {
|
||||
const eventDb = config.eventDb;
|
||||
@ -7,7 +8,7 @@ module.exports = function (app, config) {
|
||||
app.get('/events', (req, res) => {
|
||||
eventDb.getEvents().then(events => {
|
||||
eventDiffer.addDiffs(events);
|
||||
res.json({ events });
|
||||
res.json({ version, events });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -8,24 +8,24 @@ const ValidationError = require('../error/ValidationError');
|
||||
const validateRequest = require('../error/validateRequest');
|
||||
const extractUser = require('../extractUser');
|
||||
|
||||
const legacyFeatureMapper = require('../helper/legacy-feature-mapper');
|
||||
const version = 1;
|
||||
|
||||
module.exports = function (app, config) {
|
||||
const featureDb = config.featureDb;
|
||||
const eventStore = config.eventStore;
|
||||
|
||||
app.get('/features', (req, res) => {
|
||||
featureDb.getFeatures().then(features => {
|
||||
res.json({ features });
|
||||
});
|
||||
featureDb.getFeatures()
|
||||
.then((features) => features.map(legacyFeatureMapper.addOldFields))
|
||||
.then(features => res.json({ version, features }));
|
||||
});
|
||||
|
||||
app.get('/features/:featureName', (req, res) => {
|
||||
featureDb.getFeature(req.params.featureName)
|
||||
.then(feature => {
|
||||
res.json(feature);
|
||||
})
|
||||
.catch(() => {
|
||||
res.status(404).json({ error: 'Could not find feature' });
|
||||
});
|
||||
.then(legacyFeatureMapper.addOldFields)
|
||||
.then(feature => res.json(feature).end())
|
||||
.catch(() => res.status(404).json({ error: 'Could not find feature' }));
|
||||
});
|
||||
|
||||
app.post('/features', (req, res) => {
|
||||
@ -33,24 +33,20 @@ module.exports = function (app, config) {
|
||||
req.checkBody('name', 'Name must match format ^[0-9a-zA-Z\\.\\-]+$').matches(/^[0-9a-zA-Z\\.\\-]+$/i);
|
||||
|
||||
validateRequest(req)
|
||||
.then(validateFormat)
|
||||
.then(validateUniqueName)
|
||||
.then(() => eventStore.create({
|
||||
type: eventType.featureCreated,
|
||||
createdBy: extractUser(req),
|
||||
data: req.body,
|
||||
data: legacyFeatureMapper.toNewFormat(req.body),
|
||||
}))
|
||||
.then(() => {
|
||||
res.status(201).end();
|
||||
})
|
||||
.then(() => res.status(201).end())
|
||||
.catch(NameExistsError, () => {
|
||||
res.status(403).json([{
|
||||
msg: `A feature named '${req.body.name}' already exists. It could be archived.`,
|
||||
}])
|
||||
.end();
|
||||
})
|
||||
.catch(ValidationError, () => {
|
||||
res.status(400).json(req.validationErrors());
|
||||
res.status(403)
|
||||
.json([{ msg: `A feature named '${req.body.name}' already exists.` }])
|
||||
.end();
|
||||
})
|
||||
.catch(ValidationError, () => res.status(400).json(req.validationErrors()))
|
||||
.catch(err => {
|
||||
logger.error('Could not create feature toggle', err);
|
||||
res.status(500).end();
|
||||
@ -60,7 +56,7 @@ module.exports = function (app, config) {
|
||||
app.put('/features/:featureName', (req, res) => {
|
||||
const featureName = req.params.featureName;
|
||||
const userName = extractUser(req);
|
||||
const updatedFeature = req.body;
|
||||
const updatedFeature = legacyFeatureMapper.toNewFormat(req.body);
|
||||
|
||||
updatedFeature.name = featureName;
|
||||
|
||||
@ -70,12 +66,8 @@ module.exports = function (app, config) {
|
||||
createdBy: userName,
|
||||
data: updatedFeature,
|
||||
}))
|
||||
.then(() => {
|
||||
res.status(200).end();
|
||||
})
|
||||
.catch(NotFoundError, () => {
|
||||
res.status(404).end();
|
||||
})
|
||||
.then(() => res.status(200).end())
|
||||
.catch(NotFoundError, () => res.status(404).end())
|
||||
.catch(err => {
|
||||
logger.error(`Could not update feature toggle=${featureName}`, err);
|
||||
res.status(500).end();
|
||||
@ -94,12 +86,8 @@ module.exports = function (app, config) {
|
||||
name: featureName,
|
||||
},
|
||||
}))
|
||||
.then(() => {
|
||||
res.status(200).end();
|
||||
})
|
||||
.catch(NotFoundError, () => {
|
||||
res.status(404).end();
|
||||
})
|
||||
.then(() => res.status(200).end())
|
||||
.catch(NotFoundError, () => res.status(404).end())
|
||||
.catch(err => {
|
||||
logger.error(`Could not archive feature=${featureName}`, err);
|
||||
res.status(500).end();
|
||||
@ -116,4 +104,12 @@ module.exports = function (app, config) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function validateFormat (req) {
|
||||
if (req.body.strategy && req.body.strategies) {
|
||||
return BPromise.reject(new ValidationError('Cannot use both "strategy" and "strategies".'));
|
||||
}
|
||||
|
||||
return BPromise.resolve(req);
|
||||
}
|
||||
};
|
||||
|
@ -7,6 +7,7 @@ const ValidationError = require('../error/ValidationError');
|
||||
const NotFoundError = require('../error/NotFoundError');
|
||||
const validateRequest = require('../error/validateRequest');
|
||||
const extractUser = require('../extractUser');
|
||||
const version = 1;
|
||||
|
||||
module.exports = function (app, config) {
|
||||
const strategyDb = config.strategyDb;
|
||||
@ -14,7 +15,7 @@ module.exports = function (app, config) {
|
||||
|
||||
app.get('/strategies', (req, res) => {
|
||||
strategyDb.getStrategies().then(strategies => {
|
||||
res.json({ strategies });
|
||||
res.json({ version, strategies });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,2 @@
|
||||
'use strict';
|
||||
module.exports = require('../scripts/migration-runner').create('007-add-strategies-to-features');
|
@ -0,0 +1,13 @@
|
||||
--create old columns
|
||||
ALTER TABLE features ADD "parameters" json;
|
||||
ALTER TABLE features ADD "strategy_name" varchar(255);
|
||||
|
||||
--populate old columns
|
||||
UPDATE features
|
||||
SET strategy_name = f.strategies->0->>'name',
|
||||
parameters = f.strategies->0->'parameters'
|
||||
FROM features as f
|
||||
WHERE f.name = features.name;
|
||||
|
||||
--drop new column
|
||||
ALTER TABLE features DROP COLUMN "strategies";
|
@ -0,0 +1,12 @@
|
||||
--create new strategies-column
|
||||
ALTER TABLE features ADD "strategies" json;
|
||||
|
||||
--populate the strategies column
|
||||
UPDATE features
|
||||
SET strategies = ('[{"name":"'||f.strategy_name||'","parameters":'||f.parameters||'}]')::json
|
||||
FROM features as f
|
||||
WHERE f.name = features.name;
|
||||
|
||||
--delete old strategy-columns
|
||||
ALTER TABLE features DROP COLUMN "strategy_name";
|
||||
ALTER TABLE features DROP COLUMN "parameters";
|
@ -35,12 +35,13 @@
|
||||
"start:dev:pg": "pg_virtualenv npm run start:dev:pg-chain",
|
||||
"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",
|
||||
"test": "export PORT=4243 ; mocha test test/*.js && npm run test:coverage",
|
||||
"test": "export PORT=4243 ; mocha test/**/*.js && npm run test:coverage",
|
||||
"test:unit": "mocha test/unit/**/*.js ",
|
||||
"test:ci": "npm run db-migrate && npm run test",
|
||||
"test:watch": "mocha --watch test test/*",
|
||||
"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 --report lcovonly -- -R spec",
|
||||
"test:coverage": "istanbul cover ./node_modules/mocha/bin/_mocha test/**/*.js --report lcovonly -- -R spec",
|
||||
"test:coverage-report": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -64,6 +65,7 @@
|
||||
"istanbul": "^0.4.3",
|
||||
"mocha": "^2.4.5",
|
||||
"mocha-lcov-reporter": "1.2.0",
|
||||
"sinon": "^1.17.5",
|
||||
"supertest": "^1.2.0",
|
||||
"supervisor": "^0.10.0",
|
||||
"unleash-frontend": "1.0.0-alpha.2"
|
||||
|
@ -1,5 +1,5 @@
|
||||
'use strict';
|
||||
const logger = require('../lib/logger');
|
||||
const logger = require('../../lib/logger');
|
||||
const assert = require('assert');
|
||||
const specHelper = require('./specHelper');
|
||||
const request = specHelper.request;
|
||||
@ -111,4 +111,55 @@ describe('The features api', () => {
|
||||
.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);
|
||||
});
|
||||
});
|
||||
});
|
@ -4,14 +4,14 @@ process.env.NODE_ENV = 'test';
|
||||
const BPromise = require('bluebird');
|
||||
let request = require('supertest');
|
||||
const databaseUri = require('./databaseConfig').getDatabaseUri();
|
||||
const knex = require('../lib/db/dbPool')(databaseUri);
|
||||
const eventDb = require('../lib/db/event')(knex);
|
||||
const EventStore = require('../lib/eventStore');
|
||||
const knex = require('../../lib/db/dbPool')(databaseUri);
|
||||
const eventDb = require('../../lib/db/event')(knex);
|
||||
const EventStore = require('../../lib/eventStore');
|
||||
const eventStore = new EventStore(eventDb);
|
||||
const featureDb = require('../lib/db/feature')(knex, eventStore);
|
||||
const strategyDb = require('../lib/db/strategy')(knex, eventStore);
|
||||
const featureDb = require('../../lib/db/feature')(knex, eventStore);
|
||||
const strategyDb = require('../../lib/db/strategy')(knex, eventStore);
|
||||
|
||||
const app = require('../app')({
|
||||
const app = require('../../app')({
|
||||
baseUriPath: '',
|
||||
db: knex,
|
||||
eventDb,
|
||||
@ -46,52 +46,60 @@ function createFeatures () {
|
||||
name: 'featureX',
|
||||
description: 'the #1 feature',
|
||||
enabled: true,
|
||||
strategy: 'default',
|
||||
strategies: [{ name: 'default', parameters: {} }],
|
||||
},
|
||||
{
|
||||
name: 'featureY',
|
||||
description: 'soon to be the #1 feature',
|
||||
enabled: false,
|
||||
strategy: 'baz',
|
||||
parameters: {
|
||||
foo: 'bar',
|
||||
},
|
||||
strategies: [{
|
||||
name: 'baz',
|
||||
parameters: {
|
||||
foo: 'bar',
|
||||
},
|
||||
}],
|
||||
},
|
||||
{
|
||||
name: 'featureZ',
|
||||
description: 'terrible feature',
|
||||
enabled: true,
|
||||
strategy: 'baz',
|
||||
parameters: {
|
||||
foo: 'rab',
|
||||
},
|
||||
strategies: [{
|
||||
name: 'baz',
|
||||
parameters: {
|
||||
foo: 'rab',
|
||||
},
|
||||
}],
|
||||
},
|
||||
{
|
||||
name: 'featureArchivedX',
|
||||
description: 'the #1 feature',
|
||||
enabled: true,
|
||||
archived: true,
|
||||
strategy: 'default',
|
||||
strategies: [{ name: 'default', parameters: {} }],
|
||||
},
|
||||
{
|
||||
name: 'featureArchivedY',
|
||||
description: 'soon to be the #1 feature',
|
||||
enabled: false,
|
||||
archived: true,
|
||||
strategy: 'baz',
|
||||
parameters: {
|
||||
foo: 'bar',
|
||||
},
|
||||
strategies: [{
|
||||
name: 'baz',
|
||||
parameters: {
|
||||
foo: 'bar',
|
||||
},
|
||||
}],
|
||||
},
|
||||
{
|
||||
name: 'featureArchivedZ',
|
||||
description: 'terrible feature',
|
||||
enabled: true,
|
||||
archived: true,
|
||||
strategy: 'baz',
|
||||
parameters: {
|
||||
foo: 'rab',
|
||||
},
|
||||
strategies: [{
|
||||
name: 'baz',
|
||||
parameters: {
|
||||
foo: 'rab',
|
||||
},
|
||||
}],
|
||||
},
|
||||
], feature => featureDb._createFeature(feature));
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
const eventDiffer = require('../lib/eventDiffer');
|
||||
const eventType = require('../lib/eventType');
|
||||
const eventDiffer = require('../../lib/eventDiffer');
|
||||
const eventType = require('../../lib/eventType');
|
||||
const assert = require('assert');
|
||||
|
||||
describe('eventDiffer', () => {
|
@ -0,0 +1,65 @@
|
||||
'use strict';
|
||||
const assert = require('assert');
|
||||
|
||||
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',
|
||||
parameters: {
|
||||
val: 'bar',
|
||||
},
|
||||
};
|
||||
|
||||
const mappedFeature = mapper.toNewFormat(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);
|
||||
});
|
||||
});
|
73
packages/unleash-api/test/unit/routes/feature.test.js
Normal file
73
packages/unleash-api/test/unit/routes/feature.test.js
Normal file
@ -0,0 +1,73 @@
|
||||
'use strict';
|
||||
|
||||
const supertest = require('supertest');
|
||||
const BPromise = require('bluebird');
|
||||
BPromise.promisifyAll(supertest);
|
||||
const assert = require('assert');
|
||||
const sinon = require('sinon');
|
||||
|
||||
let request;
|
||||
let featureDb;
|
||||
|
||||
describe('Unit: The features api', () => {
|
||||
beforeEach(done => {
|
||||
featureDb = createFeatureDb();
|
||||
|
||||
const app = require('../../../app')({
|
||||
baseUriPath: '',
|
||||
db: sinon.stub(),
|
||||
eventDb: sinon.stub(),
|
||||
eventStore: sinon.stub(),
|
||||
featureDb,
|
||||
strategyDb: sinon.stub(),
|
||||
});
|
||||
|
||||
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) => {
|
||||
featureDb.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) => {
|
||||
featureDb.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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createFeatureDb () {
|
||||
const _features = [];
|
||||
return {
|
||||
getFeatures: () => BPromise.resolve(_features),
|
||||
addFeature: (feature) => _features.push(feature),
|
||||
};
|
||||
}
|
47
packages/unleash-api/test/unit/routes/strategies.test.js
Normal file
47
packages/unleash-api/test/unit/routes/strategies.test.js
Normal file
@ -0,0 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
const supertest = require('supertest');
|
||||
const BPromise = require('bluebird');
|
||||
BPromise.promisifyAll(supertest);
|
||||
const assert = require('assert');
|
||||
const sinon = require('sinon');
|
||||
|
||||
let request;
|
||||
let stratDb;
|
||||
|
||||
describe('Unit: The strategies api', () => {
|
||||
beforeEach(done => {
|
||||
stratDb = createStrategyDb();
|
||||
|
||||
const app = require('../../../app')({
|
||||
baseUriPath: '',
|
||||
db: sinon.stub(),
|
||||
eventDb: sinon.stub(),
|
||||
eventStore: sinon.stub(),
|
||||
featureDb: sinon.stub(),
|
||||
strategyDb: stratDb,
|
||||
});
|
||||
|
||||
request = supertest(app);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should add version numbers for /stategies', (done) => {
|
||||
request
|
||||
.get('/strategies')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.end((err, res) => {
|
||||
assert.equal(res.body.version, 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createStrategyDb () {
|
||||
const _strategies = [{ name: 'default', parameters: {} }];
|
||||
return {
|
||||
getStrategies: () => BPromise.resolve(_strategies),
|
||||
addStrategy: (strat) => _strategies.push(strat),
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user