diff --git a/core/server/api/settings.js b/core/server/api/settings.js index b316a74cb8..b0c2b60366 100644 --- a/core/server/api/settings.js +++ b/core/server/api/settings.js @@ -3,12 +3,10 @@ var _ = require('lodash'), dataProvider = require('../models'), Promise = require('bluebird'), - config = require('../config'), canThis = require('../permissions').canThis, errors = require('../errors'), utils = require('./utils'), i18n = require('../i18n'), - filterPackages = require('../utils/packages').filterPackages, docName = 'settings', settings, @@ -68,6 +66,17 @@ settingsFilter = function (settings, filter) { /** * ### Read Settings Result + * + * Converts the models to keyed JSON + * E.g. + * dbHash: { + * id: '123abc', + * key: 'dbash', + * value: 'xxxx', + * type: 'core', + * timestamps + * } + * * @private * @param {Array} settingsModels * @returns {Settings} @@ -79,20 +88,7 @@ readSettingsResult = function (settingsModels) { } return memo; - }, {}), - themes = config.get('paths').availableThemes, - res; - - // @TODO: remove availableThemes from settings cache and create an endpoint to fetch themes - if (settings.activeTheme && themes) { - res = filterPackages(themes, settings.activeTheme.value); - - settings.availableThemes = { - key: 'availableThemes', - value: res, - type: 'theme' - }; - } + }, {}); return settings; }; @@ -255,7 +251,7 @@ settings = { } object.settings = _.reject(object.settings, function (setting) { - return setting.key === 'type' || setting.key === 'availableThemes'; + return setting.key === 'type'; }); return canEditAllSettings(object.settings, options).then(function () { diff --git a/core/server/api/themes.js b/core/server/api/themes.js index 0efffb1fe0..afe109643e 100644 --- a/core/server/api/themes.js +++ b/core/server/api/themes.js @@ -1,6 +1,7 @@ // # Themes API // RESTful API for Themes -var Promise = require('bluebird'), +var debug = require('debug')('ghost:api:themes'), + Promise = require('bluebird'), _ = require('lodash'), gscan = require('gscan'), fs = require('fs-extra'), @@ -9,12 +10,12 @@ var Promise = require('bluebird'), events = require('../events'), logging = require('../logging'), storage = require('../storage'), - settings = require('./settings'), - settingsCache = require('../settings/cache'), apiUtils = require('./utils'), utils = require('./../utils'), i18n = require('../i18n'), themeUtils = require('../themes'), + themeList = themeUtils.list, + packageUtils = require('../utils/packages'), themes; /** @@ -24,7 +25,10 @@ var Promise = require('bluebird'), */ themes = { browse: function browse() { - return Promise.resolve({themes: settingsCache.get('availableThemes')}); + debug('browsing'); + var result = packageUtils.filterPackages(themeList.getAll()); + debug('got result'); + return Promise.resolve({themes: result}); }, upload: function upload(options) { @@ -80,23 +84,18 @@ themes = { }, config.getContentPath('themes')); }) .then(function () { - // force reload of availableThemes - // right now the logic is in the ConfigManager - // if we create a theme collection, we don't have to read them from disk - return themeUtils.load(); + return themeUtils.loadOne(zip.shortName); }) - .then(function () { - // the settings endpoint is used to fetch the availableThemes - // so we have to force updating the in process cache - return settings.updateSettingsCache(); - }) - .then(function (settings) { + .then(function (themeObject) { + // @TODO fix this craziness + var toFilter = {}; + toFilter[zip.shortName] = themeObject; + themeObject = packageUtils.filterPackages(toFilter); // gscan theme structure !== ghost theme structure - var themeObject = _.find(settings.availableThemes.value, {name: zip.shortName}) || {}; if (theme.results.warning.length > 0) { themeObject.warnings = _.cloneDeep(theme.results.warning); } - return {themes: [themeObject]}; + return {themes: themeObject}; }) .finally(function () { // remove zip upload from multer @@ -119,7 +118,7 @@ themes = { download: function download(options) { var themeName = options.name, - theme = config.get('paths').availableThemes[themeName], + theme = themeList.get(themeName), storageAdapter = storage.getStorage('themes'); if (!theme) { @@ -148,20 +147,17 @@ themes = { throw new errors.ValidationError({message: i18n.t('errors.api.themes.destroyCasper')}); } - theme = config.get('paths').availableThemes[name]; + theme = themeList.get(name); if (!theme) { throw new errors.NotFoundError({message: i18n.t('errors.api.themes.themeDoesNotExist')}); } - events.emit('theme.deleted', name); return storageAdapter.delete(name, config.getContentPath('themes')); }) .then(function () { - return themeUtils.load(); - }) - .then(function () { - return settings.updateSettingsCache(); + themeList.del(name); + events.emit('theme.deleted', name); }); } }; diff --git a/core/server/apps/amp/tests/router_spec.js b/core/server/apps/amp/tests/router_spec.js index affdf1938b..b0ebb88dc7 100644 --- a/core/server/apps/amp/tests/router_spec.js +++ b/core/server/apps/amp/tests/router_spec.js @@ -7,6 +7,7 @@ var rewire = require('rewire'), errors = require('../../../errors'), should = require('should'), configUtils = require('../../../../test/utils/configUtils'), + themeList = require('../../../themes').list, sandbox = sinon.sandbox.create(); // Helper function to prevent unit tests @@ -53,10 +54,11 @@ describe('AMP Controller', function () { afterEach(function () { sandbox.restore(); configUtils.restore(); + themeList.init(); }); it('should render default amp page when theme has no amp template', function (done) { - configUtils.set({paths: {availableThemes: {casper: {}}}}); + themeList.init({casper: {}}); setResponseContextStub = sandbox.stub(); ampController.__set__('setResponseContext', setResponseContextStub); @@ -70,9 +72,9 @@ describe('AMP Controller', function () { }); it('should render theme amp page when theme has amp template', function (done) { - configUtils.set({paths: {availableThemes: {casper: { + themeList.init({casper: { 'amp.hbs': '/content/themes/casper/amp.hbs' - }}}}); + }}); setResponseContextStub = sandbox.stub(); ampController.__set__('setResponseContext', setResponseContextStub); @@ -86,7 +88,7 @@ describe('AMP Controller', function () { }); it('should render with error when error is passed in', function (done) { - configUtils.set({paths: {availableThemes: {casper: {}}}}); + themeList.init({casper: {}}); res.error = 'Test Error'; setResponseContextStub = sandbox.stub(); @@ -103,7 +105,7 @@ describe('AMP Controller', function () { it('does not render amp page when amp context is missing', function (done) { var renderSpy; - configUtils.set({paths: {availableThemes: {casper: {}}}}); + themeList.init({casper: {}}); setResponseContextStub = sandbox.stub(); ampController.__set__('setResponseContext', setResponseContextStub); @@ -121,7 +123,7 @@ describe('AMP Controller', function () { it('does not render amp page when context is other than amp and post', function (done) { var renderSpy; - configUtils.set({paths: {availableThemes: {casper: {}}}}); + themeList.init({casper: {}}); setResponseContextStub = sandbox.stub(); ampController.__set__('setResponseContext', setResponseContextStub); diff --git a/core/server/apps/private-blogging/tests/controller_spec.js b/core/server/apps/private-blogging/tests/controller_spec.js index ab159cb7bc..bb474f8cae 100644 --- a/core/server/apps/private-blogging/tests/controller_spec.js +++ b/core/server/apps/private-blogging/tests/controller_spec.js @@ -3,6 +3,7 @@ var privateController = require('../lib/router').controller, path = require('path'), sinon = require('sinon'), configUtils = require('../../../../test/utils/configUtils'), + themeList = require('../../../themes').list, sandbox = sinon.sandbox.create(); describe('Private Controller', function () { @@ -42,10 +43,11 @@ describe('Private Controller', function () { afterEach(function () { sandbox.restore(); configUtils.restore(); + themeList.init(); }); it('Should render default password page when theme has no password template', function (done) { - configUtils.set({paths: {availableThemes: {casper: {}}}}); + themeList.init({casper: {}}); res.render = function (view) { view.should.eql(defaultPath); @@ -56,9 +58,9 @@ describe('Private Controller', function () { }); it('Should render theme password page when it exists', function (done) { - configUtils.set({paths: {availableThemes: {casper: { + themeList.init({casper: { 'private.hbs': '/content/themes/casper/private.hbs' - }}}}); + }}); res.render = function (view) { view.should.eql('private'); @@ -69,7 +71,7 @@ describe('Private Controller', function () { }); it('Should render with error when error is passed in', function (done) { - configUtils.set({paths: {availableThemes: {casper: {}}}}); + themeList.init({casper: {}}); res.error = 'Test Error'; res.render = function (view, context) { diff --git a/core/server/controllers/frontend/templates.js b/core/server/controllers/frontend/templates.js index 33054fc507..d614f795c9 100644 --- a/core/server/controllers/frontend/templates.js +++ b/core/server/controllers/frontend/templates.js @@ -2,11 +2,11 @@ // // Figure out which template should be used to render a request // based on the templates which are allowed, and what is available in the theme -var _ = require('lodash'), - config = require('../../config'); +var _ = require('lodash'), + themeList = require('../../themes').list; function getActiveThemePaths(activeTheme) { - return config.get('paths').availableThemes[activeTheme]; + return themeList.get(activeTheme); } /** diff --git a/core/server/middleware/theme-handler.js b/core/server/middleware/theme-handler.js index 2426594dcb..6b6c6eddfb 100644 --- a/core/server/middleware/theme-handler.js +++ b/core/server/middleware/theme-handler.js @@ -9,6 +9,7 @@ var _ = require('lodash'), logging = require('../logging'), errors = require('../errors'), i18n = require('../i18n'), + themeList = require('../themes').list, themeHandler; themeHandler = { @@ -95,7 +96,7 @@ themeHandler = { // Check if the theme changed if (activeTheme.value !== blogApp.get('activeTheme')) { // Change theme - if (!config.get('paths').availableThemes.hasOwnProperty(activeTheme.value)) { + if (!themeList.get(activeTheme.value)) { if (!res.isAdmin) { return next(new errors.NotFoundError({ message: i18n.t('errors.middleware.themehandler.missingTheme', {theme: activeTheme.value}) diff --git a/core/server/themes/index.js b/core/server/themes/index.js index 1faa33fd26..3a68776a68 100644 --- a/core/server/themes/index.js +++ b/core/server/themes/index.js @@ -1,7 +1,11 @@ var themeLoader = require('./loader'); +// @TODO: reduce the amount of things we expose to the outside world +// Make this a nice clean sensible API we can all understand! module.exports = { init: themeLoader.init, - load: themeLoader.load, + loadAll: themeLoader.loadAllThemes, + loadOne: themeLoader.loadOneTheme, + list: require('./list'), validate: require('./validate') }; diff --git a/core/server/themes/list.js b/core/server/themes/list.js new file mode 100644 index 0000000000..287cdea27c --- /dev/null +++ b/core/server/themes/list.js @@ -0,0 +1,36 @@ +/** + * Store themes after loading them from the file system + */ +var _ = require('lodash'), + themeListCache = {}; + +module.exports = { + get: function get(key) { + return themeListCache[key]; + }, + + getAll: function getAll() { + return themeListCache; + }, + + set: function set(key, theme) { + themeListCache[key] = _.cloneDeep(theme); + return themeListCache[key]; + }, + + del: function del(key) { + delete themeListCache[key]; + }, + + init: function init(themes) { + var self = this; + // First, reset the cache + themeListCache = {}; + // For each theme, call set. Allows us to do processing on set later. + _.each(themes, function (theme, key) { + self.set(key, theme); + }); + + return themeListCache; + } +}; diff --git a/core/server/themes/loader.js b/core/server/themes/loader.js index 029feb9430..e36cadce11 100644 --- a/core/server/themes/loader.js +++ b/core/server/themes/loader.js @@ -1,23 +1,32 @@ var debug = require('debug')('ghost:themes:loader'), config = require('../config'), events = require('../events'), + themeList = require('./list'), read = require('./read'), - settingsApi = require('../api/settings'), settingsCache = require('../settings/cache'), - updateConfigAndCache, - loadThemes, + updateThemeList, + loadAllThemes, + loadOneTheme, initThemes; -updateConfigAndCache = function updateConfigAndCache(themes) { +updateThemeList = function updateThemeList(themes) { debug('loading themes', Object.keys(themes)); - config.set('paths:availableThemes', themes); - settingsApi.updateSettingsCache(); + themeList.init(themes); }; -loadThemes = function loadThemes() { +loadAllThemes = function loadAllThemes() { return read .all(config.getContentPath('themes')) - .then(updateConfigAndCache); + .then(updateThemeList); +}; + +loadOneTheme = function loadOneTheme(themeName) { + return read + .one(config.getContentPath('themes'), themeName) + .then(function (readThemes) { + // @TODO change read one to not return a keyed object + return themeList.set(themeName, readThemes[themeName]); + }); }; initThemes = function initThemes() { @@ -25,16 +34,17 @@ initThemes = function initThemes() { // Register a listener for server-start to load all themes events.on('server:start', function readAllThemesOnServerStart() { - loadThemes(); + loadAllThemes(); }); // Just read the active theme for now return read .one(config.getContentPath('themes'), settingsCache.get('activeTheme')) - .then(updateConfigAndCache); + .then(updateThemeList); }; module.exports = { init: initThemes, - load: loadThemes + loadAllThemes: loadAllThemes, + loadOneTheme: loadOneTheme }; diff --git a/core/server/themes/validate.js b/core/server/themes/validate.js index 9ba6394c09..5c6886c483 100644 --- a/core/server/themes/validate.js +++ b/core/server/themes/validate.js @@ -1,19 +1,19 @@ var Promise = require('bluebird'), - config = require('../config'), errors = require('../errors'), i18n = require('../i18n'), + themeList = require('./list'), validateActiveTheme; // @TODO replace this with something PROPER - we should probably attempt to read the theme from the // File system at this point and validate the theme using gscan rather than just checking if it's in a cache object validateActiveTheme = function validateActiveTheme(themeName) { - if (!config.get('paths').availableThemes || Object.keys(config.get('paths').availableThemes).length === 0) { + if (!themeList.getAll() || Object.keys(themeList.getAll()).length === 0) { // We haven't yet loaded all themes, this is probably being called early? return Promise.resolve(); } // Else, if we have a list, check if the theme is in it - if (!config.get('paths').availableThemes.hasOwnProperty(themeName)) { + if (!themeList.get(themeName)) { return Promise.reject(new errors.ValidationError({message: i18n.t('notices.data.validation.index.themeCannotBeActivated', {themeName: themeName}), context: 'activeTheme'})); } }; diff --git a/core/test/functional/routes/api/themes_spec.js b/core/test/functional/routes/api/themes_spec.js index 8061e82daf..71a89267a0 100644 --- a/core/test/functional/routes/api/themes_spec.js +++ b/core/test/functional/routes/api/themes_spec.js @@ -65,17 +65,18 @@ describe('Themes API', function () { }); describe('success cases', function () { - it('get all available themes', function (done) { - request.get(testUtils.API.getApiQuery('settings/')) + it('get all themes', function (done) { + request.get(testUtils.API.getApiQuery('themes/')) .set('Authorization', 'Bearer ' + scope.ownerAccessToken) .end(function (err, res) { if (err) { return done(err); } - var availableThemes = _.find(res.body.settings, {key: 'availableThemes'}); - should.exist(availableThemes); - availableThemes.value.length.should.be.above(0); + var jsonResponse = res.body; + should.exist(jsonResponse.themes); + testUtils.API.checkResponse(jsonResponse, 'themes'); + jsonResponse.themes.length.should.be.above(0); done(); }); }); @@ -108,19 +109,21 @@ describe('Themes API', function () { }); }); - it('get all available themes + new theme', function (done) { - request.get(testUtils.API.getApiQuery('settings/')) + it('get all themes + new theme', function (done) { + request.get(testUtils.API.getApiQuery('themes/')) .set('Authorization', 'Bearer ' + scope.ownerAccessToken) .end(function (err, res) { if (err) { return done(err); } - var availableThemes = _.find(res.body.settings, {key: 'availableThemes'}); - should.exist(availableThemes); + var jsonResponse = res.body; + should.exist(jsonResponse.themes); + testUtils.API.checkResponse(jsonResponse, 'themes'); + jsonResponse.themes.length.should.be.above(0); // ensure the new 'valid' theme is available - should.exist(_.find(availableThemes.value, {name: 'valid'})); + should.exist(_.find(jsonResponse.themes, {name: 'valid'})); done(); }); }); diff --git a/core/test/unit/controllers/frontend/channels_spec.js b/core/test/unit/controllers/frontend/channels_spec.js index 868fe6a95e..fd8df9b2b4 100644 --- a/core/test/unit/controllers/frontend/channels_spec.js +++ b/core/test/unit/controllers/frontend/channels_spec.js @@ -6,7 +6,7 @@ var should = require('should'), // Stuff we are testing channels = require('../../../../server/controllers/frontend/channels'), api = require('../../../../server/api'), - configUtils = require('../../../utils/configUtils'), + themeList = require('../../../../server/themes').list, sandbox = sinon.sandbox.create(); describe('Channels', function () { @@ -104,7 +104,7 @@ describe('Channels', function () { }); afterEach(function () { - configUtils.restore(); + themeList.init(); sandbox.restore(); }); @@ -120,9 +120,9 @@ describe('Channels', function () { // Return basic paths for the activeTheme function setupActiveTheme() { - configUtils.set('paths', {availableThemes: {casper: { + themeList.init({casper: { 'index.hbs': '/content/themes/casper/index.hbs' - }}}); + }}); } beforeEach(function () { @@ -141,10 +141,10 @@ describe('Channels', function () { }); it('should render the first page of the index channel using home.hbs if available', function (done) { - configUtils.set({paths: {availableThemes: {casper: { + themeList.init({casper: { 'index.hbs': '/content/themes/casper/index.hbs', 'home.hbs': '/content/themes/casper/home.hbs' - }}}}); + }}); testChannelRender({url: '/'}, function (view) { should.exist(view); @@ -163,10 +163,10 @@ describe('Channels', function () { }); it('should use index.hbs for second page even if home.hbs is available', function (done) { - configUtils.set({paths: {availableThemes: {casper: { + themeList.init({casper: { 'index.hbs': '/content/themes/casper/index.hbs', 'home.hbs': '/content/themes/casper/home.hbs' - }}}}); + }}); testChannelRender({url: '/page/2/'}, function (view) { should.exist(view); @@ -255,9 +255,9 @@ describe('Channels', function () { // Return basic paths for the activeTheme function setupActiveTheme() { - configUtils.set('paths', {availableThemes: {casper: { + themeList.init({casper: { 'index.hbs': '/content/themes/casper/index.hbs' - }}}); + }}); } beforeEach(function () { @@ -277,10 +277,10 @@ describe('Channels', function () { }); it('should render the first page of the tag channel using tag.hbs by default', function (done) { - configUtils.set('paths',{availableThemes: {casper: { + themeList.init({casper: { 'index.hbs': '/content/themes/casper/index.hbs', 'tag.hbs': '/content/themes/casper/tag.hbs' - }}}); + }}); testChannelRender({url: '/tag/my-tag/'}, function (view) { should.exist(view); @@ -291,11 +291,11 @@ describe('Channels', function () { }); it('should render the first page of the tag channel using tag-:slug.hbs if available', function (done) { - configUtils.set('paths', {availableThemes: {casper: { + themeList.init({casper: { 'index.hbs': '/content/themes/casper/index.hbs', 'tag.hbs': '/content/themes/casper/tag.hbs', 'tag-my-tag.hbs': '/content/themes/casper/tag-my-tag.hbs' - }}}); + }}); testChannelRender({url: '/tag/my-tag/'}, function (view) { should.exist(view); @@ -315,10 +315,10 @@ describe('Channels', function () { }); it('should use tag.hbs to render the tag channel if available', function (done) { - configUtils.set('paths', {availableThemes: {casper: { + themeList.init({casper: { 'index.hbs': '/content/themes/casper/index.hbs', 'tag.hbs': '/content/themes/casper/tag.hbs' - }}}); + }}); testChannelRender({url: '/tag/my-tag/page/2/'}, function (view) { should.exist(view); @@ -328,11 +328,11 @@ describe('Channels', function () { }); it('should use tag-:slug.hbs to render the tag channel if available', function (done) { - configUtils.set('paths', {availableThemes: {casper: { + themeList.init({casper: { 'index.hbs': '/content/themes/casper/index.hbs', 'tag.hbs': '/content/themes/casper/tag.hbs', 'tag-my-tag.hbs': '/content/themes/casper/tag-my-tag.hbs' - }}}); + }}); testChannelRender({url: '/tag/my-tag/page/2/'}, function (view) { should.exist(view); diff --git a/core/test/unit/controllers/frontend/index_spec.js b/core/test/unit/controllers/frontend/index_spec.js index cc1546c91c..d909455dee 100644 --- a/core/test/unit/controllers/frontend/index_spec.js +++ b/core/test/unit/controllers/frontend/index_spec.js @@ -6,6 +6,7 @@ var moment = require('moment'), api = require('../../../../server/api'), frontend = require('../../../../server/controllers/frontend'), configUtils = require('../../../utils/configUtils'), + themeList = require('../../../../server/themes').list, settingsCache = require('../../../../server/settings/cache'), sandbox = sinon.sandbox.create(); @@ -121,7 +122,7 @@ describe('Frontend Controller', function () { describe('static pages', function () { describe('custom page templates', function () { it('it will render a custom page-slug template if it exists', function (done) { - configUtils.set({paths: {availableThemes: {casper: casper}}}); + themeList.init({casper: casper}); req.path = '/' + mockPosts[2].posts[0].slug + '/'; req.route = {path: '*'}; res.render = function (view, context) { @@ -136,7 +137,7 @@ describe('Frontend Controller', function () { it('it will use page.hbs if it exists and no page-slug template is present', function (done) { delete casper['page-about.hbs']; - configUtils.set({paths: {availableThemes: {casper: casper}}}); + themeList.init({casper: casper}); req.path = '/' + mockPosts[2].posts[0].slug + '/'; req.route = {path: '*'}; res.render = function (view, context) { @@ -152,7 +153,7 @@ describe('Frontend Controller', function () { it('defaults to post.hbs without a page.hbs or page-slug template', function (done) { delete casper['page-about.hbs']; delete casper['page.hbs']; - configUtils.set({paths: {availableThemes: {casper: casper}}}); + themeList.init({casper: casper}); req.path = '/' + mockPosts[2].posts[0].slug + '/'; req.route = {path: '*'}; res.render = function (view, context) { @@ -168,7 +169,7 @@ describe('Frontend Controller', function () { describe('permalink set to slug', function () { it('will render static page via /:slug/', function (done) { - configUtils.set({paths: {availableThemes: {casper: casper}}}); + themeList.init({casper: casper}); req.path = '/' + mockPosts[0].posts[0].slug + '/'; req.route = {path: '*'}; @@ -237,7 +238,7 @@ describe('Frontend Controller', function () { }); it('will render static page via /:slug', function (done) { - configUtils.set({paths: {availableThemes: {casper: casper}}}); + themeList.init({casper: casper}); req.path = '/' + mockPosts[0].posts[0].slug + '/'; req.route = {path: '*'}; @@ -293,7 +294,7 @@ describe('Frontend Controller', function () { }); it('will render post via /:slug/', function (done) { - configUtils.set({paths: {availableThemes: {casper: casper}}}); + themeList.init({casper: casper}); req.path = '/' + mockPosts[1].posts[0].slug + '/'; req.route = {path: '*'}; @@ -382,7 +383,7 @@ describe('Frontend Controller', function () { }); it('will render post via /YYYY/MM/DD/:slug/', function (done) { - configUtils.set({paths: {availableThemes: {casper: casper}}}); + themeList.init({casper: casper}); var date = moment(mockPosts[1].posts[0].published_at).format('YYYY/MM/DD'); req.path = '/' + [date, mockPosts[1].posts[0].slug].join('/') + '/'; req.route = {path: '*'}; @@ -459,7 +460,7 @@ describe('Frontend Controller', function () { }); it('will render post via /:author/:slug/', function (done) { - configUtils.set({paths: {availableThemes: {casper: casper}}}); + themeList.init({casper: casper}); req.path = '/' + ['test', mockPosts[1].posts[0].slug].join('/') + '/'; req.route = {path: '*'}; @@ -540,7 +541,7 @@ describe('Frontend Controller', function () { beforeEach(function () { localSettingsCache.permalinks = '/:year/:slug/'; - configUtils.set({paths: {availableThemes: {casper: casper}}}); + themeList.init({casper: casper}); var date = moment(mockPosts[1].posts[0].published_at).format('YYYY'); mockPosts[1].posts[0].url = '/' + date + '/' + mockPosts[1].posts[0].slug + '/'; @@ -752,7 +753,7 @@ describe('Frontend Controller', function () { redirect: sinon.spy() }; - configUtils.set({paths: {availableThemes: {casper: {}}}}); + themeList.init({casper: {}}); }); it('should render draft post', function (done) { @@ -768,7 +769,7 @@ describe('Frontend Controller', function () { }); it('should render draft page', function (done) { - configUtils.set({paths: {availableThemes: {casper: {'page.hbs': '/content/themes/casper/page.hbs'}}}}); + themeList.init({casper: {'page.hbs': '/content/themes/casper/page.hbs'}}); req.params = {uuid: 'abc-1234-01'}; res.render = function (view, context) { view.should.equal('page'); diff --git a/core/test/unit/controllers/frontend/templates_spec.js b/core/test/unit/controllers/frontend/templates_spec.js index edde205c75..d207cd8133 100644 --- a/core/test/unit/controllers/frontend/templates_spec.js +++ b/core/test/unit/controllers/frontend/templates_spec.js @@ -4,11 +4,11 @@ var should = require('should'), // Stuff we are testing templates = rewire('../../../../server/controllers/frontend/templates'), - configUtils = require('../../../utils/configUtils'); + themeList = require('../../../../server/themes').list; describe('templates', function () { afterEach(function () { - configUtils.restore(); + themeList.init(); }); describe('utils', function () { @@ -48,21 +48,16 @@ describe('templates', function () { describe('single', function () { describe('with many templates', function () { beforeEach(function () { - configUtils.set({ - paths: { - availableThemes: { - casper: { - assets: null, - 'default.hbs': '/content/themes/casper/default.hbs', - 'index.hbs': '/content/themes/casper/index.hbs', - 'page.hbs': '/content/themes/casper/page.hbs', - 'page-about.hbs': '/content/themes/casper/page-about.hbs', - 'post.hbs': '/content/themes/casper/post.hbs', - 'post-welcome-to-ghost.hbs': '/content/themes/casper/post-welcome-to-ghost.hbs' - } - } - } - }); + themeList.init({casper: { + assets: null, + 'default.hbs': '/content/themes/casper/default.hbs', + 'index.hbs': '/content/themes/casper/index.hbs', + 'page.hbs': '/content/themes/casper/page.hbs', + 'page-about.hbs': '/content/themes/casper/page-about.hbs', + 'post.hbs': '/content/themes/casper/post.hbs', + 'post-welcome-to-ghost.hbs': '/content/themes/casper/post-welcome-to-ghost.hbs' + + }}); }); it('will return correct template for a post WITHOUT custom template', function () { @@ -103,10 +98,10 @@ describe('templates', function () { }); it('will fall back to post even if no index.hbs', function () { - configUtils.set({paths: {availableThemes: {casper: { + themeList.init({casper: { assets: null, 'default.hbs': '/content/themes/casper/default.hbs' - }}}}); + }}); var view = templates.single('casper', {page: 1}); should.exist(view); @@ -117,11 +112,11 @@ describe('templates', function () { describe('channel', function () { describe('without tag templates', function () { beforeEach(function () { - configUtils.set({paths: {availableThemes: {casper: { + themeList.init({casper: { assets: null, 'default.hbs': '/content/themes/casper/default.hbs', 'index.hbs': '/content/themes/casper/index.hbs' - }}}}); + }}); }); it('will return correct view for a tag', function () { @@ -133,13 +128,13 @@ describe('templates', function () { describe('with tag templates', function () { beforeEach(function () { - configUtils.set({paths: {availableThemes: {casper: { + themeList.init({casper: { assets: null, 'default.hbs': '/content/themes/casper/default.hbs', 'index.hbs': '/content/themes/casper/index.hbs', 'tag.hbs': '/content/themes/casper/tag.hbs', 'tag-design.hbs': '/content/themes/casper/tag-about.hbs' - }}}}); + }}); }); it('will return correct view for a tag', function () { @@ -156,10 +151,10 @@ describe('templates', function () { }); it('will fall back to index even if no index.hbs', function () { - configUtils.set({paths: {availableThemes: {casper: { + themeList.init({casper: { assets: null, 'default.hbs': '/content/themes/casper/default.hbs' - }}}}); + }}); var view = templates.channel('casper', {name: 'tag', slugParam: 'development', slugTemplate: true}); should.exist(view); diff --git a/core/test/unit/middleware/theme-handler_spec.js b/core/test/unit/middleware/theme-handler_spec.js index b9a40175b8..b1fa4ba547 100644 --- a/core/test/unit/middleware/theme-handler_spec.js +++ b/core/test/unit/middleware/theme-handler_spec.js @@ -4,10 +4,10 @@ var sinon = require('sinon'), Promise = require('bluebird'), fs = require('fs'), hbs = require('express-hbs'), + themeList = require('../../../server/themes').list, themeHandler = require('../../../server/middleware/theme-handler'), logging = require('../../../server/logging'), api = require('../../../server/api'), - configUtils = require('../../utils/configUtils'), sandbox = sinon.sandbox.create(); describe('Theme Handler', function () { @@ -23,7 +23,7 @@ describe('Theme Handler', function () { afterEach(function () { sandbox.restore(); - configUtils.restore(); + themeList.init(); }); describe('activateTheme', function () { @@ -89,6 +89,10 @@ describe('Theme Handler', function () { }); describe('updateActiveTheme', function () { + beforeEach(function () { + themeList.init({casper: {}}); + }); + it('updates the active theme if changed', function (done) { var activateThemeSpy = sandbox.spy(themeHandler, 'activateTheme'); @@ -99,7 +103,6 @@ describe('Theme Handler', function () { }] })); blogApp.set('activeTheme', 'not-casper'); - configUtils.set({paths: {availableThemes: {casper: {}}}}); themeHandler.updateActiveTheme(req, res, function () { activateThemeSpy.called.should.be.true(); @@ -116,7 +119,6 @@ describe('Theme Handler', function () { }] })); blogApp.set('activeTheme', 'casper'); - configUtils.set({paths: {availableThemes: {casper: {}}}}); themeHandler.updateActiveTheme(req, res, function () { activateThemeSpy.called.should.be.false(); @@ -135,7 +137,6 @@ describe('Theme Handler', function () { })); blogApp.set('activeTheme', 'not-casper'); - configUtils.set({paths: {availableThemes: {casper: {}}}}); themeHandler.updateActiveTheme(req, res, function (err) { should.exist(err); @@ -158,7 +159,6 @@ describe('Theme Handler', function () { res.isAdmin = true; blogApp.set('activeTheme', 'not-casper'); - configUtils.set({paths: {availableThemes: {casper: {}}}}); themeHandler.updateActiveTheme(req, res, function () { activateThemeSpy.called.should.be.false(); diff --git a/core/test/unit/server_helpers/body_class_spec.js b/core/test/unit/server_helpers/body_class_spec.js index 5de265bdb1..ea864c0c3b 100644 --- a/core/test/unit/server_helpers/body_class_spec.js +++ b/core/test/unit/server_helpers/body_class_spec.js @@ -1,7 +1,7 @@ var should = require('should'), hbs = require('express-hbs'), utils = require('./utils'), - configUtils = require('../../utils/configUtils'), + themeList = require('../../../server/themes').list, // Stuff we are testing handlebars = hbs.handlebars, @@ -11,18 +11,16 @@ describe('{{body_class}} helper', function () { var options = {}; before(function () { utils.loadHelpers(); - configUtils.set({paths: { - availableThemes: { - casper: { - assets: null, - 'default.hbs': '/content/themes/casper/default.hbs', - 'index.hbs': '/content/themes/casper/index.hbs', - 'page.hbs': '/content/themes/casper/page.hbs', - 'page-about.hbs': '/content/themes/casper/page-about.hbs', - 'post.hbs': '/content/themes/casper/post.hbs' - } + themeList.init({ + casper: { + assets: null, + 'default.hbs': '/content/themes/casper/default.hbs', + 'index.hbs': '/content/themes/casper/index.hbs', + 'page.hbs': '/content/themes/casper/page.hbs', + 'page-about.hbs': '/content/themes/casper/page-about.hbs', + 'post.hbs': '/content/themes/casper/post.hbs' } - }}); + }); }); beforeEach(function () { @@ -37,7 +35,7 @@ describe('{{body_class}} helper', function () { }); after(function () { - configUtils.restore(); + themeList.init(); }); it('has loaded body_class helper', function () { diff --git a/core/test/unit/themes_spec.js b/core/test/unit/themes_spec.js index 1eb80da766..e0caefa785 100644 --- a/core/test/unit/themes_spec.js +++ b/core/test/unit/themes_spec.js @@ -1,13 +1,22 @@ var should = require('should'), + sinon = require('sinon'), + _ = require('lodash'), fs = require('fs'), tmp = require('tmp'), join = require('path').join, - readThemes = require('../../server/themes/read'); + themeList = require('../../server/themes').list, + readThemes = require('../../server/themes/read'), + + sandbox = sinon.sandbox.create(); // To stop jshint complaining should.equal(true, true); describe('Themes', function () { + afterEach(function () { + sandbox.restore(); + }); + describe('Read All', function () { it('should read directory and include only folders', function (done) { var themePath = tmp.dirSync({unsafeCleanup: true}); @@ -126,4 +135,73 @@ describe('Themes', function () { .finally(themePath.removeCallback); }); }); + + describe('List', function () { + beforeEach(function () { + themeList.init({ + casper: {foo: 'bar'}, + 'not-casper': {bar: 'baz'} + }); + }); + + it('get() allows getting a single theme', function () { + themeList.get('casper').should.eql({foo: 'bar'}); + }); + + it('get() with no args should do nothing', function () { + should.not.exist(themeList.get()); + }); + + it('getAll() returns all themes', function () { + themeList.getAll().should.be.an.Object().with.properties('casper', 'not-casper'); + Object.keys(themeList.getAll()).should.have.length(2); + }); + + it('set() updates an existing theme', function () { + var origCasper = _.cloneDeep(themeList.get('casper')); + themeList.set('casper', {magic: 'update'}); + + themeList.get('casper').should.not.eql(origCasper); + themeList.get('casper').should.eql({magic: 'update'}); + }); + + it('set() can add a new theme', function () { + themeList.set('rasper', {color: 'red'}); + themeList.get('rasper').should.eql({color: 'red'}); + }); + + it('del() removes a key from the list', function () { + should.exist(themeList.get('casper')); + should.exist(themeList.get('not-casper')); + themeList.del('casper'); + should.not.exist(themeList.get('casper')); + should.exist(themeList.get('not-casper')); + }); + + it('del() with no argument does nothing', function () { + should.exist(themeList.get('casper')); + should.exist(themeList.get('not-casper')); + themeList.del(); + should.exist(themeList.get('casper')); + should.exist(themeList.get('not-casper')); + }); + + it('init() calls set for each theme', function () { + var setSpy = sandbox.spy(themeList, 'set'); + + themeList.init({test: {a: 'b'}, casper: {c: 'd'}}); + setSpy.calledTwice.should.be.true(); + setSpy.firstCall.calledWith('test', {a: 'b'}).should.be.true(); + setSpy.secondCall.calledWith('casper', {c: 'd'}).should.be.true(); + }); + + it('init() with empty object resets the list', function () { + themeList.init(); + var result = themeList.getAll(); + should.exist(result); + result.should.be.an.Object(); + result.should.eql({}); + Object.keys(result).should.have.length(0); + }); + }); }); diff --git a/core/test/utils/api.js b/core/test/utils/api.js index f611a4f00a..0401cee520 100644 --- a/core/test/utils/api.js +++ b/core/test/utils/api.js @@ -32,6 +32,7 @@ var _ = require('lodash'), permission: _.keys(schema.permissions), notification: ['type', 'message', 'status', 'id', 'dismissible', 'location'], theme: ['uuid', 'name', 'version', 'active'], + themes: ['themes'], invites: _(schema.invites).keys().without('token').value() }; diff --git a/core/test/utils/index.js b/core/test/utils/index.js index 3d4786145e..77ecdd937f 100644 --- a/core/test/utils/index.js +++ b/core/test/utils/index.js @@ -449,7 +449,7 @@ toDoList = { clients: function insertClients() { return fixtures.insertClients(); }, filter: function createFilterParamFixtures() { return filterData(DataGenerator); }, invites: function insertInvites() { return fixtures.insertInvites(); }, - themes: function loadThemes() { return themes.load(); } + themes: function loadThemes() { return themes.loadAll(); } }; /**