0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-15 03:01:37 -05:00

Added JSON Schema validations for /tags (#10486)

Added JSON Schema validations for /tags endpoints

refs #10438
refs #9100

- Added JSON Schemas for POST/PUT /tags endpoints
- Added 'strip' keyword definition schema allowing to strip data and not throw errors on further validation stages
This commit is contained in:
Naz Gargol 2019-02-13 12:26:32 +00:00 committed by GitHub
parent f8b62a063b
commit 40cc6e6548
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 396 additions and 2 deletions

View file

@ -11,6 +11,10 @@ module.exports = {
return require('./settings');
},
get tags() {
return require('./tags');
},
get users() {
return require('./users');
}

View file

@ -0,0 +1,23 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "tags.add",
"title": "tags.add",
"description": "Schema for tags.add",
"type": "object",
"additionalProperties": false,
"properties": {
"tags": {
"type": "array",
"minItems": 1,
"maxItems": 1,
"additionalProperties": false,
"items": {
"type": "object",
"allOf": [{"$ref": "tags#/definitions/tag"}],
"required": ["name"]
}
}
},
"required": [ "tags" ]
}

View file

@ -0,0 +1,18 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "tags.edit",
"title": "tags.edit",
"description": "Schema for tags.edit",
"type": "object",
"additionalProperties": false,
"properties": {
"tags": {
"type": "array",
"minItems": 1,
"maxItems": 1,
"items": {"$ref": "tags#/definitions/tag"}
}
},
"required": [ "tags" ]
}

View file

@ -0,0 +1,66 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "tags",
"title": "tags",
"description": "Base tags definitions",
"definitions": {
"tag": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"maxLength": 191,
"pattern": "^([^,]|$)"
},
"slug": {
"type": ["string", "null"],
"maxLength": 191
},
"description": {
"type": ["string", "null"],
"maxLength": 500
},
"feature_image": {
"type": ["string", "null"],
"format": "uri-reference",
"maxLength": 2000
},
"visibility": {
"type": "string",
"enum": ["public", "internal"]
},
"meta_title": {
"type": ["string", "null"],
"maxLength": 300
},
"meta_description": {
"type": ["string", "null"],
"maxLength": 500
},
"id": {
"strip": true
},
"parent": {
"strip": true
},
"parent_id": {
"strip": true
},
"created_at": {
"strip": true
},
"created_by": {
"strip": true
},
"updated_at": {
"strip": true
},
"updated_by": {
"strip": true
}
}
}
}
}

View file

@ -0,0 +1,15 @@
const jsonSchema = require('../utils/json-schema');
module.exports = {
add(apiConfig, frame) {
const schema = require('./schemas/tags-add');
const definitions = require('./schemas/tags');
return jsonSchema.validate(schema, definitions, frame.data);
},
edit(apiConfig, frame) {
const schema = require('./schemas/tags-edit');
const definitions = require('./schemas/tags');
return jsonSchema.validate(schema, definitions, frame.data);
}
};

View file

@ -1,14 +1,17 @@
const Ajv = require('ajv');
const stripKeyword = require('./strip-keyword');
const common = require('../../../../../lib/common');
const validate = (schema, definitions, json) => {
const validate = (schema, definitions, data) => {
const ajv = new Ajv({
allErrors: true
});
stripKeyword(ajv);
const validation = ajv.addSchema(definitions).compile(schema);
validation(json);
validation(data);
if (validation.errors) {
return Promise.reject(new common.errors.ValidationError({

View file

@ -0,0 +1,22 @@
/**
* 'strip' keyword is introduced into schemas for following behavior:
* properties that are 'known' but should not be present in the model
* should be stripped from data and not throw validation errors.
*
* An example of such property is `tag.parent` which we want to ignore
* but not necessarily throw a validation error as it was present in
* responses in previous versions of API
*/
module.exports = function defFunc(ajv) {
defFunc.definition = {
errors: false,
modifying: true,
valid: true,
validate: function (schema, data, parentSchema, dataPath, parentData, propName) {
delete parentData[propName];
}
};
ajv.addKeyword('strip', defFunc.definition);
return ajv;
};

View file

@ -0,0 +1,243 @@
const _ = require('lodash');
const should = require('should');
const sinon = require('sinon');
const Promise = require('bluebird');
const common = require('../../../../../../../server/lib/common');
const validators = require('../../../../../../../server/api/v2/utils/validators');
describe('Unit: v2/utils/validators/input/tags', function () {
afterEach(function () {
sinon.restore();
});
describe('add', function () {
const apiConfig = {
docName: 'tags'
};
describe('required fields', function () {
it('should fail with no data', function () {
const frame = {
options: {},
data: {}
};
return validators.input.tags.add(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should fail with no tags', function () {
const frame = {
options: {},
data: {
posts: []
}
};
return validators.input.tags.add(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should fail with no tags in array', function () {
const frame = {
options: {},
data: {
tags: []
}
};
return validators.input.tags.add(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should fail with more than tags', function () {
const frame = {
options: {},
data: {
tags: [],
posts: []
}
};
return validators.input.tags.add(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should fail without required fields', function () {
const frame = {
options: {},
data: {
tags: [{
what: 'a fail'
}],
}
};
return validators.input.tags.add(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should pass with required fields', function () {
const frame = {
options: {},
data: {
tags: [{
name: 'pass'
}],
}
};
return validators.input.tags.add(apiConfig, frame);
});
it('should remove `strip`able fields and leave regular fields', function () {
const frame = {
options: {},
data: {
tags: [{
name: 'pass',
parent: 'strip me',
created_at: 'strip me',
created_by: 'strip me',
updated_at: 'strip me',
updated_by: 'strip me'
}],
}
};
let result = validators.input.tags.add(apiConfig, frame);
should.exist(frame.data.tags[0].name);
should.not.exist(frame.data.tags[0].parent);
should.not.exist(frame.data.tags[0].created_at);
should.not.exist(frame.data.tags[0].created_by);
should.not.exist(frame.data.tags[0].updated_at);
should.not.exist(frame.data.tags[0].updated_by);
return result;
});
});
describe('field formats', function () {
const fieldMap = {
name: [123, new Date(), ',starts-with-coma', _.repeat('a', 192), null],
slug: [123, new Date(), _.repeat('a', 192), null],
description: [123, new Date(), _.repeat('a', 500)],
feature_image: [123, new Date(), 'abc'],
visibility: [123, new Date(), 'abc', null],
meta_title: [123, new Date(), _.repeat('a', 301)],
meta_description: [123, new Date(), _.repeat('a', 501)],
};
Object.keys(fieldMap).forEach(key => {
it(`should fail for bad ${key}`, function () {
const badValues = fieldMap[key];
const checks = badValues.map((value) => {
const tag = {};
tag[key] = value;
if (key !== 'title') {
tag.title = 'abc';
}
const frame = {
options: {},
data: {
tags: [tag]
}
};
return validators.input.tags.add(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
return Promise.all(checks);
});
});
});
});
describe('edit', function () {
const apiConfig = {
docName: 'tags'
};
describe('required fields', function () {
it('should fail with no data', function () {
const frame = {
options: {},
data: {}
};
return validators.input.tags.edit(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should fail with no tags', function () {
const frame = {
options: {},
data: {
posts: []
}
};
return validators.input.tags.edit(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should fail with more than tags', function () {
const frame = {
options: {},
data: {
tags: [],
posts: []
}
};
return validators.input.tags.edit(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should pass with some fields', function () {
const frame = {
options: {},
data: {
tags: [{
name: 'pass'
}],
}
};
return validators.input.tags.edit(apiConfig, frame);
});
});
});
});