Split RSS module into controller + other (#9224)

refs #5091, refs #9192

- This is similar to #9218, in that I'm revealing bits of code that are "controllers" in our codebase. As opposed to routes, services, renderers etc.
- This also reveals some code which is identical to the channels controller
- There is more to do here, but for now I've got the module split up, and the tests split and improved.
- Next I'll split RSS into controller + service, DRY up the controller code, etc
This commit is contained in:
Hannah Wolfe 2017-11-07 20:00:03 +00:00 committed by GitHub
parent f06270acb5
commit 60fd98679f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 812 additions and 739 deletions

View File

@ -10,18 +10,16 @@ var _ = require('lodash'),
module.exports = function channelController(req, res, next) {
// Parse the parameters we need from the URL
var channelOpts = res.locals.channel,
pageParam = req.params.page !== undefined ? req.params.page : 1,
var 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 || {};
// @TODO: fix this, we shouldn't change the channel object!
// Set page on postOptions for the query made later
channelOpts.postOptions.page = pageParam;
channelOpts.slugParam = slugParam;
res.locals.channel.postOptions.page = pageParam;
res.locals.channel.slugParam = slugParam;
// Call fetchData to get everything we need from the API
return fetchData(channelOpts).then(function handleResult(result) {
return fetchData(res.locals.channel).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({message: i18n.t('errors.errors.pageNotFound')}));

View File

@ -0,0 +1,16 @@
var crypto = require('crypto'),
generateFeed = require('./generate-feed'),
feedCache = {};
module.exports.getXML = function getFeedXml(path, data) {
var dataHash = crypto.createHash('md5').update(JSON.stringify(data)).digest('hex');
if (!feedCache[path] || feedCache[path].hash !== dataHash) {
// We need to regenerate
feedCache[path] = {
hash: dataHash,
xml: generateFeed(data)
};
}
return feedCache[path].xml;
};

View File

@ -0,0 +1,83 @@
var _ = require('lodash'),
url = require('url'),
utils = require('../../../utils'),
errors = require('../../../errors'),
i18n = require('../../../i18n'),
safeString = require('../../../utils/index').safeString,
settingsCache = require('../../../settings/cache'),
// Really ugly temporary hack for location of things
fetchData = require('../../../controllers/frontend/fetch-data'),
handleError = require('../../../controllers/frontend/error'),
feedCache = require('./cache'),
generate;
// @TODO: is this the right logic? Where should this live?!
function getBaseUrlForRSSReq(originalUrl, pageParam) {
return url.parse(originalUrl).pathname.replace(new RegExp('/' + pageParam + '/$'), '/');
}
// @TODO: is this really correct? Should we be using meta data title?
function getTitle(relatedData) {
relatedData = relatedData || {};
var titleStart = _.get(relatedData, 'author[0].name') || _.get(relatedData, 'tag[0].name') || '';
titleStart += titleStart ? ' - ' : '';
return titleStart + settingsCache.get('title');
}
// @TODO: merge this with the rest of the data processing for RSS
// @TODO: swap the fetchData call + duplicate code from channels with something DRY
function getData(channelOpts) {
channelOpts.data = channelOpts.data || {};
return fetchData(channelOpts).then(function formatResult(result) {
var response = {};
response.title = getTitle(result.data);
response.description = settingsCache.get('description');
response.results = {
posts: result.posts,
meta: result.meta
};
return response;
});
}
// @TODO finish refactoring this - it's now a controller
generate = function generate(req, res, next) {
// Parse the parameters we need from the URL
var pageParam = req.params.page !== undefined ? req.params.page : 1,
slugParam = req.params.slug ? safeString(req.params.slug) : undefined;
// @TODO: fix this, we shouldn't change the channel object!
// Set page on postOptions for the query made later
res.locals.channel.postOptions.page = pageParam;
res.locals.channel.slugParam = slugParam;
return getData(res.locals.channel).then(function handleResult(data) {
// Base URL needs to be the URL for the feed without pagination:
var baseUrl = getBaseUrlForRSSReq(req.originalUrl, pageParam),
maxPage = data.results.meta.pagination.pages;
// If page is greater than number of pages we have, redirect to last page
if (pageParam > maxPage) {
return next(new errors.NotFoundError({message: i18n.t('errors.errors.pageNotFound')}));
}
data.version = res.locals.safeVersion;
data.siteUrl = utils.url.urlFor('home', {secure: req.secure}, true);
data.feedUrl = utils.url.urlFor({relativeUrl: baseUrl, secure: req.secure}, true);
data.secure = req.secure;
// @TODO this is effectively a renderer
return feedCache.getXML(baseUrl, data).then(function then(feedXml) {
res.set('Content-Type', 'text/xml; charset=UTF-8');
res.send(feedXml);
});
}).catch(handleError(next));
};
module.exports = generate;

View File

@ -0,0 +1,87 @@
var downsize = require('downsize'),
RSS = require('rss'),
utils = require('../../../utils'),
filters = require('../../../filters'),
processUrls = require('../../../utils/make-absolute-urls'),
generateFeed,
generateTags;
generateTags = function generateTags(data) {
if (data.tags) {
return data.tags.reduce(function (tags, tag) {
if (tag.visibility !== 'internal') {
tags.push(tag.name);
}
return tags;
}, []);
}
return [];
};
generateFeed = function generateFeed(data) {
var feed = new RSS({
title: data.title,
description: data.description,
generator: 'Ghost ' + data.version,
feed_url: data.feedUrl,
site_url: data.siteUrl,
image_url: utils.url.urlFor({relativeUrl: 'favicon.png'}, true),
ttl: '60',
custom_namespaces: {
content: 'http://purl.org/rss/1.0/modules/content/',
media: 'http://search.yahoo.com/mrss/'
}
});
data.results.posts.forEach(function forEach(post) {
var itemUrl = utils.url.urlFor('post', {post: post, secure: data.secure}, true),
htmlContent = processUrls(post.html, data.siteUrl, itemUrl),
item = {
title: post.title,
description: post.custom_excerpt || post.meta_description || downsize(htmlContent.html(), {words: 50}),
guid: post.id,
url: itemUrl,
date: post.published_at,
categories: generateTags(post),
author: post.author ? post.author.name : null,
custom_elements: []
},
imageUrl;
if (post.feature_image) {
imageUrl = utils.url.urlFor('image', {image: post.feature_image, secure: data.secure}, true);
// Add a media content tag
item.custom_elements.push({
'media:content': {
_attr: {
url: imageUrl,
medium: 'image'
}
}
});
// Also add the image to the content, because not all readers support media:content
htmlContent('p').first().before('<img src="' + imageUrl + '" />');
htmlContent('img').attr('alt', post.title);
}
item.custom_elements.push({
'content:encoded': {
_cdata: htmlContent.html()
}
});
filters.doFilter('rss.item', item, post).then(function then(item) {
feed.item(item);
});
});
return filters.doFilter('rss.feed', feed).then(function then(feed) {
return feed.xml();
});
};
module.exports = generateFeed;

View File

@ -1,173 +1 @@
var crypto = require('crypto'),
downsize = require('downsize'),
RSS = require('rss'),
url = require('url'),
utils = require('../../../utils'),
errors = require('../../../errors'),
i18n = require('../../../i18n'),
filters = require('../../../filters'),
processUrls = require('../../../utils/make-absolute-urls'),
settingsCache = require('../../../settings/cache'),
// Really ugly temporary hack for location of things
fetchData = require('../../../controllers/frontend/fetch-data'),
generate,
generateFeed,
generateTags,
getFeedXml,
feedCache = {};
function handleError(next) {
return function handleError(err) {
return next(err);
};
}
function getData(channelOpts, slugParam) {
channelOpts.data = channelOpts.data || {};
return fetchData(channelOpts, slugParam).then(function (result) {
var response = {},
titleStart = '';
if (result.data && result.data.tag) { titleStart = result.data.tag[0].name + ' - ' || ''; }
if (result.data && result.data.author) { titleStart = result.data.author[0].name + ' - ' || ''; }
response.title = titleStart + settingsCache.get('title');
response.description = settingsCache.get('description');
response.results = {
posts: result.posts,
meta: result.meta
};
return response;
});
}
getFeedXml = function getFeedXml(path, data) {
var dataHash = crypto.createHash('md5').update(JSON.stringify(data)).digest('hex');
if (!feedCache[path] || feedCache[path].hash !== dataHash) {
// We need to regenerate
feedCache[path] = {
hash: dataHash,
xml: generateFeed(data)
};
}
return feedCache[path].xml;
};
generateTags = function generateTags(data) {
if (data.tags) {
return data.tags.reduce(function (tags, tag) {
if (tag.visibility !== 'internal') {
tags.push(tag.name);
}
return tags;
}, []);
}
return [];
};
generateFeed = function generateFeed(data) {
var feed = new RSS({
title: data.title,
description: data.description,
generator: 'Ghost ' + data.version,
feed_url: data.feedUrl,
site_url: data.siteUrl,
image_url: utils.url.urlFor({relativeUrl: 'favicon.png'}, true),
ttl: '60',
custom_namespaces: {
content: 'http://purl.org/rss/1.0/modules/content/',
media: 'http://search.yahoo.com/mrss/'
}
});
data.results.posts.forEach(function forEach(post) {
var itemUrl = utils.url.urlFor('post', {post: post, secure: data.secure}, true),
htmlContent = processUrls(post.html, data.siteUrl, itemUrl),
item = {
title: post.title,
description: post.custom_excerpt || post.meta_description || downsize(htmlContent.html(), {words: 50}),
guid: post.id,
url: itemUrl,
date: post.published_at,
categories: generateTags(post),
author: post.author ? post.author.name : null,
custom_elements: []
},
imageUrl;
if (post.feature_image) {
imageUrl = utils.url.urlFor('image', {image: post.feature_image, secure: data.secure}, true);
// Add a media content tag
item.custom_elements.push({
'media:content': {
_attr: {
url: imageUrl,
medium: 'image'
}
}
});
// Also add the image to the content, because not all readers support media:content
htmlContent('p').first().before('<img src="' + imageUrl + '" />');
htmlContent('img').attr('alt', post.title);
}
item.custom_elements.push({
'content:encoded': {
_cdata: htmlContent.html()
}
});
filters.doFilter('rss.item', item, post).then(function then(item) {
feed.item(item);
});
});
return filters.doFilter('rss.feed', feed).then(function then(feed) {
return feed.xml();
});
};
generate = function generate(req, res, next) {
// Initialize RSS
var pageParam = req.params.page !== undefined ? req.params.page : 1,
slugParam = req.params.slug,
// Base URL needs to be the URL for the feed without pagination:
baseUrl = url.parse(req.originalUrl).pathname.replace(new RegExp('/' + pageParam + '/$'), '/'),
channelConfig = res.locals.channel;
// Ensure we at least have an empty object for postOptions
channelConfig.postOptions = channelConfig.postOptions || {};
// Set page on postOptions for the query made later
channelConfig.postOptions.page = pageParam;
channelConfig.slugParam = slugParam;
return getData(channelConfig).then(function then(data) {
var maxPage = data.results.meta.pagination.pages;
// If page is greater than number of pages we have, redirect to last page
if (pageParam > maxPage) {
return next(new errors.NotFoundError({message: i18n.t('errors.errors.pageNotFound')}));
}
data.version = res.locals.safeVersion;
data.siteUrl = utils.url.urlFor('home', {secure: req.secure}, true);
data.feedUrl = utils.url.urlFor({relativeUrl: baseUrl, secure: req.secure}, true);
data.secure = req.secure;
return getFeedXml(baseUrl, data).then(function then(feedXml) {
res.set('Content-Type', 'text/xml; charset=UTF-8');
res.send(feedXml);
});
}).catch(handleError(next));
};
module.exports = generate;
module.exports = require('./controller');

View File

@ -0,0 +1,57 @@
var should = require('should'),
sinon = require('sinon'),
rewire = require('rewire'),
configUtils = require('../../utils/configUtils'),
rssCache = rewire('../../../server/data/xml/rss/cache'),
sandbox = sinon.sandbox.create();
describe('RSS: Cache', function () {
var generateSpy, generateFeedReset;
afterEach(function () {
configUtils.restore();
sandbox.restore();
generateFeedReset();
});
beforeEach(function () {
configUtils.set({url: 'http://my-ghost-blog.com'});
generateSpy = sandbox.spy(rssCache.__get__('generateFeed'));
generateFeedReset = rssCache.__set__('generateFeed', generateSpy);
});
it('should not rebuild xml for same data and url', function (done) {
var xmlData1,
data = {
title: 'Test Title',
description: 'Testing Desc',
permalinks: '/:slug/',
results: {posts: [], meta: {pagination: {pages: 1}}}
};
rssCache.getXML('/rss/', data)
.then(function (_xmlData) {
xmlData1 = _xmlData;
// We should have called generateFeed
generateSpy.callCount.should.eql(1);
// Call RSS again to check that we didn't rebuild
return rssCache.getXML('/rss/', data);
})
.then(function (xmlData2) {
// Assertions
// We should not have called generateFeed again
generateSpy.callCount.should.eql(1);
// The data should be identical, no changing lastBuildDate
xmlData1.should.equal(xmlData2);
done();
})
.catch(done);
});
});

View File

@ -0,0 +1,361 @@
var should = require('should'),
sinon = require('sinon'),
rewire = require('rewire'),
_ = require('lodash'),
Promise = require('bluebird'),
testUtils = require('../../utils'),
channelUtils = require('../../utils/channelUtils'),
api = require('../../../server/api'),
settingsCache = require('../../../server/settings/cache'),
rssController = rewire('../../../server/data/xml/rss/controller'),
rssCache = require('../../../server/data/xml/rss/cache'),
configUtils = require('../../utils/configUtils'),
sandbox = sinon.sandbox.create();
// Helper function to prevent unit tests
// from failing via timeout when they
// should just immediately fail
function failTest(done) {
return function (err) {
done(err);
};
}
describe('RSS', function () {
describe('RSS: Controller only', function () {
var req, res, posts, getDataStub, resetGetData, rssCacheStub;
before(function () {
posts = _.cloneDeep(testUtils.DataGenerator.forKnex.posts);
posts = _.filter(posts, function filter(post) {
return post.status === 'published' && post.page === false;
});
_.each(posts, function (post, i) {
post.id = i;
post.url = '/' + post.slug + '/';
post.author = {name: 'Joe Bloggs'};
});
});
beforeEach(function () {
// Minimum setup of req and res
req = {
params: {},
originalUrl: '/rss/'
};
res = {
locals: {
safeVersion: '0.6',
channel: channelUtils.getTestChannel('index')
},
set: sinon.stub(),
send: sinon.spy()
};
// @TODO Get rid of this! - shouldn't be set on the channel
res.locals.channel.isRSS = true;
// Overwrite getData
getDataStub = sandbox.stub();
resetGetData = rssController.__set__('getData', getDataStub);
rssCacheStub = sandbox.stub(rssCache, 'getXML').returns(new Promise.resolve('dummyxml'));
});
afterEach(function () {
sandbox.restore();
configUtils.restore();
resetGetData();
});
it('should fetch data and attempt to send XML', function (done) {
getDataStub.returns(new Promise.resolve({
results: {meta: {pagination: {pages: 3}}}
}));
res.send = function (result) {
result.should.eql('dummyxml');
res.set.calledOnce.should.be.true();
res.set.calledWith('Content-Type', 'text/xml; charset=UTF-8').should.be.true();
getDataStub.calledOnce.should.be.true();
rssCacheStub.calledOnce.should.be.true();
rssCacheStub.calledWith('/rss/').should.be.true();
done();
};
rssController(req, res, failTest(done));
});
it('can handle paginated urls', function (done) {
getDataStub.returns(new Promise.resolve({
results: {meta: {pagination: {pages: 3}}}
}));
req.originalUrl = '/rss/2/';
req.params.page = 2;
res.send = function (result) {
result.should.eql('dummyxml');
res.set.calledOnce.should.be.true();
res.set.calledWith('Content-Type', 'text/xml; charset=UTF-8').should.be.true();
getDataStub.calledOnce.should.be.true();
rssCacheStub.calledOnce.should.be.true();
rssCacheStub.calledWith('/rss/').should.be.true();
done();
};
rssController(req, res, failTest(done));
});
it('can handle paginated urls with subdirectories', function (done) {
getDataStub.returns(new Promise.resolve({
results: {meta: {pagination: {pages: 3}}}
}));
req.originalUrl = '/blog/rss/2/';
req.params.page = 2;
res.send = function (result) {
result.should.eql('dummyxml');
res.set.calledOnce.should.be.true();
res.set.calledWith('Content-Type', 'text/xml; charset=UTF-8').should.be.true();
getDataStub.calledOnce.should.be.true();
rssCacheStub.calledOnce.should.be.true();
rssCacheStub.calledWith('/blog/rss/').should.be.true();
done();
};
rssController(req, res, failTest(done));
});
it('can handle paginated urls for channels', function (done) {
getDataStub.returns(new Promise.resolve({
results: {meta: {pagination: {pages: 3}}}
}));
req.originalUrl = '/tags/test/rss/2/';
req.params.page = 2;
req.params.slug = 'test';
res.send = function (result) {
result.should.eql('dummyxml');
res.set.calledOnce.should.be.true();
res.set.calledWith('Content-Type', 'text/xml; charset=UTF-8').should.be.true();
getDataStub.calledOnce.should.be.true();
rssCacheStub.calledOnce.should.be.true();
rssCacheStub.calledWith('/tags/test/rss/').should.be.true();
done();
};
rssController(req, res, failTest(done));
});
it('should call next with 404 if page number too big', function (done) {
getDataStub.returns(new Promise.resolve({
results: {meta: {pagination: {pages: 3}}}
}));
req.originalUrl = '/rss/4/';
req.params.page = 4;
rssController(req, res, function (err) {
should.exist(err);
err.statusCode.should.eql(404);
res.send.called.should.be.false();
done();
});
});
});
// These tests check the RSS feed from controller to result
// @TODO: test only the data generation, once we've refactored to make that easier
describe('RSS: data generation', function () {
var apiBrowseStub, apiTagStub, apiUserStub, req, res;
beforeEach(function () {
apiBrowseStub = sandbox.stub(api.posts, 'browse', function () {
return Promise.resolve({posts: [], meta: {pagination: {pages: 3}}});
});
apiTagStub = sandbox.stub(api.tags, 'read', function () {
return Promise.resolve({tags: [{name: 'Magic'}]});
});
apiUserStub = sandbox.stub(api.users, 'read', function () {
return Promise.resolve({users: [{name: 'Joe Blogs'}]});
});
req = {
params: {},
originalUrl: '/rss/'
};
res = {
locals: {
safeVersion: '0.6'
},
set: sinon.stub()
};
sandbox.stub(settingsCache, 'get', function (key) {
var obj = {
title: 'Test',
description: 'Some Text',
permalinks: '/:slug/'
};
return obj[key];
});
configUtils.set({
url: 'http://my-ghost-blog.com'
});
});
afterEach(function () {
sandbox.restore();
configUtils.restore();
});
it('should process the data correctly for the index feed', function (done) {
// setup
req.originalUrl = '/rss/';
res.locals.channel = channelUtils.getTestChannel('index');
res.locals.channel.isRSS = true;
// test
res.send = function send(xmlData) {
apiBrowseStub.calledOnce.should.be.true();
apiBrowseStub.calledWith({
page: 1,
include: 'author,tags'
}).should.be.true();
apiTagStub.called.should.be.false();
apiUserStub.called.should.be.false();
xmlData.should.match(/<channel><title><!\[CDATA\[Test\]\]><\/title>/);
xmlData.should.match(/<atom:link href="http:\/\/my-ghost-blog.com\/rss\/" rel="self" type="application\/rss\+xml"\/>/);
done();
};
rssController(req, res, failTest(done));
});
it('should process the data correctly for the paginated index feed', function (done) {
// setup
req.originalUrl = '/rss/2/';
req.params.page = '2';
res.locals.channel = channelUtils.getTestChannel('index');
res.locals.channel.isRSS = true;
// test
res.send = function send(xmlData) {
apiBrowseStub.calledOnce.should.be.true();
apiBrowseStub.calledWith({
page: '2',
include: 'author,tags'
}).should.be.true();
apiTagStub.called.should.be.false();
apiUserStub.called.should.be.false();
xmlData.should.match(/<channel><title><!\[CDATA\[Test\]\]><\/title>/);
xmlData.should.match(/<atom:link href="http:\/\/my-ghost-blog.com\/rss\/" rel="self" type="application\/rss\+xml"\/>/);
done();
};
rssController(req, res, failTest(done));
});
it('should process the data correctly for a tag feed', function (done) {
// setup
req.originalUrl = '/tag/magic/rss/';
req.params.slug = 'magic';
res.locals.channel = channelUtils.getTestChannel('tag');
res.locals.channel.isRSS = true;
// test
res.send = function send(xmlData) {
apiBrowseStub.calledOnce.should.be.true();
apiBrowseStub.calledWith({
page: 1,
filter: 'tags:\'magic\'+tags.visibility:public',
include: 'author,tags'
}).should.be.true();
apiTagStub.calledOnce.should.be.true();
xmlData.should.match(/<channel><title><!\[CDATA\[Magic - Test\]\]><\/title>/);
xmlData.should.match(/<atom:link href="http:\/\/my-ghost-blog.com\/tag\/magic\/rss\/" rel="self" type="application\/rss\+xml"\/>/);
done();
};
rssController(req, res, failTest(done));
});
it('should process the data correctly for a paginated tag feed', function (done) {
// setup
req.originalUrl = '/tag/magic/rss/2/';
req.params.slug = 'magic';
req.params.page = '2';
res.locals.channel = channelUtils.getTestChannel('tag');
res.locals.channel.isRSS = true;
// test
res.send = function send(xmlData) {
apiBrowseStub.calledOnce.should.be.true();
apiBrowseStub.calledWith({
page: '2',
filter: 'tags:\'magic\'+tags.visibility:public',
include: 'author,tags'
}).should.be.true();
apiTagStub.calledOnce.should.be.true();
xmlData.should.match(/<channel><title><!\[CDATA\[Magic - Test\]\]><\/title>/);
xmlData.should.match(/<atom:link href="http:\/\/my-ghost-blog.com\/tag\/magic\/rss\/" rel="self" type="application\/rss\+xml"\/>/);
done();
};
rssController(req, res, failTest(done));
});
it('should process the data correctly for an author feed', function (done) {
req.originalUrl = '/author/joe/rss/';
req.params.slug = 'joe';
res.locals.channel = channelUtils.getTestChannel('author');
res.locals.channel.isRSS = true;
// test
res.send = function send(xmlData) {
apiBrowseStub.calledOnce.should.be.true();
apiBrowseStub.calledWith({page: 1, filter: 'author:\'joe\'', include: 'author,tags'}).should.be.true();
apiUserStub.calledOnce.should.be.true();
xmlData.should.match(/<channel><title><!\[CDATA\[Joe Blogs - Test\]\]><\/title>/);
xmlData.should.match(/<atom:link href="http:\/\/my-ghost-blog.com\/author\/joe\/rss\/" rel="self" type="application\/rss\+xml"\/>/);
done();
};
rssController(req, res, failTest(done));
});
it('should process the data correctly for a paginated author feed', function (done) {
req.originalUrl = '/author/joe/rss/2/';
req.params.slug = 'joe';
req.params.page = '2';
res.locals.channel = channelUtils.getTestChannel('author');
res.locals.channel.isRSS = true;
// test
res.send = function send(xmlData) {
apiBrowseStub.calledOnce.should.be.true();
apiBrowseStub.calledWith({page: '2', filter: 'author:\'joe\'', include: 'author,tags'}).should.be.true();
apiUserStub.calledOnce.should.be.true();
xmlData.should.match(/<channel><title><!\[CDATA\[Joe Blogs - Test\]\]><\/title>/);
xmlData.should.match(/<atom:link href="http:\/\/my-ghost-blog.com\/author\/joe\/rss\/" rel="self" type="application\/rss\+xml"\/>/);
done();
};
rssController(req, res, failTest(done));
});
});
});

View File

@ -0,0 +1,202 @@
var should = require('should'),
_ = require('lodash'),
testUtils = require('../../utils'),
configUtils = require('../../utils/configUtils'),
generateFeed = require('../../../server/data/xml/rss/generate-feed');
describe('RSS: Generate Feed', function () {
var data = {},
// Static set of posts
posts;
before(function () {
posts = _.cloneDeep(testUtils.DataGenerator.forKnex.posts);
posts = _.filter(posts, function filter(post) {
return post.status === 'published' && post.page === false;
});
_.each(posts, function (post, i) {
post.id = i;
post.url = '/' + post.slug + '/';
post.author = {name: 'Joe Bloggs'};
});
});
afterEach(function () {
configUtils.restore();
});
beforeEach(function () {
configUtils.set({url: 'http://my-ghost-blog.com'});
data.version = '0.6';
data.siteUrl = 'http://my-ghost-blog.com/';
data.feedUrl = 'http://my-ghost-blog.com/rss/';
data.title = 'Test Title';
data.description = 'Testing Desc';
data.permalinks = '/:slug/';
});
it('should get the RSS tags correct', function (done) {
data.results = {posts: [], meta: {pagination: {pages: 1}}};
generateFeed(data).then(function (xmlData) {
should.exist(xmlData);
// xml & rss tags
xmlData.should.match(/^<\?xml version="1.0" encoding="UTF-8"\?>/);
xmlData.should.match(/<rss/);
xmlData.should.match(/xmlns:dc="http:\/\/purl.org\/dc\/elements\/1.1\/"/);
xmlData.should.match(/xmlns:content="http:\/\/purl.org\/rss\/1.0\/modules\/content\/"/);
xmlData.should.match(/xmlns:atom="http:\/\/www.w3.org\/2005\/Atom"/);
xmlData.should.match(/version="2.0"/);
xmlData.should.match(/xmlns:media="http:\/\/search.yahoo.com\/mrss\/"/);
// channel tags
xmlData.should.match(/<channel><title><!\[CDATA\[Test Title\]\]><\/title>/);
xmlData.should.match(/<description><!\[CDATA\[Testing Desc\]\]><\/description>/);
xmlData.should.match(/<link>http:\/\/my-ghost-blog.com\/<\/link>/);
xmlData.should.match(/<image><url>http:\/\/my-ghost-blog.com\/favicon.png<\/url><title>Test Title<\/title><link>http:\/\/my-ghost-blog.com\/<\/link><\/image>/);
xmlData.should.match(/<generator>Ghost 0.6<\/generator>/);
xmlData.should.match(/<lastBuildDate>.*?<\/lastBuildDate>/);
xmlData.should.match(/<atom:link href="http:\/\/my-ghost-blog.com\/rss\/" rel="self"/);
xmlData.should.match(/type="application\/rss\+xml"\/><ttl>60<\/ttl>/);
xmlData.should.match(/<\/channel><\/rss>$/);
done();
}).catch(done);
});
it('should get the item tags correct', function (done) {
data.results = {posts: posts, meta: {pagination: {pages: 1}}};
generateFeed(data).then(function (xmlData) {
should.exist(xmlData);
// item tags
xmlData.should.match(/<item><title><!\[CDATA\[HTML Ipsum\]\]><\/title>/);
xmlData.should.match(/<description><!\[CDATA\[<h1>HTML Ipsum Presents<\/h1>/);
xmlData.should.match(/<link>http:\/\/my-ghost-blog.com\/html-ipsum\/<\/link>/);
xmlData.should.match(/<image><url>http:\/\/my-ghost-blog.com\/favicon.png<\/url><title>Test Title<\/title><link>http:\/\/my-ghost-blog.com\/<\/link><\/image>/);
xmlData.should.match(/<guid isPermaLink="false">/);
xmlData.should.match(/<\/guid><dc:creator><!\[CDATA\[Joe Bloggs\]\]><\/dc:creator>/);
xmlData.should.match(/<pubDate>Thu, 01 Jan 2015/);
xmlData.should.match(/<content:encoded><!\[CDATA\[<h1>HTML Ipsum Presents<\/h1><p><strong>Pellentes/);
xmlData.should.match(/<\/code><\/pre>\]\]><\/content:encoded><\/item>/);
xmlData.should.not.match(/<author>/);
// basic structure check
var postEnd = '<\/code><\/pre>\]\]><\/content:encoded>',
firstIndex = xmlData.indexOf(postEnd);
// The first title should be before the first content
xmlData.indexOf('HTML Ipsum').should.be.below(firstIndex);
// The second title should be after the first content
xmlData.indexOf('Ghostly Kitchen Sink').should.be.above(firstIndex);
done();
}).catch(done);
});
it('should only return visible tags', function (done) {
var postWithTags = posts[2];
postWithTags.tags = [
{name: 'public', visibility: 'public'},
{name: 'internal', visibility: 'internal'},
{name: 'visibility'}
];
data.results = {posts: [postWithTags], meta: {pagination: {pages: 1}}};
generateFeed(data).then(function (xmlData) {
should.exist(xmlData);
// item tags
xmlData.should.match(/<title><!\[CDATA\[Short and Sweet\]\]>/);
xmlData.should.match(/<description><!\[CDATA\[test stuff/);
xmlData.should.match(/<content:encoded><!\[CDATA\[<div class="kg-card-markdown"><h2 id="testing">testing<\/h2>\n/);
xmlData.should.match(/<img src="http:\/\/placekitten.com\/500\/200"/);
xmlData.should.match(/<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/);
xmlData.should.match(/<category><!\[CDATA\[public\]\]/);
xmlData.should.match(/<category><!\[CDATA\[visibility\]\]/);
xmlData.should.not.match(/<category><!\[CDATA\[internal\]\]/);
done();
}).catch(done);
});
it('should use meta_description and image where available', function (done) {
data.results = {posts: [posts[2]], meta: {pagination: {pages: 1}}};
generateFeed(data).then(function (xmlData) {
should.exist(xmlData);
// special/optional tags
xmlData.should.match(/<title><!\[CDATA\[Short and Sweet\]\]>/);
xmlData.should.match(/<description><!\[CDATA\[test stuff/);
xmlData.should.match(/<content:encoded><!\[CDATA\[<div class="kg-card-markdown"><h2 id="testing">testing<\/h2>\n/);
xmlData.should.match(/<img src="http:\/\/placekitten.com\/500\/200"/);
xmlData.should.match(/<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/);
done();
}).catch(done);
});
it('should use excerpt when no meta_description is set', function (done) {
data.results = {posts: [posts[0]], meta: {pagination: {pages: 1}}};
generateFeed(data).then(function (xmlData) {
should.exist(xmlData);
// special/optional tags
xmlData.should.match(/<title><!\[CDATA\[HTML Ipsum\]\]>/);
xmlData.should.match(/<description><!\[CDATA\[This is my custom excerpt!/);
done();
}).catch(done);
});
it('should process urls correctly', function (done) {
data.results = {posts: [posts[3]], meta: {pagination: {pages: 1}}};
generateFeed(data).then(function (xmlData) {
should.exist(xmlData);
// anchor URL - <a href="#nowhere" title="Anchor URL">
xmlData.should.match(/<a href="#nowhere" title="Anchor URL">/);
// relative URL - <a href="/about#nowhere" title="Relative URL">
xmlData.should.match(/<a href="http:\/\/my-ghost-blog.com\/about#nowhere" title="Relative URL">/);
// protocol relative URL - <a href="//somewhere.com/link#nowhere" title="Protocol Relative URL">
xmlData.should.match(/<a href="\/\/somewhere.com\/link#nowhere" title="Protocol Relative URL">/);
// absolute URL - <a href="http://somewhere.com/link#nowhere" title="Absolute URL">
xmlData.should.match(/<a href="http:\/\/somewhere.com\/link#nowhere" title="Absolute URL">/);
done();
}).catch(done);
});
it('should process urls correctly with subdirectory', function (done) {
configUtils.set({url: 'http://my-ghost-blog.com/blog/'});
data.siteUrl = 'http://my-ghost-blog.com/blog/';
data.feedUrl = 'http://my-ghost-blog.com/blog/rss/';
data.results = {posts: [posts[3]], meta: {pagination: {pages: 1}}};
generateFeed(data).then(function (xmlData) {
should.exist(xmlData);
// anchor URL - <a href="#nowhere" title="Anchor URL">
xmlData.should.match(/<a href="#nowhere" title="Anchor URL">/);
// relative URL - <a href="/about#nowhere" title="Relative URL">
xmlData.should.match(/<a href="http:\/\/my-ghost-blog.com\/blog\/about#nowhere" title="Relative URL">/);
// absolute URL - <a href="http://somewhere.com/link#nowhere" title="Absolute URL">
xmlData.should.match(/<a href="http:\/\/somewhere.com\/link#nowhere" title="Absolute URL">/);
done();
}).catch(done);
});
});

View File

@ -1,559 +0,0 @@
var should = require('should'),
sinon = require('sinon'),
rewire = require('rewire'),
_ = require('lodash'),
Promise = require('bluebird'),
testUtils = require('../utils'),
channelUtils = require('../utils/channelUtils'),
api = require('../../server/api'),
settingsCache = require('../../server/settings/cache'),
rss = rewire('../../server/data/xml/rss'),
configUtils = require('../utils/configUtils'),
sandbox = sinon.sandbox.create();
// Helper function to prevent unit tests
// from failing via timeout when they
// should just immediately fail
function failTest(done) {
return function (err) {
done(err);
};
}
describe('RSS', function () {
var req, res, posts;
before(function () {
posts = _.cloneDeep(testUtils.DataGenerator.forKnex.posts);
posts = _.filter(posts, function filter(post) {
return post.status === 'published' && post.page === false;
});
_.each(posts, function (post, i) {
post.id = i;
post.url = '/' + post.slug + '/';
post.author = {name: 'Joe Bloggs'};
});
});
afterEach(function () {
sandbox.restore();
rss = rewire('../../server/data/xml/rss');
configUtils.restore();
});
describe('Check XML', function () {
beforeEach(function () {
req = {
params: {},
originalUrl: '/rss/'
};
res = {
locals: {
safeVersion: '0.6'
},
set: sinon.stub()
};
configUtils.set({url: 'http://my-ghost-blog.com'});
});
it('should get the RSS tags correct', function (done) {
rss.__set__('getData', function () {
return Promise.resolve({
title: 'Test Title',
description: 'Testing Desc',
permalinks: '/:slug/',
results: {posts: [], meta: {pagination: {pages: 1}}}
});
});
res.send = function send(xmlData) {
should.exist(xmlData);
res.set.calledWith('Content-Type', 'text/xml; charset=UTF-8').should.be.true();
// xml & rss tags
xmlData.should.match(/^<\?xml version="1.0" encoding="UTF-8"\?>/);
xmlData.should.match(/<rss/);
xmlData.should.match(/xmlns:dc="http:\/\/purl.org\/dc\/elements\/1.1\/"/);
xmlData.should.match(/xmlns:content="http:\/\/purl.org\/rss\/1.0\/modules\/content\/"/);
xmlData.should.match(/xmlns:atom="http:\/\/www.w3.org\/2005\/Atom"/);
xmlData.should.match(/version="2.0"/);
xmlData.should.match(/xmlns:media="http:\/\/search.yahoo.com\/mrss\/"/);
// channel tags
xmlData.should.match(/<channel><title><!\[CDATA\[Test Title\]\]><\/title>/);
xmlData.should.match(/<description><!\[CDATA\[Testing Desc\]\]><\/description>/);
xmlData.should.match(/<link>http:\/\/my-ghost-blog.com\/<\/link>/);
xmlData.should.match(/<image><url>http:\/\/my-ghost-blog.com\/favicon.png<\/url><title>Test Title<\/title><link>http:\/\/my-ghost-blog.com\/<\/link><\/image>/);
xmlData.should.match(/<generator>Ghost 0.6<\/generator>/);
xmlData.should.match(/<lastBuildDate>.*?<\/lastBuildDate>/);
xmlData.should.match(/<atom:link href="http:\/\/my-ghost-blog.com\/rss\/" rel="self"/);
xmlData.should.match(/type="application\/rss\+xml"\/><ttl>60<\/ttl>/);
xmlData.should.match(/<\/channel><\/rss>$/);
done();
};
res.locals.channel = channelUtils.getTestChannel('index');
res.locals.channel.isRSS = true;
rss(req, res, failTest(done));
});
it('should get the item tags correct', function (done) {
rss.__set__('getData', function () {
return Promise.resolve({
title: 'Test Title',
description: 'Testing Desc',
permalinks: '/:slug/',
results: {posts: posts, meta: {pagination: {pages: 1}}}
});
});
res.send = function send(xmlData) {
should.exist(xmlData);
// item tags
xmlData.should.match(/<item><title><!\[CDATA\[HTML Ipsum\]\]><\/title>/);
xmlData.should.match(/<description><!\[CDATA\[<h1>HTML Ipsum Presents<\/h1>/);
xmlData.should.match(/<link>http:\/\/my-ghost-blog.com\/html-ipsum\/<\/link>/);
xmlData.should.match(/<image><url>http:\/\/my-ghost-blog.com\/favicon.png<\/url><title>Test Title<\/title><link>http:\/\/my-ghost-blog.com\/<\/link><\/image>/);
xmlData.should.match(/<guid isPermaLink="false">/);
xmlData.should.match(/<\/guid><dc:creator><!\[CDATA\[Joe Bloggs\]\]><\/dc:creator>/);
xmlData.should.match(/<pubDate>Thu, 01 Jan 2015/);
xmlData.should.match(/<content:encoded><!\[CDATA\[<h1>HTML Ipsum Presents<\/h1><p><strong>Pellentes/);
xmlData.should.match(/<\/code><\/pre>\]\]><\/content:encoded><\/item>/);
xmlData.should.not.match(/<author>/);
// basic structure check
var postEnd = '<\/code><\/pre>\]\]><\/content:encoded>',
firstIndex = xmlData.indexOf(postEnd);
// The first title should be before the first content
xmlData.indexOf('HTML Ipsum').should.be.below(firstIndex);
// The second title should be after the first content
xmlData.indexOf('Ghostly Kitchen Sink').should.be.above(firstIndex);
// done
done();
};
res.locals.channel = channelUtils.getTestChannel('index');
res.locals.channel.isRSS = true;
rss(req, res, failTest(done));
});
it('should only return visible tags', function (done) {
var postWithTags = posts[2];
postWithTags.tags = [
{name: 'public', visibility: 'public'},
{name: 'internal', visibility: 'internal'},
{name: 'visibility'}
];
rss.__set__('getData', function () {
return Promise.resolve({
title: 'Test Title',
description: 'Testing Desc',
permalinks: '/:slug/',
results: {posts: [postWithTags], meta: {pagination: {pages: 1}}}
});
});
res.send = function send(xmlData) {
should.exist(xmlData);
// item tags
xmlData.should.match(/<title><!\[CDATA\[Short and Sweet\]\]>/);
xmlData.should.match(/<description><!\[CDATA\[test stuff/);
xmlData.should.match(/<content:encoded><!\[CDATA\[<div class="kg-card-markdown"><h2 id="testing">testing<\/h2>\n/);
xmlData.should.match(/<img src="http:\/\/placekitten.com\/500\/200"/);
xmlData.should.match(/<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/);
xmlData.should.match(/<category><!\[CDATA\[public\]\]/);
xmlData.should.match(/<category><!\[CDATA\[visibility\]\]/);
xmlData.should.not.match(/<category><!\[CDATA\[internal\]\]/);
done();
};
res.locals.channel = channelUtils.getTestChannel('index');
res.locals.channel.isRSS = true;
rss(req, res, failTest(done));
});
it('should use meta_description and image where available', function (done) {
rss.__set__('getData', function () {
return Promise.resolve({
title: 'Test Title',
description: 'Testing Desc',
permalinks: '/:slug/',
results: {posts: [posts[2]], meta: {pagination: {pages: 1}}}
});
});
res.send = function send(xmlData) {
should.exist(xmlData);
// special/optional tags
xmlData.should.match(/<title><!\[CDATA\[Short and Sweet\]\]>/);
xmlData.should.match(/<description><!\[CDATA\[test stuff/);
xmlData.should.match(/<content:encoded><!\[CDATA\[<div class="kg-card-markdown"><h2 id="testing">testing<\/h2>\n/);
xmlData.should.match(/<img src="http:\/\/placekitten.com\/500\/200"/);
xmlData.should.match(/<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/);
done();
};
res.locals.channel = channelUtils.getTestChannel('index');
res.locals.channel.isRSS = true;
rss(req, res, failTest(done));
});
it('should use excerpt when no meta_description is set', function (done) {
rss.__set__('getData', function () {
return Promise.resolve({
title: 'Test Title',
description: 'Testing Desc',
permalinks: '/:slug/',
results: {posts: [posts[0]], meta: {pagination: {pages: 1}}}
});
});
res.send = function send(xmlData) {
should.exist(xmlData);
// special/optional tags
xmlData.should.match(/<title><!\[CDATA\[HTML Ipsum\]\]>/);
xmlData.should.match(/<description><!\[CDATA\[This is my custom excerpt!/);
done();
};
res.locals.channel = channelUtils.getTestChannel('index');
res.locals.channel.isRSS = true;
rss(req, res, failTest(done));
});
it('should process urls correctly', function (done) {
rss.__set__('getData', function () {
return Promise.resolve({
title: 'Test Title',
description: 'Testing Desc',
permalinks: '/:slug/',
results: {posts: [posts[3]], meta: {pagination: {pages: 1}}}
});
});
res.send = function send(xmlData) {
should.exist(xmlData);
// anchor URL - <a href="#nowhere" title="Anchor URL">
xmlData.should.match(/<a href="#nowhere" title="Anchor URL">/);
// relative URL - <a href="/about#nowhere" title="Relative URL">
xmlData.should.match(/<a href="http:\/\/my-ghost-blog.com\/about#nowhere" title="Relative URL">/);
// protocol relative URL - <a href="//somewhere.com/link#nowhere" title="Protocol Relative URL">
xmlData.should.match(/<a href="\/\/somewhere.com\/link#nowhere" title="Protocol Relative URL">/);
// absolute URL - <a href="http://somewhere.com/link#nowhere" title="Absolute URL">
xmlData.should.match(/<a href="http:\/\/somewhere.com\/link#nowhere" title="Absolute URL">/);
// done
done();
};
res.locals.channel = channelUtils.getTestChannel('index');
res.locals.channel.isRSS = true;
rss(req, res, failTest(done));
});
it('should process urls correctly with subdirectory', function (done) {
configUtils.set({url: 'http://my-ghost-blog.com/blog/'});
rss.__set__('getData', function () {
return Promise.resolve({
title: 'Test Title',
description: 'Testing Desc',
permalinks: '/:slug/',
results: {posts: [posts[3]], meta: {pagination: {pages: 1}}}
});
});
res.send = function send(xmlData) {
should.exist(xmlData);
// anchor URL - <a href="#nowhere" title="Anchor URL">
xmlData.should.match(/<a href="#nowhere" title="Anchor URL">/);
// relative URL - <a href="/about#nowhere" title="Relative URL">
xmlData.should.match(/<a href="http:\/\/my-ghost-blog.com\/blog\/about#nowhere" title="Relative URL">/);
// absolute URL - <a href="http://somewhere.com/link#nowhere" title="Absolute URL">
xmlData.should.match(/<a href="http:\/\/somewhere.com\/link#nowhere" title="Absolute URL">/);
// done
done();
};
res.locals.channel = channelUtils.getTestChannel('index');
res.locals.channel.isRSS = true;
rss(req, res, failTest(done));
});
});
describe('dataBuilder', function () {
var apiBrowseStub, apiTagStub, apiUserStub;
beforeEach(function () {
apiBrowseStub = sandbox.stub(api.posts, 'browse', function () {
return Promise.resolve({posts: [], meta: {pagination: {pages: 3}}});
});
apiTagStub = sandbox.stub(api.tags, 'read', function () {
return Promise.resolve({tags: [{name: 'Magic'}]});
});
apiUserStub = sandbox.stub(api.users, 'read', function () {
return Promise.resolve({users: [{name: 'Joe Blogs'}]});
});
req = {
params: {},
originalUrl: '/rss/'
};
res = {
locals: {
safeVersion: '0.6'
},
set: sinon.stub()
};
sandbox.stub(settingsCache, 'get', function (key) {
var obj = {
title: 'Test',
description: 'Some Text',
permalinks: '/:slug/'
};
return obj[key];
});
configUtils.set({
url: 'http://my-ghost-blog.com'
});
});
it('should process the data correctly for the index feed', function (done) {
res.send = function send(xmlData) {
apiBrowseStub.calledOnce.should.be.true();
apiBrowseStub.calledWith({page: 1, include: 'author,tags'}).should.be.true();
xmlData.should.match(/<channel><title><!\[CDATA\[Test\]\]><\/title>/);
done();
};
res.locals.channel = channelUtils.getTestChannel('index');
res.locals.channel.isRSS = true;
rss(req, res, failTest(done));
});
it('should process the data correctly for a tag feed', function (done) {
// setup
req.originalUrl = '/tag/magic/rss/';
req.params.slug = 'magic';
res.locals.channel = channelUtils.getTestChannel('tag');
res.locals.channel.isRSS = true;
// test
res.send = function send(xmlData) {
apiBrowseStub.calledOnce.should.be.true();
apiBrowseStub.calledWith({
page: 1,
filter: 'tags:\'magic\'+tags.visibility:public',
include: 'author,tags'
}).should.be.true();
apiTagStub.calledOnce.should.be.true();
xmlData.should.match(/<channel><title><!\[CDATA\[Magic - Test\]\]><\/title>/);
xmlData.should.match(/<atom:link href="http:\/\/my-ghost-blog.com\/tag\/magic\/rss\/" rel="self" type="application\/rss\+xml"\/>/);
done();
};
rss(req, res, failTest(done));
});
it('should process the data correctly for a paginated tag feed', function (done) {
// setup
req.originalUrl = '/tag/magic/rss/2/';
req.params.slug = 'magic';
req.params.page = '2';
res.locals.channel = channelUtils.getTestChannel('tag');
res.locals.channel.isRSS = true;
// test
res.send = function send(xmlData) {
apiBrowseStub.calledOnce.should.be.true();
apiBrowseStub.calledWith({
page: '2',
filter: 'tags:\'magic\'+tags.visibility:public',
include: 'author,tags'
}).should.be.true();
apiTagStub.calledOnce.should.be.true();
xmlData.should.match(/<channel><title><!\[CDATA\[Magic - Test\]\]><\/title>/);
xmlData.should.match(/<atom:link href="http:\/\/my-ghost-blog.com\/tag\/magic\/rss\/" rel="self" type="application\/rss\+xml"\/>/);
done();
};
rss(req, res, failTest(done));
});
it('should process the data correctly for an author feed', function (done) {
req.originalUrl = '/author/joe/rss/';
req.params.slug = 'joe';
res.locals.channel = channelUtils.getTestChannel('author');
res.locals.channel.isRSS = true;
// test
res.send = function send(xmlData) {
apiBrowseStub.calledOnce.should.be.true();
apiBrowseStub.calledWith({page: 1, filter: 'author:\'joe\'', include: 'author,tags'}).should.be.true();
apiUserStub.calledOnce.should.be.true();
xmlData.should.match(/<channel><title><!\[CDATA\[Joe Blogs - Test\]\]><\/title>/);
xmlData.should.match(/<atom:link href="http:\/\/my-ghost-blog.com\/author\/joe\/rss\/" rel="self" type="application\/rss\+xml"\/>/);
done();
};
rss(req, res, failTest(done));
});
it('should process the data correctly for a paginated author feed', function (done) {
req.originalUrl = '/author/joe/rss/2/';
req.params.slug = 'joe';
req.params.page = '2';
res.locals.channel = channelUtils.getTestChannel('author');
res.locals.channel.isRSS = true;
// test
res.send = function send(xmlData) {
apiBrowseStub.calledOnce.should.be.true();
apiBrowseStub.calledWith({page: '2', filter: 'author:\'joe\'', include: 'author,tags'}).should.be.true();
apiUserStub.calledOnce.should.be.true();
xmlData.should.match(/<channel><title><!\[CDATA\[Joe Blogs - Test\]\]><\/title>/);
xmlData.should.match(/<atom:link href="http:\/\/my-ghost-blog.com\/author\/joe\/rss\/" rel="self" type="application\/rss\+xml"\/>/);
done();
};
rss(req, res, failTest(done));
});
});
describe('caching', function () {
beforeEach(function () {
req = {
params: {},
originalUrl: '/rss/'
};
res = {
locals: {
safeVersion: '0.6'
},
set: sinon.stub()
};
configUtils.set({url: 'http://my-ghost-blog.com'});
});
it('should not rebuild xml for same data and url', function (done) {
var xmlData;
rss.__set__('getData', function () {
return Promise.resolve({
title: 'Test Title',
description: 'Testing Desc',
permalinks: '/:slug/',
results: {posts: [], meta: {pagination: {pages: 1}}}
});
});
res.locals.channel = channelUtils.getTestChannel('index');
res.locals.channel.isRSS = true;
function secondCall() {
res.send = function sendFirst(data) {
// The data should be identical, no changing lastBuildDate
data.should.equal(xmlData);
// Now call done!
done();
};
rss(req, res, failTest(done));
}
function firstCall() {
res.send = function sendFirst(data) {
xmlData = data;
// Call RSS again to check that we didn't rebuild
secondCall();
};
rss(req, res, failTest(done));
}
firstCall();
});
});
describe('pagination', function () {
beforeEach(function () {
res = {
locals: {version: ''},
redirect: sandbox.spy(),
render: sandbox.spy()
};
rss.__set__('getData', function () {
return Promise.resolve({
title: 'Test',
description: 'Testing',
permalinks: '/:slug/',
results: {posts: [], meta: {pagination: {pages: 3}}}
});
});
});
it('Should 404 if page number too big', function (done) {
configUtils.set({url: 'http://localhost:82832/'});
req = {params: {page: 4}, route: {path: '/rss/:page/'}};
req.originalUrl = req.route.path.replace(':page', req.params.page);
res.locals.channel = channelUtils.getTestChannel('index');
res.locals.channel.isRSS = true;
rss(req, res, function (err) {
should.exist(err);
err.statusCode.should.eql(404);
res.redirect.called.should.be.false();
res.render.called.should.be.false();
done();
}).catch(done);
});
it('Redirects to last page if page number too big with subdirectory', function (done) {
configUtils.set({url: 'http://localhost:82832/blog'});
req = {params: {page: 4}, route: {path: '/rss/:page/'}};
req.originalUrl = req.route.path.replace(':page', req.params.page);
res.locals.channel = channelUtils.getTestChannel('index');
res.locals.channel.isRSS = true;
rss(req, res, function (err) {
should.exist(err);
err.statusCode.should.eql(404);
res.redirect.called.should.be.false();
res.render.called.should.be.false();
done();
}).catch(done);
});
});
});