diff --git a/docs/api-v1.md b/docs/api-v1.md new file mode 100644 index 0000000000..68ed02bf11 --- /dev/null +++ b/docs/api-v1.md @@ -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} + ] + } + ] +} +``` diff --git a/docs/schema.md b/docs/schema.md new file mode 100644 index 0000000000..af6fb0c650 --- /dev/null +++ b/docs/schema.md @@ -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"; +``` diff --git a/packages/unleash-api/lib/db/feature.js b/packages/unleash-api/lib/db/feature.js index e9325df883..ba4257defa 100644 --- a/packages/unleash-api/lib/db/feature.js +++ b/packages/unleash-api/lib/db/feature.js @@ -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), }; } diff --git a/packages/unleash-api/lib/helper/legacy-feature-mapper.js b/packages/unleash-api/lib/helper/legacy-feature-mapper.js new file mode 100644 index 0000000000..35f93237c5 --- /dev/null +++ b/packages/unleash-api/lib/helper/legacy-feature-mapper.js @@ -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 }; diff --git a/packages/unleash-api/lib/routes/event.js b/packages/unleash-api/lib/routes/event.js index fe9ec09bb8..568ee9e8ff 100644 --- a/packages/unleash-api/lib/routes/event.js +++ b/packages/unleash-api/lib/routes/event.js @@ -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 }); }); }); diff --git a/packages/unleash-api/lib/routes/feature.js b/packages/unleash-api/lib/routes/feature.js index 96ffb51717..9ac253928c 100644 --- a/packages/unleash-api/lib/routes/feature.js +++ b/packages/unleash-api/lib/routes/feature.js @@ -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); + } }; diff --git a/packages/unleash-api/lib/routes/strategy.js b/packages/unleash-api/lib/routes/strategy.js index 8eacc55c0d..00356aad5b 100644 --- a/packages/unleash-api/lib/routes/strategy.js +++ b/packages/unleash-api/lib/routes/strategy.js @@ -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 }); }); }); diff --git a/packages/unleash-api/migrations/20160618193924-add-strategies-to-features.js b/packages/unleash-api/migrations/20160618193924-add-strategies-to-features.js new file mode 100644 index 0000000000..82e87ab8aa --- /dev/null +++ b/packages/unleash-api/migrations/20160618193924-add-strategies-to-features.js @@ -0,0 +1,2 @@ +'use strict'; +module.exports = require('../scripts/migration-runner').create('007-add-strategies-to-features'); diff --git a/packages/unleash-api/migrations/sql/007-add-strategies-to-features.down.sql b/packages/unleash-api/migrations/sql/007-add-strategies-to-features.down.sql new file mode 100644 index 0000000000..45adc8d9d4 --- /dev/null +++ b/packages/unleash-api/migrations/sql/007-add-strategies-to-features.down.sql @@ -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"; diff --git a/packages/unleash-api/migrations/sql/007-add-strategies-to-features.up.sql b/packages/unleash-api/migrations/sql/007-add-strategies-to-features.up.sql new file mode 100644 index 0000000000..ea01049bd3 --- /dev/null +++ b/packages/unleash-api/migrations/sql/007-add-strategies-to-features.up.sql @@ -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"; diff --git a/packages/unleash-api/package.json b/packages/unleash-api/package.json index 00b7e6ba5b..d6a9fa63c0 100644 --- a/packages/unleash-api/package.json +++ b/packages/unleash-api/package.json @@ -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" diff --git a/packages/unleash-api/test/databaseConfig.js b/packages/unleash-api/test/e2e/databaseConfig.js similarity index 100% rename from packages/unleash-api/test/databaseConfig.js rename to packages/unleash-api/test/e2e/databaseConfig.js diff --git a/packages/unleash-api/test/eventApiSpec.js b/packages/unleash-api/test/e2e/eventApiSpec.js similarity index 100% rename from packages/unleash-api/test/eventApiSpec.js rename to packages/unleash-api/test/e2e/eventApiSpec.js diff --git a/packages/unleash-api/test/featureApiSpec.js b/packages/unleash-api/test/e2e/featureApiSpec.js similarity index 63% rename from packages/unleash-api/test/featureApiSpec.js rename to packages/unleash-api/test/e2e/featureApiSpec.js index 4dd03a0165..5c383125d3 100644 --- a/packages/unleash-api/test/featureApiSpec.js +++ b/packages/unleash-api/test/e2e/featureApiSpec.js @@ -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); + }); + }); }); diff --git a/packages/unleash-api/test/featureArchiveApiSpec.js b/packages/unleash-api/test/e2e/featureArchiveApiSpec.js similarity index 100% rename from packages/unleash-api/test/featureArchiveApiSpec.js rename to packages/unleash-api/test/e2e/featureArchiveApiSpec.js diff --git a/packages/unleash-api/test/routerSpec.js b/packages/unleash-api/test/e2e/routerSpec.js similarity index 100% rename from packages/unleash-api/test/routerSpec.js rename to packages/unleash-api/test/e2e/routerSpec.js diff --git a/packages/unleash-api/test/specHelper.js b/packages/unleash-api/test/e2e/specHelper.js similarity index 69% rename from packages/unleash-api/test/specHelper.js rename to packages/unleash-api/test/e2e/specHelper.js index ebbd17abba..1033b4591b 100644 --- a/packages/unleash-api/test/specHelper.js +++ b/packages/unleash-api/test/e2e/specHelper.js @@ -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)); } diff --git a/packages/unleash-api/test/strategyApiSpec.js b/packages/unleash-api/test/e2e/strategyApiSpec.js similarity index 100% rename from packages/unleash-api/test/strategyApiSpec.js rename to packages/unleash-api/test/e2e/strategyApiSpec.js diff --git a/packages/unleash-api/test/eventDifferTest.js b/packages/unleash-api/test/unit/eventDiffer.test.js similarity index 96% rename from packages/unleash-api/test/eventDifferTest.js rename to packages/unleash-api/test/unit/eventDiffer.test.js index be3c66436f..a40e3b3b21 100644 --- a/packages/unleash-api/test/eventDifferTest.js +++ b/packages/unleash-api/test/unit/eventDiffer.test.js @@ -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', () => { diff --git a/packages/unleash-api/test/unit/helper/legacy-feature-mapper.test.js b/packages/unleash-api/test/unit/helper/legacy-feature-mapper.test.js new file mode 100644 index 0000000000..1b288da2af --- /dev/null +++ b/packages/unleash-api/test/unit/helper/legacy-feature-mapper.test.js @@ -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); + }); +}); diff --git a/packages/unleash-api/test/unit/routes/feature.test.js b/packages/unleash-api/test/unit/routes/feature.test.js new file mode 100644 index 0000000000..e4695ab16f --- /dev/null +++ b/packages/unleash-api/test/unit/routes/feature.test.js @@ -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), + }; +} diff --git a/packages/unleash-api/test/unit/routes/strategies.test.js b/packages/unleash-api/test/unit/routes/strategies.test.js new file mode 100644 index 0000000000..2b5ae03bb5 --- /dev/null +++ b/packages/unleash-api/test/unit/routes/strategies.test.js @@ -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), + }; +}