mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-23 02:41:50 +03:00
6b07d4b2a0
refs https://github.com/TryGhost/Team/issues/467
refs a6f5eb71be
- When a generated excerpt is calculated for posts/page resources it uses raw model! to get the data. Model contains untranformed __GHOST_URL__ markup which has to be additionally processed before extracint an excerpt or use the transformed `plaintext` from available attributes (chose the latter to decrease complexity)
- Removed model dependency as `attrs` at this point of serialization should always contain the `plaintext` field. It's ugly and has an unsolved bug report here - https://github.com/TryGhost/Ghost/issues/10396. The reliance should be solved at some point, but definitely not a part of this issue
450 lines
20 KiB
JavaScript
450 lines
20 KiB
JavaScript
const should = require('should');
|
|
const sinon = require('sinon');
|
|
const moment = require('moment');
|
|
const supertest = require('supertest');
|
|
const _ = require('lodash');
|
|
const labs = require('../../../../../core/server/services/labs');
|
|
const testUtils = require('../../../../utils');
|
|
const localUtils = require('./utils');
|
|
const configUtils = require('../../../../utils/configUtils');
|
|
const urlUtils = require('../../../../utils/urlUtils');
|
|
const config = require('../../../../../core/shared/config');
|
|
|
|
const ghost = testUtils.startGhost;
|
|
let request;
|
|
|
|
describe('api/v3/content/posts', function () {
|
|
before(function () {
|
|
return ghost()
|
|
.then(function () {
|
|
request = supertest.agent(config.get('url'));
|
|
})
|
|
.then(function () {
|
|
return testUtils.initFixtures('users:no-owner', 'user:inactive', 'posts', 'tags:extra', 'api_keys');
|
|
});
|
|
});
|
|
|
|
afterEach(function () {
|
|
configUtils.restore();
|
|
urlUtils.restore();
|
|
});
|
|
|
|
const validKey = localUtils.getValidKey();
|
|
|
|
it('browse posts', function (done) {
|
|
request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}`))
|
|
.set('Origin', testUtils.API.getURL())
|
|
.expect('Content-Type', /json/)
|
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
.expect(200)
|
|
.end(function (err, res) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
res.headers.vary.should.eql('Accept-Encoding');
|
|
should.exist(res.headers['access-control-allow-origin']);
|
|
should.not.exist(res.headers['x-cache-invalidate']);
|
|
|
|
const jsonResponse = res.body;
|
|
should.exist(jsonResponse.posts);
|
|
localUtils.API.checkResponse(jsonResponse, 'posts');
|
|
jsonResponse.posts.should.have.length(11);
|
|
localUtils.API.checkResponse(jsonResponse.posts[0], 'post');
|
|
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
|
|
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
|
|
|
|
// Default order 'published_at desc' check
|
|
jsonResponse.posts[0].slug.should.eql('welcome');
|
|
jsonResponse.posts[6].slug.should.eql('integrations');
|
|
|
|
// check meta response for this test
|
|
jsonResponse.meta.pagination.page.should.eql(1);
|
|
jsonResponse.meta.pagination.limit.should.eql(15);
|
|
jsonResponse.meta.pagination.pages.should.eql(1);
|
|
jsonResponse.meta.pagination.total.should.eql(11);
|
|
jsonResponse.meta.pagination.hasOwnProperty('next').should.be.true();
|
|
jsonResponse.meta.pagination.hasOwnProperty('prev').should.be.true();
|
|
should.not.exist(jsonResponse.meta.pagination.next);
|
|
should.not.exist(jsonResponse.meta.pagination.prev);
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('browse posts with related authors/tags also returns primary_author/primary_tag', function (done) {
|
|
request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}&include=authors,tags`))
|
|
.set('Origin', testUtils.API.getURL())
|
|
.expect('Content-Type', /json/)
|
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
.expect(200)
|
|
.end(function (err, res) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
res.headers.vary.should.eql('Accept-Encoding');
|
|
should.exist(res.headers['access-control-allow-origin']);
|
|
should.not.exist(res.headers['x-cache-invalidate']);
|
|
|
|
const jsonResponse = res.body;
|
|
should.exist(jsonResponse.posts);
|
|
localUtils.API.checkResponse(jsonResponse, 'posts');
|
|
jsonResponse.posts.should.have.length(11);
|
|
localUtils.API.checkResponse(
|
|
jsonResponse.posts[0],
|
|
'post',
|
|
['authors', 'tags', 'primary_tag', 'primary_author'],
|
|
null
|
|
);
|
|
|
|
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
|
|
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
|
|
|
|
// Default order 'published_at desc' check
|
|
jsonResponse.posts[0].slug.should.eql('welcome');
|
|
jsonResponse.posts[6].slug.should.eql('integrations');
|
|
|
|
// check meta response for this test
|
|
jsonResponse.meta.pagination.page.should.eql(1);
|
|
jsonResponse.meta.pagination.limit.should.eql(15);
|
|
jsonResponse.meta.pagination.pages.should.eql(1);
|
|
jsonResponse.meta.pagination.total.should.eql(11);
|
|
jsonResponse.meta.pagination.hasOwnProperty('next').should.be.true();
|
|
jsonResponse.meta.pagination.hasOwnProperty('prev').should.be.true();
|
|
should.not.exist(jsonResponse.meta.pagination.next);
|
|
should.not.exist(jsonResponse.meta.pagination.prev);
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('browse posts with basic page filter should not return pages', function (done) {
|
|
request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}&filter=page:true`))
|
|
.expect('Content-Type', /json/)
|
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
.expect(200)
|
|
.end(function (err, res) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
const jsonResponse = res.body;
|
|
|
|
should.not.exist(res.headers['x-cache-invalidate']);
|
|
should.exist(jsonResponse.posts);
|
|
localUtils.API.checkResponse(jsonResponse, 'posts');
|
|
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
|
|
jsonResponse.posts.should.have.length(0);
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('browse posts with basic page filter should not return pages', function (done) {
|
|
request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}&filter=page:true,featured:true`))
|
|
.expect('Content-Type', /json/)
|
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
.expect(200)
|
|
.end(function (err, res) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
const jsonResponse = res.body;
|
|
|
|
should.not.exist(res.headers['x-cache-invalidate']);
|
|
should.exist(jsonResponse.posts);
|
|
localUtils.API.checkResponse(jsonResponse, 'posts');
|
|
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
|
|
jsonResponse.posts.should.have.length(2);
|
|
jsonResponse.posts.filter(p => (p.page === true)).should.have.length(0);
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('browse posts with published and draft status, should not return drafts', function (done) {
|
|
request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}&filter=status:published,status:draft`))
|
|
.expect('Content-Type', /json/)
|
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
.expect(200)
|
|
.end(function (err, res) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
const jsonResponse = res.body;
|
|
|
|
jsonResponse.posts.should.be.an.Array().with.lengthOf(11);
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('browse posts with slug filter, should order in slug order', function () {
|
|
return request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}&filter=slug:[write,ghostly-kitchen-sink,grow]`))
|
|
.expect('Content-Type', /json/)
|
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
.expect(200)
|
|
.then((res) => {
|
|
const jsonResponse = res.body;
|
|
|
|
jsonResponse.posts.should.be.an.Array().with.lengthOf(3);
|
|
jsonResponse.posts[0].slug.should.equal('write');
|
|
jsonResponse.posts[1].slug.should.equal('ghostly-kitchen-sink');
|
|
jsonResponse.posts[2].slug.should.equal('grow');
|
|
});
|
|
});
|
|
|
|
it('browse posts with slug filter should order taking order parameter into account', function () {
|
|
return request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}&order=slug%20DESC&filter=slug:[write,ghostly-kitchen-sink,grow]`))
|
|
.expect('Content-Type', /json/)
|
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
.expect(200)
|
|
.then((res) => {
|
|
const jsonResponse = res.body;
|
|
|
|
jsonResponse.posts.should.be.an.Array().with.lengthOf(3);
|
|
jsonResponse.posts[0].slug.should.equal('write');
|
|
jsonResponse.posts[1].slug.should.equal('grow');
|
|
jsonResponse.posts[2].slug.should.equal('ghostly-kitchen-sink');
|
|
});
|
|
});
|
|
|
|
it('ensure origin header on redirect is not getting lost', function (done) {
|
|
// NOTE: force a redirect to the admin url
|
|
configUtils.set('admin:url', 'http://localhost:9999');
|
|
urlUtils.stubUrlUtilsFromConfig();
|
|
|
|
request.get(localUtils.API.getApiQuery(`posts?key=${validKey}`))
|
|
.set('Origin', 'https://example.com')
|
|
// 301 Redirects _should_ be cached
|
|
.expect('Cache-Control', testUtils.cacheRules.year)
|
|
.expect(301)
|
|
.end(function (err, res) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
res.headers.vary.should.eql('Accept, Accept-Encoding');
|
|
res.headers.location.should.eql(`http://localhost:9999/ghost/api/v3/content/posts/?key=${validKey}`);
|
|
should.exist(res.headers['access-control-allow-origin']);
|
|
should.not.exist(res.headers['x-cache-invalidate']);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('can\'t read page', function () {
|
|
return request
|
|
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[5].id}/?key=${validKey}`))
|
|
.set('Origin', testUtils.API.getURL())
|
|
.expect('Content-Type', /json/)
|
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
.expect(404);
|
|
});
|
|
|
|
it('can read post with fields', function () {
|
|
const complexPostId = testUtils.DataGenerator.Content.posts.find(p => p.slug === 'not-so-short-bit-complex').id;
|
|
|
|
return request
|
|
.get(localUtils.API.getApiQuery(`posts/${complexPostId}/?key=${validKey}&fields=title,slug,excerpt&formats=plaintext`))
|
|
.set('Origin', testUtils.API.getURL())
|
|
.expect('Content-Type', /json/)
|
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
.expect(200)
|
|
.then((res) => {
|
|
localUtils.API.checkResponse(res.body.posts[0], 'post', null, null, ['id', 'title', 'slug', 'excerpt', 'plaintext']);
|
|
|
|
// excerpt should transform links to absolute URLs
|
|
res.body.posts[0].excerpt.should.match(/\* Aliquam \[http:\/\/127.0.0.1:2369\/about#nowhere\]/);
|
|
});
|
|
});
|
|
|
|
describe('content gating', function () {
|
|
let publicPost;
|
|
let membersPost;
|
|
let paidPost;
|
|
let membersPostWithPaywallCard;
|
|
|
|
before(function () {
|
|
// NOTE: ideally this would be set through Admin API request not a stub
|
|
sinon.stub(labs, 'isSet').withArgs('members').returns(true);
|
|
});
|
|
|
|
before (function () {
|
|
publicPost = testUtils.DataGenerator.forKnex.createPost({
|
|
slug: 'free-to-see',
|
|
visibility: 'public',
|
|
published_at: moment().add(15, 'seconds').toDate() // here to ensure sorting is not modified
|
|
});
|
|
|
|
membersPost = testUtils.DataGenerator.forKnex.createPost({
|
|
slug: 'thou-shalt-not-be-seen',
|
|
visibility: 'members',
|
|
published_at: moment().add(45, 'seconds').toDate() // here to ensure sorting is not modified
|
|
});
|
|
|
|
paidPost = testUtils.DataGenerator.forKnex.createPost({
|
|
slug: 'thou-shalt-be-paid-for',
|
|
visibility: 'paid',
|
|
published_at: moment().add(30, 'seconds').toDate() // here to ensure sorting is not modified
|
|
});
|
|
|
|
membersPostWithPaywallCard = testUtils.DataGenerator.forKnex.createPost({
|
|
slug: 'thou-shalt-have-a-taste',
|
|
visibility: 'members',
|
|
mobiledoc: '{"version":"0.3.1","markups":[],"atoms":[],"cards":[["paywall",{}]],"sections":[[1,"p",[[0,[],0,"Free content"]]],[10,0],[1,"p",[[0,[],0,"Members content"]]]]}',
|
|
html: '<p>Free content</p><!--members-only--><p>Members content</p>',
|
|
published_at: moment().add(5, 'seconds').toDate()
|
|
});
|
|
|
|
return testUtils.fixtures.insertPosts([
|
|
publicPost,
|
|
membersPost,
|
|
paidPost,
|
|
membersPostWithPaywallCard
|
|
]);
|
|
});
|
|
|
|
it('public post fields are always visible', function () {
|
|
return request
|
|
.get(localUtils.API.getApiQuery(`posts/${publicPost.id}/?key=${validKey}&fields=slug,html,plaintext&formats=html,plaintext`))
|
|
.set('Origin', testUtils.API.getURL())
|
|
.expect('Content-Type', /json/)
|
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
.expect(200)
|
|
.then((res) => {
|
|
const jsonResponse = res.body;
|
|
should.exist(jsonResponse.posts);
|
|
const post = jsonResponse.posts[0];
|
|
|
|
localUtils.API.checkResponse(post, 'post', null, null, ['id', 'slug', 'html', 'plaintext']);
|
|
post.slug.should.eql('free-to-see');
|
|
post.html.should.not.eql('');
|
|
post.plaintext.should.not.eql('');
|
|
});
|
|
});
|
|
|
|
it('cannot read members only post content', function () {
|
|
return request
|
|
.get(localUtils.API.getApiQuery(`posts/${membersPost.id}/?key=${validKey}`))
|
|
.set('Origin', testUtils.API.getURL())
|
|
.expect('Content-Type', /json/)
|
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
.expect(200)
|
|
.then((res) => {
|
|
const jsonResponse = res.body;
|
|
should.exist(jsonResponse.posts);
|
|
const post = jsonResponse.posts[0];
|
|
|
|
localUtils.API.checkResponse(post, 'post', null, null);
|
|
post.slug.should.eql('thou-shalt-not-be-seen');
|
|
post.html.should.eql('');
|
|
post.excerpt.should.eql('');
|
|
});
|
|
});
|
|
|
|
it('cannot read paid only post content', function () {
|
|
return request
|
|
.get(localUtils.API.getApiQuery(`posts/${paidPost.id}/?key=${validKey}`))
|
|
.set('Origin', testUtils.API.getURL())
|
|
.expect('Content-Type', /json/)
|
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
.expect(200)
|
|
.then((res) => {
|
|
const jsonResponse = res.body;
|
|
should.exist(jsonResponse.posts);
|
|
const post = jsonResponse.posts[0];
|
|
|
|
localUtils.API.checkResponse(post, 'post', null, null);
|
|
post.slug.should.eql('thou-shalt-be-paid-for');
|
|
post.html.should.eql('');
|
|
post.excerpt.should.eql('');
|
|
});
|
|
});
|
|
|
|
it('cannot read members only post plaintext', function () {
|
|
return request
|
|
.get(localUtils.API.getApiQuery(`posts/${membersPost.id}/?key=${validKey}&formats=html,plaintext&fields=html,plaintext`))
|
|
.set('Origin', testUtils.API.getURL())
|
|
.expect('Content-Type', /json/)
|
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
.expect(200)
|
|
.then((res) => {
|
|
const jsonResponse = res.body;
|
|
should.exist(jsonResponse.posts);
|
|
const post = jsonResponse.posts[0];
|
|
|
|
localUtils.API.checkResponse(post, 'post', null, null, ['id', 'html', 'plaintext']);
|
|
post.html.should.eql('');
|
|
post.plaintext.should.eql('');
|
|
});
|
|
});
|
|
|
|
it('can read "free" html and plaintext content of members post when using paywall card', function () {
|
|
return request
|
|
.get(localUtils.API.getApiQuery(`posts/${membersPostWithPaywallCard.id}/?key=${validKey}&formats=html,plaintext`))
|
|
.set('Origin', testUtils.API.getURL())
|
|
.expect('Content-Type', /json/)
|
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
.expect(200)
|
|
.then((res) => {
|
|
const jsonResponse = res.body;
|
|
should.exist(jsonResponse.posts);
|
|
const post = jsonResponse.posts[0];
|
|
|
|
localUtils.API.checkResponse(post, 'post', ['plaintext']);
|
|
post.html.should.eql('<p>Free content</p>');
|
|
post.plaintext.should.eql('Free content');
|
|
});
|
|
});
|
|
|
|
it('cannot browse members only posts content', function () {
|
|
return request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}`))
|
|
.set('Origin', testUtils.API.getURL())
|
|
.expect('Content-Type', /json/)
|
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
.expect(200)
|
|
.then((res) => {
|
|
res.headers.vary.should.eql('Accept-Encoding');
|
|
should.exist(res.headers['access-control-allow-origin']);
|
|
should.not.exist(res.headers['x-cache-invalidate']);
|
|
|
|
const jsonResponse = res.body;
|
|
should.exist(jsonResponse.posts);
|
|
localUtils.API.checkResponse(jsonResponse, 'posts');
|
|
jsonResponse.posts.should.have.length(15);
|
|
localUtils.API.checkResponse(jsonResponse.posts[0], 'post', null, null);
|
|
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
|
|
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
|
|
|
|
// Default order 'published_at desc' check
|
|
jsonResponse.posts[0].slug.should.eql('thou-shalt-not-be-seen');
|
|
jsonResponse.posts[1].slug.should.eql('thou-shalt-be-paid-for');
|
|
jsonResponse.posts[2].slug.should.eql('free-to-see');
|
|
jsonResponse.posts[3].slug.should.eql('thou-shalt-have-a-taste');
|
|
jsonResponse.posts[8].slug.should.eql('sell');
|
|
|
|
jsonResponse.posts[0].html.should.eql('');
|
|
jsonResponse.posts[1].html.should.eql('');
|
|
jsonResponse.posts[2].html.should.not.eql('');
|
|
jsonResponse.posts[8].html.should.not.eql('');
|
|
|
|
jsonResponse.posts[0].excerpt.should.eql('');
|
|
jsonResponse.posts[1].excerpt.should.eql('');
|
|
jsonResponse.posts[2].excerpt.should.not.eql('');
|
|
jsonResponse.posts[3].excerpt.should.not.eql('');
|
|
jsonResponse.posts[8].excerpt.should.not.eql('');
|
|
|
|
// check meta response for this test
|
|
jsonResponse.meta.pagination.page.should.eql(1);
|
|
jsonResponse.meta.pagination.limit.should.eql(15);
|
|
jsonResponse.meta.pagination.pages.should.eql(1);
|
|
jsonResponse.meta.pagination.total.should.eql(15);
|
|
jsonResponse.meta.pagination.hasOwnProperty('next').should.be.true();
|
|
jsonResponse.meta.pagination.hasOwnProperty('prev').should.be.true();
|
|
should.not.exist(jsonResponse.meta.pagination.next);
|
|
should.not.exist(jsonResponse.meta.pagination.prev);
|
|
});
|
|
});
|
|
});
|
|
});
|