diff --git a/core/server/data/validation/index.js b/core/server/data/validation/index.js
index bbbae182d7..82d914d424 100644
--- a/core/server/data/validation/index.js
+++ b/core/server/data/validation/index.js
@@ -1,343 +1,8 @@
-const schema = require('../schema').tables;
-const _ = require('lodash');
-const validator = require('validator');
-const moment = require('moment-timezone');
-const assert = require('assert');
-const Promise = require('bluebird');
-const i18n = require('../../../shared/i18n');
-const errors = require('@tryghost/errors');
-const settingsCache = require('../../services/settings/cache');
-const urlUtils = require('../../../shared/url-utils');
-
-function assertString(input) {
- assert(typeof input === 'string', 'Validator js validates strings only');
-}
-
-/**
- * Counts repeated characters in a string. When 50% or more characters are the same,
- * we return false and therefore invalidate the string.
- * @param {String} stringToTest The password string to check.
- * @return {Boolean}
- */
-function characterOccurance(stringToTest) {
- const chars = {};
- let allowedOccurancy;
- let valid = true;
-
- stringToTest = _.toString(stringToTest);
- allowedOccurancy = stringToTest.length / 2;
-
- // Loop through string and accumulate character counts
- _.each(stringToTest, function (char) {
- if (!chars[char]) {
- chars[char] = 1;
- } else {
- chars[char] += 1;
- }
- });
-
- // check if any of the accumulated chars exceed the allowed occurancy
- // of 50% of the words' length.
- _.forIn(chars, function (charCount) {
- if (charCount >= allowedOccurancy) {
- valid = false;
- }
- });
-
- return valid;
-}
-
-// extends has been removed in validator >= 5.0.0, need to monkey-patch it back in
-// @TODO: We modify the global validator dependency here! https://github.com/chriso/validator.js/issues/525#issuecomment-213149570
-validator.extend = function (name, fn) {
- validator[name] = function () {
- const args = Array.prototype.slice.call(arguments);
- assertString(args[0]);
- return fn.apply(validator, args);
- };
-};
-
-// Provide a few custom validators
-validator.extend('empty', function empty(str) {
- return _.isEmpty(str);
-});
-
-validator.extend('notContains', function notContains(str, badString) {
- return !_.includes(str, badString);
-});
-
-validator.extend('isTimezone', function isTimezone(str) {
- return moment.tz.zone(str) ? true : false;
-});
-
-validator.extend('isEmptyOrURL', function isEmptyOrURL(str) {
- return (_.isEmpty(str) || validator.isURL(str, {require_protocol: false}));
-});
-
-validator.extend('isSlug', function isSlug(str) {
- return validator.matches(str, /^[a-z0-9\-_]+$/);
-});
-
-/**
- * Validation against simple password rules
- * Returns false when validation fails and true for a valid password
- * @param {String} password The password string to check.
- * @param {String} email The users email address to validate agains password.
- * @param {String} blogTitle Optional blogTitle value, when blog title is not set yet, e. g. in setup process.
- * @return {Object} example for returned validation Object:
- * invalid password: `validationResult: {isValid: false, message: 'Sorry, you cannot use an insecure password.'}`
- * valid password: `validationResult: {isValid: true}`
- */
-function validatePassword(password, email, blogTitle) {
- const validationResult = {isValid: true};
- const disallowedPasswords = ['password', 'ghost', 'passw0rd'];
- let blogUrl = urlUtils.urlFor('home', true);
-
- const badPasswords = [
- '1234567890',
- 'qwertyuiop',
- 'qwertzuiop',
- 'asdfghjkl;',
- 'abcdefghij',
- '0987654321',
- '1q2w3e4r5t',
- '12345asdfg'
- ];
-
- blogTitle = blogTitle ? blogTitle : settingsCache.get('title');
- blogUrl = blogUrl.replace(/^http(s?):\/\//, '');
-
- // password must be longer than 10 characters
- if (!validator.isLength(password, 10)) {
- validationResult.isValid = false;
- validationResult.message = i18n.t('errors.models.user.passwordDoesNotComplyLength', {minLength: 10});
-
- return validationResult;
- }
-
- // dissallow password from badPasswords list (e. g. '1234567890')
- _.each(badPasswords, function (badPassword) {
- if (badPassword === password) {
- validationResult.isValid = false;
- }
- });
-
- // password must not match with users' email
- if (email && email.toLowerCase() === password.toLowerCase()) {
- validationResult.isValid = false;
- }
-
- // password must not contain the words 'ghost', 'password', or 'passw0rd'
- _.each(disallowedPasswords, function (disallowedPassword) {
- if (password.toLowerCase().indexOf(disallowedPassword) >= 0) {
- validationResult.isValid = false;
- }
- });
-
- // password must not match with blog title
- if (blogTitle && blogTitle.toLowerCase() === password.toLowerCase()) {
- validationResult.isValid = false;
- }
-
- // password must not match with blog URL (without protocol, with or without trailing slash)
- if (blogUrl && (blogUrl.toLowerCase() === password.toLowerCase() || blogUrl.toLowerCase().replace(/\/$/, '') === password.toLowerCase())) {
- validationResult.isValid = false;
- }
-
- // dissallow passwords where 50% or more of characters are the same
- if (!characterOccurance(password)) {
- validationResult.isValid = false;
- }
-
- // Generic error message for the rules where no dedicated error massage is set
- if (!validationResult.isValid && !validationResult.message) {
- validationResult.message = i18n.t('errors.models.user.passwordDoesNotComplySecurity');
- }
-
- return validationResult;
-}
-
-/**
- * Validate model against schema.
- *
- * ## on model update
- * - only validate changed fields
- * - otherwise we could throw errors which the user is out of control
- * - e.g.
- * - we add a new field without proper validation, release goes out
- * - we add proper validation for a single field
- * - if you call `user.save()` the default fallback in bookshelf is `options.method=update`.
- * - we set `options.method` explicit for adding resources (because otherwise bookshelf uses `update`)
- *
- * ## on model add
- * - validate everything to catch required fields
- */
-function validateSchema(tableName, model, options) {
- options = options || {};
-
- const columns = _.keys(schema[tableName]);
- let validationErrors = [];
-
- _.each(columns, function each(columnKey) {
- let message = ''; // KEEP: Validator.js only validates strings.
- const strVal = _.toString(model.get(columnKey));
-
- if (options.method !== 'insert' && !_.has(model.changed, columnKey)) {
- return;
- }
-
- // check nullable
- if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'nullable') &&
- schema[tableName][columnKey].nullable !== true &&
- !Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'defaultTo')
- ) {
- if (validator.empty(strVal)) {
- message = i18n.t('notices.data.validation.index.valueCannotBeBlank', {
- tableName: tableName,
- columnKey: columnKey
- });
- validationErrors.push(new errors.ValidationError({
- message: message,
- context: tableName + '.' + columnKey
- }));
- }
- }
-
- // validate boolean columns
- if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'type')
- && schema[tableName][columnKey].type === 'bool') {
- if (!(validator.isBoolean(strVal) || validator.empty(strVal))) {
- message = i18n.t('notices.data.validation.index.valueMustBeBoolean', {
- tableName: tableName,
- columnKey: columnKey
- });
- validationErrors.push(new errors.ValidationError({
- message: message,
- context: tableName + '.' + columnKey
- }));
- }
-
- // CASE: ensure we transform 0|1 to false|true
- if (!validator.empty(strVal)) {
- model.set(columnKey, !!model.get(columnKey));
- }
- }
-
- // TODO: check if mandatory values should be enforced
- if (model.get(columnKey) !== null && model.get(columnKey) !== undefined) {
- // check length
- if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'maxlength')) {
- if (!validator.isLength(strVal, 0, schema[tableName][columnKey].maxlength)) {
- message = i18n.t('notices.data.validation.index.valueExceedsMaxLength',
- {
- tableName: tableName,
- columnKey: columnKey,
- maxlength: schema[tableName][columnKey].maxlength
- });
- validationErrors.push(new errors.ValidationError({
- message: message,
- context: tableName + '.' + columnKey
- }));
- }
- }
-
- // check validations objects
- if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'validations')) {
- validationErrors = validationErrors.concat(validate(strVal, columnKey, schema[tableName][columnKey].validations, tableName));
- }
-
- // check type
- if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'type')) {
- if (schema[tableName][columnKey].type === 'integer' && !validator.isInt(strVal)) {
- message = i18n.t('notices.data.validation.index.valueIsNotInteger', {
- tableName: tableName,
- columnKey: columnKey
- });
- validationErrors.push(new errors.ValidationError({
- message: message,
- context: tableName + '.' + columnKey
- }));
- }
- }
- }
- });
-
- if (validationErrors.length !== 0) {
- return Promise.reject(validationErrors);
- }
-
- return Promise.resolve();
-}
-
-/**
- * Validate keys using the validator module.
- * Each validation's key is a method name and its value is an array of options
- * eg:
- * validations: { isURL: true, isLength: [20, 40] }
- * will validate that a values's length is a URL between 20 and 40 chars.
- *
- * If you pass a boolean as the value, it will specify the "good" result. By default
- * the "good" result is assumed to be true.
- * eg:
- * validations: { isNull: false } // means the "good" result would
- * // fail the `isNull` check, so
- * // not null.
- *
- * available validators: https://github.com/chriso/validator.js#validators
- * @param {String} value the value to validate.
- * @param {String} key the db column key of the value to validate.
- * @param {Object} validations the validations object as described above.
- * @param {String} tableName (optional) the db table of the value to validate, used for error message.
- * @return {Array} returns an Array including the found validation errors (empty if none found);
- */
-function validate(value, key, validations, tableName) {
- const validationErrors = [];
- let translation;
- value = _.toString(value);
-
- _.each(validations, function each(validationOptions, validationName) {
- let goodResult = true;
-
- if (_.isBoolean(validationOptions)) {
- goodResult = validationOptions;
- validationOptions = [];
- } else if (!_.isArray(validationOptions)) {
- validationOptions = [validationOptions];
- }
-
- validationOptions.unshift(value);
-
- // equivalent of validator.isSomething(option1, option2)
- if (validator[validationName].apply(validator, validationOptions) !== goodResult) {
- // CASE: You can define specific translations for validators e.g. isLength
- if (i18n.doesTranslationKeyExist('notices.data.validation.index.validationFailedTypes.' + validationName)) {
- translation = i18n.t('notices.data.validation.index.validationFailedTypes.' + validationName, _.merge({
- validationName: validationName,
- key: key,
- tableName: tableName
- }, validationOptions[1]));
- } else {
- translation = i18n.t('notices.data.validation.index.validationFailed', {
- validationName: validationName,
- key: key
- });
- }
-
- validationErrors.push(new errors.ValidationError({
- message: translation,
- context: `${tableName}.${key}`
- }));
- }
-
- validationOptions.shift();
- }, this);
-
- return validationErrors;
-}
-
module.exports = {
- validate,
- validator,
- validatePassword,
- validateSchema
+ validate: require('./validate'),
+ validator: require('./validator'),
+
+ // These two things are dependent on validator, not related
+ validatePassword: require('./password'),
+ validateSchema: require('./schema')
};
diff --git a/core/server/data/validation/password.js b/core/server/data/validation/password.js
new file mode 100644
index 0000000000..a12d5af5fb
--- /dev/null
+++ b/core/server/data/validation/password.js
@@ -0,0 +1,122 @@
+const _ = require('lodash');
+
+const validator = require('./validator');
+
+const i18n = require('../../../shared/i18n');
+const settingsCache = require('../../services/settings/cache');
+const urlUtils = require('../../../shared/url-utils');
+
+/**
+ * Counts repeated characters in a string. When 50% or more characters are the same,
+ * we return false and therefore invalidate the string.
+ * @param {String} stringToTest The password string to check.
+ * @return {Boolean}
+ */
+function characterOccurance(stringToTest) {
+ const chars = {};
+ let allowedOccurancy;
+ let valid = true;
+
+ stringToTest = _.toString(stringToTest);
+ allowedOccurancy = stringToTest.length / 2;
+
+ // Loop through string and accumulate character counts
+ _.each(stringToTest, function (char) {
+ if (!chars[char]) {
+ chars[char] = 1;
+ } else {
+ chars[char] += 1;
+ }
+ });
+
+ // check if any of the accumulated chars exceed the allowed occurancy
+ // of 50% of the words' length.
+ _.forIn(chars, function (charCount) {
+ if (charCount >= allowedOccurancy) {
+ valid = false;
+ }
+ });
+
+ return valid;
+}
+
+/**
+ * Validation against simple password rules
+ * Returns false when validation fails and true for a valid password
+ * @param {String} password The password string to check.
+ * @param {String} email The users email address to validate agains password.
+ * @param {String} blogTitle Optional blogTitle value, when blog title is not set yet, e. g. in setup process.
+ * @return {Object} example for returned validation Object:
+ * invalid password: `validationResult: {isValid: false, message: 'Sorry, you cannot use an insecure password.'}`
+ * valid password: `validationResult: {isValid: true}`
+ */
+function validatePassword(password, email, blogTitle) {
+ const validationResult = {isValid: true};
+ const disallowedPasswords = ['password', 'ghost', 'passw0rd'];
+ let blogUrl = urlUtils.urlFor('home', true);
+
+ const badPasswords = [
+ '1234567890',
+ 'qwertyuiop',
+ 'qwertzuiop',
+ 'asdfghjkl;',
+ 'abcdefghij',
+ '0987654321',
+ '1q2w3e4r5t',
+ '12345asdfg'
+ ];
+
+ blogTitle = blogTitle ? blogTitle : settingsCache.get('title');
+ blogUrl = blogUrl.replace(/^http(s?):\/\//, '');
+
+ // password must be longer than 10 characters
+ if (!validator.isLength(password, 10)) {
+ validationResult.isValid = false;
+ validationResult.message = i18n.t('errors.models.user.passwordDoesNotComplyLength', {minLength: 10});
+
+ return validationResult;
+ }
+
+ // dissallow password from badPasswords list (e. g. '1234567890')
+ _.each(badPasswords, function (badPassword) {
+ if (badPassword === password) {
+ validationResult.isValid = false;
+ }
+ });
+
+ // password must not match with users' email
+ if (email && email.toLowerCase() === password.toLowerCase()) {
+ validationResult.isValid = false;
+ }
+
+ // password must not contain the words 'ghost', 'password', or 'passw0rd'
+ _.each(disallowedPasswords, function (disallowedPassword) {
+ if (password.toLowerCase().indexOf(disallowedPassword) >= 0) {
+ validationResult.isValid = false;
+ }
+ });
+
+ // password must not match with blog title
+ if (blogTitle && blogTitle.toLowerCase() === password.toLowerCase()) {
+ validationResult.isValid = false;
+ }
+
+ // password must not match with blog URL (without protocol, with or without trailing slash)
+ if (blogUrl && (blogUrl.toLowerCase() === password.toLowerCase() || blogUrl.toLowerCase().replace(/\/$/, '') === password.toLowerCase())) {
+ validationResult.isValid = false;
+ }
+
+ // dissallow passwords where 50% or more of characters are the same
+ if (!characterOccurance(password)) {
+ validationResult.isValid = false;
+ }
+
+ // Generic error message for the rules where no dedicated error massage is set
+ if (!validationResult.isValid && !validationResult.message) {
+ validationResult.message = i18n.t('errors.models.user.passwordDoesNotComplySecurity');
+ }
+
+ return validationResult;
+}
+
+module.exports = validatePassword;
diff --git a/core/server/data/validation/schema.js b/core/server/data/validation/schema.js
new file mode 100644
index 0000000000..c5546f86c7
--- /dev/null
+++ b/core/server/data/validation/schema.js
@@ -0,0 +1,122 @@
+const _ = require('lodash');
+const Promise = require('bluebird');
+
+const i18n = require('../../../shared/i18n');
+const errors = require('@tryghost/errors');
+
+const schema = require('../schema').tables;
+const validator = require('./validator');
+const validate = require('./validate');
+
+/**
+ * Validate model against schema.
+ *
+ * ## on model update
+ * - only validate changed fields
+ * - otherwise we could throw errors which the user is out of control
+ * - e.g.
+ * - we add a new field without proper validation, release goes out
+ * - we add proper validation for a single field
+ * - if you call `user.save()` the default fallback in bookshelf is `options.method=update`.
+ * - we set `options.method` explicit for adding resources (because otherwise bookshelf uses `update`)
+ *
+ * ## on model add
+ * - validate everything to catch required fields
+ */
+function validateSchema(tableName, model, options) {
+ options = options || {};
+
+ const columns = _.keys(schema[tableName]);
+ let validationErrors = [];
+
+ _.each(columns, function each(columnKey) {
+ let message = ''; // KEEP: Validator.js only validates strings.
+ const strVal = _.toString(model.get(columnKey));
+
+ if (options.method !== 'insert' && !_.has(model.changed, columnKey)) {
+ return;
+ }
+
+ // check nullable
+ if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'nullable') &&
+ schema[tableName][columnKey].nullable !== true &&
+ !Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'defaultTo')
+ ) {
+ if (validator.empty(strVal)) {
+ message = i18n.t('notices.data.validation.index.valueCannotBeBlank', {
+ tableName: tableName,
+ columnKey: columnKey
+ });
+ validationErrors.push(new errors.ValidationError({
+ message: message,
+ context: tableName + '.' + columnKey
+ }));
+ }
+ }
+
+ // validate boolean columns
+ if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'type')
+ && schema[tableName][columnKey].type === 'bool') {
+ if (!(validator.isBoolean(strVal) || validator.empty(strVal))) {
+ message = i18n.t('notices.data.validation.index.valueMustBeBoolean', {
+ tableName: tableName,
+ columnKey: columnKey
+ });
+ validationErrors.push(new errors.ValidationError({
+ message: message,
+ context: tableName + '.' + columnKey
+ }));
+ }
+
+ // CASE: ensure we transform 0|1 to false|true
+ if (!validator.empty(strVal)) {
+ model.set(columnKey, !!model.get(columnKey));
+ }
+ }
+
+ // TODO: check if mandatory values should be enforced
+ if (model.get(columnKey) !== null && model.get(columnKey) !== undefined) {
+ // check length
+ if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'maxlength')) {
+ if (!validator.isLength(strVal, 0, schema[tableName][columnKey].maxlength)) {
+ message = i18n.t('notices.data.validation.index.valueExceedsMaxLength',
+ {
+ tableName: tableName,
+ columnKey: columnKey,
+ maxlength: schema[tableName][columnKey].maxlength
+ });
+ validationErrors.push(new errors.ValidationError({
+ message: message,
+ context: tableName + '.' + columnKey
+ }));
+ }
+ }
+
+ // check validations objects
+ if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'validations')) {
+ validationErrors = validationErrors.concat(validate(strVal, columnKey, schema[tableName][columnKey].validations, tableName));
+ }
+
+ // check type
+ if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'type')) {
+ if (schema[tableName][columnKey].type === 'integer' && !validator.isInt(strVal)) {
+ message = i18n.t('notices.data.validation.index.valueIsNotInteger', {
+ tableName: tableName,
+ columnKey: columnKey
+ });
+ validationErrors.push(new errors.ValidationError({
+ message: message,
+ context: tableName + '.' + columnKey
+ }));
+ }
+ }
+ }
+ });
+
+ if (validationErrors.length !== 0) {
+ return Promise.reject(validationErrors);
+ }
+
+ return Promise.resolve();
+}
+module.exports = validateSchema;
diff --git a/core/server/data/validation/validate.js b/core/server/data/validation/validate.js
new file mode 100644
index 0000000000..7740ef3d54
--- /dev/null
+++ b/core/server/data/validation/validate.js
@@ -0,0 +1,72 @@
+const _ = require('lodash');
+const validator = require('./validator');
+const i18n = require('../../../shared/i18n');
+const errors = require('@tryghost/errors');
+
+/**
+ * Validate keys using the validator module.
+ * Each validation's key is a method name and its value is an array of options
+ * eg:
+ * validations: { isURL: true, isLength: [20, 40] }
+ * will validate that a values's length is a URL between 20 and 40 chars.
+ *
+ * If you pass a boolean as the value, it will specify the "good" result. By default
+ * the "good" result is assumed to be true.
+ * eg:
+ * validations: { isNull: false } // means the "good" result would
+ * // fail the `isNull` check, so
+ * // not null.
+ *
+ * available validators: https://github.com/chriso/validator.js#validators
+ * @param {String} value the value to validate.
+ * @param {String} key the db column key of the value to validate.
+ * @param {Object} validations the validations object as described above.
+ * @param {String} tableName (optional) the db table of the value to validate, used for error message.
+ * @return {Array} returns an Array including the found validation errors (empty if none found);
+ */
+function validate(value, key, validations, tableName) {
+ const validationErrors = [];
+ let translation;
+ value = _.toString(value);
+
+ _.each(validations, function each(validationOptions, validationName) {
+ let goodResult = true;
+
+ if (_.isBoolean(validationOptions)) {
+ goodResult = validationOptions;
+ validationOptions = [];
+ } else if (!_.isArray(validationOptions)) {
+ validationOptions = [validationOptions];
+ }
+
+ validationOptions.unshift(value);
+
+ // equivalent of validator.isSomething(option1, option2)
+ if (validator[validationName].apply(validator, validationOptions) !== goodResult) {
+ // CASE: You can define specific translations for validators e.g. isLength
+ if (i18n.doesTranslationKeyExist('notices.data.validation.index.validationFailedTypes.' + validationName)) {
+ translation = i18n.t('notices.data.validation.index.validationFailedTypes.' + validationName, _.merge({
+ validationName: validationName,
+ key: key,
+ tableName: tableName
+ }, validationOptions[1]));
+ } else {
+ translation = i18n.t('notices.data.validation.index.validationFailed', {
+ validationName: validationName,
+ key: key
+ });
+ }
+
+ validationErrors.push(new errors.ValidationError({
+ message: translation,
+ context: `${tableName}.${key}`
+ }));
+ }
+
+ validationOptions.shift();
+ }, this);
+
+ return validationErrors;
+}
+
+module.exports = validate;
diff --git a/core/server/data/validation/validator.js b/core/server/data/validation/validator.js
new file mode 100644
index 0000000000..8a65c88289
--- /dev/null
+++ b/core/server/data/validation/validator.js
@@ -0,0 +1,42 @@
+const _ = require('lodash');
+
+const validator = require('validator');
+const moment = require('moment-timezone');
+const assert = require('assert');
+
+function assertString(input) {
+ assert(typeof input === 'string', 'Validator js validates strings only');
+}
+
+// extends has been removed in validator >= 5.0.0, need to monkey-patch it back in
+// @TODO: We modify the global validator dependency here! https://github.com/chriso/validator.js/issues/525#issuecomment-213149570
+validator.extend = function (name, fn) {
+ validator[name] = function () {
+ const args = Array.prototype.slice.call(arguments);
+ assertString(args[0]);
+ return fn.apply(validator, args);
+ };
+};
+
+// Provide a few custom validators
+validator.extend('empty', function empty(str) {
+ return _.isEmpty(str);
+});
+
+validator.extend('notContains', function notContains(str, badString) {
+ return !_.includes(str, badString);
+});
+
+validator.extend('isTimezone', function isTimezone(str) {
+ return moment.tz.zone(str) ? true : false;
+});
+
+validator.extend('isEmptyOrURL', function isEmptyOrURL(str) {
+ return (_.isEmpty(str) || validator.isURL(str, {require_protocol: false}));
+});
+
+validator.extend('isSlug', function isSlug(str) {
+ return validator.matches(str, /^[a-z0-9\-_]+$/);
+});
+
+module.exports = validator;
diff --git a/test/unit/data/validation/index_spec.js b/test/unit/data/validation/index_spec.js
index 46248bfb01..1e1b52cce1 100644
--- a/test/unit/data/validation/index_spec.js
+++ b/test/unit/data/validation/index_spec.js
@@ -1,21 +1,14 @@
const should = require('should');
-const _ = require('lodash');
-const ObjectId = require('bson-objectid');
-const testUtils = require('../../../utils');
-const models = require('../../../../core/server/models');
+
const validation = require('../../../../core/server/data/validation');
// Validate our customizations
describe('Validation', function () {
- before(function () {
- models.init();
- });
-
it('should export our required functions', function () {
should.exist(validation);
validation.should.have.properties(
- ['validate', 'validator', 'validateSchema']
+ ['validate', 'validator', 'validateSchema', 'validatePassword']
);
validation.validate.should.be.a.Function();
@@ -24,160 +17,4 @@ describe('Validation', function () {
validation.validator.should.have.properties(['empty', 'notContains', 'isTimezone', 'isEmptyOrURL', 'isSlug']);
});
-
- describe('Validate Schema', function () {
- describe('models.add', function () {
- it('blank model', function () {
- // NOTE: Fields with `defaultTo` are getting ignored. This is handled on the DB level.
- return validation.validateSchema('posts', models.Post.forge(), {method: 'insert'})
- .then(function () {
- throw new Error('Expected ValidationError.');
- })
- .catch(function (err) {
- if (!_.isArray(err)) {
- throw err;
- }
-
- err.length.should.eql(7);
-
- const errorMessages = _.map(err, function (object) {
- return object.message;
- }).join(',');
-
- // NOTE: Some of these fields are auto-filled in the model layer (e.g. author_id, created_at etc.)
- ['id', 'uuid', 'slug', 'title', 'author_id', 'created_at', 'created_by'].forEach(function (attr) {
- errorMessages.should.match(new RegExp('posts.' + attr));
- });
- });
- });
-
- it('blank id', function () {
- const postModel = models.Post.forge(testUtils.DataGenerator.forKnex.createPost({
- id: null,
- slug: 'test'
- }));
-
- return validation.validateSchema('posts', postModel, {method: 'insert'})
- .then(function () {
- throw new Error('Expected ValidationError.');
- })
- .catch(function (err) {
- if (!_.isArray(err)) {
- throw err;
- }
-
- err.length.should.eql(1);
- err[0].message.should.match(/posts\.id/);
- });
- });
-
- it('should pass', function () {
- return validation.validateSchema(
- 'posts',
- models.Post.forge(testUtils.DataGenerator.forKnex.createPost({slug: 'title'})),
- {method: 'insert'}
- );
- });
-
- it('transforms 0 and 1', function () {
- const post = models.Post.forge(testUtils.DataGenerator.forKnex.createPost({slug: 'test', featured: 0}));
- post.get('featured').should.eql(0);
-
- return validation.validateSchema('posts', post, {method: 'insert'})
- .then(function () {
- post.get('featured').should.eql(false);
- });
- });
-
- it('keeps true or false', function () {
- const post = models.Post.forge(testUtils.DataGenerator.forKnex.createPost({slug: 'test', featured: true}));
- post.get('featured').should.eql(true);
-
- return validation.validateSchema('posts', post, {method: 'insert'})
- .then(function () {
- post.get('featured').should.eql(true);
- });
- });
- });
-
- describe('webhooks.add', function () {
- it('event name is not lowercase', function () {
- const webhook = models.Webhook.forge(testUtils.DataGenerator.forKnex.createWebhook({
- event: 'Test',
- integration_id: testUtils.DataGenerator.Content.integrations[0].id
- }));
-
- // NOTE: Fields with `defaultTo` are getting ignored. This is handled on the DB level.
- return validation.validateSchema('webhooks', webhook, {method: 'insert'})
- .then(function () {
- throw new Error('Expected ValidationError.');
- })
- .catch(function (err) {
- if (!_.isArray(err)) {
- throw err;
- }
-
- err.length.should.eql(1);
- err[0].errorType.should.eql('ValidationError');
- err[0].message.should.match(/isLowercase/);
- });
- });
- });
-
- describe('models.edit', function () {
- it('uuid is invalid', function () {
- const postModel = models.Post.forge({id: ObjectId().toHexString(), uuid: '1234'});
-
- postModel.changed = {uuid: postModel.get('uuid')};
-
- return validation.validateSchema('posts', postModel)
- .then(function () {
- throw new Error('Expected ValidationError.');
- })
- .catch(function (err) {
- if (!_.isArray(err)) {
- throw err;
- }
-
- err.length.should.eql(1);
- err[0].message.should.match(/isUUID/);
- });
- });
-
- it('date is null', function () {
- const postModel = models.Post.forge({id: ObjectId().toHexString(), created_at: null});
-
- postModel.changed = {created_at: postModel.get('updated_at')};
-
- return validation.validateSchema('posts', postModel)
- .then(function () {
- throw new Error('Expected ValidationError.');
- })
- .catch(function (err) {
- if (!_.isArray(err)) {
- throw err;
- }
-
- err.length.should.eql(1);
- err[0].message.should.match(/posts\.created_at/);
- });
- });
- });
- });
-
- describe('Assert the Validator dependency', function () {
- const validator = validation.validator;
-
- it('isEmptyOrUrl filters javascript urls', function () {
- validator.isEmptyOrURL('javascript:alert(0)').should.be.false();
- validator.isEmptyOrURL('http://example.com/lol//').should.be.false();
- validator.isEmptyOrURL('http://example.com/lol?somequery=').should.be.false();
- validator.isEmptyOrURL('').should.be.true();
- validator.isEmptyOrURL('http://localhost:2368').should.be.true();
- validator.isEmptyOrURL('http://example.com/test/').should.be.true();
- validator.isEmptyOrURL('http://www.example.com/test/').should.be.true();
- validator.isEmptyOrURL('http://example.com/foo?somequery=bar').should.be.true();
- validator.isEmptyOrURL('example.com/test/').should.be.true();
- });
- });
});
diff --git a/test/unit/data/validation/schema_spec.js b/test/unit/data/validation/schema_spec.js
new file mode 100644
index 0000000000..b377f2561f
--- /dev/null
+++ b/test/unit/data/validation/schema_spec.js
@@ -0,0 +1,150 @@
+const should = require('should');
+const _ = require('lodash');
+const ObjectId = require('bson-objectid');
+const testUtils = require('../../../utils');
+const models = require('../../../../core/server/models');
+const validation = require('../../../../core/server/data/validation');
+
+describe('Validate Schema', function () {
+ before(function () {
+ models.init();
+ });
+
+ describe('models.add', function () {
+ it('blank model', function () {
+ // NOTE: Fields with `defaultTo` are getting ignored. This is handled on the DB level.
+ return validation.validateSchema('posts', models.Post.forge(), {method: 'insert'})
+ .then(function () {
+ throw new Error('Expected ValidationError.');
+ })
+ .catch(function (err) {
+ if (!_.isArray(err)) {
+ throw err;
+ }
+
+ err.length.should.eql(7);
+
+ const errorMessages = _.map(err, function (object) {
+ return object.message;
+ }).join(',');
+
+ // NOTE: Some of these fields are auto-filled in the model layer (e.g. author_id, created_at etc.)
+ ['id', 'uuid', 'slug', 'title', 'author_id', 'created_at', 'created_by'].forEach(function (attr) {
+ errorMessages.should.match(new RegExp('posts.' + attr));
+ });
+ });
+ });
+
+ it('blank id', function () {
+ const postModel = models.Post.forge(testUtils.DataGenerator.forKnex.createPost({
+ id: null,
+ slug: 'test'
+ }));
+
+ return validation.validateSchema('posts', postModel, {method: 'insert'})
+ .then(function () {
+ throw new Error('Expected ValidationError.');
+ })
+ .catch(function (err) {
+ if (!_.isArray(err)) {
+ throw err;
+ }
+
+ err.length.should.eql(1);
+ err[0].message.should.match(/posts\.id/);
+ });
+ });
+
+ it('should pass', function () {
+ return validation.validateSchema(
+ 'posts',
+ models.Post.forge(testUtils.DataGenerator.forKnex.createPost({slug: 'title'})),
+ {method: 'insert'}
+ );
+ });
+
+ it('transforms 0 and 1', function () {
+ const post = models.Post.forge(testUtils.DataGenerator.forKnex.createPost({slug: 'test', featured: 0}));
+ post.get('featured').should.eql(0);
+
+ return validation.validateSchema('posts', post, {method: 'insert'})
+ .then(function () {
+ post.get('featured').should.eql(false);
+ });
+ });
+
+ it('keeps true or false', function () {
+ const post = models.Post.forge(testUtils.DataGenerator.forKnex.createPost({slug: 'test', featured: true}));
+ post.get('featured').should.eql(true);
+
+ return validation.validateSchema('posts', post, {method: 'insert'})
+ .then(function () {
+ post.get('featured').should.eql(true);
+ });
+ });
+ });
+
+ describe('webhooks.add', function () {
+ it('event name is not lowercase', function () {
+ const webhook = models.Webhook.forge(testUtils.DataGenerator.forKnex.createWebhook({
+ event: 'Test',
+ integration_id: testUtils.DataGenerator.Content.integrations[0].id
+ }));
+
+ // NOTE: Fields with `defaultTo` are getting ignored. This is handled on the DB level.
+ return validation.validateSchema('webhooks', webhook, {method: 'insert'})
+ .then(function () {
+ throw new Error('Expected ValidationError.');
+ })
+ .catch(function (err) {
+ if (!_.isArray(err)) {
+ throw err;
+ }
+
+ err.length.should.eql(1);
+ err[0].errorType.should.eql('ValidationError');
+ err[0].message.should.match(/isLowercase/);
+ });
+ });
+ });
+
+ describe('models.edit', function () {
+ it('uuid is invalid', function () {
+ const postModel = models.Post.forge({id: ObjectId().toHexString(), uuid: '1234'});
+
+ postModel.changed = {uuid: postModel.get('uuid')};
+
+ return validation.validateSchema('posts', postModel)
+ .then(function () {
+ throw new Error('Expected ValidationError.');
+ })
+ .catch(function (err) {
+ if (!_.isArray(err)) {
+ throw err;
+ }
+
+ err.length.should.eql(1);
+ err[0].message.should.match(/isUUID/);
+ });
+ });
+
+ it('date is null', function () {
+ const postModel = models.Post.forge({id: ObjectId().toHexString(), created_at: null});
+
+ postModel.changed = {created_at: postModel.get('updated_at')};
+
+ return validation.validateSchema('posts', postModel)
+ .then(function () {
+ throw new Error('Expected ValidationError.');
+ })
+ .catch(function (err) {
+ if (!_.isArray(err)) {
+ throw err;
+ }
+
+ err.length.should.eql(1);
+ err[0].message.should.match(/posts\.created_at/);
+ });
+ });
+ });
+});
diff --git a/test/unit/data/validation/validator_spec.js b/test/unit/data/validation/validator_spec.js
new file mode 100644
index 0000000000..7637ab5a94
--- /dev/null
+++ b/test/unit/data/validation/validator_spec.js
@@ -0,0 +1,19 @@
+const should = require('should');
+
+const validation = require('../../../../core/server/data/validation');
+
+describe('Validator dependency', function () {
+ const validator = validation.validator;
+
+ it('isEmptyOrUrl filters javascript urls', function () {
+ validator.isEmptyOrURL('javascript:alert(0)').should.be.false();
+ validator.isEmptyOrURL('http://example.com/lol//').should.be.false();
+ validator.isEmptyOrURL('http://example.com/lol?somequery=').should.be.false();
+ validator.isEmptyOrURL('').should.be.true();
+ validator.isEmptyOrURL('http://localhost:2368').should.be.true();
+ validator.isEmptyOrURL('http://example.com/test/').should.be.true();
+ validator.isEmptyOrURL('http://www.example.com/test/').should.be.true();
+ validator.isEmptyOrURL('http://example.com/foo?somequery=bar').should.be.true();
+ validator.isEmptyOrURL('example.com/test/').should.be.true();
+ });
+});