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:
parent
f8b62a063b
commit
40cc6e6548
8 changed files with 396 additions and 2 deletions
|
@ -11,6 +11,10 @@ module.exports = {
|
|||
return require('./settings');
|
||||
},
|
||||
|
||||
get tags() {
|
||||
return require('./tags');
|
||||
},
|
||||
|
||||
get users() {
|
||||
return require('./users');
|
||||
}
|
||||
|
|
|
@ -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" ]
|
||||
}
|
|
@ -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" ]
|
||||
}
|
66
core/server/api/v2/utils/validators/input/schemas/tags.json
Normal file
66
core/server/api/v2/utils/validators/input/schemas/tags.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
core/server/api/v2/utils/validators/input/tags.js
Normal file
15
core/server/api/v2/utils/validators/input/tags.js
Normal 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);
|
||||
}
|
||||
};
|
|
@ -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({
|
||||
|
|
22
core/server/api/v2/utils/validators/utils/strip-keyword.js
Normal file
22
core/server/api/v2/utils/validators/utils/strip-keyword.js
Normal 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;
|
||||
};
|
243
core/test/unit/api/v2/utils/validators/input/tags_spec.js
Normal file
243
core/test/unit/api/v2/utils/validators/input/tags_spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue