mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
RSS service + controller improved for consistency (#9233)
refs #9192, refs #5091 - Moved all url generation into generate-feed.js, so we can see as much data processing as possible in a single place. - Refactored the way res.locals were used, to be more like how express uses them prior to rendering - Removed a bunch of code & tests todo with context for RSS - I can't see any way that'd be used, unless we switched the rendering to use a template. - moved the RSS rendering to be part of the service, not controller - updated the tests significantly Note: RSS generate-feed has a complete duplication of the code used in the excerpt helper in order to create an item description
This commit is contained in:
parent
8ad64e3e8a
commit
e41d0c76fb
12 changed files with 405 additions and 391 deletions
|
@ -9,7 +9,7 @@ var _ = require('lodash'),
|
||||||
renderChannel = require('./frontend/render-channel');
|
renderChannel = require('./frontend/render-channel');
|
||||||
|
|
||||||
// This here is a controller.
|
// This here is a controller.
|
||||||
// The "route" is handled in controllers/channels/router.js
|
// The "route" is handled in services/channels/router.js
|
||||||
// There's both a top-level channelS router, and an individual channel one
|
// There's both a top-level channelS router, and an individual channel one
|
||||||
module.exports = function channelController(req, res, next) {
|
module.exports = function channelController(req, res, next) {
|
||||||
// Parse the parameters we need from the URL
|
// Parse the parameters we need from the URL
|
||||||
|
|
|
@ -18,7 +18,6 @@ var config = require('../../config'),
|
||||||
privatePattern = new RegExp('^\\/' + config.get('routeKeywords').private + '\\/'),
|
privatePattern = new RegExp('^\\/' + config.get('routeKeywords').private + '\\/'),
|
||||||
subscribePattern = new RegExp('^\\/' + config.get('routeKeywords').subscribe + '\\/'),
|
subscribePattern = new RegExp('^\\/' + config.get('routeKeywords').subscribe + '\\/'),
|
||||||
ampPattern = new RegExp('\\/' + config.get('routeKeywords').amp + '\\/$'),
|
ampPattern = new RegExp('\\/' + config.get('routeKeywords').amp + '\\/$'),
|
||||||
rssPattern = new RegExp('^\\/rss\\/'),
|
|
||||||
homePattern = new RegExp('^\\/$');
|
homePattern = new RegExp('^\\/$');
|
||||||
|
|
||||||
function setResponseContext(req, res, data) {
|
function setResponseContext(req, res, data) {
|
||||||
|
@ -42,11 +41,6 @@ function setResponseContext(req, res, data) {
|
||||||
res.locals.context.push('home');
|
res.locals.context.push('home');
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is not currently used, as setRequestContext is not called for RSS feeds
|
|
||||||
if (rssPattern.test(res.locals.relativeUrl)) {
|
|
||||||
res.locals.context.push('rss');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add context 'amp' to either post or page, if we have an `*/amp` route
|
// Add context 'amp' to either post or page, if we have an `*/amp` route
|
||||||
if (ampPattern.test(res.locals.relativeUrl) && data.post) {
|
if (ampPattern.test(res.locals.relativeUrl) && data.post) {
|
||||||
res.locals.context.push('amp');
|
res.locals.context.push('amp');
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
var _ = require('lodash'),
|
var _ = require('lodash'),
|
||||||
url = require('url'),
|
url = require('url'),
|
||||||
utils = require('../utils'),
|
|
||||||
errors = require('../errors'),
|
errors = require('../errors'),
|
||||||
i18n = require('../i18n'),
|
i18n = require('../i18n'),
|
||||||
safeString = require('../utils/index').safeString,
|
safeString = require('../utils/index').safeString,
|
||||||
|
@ -10,7 +9,7 @@ var _ = require('lodash'),
|
||||||
fetchData = require('./frontend/fetch-data'),
|
fetchData = require('./frontend/fetch-data'),
|
||||||
handleError = require('./frontend/error'),
|
handleError = require('./frontend/error'),
|
||||||
|
|
||||||
rssCache = require('../services/rss'),
|
rssService = require('../services/rss'),
|
||||||
generate;
|
generate;
|
||||||
|
|
||||||
// @TODO: is this the right logic? Where should this live?!
|
// @TODO: is this the right logic? Where should this live?!
|
||||||
|
@ -33,27 +32,25 @@ function getData(channelOpts) {
|
||||||
channelOpts.data = channelOpts.data || {};
|
channelOpts.data = channelOpts.data || {};
|
||||||
|
|
||||||
return fetchData(channelOpts).then(function formatResult(result) {
|
return fetchData(channelOpts).then(function formatResult(result) {
|
||||||
var response = {};
|
var response = _.pick(result, ['posts', 'meta']);
|
||||||
|
|
||||||
response.title = getTitle(result.data);
|
response.title = getTitle(result.data);
|
||||||
response.description = settingsCache.get('description');
|
response.description = settingsCache.get('description');
|
||||||
response.results = {
|
|
||||||
posts: result.posts,
|
|
||||||
meta: result.meta
|
|
||||||
};
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// This here is a controller.
|
// This here is a controller.
|
||||||
// The "route" is handled in controllers/channels/router.js
|
// The "route" is handled in services/channels/router.js
|
||||||
// We can only generate RSS for channels, so that sorta makes sense, but the location is rubbish
|
// We can only generate RSS for channels, so that sorta makes sense, but the location is rubbish
|
||||||
// @TODO finish refactoring this - it's now a controller
|
// @TODO finish refactoring this - it's now a controller
|
||||||
generate = function generate(req, res, next) {
|
generate = function generate(req, res, next) {
|
||||||
// Parse the parameters we need from the URL
|
// Parse the parameters we need from the URL
|
||||||
var 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;
|
slugParam = req.params.slug ? safeString(req.params.slug) : undefined,
|
||||||
|
// Base URL needs to be the URL for the feed without pagination:
|
||||||
|
baseUrl = getBaseUrlForRSSReq(req.originalUrl, pageParam);
|
||||||
|
|
||||||
// @TODO: fix this, we shouldn't change the channel object!
|
// @TODO: fix this, we shouldn't change the channel object!
|
||||||
// Set page on postOptions for the query made later
|
// Set page on postOptions for the query made later
|
||||||
|
@ -61,31 +58,13 @@ generate = function generate(req, res, next) {
|
||||||
res.locals.channel.slugParam = slugParam;
|
res.locals.channel.slugParam = slugParam;
|
||||||
|
|
||||||
return getData(res.locals.channel).then(function handleResult(data) {
|
return getData(res.locals.channel).then(function handleResult(data) {
|
||||||
// Base URL needs to be the URL for the feed without pagination:
|
// If page is greater than number of pages we have, go straight to 404
|
||||||
var baseUrl = getBaseUrlForRSSReq(req.originalUrl, pageParam),
|
if (pageParam > data.meta.pagination.pages) {
|
||||||
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')}));
|
return next(new errors.NotFoundError({message: i18n.t('errors.errors.pageNotFound')}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renderer begin
|
// Render call - to a special RSS renderer
|
||||||
// Format data
|
return rssService.render(res, baseUrl, data);
|
||||||
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;
|
|
||||||
|
|
||||||
// No context, no template
|
|
||||||
// @TODO: should we have context? The context file expects it!
|
|
||||||
|
|
||||||
// Render call - to a different renderer
|
|
||||||
// @TODO this is effectively a renderer
|
|
||||||
return rssCache.getXML(baseUrl, data).then(function then(feedXml) {
|
|
||||||
res.set('Content-Type', 'text/xml; charset=UTF-8');
|
|
||||||
res.send(feedXml);
|
|
||||||
});
|
|
||||||
}).catch(handleError(next));
|
}).catch(handleError(next));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,15 +2,15 @@ var crypto = require('crypto'),
|
||||||
generateFeed = require('./generate-feed'),
|
generateFeed = require('./generate-feed'),
|
||||||
feedCache = {};
|
feedCache = {};
|
||||||
|
|
||||||
module.exports.getXML = function getFeedXml(path, data) {
|
module.exports.getXML = function getFeedXml(baseUrl, data) {
|
||||||
var dataHash = crypto.createHash('md5').update(JSON.stringify(data)).digest('hex');
|
var dataHash = crypto.createHash('md5').update(JSON.stringify(data)).digest('hex');
|
||||||
if (!feedCache[path] || feedCache[path].hash !== dataHash) {
|
if (!feedCache[baseUrl] || feedCache[baseUrl].hash !== dataHash) {
|
||||||
// We need to regenerate
|
// We need to regenerate
|
||||||
feedCache[path] = {
|
feedCache[baseUrl] = {
|
||||||
hash: dataHash,
|
hash: dataHash,
|
||||||
xml: generateFeed(data)
|
xml: generateFeed(baseUrl, data)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return feedCache[path].xml;
|
return feedCache[baseUrl].xml;
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,7 @@ var downsize = require('downsize'),
|
||||||
processUrls = require('../../utils/make-absolute-urls'),
|
processUrls = require('../../utils/make-absolute-urls'),
|
||||||
|
|
||||||
generateFeed,
|
generateFeed,
|
||||||
|
generateItem,
|
||||||
generateTags;
|
generateTags;
|
||||||
|
|
||||||
generateTags = function generateTags(data) {
|
generateTags = function generateTags(data) {
|
||||||
|
@ -20,60 +21,77 @@ generateTags = function generateTags(data) {
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
generateFeed = function generateFeed(data) {
|
generateItem = function generateItem(post, siteUrl, secure) {
|
||||||
var feed = new RSS({
|
var itemUrl = utils.url.urlFor('post', {post: post, secure: secure}, true),
|
||||||
title: data.title,
|
htmlContent = processUrls(post.html, siteUrl, itemUrl),
|
||||||
description: data.description,
|
item = {
|
||||||
generator: 'Ghost ' + data.version,
|
title: post.title,
|
||||||
feed_url: data.feedUrl,
|
// @TODO: DRY this up with data/meta/index & other excerpt code
|
||||||
site_url: data.siteUrl,
|
description: post.custom_excerpt || post.meta_description || downsize(htmlContent.html(), {words: 50}),
|
||||||
image_url: utils.url.urlFor({relativeUrl: 'favicon.png'}, true),
|
guid: post.id,
|
||||||
ttl: '60',
|
url: itemUrl,
|
||||||
custom_namespaces: {
|
date: post.published_at,
|
||||||
content: 'http://purl.org/rss/1.0/modules/content/',
|
categories: generateTags(post),
|
||||||
media: 'http://search.yahoo.com/mrss/'
|
author: post.author ? post.author.name : null,
|
||||||
|
custom_elements: []
|
||||||
|
},
|
||||||
|
imageUrl;
|
||||||
|
|
||||||
|
if (post.feature_image) {
|
||||||
|
imageUrl = utils.url.urlFor('image', {image: post.feature_image, secure: 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()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
data.results.posts.forEach(function forEach(post) {
|
return item;
|
||||||
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);
|
* Generate Feed
|
||||||
|
*
|
||||||
// Add a media content tag
|
* Data is an object which contains the res.locals + results from fetching a channel, but without related data.
|
||||||
item.custom_elements.push({
|
*
|
||||||
'media:content': {
|
* @param {string} baseUrl
|
||||||
_attr: {
|
* @param {{title, description, safeVersion, secure, posts}} data
|
||||||
url: imageUrl,
|
*/
|
||||||
medium: 'image'
|
generateFeed = function generateFeed(baseUrl, data) {
|
||||||
}
|
var siteUrl = utils.url.urlFor('home', {secure: data.secure}, true),
|
||||||
}
|
feedUrl = utils.url.urlFor({relativeUrl: baseUrl, secure: data.secure}, true),
|
||||||
});
|
feed = new RSS({
|
||||||
|
title: data.title,
|
||||||
// Also add the image to the content, because not all readers support media:content
|
description: data.description,
|
||||||
htmlContent('p').first().before('<img src="' + imageUrl + '" />');
|
generator: 'Ghost ' + data.safeVersion,
|
||||||
htmlContent('img').attr('alt', post.title);
|
feed_url: feedUrl,
|
||||||
}
|
site_url: siteUrl,
|
||||||
|
image_url: utils.url.urlFor({relativeUrl: 'favicon.png'}, true),
|
||||||
item.custom_elements.push({
|
ttl: '60',
|
||||||
'content:encoded': {
|
custom_namespaces: {
|
||||||
_cdata: htmlContent.html()
|
content: 'http://purl.org/rss/1.0/modules/content/',
|
||||||
|
media: 'http://search.yahoo.com/mrss/'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
data.posts.forEach(function forEach(post) {
|
||||||
|
var item = generateItem(post, siteUrl, data.secure);
|
||||||
|
|
||||||
filters.doFilter('rss.item', item, post).then(function then(item) {
|
filters.doFilter('rss.item', item, post).then(function then(item) {
|
||||||
feed.item(item);
|
feed.item(item);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
module.exports = require('./cache');
|
module.exports = require('./renderer');
|
||||||
|
|
15
core/server/services/rss/renderer.js
Normal file
15
core/server/services/rss/renderer.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
var _ = require('lodash'),
|
||||||
|
rssCache = require('./cache');
|
||||||
|
|
||||||
|
module.exports.render = function render(res, baseUrl, data) {
|
||||||
|
// Format data - this is the same as what Express does
|
||||||
|
var rssData = _.merge({}, res.locals, data);
|
||||||
|
|
||||||
|
// Fetch RSS from the cache
|
||||||
|
return rssCache
|
||||||
|
.getXML(baseUrl, rssData)
|
||||||
|
.then(function then(feedXml) {
|
||||||
|
res.set('Content-Type', 'text/xml; charset=UTF-8');
|
||||||
|
res.send(feedXml);
|
||||||
|
});
|
||||||
|
};
|
|
@ -410,48 +410,6 @@ describe('Contexts', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('RSS', function () {
|
|
||||||
// NOTE: this works, but is never used in reality, as setResponseContext isn't called
|
|
||||||
// for RSS feeds at the moment.
|
|
||||||
it('should correctly identify /rss/ as rss', function () {
|
|
||||||
// Setup test
|
|
||||||
setupContext('/rss/');
|
|
||||||
|
|
||||||
// Execute test
|
|
||||||
setResponseContext(req, res, data);
|
|
||||||
|
|
||||||
// Check context
|
|
||||||
should.exist(res.locals.context);
|
|
||||||
res.locals.context.should.be.an.Array().with.lengthOf(1);
|
|
||||||
res.locals.context[0].should.eql('rss');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('will not identify /rss/2/ as rss & paged without page param', function () {
|
|
||||||
// Setup test by setting relativeUrl
|
|
||||||
setupContext('/rss/2/');
|
|
||||||
|
|
||||||
// Execute test
|
|
||||||
setResponseContext(req, res, data);
|
|
||||||
|
|
||||||
// Check context
|
|
||||||
should.exist(res.locals.context);
|
|
||||||
res.locals.context.should.be.an.Array().with.lengthOf(1);
|
|
||||||
res.locals.context[0].should.eql('rss');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should correctly identify /rss/2/ as rss & paged with page param', function () {
|
|
||||||
// Setup test by setting relativeUrl
|
|
||||||
setupContext('/rss/2/', 2);
|
|
||||||
|
|
||||||
// Execute test
|
|
||||||
setResponseContext(req, res, data);
|
|
||||||
|
|
||||||
should.exist(res.locals.context);
|
|
||||||
res.locals.context.should.be.an.Array().with.lengthOf(2);
|
|
||||||
res.locals.context[0].should.eql('paged');
|
|
||||||
res.locals.context[1].should.eql('rss');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('AMP', function () {
|
describe('AMP', function () {
|
||||||
it('should correctly identify an AMP post', function () {
|
it('should correctly identify an AMP post', function () {
|
||||||
// Setup test
|
// Setup test
|
||||||
|
|
|
@ -3,13 +3,10 @@ var should = require('should'),
|
||||||
rewire = require('rewire'),
|
rewire = require('rewire'),
|
||||||
_ = require('lodash'),
|
_ = require('lodash'),
|
||||||
Promise = require('bluebird'),
|
Promise = require('bluebird'),
|
||||||
testUtils = require('../../utils'),
|
|
||||||
channelUtils = require('../../utils/channelUtils'),
|
channelUtils = require('../../utils/channelUtils'),
|
||||||
api = require('../../../server/api'),
|
|
||||||
settingsCache = require('../../../server/settings/cache'),
|
settingsCache = require('../../../server/settings/cache'),
|
||||||
rssController = rewire('../../../server/controllers/rss'),
|
rssController = rewire('../../../server/controllers/rss'),
|
||||||
rssCache = require('../../../server/services/rss'),
|
rssService = require('../../../server/services/rss'),
|
||||||
configUtils = require('../../utils/configUtils'),
|
|
||||||
|
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create();
|
||||||
|
|
||||||
|
@ -24,20 +21,7 @@ function failTest(done) {
|
||||||
|
|
||||||
describe('RSS', function () {
|
describe('RSS', function () {
|
||||||
describe('RSS: Controller only', function () {
|
describe('RSS: Controller only', function () {
|
||||||
var req, res, posts, getDataStub, resetGetData, rssCacheStub;
|
var req, res, next, getDataStub, fakeData, resetGetData, rssServiceStub;
|
||||||
|
|
||||||
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 () {
|
beforeEach(function () {
|
||||||
// Minimum setup of req and res
|
// Minimum setup of req and res
|
||||||
|
@ -49,313 +33,259 @@ describe('RSS', function () {
|
||||||
res = {
|
res = {
|
||||||
locals: {
|
locals: {
|
||||||
safeVersion: '0.6',
|
safeVersion: '0.6',
|
||||||
channel: channelUtils.getTestChannel('index')
|
channel: {postOptions: {}}
|
||||||
},
|
}
|
||||||
set: sinon.stub(),
|
|
||||||
send: sinon.spy()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// @TODO Get rid of this! - shouldn't be set on the channel
|
next = sandbox.stub();
|
||||||
res.locals.channel.isRSS = true;
|
|
||||||
|
|
||||||
// Overwrite getData
|
// Overwrite getData
|
||||||
getDataStub = sandbox.stub();
|
fakeData = {meta: {pagination: {pages: 3}}};
|
||||||
|
getDataStub = sandbox.stub().returns(new Promise.resolve(fakeData));
|
||||||
|
|
||||||
resetGetData = rssController.__set__('getData', getDataStub);
|
resetGetData = rssController.__set__('getData', getDataStub);
|
||||||
|
|
||||||
rssCacheStub = sandbox.stub(rssCache, 'getXML').returns(new Promise.resolve('dummyxml'));
|
rssServiceStub = sandbox.stub(rssService, 'render').returns(new Promise.resolve());
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
configUtils.restore();
|
|
||||||
resetGetData();
|
resetGetData();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch data and attempt to send XML', function (done) {
|
it('should fetch data and attempt to send XML', function (done) {
|
||||||
getDataStub.returns(new Promise.resolve({
|
rssController(req, res, next)
|
||||||
results: {meta: {pagination: {pages: 3}}}
|
.then(function () {
|
||||||
}));
|
next.called.should.be.false();
|
||||||
|
|
||||||
res.send = function (result) {
|
getDataStub.calledOnce.should.be.true();
|
||||||
result.should.eql('dummyxml');
|
getDataStub.calledWith(res.locals.channel).should.be.true();
|
||||||
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));
|
rssServiceStub.calledOnce.should.be.true();
|
||||||
|
rssServiceStub.firstCall.args[0].should.eql(res);
|
||||||
|
rssServiceStub.firstCall.args[1].should.eql('/rss/');
|
||||||
|
rssServiceStub.firstCall.args[2].should.match(fakeData);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can handle paginated urls', function (done) {
|
it('can handle paginated urls', function (done) {
|
||||||
getDataStub.returns(new Promise.resolve({
|
|
||||||
results: {meta: {pagination: {pages: 3}}}
|
|
||||||
}));
|
|
||||||
|
|
||||||
req.originalUrl = '/rss/2/';
|
req.originalUrl = '/rss/2/';
|
||||||
req.params.page = 2;
|
req.params.page = 2;
|
||||||
|
|
||||||
res.send = function (result) {
|
rssController(req, res, next)
|
||||||
result.should.eql('dummyxml');
|
.then(function () {
|
||||||
res.set.calledOnce.should.be.true();
|
next.called.should.be.false();
|
||||||
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));
|
getDataStub.calledOnce.should.be.true();
|
||||||
|
getDataStub.calledWith(res.locals.channel).should.be.true();
|
||||||
|
|
||||||
|
rssServiceStub.calledOnce.should.be.true();
|
||||||
|
rssServiceStub.firstCall.args[0].should.eql(res);
|
||||||
|
rssServiceStub.firstCall.args[1].should.eql('/rss/');
|
||||||
|
rssServiceStub.firstCall.args[2].should.match(fakeData);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can handle paginated urls with subdirectories', function (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.originalUrl = '/blog/rss/2/';
|
||||||
req.params.page = 2;
|
req.params.page = 2;
|
||||||
|
|
||||||
res.send = function (result) {
|
rssController(req, res, next)
|
||||||
result.should.eql('dummyxml');
|
.then(function () {
|
||||||
res.set.calledOnce.should.be.true();
|
next.called.should.be.false();
|
||||||
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));
|
getDataStub.calledOnce.should.be.true();
|
||||||
|
getDataStub.calledWith(res.locals.channel).should.be.true();
|
||||||
|
|
||||||
|
rssServiceStub.calledOnce.should.be.true();
|
||||||
|
rssServiceStub.firstCall.args[0].should.eql(res);
|
||||||
|
rssServiceStub.firstCall.args[1].should.eql('/blog/rss/');
|
||||||
|
rssServiceStub.firstCall.args[2].should.match(fakeData);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can handle paginated urls for channels', function (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.originalUrl = '/tags/test/rss/2/';
|
||||||
req.params.page = 2;
|
req.params.page = 2;
|
||||||
req.params.slug = 'test';
|
req.params.slug = 'test';
|
||||||
|
|
||||||
res.send = function (result) {
|
rssController(req, res, next)
|
||||||
result.should.eql('dummyxml');
|
.then(function () {
|
||||||
res.set.calledOnce.should.be.true();
|
next.called.should.be.false();
|
||||||
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));
|
getDataStub.calledOnce.should.be.true();
|
||||||
|
getDataStub.calledWith(res.locals.channel).should.be.true();
|
||||||
|
|
||||||
|
rssServiceStub.calledOnce.should.be.true();
|
||||||
|
rssServiceStub.firstCall.args[0].should.eql(res);
|
||||||
|
rssServiceStub.firstCall.args[1].should.eql('/tags/test/rss/');
|
||||||
|
rssServiceStub.firstCall.args[2].should.match(fakeData);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call next with 404 if page number too big', function (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.originalUrl = '/rss/4/';
|
||||||
req.params.page = 4;
|
req.params.page = 4;
|
||||||
|
|
||||||
rssController(req, res, function (err) {
|
rssController(req, res, next)
|
||||||
should.exist(err);
|
.then(function () {
|
||||||
err.statusCode.should.eql(404);
|
next.called.should.be.true();
|
||||||
res.send.called.should.be.false();
|
next.firstCall.args[0].statusCode.should.eql(404);
|
||||||
done();
|
|
||||||
});
|
getDataStub.calledOnce.should.be.true();
|
||||||
|
getDataStub.calledWith(res.locals.channel).should.be.true();
|
||||||
|
|
||||||
|
rssServiceStub.called.should.be.false();
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// These tests check the RSS feed from controller to result
|
// These tests check the RSS feed from controller to result
|
||||||
// @TODO: test only the data generation, once we've refactored to make that easier
|
// @TODO: test only the data generation, once we've refactored to make that easier
|
||||||
describe('RSS: data generation', function () {
|
describe('RSS: getData / getTitle', function () {
|
||||||
var apiBrowseStub, apiTagStub, apiUserStub, req, res;
|
var fetchDataStub, resetFetchData, getData;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
apiBrowseStub = sandbox.stub(api.posts, 'browse', function () {
|
fetchDataStub = sandbox.stub();
|
||||||
return Promise.resolve({posts: [], meta: {pagination: {pages: 3}}});
|
resetFetchData = rssController.__set__('fetchData', fetchDataStub);
|
||||||
});
|
getData = rssController.__get__('getData');
|
||||||
|
|
||||||
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) {
|
sandbox.stub(settingsCache, 'get', function (key) {
|
||||||
var obj = {
|
var obj = {
|
||||||
title: 'Test',
|
title: 'Test',
|
||||||
description: 'Some Text',
|
description: 'Some Text'
|
||||||
permalinks: '/:slug/'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return obj[key];
|
return obj[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
configUtils.set({
|
|
||||||
url: 'http://my-ghost-blog.com'
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
configUtils.restore();
|
resetFetchData();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should process the data correctly for the index feed', function (done) {
|
it('should process the data correctly for the index feed', function (done) {
|
||||||
// setup
|
fetchDataStub.returns(new Promise.resolve({posts: [{test: 'hey'}], meta: {foo: 'you'}}));
|
||||||
req.originalUrl = '/rss/';
|
|
||||||
res.locals.channel = channelUtils.getTestChannel('index');
|
|
||||||
res.locals.channel.isRSS = true;
|
|
||||||
|
|
||||||
// test
|
var channel = channelUtils.getTestChannel('index');
|
||||||
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));
|
getData(channel)
|
||||||
});
|
.then(function (result) {
|
||||||
|
fetchDataStub.calledOnce.should.be.true();
|
||||||
|
fetchDataStub.calledWith(channel).should.be.true();
|
||||||
|
|
||||||
it('should process the data correctly for the paginated index feed', function (done) {
|
result.should.eql({
|
||||||
// setup
|
title: 'Test',
|
||||||
req.originalUrl = '/rss/2/';
|
description: 'Some Text',
|
||||||
req.params.page = '2';
|
posts: [{test: 'hey'}],
|
||||||
res.locals.channel = channelUtils.getTestChannel('index');
|
meta: {foo: 'you'}
|
||||||
res.locals.channel.isRSS = true;
|
});
|
||||||
|
done();
|
||||||
// test
|
})
|
||||||
res.send = function send(xmlData) {
|
.catch(done);
|
||||||
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) {
|
it('should process the data correctly for a tag feed', function (done) {
|
||||||
// setup
|
fetchDataStub.returns(new Promise.resolve({posts: [{test: 'hey'}], meta: {foo: 'you'}}));
|
||||||
req.originalUrl = '/tag/magic/rss/';
|
|
||||||
req.params.slug = 'magic';
|
|
||||||
res.locals.channel = channelUtils.getTestChannel('tag');
|
|
||||||
res.locals.channel.isRSS = true;
|
|
||||||
|
|
||||||
// test
|
var channel = channelUtils.getTestChannel('tag');
|
||||||
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));
|
getData(channel)
|
||||||
|
.then(function (result) {
|
||||||
|
fetchDataStub.calledOnce.should.be.true();
|
||||||
|
fetchDataStub.calledWith(channel).should.be.true();
|
||||||
|
|
||||||
|
result.should.eql({
|
||||||
|
title: 'Test',
|
||||||
|
description: 'Some Text',
|
||||||
|
posts: [{test: 'hey'}],
|
||||||
|
meta: {foo: 'you'}
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should process the data correctly for a paginated tag feed', function (done) {
|
it('should process the data correctly for a tag feed WITH related data', function (done) {
|
||||||
// setup
|
fetchDataStub.returns(new Promise.resolve({
|
||||||
req.originalUrl = '/tag/magic/rss/2/';
|
posts: [{test: 'hey'}],
|
||||||
req.params.slug = 'magic';
|
meta: {foo: 'you'},
|
||||||
req.params.page = '2';
|
data: {tag: [{name: 'there'}]}
|
||||||
res.locals.channel = channelUtils.getTestChannel('tag');
|
}));
|
||||||
res.locals.channel.isRSS = true;
|
|
||||||
|
|
||||||
// test
|
var channel = channelUtils.getTestChannel('tag');
|
||||||
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();
|
getData(channel)
|
||||||
xmlData.should.match(/<channel><title><!\[CDATA\[Magic - Test\]\]><\/title>/);
|
.then(function (result) {
|
||||||
xmlData.should.match(/<atom:link href="http:\/\/my-ghost-blog.com\/tag\/magic\/rss\/" rel="self" type="application\/rss\+xml"\/>/);
|
fetchDataStub.calledOnce.should.be.true();
|
||||||
done();
|
fetchDataStub.calledWith(channel).should.be.true();
|
||||||
};
|
|
||||||
|
|
||||||
rssController(req, res, failTest(done));
|
result.should.eql({
|
||||||
|
title: 'there - Test',
|
||||||
|
description: 'Some Text',
|
||||||
|
posts: [{test: 'hey'}],
|
||||||
|
meta: {foo: 'you'}
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should process the data correctly for an author feed', function (done) {
|
it('should process the data correctly for an author feed', function (done) {
|
||||||
req.originalUrl = '/author/joe/rss/';
|
fetchDataStub.returns(new Promise.resolve({posts: [{test: 'hey'}], meta: {foo: 'you'}}));
|
||||||
req.params.slug = 'joe';
|
|
||||||
res.locals.channel = channelUtils.getTestChannel('author');
|
|
||||||
res.locals.channel.isRSS = true;
|
|
||||||
|
|
||||||
// test
|
var channel = channelUtils.getTestChannel('author');
|
||||||
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));
|
getData(channel)
|
||||||
|
.then(function (result) {
|
||||||
|
fetchDataStub.calledOnce.should.be.true();
|
||||||
|
fetchDataStub.calledWith(channel).should.be.true();
|
||||||
|
|
||||||
|
result.should.eql({
|
||||||
|
title: 'Test',
|
||||||
|
description: 'Some Text',
|
||||||
|
posts: [{test: 'hey'}],
|
||||||
|
meta: {foo: 'you'}
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should process the data correctly for a paginated author feed', function (done) {
|
it('should process the data correctly for an author feed WITH related data', function (done) {
|
||||||
req.originalUrl = '/author/joe/rss/2/';
|
fetchDataStub.returns(new Promise.resolve({
|
||||||
req.params.slug = 'joe';
|
posts: [{test: 'hey'}],
|
||||||
req.params.page = '2';
|
meta: {foo: 'you'},
|
||||||
res.locals.channel = channelUtils.getTestChannel('author');
|
data: {author: [{name: 'there'}]}
|
||||||
res.locals.channel.isRSS = true;
|
}));
|
||||||
|
|
||||||
// test
|
var channel = channelUtils.getTestChannel('author');
|
||||||
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));
|
getData(channel)
|
||||||
|
.then(function (result) {
|
||||||
|
fetchDataStub.calledOnce.should.be.true();
|
||||||
|
fetchDataStub.calledWith(channel).should.be.true();
|
||||||
|
|
||||||
|
result.should.eql({
|
||||||
|
title: 'there - Test',
|
||||||
|
description: 'Some Text',
|
||||||
|
posts: [{test: 'hey'}],
|
||||||
|
meta: {foo: 'you'}
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,8 +27,8 @@ describe('RSS: Cache', function () {
|
||||||
data = {
|
data = {
|
||||||
title: 'Test Title',
|
title: 'Test Title',
|
||||||
description: 'Testing Desc',
|
description: 'Testing Desc',
|
||||||
permalinks: '/:slug/',
|
posts: [],
|
||||||
results: {posts: [], meta: {pagination: {pages: 1}}}
|
meta: {pagination: {pages: 1}}
|
||||||
};
|
};
|
||||||
|
|
||||||
rssCache.getXML('/rss/', data)
|
rssCache.getXML('/rss/', data)
|
||||||
|
|
|
@ -7,6 +7,7 @@ var should = require('should'),
|
||||||
|
|
||||||
describe('RSS: Generate Feed', function () {
|
describe('RSS: Generate Feed', function () {
|
||||||
var data = {},
|
var data = {},
|
||||||
|
baseUrl,
|
||||||
// Static set of posts
|
// Static set of posts
|
||||||
posts;
|
posts;
|
||||||
|
|
||||||
|
@ -30,18 +31,18 @@ describe('RSS: Generate Feed', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
configUtils.set({url: 'http://my-ghost-blog.com'});
|
configUtils.set({url: 'http://my-ghost-blog.com'});
|
||||||
|
|
||||||
data.version = '0.6';
|
baseUrl = '/rss/';
|
||||||
data.siteUrl = 'http://my-ghost-blog.com/';
|
|
||||||
data.feedUrl = 'http://my-ghost-blog.com/rss/';
|
data.safeVersion = '0.6';
|
||||||
data.title = 'Test Title';
|
data.title = 'Test Title';
|
||||||
data.description = 'Testing Desc';
|
data.description = 'Testing Desc';
|
||||||
data.permalinks = '/:slug/';
|
data.meta = {pagination: {pages: 1}};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get the RSS tags correct', function (done) {
|
it('should get the RSS tags correct', function (done) {
|
||||||
data.results = {posts: [], meta: {pagination: {pages: 1}}};
|
data.posts = [];
|
||||||
|
|
||||||
generateFeed(data).then(function (xmlData) {
|
generateFeed(baseUrl, data).then(function (xmlData) {
|
||||||
should.exist(xmlData);
|
should.exist(xmlData);
|
||||||
|
|
||||||
// xml & rss tags
|
// xml & rss tags
|
||||||
|
@ -69,9 +70,9 @@ describe('RSS: Generate Feed', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get the item tags correct', function (done) {
|
it('should get the item tags correct', function (done) {
|
||||||
data.results = {posts: posts, meta: {pagination: {pages: 1}}};
|
data.posts = posts;
|
||||||
|
|
||||||
generateFeed(data).then(function (xmlData) {
|
generateFeed(baseUrl, data).then(function (xmlData) {
|
||||||
should.exist(xmlData);
|
should.exist(xmlData);
|
||||||
|
|
||||||
// item tags
|
// item tags
|
||||||
|
@ -107,9 +108,9 @@ describe('RSS: Generate Feed', function () {
|
||||||
{name: 'visibility'}
|
{name: 'visibility'}
|
||||||
];
|
];
|
||||||
|
|
||||||
data.results = {posts: [postWithTags], meta: {pagination: {pages: 1}}};
|
data.posts = [postWithTags];
|
||||||
|
|
||||||
generateFeed(data).then(function (xmlData) {
|
generateFeed(baseUrl, data).then(function (xmlData) {
|
||||||
should.exist(xmlData);
|
should.exist(xmlData);
|
||||||
// item tags
|
// item tags
|
||||||
xmlData.should.match(/<title><!\[CDATA\[Short and Sweet\]\]>/);
|
xmlData.should.match(/<title><!\[CDATA\[Short and Sweet\]\]>/);
|
||||||
|
@ -124,10 +125,28 @@ describe('RSS: Generate Feed', function () {
|
||||||
}).catch(done);
|
}).catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use meta_description and image where available', function (done) {
|
it('should no error if author is somehow not present', function (done) {
|
||||||
data.results = {posts: [posts[2]], meta: {pagination: {pages: 1}}};
|
data.posts = [_.omit(posts[2], 'author')];
|
||||||
|
|
||||||
generateFeed(data).then(function (xmlData) {
|
generateFeed(baseUrl, 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"\/>/);
|
||||||
|
xmlData.should.not.match(/<dc:creator>/);
|
||||||
|
|
||||||
|
done();
|
||||||
|
}).catch(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use meta_description and image where available', function (done) {
|
||||||
|
data.posts = [posts[2]];
|
||||||
|
|
||||||
|
generateFeed(baseUrl, data).then(function (xmlData) {
|
||||||
should.exist(xmlData);
|
should.exist(xmlData);
|
||||||
|
|
||||||
// special/optional tags
|
// special/optional tags
|
||||||
|
@ -142,9 +161,9 @@ describe('RSS: Generate Feed', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use excerpt when no meta_description is set', function (done) {
|
it('should use excerpt when no meta_description is set', function (done) {
|
||||||
data.results = {posts: [posts[0]], meta: {pagination: {pages: 1}}};
|
data.posts = [posts[0]];
|
||||||
|
|
||||||
generateFeed(data).then(function (xmlData) {
|
generateFeed(baseUrl, data).then(function (xmlData) {
|
||||||
should.exist(xmlData);
|
should.exist(xmlData);
|
||||||
|
|
||||||
// special/optional tags
|
// special/optional tags
|
||||||
|
@ -156,9 +175,9 @@ describe('RSS: Generate Feed', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should process urls correctly', function (done) {
|
it('should process urls correctly', function (done) {
|
||||||
data.results = {posts: [posts[3]], meta: {pagination: {pages: 1}}};
|
data.posts = [posts[3]];
|
||||||
|
|
||||||
generateFeed(data).then(function (xmlData) {
|
generateFeed(baseUrl, data).then(function (xmlData) {
|
||||||
should.exist(xmlData);
|
should.exist(xmlData);
|
||||||
|
|
||||||
// anchor URL - <a href="#nowhere" title="Anchor URL">
|
// anchor URL - <a href="#nowhere" title="Anchor URL">
|
||||||
|
@ -180,11 +199,10 @@ describe('RSS: Generate Feed', function () {
|
||||||
it('should process urls correctly with subdirectory', function (done) {
|
it('should process urls correctly with subdirectory', function (done) {
|
||||||
configUtils.set({url: 'http://my-ghost-blog.com/blog/'});
|
configUtils.set({url: 'http://my-ghost-blog.com/blog/'});
|
||||||
|
|
||||||
data.siteUrl = 'http://my-ghost-blog.com/blog/';
|
baseUrl = '/blog/rss/';
|
||||||
data.feedUrl = 'http://my-ghost-blog.com/blog/rss/';
|
|
||||||
data.results = {posts: [posts[3]], meta: {pagination: {pages: 1}}};
|
data.results = {posts: [posts[3]], meta: {pagination: {pages: 1}}};
|
||||||
|
|
||||||
generateFeed(data).then(function (xmlData) {
|
generateFeed(baseUrl, data).then(function (xmlData) {
|
||||||
should.exist(xmlData);
|
should.exist(xmlData);
|
||||||
|
|
||||||
// anchor URL - <a href="#nowhere" title="Anchor URL">
|
// anchor URL - <a href="#nowhere" title="Anchor URL">
|
||||||
|
|
102
core/test/unit/services/rss/renderer_spec.js
Normal file
102
core/test/unit/services/rss/renderer_spec.js
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
var should = require('should'),
|
||||||
|
sinon = require('sinon'),
|
||||||
|
Promise = require('bluebird'),
|
||||||
|
|
||||||
|
rssCache = require('../../../../server/services/rss/cache'),
|
||||||
|
renderer = require('../../../../server/services/rss/renderer'),
|
||||||
|
|
||||||
|
sandbox = sinon.sandbox.create();
|
||||||
|
|
||||||
|
describe('RSS: Renderer', function () {
|
||||||
|
var rssCacheStub, res, baseUrl;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
rssCacheStub = sandbox.stub(rssCache, 'getXML');
|
||||||
|
|
||||||
|
res = {
|
||||||
|
locals: {},
|
||||||
|
set: sinon.stub(),
|
||||||
|
send: sinon.spy()
|
||||||
|
};
|
||||||
|
|
||||||
|
baseUrl = '/rss/';
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the cache and attempts to render, even without data', function (done) {
|
||||||
|
rssCacheStub.returns(new Promise.resolve('dummyxml'));
|
||||||
|
|
||||||
|
renderer.render(res, baseUrl).then(function () {
|
||||||
|
rssCacheStub.calledOnce.should.be.true();
|
||||||
|
rssCacheStub.firstCall.args.should.eql(['/rss/', {}]);
|
||||||
|
|
||||||
|
res.set.calledOnce.should.be.true();
|
||||||
|
res.set.calledWith('Content-Type', 'text/xml; charset=UTF-8').should.be.true();
|
||||||
|
|
||||||
|
res.send.calledOnce.should.be.true();
|
||||||
|
res.send.calledWith('dummyxml').should.be.true();
|
||||||
|
|
||||||
|
done();
|
||||||
|
}).catch(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly merges locals into empty data before rendering', function (done) {
|
||||||
|
rssCacheStub.returns(new Promise.resolve('dummyxml'));
|
||||||
|
|
||||||
|
res.locals = {foo: 'bar'};
|
||||||
|
|
||||||
|
renderer.render(res, baseUrl).then(function () {
|
||||||
|
rssCacheStub.calledOnce.should.be.true();
|
||||||
|
rssCacheStub.firstCall.args.should.eql(['/rss/', {foo: 'bar'}]);
|
||||||
|
|
||||||
|
res.set.calledOnce.should.be.true();
|
||||||
|
res.set.calledWith('Content-Type', 'text/xml; charset=UTF-8').should.be.true();
|
||||||
|
|
||||||
|
res.send.calledOnce.should.be.true();
|
||||||
|
res.send.calledWith('dummyxml').should.be.true();
|
||||||
|
|
||||||
|
done();
|
||||||
|
}).catch(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly merges locals into non-empty data before rendering', function (done) {
|
||||||
|
rssCacheStub.returns(new Promise.resolve('dummyxml'));
|
||||||
|
|
||||||
|
res.locals = {foo: 'bar'};
|
||||||
|
var data = {foo: 'baz', fizz: 'buzz'};
|
||||||
|
|
||||||
|
renderer.render(res, baseUrl, data).then(function () {
|
||||||
|
rssCacheStub.calledOnce.should.be.true();
|
||||||
|
rssCacheStub.firstCall.args.should.eql(['/rss/', {foo: 'baz', fizz: 'buzz'}]);
|
||||||
|
|
||||||
|
res.set.calledOnce.should.be.true();
|
||||||
|
res.set.calledWith('Content-Type', 'text/xml; charset=UTF-8').should.be.true();
|
||||||
|
|
||||||
|
res.send.calledOnce.should.be.true();
|
||||||
|
res.send.calledWith('dummyxml').should.be.true();
|
||||||
|
|
||||||
|
done();
|
||||||
|
}).catch(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if it gets an error', function (done) {
|
||||||
|
rssCacheStub.returns(new Promise.reject(new Error('Fake Error')));
|
||||||
|
|
||||||
|
renderer.render(res, baseUrl).then(function () {
|
||||||
|
done('This should have errored');
|
||||||
|
}).catch(function (err) {
|
||||||
|
err.message.should.eql('Fake Error');
|
||||||
|
|
||||||
|
rssCacheStub.calledOnce.should.be.true();
|
||||||
|
rssCacheStub.firstCall.args.should.eql(['/rss/', {}]);
|
||||||
|
|
||||||
|
res.set.called.should.be.false();
|
||||||
|
res.send.called.should.be.false();
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Reference in a new issue