const should = require('should'); const supertest = require('supertest'); const _ = require('lodash'); const url = require('url'); const cheerio = require('cheerio'); const moment = require('moment'); const testUtils = require('../../utils'); const configUtils = require('../../utils/configUtils'); const config = require('../../../core/shared/config'); const localUtils = require('./utils'); describe('Posts Content API', function () { let request; before(async function () { await testUtils.startGhost(); request = supertest.agent(config.get('url')); await testUtils.initFixtures('owner:post', 'users:no-owner', 'user:inactive', 'posts', 'tags:extra', 'api_keys'); }); afterEach(function () { configUtils.restore(); }); const validKey = localUtils.getValidKey(); it('Can request posts', async function () { const res = await 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); 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); // kitchen sink res.body.posts[9].slug.should.eql(testUtils.DataGenerator.Content.posts[1].slug); let urlParts = url.parse(res.body.posts[9].feature_image); should.exist(urlParts.protocol); should.exist(urlParts.host); urlParts = url.parse(res.body.posts[9].url); should.exist(urlParts.protocol); should.exist(urlParts.host); const $ = cheerio.load(res.body.posts[9].html); urlParts = url.parse($('img').attr('src')); should.exist(urlParts.protocol); should.exist(urlParts.host); res.body.posts[7].slug.should.eql('not-so-short-bit-complex'); res.body.posts[7].html.should.match(/ { if (post.featured) { post.featured.should.equal(true); } else { const tags = _.map(post.tags, 'slug'); tags.should.containEql('kitchen-sink'); } }); // The meta object should contain the right detail jsonResponse.meta.should.have.property('pagination'); jsonResponse.meta.pagination.should.be.an.Object().with.properties(['page', 'limit', 'pages', 'total', 'next', 'prev']); 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(4); should.equal(jsonResponse.meta.pagination.next, null); should.equal(jsonResponse.meta.pagination.prev, null); }); it('Can filter posts by authors', async function () { const res = await request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}&filter=authors:[joe-bloggs,pat,ghost,slimer-mcectoplasm]&include=authors`)) .expect('Content-Type', /json/) .expect('Cache-Control', testUtils.cacheRules.private) .expect(200); const jsonResponse = res.body; const ids = _.map(jsonResponse.posts, 'id'); should.not.exist(res.headers['x-cache-invalidate']); should.exist(jsonResponse.posts); localUtils.API.checkResponse(jsonResponse, 'posts'); localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); // 2. The data part of the response should be correct // We should have 2 matching items jsonResponse.posts.should.be.an.Array().with.lengthOf(11); // The API orders by number of matched authors. jsonResponse.posts[0].slug.should.eql('not-so-short-bit-complex'); // Each post must either have the author 'joe-bloggs' or 'ghost', 'pat' is non existing author const primaryAuthors = _.map(jsonResponse.posts, function (post) { return post.primary_author.slug; }); primaryAuthors.should.matchAny(/joe-bloggs|ghost'/); _.filter(primaryAuthors, (value) => { return value === 'ghost'; }).length.should.eql(7); _.filter(primaryAuthors, (value) => { return value === 'joe-bloggs'; }).length.should.eql(4); }); it('Can request fields of posts', async function () { const res = await request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}&fields=url`)) .set('Origin', testUtils.API.getURL()) .expect('Content-Type', /json/) .expect('Cache-Control', testUtils.cacheRules.private) .expect(200); const jsonResponse = res.body; should.exist(jsonResponse.posts); localUtils.API.checkResponse(jsonResponse.posts[0], 'post', false, false, ['url']); res.body.posts[0].url.should.eql('http://127.0.0.1:2369/welcome/'); }); it('Can include relations', async function () { const res = await request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}&include=tags,authors`)) .set('Origin', testUtils.API.getURL()) .expect('Content-Type', /json/) .expect('Cache-Control', testUtils.cacheRules.private) .expect(200); should.exist(res.body.posts); // kitchen sink res.body.posts[9].slug.should.eql(testUtils.DataGenerator.Content.posts[1].slug); should.exist(res.body.posts[9].tags); should.exist(res.body.posts[9].tags[0].url); should.exist(url.parse(res.body.posts[9].tags[0].url).protocol); should.exist(url.parse(res.body.posts[9].tags[0].url).host); should.exist(res.body.posts[9].primary_tag); should.exist(res.body.posts[9].primary_tag.url); should.exist(url.parse(res.body.posts[9].primary_tag.url).protocol); should.exist(url.parse(res.body.posts[9].primary_tag.url).host); should.exist(res.body.posts[9].authors); should.exist(res.body.posts[9].authors[0].url); should.exist(url.parse(res.body.posts[9].authors[0].url).protocol); should.exist(url.parse(res.body.posts[9].authors[0].url).host); should.exist(res.body.posts[9].primary_author); should.exist(res.body.posts[9].primary_author.url); should.exist(url.parse(res.body.posts[9].primary_author.url).protocol); should.exist(url.parse(res.body.posts[9].primary_author.url).host); }); it('Can request posts from different origin', async function () { const res = await request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}`)) .set('Origin', 'https://example.com') .expect('Content-Type', /json/) .expect('Cache-Control', testUtils.cacheRules.private) .expect(200); 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); }); it('Can filter by published date', async function () { function createFilter(publishedAt, op) { // This line deliberately uses double quotes because GQL cannot handle either double quotes // or escaped singles, see TryGhost/GQL#34 return encodeURIComponent('published_at:' + op + '\'' + publishedAt + '\''); } const res = await request .get(localUtils.API.getApiQuery(`posts/?key=${validKey}&limit=1`)) .set('Origin', testUtils.API.getURL()) .expect('Content-Type', /json/) .expect('Cache-Control', testUtils.cacheRules.private) .expect(200); should.exist(res.body.posts[0]); const post = res.body.posts[0]; const publishedAt = moment(post.published_at).format('YYYY-MM-DD HH:mm:ss'); post.title.should.eql('Start here for a quick overview of everything you need to know'); const res2 = await request .get(localUtils.API.getApiQuery(`posts/?key=${validKey}&limit=1&filter=${createFilter(publishedAt, `<`)}`)) .set('Origin', testUtils.API.getURL()) .expect('Content-Type', /json/) .expect('Cache-Control', testUtils.cacheRules.private) .expect(200); should.exist(res2.body.posts[0]); const post2 = res2.body.posts[0]; const publishedAt2 = moment(post2.published_at).format('YYYY-MM-DD HH:mm:ss'); post2.title.should.eql('Customizing your brand and design settings'); const res3 = await request .get(localUtils.API.getApiQuery(`posts/?key=${validKey}&limit=1&filter=${createFilter(publishedAt2, `>`)}`)) .set('Origin', testUtils.API.getURL()) .expect('Content-Type', /json/) .expect('Cache-Control', testUtils.cacheRules.private) .expect(200); should.exist(res3.body.posts[0]); const post3 = res3.body.posts[0]; post3.title.should.eql('Start here for a quick overview of everything you need to know'); }); it('Can request a single post', async function () { const res = await request .get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[0].id}/?key=${validKey}`)) .set('Origin', testUtils.API.getURL()) .expect('Content-Type', /json/) .expect('Cache-Control', testUtils.cacheRules.private) .expect(200); should.not.exist(res.headers['x-cache-invalidate']); const jsonResponse = res.body; should.exist(jsonResponse.posts); should.not.exist(jsonResponse.meta); jsonResponse.posts.should.have.length(1); localUtils.API.checkResponse(jsonResponse.posts[0], 'post'); }); });