Ghost/test/unit/services/rss/generate-feed.test.js

244 lines
11 KiB
JavaScript
Raw Normal View History

const should = require('should');
const sinon = require('sinon');
const _ = require('lodash');
const testUtils = require('../../../utils');
const configUtils = require('../../../utils/configUtils');
const urlService = require('../../../../core/frontend/services/url');
const generateFeed = require('../../../../core/frontend/services/rss/generate-feed');
describe('RSS: Generate Feed', function () {
const data = {};
let baseUrl;
// Static set of posts
let posts;
before(function () {
posts = _.cloneDeep(testUtils.DataGenerator.forKnex.posts);
✨Dynamic Routing Beta (#9596) refs #9601 ### Dynamic Routing This is the beta version of dynamic routing. - we had a initial implementation of "channels" available in the codebase - we have removed and moved this implementation - there is now a centralised place for dynamic routing - server/services/routing - each routing component is represented by a router type e.g. collections, routes, static pages, taxonomies, rss, preview of posts - keep as much as possible logic of routing helpers, middlewares and controllers - ensure test coverage - connect all the things together - yaml file + validation - routing + routers - url service - sitemaps - url access - deeper implementation of yaml validations - e.g. hard require slashes - ensure routing hierarchy/order - e.g. you enable the subscriber app - you have a custom static page, which lives under the same slug /subscribe - static pages are stronger than apps - e.g. the first collection owns the post it has filtered - a post cannot live in two collections - ensure apps are still working and hook into the routers layer (or better said: and register in the routing service) - put as much as possible comments to the code base for better understanding - ensure a clean debug log - ensure we can unmount routes - e.g. you have a collection permalink of /:slug/ represented by {globals.permalink} - and you change the permalink in the admin to dated permalink - the express route get's refreshed from /:slug/ to /:year/:month/:day/:slug/ - unmount without server restart, yey - ensure we are backwards compatible - e.g. render home.hbs for collection index if collection route is / - ensure you can access your configured permalink from the settings table with {globals.permalink} ### Render 503 if url service did not finish - return 503 if the url service has not finished generating the resource urls ### Rewrite sitemaps - we have rewritten the sitemaps "service", because the url generator does no longer happen on runtime - we generate all urls on bootstrap - the sitemaps service will consume created resource and router urls - these urls will be shown on the xml pages - we listen on url events - we listen on router events - we no longer have to fetch the resources, which is nice - the urlservice pre-fetches resources and emits their urls - the urlservice is the only component who knows which urls are valid - i made some ES6 adaptions - we keep the caching logic -> only regenerate xml if there is a change - updated tests - checked test coverage (100%) ### Re-work usage of Url utility - replace all usages of `urlService.utils.urlFor` by `urlService.getByResourceId` - only for resources e.g. post, author, tag - this is important, because with dynamic routing we no longer create static urls based on the settings permalink on runtime - adapt url utility - adapt tests
2018-06-05 20:02:20 +03:00
posts = _.filter(posts, function filter(post) {
return post.status === 'published' && post.type === 'post';
});
posts[2].meta_description = 'test stuffs';
✨Dynamic Routing Beta (#9596) refs #9601 ### Dynamic Routing This is the beta version of dynamic routing. - we had a initial implementation of "channels" available in the codebase - we have removed and moved this implementation - there is now a centralised place for dynamic routing - server/services/routing - each routing component is represented by a router type e.g. collections, routes, static pages, taxonomies, rss, preview of posts - keep as much as possible logic of routing helpers, middlewares and controllers - ensure test coverage - connect all the things together - yaml file + validation - routing + routers - url service - sitemaps - url access - deeper implementation of yaml validations - e.g. hard require slashes - ensure routing hierarchy/order - e.g. you enable the subscriber app - you have a custom static page, which lives under the same slug /subscribe - static pages are stronger than apps - e.g. the first collection owns the post it has filtered - a post cannot live in two collections - ensure apps are still working and hook into the routers layer (or better said: and register in the routing service) - put as much as possible comments to the code base for better understanding - ensure a clean debug log - ensure we can unmount routes - e.g. you have a collection permalink of /:slug/ represented by {globals.permalink} - and you change the permalink in the admin to dated permalink - the express route get's refreshed from /:slug/ to /:year/:month/:day/:slug/ - unmount without server restart, yey - ensure we are backwards compatible - e.g. render home.hbs for collection index if collection route is / - ensure you can access your configured permalink from the settings table with {globals.permalink} ### Render 503 if url service did not finish - return 503 if the url service has not finished generating the resource urls ### Rewrite sitemaps - we have rewritten the sitemaps "service", because the url generator does no longer happen on runtime - we generate all urls on bootstrap - the sitemaps service will consume created resource and router urls - these urls will be shown on the xml pages - we listen on url events - we listen on router events - we no longer have to fetch the resources, which is nice - the urlservice pre-fetches resources and emits their urls - the urlservice is the only component who knows which urls are valid - i made some ES6 adaptions - we keep the caching logic -> only regenerate xml if there is a change - updated tests - checked test coverage (100%) ### Re-work usage of Url utility - replace all usages of `urlService.utils.urlFor` by `urlService.getByResourceId` - only for resources e.g. post, author, tag - this is important, because with dynamic routing we no longer create static urls based on the settings permalink on runtime - adapt url utility - adapt tests
2018-06-05 20:02:20 +03:00
_.each(posts, function (post) {
post.url = '/' + post.slug + '/';
✨ Multiple authors (#9426) no issue This PR adds the server side logic for multiple authors. This adds the ability to add multiple authors per post. We keep and support single authors (maybe till the next major - this is still in discussion) ### key notes - `authors` are not fetched by default, only if we need them - the migration script iterates over all posts and figures out if an author_id is valid and exists (in master we can add invalid author_id's) and then adds the relation (falls back to owner if invalid) - ~~i had to push a fork of bookshelf to npm because we currently can't bump bookshelf + the two bugs i discovered are anyway not yet merged (https://github.com/kirrg001/bookshelf/commits/master)~~ replaced by new bookshelf release - the implementation of single & multiple authors lives in a single place (introduction of a new concept: model relation) - if you destroy an author, we keep the behaviour for now -> remove all posts where the primary author id matches. furthermore, remove all relations in posts_authors (e.g. secondary author) - we make re-use of the `excludeAttrs` concept which was invented in the contributors PR (to protect editing authors as author/contributor role) -> i've added a clear todo that we need a logic to make a diff of the target relation -> both for tags and authors - `authors` helper available (same as `tags` helper) - `primary_author` computed field available - `primary_author` functionality available (same as `primary_tag` e.g. permalinks, prev/next helper etc)
2018-03-27 17:16:15 +03:00
post.primary_author = {name: 'Joe Bloggs'};
// data is from fixtures that are inserted directly into the database via knex
// that means it has raw __GHOST_URL__ values that would typically be modified by the model layer
// we're not using the model layer here so we need to transform manually
Object.entries(post).forEach(([key, value]) => {
if (value && typeof value === 'string') {
post[key] = value.replace(/__GHOST_URL__/g, 'http://my-ghost-blog.com');
}
});
});
});
afterEach(function () {
sinon.restore();
configUtils.restore();
});
beforeEach(function () {
sinon.stub(urlService, 'getUrlByResourceId');
✨Dynamic Routing Beta (#9596) refs #9601 ### Dynamic Routing This is the beta version of dynamic routing. - we had a initial implementation of "channels" available in the codebase - we have removed and moved this implementation - there is now a centralised place for dynamic routing - server/services/routing - each routing component is represented by a router type e.g. collections, routes, static pages, taxonomies, rss, preview of posts - keep as much as possible logic of routing helpers, middlewares and controllers - ensure test coverage - connect all the things together - yaml file + validation - routing + routers - url service - sitemaps - url access - deeper implementation of yaml validations - e.g. hard require slashes - ensure routing hierarchy/order - e.g. you enable the subscriber app - you have a custom static page, which lives under the same slug /subscribe - static pages are stronger than apps - e.g. the first collection owns the post it has filtered - a post cannot live in two collections - ensure apps are still working and hook into the routers layer (or better said: and register in the routing service) - put as much as possible comments to the code base for better understanding - ensure a clean debug log - ensure we can unmount routes - e.g. you have a collection permalink of /:slug/ represented by {globals.permalink} - and you change the permalink in the admin to dated permalink - the express route get's refreshed from /:slug/ to /:year/:month/:day/:slug/ - unmount without server restart, yey - ensure we are backwards compatible - e.g. render home.hbs for collection index if collection route is / - ensure you can access your configured permalink from the settings table with {globals.permalink} ### Render 503 if url service did not finish - return 503 if the url service has not finished generating the resource urls ### Rewrite sitemaps - we have rewritten the sitemaps "service", because the url generator does no longer happen on runtime - we generate all urls on bootstrap - the sitemaps service will consume created resource and router urls - these urls will be shown on the xml pages - we listen on url events - we listen on router events - we no longer have to fetch the resources, which is nice - the urlservice pre-fetches resources and emits their urls - the urlservice is the only component who knows which urls are valid - i made some ES6 adaptions - we keep the caching logic -> only regenerate xml if there is a change - updated tests - checked test coverage (100%) ### Re-work usage of Url utility - replace all usages of `urlService.utils.urlFor` by `urlService.getByResourceId` - only for resources e.g. post, author, tag - this is important, because with dynamic routing we no longer create static urls based on the settings permalink on runtime - adapt url utility - adapt tests
2018-06-05 20:02:20 +03:00
baseUrl = '/rss/';
data.safeVersion = '0.6';
data.title = 'Test Title';
data.description = 'Testing Desc';
data.meta = {pagination: {pages: 1}};
});
describe('without subdirectory', function () {
beforeEach(function () {
configUtils.set({url: 'http://my-ghost-blog.com'});
});
it('should get the RSS tags correct', function (done) {
data.posts = [];
generateFeed(baseUrl, 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.posts = posts;
_.each(data.posts, function (post) {
urlService.getUrlByResourceId.withArgs(post.id, {secure: undefined, absolute: true}).returns('http://my-ghost-blog.com/' + post.slug + '/');
});
generateFeed(baseUrl, 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>/);
xmlData.should.match(/<\/code><\/pre>\]\]><\/content:encoded><\/item>/);
xmlData.should.not.match(/<author>/);
// basic structure check
const postEnd = '<\/code><\/pre>\]\]><\/content:encoded>';
const 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) {
const postWithTags = posts[2];
postWithTags.tags = [
{name: 'public', visibility: 'public'},
{name: 'internal', visibility: 'internal'},
{name: 'visibility'}
];
data.posts = [postWithTags];
generateFeed(baseUrl, 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\[<!--kg-card-begin: 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 not error if author is somehow not present', function (done) {
data.posts = [_.omit(posts[2], 'primary_author')];
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\[<!--kg-card-begin: 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 not error if post content is null', function (done) {
data.posts = [Object.assign({}, posts[2], {html: null})];
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\/>/);
xmlData.should.match(/<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/);
xmlData.should.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);
// special/optional tags
xmlData.should.match(/<title><!\[CDATA\[Short and Sweet\]\]>/);
xmlData.should.match(/<description><!\[CDATA\[test stuff/);
xmlData.should.match(/<content:encoded><!\[CDATA\[<!--kg-card-begin: 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);
✨Dynamic Routing Beta (#9596) refs #9601 ### Dynamic Routing This is the beta version of dynamic routing. - we had a initial implementation of "channels" available in the codebase - we have removed and moved this implementation - there is now a centralised place for dynamic routing - server/services/routing - each routing component is represented by a router type e.g. collections, routes, static pages, taxonomies, rss, preview of posts - keep as much as possible logic of routing helpers, middlewares and controllers - ensure test coverage - connect all the things together - yaml file + validation - routing + routers - url service - sitemaps - url access - deeper implementation of yaml validations - e.g. hard require slashes - ensure routing hierarchy/order - e.g. you enable the subscriber app - you have a custom static page, which lives under the same slug /subscribe - static pages are stronger than apps - e.g. the first collection owns the post it has filtered - a post cannot live in two collections - ensure apps are still working and hook into the routers layer (or better said: and register in the routing service) - put as much as possible comments to the code base for better understanding - ensure a clean debug log - ensure we can unmount routes - e.g. you have a collection permalink of /:slug/ represented by {globals.permalink} - and you change the permalink in the admin to dated permalink - the express route get's refreshed from /:slug/ to /:year/:month/:day/:slug/ - unmount without server restart, yey - ensure we are backwards compatible - e.g. render home.hbs for collection index if collection route is / - ensure you can access your configured permalink from the settings table with {globals.permalink} ### Render 503 if url service did not finish - return 503 if the url service has not finished generating the resource urls ### Rewrite sitemaps - we have rewritten the sitemaps "service", because the url generator does no longer happen on runtime - we generate all urls on bootstrap - the sitemaps service will consume created resource and router urls - these urls will be shown on the xml pages - we listen on url events - we listen on router events - we no longer have to fetch the resources, which is nice - the urlservice pre-fetches resources and emits their urls - the urlservice is the only component who knows which urls are valid - i made some ES6 adaptions - we keep the caching logic -> only regenerate xml if there is a change - updated tests - checked test coverage (100%) ### Re-work usage of Url utility - replace all usages of `urlService.utils.urlFor` by `urlService.getByResourceId` - only for resources e.g. post, author, tag - this is important, because with dynamic routing we no longer create static urls based on the settings permalink on runtime - adapt url utility - adapt tests
2018-06-05 20:02:20 +03:00
});
it('should use excerpt when no meta_description is set', function (done) {
data.posts = [posts[0]];
_.each(data.posts, function (post) {
urlService.getUrlByResourceId.withArgs(post.id, {secure: undefined, absolute: true}).returns('http://my-ghost-blog.com/' + post.slug + '/');
});
generateFeed(baseUrl, 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.posts = [posts[3]];
generateFeed(baseUrl, 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);
});
});
});