Ghost/test/unit/models/settings_spec.js
Hannah Wolfe 829e8ed010 Expanded requires of lib/common i18n and events
- Having these as destructured from the same package is hindering refactoring now
- Events should really only ever be used server-side
- i18n should be a shared module for now so it can be used everywhere until we figure out something better
- Having them seperate also allows us to lint them properly
2021-05-03 17:14:52 +01:00

442 lines
16 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/events');
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();
eventSpy = sinon.spy(events, 'emit');
});
afterEach(function () {
tracker.uninstall();
mockDb.unmock(knex);
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) => {
if (query.method === 'select') {
return query.response([{
id: 1, // NOTE: `id` imitates existing value for 'edit' event
key: 'description',
value: 'db value'
}]);
}
return query.response([{}]);
});
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('format', function () {
it('transforms urls when persisting to db', function () {
const setting = models.Settings.forge();
let returns = setting.formatOnWrite({key: 'cover_image', value: 'http://127.0.0.1:2369/cover_image.png', type: 'string'});
should.equal(returns.value, '__GHOST_URL__/cover_image.png');
returns = setting.formatOnWrite({key: 'logo', value: 'http://127.0.0.1:2369/logo.png', type: 'string'});
should.equal(returns.value, '__GHOST_URL__/logo.png');
returns = setting.formatOnWrite({key: 'icon', value: 'http://127.0.0.1:2369/icon.png', type: 'string'});
should.equal(returns.value, '__GHOST_URL__/icon.png');
returns = setting.formatOnWrite({key: 'portal_button_icon', value: 'http://127.0.0.1:2369/portal_button_icon.png', type: 'string'});
should.equal(returns.value, '__GHOST_URL__/portal_button_icon.png');
returns = setting.formatOnWrite({key: 'og_image', value: 'http://127.0.0.1:2369/og_image.png', type: 'string'});
should.equal(returns.value, '__GHOST_URL__/og_image.png');
returns = setting.formatOnWrite({key: 'twitter_image', value: 'http://127.0.0.1:2369/twitter_image.png', type: 'string'});
should.equal(returns.value, '__GHOST_URL__/twitter_image.png');
});
});
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');
returns = setting.parse({key: 'cover_image', value: '__GHOST_URL__/cover_image.png', type: 'string'});
should.equal(returns.value, 'http://127.0.0.1:2369/cover_image.png');
returns = setting.parse({key: 'logo', value: '__GHOST_URL__/logo.png', type: 'string'});
should.equal(returns.value, 'http://127.0.0.1:2369/logo.png');
returns = setting.parse({key: 'icon', value: '__GHOST_URL__/icon.png', type: 'string'});
should.equal(returns.value, 'http://127.0.0.1:2369/icon.png');
returns = setting.parse({key: 'portal_button_icon', value: '__GHOST_URL__/portal_button_icon.png', type: 'string'});
should.equal(returns.value, 'http://127.0.0.1:2369/portal_button_icon.png');
returns = setting.parse({key: 'og_image', value: '__GHOST_URL__/og_image.png', type: 'string'});
should.equal(returns.value, 'http://127.0.0.1:2369/og_image.png');
returns = setting.parse({key: 'twitter_image', value: '__GHOST_URL__/twitter_image.png', type: 'string'});
should.equal(returns.value, 'http://127.0.0.1:2369/twitter_image.png');
});
});
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'
});
});
});
});