2019-03-06 14:56:26 +03:00
|
|
|
const should = require('should');
|
2019-03-26 06:37:32 +03:00
|
|
|
const sinon = require('sinon');
|
|
|
|
const mockDb = require('mock-knex');
|
2020-03-30 18:26:47 +03:00
|
|
|
const models = require('../../../core/server/models');
|
|
|
|
const {knex} = require('../../../core/server/data/db');
|
2020-05-28 19:54:18 +03:00
|
|
|
const {events} = require('../../../core/server/lib/common');
|
2020-05-28 13:40:49 +03:00
|
|
|
const defaultSettings = require('../../../core/server/data/schema/default-settings');
|
2020-07-15 18:11:27 +03:00
|
|
|
const errors = require('@tryghost/errors');
|
2019-03-06 14:56:26 +03:00
|
|
|
|
|
|
|
describe('Unit: models/settings', function () {
|
|
|
|
before(function () {
|
|
|
|
models.init();
|
|
|
|
});
|
|
|
|
|
2019-03-26 06:37:32 +03:00
|
|
|
describe('events', function () {
|
|
|
|
let tracker;
|
|
|
|
let eventSpy;
|
|
|
|
|
|
|
|
beforeEach(function () {
|
|
|
|
mockDb.mock(knex);
|
|
|
|
tracker = mockDb.getTracker();
|
|
|
|
tracker.install();
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(function () {
|
|
|
|
mockDb.unmock(knex);
|
|
|
|
});
|
|
|
|
|
|
|
|
beforeEach(function () {
|
2020-05-28 19:54:18 +03:00
|
|
|
eventSpy = sinon.spy(events, 'emit');
|
2019-03-26 06:37:32 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
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]();
|
|
|
|
});
|
|
|
|
|
2020-06-30 15:04:56 +03:00
|
|
|
return models.Settings.add({
|
2019-08-19 14:41:09 +03:00
|
|
|
key: 'description',
|
2020-06-30 15:04:56 +03:00
|
|
|
value: 'added value',
|
|
|
|
type: 'string'
|
2019-08-19 14:41:09 +03:00
|
|
|
})
|
2019-03-26 06:37:32 +03:00
|
|
|
.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([{
|
2019-08-19 14:41:09 +03:00
|
|
|
id: 1, // NOTE: `id` imitates existing value for 'edit' event
|
2019-03-26 06:37:32 +03:00
|
|
|
key: 'description',
|
|
|
|
value: 'db value'
|
|
|
|
}]);
|
|
|
|
}
|
|
|
|
][step - 1]();
|
|
|
|
});
|
|
|
|
|
|
|
|
return models.Settings.edit({
|
2019-08-19 14:41:09 +03:00
|
|
|
key: 'description',
|
|
|
|
value: 'edited value'
|
|
|
|
})
|
2019-03-26 06:37:32 +03:00
|
|
|
.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 () {
|
2020-05-28 19:54:18 +03:00
|
|
|
eventSpy = sinon.spy(events, 'emit');
|
2019-03-26 06:37:32 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(function () {
|
|
|
|
sinon.restore();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('populates unset defaults', function () {
|
2020-06-25 16:22:15 +03:00
|
|
|
let insertQueries = [];
|
|
|
|
|
2019-03-26 06:37:32 +03:00
|
|
|
tracker.on('query', (query) => {
|
2020-06-25 16:22:15 +03:00
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
2019-03-26 06:37:32 +03:00
|
|
|
return query.response([{}]);
|
|
|
|
});
|
|
|
|
|
|
|
|
return models.Settings.populateDefaults()
|
|
|
|
.then(() => {
|
2020-05-28 13:40:49 +03:00
|
|
|
const numberOfSettings = Object.keys(defaultSettings).reduce((settings, settingGroup) => {
|
|
|
|
return settings.concat(Object.keys(defaultSettings[settingGroup]));
|
|
|
|
}, []).length;
|
|
|
|
|
2020-06-25 16:22:15 +03:00
|
|
|
insertQueries.length.should.equal(numberOfSettings);
|
2019-03-26 06:37:32 +03:00
|
|
|
|
2020-06-25 16:22:15 +03:00
|
|
|
// non-existent columns should not be populated
|
|
|
|
insertQueries[0].sql.should.not.match(/group/);
|
|
|
|
insertQueries[0].sql.should.not.match(/flags/);
|
2019-03-26 06:37:32 +03:00
|
|
|
|
2020-06-25 16:22:15 +03:00
|
|
|
// no events are emitted because we're not using the model layer
|
|
|
|
eventSpy.callCount.should.equal(0);
|
2019-03-26 06:37:32 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('doesn\'t overwrite any existing settings', function () {
|
2020-06-25 16:22:15 +03:00
|
|
|
let insertQueries = [];
|
|
|
|
|
2019-03-26 06:37:32 +03:00
|
|
|
tracker.on('query', (query) => {
|
2020-06-25 16:22:15 +03:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2019-03-26 06:37:32 +03:00
|
|
|
return query.response([{
|
|
|
|
key: 'description',
|
|
|
|
value: 'Adam\'s Blog'
|
|
|
|
}]);
|
|
|
|
});
|
|
|
|
|
|
|
|
return models.Settings.populateDefaults()
|
|
|
|
.then(() => {
|
2020-06-25 16:22:15 +03:00
|
|
|
const numberOfSettings = Object.keys(defaultSettings).reduce((settings, settingGroup) => {
|
|
|
|
return settings.concat(Object.keys(defaultSettings[settingGroup]));
|
|
|
|
}, []).length;
|
|
|
|
|
|
|
|
insertQueries.length.should.equal(numberOfSettings - 1);
|
2019-03-26 06:37:32 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-03-06 14:56:26 +03:00
|
|
|
describe('parse', function () {
|
|
|
|
it('ensure correct parsing when fetching from db', function () {
|
|
|
|
const setting = models.Settings.forge();
|
|
|
|
|
2020-06-30 15:04:56 +03:00
|
|
|
let returns = setting.parse({key: 'is_private', value: 'false', type: 'boolean'});
|
2019-03-06 14:56:26 +03:00
|
|
|
should.equal(returns.value, false);
|
|
|
|
|
2020-06-30 15:04:56 +03:00
|
|
|
returns = setting.parse({key: 'is_private', value: false, type: 'boolean'});
|
2019-03-06 14:56:26 +03:00
|
|
|
should.equal(returns.value, false);
|
|
|
|
|
2020-06-30 15:04:56 +03:00
|
|
|
returns = setting.parse({key: 'is_private', value: true, type: 'boolean'});
|
2019-03-06 14:56:26 +03:00
|
|
|
should.equal(returns.value, true);
|
|
|
|
|
2020-06-30 15:04:56 +03:00
|
|
|
returns = setting.parse({key: 'is_private', value: 'true', type: 'boolean'});
|
2019-03-06 14:56:26 +03:00
|
|
|
should.equal(returns.value, true);
|
|
|
|
|
2020-06-30 15:04:56 +03:00
|
|
|
returns = setting.parse({key: 'is_private', value: '0', type: 'boolean'});
|
2019-03-06 14:56:26 +03:00
|
|
|
should.equal(returns.value, false);
|
|
|
|
|
2020-06-30 15:04:56 +03:00
|
|
|
returns = setting.parse({key: 'is_private', value: '1', type: 'boolean'});
|
2019-03-06 14:56:26 +03:00
|
|
|
should.equal(returns.value, true);
|
|
|
|
|
|
|
|
returns = setting.parse({key: 'something', value: 'null'});
|
2019-03-11 19:25:45 +03:00
|
|
|
should.equal(returns.value, 'null');
|
2019-03-06 14:56:26 +03:00
|
|
|
});
|
|
|
|
});
|
2020-07-15 18:11:27 +03:00
|
|
|
|
|
|
|
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'
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2019-03-06 14:56:26 +03:00
|
|
|
});
|