diff --git a/core/server/controllers/frontend/error.js b/core/server/controllers/frontend/error.js new file mode 100644 index 0000000000..bb84c78627 --- /dev/null +++ b/core/server/controllers/frontend/error.js @@ -0,0 +1,12 @@ +function handleError(next) { + return function handleError(err) { + // If we've thrown an error message of type: 'NotFound' then we found no path match. + if (err.errorType === 'NotFoundError') { + return next(); + } + + return next(err); + }; +} + +module.exports = handleError; diff --git a/core/server/controllers/frontend/fetch-data.js b/core/server/controllers/frontend/fetch-data.js new file mode 100644 index 0000000000..f480bedda5 --- /dev/null +++ b/core/server/controllers/frontend/fetch-data.js @@ -0,0 +1,17 @@ +var api = require('../../api'); + +function fetchData(options) { + return api.settings.read('postsPerPage').then(function then(response) { + var postPP = response.settings[0], + postsPerPage = parseInt(postPP.value, 10); + + // No negative posts per page, must be number + if (!isNaN(postsPerPage) && postsPerPage > 0) { + options.limit = postsPerPage; + } + options.include = 'author,tags,fields'; + return api.posts.browse(options); + }); +} + +module.exports = fetchData; diff --git a/core/server/controllers/frontend/format-response.js b/core/server/controllers/frontend/format-response.js new file mode 100644 index 0000000000..d8f0ba04ae --- /dev/null +++ b/core/server/controllers/frontend/format-response.js @@ -0,0 +1,31 @@ +var _ = require('lodash'); + +/** + * formats variables for handlebars in multi-post contexts. + * If extraValues are available, they are merged in the final value + * @return {Object} containing page variables + */ +function formatPageResponse(posts, page, extraValues) { + extraValues = extraValues || {}; + + var resp = { + posts: posts, + pagination: page.meta.pagination + }; + return _.extend(resp, extraValues); +} + +/** + * similar to formatPageResponse, but for single post pages + * @return {Object} containing page variables + */ +function formatResponse(post) { + return { + post: post + }; +} + +module.exports = { + channel: formatPageResponse, + single: formatResponse +}; diff --git a/core/server/controllers/frontend/index.js b/core/server/controllers/frontend/index.js index c5a6febde9..855f167be9 100644 --- a/core/server/controllers/frontend/index.js +++ b/core/server/controllers/frontend/index.js @@ -15,87 +15,16 @@ var _ = require('lodash'), template = require('../../helpers/template'), routeMatch = require('path-match')(), safeString = require('../../utils/index').safeString, + handleError = require('./error'), + fetchData = require('./fetch-data'), + formatResponse = require('./format-response'), setResponseContext = require('./context'), + setRequestIsSecure = require('./secure'), + getActiveThemePaths = require('./theme-paths'), frontendControllers, staticPostPermalink = routeMatch('/:slug/:edit?'); -function getPostPage(options) { - return api.settings.read('postsPerPage').then(function then(response) { - var postPP = response.settings[0], - postsPerPage = parseInt(postPP.value, 10); - - // No negative posts per page, must be number - if (!isNaN(postsPerPage) && postsPerPage > 0) { - options.limit = postsPerPage; - } - options.include = 'author,tags,fields'; - return api.posts.browse(options); - }); -} - -/** - * formats variables for handlebars in multi-post contexts. - * If extraValues are available, they are merged in the final value - * @return {Object} containing page variables - */ -function formatPageResponse(posts, page, extraValues) { - extraValues = extraValues || {}; - - var resp = { - posts: posts, - pagination: page.meta.pagination - }; - return _.extend(resp, extraValues); -} - -/** - * similar to formatPageResponse, but for single post pages - * @return {Object} containing page variables - */ -function formatResponse(post) { - return { - post: post - }; -} - -function handleError(next) { - return function handleError(err) { - // If we've thrown an error message of type: 'NotFound' then we found no path match. - if (err.errorType === 'NotFoundError') { - return next(); - } - - return next(err); - }; -} - -// Add Request context parameter to the data object -// to be passed down to the templates -function setReqCtx(req, data) { - (Array.isArray(data) ? data : [data]).forEach(function forEach(d) { - d.secure = req.secure; - }); -} - -/** - * Returns the paths object of the active theme via way of a promise. - * @return {Promise} The promise resolves with the value of the paths. - */ -function getActiveThemePaths() { - return api.settings.read({ - key: 'activeTheme', - context: { - internal: true - } - }).then(function then(response) { - var activeTheme = response.settings[0], - paths = config.paths.availableThemes[activeTheme.value]; - - return paths; - }); -} - /* * Sets the response context around a post and renders it * with the current theme's post view. Used by post preview @@ -106,7 +35,7 @@ function renderPost(req, res) { return function renderPost(post) { return getActiveThemePaths().then(function then(paths) { var view = template.getThemeViewForPost(paths, post), - response = formatResponse(post); + response = formatResponse.single(post); setResponseContext(req, res, response); res.render(view, response); @@ -149,17 +78,17 @@ function renderChannel(channelOpts) { return res.redirect(createUrl()); } - return getPostPage(options).then(function then(page) { + return fetchData(options).then(function then(page) { // If page is greater than number of pages we have, redirect to last page if (pageParam > page.meta.pagination.pages) { return res.redirect(createUrl(page.meta.pagination.pages)); } - setReqCtx(req, page.posts); + setRequestIsSecure(req, page.posts); if (channelOpts.filter && page.meta.filters[channelOpts.filter]) { filterKey = page.meta.filters[channelOpts.filter]; filter = (_.isArray(filterKey)) ? filterKey[0] : filterKey; - setReqCtx(req, filter); + setRequestIsSecure(req, filter); } filters.doFilter('prePostsRender', page.posts, res.locals).then(function then(posts) { @@ -183,9 +112,9 @@ function renderChannel(channelOpts) { return next(); } - result = formatPageResponse(posts, page, extra); + result = formatResponse.channel(posts, page, extra); } else { - result = formatPageResponse(posts, page); + result = formatResponse.channel(posts, page); } setResponseContext(req, res); @@ -232,7 +161,7 @@ frontendControllers = { return res.redirect(301, config.urlFor('post', {post: post})); } - setReqCtx(req, post); + setRequestIsSecure(req, post); filters.doFilter('prePostsRender', post, res.locals) .then(renderPost(req, res)); @@ -301,7 +230,7 @@ frontendControllers = { return Promise.reject(new errors.NotFoundError()); } - setReqCtx(req, post); + setRequestIsSecure(req, post); filters.doFilter('prePostsRender', post, res.locals) .then(renderPost(req, res)); diff --git a/core/server/controllers/frontend/secure.js b/core/server/controllers/frontend/secure.js new file mode 100644 index 0000000000..c91e4b2e3d --- /dev/null +++ b/core/server/controllers/frontend/secure.js @@ -0,0 +1,10 @@ +// TODO: figure out how to remove the need for this +// Add Request context parameter to the data object +// to be passed down to the templates +function setRequestIsSecure(req, data) { + (Array.isArray(data) ? data : [data]).forEach(function forEach(d) { + d.secure = req.secure; + }); +} + +module.exports = setRequestIsSecure; diff --git a/core/server/controllers/frontend/theme-paths.js b/core/server/controllers/frontend/theme-paths.js new file mode 100644 index 0000000000..0d2672d4ec --- /dev/null +++ b/core/server/controllers/frontend/theme-paths.js @@ -0,0 +1,22 @@ +var api = require('../../api'), + config = require('../../config'); + +/** + * Returns the paths object of the active theme via way of a promise. + * @return {Promise} The promise resolves with the value of the paths. + */ +function getActiveThemePaths() { + return api.settings.read({ + key: 'activeTheme', + context: { + internal: true + } + }).then(function then(response) { + var activeTheme = response.settings[0], + paths = config.paths.availableThemes[activeTheme.value]; + + return paths; + }); +} + +module.exports = getActiveThemePaths; diff --git a/core/test/unit/controllers/frontend/context_spec.js b/core/test/unit/controllers/frontend/context_spec.js index c533321563..f4e47ffbd0 100644 --- a/core/test/unit/controllers/frontend/context_spec.js +++ b/core/test/unit/controllers/frontend/context_spec.js @@ -245,4 +245,17 @@ describe('Contexts', function () { res.locals.context[0].should.eql('page'); }); }); + + describe('Private', function () { + it('should correctly identify `/private/` as the private route', function () { + // Setup test by setting relativeUrl + res.locals.relativeUrl = '/private/?r='; + + setResponseContext(req, res, data); + + should.exist(res.locals.context); + res.locals.context.should.be.an.Array.with.lengthOf(1); + res.locals.context[0].should.eql('private'); + }); + }); }); diff --git a/core/test/unit/controllers/frontend/error_spec.js b/core/test/unit/controllers/frontend/error_spec.js new file mode 100644 index 0000000000..7eec67fd51 --- /dev/null +++ b/core/test/unit/controllers/frontend/error_spec.js @@ -0,0 +1,43 @@ +/*globals describe, beforeEach, afterEach, it*/ +/*jshint expr:true*/ +var should = require('should'), + sinon = require('sinon'), + errors = require('../../../../server/errors'), + +// Stuff we are testing + handleError = require('../../../../server/controllers/frontend/error'), + + sandbox = sinon.sandbox.create(); + +// To stop jshint complaining +should.equal(true, true); + +describe('handleError', function () { + var next; + beforeEach(function () { + next = sandbox.spy(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should call next with no args for 404 errors', function () { + var notFoundError = new errors.NotFoundError('Something wasn\'t found'); + handleError(next)(notFoundError); + + next.calledOnce.should.be.true; + next.firstCall.args.should.be.empty; + }); + + it('should call next with error for other errors', function () { + var otherError = new errors.MethodNotAllowedError('Something wasn\'t allowed'); + + handleError(next)(otherError); + + next.calledOnce.should.be.true; + next.firstCall.args.should.have.lengthOf(1); + next.firstCall.args[0].should.be.an.Object; + next.firstCall.args[0].should.be.instanceof(Error); + }); +}); diff --git a/core/test/unit/controllers/frontend/fetch-data_spec.js b/core/test/unit/controllers/frontend/fetch-data_spec.js new file mode 100644 index 0000000000..c4104a1741 --- /dev/null +++ b/core/test/unit/controllers/frontend/fetch-data_spec.js @@ -0,0 +1,79 @@ +/*globals describe, beforeEach, afterEach, it*/ +/*jshint expr:true*/ +var should = require('should'), + sinon = require('sinon'), + Promise = require('bluebird'), + + // Stuff we are testing + api = require('../../../../server/api'), + fetchData = require('../../../../server/controllers/frontend/fetch-data'), + + sandbox = sinon.sandbox.create(); + +describe('fetchData', function () { + var apiSettingsStub, + apiPostsStub; + + beforeEach(function () { + apiPostsStub = sandbox.stub(api.posts, 'browse').returns(new Promise.resolve({})); + apiSettingsStub = sandbox.stub(api.settings, 'read'); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('valid postsPerPage', function () { + beforeEach(function () { + apiSettingsStub.withArgs('postsPerPage').returns(Promise.resolve({ + settings: [{ + key: 'postsPerPage', + value: '10' + }] + })); + }); + + it('Adds limit & includes to options by default', function (done) { + fetchData({}).then(function () { + apiSettingsStub.calledOnce.should.be.true; + apiPostsStub.calledOnce.should.be.true; + apiPostsStub.firstCall.args[0].should.be.an.Object; + apiPostsStub.firstCall.args[0].should.have.property('include'); + apiPostsStub.firstCall.args[0].should.have.property('limit', 10); + done(); + }).catch(done); + }); + + it('Throws error if no options are passed', function (done) { + fetchData().then(function () { + done('Should have thrown an error here'); + }).catch(function (err) { + should.exist(err); + done(); + }); + }); + }); + + describe('invalid postsPerPage', function () { + beforeEach(function () { + apiSettingsStub.withArgs('postsPerPage').returns(Promise.resolve({ + settings: [{ + key: 'postsPerPage', + value: '-1' + }] + })); + }); + + it('Will not add limit if postsPerPage is not valid', function (done) { + fetchData({}).then(function () { + apiSettingsStub.calledOnce.should.be.true; + apiPostsStub.calledOnce.should.be.true; + apiPostsStub.firstCall.args[0].should.be.an.Object; + apiPostsStub.firstCall.args[0].should.have.property('include'); + apiPostsStub.firstCall.args[0].should.not.have.property('limit'); + + done(); + }).catch(done); + }); + }); +}); diff --git a/core/test/unit/controllers/frontend/index_spec.js b/core/test/unit/controllers/frontend/index_spec.js index edeabbeebc..6e2bd7ca08 100644 --- a/core/test/unit/controllers/frontend/index_spec.js +++ b/core/test/unit/controllers/frontend/index_spec.js @@ -4,16 +4,15 @@ var moment = require('moment'), should = require('should'), sinon = require('sinon'), Promise = require('bluebird'), - rewire = require('rewire'), _ = require('lodash'), + path = require('path'), // Stuff we are testing api = require('../../../../server/api'), + frontend = require('../../../../server/controllers/frontend'), - frontend = rewire('../../../../server/controllers/frontend'), config = require('../../../../server/config'), - origConfig = _.cloneDeep(config), - defaultConfig = require('../../../../../config.example')[process.env.NODE_ENV]; + origConfig = _.cloneDeep(config); // To stop jshint complaining should.equal(true, true); @@ -21,21 +20,14 @@ should.equal(true, true); describe('Frontend Controller', function () { var sandbox, apiSettingsStub, - adminEditPagePath = '/ghost/editor/', - - resetConfig = function () { - config.set(_.merge({}, origConfig, defaultConfig)); - }; + adminEditPagePath = '/ghost/editor/'; beforeEach(function () { sandbox = sinon.sandbox.create(); - - // Reset frontend controller for next test - frontend = rewire('../../../../server/controllers/frontend'); }); afterEach(function () { - resetConfig(); + config.set(origConfig); sandbox.restore(); }); @@ -43,14 +35,14 @@ describe('Frontend Controller', function () { // from failing via timeout when they // should just immediately fail function failTest(done, msg) { - return function (stuf) { - console.log(msg, stuf); + return function () { done(new Error(msg)); }; } describe('homepage redirects', function () { - var res; + var res, + req; beforeEach(function () { res = { @@ -58,6 +50,11 @@ describe('Frontend Controller', function () { render: sandbox.spy() }; + req = { + params: {page: 0}, + route: {path: '/page/:page/'} + }; + sandbox.stub(api.posts, 'browse', function () { return Promise.resolve({posts: {}, meta: {pagination: {pages: 3}}}); }); @@ -71,14 +68,8 @@ describe('Frontend Controller', function () { })); }); - afterEach(function () { - config.set({paths: origConfig.paths}); - frontend.__set__('config', {paths: origConfig.paths}); - sandbox.restore(); - }); - it('Redirects to home if page number is -1', function () { - var req = {params: {page: -1}, route: {path: '/page/:page/'}}; + req.params.page = -1; frontend.homepage(req, res, null); @@ -88,7 +79,7 @@ describe('Frontend Controller', function () { }); it('Redirects to home if page number is 0', function () { - var req = {params: {page: 0}, route: {path: '/page/:page/'}}; + req.params.page = 0; frontend.homepage(req, res, null); @@ -98,7 +89,7 @@ describe('Frontend Controller', function () { }); it('Redirects to home if page number is 1', function () { - var req = {params: {page: 1}, route: {path: '/page/:page/'}}; + req.params.page = 1; frontend.homepage(req, res, null); @@ -108,11 +99,11 @@ describe('Frontend Controller', function () { }); it('Redirects to home if page number is 0 with subdirectory', function () { - frontend.__set__('config', { - paths: {subdir: '/blog'} + config.set({ + url: 'http://my-ghost-blog.com/blog' }); - var req = {params: {page: 0}, route: {path: '/page/:page/'}}; + req.params.page = 0; frontend.homepage(req, res, null); @@ -122,11 +113,11 @@ describe('Frontend Controller', function () { }); it('Redirects to home if page number is 1 with subdirectory', function () { - frontend.__set__('config', { - paths: {subdir: '/blog'} + config.set({ + url: 'http://my-ghost-blog.com/blog' }); - var req = {params: {page: 1}, route: {path: '/page/:page/'}}; + req.params.page = 1; frontend.homepage(req, res, null); @@ -136,7 +127,7 @@ describe('Frontend Controller', function () { }); it('Redirects to last page if page number too big', function (done) { - var req = {params: {page: 4}, route: {path: '/page/:page/'}}; + req.params.page = 4; frontend.homepage(req, res, done).then(function () { res.redirect.called.should.be.true; @@ -147,11 +138,11 @@ describe('Frontend Controller', function () { }); it('Redirects to last page if page number too big with subdirectory', function (done) { - frontend.__set__('config', { - paths: {subdir: '/blog'} + config.set({ + url: 'http://my-ghost-blog.com/blog' }); - var req = {params: {page: 4}, route: {path: '/page/:page/'}}; + req.params.page = 4; frontend.homepage(req, res, done).then(function () { res.redirect.calledOnce.should.be.true; @@ -163,6 +154,8 @@ describe('Frontend Controller', function () { }); describe('homepage', function () { + var req, res; + beforeEach(function () { sandbox.stub(api.posts, 'browse', function () { return Promise.resolve({ @@ -177,14 +170,12 @@ describe('Frontend Controller', function () { }); apiSettingsStub = sandbox.stub(api.settings, 'read'); - apiSettingsStub.withArgs(sinon.match.has('key', 'activeTheme')).returns(Promise.resolve({ settings: [{ key: 'activeTheme', value: 'casper' }] })); - apiSettingsStub.withArgs('postsPerPage').returns(Promise.resolve({ settings: [{ key: 'postsPerPage', @@ -192,129 +183,63 @@ describe('Frontend Controller', function () { }] })); - frontend.__set__('config', { - paths: { - subdir: '', - availableThemes: { - casper: { - assets: null, - 'default.hbs': '/content/themes/casper/default.hbs', - 'index.hbs': '/content/themes/casper/index.hbs', - 'home.hbs': '/content/themes/casper/home.hbs', - 'page.hbs': '/content/themes/casper/page.hbs', - 'tag.hbs': '/content/themes/casper/tag.hbs' - } - } - }, - routeKeywords: { - page: 'page', - tag: 'tag', - author: 'author' - } - }); + req = { + path: '/', params: {}, route: {} + }; + + res = { + locals: {} + }; }); it('Renders home.hbs template when it exists in the active theme', function (done) { - var req = { - path: '/', - params: {}, - route: {} - }, - res = { - locals: {}, - render: function (view) { - view.should.equal('home'); - done(); - } - }; + config.set({paths: {availableThemes: {casper: { + 'index.hbs': '/content/themes/casper/index.hbs', + 'home.hbs': '/content/themes/casper/home.hbs' + }}}}); + + res.render = function (view) { + view.should.equal('home'); + done(); + }; frontend.homepage(req, res, failTest(done)); }); - it('Renders index.hbs template on 2nd page when home.bs exists', function (done) { - var req = { - path: '/page/2/', - params: { - page: 2 - }, - route: {} - }, - res = { - locals: {}, - render: function (view) { - view.should.equal('index'); - done(); - } - }; + it('Renders index.hbs template on 2nd page when home.hbs exists', function (done) { + config.set({paths: {availableThemes: {casper: { + 'index.hbs': '/content/themes/casper/index.hbs', + 'home.hbs': '/content/themes/casper/home.hbs' + }}}}); + + req.path = '/page/2/'; + req.params = {page: 2}; + + res.render = function (view) { + // assertion + view.should.equal('index'); + done(); + }; frontend.homepage(req, res, failTest(done)); }); it('Renders index.hbs template when home.hbs doesn\'t exist', function (done) { - frontend.__set__('config', { - paths: { - subdir: '', - 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', - 'tag.hbs': '/content/themes/casper/tag.hbs' - } - } - }, - routeKeywords: { - page: 'page', - tag: 'tag', - author: 'author' - } - }); + config.set({paths: {availableThemes: {casper: { + 'index.hbs': '/content/themes/casper/index.hbs' + }}}}); - var req = { - path: '/', - params: {}, - route: {} - }, - res = { - locals: {}, - render: function (view) { - view.should.equal('index'); - done(); - } - }; + res.render = function (view) { + view.should.equal('index'); + done(); + }; frontend.homepage(req, res, failTest(done)); }); }); describe('tag', function () { - var mockPosts = [{ - status: 'published', - id: 1, - title: 'Test static page', - slug: 'test-static-page', - markdown: 'Test static page content', - page: 1, - published_at: new Date('2013/12/30').getTime(), - author: { - id: 1, - name: 'Test User', - email: 'test@ghost.org' - } - }, { - status: 'published', - id: 2, - title: 'Test normal post', - slug: 'test-normal-post', - markdown: 'The test normal post content', - page: 0, - published_at: new Date('2014/1/2').getTime(), - author: { - id: 1, - name: 'Test User' - } - }], + var req, res, mockTags = [{ name: 'video', slug: 'video', @@ -328,7 +253,7 @@ describe('Frontend Controller', function () { beforeEach(function () { sandbox.stub(api.posts, 'browse', function () { return Promise.resolve({ - posts: mockPosts, + posts: [{}], meta: { pagination: { page: 1, @@ -356,62 +281,79 @@ describe('Frontend Controller', function () { value: '10' }] })); + apiSettingsStub.withArgs('permalinks').returns(Promise.resolve({ + settings: [{ + key: 'permalinks', + value: '/tag/:slug/' + }] + })); - frontend.__set__('config', { - paths: { - subdir: '', - 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', - 'tag.hbs': '/content/themes/casper/tag.hbs' - } - } - }, - routeKeywords: { - page: 'page', - tag: 'tag', - author: 'author' - } - }); + req = { + path: '/', params: {}, route: {} + }; + + res = { + locals: {} + }; }); - describe('custom tag template', function () { - beforeEach(function () { - apiSettingsStub.withArgs('permalinks').returns(Promise.resolve({ - settings: [{ - key: 'permalinks', - value: '/tag/:slug/' - }] - })); - }); + it('it will render custom tag-slug template if it exists', function (done) { + config.set({paths: {availableThemes: {casper: { + 'tag-video.hbs': '/content/themes/casper/tag-video.hbs', + 'tag.hbs': '/content/themes/casper/tag.hbs', + 'index.hbs': '/content/themes/casper/index.hbs' + }}}}); - it('it will render custom tag template if it exists', function (done) { - var req = { - path: '/tag/' + mockTags[0].slug, - params: {}, - route: { - path: '/tag/:slug' - } - }, - res = { - locals: {}, - render: function (view, context) { - view.should.equal('tag'); - context.tag.should.equal(mockTags[0]); - done(); - } - }; + req.path = '/tag/' + mockTags[0].slug; + req.params.slug = mockTags[0].slug; + req.route = {path: '/tag/:slug'}; + res.render = function (view, context) { + view.should.equal('tag-video'); + context.tag.should.equal(mockTags[0]); + done(); + }; - frontend.tag(req, res, failTest(done)); - }); + frontend.tag(req, res, failTest(done)); + }); + + it('it will render tag template if it exists and there is no tag-slug template', function (done) { + config.set({paths: {availableThemes: {casper: { + 'tag.hbs': '/content/themes/casper/tag.hbs', + 'index.hbs': '/content/themes/casper/index.hbs' + }}}}); + + req.path = '/tag/' + mockTags[0].slug; + req.params.slug = mockTags[0].slug; + req.route = {path: '/tag/:slug'}; + res.render = function (view, context) { + view.should.equal('tag'); + context.tag.should.equal(mockTags[0]); + done(); + }; + + frontend.tag(req, res, failTest(done)); + }); + + it('it will fall back to index if there are no custom templates', function (done) { + config.set({paths: {availableThemes: {casper: { + 'index.hbs': '/content/themes/casper/index.hbs' + }}}}); + + req.path = '/tag/' + mockTags[0].slug; + req.params.slug = mockTags[0].slug; + req.route = {path: '/tag/:slug'}; + res.render = function (view, context) { + view.should.equal('index'); + context.tag.should.equal(mockTags[0]); + done(); + }; + + frontend.tag(req, res, failTest(done)); }); }); describe('tag redirects', function () { - var res; + var res, req; beforeEach(function () { res = { @@ -419,6 +361,10 @@ describe('Frontend Controller', function () { render: sandbox.spy() }; + req = { + path: '/', params: {}, route: {} + }; + sandbox.stub(api.posts, 'browse', function () { return Promise.resolve({posts: {}, meta: {pagination: {pages: 3}}}); }); @@ -433,7 +379,7 @@ describe('Frontend Controller', function () { }); it('Redirects to base tag page if page number is -1', function () { - var req = {params: {page: -1, slug: 'pumpkin'}}; + req.params = {page: -1, slug: 'pumpkin'}; frontend.tag(req, res, null); @@ -443,7 +389,7 @@ describe('Frontend Controller', function () { }); it('Redirects to base tag page if page number is 0', function () { - var req = {params: {page: 0, slug: 'pumpkin'}}; + req.params = {page: 0, slug: 'pumpkin'}; frontend.tag(req, res, null); @@ -453,7 +399,7 @@ describe('Frontend Controller', function () { }); it('Redirects to base tag page if page number is 1', function () { - var req = {params: {page: 1, slug: 'pumpkin'}}; + req.params = {page: 1, slug: 'pumpkin'}; frontend.tag(req, res, null); @@ -463,12 +409,9 @@ describe('Frontend Controller', function () { }); it('Redirects to base tag page if page number is 0 with subdirectory', function () { - frontend.__set__('config', { - paths: {subdir: '/blog'}, - routeKeywords: {tag: 'tag'} - }); + config.set({url: 'http://my-ghost-blog.com/blog/'}); - var req = {params: {page: 0, slug: 'pumpkin'}}; + req.params = {page: 0, slug: 'pumpkin'}; frontend.tag(req, res, null); @@ -478,12 +421,9 @@ describe('Frontend Controller', function () { }); it('Redirects to base tag page if page number is 1 with subdirectory', function () { - frontend.__set__('config', { - paths: {subdir: '/blog'}, - routeKeywords: {tag: 'tag'} - }); + config.set({url: 'http://my-ghost-blog.com/blog/'}); - var req = {params: {page: 1, slug: 'pumpkin'}}; + req.params = {page: 1, slug: 'pumpkin'}; frontend.tag(req, res, null); @@ -493,7 +433,7 @@ describe('Frontend Controller', function () { }); it('Redirects to last page if page number too big', function (done) { - var req = {params: {page: 4, slug: 'pumpkin'}}; + req.params = {page: 4, slug: 'pumpkin'}; frontend.tag(req, res, done).then(function () { res.redirect.called.should.be.true; @@ -504,12 +444,9 @@ describe('Frontend Controller', function () { }); it('Redirects to last page if page number too big with subdirectory', function (done) { - frontend.__set__('config', { - paths: {subdir: '/blog'}, - routeKeywords: {tag: 'tag'} - }); + config.set({url: 'http://my-ghost-blog.com/blog/'}); - var req = {params: {page: 4, slug: 'pumpkin'}}; + req.params = {page: 4, slug: 'pumpkin'}; frontend.tag(req, res, done).then(function () { res.redirect.calledOnce.should.be.true; @@ -521,7 +458,7 @@ describe('Frontend Controller', function () { }); describe('single', function () { - var mockPosts = [{ + var req, res, casper, mockPosts = [{ posts: [{ status: 'published', id: 1, @@ -582,7 +519,6 @@ describe('Frontend Controller', function () { }); apiSettingsStub = sandbox.stub(api.settings, 'read'); - apiSettingsStub.withArgs(sinon.match.has('key', 'activeTheme')).returns(Promise.resolve({ settings: [{ key: 'activeTheme', @@ -590,26 +526,24 @@ describe('Frontend Controller', function () { }] })); - frontend.__set__('config', { - paths: { - subdir: '', - 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' - } - } - }, - routeKeywords: { - page: 'page', - tag: 'tag', - author: 'author' - } - }); + 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' + }; + + req = { + path: '/', params: {}, route: {} + }; + + res = { + locals: {}, + render: sinon.spy(), + redirect: sinon.spy() + }; }); describe('static pages', function () { @@ -622,23 +556,48 @@ describe('Frontend Controller', function () { })); }); - it('it will render custom page template if it exists', function (done) { - var req = { - path: '/' + mockPosts[2].posts[0].slug + '/', - route: { - path: '*' - }, - params: {} - }, - res = { - locals: {}, - render: function (view, context) { - view.should.equal('page-' + mockPosts[2].posts[0].slug); - context.post.should.equal(mockPosts[2].posts[0]); - done(); - } - }; + it('it will render a custom page-slug template if it exists', function (done) { + config.set({paths: {availableThemes: {casper: casper}}}); + req.path = '/' + mockPosts[2].posts[0].slug + '/'; + req.route = {path: '*'}; + res.render = function (view, context) { + view.should.equal('page-' + mockPosts[2].posts[0].slug); + context.post.should.equal(mockPosts[2].posts[0]); + done(); + }; mockPosts[2].posts[0].url = req.path; + + frontend.single(req, res, failTest(done)); + }); + + it('it will use page.hbs if it exists and no page-slug template is present', function (done) { + delete casper['page-about.hbs']; + config.set({paths: {availableThemes: {casper: casper}}}); + req.path = '/' + mockPosts[2].posts[0].slug + '/'; + req.route = {path: '*'}; + res.render = function (view, context) { + view.should.equal('page'); + context.post.should.equal(mockPosts[2].posts[0]); + done(); + }; + mockPosts[2].posts[0].url = req.path; + + frontend.single(req, res, failTest(done)); + }); + + it('defaults to post.hbs without a page.hbs or page-slug template', function (done) { + delete casper['page-about.hbs']; + delete casper['page.hbs']; + config.set({paths: {availableThemes: {casper: casper}}}); + req.path = '/' + mockPosts[2].posts[0].slug + '/'; + req.route = {path: '*'}; + res.render = function (view, context) { + view.should.equal('post'); + context.post.should.equal(mockPosts[2].posts[0]); + done(); + }; + mockPosts[2].posts[0].url = req.path; + frontend.single(req, res, failTest(done)); }); }); @@ -653,33 +612,21 @@ describe('Frontend Controller', function () { }); it('will render static page via /:slug/', function (done) { - var req = { - path: '/' + mockPosts[0].posts[0].slug + '/', - route: { - path: '*' - }, - params: {} - }, - res = { - locals: {}, - render: function (view, context) { - view.should.equal('page'); - context.post.should.equal(mockPosts[0].posts[0]); - done(); - } - }; + config.set({paths: {availableThemes: {casper: casper}}}); + + req.path = '/' + mockPosts[0].posts[0].slug + '/'; + req.route = {path: '*'}; + res.render = function (view, context) { + view.should.equal('page'); + context.post.should.equal(mockPosts[0].posts[0]); + done(); + }; frontend.single(req, res, failTest(done)); }); it('will NOT render static page via /YYY/MM/DD/:slug', function (done) { - var req = { - path: '/' + ['2012/12/30', mockPosts[0].posts[0].slug].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy() - }; + req.path = '/' + ['2012/12/30', mockPosts[0].posts[0].slug].join('/') + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -688,13 +635,7 @@ describe('Frontend Controller', function () { }); it('will NOT render static page via /:author/:slug', function (done) { - var req = { - path: '/' + ['test', mockPosts[0].posts[0].slug].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy() - }; + req.path = '/' + ['test', mockPosts[0].posts[0].slug].join('/') + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -703,31 +644,18 @@ describe('Frontend Controller', function () { }); it('will redirect static page to admin edit page via /:slug/edit', function (done) { - var req = { - path: '/' + [mockPosts[0].posts[0].slug, 'edit'].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: function (arg) { - res.render.called.should.be.false; - arg.should.eql(adminEditPagePath + mockPosts[0].posts[0].id + '/'); - done(); - } - }; + req.path = '/' + [mockPosts[0].posts[0].slug, 'edit'].join('/') + '/'; + res.redirect = function (arg) { + res.render.called.should.be.false; + arg.should.eql(adminEditPagePath + mockPosts[0].posts[0].id + '/'); + done(); + }; frontend.single(req, res, failTest(done)); }); it('will NOT redirect static page to admin edit page via /YYYY/MM/DD/:slug/edit', function (done) { - var req = { - path: '/' + ['2012/12/30', mockPosts[0].posts[0].slug, 'edit'].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: sinon.spy() - }; + req.path = '/' + ['2012/12/30', mockPosts[0].posts[0].slug, 'edit'].join('/') + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -737,14 +665,7 @@ describe('Frontend Controller', function () { }); it('will NOT redirect static page to admin edit page via /:author/:slug/edit', function (done) { - var req = { - path: '/' + ['test', mockPosts[0].posts[0].slug, 'edit'].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: sinon.spy() - }; + req.path = '/' + ['test', mockPosts[0].posts[0].slug, 'edit'].join('/') + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -764,32 +685,22 @@ describe('Frontend Controller', function () { }); it('will render static page via /:slug', function (done) { - var req = { - path: '/' + mockPosts[0].posts[0].slug + '/', - route: { - path: '*' - }, - params: {} - }, - res = { - locals: {}, - render: function (view, context) { - view.should.equal('page'); - context.post.should.equal(mockPosts[0].posts[0]); - done(); - } - }; + config.set({paths: {availableThemes: {casper: casper}}}); + + req.path = '/' + mockPosts[0].posts[0].slug + '/'; + req.route = {path: '*'}; + res.render = function (view, context) { + view.should.equal('page'); + context.post.should.equal(mockPosts[0].posts[0]); + done(); + }; frontend.single(req, res, failTest(done)); }); it('will NOT render static page via /YYYY/MM/DD/:slug', function (done) { - var req = { - path: '/' + ['2012/12/30', mockPosts[0].posts[0].slug].join('/') + '/' - }, - res = { - render: sinon.spy() - }; + req.path = '/' + ['2012/12/30', mockPosts[0].posts[0].slug].join('/') + '/'; + res.render = sinon.spy(); frontend.single(req, res, function () { res.render.called.should.be.false; @@ -798,31 +709,21 @@ describe('Frontend Controller', function () { }); it('will redirect static page to admin edit page via /:slug/edit', function (done) { - var req = { - path: '/' + [mockPosts[0].posts[0].slug, 'edit'].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: function (arg) { - res.render.called.should.be.false; - arg.should.eql(adminEditPagePath + mockPosts[0].posts[0].id + '/'); - done(); - } - }; + req.path = '/' + [mockPosts[0].posts[0].slug, 'edit'].join('/') + '/'; + res.render = sinon.spy(); + res.redirect = function (arg) { + res.render.called.should.be.false; + arg.should.eql(adminEditPagePath + mockPosts[0].posts[0].id + '/'); + done(); + }; frontend.single(req, res, failTest(done)); }); it('will NOT redirect static page to admin edit page via /YYYY/MM/DD/:slug/edit', function (done) { - var req = { - path: '/' + ['2012/12/30', mockPosts[0].posts[0].slug, 'edit'].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: sinon.spy() - }; + req.path = '/' + ['2012/12/30', mockPosts[0].posts[0].slug, 'edit'].join('/') + '/'; + res.render = sinon.spy(); + res.redirect = sinon.spy(); frontend.single(req, res, function () { res.render.called.should.be.false; @@ -846,34 +747,22 @@ describe('Frontend Controller', function () { }); it('will render post via /:slug/', function (done) { - var req = { - path: '/' + mockPosts[1].posts[0].slug + '/', - route: { - path: '*' - }, - params: {} - }, - res = { - locals: {}, - render: function (view, context) { - view.should.equal('post'); - context.post.should.exist; - context.post.should.equal(mockPosts[1].posts[0]); - done(); - } - }; + config.set({paths: {availableThemes: {casper: casper}}}); + + req.path = '/' + mockPosts[1].posts[0].slug + '/'; + req.route = {path: '*'}; + res.render = function (view, context) { + view.should.equal('post'); + context.post.should.exist; + context.post.should.equal(mockPosts[1].posts[0]); + done(); + }; frontend.single(req, res, failTest(done)); }); it('will NOT render post via /YYYY/MM/DD/:slug', function (done) { - var req = { - path: '/' + ['2012/12/30', mockPosts[1].posts[0].slug].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy() - }; + req.path = '/' + ['2012/12/30', mockPosts[1].posts[0].slug].join('/') + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -882,13 +771,7 @@ describe('Frontend Controller', function () { }); it('will NOT render post via /:author/:slug', function (done) { - var req = { - path: '/' + ['test', mockPosts[1].posts[0].slug].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy() - }; + req.path = '/' + ['test', mockPosts[1].posts[0].slug].join('/') + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -898,31 +781,18 @@ describe('Frontend Controller', function () { // Handle Edit append it('will redirect post to admin edit page via /:slug/edit', function (done) { - var req = { - path: '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: function (arg) { - res.render.called.should.be.false; - arg.should.eql(adminEditPagePath + mockPosts[1].posts[0].id + '/'); - done(); - } - }; + req.path = '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') + '/'; + res.redirect = function (arg) { + res.render.called.should.be.false; + arg.should.eql(adminEditPagePath + mockPosts[1].posts[0].id + '/'); + done(); + }; frontend.single(req, res, failTest(done)); }); it('will NOT redirect post to admin edit page via /YYYY/MM/DD/:slug/edit', function (done) { - var req = { - path: '/' + ['2012/12/30', mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: sinon.spy() - }; + req.path = '/' + ['2012/12/30', mockPosts[1].posts[0].slug, 'edit'].join('/') + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -932,14 +802,7 @@ describe('Frontend Controller', function () { }); it('will NOT redirect post to admin edit page via /:author/:slug/edit', function (done) { - var req = { - path: '/' + ['test', mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: sinon.spy() - }; + req.path = '/' + ['test', mockPosts[1].posts[0].slug, 'edit'].join('/') + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -949,16 +812,13 @@ describe('Frontend Controller', function () { }); it('should call next if post is not found', function (done) { - var req = { - path: '/unknown/' - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: sinon.spy() - }; + req.path = '/unknown/'; frontend.single(req, res, function (err) { + if (err) { + return done(err); + } + should.not.exist(err); res.render.called.should.be.false; res.redirect.called.should.be.false; @@ -980,36 +840,24 @@ describe('Frontend Controller', function () { }); it('will render post via /YYYY/MM/DD/:slug/', function (done) { - var date = moment(mockPosts[1].posts[0].published_at).format('YYYY/MM/DD'), - req = { - path: '/' + [date, mockPosts[1].posts[0].slug].join('/') + '/', - route: { - path: '*' - }, - params: {} - }, - res = { - locals: {}, - render: function (view, context) { - view.should.equal('post'); - context.post.should.exist; - context.post.should.equal(mockPosts[1].posts[0]); - done(); - } - }; + config.set({paths: {availableThemes: {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: '*'}; + + res.render = function (view, context) { + view.should.equal('post'); + context.post.should.exist; + context.post.should.equal(mockPosts[1].posts[0]); + done(); + }; frontend.single(req, res, failTest(done)); }); it('will NOT render post via /YYYY/MM/DD/:slug/ with non-matching date in url', function (done) { - var date = moment(mockPosts[1].published_at).subtract(1, 'days').format('YYYY/MM/DD'), - req = { - path: '/' + [date, mockPosts[1].posts[0].slug].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy() - }; + var date = moment(mockPosts[1].published_at).subtract(1, 'days').format('YYYY/MM/DD'); + req.path = '/' + [date, mockPosts[1].posts[0].slug].join('/') + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -1018,13 +866,7 @@ describe('Frontend Controller', function () { }); it('will NOT render post via /:slug/', function (done) { - var req = { - path: '/' + mockPosts[1].posts[0].slug + '/' - }, - res = { - locals: {}, - render: sinon.spy() - }; + req.path = '/' + mockPosts[1].posts[0].slug + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -1033,13 +875,7 @@ describe('Frontend Controller', function () { }); it('will NOT render post via /:author/:slug/', function (done) { - var req = { - path: '/' + ['test', mockPosts[1].posts[0].slug].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy() - }; + req.path = '/' + ['test', mockPosts[1].posts[0].slug].join('/') + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -1049,32 +885,19 @@ describe('Frontend Controller', function () { // Handle Edit append it('will redirect post to admin edit page via /YYYY/MM/DD/:slug/edit/', function (done) { - var dateFormat = moment(mockPosts[1].posts[0].published_at).format('YYYY/MM/DD'), - req = { - path: '/' + [dateFormat, mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: function (arg) { - res.render.called.should.be.false; - arg.should.eql(adminEditPagePath + mockPosts[1].posts[0].id + '/'); - done(); - } - }; + var dateFormat = moment(mockPosts[1].posts[0].published_at).format('YYYY/MM/DD'); + req.path = '/' + [dateFormat, mockPosts[1].posts[0].slug, 'edit'].join('/') + '/'; + res.redirect = function (arg) { + res.render.called.should.be.false; + arg.should.eql(adminEditPagePath + mockPosts[1].posts[0].id + '/'); + done(); + }; frontend.single(req, res, failTest(done)); }); it('will NOT redirect post to admin edit page via /:slug/edit/', function (done) { - var req = { - path: '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: sinon.spy() - }; + req.path = '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -1084,14 +907,7 @@ describe('Frontend Controller', function () { }); it('will NOT redirect post to admin edit page via /:author/:slug/edit/', function (done) { - var req = { - path: '/' + ['test', mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: sinon.spy() - }; + req.path = '/' + ['test', mockPosts[1].posts[0].slug, 'edit'].join('/') + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -1114,35 +930,23 @@ describe('Frontend Controller', function () { }); it('will render post via /:author/:slug/', function (done) { - var req = { - path: '/' + ['test', mockPosts[1].posts[0].slug].join('/') + '/', - route: { - path: '*' - }, - params: {} - }, - res = { - locals: {}, - render: function (view, context) { - view.should.equal('post'); - should.exist(context.post); - context.post.should.equal(mockPosts[1].posts[0]); - done(); - } - }; + config.set({paths: {availableThemes: {casper: casper}}}); + + req.path = '/' + ['test', mockPosts[1].posts[0].slug].join('/') + '/'; + req.route = {path: '*'}; + res.render = function (view, context) { + view.should.equal('post'); + should.exist(context.post); + context.post.should.equal(mockPosts[1].posts[0]); + done(); + }; frontend.single(req, res, failTest(done)); }); it('will NOT render post via /YYYY/MM/DD/:slug/', function (done) { - var date = moment(mockPosts[1].posts[0].published_at).format('YYYY/MM/DD'), - req = { - path: '/' + [date, mockPosts[1].posts[0].slug].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy() - }; + var date = moment(mockPosts[1].posts[0].published_at).format('YYYY/MM/DD'); + req.path = '/' + [date, mockPosts[1].posts[0].slug].join('/') + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -1151,13 +955,7 @@ describe('Frontend Controller', function () { }); it('will NOT render post via /:author/:slug/ when author does not match post author', function (done) { - var req = { - path: '/' + ['test-2', mockPosts[1].posts[0].slug].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy() - }; + req.path = '/' + ['test-2', mockPosts[1].posts[0].slug].join('/') + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -1166,13 +964,7 @@ describe('Frontend Controller', function () { }); it('will NOT render post via /:slug/', function (done) { - var req = { - path: '/' + mockPosts[1].posts[0].slug + '/' - }, - res = { - locals: {}, - render: sinon.spy() - }; + req.path = '/' + mockPosts[1].posts[0].slug + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -1182,32 +974,20 @@ describe('Frontend Controller', function () { // Handle Edit append it('will redirect post to admin edit page via /:author/:slug/edit/', function (done) { - var req = { - path: '/' + ['test', mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: function (arg) { - res.render.called.should.be.false; - arg.should.eql(adminEditPagePath + mockPosts[1].posts[0].id + '/'); - done(); - } - }; + req.path = '/' + ['test', mockPosts[1].posts[0].slug, 'edit'].join('/') + '/'; + + res.redirect = function (arg) { + res.render.called.should.be.false; + arg.should.eql(adminEditPagePath + mockPosts[1].posts[0].id + '/'); + done(); + }; frontend.single(req, res, failTest(done)); }); it('will NOT redirect post to admin edit page via /YYYY/MM/DD/:slug/edit/', function (done) { - var date = moment(mockPosts[1].posts[0].published_at).format('YYYY/MM/DD'), - req = { - path: '/' + [date, mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: sinon.spy() - }; + var date = moment(mockPosts[1].posts[0].published_at).format('YYYY/MM/DD'); + req.path = '/' + [date, mockPosts[1].posts[0].slug, 'edit'].join('/') + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -1217,14 +997,7 @@ describe('Frontend Controller', function () { }); it('will NOT redirect post to admin edit page /:slug/edit/', function (done) { - var req = { - path: '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: sinon.spy() - }; + req.path = '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') + '/'; frontend.single(req, res, function () { res.render.called.should.be.false; @@ -1386,25 +1159,148 @@ describe('Frontend Controller', function () { }); }); - describe('private', function () { - var req, config; + describe('rss redirects', function () { + var res, + apiUsersStub; beforeEach(function () { + res = { + locals: {version: ''}, + redirect: sandbox.spy(), + render: sandbox.spy() + }; + + sandbox.stub(api.posts, 'browse', function () { + return Promise.resolve({posts: {}, meta: {pagination: {pages: 3}}}); + }); + + apiUsersStub = sandbox.stub(api.users, 'read').returns(Promise.resolve({})); + + apiSettingsStub = sandbox.stub(api.settings, 'read'); + apiSettingsStub.withArgs('title').returns(Promise.resolve({ + settings: [{ + key: 'title', + value: 'Test' + }] + })); + apiSettingsStub.withArgs('description').returns(Promise.resolve({ + settings: [{ + key: 'description', + value: 'Some Text' + }] + })); + apiSettingsStub.withArgs('permalinks').returns(Promise.resolve({ + settings: [{ + key: 'permalinks', + value: '/:slug/' + }] + })); + }); + + it('Redirects to rss if page number is 0', function () { + var req = {params: {page: -1}, route: {path: '/rss/:page/'}}; + req.originalUrl = req.route.path.replace(':page', req.params.page); + + frontend.rss(req, res, null); + + res.redirect.called.should.be.true; + res.redirect.calledWith('/rss/').should.be.true; + res.render.called.should.be.false; + }); + + it('Redirects to rss if page number is 0', function () { + var req = {params: {page: 0}, route: {path: '/rss/:page/'}}; + req.originalUrl = req.route.path.replace(':page', req.params.page); + + frontend.rss(req, res, null); + + res.redirect.called.should.be.true; + res.redirect.calledWith('/rss/').should.be.true; + res.render.called.should.be.false; + }); + + it('Redirects to home if page number is 1', function () { + var req = {params: {page: 1}, route: {path: '/rss/:page/'}}; + req.originalUrl = req.route.path.replace(':page', req.params.page); + + frontend.rss(req, res, null); + + res.redirect.called.should.be.true; + res.redirect.calledWith('/rss/').should.be.true; + res.render.called.should.be.false; + }); + + it('Redirects to home if page number is 0 with subdirectory', function () { + config.set({url: 'http://testurl.com/blog'}); + + var req = {params: {page: 0}, route: {path: '/rss/:page/'}}; + req.originalUrl = req.route.path.replace(':page', req.params.page); + + frontend.rss(req, res, null); + + res.redirect.called.should.be.true; + res.redirect.calledWith('/blog/rss/').should.be.true; + res.render.called.should.be.false; + }); + + it('Redirects to home if page number is 1 with subdirectory', function () { + config.set({url: 'http://testurl.com/blog'}); + + var req = {params: {page: 1}, route: {path: '/rss/:page/'}}; + req.originalUrl = req.route.path.replace(':page', req.params.page); + + frontend.rss(req, res, null); + + res.redirect.called.should.be.true; + res.redirect.calledWith('/blog/rss/').should.be.true; + res.render.called.should.be.false; + }); + + it('Redirects to last page if page number too big', function (done) { + config.set({url: 'http://testurl.com/'}); + + var req = {params: {page: 4}, route: {path: '/rss/:page/'}}; + req.originalUrl = req.route.path.replace(':page', req.params.page); + + frontend.rss(req, res, done).then(function () { + res.redirect.called.should.be.true; + res.redirect.calledWith('/rss/3/').should.be.true; + res.render.called.should.be.false; + done(); + }).catch(done); + }); + + it('Redirects to last page if page number too big with subdirectory', function (done) { + config.set({url: 'http://testurl.com/blog'}); + + var req = {params: {page: 4}, route: {path: '/rss/:page/'}}; + req.originalUrl = req.route.path.replace(':page', req.params.page); + + frontend.rss(req, res, done).then(function () { + res.redirect.calledOnce.should.be.true; + res.redirect.calledWith('/blog/rss/3/').should.be.true; + res.render.called.should.be.false; + done(); + }).catch(done); + }); + }); + + describe('private', function () { + var res, req, defaultPath; + + beforeEach(function () { + res = { + locals: {version: ''}, + render: sandbox.spy() + }; + req = { route: {path: '/private/?r=/'}, query: {r: ''}, params: {} }; - config = { - paths: { - adminViews: '/core/server/views', - availableThemes: { - casper: {} - } - }, - routeKeywords: {private: 'private'} - }; + defaultPath = path.join(config.paths.appRoot, '/core/server/views/private.hbs'); apiSettingsStub = sandbox.stub(api.settings, 'read'); apiSettingsStub.withArgs(sinon.match.has('key', 'activeTheme')).returns(Promise.resolve({ @@ -1416,52 +1312,45 @@ describe('Frontend Controller', function () { }); it('Should render default password page when theme has no password template', function (done) { - var res = { - locals: {version: '', relativeUrl: '/private/'}, - render: function (view) { - view.should.match(/private.hbs/); - done(); - } + config.set({paths: {availableThemes: {casper: {}}}}); + + res.render = function (view) { + view.should.eql(defaultPath); + done(); }; - frontend.__set__('config', config); frontend.private(req, res, failTest(done)); }); it('Should render theme password page when it exists', function (done) { - var res = { - locals: {version: '', relativeUrl: '/private/'}, - render: function (view) { - view.should.equal('private'); - done(); - } - }; - config.paths.availableThemes.casper = { + config.set({paths: {availableThemes: {casper: { 'private.hbs': '/content/themes/casper/private.hbs' + }}}}); + + res.render = function (view) { + view.should.eql('private'); + done(); }; - frontend.__set__('config', config); frontend.private(req, res, failTest(done)); }); it('Should render with error when error is passed in', function (done) { - var res = { - error: 'Test Error', - locals: {version: '', relativeUrl: '/private/'}, - render: function (view, context) { - view.should.match(/private.hbs/); - context.error.should.equal('Test Error'); - done(); - } + config.set({paths: {availableThemes: {casper: {}}}}); + res.error = 'Test Error'; + + res.render = function (view, context) { + view.should.eql(defaultPath); + context.should.eql({error: 'Test Error'}); + done(); }; - frontend.__set__('config', config); frontend.private(req, res, failTest(done)); }); }); describe('preview', function () { - var mockPosts = [{ + var req, res, mockPosts = [{ posts: [{ status: 'draft', uuid: 'abc-1234-01', @@ -1531,78 +1420,44 @@ describe('Frontend Controller', function () { }] })); - frontend.__set__('config', { - paths: { - subdir: '', - 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' - } - } - }, - routeKeywords: { - page: 'page', - tag: 'tag', - author: 'author' - }, - urlFor: function (type, posts) { - return posts.post.url; - } - }); + req = { + path: '/', params: {}, route: {} + }; + + res = { + locals: {}, + render: sinon.spy(), + redirect: sinon.spy() + }; }); it('should render draft post', function (done) { - var req = { - params: { - uuid: 'abc-1234-02' - } - }, - res = { - render: function (view, context) { - view.should.equal('post'); - should.exist(context.post); - context.post.should.equal(mockPosts[1].posts[0]); - done(); - } - }; + req.params = {uuid: 'abc-1234-02'}; + res.render = function (view, context) { + view.should.equal('post'); + should.exist(context.post); + context.post.should.equal(mockPosts[1].posts[0]); + done(); + }; frontend.preview(req, res, failTest(done)); }); it('should render draft page', function (done) { - var req = { - params: { - uuid: 'abc-1234-01' - } - }, - res = { - render: function (view, context) { - view.should.equal('page'); - should.exist(context.post); - context.post.should.equal(mockPosts[0].posts[0]); - done(); - } - }; + config.set({paths: {availableThemes: {casper: {'page.hbs': '/content/themes/casper/page.hbs'}}}}); + req.params = {uuid: 'abc-1234-01'}; + res.render = function (view, context) { + view.should.equal('page'); + should.exist(context.post); + context.post.should.equal(mockPosts[0].posts[0]); + done(); + }; frontend.preview(req, res, failTest(done)); }); it('should call next if post is not found', function (done) { - var req = { - params: { - uuid: 'abc-1234-04' - } - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: sinon.spy() - }; + req.params = {uuid: 'abc-1234-04'}; frontend.preview(req, res, function (err) { should.not.exist(err); @@ -1613,21 +1468,13 @@ describe('Frontend Controller', function () { }); it('should call redirect if post is published', function (done) { - var req = { - params: { - uuid: 'abc-1234-03' - } - }, - res = { - locals: {}, - render: sinon.spy(), - redirect: function (status, url) { - res.render.called.should.be.false; - status.should.eql(301); - url.should.eql('/getting-started/'); - done(); - } - }; + req.params = {uuid: 'abc-1234-03'}; + res.redirect = function (status, url) { + res.render.called.should.be.false; + status.should.eql(301); + url.should.eql('/getting-started/'); + done(); + }; frontend.preview(req, res, failTest(done)); });