0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Merge pull request #6469 from ErisDS/dynamic-channel-routing

Dynamic channel routing
This commit is contained in:
Sebastian Gierlinger 2016-02-15 19:27:41 +01:00
commit 3c5c5ad9d0
12 changed files with 1180 additions and 821 deletions

View file

@ -1,8 +1,8 @@
var _ = require('lodash'),
config = require('../../config'),
getConfig;
channelConfig;
getConfig = function getConfig(name) {
channelConfig = function channelConfig() {
var defaults = {
index: {
name: 'index',
@ -22,7 +22,8 @@ getConfig = function getConfig(name) {
options: {slug: '%s'}
}
},
slugTemplate: true
slugTemplate: true,
editRedirect: '/ghost/settings/tags/:slug/'
},
author: {
name: 'author',
@ -37,11 +38,18 @@ getConfig = function getConfig(name) {
options: {slug: '%s'}
}
},
slugTemplate: true
slugTemplate: true,
editRedirect: '/ghost/team/:slug/'
}
};
return _.cloneDeep(defaults[name]);
return defaults;
};
module.exports = getConfig;
module.exports.list = function list() {
return channelConfig();
};
module.exports.get = function get(name) {
return _.cloneDeep(channelConfig()[name]);
};

View file

@ -0,0 +1,93 @@
var express = require('express'),
_ = require('lodash'),
config = require('../../config'),
errors = require('../../errors'),
rss = require('../../data/xml/rss'),
utils = require('../../utils'),
channelConfig = require('./channel-config'),
renderChannel = require('./render-channel'),
rssRouter,
channelRouter;
function handlePageParam(req, res, next, page) {
var pageRegex = new RegExp('/' + config.routeKeywords.page + '/(.*)?/'),
rssRegex = new RegExp('/rss/(.*)?/');
page = parseInt(page, 10);
if (page === 1) {
// Page 1 is an alias, do a permanent 301 redirect
if (rssRegex.test(req.url)) {
return utils.redirect301(res, req.originalUrl.replace(rssRegex, '/rss/'));
} else {
return utils.redirect301(res, req.originalUrl.replace(pageRegex, '/'));
}
} else if (page < 1 || isNaN(page)) {
// Nothing less than 1 is a valid page number, go straight to a 404
return next(new errors.NotFoundError());
} else {
// Set req.params.page to the already parsed number, and continue
req.params.page = page;
return next();
}
}
rssRouter = function rssRouter(channelConfig) {
function rssConfigMiddleware(req, res, next) {
req.channelConfig.isRSS = true;
next();
}
// @TODO move this to an RSS module
var router = express.Router({mergeParams: true}),
stack = [channelConfig, rssConfigMiddleware, rss],
baseRoute = '/rss/';
router.get(baseRoute, stack);
router.get(baseRoute + ':page/', stack);
router.get('/feed/', function redirectToRSS(req, res) {
return utils.redirect301(res, config.paths.subdir + req.baseUrl + baseRoute);
});
router.param('page', handlePageParam);
return router;
};
channelRouter = function router() {
function channelConfigMiddleware(channel) {
return function doChannelConfig(req, res, next) {
req.channelConfig = _.cloneDeep(channel);
next();
};
}
var channelsRouter = express.Router({mergeParams: true}),
baseRoute = '/',
pageRoute = '/' + config.routeKeywords.page + '/:page/';
_.each(channelConfig.list(), function (channel) {
var channelRouter = express.Router({mergeParams: true}),
configChannel = channelConfigMiddleware(channel);
// @TODO figure out how to collapse this into a single rule
channelRouter.get(baseRoute, configChannel, renderChannel);
channelRouter.get(pageRoute, configChannel, renderChannel);
channelRouter.param('page', handlePageParam);
channelRouter.use(rssRouter(configChannel));
if (channel.editRedirect) {
channelRouter.get('/edit/', function redirect(req, res) {
res.redirect(config.paths.subdir + channel.editRedirect.replace(':slug', req.params.slug));
});
}
// Mount this channel router on the parent channels router
channelsRouter.use(channel.route, channelRouter);
});
return channelsRouter;
};
module.exports.router = channelRouter;

View file

@ -6,21 +6,17 @@
var _ = require('lodash'),
api = require('../../api'),
rss = require('../../data/xml/rss'),
path = require('path'),
config = require('../../config'),
errors = require('../../errors'),
filters = require('../../filters'),
Promise = require('bluebird'),
templates = require('./templates'),
templates = require('./templates'),
routeMatch = require('path-match')(),
safeString = require('../../utils/index').safeString,
handleError = require('./error'),
fetchData = require('./fetch-data'),
formatResponse = require('./format-response'),
channelConfig = require('./channel-config'),
setResponseContext = require('./context'),
setRequestIsSecure = require('./secure'),
setRequestIsSecure = require('./secure'),
frontendControllers,
staticPostPermalink = routeMatch('/:slug/:edit?');
@ -41,69 +37,7 @@ function renderPost(req, res) {
};
}
function renderChannel(name) {
return function renderChannel(req, res, next) {
// Parse the parameters we need from the URL
var channelOpts = channelConfig(name),
pageParam = req.params.page !== undefined ? req.params.page : 1,
slugParam = req.params.slug ? safeString(req.params.slug) : undefined;
// Ensure we at least have an empty object for postOptions
channelOpts.postOptions = channelOpts.postOptions || {};
// Set page on postOptions for the query made later
channelOpts.postOptions.page = pageParam;
channelOpts.slugParam = slugParam;
// Call fetchData to get everything we need from the API
return fetchData(channelOpts).then(function handleResult(result) {
// If page is greater than number of pages we have, go straight to 404
if (pageParam > result.meta.pagination.pages) {
return next(new errors.NotFoundError());
}
// @TODO: figure out if this can be removed, it's supposed to ensure that absolutely URLs get generated
// correctly for the various objects, but I believe it doesn't work and a different approach is needed.
setRequestIsSecure(req, result.posts);
_.each(result.data, function (data) {
setRequestIsSecure(req, data);
});
// @TODO: properly design these filters
filters.doFilter('prePostsRender', result.posts, res.locals).then(function then(posts) {
var view = templates.channel(req.app.get('activeTheme'), channelOpts);
// Do final data formatting and then render
result.posts = posts;
result = formatResponse.channel(result);
setResponseContext(req, res);
res.render(view, result);
});
}).catch(handleError(next));
};
}
frontendControllers = {
index: renderChannel('index'),
tag: renderChannel('tag'),
author: renderChannel('author'),
rss: function (req, res, next) {
// Temporary hack, channels will allow us to resolve this better eventually
var tagPattern = new RegExp('^\\/' + config.routeKeywords.tag + '\\/.+'),
authorPattern = new RegExp('^\\/' + config.routeKeywords.author + '\\/.+');
if (tagPattern.test(res.locals.relativeUrl)) {
req.channelConfig = channelConfig('tag');
} else if (authorPattern.test(res.locals.relativeUrl)) {
req.channelConfig = channelConfig('author');
} else {
req.channelConfig = channelConfig('index');
}
req.channelConfig.isRSS = true;
return rss(req, res, next);
},
preview: function preview(req, res, next) {
var params = {
uuid: req.params.uuid,

View file

@ -0,0 +1,51 @@
var _ = require('lodash'),
errors = require('../../errors'),
filters = require('../../filters'),
safeString = require('../../utils/index').safeString,
handleError = require('./error'),
fetchData = require('./fetch-data'),
formatResponse = require('./format-response'),
setResponseContext = require('./context'),
setRequestIsSecure = require('./secure'),
templates = require('./templates');
function renderChannel(req, res, next) {
// Parse the parameters we need from the URL
var channelOpts = req.channelConfig,
pageParam = req.params.page !== undefined ? req.params.page : 1,
slugParam = req.params.slug ? safeString(req.params.slug) : undefined;
// Ensure we at least have an empty object for postOptions
channelOpts.postOptions = channelOpts.postOptions || {};
// Set page on postOptions for the query made later
channelOpts.postOptions.page = pageParam;
channelOpts.slugParam = slugParam;
// Call fetchData to get everything we need from the API
return fetchData(channelOpts).then(function handleResult(result) {
// If page is greater than number of pages we have, go straight to 404
if (pageParam > result.meta.pagination.pages) {
return next(new errors.NotFoundError());
}
// @TODO: figure out if this can be removed, it's supposed to ensure that absolutely URLs get generated
// correctly for the various objects, but I believe it doesn't work and a different approach is needed.
setRequestIsSecure(req, result.posts);
_.each(result.data, function (data) {
setRequestIsSecure(req, data);
});
// @TODO: properly design these filters
filters.doFilter('prePostsRender', result.posts, res.locals).then(function then(posts) {
var view = templates.channel(req.app.get('activeTheme'), channelOpts);
// Do final data formatting and then render
result.posts = posts;
result = formatResponse.channel(result);
setResponseContext(req, res);
res.render(view, result);
});
}).catch(handleError(next));
}
module.exports = renderChannel;

View file

@ -1,6 +1,6 @@
var frontend = require('../controllers/frontend'),
channels = require('../controllers/frontend/channels'),
config = require('../config'),
errors = require('../errors'),
express = require('express'),
utils = require('../utils'),
@ -10,52 +10,19 @@ frontendRoutes = function frontendRoutes(middleware) {
var router = express.Router(),
subdir = config.paths.subdir,
routeKeywords = config.routeKeywords,
indexRouter = express.Router(),
tagRouter = express.Router({mergeParams: true}),
authorRouter = express.Router({mergeParams: true}),
rssRouter = express.Router({mergeParams: true}),
privateRouter = express.Router();
function redirect301(res, path) {
/*jslint unparam:true*/
res.set({'Cache-Control': 'public, max-age=' + utils.ONE_YEAR_S});
res.redirect(301, path);
}
function handlePageParam(req, res, next, page) {
var pageRegex = new RegExp('/' + routeKeywords.page + '/(.*)?/'),
rssRegex = new RegExp('/rss/(.*)?/');
page = parseInt(page, 10);
if (page === 1) {
// Page 1 is an alias, do a permanent 301 redirect
if (rssRegex.test(req.url)) {
return redirect301(res, req.originalUrl.replace(rssRegex, '/rss/'));
} else {
return redirect301(res, req.originalUrl.replace(pageRegex, '/'));
}
} else if (page < 1 || isNaN(page)) {
// Nothing less than 1 is a valid page number, go straight to a 404
return next(new errors.NotFoundError());
} else {
// Set req.params.page to the already parsed number, and continue
req.params.page = page;
return next();
}
}
// ### Admin routes
router.get(/^\/(logout|signout)\/$/, function redirectToSignout(req, res) {
redirect301(res, subdir + '/ghost/signout/');
utils.redirect301(res, subdir + '/ghost/signout/');
});
router.get(/^\/signup\/$/, function redirectToSignup(req, res) {
redirect301(res, subdir + '/ghost/signup/');
utils.redirect301(res, subdir + '/ghost/signup/');
});
// redirect to /ghost and let that do the authentication to prevent redirects to /ghost//admin etc.
router.get(/^\/((ghost-admin|admin|wp-admin|dashboard|signin|login)\/?)$/, function redirectToAdmin(req, res) {
redirect301(res, subdir + '/ghost/');
utils.redirect301(res, subdir + '/ghost/');
});
// password-protected frontend route
@ -71,46 +38,15 @@ frontendRoutes = function frontendRoutes(middleware) {
frontend.private
);
rssRouter.route('/rss/').get(frontend.rss);
rssRouter.route('/rss/:page/').get(frontend.rss);
rssRouter.route('/feed/').get(function redirect(req, res) {
redirect301(res, subdir + '/rss/');
});
rssRouter.param('page', handlePageParam);
// Index
indexRouter.route('/').get(frontend.index);
indexRouter.route('/' + routeKeywords.page + '/:page/').get(frontend.index);
indexRouter.param('page', handlePageParam);
indexRouter.use(rssRouter);
// Tags
tagRouter.route('/').get(frontend.tag);
tagRouter.route('/' + routeKeywords.page + '/:page/').get(frontend.tag);
tagRouter.route('/edit?').get(function redirect(req, res) {
res.redirect(subdir + '/ghost/settings/tags/' + req.params.slug + '/');
});
tagRouter.param('page', handlePageParam);
tagRouter.use(rssRouter);
// Authors
authorRouter.route('/').get(frontend.author);
authorRouter.route('/edit?').get(function redirect(req, res) {
res.redirect(subdir + '/ghost/team/' + req.params.slug + '/');
});
authorRouter.route('/' + routeKeywords.page + '/:page/').get(frontend.author);
authorRouter.param('page', handlePageParam);
authorRouter.use(rssRouter);
// Mount the Routers
router.use('/' + routeKeywords.private + '/', privateRouter);
router.use('/' + routeKeywords.author + '/:slug/', authorRouter);
router.use('/' + routeKeywords.tag + '/:slug/', tagRouter);
router.use('/', indexRouter);
// Post Live Preview
router.get('/' + routeKeywords.preview + '/:uuid', frontend.preview);
// Private
router.use('/' + routeKeywords.private + '/', privateRouter);
// Channels
router.use(channels.router());
// Default
router.get('*', frontend.single);

View file

@ -94,6 +94,11 @@ utils = {
base64String += '=';
}
return base64String;
},
redirect301: function redirect301(res, path) {
/*jslint unparam:true*/
res.set({'Cache-Control': 'public, max-age=' + utils.ONE_YEAR_S});
res.redirect(301, path);
}
};

View file

@ -0,0 +1,554 @@
/*global describe, it, before, after */
// # Channel Route Tests
// As it stands, these tests depend on the database, and as such are integration tests.
// These tests are here to cover the headers sent with requests and high-level redirects that can't be
// tested with the unit tests
var request = require('supertest'),
should = require('should'),
cheerio = require('cheerio'),
testUtils = require('../../utils'),
ghost = require('../../../../core');
describe('Channel Routes', function () {
function doEnd(done) {
return function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
should.not.exist(res.headers['X-CSRF-Token']);
should.not.exist(res.headers['set-cookie']);
should.exist(res.headers.date);
done();
};
}
before(function (done) {
ghost().then(function (ghostServer) {
// Setup the request object with the ghost express app
request = request(ghostServer.rootApp);
done();
}).catch(function (e) {
console.log('Ghost Error: ', e);
console.log(e.stack);
});
});
after(testUtils.teardown);
describe('Index', function () {
it('should respond with html', function (done) {
request.get('/')
.expect('Content-Type', /html/)
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
var $ = cheerio.load(res.text);
should.not.exist(res.headers['x-cache-invalidate']);
should.not.exist(res.headers['X-CSRF-Token']);
should.not.exist(res.headers['set-cookie']);
should.exist(res.headers.date);
$('title').text().should.equal('Ghost');
$('.content .post').length.should.equal(1);
$('.poweredby').text().should.equal('Proudly published with Ghost');
$('body.home-template').length.should.equal(1);
$('article.post').length.should.equal(1);
$('article.tag-getting-started').length.should.equal(1);
done();
});
});
it('should not have as second page', function (done) {
request.get('/page/2/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
describe('RSS', function () {
before(function (done) {
testUtils.initData().then(function () {
return testUtils.fixtures.overrideOwnerUser();
}).then(function () {
done();
});
});
after(testUtils.teardown);
it('should 301 redirect with CC=1year without slash', function (done) {
request.get('/rss')
.expect('Location', '/rss/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should respond with 200 & CC=public', function (done) {
request.get('/rss/')
.expect('Content-Type', 'text/xml; charset=utf-8')
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
should.not.exist(res.headers['X-CSRF-Token']);
should.not.exist(res.headers['set-cookie']);
should.exist(res.headers.date);
// The remainder of the XML is tested in the unit/xml_spec.js
res.text.should.match(/^<\?xml version="1.0" encoding="UTF-8"\?><rss/);
done();
});
});
it('should get 301 redirect with CC=1year to /rss/ from /feed/', function (done) {
request.get('/feed/')
.expect('Location', '/rss/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
});
describe('Paged', function () {
// Add enough posts to trigger pages for both the index (5 pp) and rss (15 pp)
// insertPosts adds 5 published posts, 1 draft post, 1 published static page and one draft page
// we then insert with max 11 which ensures we have 16 published posts
before(function (done) {
testUtils.initData().then(function () {
return testUtils.fixtures.insertPosts();
}).then(function () {
return testUtils.fixtures.insertMorePosts(11);
}).then(function () {
done();
}).catch(done);
});
after(testUtils.teardown);
it('should redirect without slash', function (done) {
request.get('/page/2')
.expect('Location', '/page/2/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should respond with html', function (done) {
request.get('/page/2/')
.expect('Content-Type', /html/)
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(200)
.end(doEnd(done));
});
it('should redirect page 1', function (done) {
request.get('/page/1/')
.expect('Location', '/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should 404 if page too high', function (done) {
request.get('/page/5/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 if page is zero', function (done) {
request.get('/page/0/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 if page is less than zero', function (done) {
request.get('/page/-5/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 if page is NaN', function (done) {
request.get('/page/one/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
describe('RSS', function () {
it('should redirect without slash', function (done) {
request.get('/rss/2')
.expect('Location', '/rss/2/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should respond with xml', function (done) {
request.get('/rss/2/')
.expect('Content-Type', /xml/)
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(200)
.end(doEnd(done));
});
});
});
});
describe('Tag', function () {
before(function (done) {
testUtils.clearData().then(function () {
// we initialise data, but not a user. No user should be required for navigating the frontend
return testUtils.initData();
}).then(function () {
return testUtils.fixtures.overrideOwnerUser('ghost-owner');
}).then(function () {
done();
}).catch(done);
});
it('should 404 for /tag/ route', function (done) {
request.get('/tag/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 for unknown tag', function (done) {
request.get('/tag/spectacular/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 for unknown tag with invalid characters', function (done) {
request.get('/tag/~$pectacular~/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
describe('RSS', function () {
it('should redirect without slash', function (done) {
request.get('/tag/getting-started/rss')
.expect('Location', '/tag/getting-started/rss/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should respond with xml', function (done) {
request.get('/tag/getting-started/rss/')
.expect('Content-Type', /xml/)
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(200)
.end(doEnd(done));
});
});
describe('Paged', function () {
// Add enough posts to trigger pages
before(function (done) {
testUtils.initData().then(function () {
return testUtils.fixtures.insertPosts();
}).then(function () {
return testUtils.fixtures.insertMorePosts(22);
}).then(function () {
return testUtils.fixtures.insertMorePostsTags(22);
}).then(function () {
done();
}).catch(done);
});
after(testUtils.teardown);
it('should redirect without slash', function (done) {
request.get('/tag/injection/page/2')
.expect('Location', '/tag/injection/page/2/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should respond with html', function (done) {
request.get('/tag/injection/page/2/')
.expect('Content-Type', /html/)
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(200)
.end(doEnd(done));
});
it('should redirect page 1', function (done) {
request.get('/tag/injection/page/1/')
.expect('Location', '/tag/injection/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should 404 if page too high', function (done) {
request.get('/tag/injection/page/4/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 if page too low', function (done) {
request.get('/tag/injection/page/0/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
describe('RSS', function () {
it('should redirect page 1', function (done) {
request.get('/tag/getting-started/rss/1/')
.expect('Location', '/tag/getting-started/rss/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should 404 if page too high', function (done) {
request.get('/tag/getting-started/rss/2/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 if page too low', function (done) {
request.get('/tag/getting-started/rss/0/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
});
});
describe('Edit', function () {
it('should redirect without slash', function (done) {
request.get('/tag/getting-started/edit')
.expect('Location', '/tag/getting-started/edit/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should redirect to tag settings', function (done) {
request.get('/tag/getting-started/edit/')
.expect('Location', '/ghost/settings/tags/getting-started/')
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(302)
.end(doEnd(done));
});
it('should 404 for non-edit parameter', function (done) {
request.get('/tag/getting-started/notedit/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
});
});
describe('Author', function () {
before(function (done) {
testUtils.clearData().then(function () {
// we initialise data, but not a user. No user should be required for navigating the frontend
return testUtils.initData();
}).then(function () {
return testUtils.fixtures.overrideOwnerUser('ghost-owner');
}).then(function () {
done();
}).catch(done);
});
after(testUtils.teardown);
it('should 404 for /author/ route', function (done) {
request.get('/author/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 for unknown author', function (done) {
request.get('/author/spectacular/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 for unknown author with invalid characters', function (done) {
request.get('/author/ghost!user^/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
describe('RSS', function () {
it('should redirect without slash', function (done) {
request.get('/author/ghost-owner/rss')
.expect('Location', '/author/ghost-owner/rss/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should respond with xml', function (done) {
request.get('/author/ghost-owner/rss/')
.expect('Content-Type', /xml/)
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(200)
.end(doEnd(done));
});
});
describe('Paged', function () {
// Add enough posts to trigger pages
before(function (done) {
testUtils.clearData().then(function () {
// we initialise data, but not a user. No user should be required for navigating the frontend
return testUtils.initData();
}).then(function () {
return testUtils.fixtures.overrideOwnerUser('ghost-owner');
}).then(function () {
return testUtils.fixtures.insertPosts();
}).then(function () {
return testUtils.fixtures.insertMorePosts(9);
}).then(function () {
done();
}).catch(done);
});
after(testUtils.teardown);
it('should redirect without slash', function (done) {
request.get('/author/ghost-owner/page/2')
.expect('Location', '/author/ghost-owner/page/2/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should respond with html', function (done) {
request.get('/author/ghost-owner/page/2/')
.expect('Content-Type', /html/)
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(200)
.end(doEnd(done));
});
it('should redirect page 1', function (done) {
request.get('/author/ghost-owner/page/1/')
.expect('Location', '/author/ghost-owner/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should 404 if page too high', function (done) {
request.get('/author/ghost-owner/page/4/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 if page too low', function (done) {
request.get('/author/ghost-owner/page/0/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
describe('RSS', function () {
it('should redirect page 1', function (done) {
request.get('/author/ghost-owner/rss/1/')
.expect('Location', '/author/ghost-owner/rss/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should 404 if page too high', function (done) {
request.get('/author/ghost-owner/rss/2/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 if page too low', function (done) {
request.get('/author/ghost-owner/rss/0/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
});
});
describe('Edit', function () {
it('should redirect without slash', function (done) {
request.get('/author/ghost-owner/edit')
.expect('Location', '/author/ghost-owner/edit/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should redirect to editor', function (done) {
request.get('/author/ghost-owner/edit/')
.expect('Location', '/ghost/team/ghost-owner/')
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(302)
.end(doEnd(done));
});
it('should 404 for something that isn\'t edit', function (done) {
request.get('/author/ghost-owner/notedit/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
});
});
});

View file

@ -79,30 +79,6 @@ describe('Frontend Routing', function () {
.end(doEnd(done));
});
it('should 404 for unknown tag', function (done) {
request.get('/tag/spectacular/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 for unknown tag with invalid characters', function (done) {
request.get('/tag/~$pectacular~/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 for unknown author', function (done) {
request.get('/author/spectacular/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 for encoded char not 301 from uncapitalise', function (done) {
request.get('/|/')
.expect('Cache-Control', testUtils.cacheRules.private)
@ -110,52 +86,6 @@ describe('Frontend Routing', function () {
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 for unknown author with invalid characters', function (done) {
request.get('/author/ghost!user^/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
});
describe('Home', function () {
it('should respond with html', function (done) {
request.get('/')
.expect('Content-Type', /html/)
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
var $ = cheerio.load(res.text);
should.not.exist(res.headers['x-cache-invalidate']);
should.not.exist(res.headers['X-CSRF-Token']);
should.not.exist(res.headers['set-cookie']);
should.exist(res.headers.date);
$('title').text().should.equal('Ghost');
$('.content .post').length.should.equal(1);
$('.poweredby').text().should.equal('Proudly published with Ghost');
$('body.home-template').length.should.equal(1);
$('article.post').length.should.equal(1);
$('article.tag-getting-started').length.should.equal(1);
done();
});
});
it('should not have as second page', function (done) {
request.get('/page/2/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
});
describe('Single post', function () {
@ -365,427 +295,6 @@ describe('Frontend Routing', function () {
});
});
describe('Archive pages', function () {
// Add enough posts to trigger pages for both the archive (5 pp) and rss (15 pp)
// insertPosts adds 5 published posts, 1 draft post, 1 published static page and one draft page
// we then insert with max 11 which ensures we have 16 published posts
before(function (done) {
testUtils.initData().then(function () {
return testUtils.fixtures.insertPosts();
}).then(function () {
return testUtils.fixtures.insertMorePosts(9);
}).then(function () {
done();
}).catch(done);
});
after(testUtils.teardown);
it('should redirect without slash', function (done) {
request.get('/page/2')
.expect('Location', '/page/2/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should respond with html', function (done) {
request.get('/page/2/')
.expect('Content-Type', /html/)
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(200)
.end(doEnd(done));
});
it('should redirect page 1', function (done) {
request.get('/page/1/')
.expect('Location', '/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should 404 if page too high', function (done) {
request.get('/page/4/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 if page is zero', function (done) {
request.get('/page/0/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 if page is less than zero', function (done) {
request.get('/page/-5/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 if page is NaN', function (done) {
request.get('/page/one/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
});
describe('RSS', function () {
/**
* These tests are here to cover the headers sent with requests
* and high-level redirects that can't be tested with the unit
* tests in unit/rss_spec.js
*/
before(function (done) {
testUtils.initData().then(function () {
return testUtils.fixtures.overrideOwnerUser();
}).then(function () {
done();
});
});
after(testUtils.teardown);
it('should 301 redirect with CC=1year without slash', function (done) {
request.get('/rss')
.expect('Location', '/rss/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should respond with 200 & CC=public', function (done) {
request.get('/rss/')
.expect('Content-Type', 'text/xml; charset=utf-8')
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
should.not.exist(res.headers['X-CSRF-Token']);
should.not.exist(res.headers['set-cookie']);
should.exist(res.headers.date);
// The remainder of the XML is tested in the unit/xml_spec.js
res.text.should.match(/^<\?xml version="1.0" encoding="UTF-8"\?><rss/);
done();
});
});
it('should get 301 redirect with CC=1year to /rss/ from /feed/', function (done) {
request.get('/feed/')
.expect('Location', '/rss/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
describe('RSS pages', function () {
before(function (done) {
testUtils.fixtures.insertPosts().then(function () {
return testUtils.fixtures.insertMorePosts(11);
}).then(function () {
done();
}).catch(done);
});
it('should redirect without slash', function (done) {
request.get('/rss/2')
.expect('Location', '/rss/2/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should respond with xml', function (done) {
request.get('/rss/2/')
.expect('Content-Type', /xml/)
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(200)
.end(doEnd(done));
});
});
});
describe('Author pages', function () {
// Add enough posts to trigger tag pages
before(function (done) {
testUtils.clearData().then(function () {
// we initialise data, but not a user. No user should be required for navigating the frontend
return testUtils.initData();
}).then(function () {
return testUtils.fixtures.overrideOwnerUser('ghost-owner');
}).then(function () {
return testUtils.fixtures.insertPosts();
}).then(function () {
return testUtils.fixtures.insertMorePosts(9);
}).then(function () {
done();
}).catch(done);
});
after(testUtils.teardown);
it('should 404 for /author/ route', function (done) {
request.get('/author/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should redirect without slash', function (done) {
request.get('/author/ghost-owner/page/2')
.expect('Location', '/author/ghost-owner/page/2/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should respond with html', function (done) {
request.get('/author/ghost-owner/page/2/')
.expect('Content-Type', /html/)
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(200)
.end(doEnd(done));
});
it('should redirect page 1', function (done) {
request.get('/author/ghost-owner/page/1/')
.expect('Location', '/author/ghost-owner/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should 404 if page too high', function (done) {
request.get('/author/ghost-owner/page/4/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 if page too low', function (done) {
request.get('/author/ghost-owner/page/0/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
describe('Author based RSS pages', function () {
it('should redirect without slash', function (done) {
request.get('/author/ghost-owner/rss')
.expect('Location', '/author/ghost-owner/rss/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should respond with xml', function (done) {
request.get('/author/ghost-owner/rss/')
.expect('Content-Type', /xml/)
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(200)
.end(doEnd(done));
});
it('should redirect page 1', function (done) {
request.get('/author/ghost-owner/rss/1/')
.expect('Location', '/author/ghost-owner/rss/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should 404 if page too high', function (done) {
request.get('/author/ghost-owner/rss/2/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 if page too low', function (done) {
request.get('/author/ghost-owner/rss/0/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
});
describe('Author edit', function () {
it('should redirect without slash', function (done) {
request.get('/author/ghost-owner/edit')
.expect('Location', '/author/ghost-owner/edit/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should redirect to editor', function (done) {
request.get('/author/ghost-owner/edit/')
.expect('Location', '/ghost/team/ghost-owner/')
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(302)
.end(doEnd(done));
});
it('should 404 for something that isn\'t edit', function (done) {
request.get('/author/ghost-owner/notedit/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
});
});
describe('Tag pages', function () {
// Add enough posts to trigger tag pages
before(function (done) {
testUtils.initData().then(function () {
return testUtils.fixtures.insertPosts();
}).then(function () {
return testUtils.fixtures.insertMorePosts(22);
}).then(function () {
return testUtils.fixtures.insertMorePostsTags(22);
}).then(function () {
done();
}).catch(done);
});
after(testUtils.teardown);
it('should 404 for /tag/ route', function (done) {
request.get('/tag/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should redirect without slash', function (done) {
request.get('/tag/injection/page/2')
.expect('Location', '/tag/injection/page/2/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should respond with html', function (done) {
request.get('/tag/injection/page/2/')
.expect('Content-Type', /html/)
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(200)
.end(doEnd(done));
});
it('should redirect page 1', function (done) {
request.get('/tag/injection/page/1/')
.expect('Location', '/tag/injection/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should 404 if page too high', function (done) {
request.get('/tag/injection/page/4/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 if page too low', function (done) {
request.get('/tag/injection/page/0/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
describe('Tag based RSS pages', function () {
it('should redirect without slash', function (done) {
request.get('/tag/getting-started/rss')
.expect('Location', '/tag/getting-started/rss/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should respond with xml', function (done) {
request.get('/tag/getting-started/rss/')
.expect('Content-Type', /xml/)
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(200)
.end(doEnd(done));
});
it('should redirect page 1', function (done) {
request.get('/tag/getting-started/rss/1/')
.expect('Location', '/tag/getting-started/rss/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should 404 if page too high', function (done) {
request.get('/tag/getting-started/rss/2/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
it('should 404 if page too low', function (done) {
request.get('/tag/getting-started/rss/0/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
});
});
describe('Tag edit', function () {
it('should redirect without slash', function (done) {
request.get('/tag/getting-started/edit')
.expect('Location', '/tag/getting-started/edit/')
.expect('Cache-Control', testUtils.cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should redirect to tag settings', function (done) {
request.get('/tag/getting-started/edit/')
.expect('Location', '/ghost/settings/tags/getting-started/')
.expect('Cache-Control', testUtils.cacheRules.public)
.expect(302)
.end(doEnd(done));
});
it('should 404 for non-edit parameter', function (done) {
request.get('/tag/getting-started/notedit/')
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.expect(/Page not found/)
.end(doEnd(done));
});
});
describe('Subdirectory (no slash)', function () {
var forkedGhost, request;
before(function (done) {

View file

@ -0,0 +1,421 @@
/*globals describe, before, beforeEach, afterEach, it*/
var should = require('should'),
sinon = require('sinon'),
Promise = require('bluebird'),
_ = require('lodash'),
// Stuff we are testing
channels = require('../../../../server/controllers/frontend/channels'),
api = require('../../../../server/api'),
configUtils = require('../../../utils/configUtils'),
sandbox = sinon.sandbox.create();
describe('Channels', function () {
var channelRouter, req, res;
// Initialise 'req' with the bare minimum properties
function setupRequest() {
req = {
method: 'get',
app: {
get: sandbox.stub().returns('casper')
}
};
}
// Prevent unit tests from failing via timeout when they should just immediately fail
function failTest(done) {
return function (err) {
done(err || 'Next was called with no Error');
};
}
// Run a test which should result in a render
function testChannelRender(props, assertions, done) {
res = {
redirect: sandbox.spy()
};
res.render = function (view) {
assertions(view);
res.redirect.called.should.be.false();
done();
};
_.extend(req, props);
channelRouter(req, res, failTest(done));
}
// Run a test which should result in a redirect
function testChannelRedirect(props, assertions, done) {
res = {
render: sandbox.spy(),
set: sandbox.spy()
};
res.redirect = function (status, path) {
assertions(status, path);
res.render.called.should.be.false();
done();
};
_.extend(req, props);
channelRouter(req, res, failTest(done));
}
// Run a test which should result in next() being called
function testChannelNext(props, assertions, done) {
res = {
redirect: sandbox.spy(),
render: sandbox.spy(),
set: sandbox.spy()
};
_.extend(req, props);
channelRouter(req, res, function (empty) {
assertions(empty);
res.redirect.called.should.be.false();
res.render.called.should.be.false();
done();
});
}
// Run a test which results in next being called with an error
function testChannelError(props, assertions, done) {
testChannelNext(props, function (error) {
should.exist(error);
assertions(error);
}, done);
}
// Run a test which results in an explicit 404
function testChannel404(props, done) {
testChannelError(props, function (error) {
error.errorType.should.eql('NotFoundError');
error.code.should.eql(404);
}, done);
}
before(function () {
// We don't overwrite this, so only do it once
channelRouter = channels.router();
});
afterEach(function () {
configUtils.restore();
sandbox.restore();
});
describe('Index', function () {
var postAPIStub;
// Stub the posts api
function setupPostsAPIStub() {
postAPIStub = sandbox.stub(api.posts, 'browse', function () {
return Promise.resolve({posts: [{}], meta: {pagination: {pages: 3}}});
});
}
// Return basic paths for the activeTheme
function setupActiveTheme() {
configUtils.set({paths: {availableThemes: {casper: {
'index.hbs': '/content/themes/casper/index.hbs'
}}}});
}
beforeEach(function () {
// Setup Env for tests
setupPostsAPIStub();
setupActiveTheme();
setupRequest();
});
it('should render the first page of the index channel', function (done) {
testChannelRender({url: '/'}, function (view) {
should.exist(view);
view.should.eql('index');
postAPIStub.calledOnce.should.be.true();
}, done);
});
it('should render the first page of the index channel using home.hbs if available', function (done) {
configUtils.set({paths: {availableThemes: {casper: {
'index.hbs': '/content/themes/casper/index.hbs',
'home.hbs': '/content/themes/casper/home.hbs'
}}}});
testChannelRender({url: '/'}, function (view) {
should.exist(view);
view.should.eql('home');
postAPIStub.calledOnce.should.be.true();
}, done);
});
describe('Paged', function () {
it('should render the second page of the index channel', function (done) {
testChannelRender({url: '/page/2/'}, function (view) {
should.exist(view);
view.should.eql('index');
postAPIStub.calledOnce.should.be.true();
}, done);
});
it('should use index.hbs for second page even if home.hbs is available', function (done) {
configUtils.set({paths: {availableThemes: {casper: {
'index.hbs': '/content/themes/casper/index.hbs',
'home.hbs': '/content/themes/casper/home.hbs'
}}}});
testChannelRender({url: '/page/2/'}, function (view) {
should.exist(view);
view.should.eql('index');
postAPIStub.calledOnce.should.be.true();
}, done);
});
it('should render the third page of the index channel', function (done) {
testChannelRender({url: '/page/3/'}, function (view) {
should.exist(view);
view.should.eql('index');
postAPIStub.calledOnce.should.be.true();
}, done);
});
it('should redirect /page/1/ to /', function (done) {
testChannelRedirect({url: '/page/1/'}, function (status, path) {
status.should.eql(301);
path.should.eql('/');
res.set.called.should.be.true();
postAPIStub.called.should.be.false();
}, done);
});
it('should 404 for /page/0/', function (done) {
testChannel404({url: '/page/0/'}, done);
});
it('should 404 for /page/4/', function (done) {
testChannel404({url: '/page/4/'}, done);
});
});
describe('RSS', function () {
it('should redirect /feed/ to /rss/', function (done) {
testChannelRedirect({url: '/feed/'}, function (status, path) {
status.should.eql(301);
path.should.eql('/rss/');
res.set.called.should.be.true();
postAPIStub.called.should.be.false();
}, done);
});
it('should redirect /rss/1/ to /rss/', function (done) {
testChannelRedirect({url: '/rss/1/'}, function (status, path) {
status.should.eql(301);
path.should.eql('/rss/');
res.set.called.should.be.true();
postAPIStub.called.should.be.false();
}, done);
});
it('should 404 for /rss/0/', function (done) {
testChannel404({url: '/rss/0/'}, done);
});
it('should 404 for /rss/4/', function (done) {
testChannel404({url: '/rss/4/'}, done);
});
});
describe('Edit', function () {
it('should NOT redirect /edit/, should pass through', function (done) {
testChannelNext({url: '/edit/'}, done);
});
});
});
describe('Tag', function () {
var postAPIStub, tagAPIStub;
// Stub the posts and tags api
function setupAPIStubs() {
postAPIStub = sandbox.stub(api.posts, 'browse', function () {
return Promise.resolve({posts: [{}], meta: {pagination: {pages: 3}}});
});
tagAPIStub = sandbox.stub(api.tags, 'read', function () {
return Promise.resolve({tags: [{}]});
});
}
// Return basic paths for the activeTheme
function setupActiveTheme() {
configUtils.set({paths: {availableThemes: {casper: {
'index.hbs': '/content/themes/casper/index.hbs'
}}}});
}
beforeEach(function () {
// Setup Env for tests
setupAPIStubs();
setupActiveTheme();
setupRequest();
});
it('should render the first page of the tag channel using index.hbs by default', function (done) {
testChannelRender({url: '/tag/my-tag/'}, function (view) {
should.exist(view);
view.should.eql('index');
postAPIStub.calledOnce.should.be.true();
tagAPIStub.calledOnce.should.be.true();
}, done);
});
it('should render the first page of the tag channel using tag.hbs by default', function (done) {
configUtils.set({paths: {availableThemes: {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);
view.should.eql('tag');
postAPIStub.calledOnce.should.be.true();
tagAPIStub.calledOnce.should.be.true();
}, done);
});
it('should render the first page of the tag channel using tag-:slug.hbs if available', function (done) {
configUtils.set({paths: {availableThemes: {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);
view.should.eql('tag-my-tag');
postAPIStub.calledOnce.should.be.true();
tagAPIStub.calledOnce.should.be.true();
}, done);
});
describe('Paged', function () {
it('should render the second page of the tag channel', function (done) {
testChannelRender({url: '/tag/my-tag/page/2/'}, function (view) {
should.exist(view);
view.should.eql('index');
postAPIStub.calledOnce.should.be.true();
}, done);
});
it('should use tag.hbs to render the tag channel if available', function (done) {
configUtils.set({paths: {availableThemes: {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);
view.should.eql('tag');
postAPIStub.calledOnce.should.be.true();
}, done);
});
it('should use tag-:slug.hbs to render the tag channel if available', function (done) {
configUtils.set({paths: {availableThemes: {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);
view.should.eql('tag-my-tag');
postAPIStub.calledOnce.should.be.true();
}, done);
});
it('should render the second page of the tag channel', function (done) {
testChannelRender({url: '/tag/my-tag/page/2/'}, function (view) {
should.exist(view);
view.should.eql('index');
postAPIStub.calledOnce.should.be.true();
}, done);
});
it('should render the third page of the tag channel', function (done) {
testChannelRender({url: '/tag/my-tag/page/3/'}, function (view) {
should.exist(view);
view.should.eql('index');
postAPIStub.calledOnce.should.be.true();
}, done);
});
it('should redirect /tag/my-tag/page/1/ to /tag/my-tag/', function (done) {
testChannelRedirect({url: '/tag/my-tag/page/1/'}, function (status, path) {
status.should.eql(301);
path.should.eql('/tag/my-tag/');
res.set.called.should.be.true();
postAPIStub.called.should.be.false();
}, done);
});
it('should 404 for /tag/my-tag/page/0/', function (done) {
testChannel404({url: '/tag/my-tag/page/0/'}, done);
});
it('should 404 for /tag/my-tag/page/4/', function (done) {
testChannel404({url: '/tag/my-tag/page/4/'}, done);
});
});
describe('RSS', function () {
it('should redirect /tag/my-tag/feed/ to /tag/my-tag/rss/', function (done) {
testChannelRedirect({url: '/tag/my-tag/feed/'}, function (status, path) {
status.should.eql(301);
path.should.eql('/tag/my-tag/rss/');
res.set.called.should.be.true();
postAPIStub.called.should.be.false();
}, done);
});
it('should redirect /tag/my-tag/rss/1/ to /tag/my-tag/rss/', function (done) {
testChannelRedirect({url: '/tag/my-tag/rss/1/'}, function (status, path) {
status.should.eql(301);
path.should.eql('/tag/my-tag/rss/');
res.set.called.should.be.true();
postAPIStub.called.should.be.false();
}, done);
});
it('should 404 for /tag/my-tag/rss/0/', function (done) {
testChannel404({url: '/tag/my-tag/rss/0/'}, done);
});
it('should 404 for /tag/my-tag/rss/4/', function (done) {
testChannel404({url: '/tag/my-tag/rss/4/'}, done);
});
});
describe('Edit', function () {
it('should redirect /edit/ to ghost admin', function (done) {
testChannelRedirect({url: '/tag/my-tag/edit/'}, function (path) {
path.should.eql('/ghost/settings/tags/my-tag/');
postAPIStub.called.should.be.false();
}, done);
});
});
});
});

View file

@ -34,177 +34,6 @@ describe('Frontend Controller', function () {
};
}
describe('index', function () {
var req, res;
beforeEach(function () {
sandbox.stub(api.posts, 'browse', function () {
return Promise.resolve({
posts: [],
meta: {
pagination: {
page: 1,
pages: 3
}
}
});
});
configUtils.set({
theme: {
permalinks: '/:slug/',
postsPerPage: 10
}
});
req = {
app: {get: function () { return 'casper';}},
path: '/', params: {}, route: {}
};
res = {
locals: {}
};
});
it('Renders home.hbs template when it exists in the active theme', function (done) {
configUtils.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.index(req, res, failTest(done));
});
it('Renders index.hbs template on 2nd page when home.hbs exists', function (done) {
configUtils.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.index(req, res, failTest(done));
});
it('Renders index.hbs template when home.hbs doesn\'t exist', function (done) {
configUtils.set({paths: {availableThemes: {casper: {
'index.hbs': '/content/themes/casper/index.hbs'
}}}});
res.render = function (view) {
view.should.equal('index');
done();
};
frontend.index(req, res, failTest(done));
});
});
describe('tag', function () {
var req, res,
mockTags = [{
name: 'video',
slug: 'video',
id: 1
}, {
name: 'audio',
slug: 'audio',
id: 2
}];
beforeEach(function () {
sandbox.stub(api.posts, 'browse').returns(new Promise.resolve({
posts: [{}],
meta: {pagination: {page: 1, pages: 1}}
}));
sandbox.stub(api.tags, 'read').returns(new Promise.resolve({tags: [mockTags[0]]}));
configUtils.set({
theme: {
permalinks: '/tag/:slug/',
postsPerPage: '10'
}
});
req = {
app: {get: function () { return 'casper';}},
path: '/', params: {}, route: {}
};
res = {
locals: {}
};
});
it('it will render custom tag-slug template if it exists', function (done) {
configUtils.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'
}}}});
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.eql(mockTags[0]);
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) {
configUtils.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.eql(mockTags[0]);
done();
};
frontend.tag(req, res, failTest(done));
});
it('it will fall back to index if there are no custom templates', function (done) {
configUtils.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.eql(mockTags[0]);
done();
};
frontend.tag(req, res, failTest(done));
});
});
describe('single', function () {
var req, res, casper, mockPosts = [{
posts: [{

View file

@ -104,7 +104,7 @@ describe('RSS', function () {
done();
};
req.channelConfig = channelConfig('index');
req.channelConfig = channelConfig.get('index');
req.channelConfig.isRSS = true;
rss(req, res, failTest(done));
});
@ -146,7 +146,7 @@ describe('RSS', function () {
done();
};
req.channelConfig = channelConfig('index');
req.channelConfig = channelConfig.get('index');
req.channelConfig.isRSS = true;
rss(req, res, failTest(done));
});
@ -175,7 +175,7 @@ describe('RSS', function () {
done();
};
req.channelConfig = channelConfig('index');
req.channelConfig = channelConfig.get('index');
req.channelConfig.isRSS = true;
rss(req, res, failTest(done));
});
@ -209,7 +209,7 @@ describe('RSS', function () {
done();
};
req.channelConfig = channelConfig('index');
req.channelConfig = channelConfig.get('index');
req.channelConfig.isRSS = true;
rss(req, res, failTest(done));
});
@ -241,7 +241,7 @@ describe('RSS', function () {
done();
};
req.channelConfig = channelConfig('index');
req.channelConfig = channelConfig.get('index');
req.channelConfig.isRSS = true;
rss(req, res, failTest(done));
});
@ -289,7 +289,7 @@ describe('RSS', function () {
done();
};
req.channelConfig = channelConfig('index');
req.channelConfig = channelConfig.get('index');
req.channelConfig.isRSS = true;
rss(req, res, failTest(done));
});
@ -298,7 +298,7 @@ describe('RSS', function () {
// setup
req.originalUrl = '/tag/magic/rss/';
req.params.slug = 'magic';
req.channelConfig = channelConfig('tag');
req.channelConfig = channelConfig.get('tag');
req.channelConfig.isRSS = true;
// test
@ -316,7 +316,7 @@ describe('RSS', function () {
it('should process the data correctly for an author feed', function (done) {
req.originalUrl = '/author/joe/rss/';
req.params.slug = 'joe';
req.channelConfig = channelConfig('author');
req.channelConfig = channelConfig.get('author');
req.channelConfig.isRSS = true;
// test
@ -359,7 +359,7 @@ describe('RSS', function () {
results: {posts: [], meta: {pagination: {pages: 1}}}
});
});
req.channelConfig = channelConfig('index');
req.channelConfig = channelConfig.get('index');
req.channelConfig.isRSS = true;
function secondCall() {
@ -412,7 +412,7 @@ describe('RSS', function () {
req = {params: {page: 4}, route: {path: '/rss/:page/'}};
req.originalUrl = req.route.path.replace(':page', req.params.page);
req.channelConfig = channelConfig('index');
req.channelConfig = channelConfig.get('index');
req.channelConfig.isRSS = true;
rss(req, res, function (err) {
@ -429,7 +429,7 @@ describe('RSS', function () {
req = {params: {page: 4}, route: {path: '/rss/:page/'}};
req.originalUrl = req.route.path.replace(':page', req.params.page);
req.channelConfig = channelConfig('index');
req.channelConfig = channelConfig.get('index');
req.channelConfig.isRSS = true;
rss(req, res, function (err) {

View file

@ -1,6 +1,7 @@
/*globals describe, it*/
/*jshint expr:true*/
var should = require('should'),
sinon = require('sinon'),
parsePackageJson = require('../../server/utils/parse-package-json'),
validateThemes = require('../../server/utils/validate-themes'),
readDirectory = require('../../server/utils/read-directory'),
@ -455,4 +456,22 @@ describe('Server Utilities', function () {
});
});
});
describe('redirect301', function () {
it('performs a 301 correctly', function (done) {
var res = {};
res.set = sinon.spy();
res.redirect = function (code, path) {
code.should.equal(301);
path.should.eql('my/awesome/path');
res.set.calledWith({'Cache-Control': 'public, max-age=' + utils.ONE_YEAR_S}).should.be.true();
done();
};
utils.redirect301(res, 'my/awesome/path');
});
});
});