1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-10-23 20:07:40 +02:00
unleash.unleash/test/e2e/api/admin/feature.e2e.test.js
Christopher Kolstad 063d3f0e4a
Make feature-toggle-store return 409
- After seeing frontend behaviour where the user could add the same
tag multiple times, and not get errors or be stopped doing so, we'll
change the backend to return a 409 if you tag a feature with a tag it
already has.

- Previous to this commit, the setup was to do `onConflict().ignore()`
  which caused the frontend to not get any help from the backend as to
  whether or not the operation was allowed

- This fix adds a custom error and adds a branch to the handleError util
  method for handling just that error type with a 409.

- This caused a couple of tests to receive 409, probably due to
  insufficient cleanup between tests. Adding faker as a dev-dependency and randomising
  toggle names and tag values for each test reduces the chance that
  we'll run into duplicate issues in the future for the tests that
  touches this problem

fixes: #711
2021-02-08 16:22:15 +01:00

520 lines
16 KiB
JavaScript

'use strict';
const test = require('ava');
const faker = require('faker');
const dbInit = require('../../helpers/database-init');
const { setupApp } = require('../../helpers/test-helper');
const getLogger = require('../../../fixtures/no-logger');
let stores;
test.before(async () => {
const db = await dbInit('feature_api_serial', getLogger);
stores = db.stores;
});
test.after(async () => {
await stores.db.destroy();
});
test.serial('returns list of feature toggles', async t => {
const request = await setupApp(stores);
return request
.get('/api/admin/features')
.expect('Content-Type', /json/)
.expect(200)
.expect(res => {
t.true(res.body.features.length === 4);
});
});
test.serial('gets a feature by name', async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.get('/api/admin/features/featureX')
.expect('Content-Type', /json/)
.expect(200);
});
test.serial('cant get feature that dose not exist', async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.get('/api/admin/features/myfeature')
.expect('Content-Type', /json/)
.expect(404);
});
test.serial('creates new feature toggle', async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.post('/api/admin/features')
.send({
name: 'com.test.feature',
enabled: false,
strategies: [{ name: 'default' }],
})
.set('Content-Type', 'application/json')
.expect(201);
});
test.serial('creates new feature toggle with variants', async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.post('/api/admin/features')
.send({
name: 'com.test.variants',
enabled: false,
strategies: [{ name: 'default' }],
variants: [
{ name: 'variant1', weight: 50 },
{ name: 'variant2', weight: 50 },
],
})
.set('Content-Type', 'application/json')
.expect(201);
});
test.serial('fetch feature toggle with variants', async t => {
t.plan(1);
const request = await setupApp(stores);
return request
.get('/api/admin/features/feature.with.variants')
.expect(res => {
t.true(res.body.variants.length === 2);
});
});
test.serial('creates new feature toggle with createdBy unknown', async t => {
t.plan(1);
const request = await setupApp(stores);
await request
.post('/api/admin/features')
.send({
name: 'com.test.Username',
enabled: false,
strategies: [{ name: 'default' }],
})
.expect(201);
await request.get('/api/admin/events').expect(res => {
t.is(res.body.events[0].createdBy, 'unknown');
});
});
test.serial('require new feature toggle to have a name', async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.post('/api/admin/features')
.send({ name: '' })
.set('Content-Type', 'application/json')
.expect(400);
});
test.serial(
'can not change status of feature toggle that does not exist',
async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.put('/api/admin/features/should-not-exist')
.send({ name: 'should-not-exist', enabled: false })
.set('Content-Type', 'application/json')
.expect(404);
},
);
test.serial('can change status of feature toggle that does exist', async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.put('/api/admin/features/featureY')
.send({
name: 'featureY',
enabled: true,
strategies: [{ name: 'default' }],
})
.set('Content-Type', 'application/json')
.expect(200);
});
test.serial('can not toggle of feature that does not exist', async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.post('/api/admin/features/should-not-exist/toggle')
.set('Content-Type', 'application/json')
.expect(404);
});
test.serial('can toggle a feature that does exist', async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.post('/api/admin/features/featureY/toggle')
.set('Content-Type', 'application/json')
.expect(200);
});
test.serial('archives a feature by name', async t => {
t.plan(0);
const request = await setupApp(stores);
return request.delete('/api/admin/features/featureX').expect(200);
});
test.serial('can not archive unknown feature', async t => {
t.plan(0);
const request = await setupApp(stores);
return request.delete('/api/admin/features/featureUnknown').expect(404);
});
test.serial('refuses to create a feature with an existing name', async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.post('/api/admin/features')
.send({ name: 'featureX' })
.set('Content-Type', 'application/json')
.expect(409);
});
test.serial('refuses to validate a feature with an existing name', async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.post('/api/admin/features/validate')
.send({ name: 'featureX' })
.set('Content-Type', 'application/json')
.expect(409);
});
test.serial(
'new strategies api can add two strategies to a feature toggle',
async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.put('/api/admin/features/featureY')
.send({
name: 'featureY',
description: 'soon to be the #14 feature',
enabled: false,
strategies: [
{
name: 'baz',
parameters: { foo: 'bar' },
},
],
})
.set('Content-Type', 'application/json')
.expect(200);
},
);
test.serial('should not be possible to create archived toggle', async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.post('/api/admin/features')
.send({
name: 'featureArchivedX',
enabled: false,
strategies: [{ name: 'default' }],
})
.set('Content-Type', 'application/json')
.expect(409);
});
test.serial('creates new feature toggle with variant overrides', async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.post('/api/admin/features')
.send({
name: 'com.test.variants.overrieds',
enabled: false,
strategies: [{ name: 'default' }],
variants: [
{
name: 'variant1',
weight: 50,
overrides: [
{
contextName: 'userId',
values: ['123'],
},
],
},
{ name: 'variant2', weight: 50 },
],
})
.set('Content-Type', 'application/json')
.expect(201);
});
test.serial('creates new feature toggle without type', async t => {
t.plan(1);
const request = await setupApp(stores);
await request.post('/api/admin/features').send({
name: 'com.test.noType',
enabled: false,
strategies: [{ name: 'default' }],
});
await new Promise(r => setTimeout(r, 200));
return request.get('/api/admin/features/com.test.noType').expect(res => {
t.is(res.body.type, 'release');
});
});
test.serial('creates new feature toggle with type', async t => {
t.plan(1);
const request = await setupApp(stores);
await request.post('/api/admin/features').send({
name: 'com.test.withType',
type: 'killswitch',
enabled: false,
strategies: [{ name: 'default' }],
});
await new Promise(r => setTimeout(r, 200));
return request.get('/api/admin/features/com.test.withType').expect(res => {
t.is(res.body.type, 'killswitch');
});
});
test.serial('tags feature with new tag', async t => {
t.plan(1);
const request = await setupApp(stores);
await request.post('/api/admin/features').send({
name: 'test.feature',
type: 'killswitch',
enabled: true,
strategies: [{ name: 'default' }],
});
await request
.post('/api/admin/features/test.feature/tags')
.send({
value: 'TeamGreen',
type: 'simple',
})
.set('Content-Type', 'application/json');
await new Promise(r => setTimeout(r, 200));
return request.get('/api/admin/features/test.feature/tags').expect(res => {
t.is(res.body.tags[0].value, 'TeamGreen');
});
});
test.serial(
'tagging a feature with an already existing tag should be a noop',
async t => {
t.plan(1);
const request = await setupApp(stores);
await request.post('/api/admin/features').send({
name: 'test.feature',
type: 'killswitch',
enabled: true,
strategies: [{ name: 'default' }],
});
await request.post('/api/admin/features/test.feature/tags').send({
value: 'TeamGreen',
type: 'simple',
});
await request.post('/api/admin/features/test.feature/tags').send({
value: 'TeamGreen',
type: 'simple',
});
await new Promise(r => setTimeout(r, 200));
return request
.get('/api/admin/features/test.feature/tags')
.expect('Content-Type', /json/)
.expect(200)
.expect(res => {
t.is(res.body.tags.length, 1);
});
},
);
test.serial('can untag feature', async t => {
t.plan(1);
const request = await setupApp(stores);
const feature1Name = faker.helpers.slugify(faker.lorem.words(3));
await request.post('/api/admin/features').send({
name: feature1Name,
type: 'killswitch',
enabled: true,
strategies: [{ name: 'default' }],
});
const tag = { value: faker.lorem.word(), type: 'simple' };
await request
.post(`/api/admin/features/${feature1Name}/tags`)
.send(tag)
.expect(201);
await request
.delete(
`/api/admin/features/${feature1Name}/tags/${tag.type}/${tag.value}`,
)
.expect(200);
return request
.get(`/api/admin/features/${feature1Name}/tags`)
.expect('Content-Type', /json/)
.expect(200)
.expect(res => {
t.is(res.body.tags.length, 0);
});
});
test.serial('Can get features tagged by tag', async t => {
t.plan(2);
const request = await setupApp(stores);
const feature1Name = faker.helpers.slugify(faker.lorem.words(3));
const feature2Name = faker.helpers.slugify(faker.lorem.words(3));
await request.post('/api/admin/features').send({
name: feature1Name,
type: 'killswitch',
enabled: true,
strategies: [{ name: 'default' }],
});
await request.post('/api/admin/features').send({
name: feature2Name,
type: 'killswitch',
enabled: true,
strategies: [{ name: 'default' }],
});
const tag = { value: faker.lorem.word(), type: 'simple' };
await request
.post(`/api/admin/features/${feature1Name}/tags`)
.send(tag)
.expect(201);
return request
.get(`/api/admin/features?tag=${tag.type}:${tag.value}`)
.expect('Content-Type', /json/)
.expect(200)
.expect(res => {
t.is(res.body.features.length, 1);
t.is(res.body.features[0].name, feature1Name);
});
});
test.serial('Can query for multiple tags using OR', async t => {
t.plan(2);
const request = await setupApp(stores);
const feature1Name = faker.helpers.slugify(faker.lorem.words(3));
const feature2Name = faker.helpers.slugify(faker.lorem.words(3));
await request.post('/api/admin/features').send({
name: feature1Name,
type: 'killswitch',
enabled: true,
strategies: [{ name: 'default' }],
});
await request.post('/api/admin/features').send({
name: feature2Name,
type: 'killswitch',
enabled: true,
strategies: [{ name: 'default' }],
});
const tag = { value: faker.name.firstName(), type: 'simple' };
const tag2 = { value: faker.name.firstName(), type: 'simple' };
await request
.post(`/api/admin/features/${feature1Name}/tags`)
.send(tag)
.expect(201);
await request
.post(`/api/admin/features/${feature2Name}/tags`)
.send(tag2)
.expect(201);
return request
.get(
`/api/admin/features?tag[]=${tag.type}:${tag.value}&tag[]=${tag2.type}:${tag2.value}`,
)
.expect('Content-Type', /json/)
.expect(200)
.expect(res => {
t.is(res.body.features.length, 2);
t.is(res.body.features[0].name, feature1Name);
});
});
test.serial('Querying with multiple filters ANDs the filters', async t => {
const request = await setupApp(stores);
const feature1Name = `test.${faker.helpers.slugify(faker.hacker.phrase())}`;
const feature2Name = faker.helpers.slugify(faker.lorem.words());
await request.post('/api/admin/features').send({
name: feature1Name,
type: 'killswitch',
enabled: true,
strategies: [{ name: 'default' }],
});
await request.post('/api/admin/features').send({
name: feature2Name,
type: 'killswitch',
enabled: true,
strategies: [{ name: 'default' }],
});
await request.post('/api/admin/features').send({
name: 'notestprefix.feature3',
type: 'release',
enabled: true,
strategies: [{ name: 'default' }],
});
const tag = { value: faker.lorem.word(), type: 'simple' };
const tag2 = { value: faker.name.firstName(), type: 'simple' };
await request
.post(`/api/admin/features/${feature1Name}/tags`)
.send(tag)
.expect(201);
await request
.post(`/api/admin/features/${feature2Name}/tags`)
.send(tag2)
.expect(201);
await request
.post('/api/admin/features/notestprefix.feature3/tags')
.send(tag)
.expect(201);
await request
.get(`/api/admin/features?tag=${tag.type}:${tag.value}`)
.expect('Content-Type', /json/)
.expect(200)
.expect(res => t.is(res.body.features.length, 2));
await request
.get(`/api/admin/features?namePrefix=test&tag=${tag.type}:${tag.value}`)
.expect('Content-Type', /json/)
.expect(200)
.expect(res => {
t.is(res.body.features.length, 1);
t.is(res.body.features[0].name, feature1Name);
});
});
test.serial(
'Tagging a feature with a tag it already has should return 409',
async t => {
const request = await setupApp(stores);
const feature1Name = `test.${faker.helpers.slugify(
faker.lorem.words(3),
)}`;
await request.post('/api/admin/features').send({
name: feature1Name,
type: 'killswitch',
enabled: true,
strategies: [{ name: 'default' }],
});
const tag = { value: faker.lorem.word(), type: 'simple' };
await request
.post(`/api/admin/features/${feature1Name}/tags`)
.send(tag)
.expect(201);
return request
.post(`/api/admin/features/${feature1Name}/tags`)
.send(tag)
.expect(409)
.expect(res => {
t.is(
res.body.details[0].message,
`${feature1Name} already had the tag: [${tag.type}:${tag.value}]`,
);
});
},
);