1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-31 00:16:47 +01:00

chore(modernize): Use joi schema-validation in FeatureController

This commit is contained in:
ivaosthu 2018-12-01 12:03:47 +01:00 committed by Ivar Conradi Østhus
parent 7705bfe1a9
commit 9eb0d2e535
5 changed files with 114 additions and 64 deletions

View File

@ -0,0 +1,14 @@
'use strict';
class NameInvalidError extends Error {
constructor(message) {
super();
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.message = message;
this.msg = message;
}
}
module.exports = NameInvalidError;

View File

@ -0,0 +1,30 @@
'use strict';
const joi = require('joi');
const strategiesSchema = joi.object().keys({
name: joi
.string()
.regex(/^[a-zA-Z0-9\\.\\-]{3,100}$/)
.required(),
parameters: joi.object(),
});
const featureShema = joi
.object()
.keys({
name: joi
.string()
.regex(/^[a-zA-Z0-9\\.\\-]{3,100}$/)
.required(),
enabled: joi.boolean().default(false),
description: joi.string(),
strategies: joi
.array()
.required()
.min(1)
.items(strategiesSchema),
})
.options({ allowUnknown: false, stripUnknown: true });
module.exports = { featureShema, strategiesSchema };

View File

@ -10,25 +10,32 @@ const {
FEATURE_ARCHIVED, FEATURE_ARCHIVED,
} = require('../../event-type'); } = require('../../event-type');
const NameExistsError = require('../../error/name-exists-error'); const NameExistsError = require('../../error/name-exists-error');
const NotFoundError = require('../../error/notfound-error'); const NameInvalidError = require('../../error/name-invalid-error');
const ValidationError = require('../../error/validation-error.js'); const { isUrlFriendlyName } = require('./util');
const validateRequest = require('../../error/validate-request');
const extractUser = require('../../extract-user'); const extractUser = require('../../extract-user');
const { featureShema } = require('./feature-schema');
const handleErrors = (req, res, error) => { const handleErrors = (req, res, error) => {
logger.warn('Error creating or updating feature', error); logger.warn('Error creating or updating feature', error);
switch (error.constructor) { switch (error.name) {
case NotFoundError: case 'NotFoundError':
return res.status(404).end(); return res.status(404).end();
case NameExistsError: case 'NameInvalidError':
return res
.status(400)
.json([{ msg: error.message }])
.end();
case 'NameExistsError':
return res return res
.status(403) .status(403)
.json([{ msg: error.message }]) .json([{ msg: error.message }])
.end(); .end();
case ValidationError: case 'ValidationError':
return res return res
.status(400) .status(400)
.json(req.validationErrors()) .json(error)
.end(); .end();
default: default:
logger.error('Server failed executing request', error); logger.error('Server failed executing request', error);
@ -36,39 +43,6 @@ const handleErrors = (req, res, error) => {
} }
}; };
const strategiesSchema = joi.object().keys({
name: joi
.string()
.regex(/^[a-zA-Z0-9\\.\\-]{3,100}$/)
.required(),
parameters: joi.object(),
});
function validateStrategy(featureToggle) {
return new Promise((resolve, reject) => {
if (
!featureToggle.strategies ||
featureToggle.strategies.length === 0
) {
return reject(
new ValidationError('You must define at least one strategy')
);
}
featureToggle.strategies = featureToggle.strategies.map(
strategyConfig => {
const result = joi.validate(strategyConfig, strategiesSchema);
if (result.error) {
throw result.error;
}
return result.value;
}
);
return resolve(featureToggle);
});
}
const version = 1; const version = 1;
class FeatureController extends Controller { class FeatureController extends Controller {
@ -104,42 +78,49 @@ class FeatureController extends Controller {
} }
async validate(req, res) { async validate(req, res) {
req.checkBody('name', 'Name is required').notEmpty(); const name = req.body.name;
req.checkBody('name', 'Name must be URL friendly').isUrlFriendlyName();
try { try {
await validateRequest(req); await this.validateNameFormat(name);
await this.validateUniqueName(req); await this.validateUniqueName(name);
res.status(201).end(); res.status(201).end();
} catch (error) { } catch (error) {
handleErrors(req, res, error); handleErrors(req, res, error);
} }
} }
validateUniqueName(req) { // TODO: cleanup this validation
return new Promise((resolve, reject) => { async validateUniqueName(name) {
this.featureToggleStore let msg;
.hasFeature(req.body.name) try {
.then(definition => { const definition = await this.featureToggleStore.hasFeature(name);
const msg = definition.archived msg = definition.archived
? 'An archived toggle with that name already exist' ? 'An archived toggle with that name already exist'
: 'A toggle with that name already exist'; : 'A toggle with that name already exist';
reject(new NameExistsError(msg)); } catch (error) {
}) // No conflict, everything ok!
.catch(() => resolve(req)); return;
}); }
// Interntional throw here!
throw new NameExistsError(msg);
}
// TODO: this should be part of the schema validation!
validateNameFormat(name) {
if (!isUrlFriendlyName(name)) {
throw new NameInvalidError('Name must be URL friendly');
}
} }
async createToggle(req, res) { async createToggle(req, res) {
req.checkBody('name', 'Name is required').notEmpty(); const toggleName = req.body.name;
req.checkBody('name', 'Name must be URL friendly').isUrlFriendlyName();
const userName = extractUser(req); const userName = extractUser(req);
try { try {
await validateRequest(req); await this.validateNameFormat(toggleName);
await this.validateUniqueName(req); await this.validateUniqueName(toggleName);
const featureToggle = await validateStrategy(req.body); const featureToggle = await joi.validate(req.body, featureShema);
await this.eventStore.store({ await this.eventStore.store({
type: FEATURE_CREATED, type: FEATURE_CREATED,
createdBy: userName, createdBy: userName,
@ -160,7 +141,7 @@ class FeatureController extends Controller {
try { try {
await this.featureToggleStore.getFeature(featureName); await this.featureToggleStore.getFeature(featureName);
await validateStrategy(updatedFeature); await joi.validate(updatedFeature, featureShema);
await this.eventStore.store({ await this.eventStore.store({
type: FEATURE_UPDATED, type: FEATURE_UPDATED,
createdBy: userName, createdBy: userName,

View File

@ -200,3 +200,23 @@ test('invalid feature names should not pass validation', t => {
) )
); );
}); });
test('invalid feature names should have error msg', t => {
t.plan(1);
const { request, base } = getSetup();
const name = 'ØÆ`';
return request
.post(`${base}/api/admin/features`)
.send({
name,
enabled: false,
strategies: [{ name: 'default' }],
})
.set('Content-Type', 'application/json')
.expect(400)
.expect(res => {
t.true(res.body[0].msg === 'Name must be URL friendly');
});
});

View File

@ -0,0 +1,5 @@
'use strict';
const isUrlFriendlyName = input => encodeURIComponent(input) === input;
module.exports = { isUrlFriendlyName };