Ghost/test/unit/frontend/helpers/next_post.test.js
Hannah Wolfe a4a9ba7940
🔥 Removed versioned APIs
refs: https://github.com/TryGhost/Toolbox/issues/229

- we are getting rid of the concept of having multiple api versions in a single ghost install
- removed all the code for multiple api versions & left canary wired up, but without the version in the URL
- TODO: reorganise the folders so there's no canary folder when we're closer to shipping
        we need to minimise the pain of merging changes across from main for now
2022-04-28 15:37:09 +01:00

445 lines
17 KiB
JavaScript

const errors = require('@tryghost/errors');
const sinon = require('sinon');
const Promise = require('bluebird');
const markdownToMobiledoc = require('../../../utils/fixtures/data-generator').markdownToMobiledoc;
const next_post = require('../../../../core/frontend/helpers/prev_post');
const api = require('../../../../core/frontend/services/proxy').api;
const should = require('should');
describe('{{next_post}} helper', function () {
let locals;
let browsePostsStub;
beforeEach(function () {
locals = {
root: {
_locals: {},
context: ['post']
}
};
sinon.stub(api, 'postsPublic').get(() => {
return {
browse: browsePostsStub
};
});
});
afterEach(function () {
sinon.restore();
});
describe('with valid post data - ', function () {
beforeEach(function () {
browsePostsStub = sinon.stub().callsFake(function (options) {
if (options.filter.indexOf('published_at:>') > -1) {
return Promise.resolve({
posts: [{slug: '/next/', title: 'post 3'}]
});
}
});
});
it('shows \'if\' template with next post data', async function () {
const fn = sinon.spy();
const inverse = sinon.spy();
const optionsData = {name: 'next_post', data: locals, fn: fn, inverse: inverse};
await next_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
published_at: new Date(0),
url: '/current/'
}, optionsData);
fn.calledOnce.should.be.true();
inverse.calledOnce.should.be.false();
fn.firstCall.args.should.have.lengthOf(2);
fn.firstCall.args[0].should.have.properties('slug', 'title');
fn.firstCall.args[1].should.be.an.Object().and.have.property('data');
browsePostsStub.calledOnce.should.be.true();
browsePostsStub.firstCall.args[0].include.should.eql('author,authors,tags,tiers');
});
});
describe('for valid post with no next post', function () {
beforeEach(function () {
browsePostsStub = sinon.stub().callsFake(function (options) {
if (options.filter.indexOf('published_at:>') > -1) {
return Promise.resolve({
posts: []
});
}
});
});
it('shows \'else\' template', async function () {
const fn = sinon.spy();
const inverse = sinon.spy();
const optionsData = {name: 'next_post', data: locals, fn: fn, inverse: inverse};
await next_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
published_at: new Date(0),
url: '/current/'
}, optionsData);
fn.called.should.be.false();
inverse.called.should.be.true();
inverse.firstCall.args.should.have.lengthOf(2);
inverse.firstCall.args[0].should.have.properties('slug', 'title');
inverse.firstCall.args[1].should.be.an.Object().and.have.property('data');
});
});
describe('for invalid post data', function () {
beforeEach(function () {
browsePostsStub = sinon.stub().callsFake(function (options) {
if (options.filter.indexOf('published_at:>') > -1) {
return Promise.resolve({});
}
});
});
it('shows \'else\' template', async function () {
const fn = sinon.spy();
const inverse = sinon.spy();
const optionsData = {name: 'next_post', data: locals, fn: fn, inverse: inverse};
await next_post
.call({}, optionsData);
fn.called.should.be.false();
inverse.called.should.be.true();
browsePostsStub.called.should.be.false();
});
});
describe('for page', function () {
beforeEach(function () {
locals = {
root: {
_locals: {},
context: ['page']
}
};
browsePostsStub = sinon.stub().callsFake(function (options) {
if (options.filter.indexOf('published_at:>') > -1) {
return Promise.resolve({posts: [{slug: '/previous/', title: 'post 1'}]});
}
});
});
it('shows \'else\' template', async function () {
const fn = sinon.spy();
const inverse = sinon.spy();
const optionsData = {name: 'next_post', data: locals, fn: fn, inverse: inverse};
await next_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
published_at: new Date(0),
url: '/current/',
page: true
}, optionsData);
fn.called.should.be.false();
inverse.called.should.be.true();
});
});
describe('for unpublished post', function () {
beforeEach(function () {
locals = {
root: {
context: ['preview', 'post']
}
};
browsePostsStub = sinon.stub().callsFake(function (options) {
if (options.filter.indexOf('published_at:>') > -1) {
return Promise.resolve({posts: [{slug: '/next/', title: 'post 3'}]});
}
});
});
it('shows \'else\' template', async function () {
const fn = sinon.spy();
const inverse = sinon.spy();
const optionsData = {name: 'next_post', data: locals, fn: fn, inverse: inverse};
await next_post
.call({
html: 'content',
status: 'draft',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
created_at: new Date(0),
url: '/current/'
}, optionsData);
fn.called.should.be.false();
inverse.called.should.be.true();
});
});
describe('with "in" option', function () {
beforeEach(function () {
browsePostsStub = sinon.stub().callsFake(function (options) {
if (options.filter.indexOf('published_at:>') > -1) {
return Promise.resolve({
posts: [{slug: '/next/', title: 'post 1'}]
});
}
});
});
it('shows \'if\' template with prev post data with primary_tag set', async function () {
const fn = sinon.spy();
const inverse = sinon.spy();
const optionsData = {name: 'next_post', data: locals, fn: fn, inverse: inverse, hash: {in: 'primary_tag'}};
await next_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
published_at: new Date(0),
primary_tag: {slug: 'test'},
url: '/current/'
}, optionsData);
fn.calledOnce.should.be.true();
inverse.calledOnce.should.be.false();
fn.firstCall.args.should.have.lengthOf(2);
fn.firstCall.args[0].should.have.properties('slug', 'title');
fn.firstCall.args[1].should.be.an.Object().and.have.property('data');
browsePostsStub.calledOnce.should.be.true();
browsePostsStub.firstCall.args[0].include.should.eql('author,authors,tags,tiers');
browsePostsStub.firstCall.args[0].filter.should.match(/\+primary_tag:test/);
});
it('shows \'if\' template with prev post data with primary_author set', async function () {
const fn = sinon.spy();
const inverse = sinon.spy();
const optionsData = {name: 'next_post', data: locals, fn: fn, inverse: inverse, hash: {in: 'primary_author'}};
await next_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
published_at: new Date(0),
primary_author: {slug: 'hans'},
url: '/current/'
}, optionsData);
fn.calledOnce.should.be.true();
inverse.calledOnce.should.be.false();
fn.firstCall.args.should.have.lengthOf(2);
fn.firstCall.args[0].should.have.properties('slug', 'title');
fn.firstCall.args[1].should.be.an.Object().and.have.property('data');
browsePostsStub.calledOnce.should.be.true();
browsePostsStub.firstCall.args[0].include.should.eql('author,authors,tags,tiers');
browsePostsStub.firstCall.args[0].filter.should.match(/\+primary_author:hans/);
});
it('shows \'if\' template with prev post data with author set', async function () {
const fn = sinon.spy();
const inverse = sinon.spy();
const optionsData = {name: 'next_post', data: locals, fn: fn, inverse: inverse, hash: {in: 'author'}};
await next_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
published_at: new Date(0),
author: {slug: 'author-name'},
url: '/current/'
}, optionsData);
fn.calledOnce.should.be.true();
inverse.calledOnce.should.be.false();
fn.firstCall.args.should.have.lengthOf(2);
fn.firstCall.args[0].should.have.properties('slug', 'title');
fn.firstCall.args[1].should.be.an.Object().and.have.property('data');
browsePostsStub.calledOnce.should.be.true();
browsePostsStub.firstCall.args[0].include.should.eql('author,authors,tags,tiers');
browsePostsStub.firstCall.args[0].filter.should.match(/\+author:author-name/);
});
it('shows \'if\' template with prev post data & ignores in author if author isnt present', async function () {
const fn = sinon.spy();
const inverse = sinon.spy();
const optionsData = {name: 'next_post', data: locals, fn: fn, inverse: inverse, hash: {in: 'author'}};
await next_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
published_at: new Date(0),
url: '/current/'
}, optionsData);
fn.calledOnce.should.be.true();
inverse.calledOnce.should.be.false();
fn.firstCall.args.should.have.lengthOf(2);
fn.firstCall.args[0].should.have.properties('slug', 'title');
fn.firstCall.args[1].should.be.an.Object().and.have.property('data');
browsePostsStub.calledOnce.should.be.true();
browsePostsStub.firstCall.args[0].include.should.eql('author,authors,tags,tiers');
browsePostsStub.firstCall.args[0].filter.should.not.match(/\+author:/);
});
it('shows \'if\' template with prev post data & ignores unknown in value', async function () {
const fn = sinon.spy();
const inverse = sinon.spy();
const optionsData = {name: 'next_post', data: locals, fn: fn, inverse: inverse, hash: {in: 'magic'}};
await next_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
published_at: new Date(0),
author: {slug: 'author-name'},
url: '/current/'
}, optionsData);
fn.calledOnce.should.be.true();
inverse.calledOnce.should.be.false();
fn.firstCall.args.should.have.lengthOf(2);
fn.firstCall.args[0].should.have.properties('slug', 'title');
fn.firstCall.args[1].should.be.an.Object().and.have.property('data');
browsePostsStub.calledOnce.should.be.true();
browsePostsStub.firstCall.args[0].include.should.eql('author,authors,tags,tiers');
browsePostsStub.firstCall.args[0].filter.should.not.match(/\+magic/);
});
});
describe('general error handling', function () {
beforeEach(function () {
browsePostsStub = sinon.stub().callsFake(function () {
return Promise.reject(new errors.NotFoundError({message: 'Something wasn\'t found'}));
});
});
it('should handle error from the API', async function () {
const fn = sinon.spy();
const inverse = sinon.spy();
const optionsData = {name: 'next_post', data: locals, fn: fn, inverse: inverse};
await next_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
published_at: new Date(0),
url: '/current/'
}, optionsData);
fn.called.should.be.false();
inverse.calledOnce.should.be.true();
inverse.firstCall.args[1].should.be.an.Object().and.have.property('data');
inverse.firstCall.args[1].data.should.be.an.Object().and.have.property('error');
inverse.firstCall.args[1].data.error.should.match(/^Something wasn't found/);
});
it('should show warning for call without any options', async function () {
const fn = sinon.spy();
const inverse = sinon.spy();
const optionsData = {name: 'next_post', data: {root: {}}};
await next_post
.call(
{},
optionsData
);
fn.called.should.be.false();
inverse.called.should.be.false();
});
});
describe('auth', function () {
let member;
beforeEach(function () {
member = {uuid: 'test'};
browsePostsStub = sinon.stub().callsFake(function (options) {
return Promise.resolve({
posts: [{slug: '/next/', title: 'post 3'}]
});
});
locals = {
root: {
_locals: {},
context: ['post']
},
member
};
});
it('should pass the member context', async function () {
const fn = sinon.spy();
const inverse = sinon.spy();
const optionsData = {name: 'next_post', data: locals, fn: fn, inverse: inverse};
await next_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
published_at: new Date(0),
url: '/current/'
}, optionsData);
fn.calledOnce.should.be.true();
inverse.calledOnce.should.be.false();
fn.firstCall.args.should.have.lengthOf(2);
fn.firstCall.args[0].should.have.properties('slug', 'title');
fn.firstCall.args[1].should.be.an.Object().and.have.property('data');
browsePostsStub.calledOnce.should.be.true();
browsePostsStub.firstCall.args[0].include.should.eql('author,authors,tags,tiers');
// Check context passed
browsePostsStub.firstCall.args[0].context.member.should.eql(member);
});
});
});