mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-21 01:41:46 +03:00
0c853a372b
refs https://github.com/TryGhost/Team/issues/1251 With sites that have a huge number of resources, using limit="all" can cause OOM errors at the Node level. Administrators now have the ability to cap limit="all" requests via config. This only affects the get helper used in themes, not the API, this is by design as themes have less visibility of issues.
392 lines
14 KiB
JavaScript
392 lines
14 KiB
JavaScript
const should = require('should');
|
|
const sinon = require('sinon');
|
|
const Promise = require('bluebird');
|
|
const {SafeString} = require('../../../../core/frontend/services/rendering');
|
|
|
|
// Stuff we are testing
|
|
const get = require('../../../../core/frontend/helpers/get');
|
|
|
|
const models = require('../../../../core/server/models');
|
|
const api = require('../../../../core/server/api');
|
|
const proxy = require('../../../../core/frontend/services/proxy');
|
|
|
|
describe('{{#get}} helper', function () {
|
|
let fn;
|
|
let inverse;
|
|
let locals = {};
|
|
|
|
before(function () {
|
|
models.init();
|
|
});
|
|
|
|
beforeEach(function () {
|
|
fn = sinon.spy();
|
|
inverse = sinon.spy();
|
|
|
|
locals = {root: {_locals: {apiVersion: 'v2'}}, globalProp: {foo: 'bar'}};
|
|
});
|
|
|
|
afterEach(function () {
|
|
sinon.restore();
|
|
});
|
|
|
|
describe('context preparation', function () {
|
|
let browsePostsStub;
|
|
const meta = {pagination: {}};
|
|
|
|
beforeEach(function () {
|
|
locals = {root: {_locals: {apiVersion: 'canary'}}};
|
|
|
|
browsePostsStub = sinon.stub(api.canary, 'postsPublic').get(() => {
|
|
return {
|
|
browse: sinon.stub().resolves({posts: [{feature_image_caption: '<a href="#">A link</a>'}], meta: meta})
|
|
};
|
|
});
|
|
});
|
|
|
|
it('converts html strings to SafeString', function (done) {
|
|
get.call(
|
|
{},
|
|
'posts',
|
|
{hash: {}, data: locals, fn: fn, inverse: inverse}
|
|
).then(function () {
|
|
fn.called.should.be.true();
|
|
fn.firstCall.args[0].should.be.an.Object().with.property('posts');
|
|
|
|
fn.firstCall.args[0].posts[0].feature_image_caption.should.be.an.instanceOf(SafeString);
|
|
|
|
done();
|
|
}).catch(done);
|
|
});
|
|
});
|
|
|
|
describe('authors v2', function () {
|
|
let browseAuthorsStub;
|
|
const meta = {pagination: {}};
|
|
|
|
beforeEach(function () {
|
|
locals = {root: {_locals: {apiVersion: 'v2'}}};
|
|
|
|
browseAuthorsStub = sinon.stub(api.v2, 'authorsPublic').get(() => {
|
|
return {
|
|
browse: sinon.stub().resolves({authors: [], meta: meta})
|
|
};
|
|
});
|
|
});
|
|
|
|
it('browse authors', function (done) {
|
|
get.call(
|
|
{},
|
|
'authors',
|
|
{hash: {}, data: locals, fn: fn, inverse: inverse}
|
|
).then(function () {
|
|
fn.called.should.be.true();
|
|
fn.firstCall.args[0].should.be.an.Object().with.property('authors');
|
|
fn.firstCall.args[0].authors.should.eql([]);
|
|
inverse.called.should.be.false();
|
|
|
|
done();
|
|
}).catch(done);
|
|
});
|
|
});
|
|
|
|
describe('authors canary', function () {
|
|
let browseAuthorsStub;
|
|
const meta = {pagination: {}};
|
|
|
|
beforeEach(function () {
|
|
locals = {root: {_locals: {apiVersion: 'canary'}}};
|
|
|
|
browseAuthorsStub = sinon.stub(api.canary, 'authorsPublic').get(() => {
|
|
return {
|
|
browse: sinon.stub().resolves({authors: [], meta: meta})
|
|
};
|
|
});
|
|
});
|
|
|
|
it('browse authors', function (done) {
|
|
get.call(
|
|
{},
|
|
'authors',
|
|
{hash: {}, data: locals, fn: fn, inverse: inverse}
|
|
).then(function () {
|
|
fn.called.should.be.true();
|
|
fn.firstCall.args[0].should.be.an.Object().with.property('authors');
|
|
fn.firstCall.args[0].authors.should.eql([]);
|
|
inverse.called.should.be.false();
|
|
|
|
done();
|
|
}).catch(done);
|
|
});
|
|
});
|
|
|
|
describe('authors v3', function () {
|
|
let browseAuthorsStub;
|
|
const meta = {pagination: {}};
|
|
|
|
beforeEach(function () {
|
|
locals = {root: {_locals: {apiVersion: 'v3'}}};
|
|
|
|
browseAuthorsStub = sinon.stub(api.v3, 'authorsPublic').get(() => {
|
|
return {
|
|
browse: sinon.stub().resolves({authors: [], meta: meta})
|
|
};
|
|
});
|
|
});
|
|
|
|
it('browse authors', function (done) {
|
|
get.call(
|
|
{},
|
|
'authors',
|
|
{hash: {}, data: locals, fn: fn, inverse: inverse}
|
|
).then(function () {
|
|
fn.called.should.be.true();
|
|
fn.firstCall.args[0].should.be.an.Object().with.property('authors');
|
|
fn.firstCall.args[0].authors.should.eql([]);
|
|
inverse.called.should.be.false();
|
|
|
|
done();
|
|
}).catch(done);
|
|
});
|
|
});
|
|
|
|
describe('general error handling', function () {
|
|
it('should return an error for an unknown resource', function (done) {
|
|
get.call(
|
|
{},
|
|
'magic',
|
|
{hash: {}, data: locals, fn: fn, inverse: inverse}
|
|
).then(function () {
|
|
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.eql('Invalid resource given to get helper');
|
|
|
|
done();
|
|
}).catch(done);
|
|
});
|
|
|
|
it('should handle error from the API', function (done) {
|
|
get.call(
|
|
{},
|
|
'posts',
|
|
{hash: {slug: 'thing!'}, data: locals, fn: fn, inverse: inverse}
|
|
).then(function () {
|
|
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(/^Validation/);
|
|
|
|
done();
|
|
}).catch(done);
|
|
});
|
|
|
|
it('should show warning for call without any options', function (done) {
|
|
get.call(
|
|
{},
|
|
'posts',
|
|
{data: locals}
|
|
).then(function () {
|
|
fn.called.should.be.false();
|
|
inverse.called.should.be.false();
|
|
|
|
done();
|
|
}).catch(done);
|
|
});
|
|
});
|
|
|
|
describe('path resolution', function () {
|
|
let browseStub;
|
|
let readStub;
|
|
const pubDate = new Date();
|
|
|
|
const resource = {
|
|
post: {id: 3, title: 'Test 3', author: {slug: 'cameron'}, tags: [{slug: 'test'}, {slug: 'magic'}], published_at: pubDate}
|
|
};
|
|
|
|
beforeEach(function () {
|
|
browseStub = sinon.stub().resolves();
|
|
readStub = sinon.stub().resolves();
|
|
sinon.stub(api.v2, 'postsPublic').get(() => {
|
|
return {
|
|
browse: browseStub,
|
|
read: readStub
|
|
};
|
|
});
|
|
});
|
|
|
|
it('should resolve post.tags alias', function (done) {
|
|
get.call(
|
|
resource,
|
|
'posts',
|
|
{hash: {filter: 'tags:[{{post.tags}}]'}, data: locals, fn: fn, inverse: inverse}
|
|
).then(function () {
|
|
browseStub.firstCall.args.should.be.an.Array().with.lengthOf(1);
|
|
browseStub.firstCall.args[0].should.be.an.Object().with.property('filter');
|
|
browseStub.firstCall.args[0].filter.should.eql('tags:[test,magic]');
|
|
|
|
done();
|
|
}).catch(done);
|
|
});
|
|
|
|
it('should resolve post.author alias', function (done) {
|
|
get.call(
|
|
resource,
|
|
'posts',
|
|
{hash: {filter: 'author:{{post.author}}'}, data: locals, fn: fn, inverse: inverse}
|
|
).then(function () {
|
|
browseStub.firstCall.args.should.be.an.Array().with.lengthOf(1);
|
|
browseStub.firstCall.args[0].should.be.an.Object().with.property('filter');
|
|
browseStub.firstCall.args[0].filter.should.eql('author:cameron');
|
|
|
|
done();
|
|
}).catch(done);
|
|
});
|
|
|
|
it('should resolve basic path', function (done) {
|
|
get.call(
|
|
resource,
|
|
'posts',
|
|
{hash: {filter: 'id:-{{post.id}}'}, data: locals, fn: fn, inverse: inverse}
|
|
).then(function () {
|
|
browseStub.firstCall.args.should.be.an.Array().with.lengthOf(1);
|
|
browseStub.firstCall.args[0].should.be.an.Object().with.property('filter');
|
|
browseStub.firstCall.args[0].filter.should.eql('id:-3');
|
|
|
|
done();
|
|
}).catch(done);
|
|
});
|
|
|
|
it('should handle arrays the same as handlebars', function (done) {
|
|
get.call(
|
|
resource,
|
|
'posts',
|
|
{hash: {filter: 'tags:{{post.tags.[0].slug}}'}, data: locals, fn: fn, inverse: inverse}
|
|
).then(function () {
|
|
browseStub.firstCall.args.should.be.an.Array().with.lengthOf(1);
|
|
browseStub.firstCall.args[0].should.be.an.Object().with.property('filter');
|
|
browseStub.firstCall.args[0].filter.should.eql('tags:test');
|
|
|
|
done();
|
|
}).catch(done);
|
|
});
|
|
|
|
it('should handle dates', function (done) {
|
|
get.call(
|
|
resource,
|
|
'posts',
|
|
{hash: {filter: 'published_at:<=\'{{post.published_at}}\''}, data: locals, fn: fn, inverse: inverse}
|
|
).then(function () {
|
|
browseStub.firstCall.args.should.be.an.Array().with.lengthOf(1);
|
|
browseStub.firstCall.args[0].should.be.an.Object().with.property('filter');
|
|
browseStub.firstCall.args[0].filter.should.eql(`published_at:<='${pubDate.toISOString()}'`);
|
|
|
|
done();
|
|
}).catch(done);
|
|
});
|
|
|
|
it('should output nothing if path does not resolve', function (done) {
|
|
get.call(
|
|
resource,
|
|
'posts',
|
|
{hash: {filter: 'id:{{post.thing}}'}, data: locals, fn: fn, inverse: inverse}
|
|
).then(function () {
|
|
browseStub.firstCall.args.should.be.an.Array().with.lengthOf(1);
|
|
browseStub.firstCall.args[0].should.be.an.Object().with.property('filter');
|
|
browseStub.firstCall.args[0].filter.should.eql('id:');
|
|
|
|
done();
|
|
}).catch(done);
|
|
});
|
|
|
|
it('should resolve global props', function (done) {
|
|
get.call(
|
|
resource,
|
|
'posts',
|
|
{hash: {filter: 'slug:{{@globalProp.foo}}'}, data: locals, fn: fn, inverse: inverse}
|
|
).then(function () {
|
|
browseStub.firstCall.args.should.be.an.Array().with.lengthOf(1);
|
|
browseStub.firstCall.args[0].should.be.an.Object().with.property('filter');
|
|
browseStub.firstCall.args[0].filter.should.eql('slug:bar');
|
|
|
|
done();
|
|
}).catch(done);
|
|
});
|
|
});
|
|
|
|
describe('limit=all max', function () {
|
|
let browseStub;
|
|
|
|
beforeEach(function () {
|
|
browseStub = sinon.stub().resolves();
|
|
sinon.stub(api.v2, 'postsPublic').get(() => {
|
|
return {
|
|
browse: browseStub
|
|
};
|
|
});
|
|
sinon.stub(api.v3, 'postsPublic').get(() => {
|
|
return {
|
|
browse: browseStub
|
|
};
|
|
});
|
|
sinon.stub(api.canary, 'postsPublic').get(() => {
|
|
return {
|
|
browse: browseStub
|
|
};
|
|
});
|
|
});
|
|
|
|
it('Behaves normally without config', async function () {
|
|
locals = {root: {_locals: {apiVersion: 'v2'}}};
|
|
await get.call(
|
|
{},
|
|
'posts',
|
|
{hash: {limit: 'all'}, data: locals, fn: fn, inverse: inverse}
|
|
);
|
|
browseStub.firstCall.args[0].limit.should.eql('all');
|
|
locals = {root: {_locals: {apiVersion: 'v3'}}};
|
|
await get.call(
|
|
{},
|
|
'posts',
|
|
{hash: {limit: 'all'}, data: locals, fn: fn, inverse: inverse}
|
|
);
|
|
browseStub.secondCall.args[0].limit.should.eql('all');
|
|
locals = {root: {_locals: {apiVersion: 'canary'}}};
|
|
await get.call(
|
|
{},
|
|
'posts',
|
|
{hash: {limit: 'all'}, data: locals, fn: fn, inverse: inverse}
|
|
);
|
|
browseStub.thirdCall.args[0].limit.should.eql('all');
|
|
});
|
|
|
|
it('Replaces "all" with "getHelperLimitAllMax" config, if present', async function () {
|
|
sinon.stub(proxy.config, 'get').withArgs('getHelperLimitAllMax').returns(2);
|
|
|
|
locals = {root: {_locals: {apiVersion: 'v2'}}};
|
|
await get.call(
|
|
{},
|
|
'posts',
|
|
{hash: {limit: 'all'}, data: locals, fn: fn, inverse: inverse}
|
|
);
|
|
browseStub.firstCall.args[0].limit.should.eql(2);
|
|
locals = {root: {_locals: {apiVersion: 'v3'}}};
|
|
await get.call(
|
|
{},
|
|
'posts',
|
|
{hash: {limit: 'all'}, data: locals, fn: fn, inverse: inverse}
|
|
);
|
|
browseStub.secondCall.args[0].limit.should.eql(2);
|
|
locals = {root: {_locals: {apiVersion: 'canary'}}};
|
|
await get.call(
|
|
{},
|
|
'posts',
|
|
{hash: {limit: 'all'}, data: locals, fn: fn, inverse: inverse}
|
|
);
|
|
browseStub.thirdCall.args[0].limit.should.eql(2);
|
|
});
|
|
});
|
|
});
|