mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
closes #12001 * Moved settings validation to the model This moves the settings validation out of the validation file and into the model, as it is _only_ used there. It also sets us up in the future for custom validators on individual settings. * Improved validation of stripe_plans setting - Checks `interval` is a valid string - Checks `name` & `currency` are strings * Moved stripe key validation into model The stripe key settings are all nullable and the regex validation fails when the input is `null`. Rather than reworking the entirety of how we validate with default-settings validation objects, this moves the validation into methods on the Settings model. * Added tests for new setting validations Adds tests for both valid and invalid settings, as well as helpers making future tests easier and less repetitive
404 lines
14 KiB
JavaScript
404 lines
14 KiB
JavaScript
const should = require('should');
|
|
const sinon = require('sinon');
|
|
const mockDb = require('mock-knex');
|
|
const models = require('../../../core/server/models');
|
|
const {knex} = require('../../../core/server/data/db');
|
|
const {events} = require('../../../core/server/lib/common');
|
|
const defaultSettings = require('../../../core/server/data/schema/default-settings');
|
|
const errors = require('@tryghost/errors');
|
|
|
|
describe('Unit: models/settings', function () {
|
|
before(function () {
|
|
models.init();
|
|
});
|
|
|
|
describe('events', function () {
|
|
let tracker;
|
|
let eventSpy;
|
|
|
|
beforeEach(function () {
|
|
mockDb.mock(knex);
|
|
tracker = mockDb.getTracker();
|
|
tracker.install();
|
|
});
|
|
|
|
afterEach(function () {
|
|
mockDb.unmock(knex);
|
|
});
|
|
|
|
beforeEach(function () {
|
|
eventSpy = sinon.spy(events, 'emit');
|
|
});
|
|
|
|
afterEach(function () {
|
|
sinon.restore();
|
|
});
|
|
|
|
it('emits add events', function () {
|
|
tracker.on('query', (query, step) => {
|
|
return [
|
|
function fetchAddQuery() {
|
|
query.response([{}]);
|
|
},
|
|
function addQuery() {
|
|
query.response([{
|
|
key: 'description',
|
|
value: 'added value'
|
|
}]);
|
|
}
|
|
][step - 1]();
|
|
});
|
|
|
|
return models.Settings.add({
|
|
key: 'description',
|
|
value: 'added value',
|
|
type: 'string'
|
|
})
|
|
.then(() => {
|
|
eventSpy.calledTwice.should.be.true();
|
|
eventSpy.firstCall.calledWith('settings.added').should.be.true();
|
|
eventSpy.secondCall.calledWith('settings.description.added').should.be.true();
|
|
});
|
|
});
|
|
|
|
it('emits edit events', function () {
|
|
tracker.on('query', (query, step) => {
|
|
return [
|
|
function fetchEditQuery() {
|
|
query.response([{
|
|
id: 1, // NOTE: `id` imitates existing value for 'edit' event
|
|
key: 'description',
|
|
value: 'db value'
|
|
}]);
|
|
}
|
|
][step - 1]();
|
|
});
|
|
|
|
return models.Settings.edit({
|
|
key: 'description',
|
|
value: 'edited value'
|
|
})
|
|
.then(() => {
|
|
eventSpy.calledTwice.should.be.true();
|
|
eventSpy.firstCall.calledWith('settings.edited').should.be.true();
|
|
eventSpy.secondCall.calledWith('settings.description.edited').should.be.true();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('defaults', function () {
|
|
let tracker;
|
|
let eventSpy;
|
|
|
|
beforeEach(function () {
|
|
mockDb.mock(knex);
|
|
tracker = mockDb.getTracker();
|
|
tracker.install();
|
|
});
|
|
|
|
afterEach(function () {
|
|
mockDb.unmock(knex);
|
|
tracker.uninstall();
|
|
});
|
|
|
|
beforeEach(function () {
|
|
eventSpy = sinon.spy(events, 'emit');
|
|
});
|
|
|
|
afterEach(function () {
|
|
sinon.restore();
|
|
});
|
|
|
|
it('populates unset defaults', function () {
|
|
let insertQueries = [];
|
|
|
|
tracker.on('query', (query) => {
|
|
// skip group and flags columns so we can test the insertion column skip
|
|
if (query.method === 'columnInfo') {
|
|
return query.response([
|
|
{name: 'id', type: 'varchar'},
|
|
// {name: 'group', type: 'varchar'},
|
|
{name: 'key', type: 'varchar'},
|
|
{name: 'value', type: 'varchar'},
|
|
{name: 'type', type: 'varchar'},
|
|
// {name: 'flags', type: 'varchar'},
|
|
{name: 'created_at', type: 'datetime'},
|
|
{name: 'created_by', type: 'varchar'},
|
|
{name: 'updated_at', type: 'varchar'},
|
|
{name: 'updated_by', type: 'datetime'}
|
|
]);
|
|
}
|
|
|
|
if (query.method === 'insert') {
|
|
insertQueries.push(query);
|
|
}
|
|
|
|
return query.response([{}]);
|
|
});
|
|
|
|
return models.Settings.populateDefaults()
|
|
.then(() => {
|
|
const numberOfSettings = Object.keys(defaultSettings).reduce((settings, settingGroup) => {
|
|
return settings.concat(Object.keys(defaultSettings[settingGroup]));
|
|
}, []).length;
|
|
|
|
insertQueries.length.should.equal(numberOfSettings);
|
|
|
|
// non-existent columns should not be populated
|
|
insertQueries[0].sql.should.not.match(/group/);
|
|
insertQueries[0].sql.should.not.match(/flags/);
|
|
|
|
// no events are emitted because we're not using the model layer
|
|
eventSpy.callCount.should.equal(0);
|
|
});
|
|
});
|
|
|
|
it('doesn\'t overwrite any existing settings', function () {
|
|
let insertQueries = [];
|
|
|
|
tracker.on('query', (query) => {
|
|
if (query.method === 'columnInfo') {
|
|
return query.response([
|
|
{name: 'id', type: 'varchar'},
|
|
{name: 'key', type: 'varchar'},
|
|
{name: 'value', type: 'varchar'}
|
|
]);
|
|
}
|
|
|
|
if (query.method === 'insert') {
|
|
insertQueries.push(query);
|
|
}
|
|
|
|
return query.response([{
|
|
key: 'description',
|
|
value: 'Adam\'s Blog'
|
|
}]);
|
|
});
|
|
|
|
return models.Settings.populateDefaults()
|
|
.then(() => {
|
|
const numberOfSettings = Object.keys(defaultSettings).reduce((settings, settingGroup) => {
|
|
return settings.concat(Object.keys(defaultSettings[settingGroup]));
|
|
}, []).length;
|
|
|
|
insertQueries.length.should.equal(numberOfSettings - 1);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('parse', function () {
|
|
it('ensure correct parsing when fetching from db', function () {
|
|
const setting = models.Settings.forge();
|
|
|
|
let returns = setting.parse({key: 'is_private', value: 'false', type: 'boolean'});
|
|
should.equal(returns.value, false);
|
|
|
|
returns = setting.parse({key: 'is_private', value: false, type: 'boolean'});
|
|
should.equal(returns.value, false);
|
|
|
|
returns = setting.parse({key: 'is_private', value: true, type: 'boolean'});
|
|
should.equal(returns.value, true);
|
|
|
|
returns = setting.parse({key: 'is_private', value: 'true', type: 'boolean'});
|
|
should.equal(returns.value, true);
|
|
|
|
returns = setting.parse({key: 'is_private', value: '0', type: 'boolean'});
|
|
should.equal(returns.value, false);
|
|
|
|
returns = setting.parse({key: 'is_private', value: '1', type: 'boolean'});
|
|
should.equal(returns.value, true);
|
|
|
|
returns = setting.parse({key: 'something', value: 'null'});
|
|
should.equal(returns.value, 'null');
|
|
});
|
|
});
|
|
|
|
describe('validation', function () {
|
|
async function testInvalidSetting({key, value, type, group}) {
|
|
const setting = models.Settings.forge({key, value, type, group});
|
|
|
|
let error;
|
|
try {
|
|
await setting.save();
|
|
error = null;
|
|
} catch (err) {
|
|
error = err;
|
|
} finally {
|
|
should.exist(error, `Setting Model should throw when saving invalid ${key}`);
|
|
should.ok(error instanceof errors.ValidationError, 'Setting Model should throw ValidationError');
|
|
}
|
|
}
|
|
|
|
async function testValidSetting({key, value, type, group}) {
|
|
mockDb.mock(knex);
|
|
const tracker = mockDb.getTracker();
|
|
tracker.install();
|
|
|
|
tracker.on('query', (query) => {
|
|
query.response();
|
|
});
|
|
|
|
const setting = models.Settings.forge({key, value, type, group});
|
|
|
|
let error;
|
|
try {
|
|
await setting.save();
|
|
error = null;
|
|
} catch (err) {
|
|
error = err;
|
|
} finally {
|
|
tracker.uninstall();
|
|
mockDb.unmock(knex);
|
|
should.not.exist(error, `Setting Model should not throw when saving valid ${key}`);
|
|
}
|
|
}
|
|
|
|
it('throws when stripe_secret_key is invalid', async function () {
|
|
await testInvalidSetting({
|
|
key: 'stripe_secret_key',
|
|
value: 'INVALID STRIPE SECRET KEY',
|
|
type: 'string',
|
|
group: 'members'
|
|
});
|
|
});
|
|
|
|
it('throws when stripe_publishable_key is invalid', async function () {
|
|
await testInvalidSetting({
|
|
key: 'stripe_publishable_key',
|
|
value: 'INVALID STRIPE PUBLISHABLE KEY',
|
|
type: 'string',
|
|
group: 'members'
|
|
});
|
|
});
|
|
|
|
it('does not throw when stripe_secret_key is valid', async function () {
|
|
await testValidSetting({
|
|
key: 'stripe_secret_key',
|
|
value: 'rk_live_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
type: 'string',
|
|
group: 'members'
|
|
});
|
|
await testValidSetting({
|
|
key: 'stripe_secret_key',
|
|
value: 'sk_live_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
type: 'string',
|
|
group: 'members'
|
|
});
|
|
});
|
|
|
|
it('does not throw when stripe_publishable_key is valid', async function () {
|
|
await testValidSetting({
|
|
key: 'stripe_publishable_key',
|
|
value: 'pk_live_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
type: 'string',
|
|
group: 'members'
|
|
});
|
|
});
|
|
|
|
it('throws when stripe_connect_secret_key is invalid', async function () {
|
|
await testInvalidSetting({
|
|
key: 'stripe_connect_secret_key',
|
|
value: 'INVALID STRIPE CONNECT SECRET KEY',
|
|
type: 'string',
|
|
group: 'members'
|
|
});
|
|
});
|
|
|
|
it('throws when stripe_connect_publishable_key is invalid', async function () {
|
|
await testInvalidSetting({
|
|
key: 'stripe_connect_publishable_key',
|
|
value: 'INVALID STRIPE CONNECT PUBLISHABLE KEY',
|
|
type: 'string',
|
|
group: 'members'
|
|
});
|
|
});
|
|
|
|
it('does not throw when stripe_connect_secret_key is valid', async function () {
|
|
await testValidSetting({
|
|
key: 'stripe_connect_secret_key',
|
|
value: 'sk_live_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
type: 'string',
|
|
group: 'members'
|
|
});
|
|
});
|
|
|
|
it('does not throw when stripe_connect_publishable_key is valid', async function () {
|
|
await testValidSetting({
|
|
key: 'stripe_connect_publishable_key',
|
|
value: 'pk_live_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
type: 'string',
|
|
group: 'members'
|
|
});
|
|
});
|
|
|
|
it('throws when stripe_plans has invalid name', async function () {
|
|
await testInvalidSetting({
|
|
key: 'stripe_plans',
|
|
value: JSON.stringify([{
|
|
name: null,
|
|
amount: 500,
|
|
interval: 'month',
|
|
currency: 'usd'
|
|
}]),
|
|
type: 'string',
|
|
group: 'members'
|
|
});
|
|
});
|
|
|
|
it('throws when stripe_plans has invalid amount', async function () {
|
|
await testInvalidSetting({
|
|
key: 'stripe_plans',
|
|
value: JSON.stringify([{
|
|
name: 'Monthly',
|
|
amount: 0,
|
|
interval: 'month',
|
|
currency: 'usd'
|
|
}]),
|
|
type: 'string',
|
|
group: 'members'
|
|
});
|
|
});
|
|
|
|
it('throws when stripe_plans has invalid interval', async function () {
|
|
await testInvalidSetting({
|
|
key: 'stripe_plans',
|
|
value: JSON.stringify([{
|
|
name: 'Monthly',
|
|
amount: 500,
|
|
interval: 'monthly', // should be 'month'
|
|
currency: 'usd'
|
|
}]),
|
|
type: 'string',
|
|
group: 'members'
|
|
});
|
|
});
|
|
|
|
it('throws when stripe_plans has invalid currency', async function () {
|
|
await testInvalidSetting({
|
|
key: 'stripe_plans',
|
|
value: JSON.stringify([{
|
|
name: 'Monthly',
|
|
amount: 500,
|
|
interval: 'month',
|
|
currency: null
|
|
}]),
|
|
type: 'string',
|
|
group: 'members'
|
|
});
|
|
});
|
|
|
|
it('does not throw when stripe_plans is valid', async function () {
|
|
await testValidSetting({
|
|
key: 'stripe_plans',
|
|
value: JSON.stringify([{
|
|
name: 'Monthly',
|
|
amount: 500,
|
|
interval: 'month',
|
|
currency: 'usd'
|
|
}]),
|
|
type: 'string',
|
|
group: 'members'
|
|
});
|
|
});
|
|
});
|
|
});
|