Ghost/test/regression/api/canary/admin/posts_spec.js
naz bbcc83dadb
Added support for ordering Post API resources by fields coming form posts_meta table (#12226)
refs #11729

- When ordering is done by fields from a relation (like post's `meta_title` that comes form `posts_meta` table), Bookshelf does not include those relations in the original query which caused errors. To support this usecase added a mechanism to detect fields from a relation and load those relations into query. 
- Extended ordering to include table name in ordered field name.  The information about the table name is needed to avoid using `tableName` within pagination plugin and gives path to having other than original table ordering fields (e.g. order by posts_meta table fields)
- Added test case to check ordering on posts_meta fields
- Added support for "eager loading" relations. Allows to extend query builder object with joins to related tables,
which could be used in ordering (possibly in filtering later). Bookshelf does not support ordering/filtering by proprieties coming from relations, that's why this kind of plugin and query expansion is needed
- Added note about lack of support for child relations with same property names.
2020-09-24 13:32:40 +12:00

506 lines
22 KiB
JavaScript

const should = require('should');
const supertest = require('supertest');
const ObjectId = require('bson-objectid');
const moment = require('moment-timezone');
const testUtils = require('../../../../utils');
const config = require('../../../../../core/shared/config');
const models = require('../../../../../core/server/models');
const localUtils = require('./utils');
const ghost = testUtils.startGhost;
let request;
describe('Posts API', function () {
let ghostServer;
let ownerCookie;
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
return localUtils.doAuth(request, 'users:extra', 'posts', 'emails');
})
.then(function (cookie) {
ownerCookie = cookie;
});
});
describe('Browse', function () {
it('fields & formats combined', function (done) {
request.get(localUtils.API.getApiQuery('posts/?formats=mobiledoc,html&fields=id,title'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
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(13);
localUtils.API.checkResponse(
jsonResponse.posts[0],
'post',
null,
null,
['mobiledoc', 'id', 'title', 'html']
);
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
done();
});
});
it('combined fields, formats, include and non existing', function (done) {
request.get(localUtils.API.getApiQuery('posts/?formats=mobiledoc,html,plaintext&fields=id,title,primary_tag,doesnotexist&include=authors,tags,email'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
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(13);
localUtils.API.checkResponse(
jsonResponse.posts[0],
'post',
null,
null,
['mobiledoc', 'plaintext', 'id', 'title', 'html', 'authors', 'tags', 'primary_tag', 'email']
);
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
done();
});
});
it('can order by fields coming from posts_meta table', function (done) {
request.get(localUtils.API.getApiQuery('posts/?order=meta_description%20ASC'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
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(13);
should.equal(jsonResponse.posts[0].meta_description, null);
jsonResponse.posts[12].slug.should.equal('short-and-sweet');
jsonResponse.posts[12].meta_description.should.equal('test stuff');
localUtils.API.checkResponse(
jsonResponse.posts[0],
'post'
);
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
done();
});
});
});
describe('Read', function () {
it('can\'t retrieve non existent post', function (done) {
request.get(localUtils.API.getApiQuery(`posts/${ObjectId.generate()}/`))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.errors);
testUtils.API.checkResponseValue(jsonResponse.errors[0], [
'message',
'context',
'type',
'details',
'property',
'help',
'code',
'id'
]);
done();
});
});
});
describe('Add', function () {
it('adds default title when it is missing', function () {
return request
.post(localUtils.API.getApiQuery('posts/'))
.set('Origin', config.get('url'))
.send({
posts: [{
title: ''
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.then((res) => {
should.exist(res.body.posts);
should.exist(res.body.posts[0].title);
res.body.posts[0].title.should.equal('(Untitled)');
should.exist(res.headers.location);
res.headers.location.should.equal(`http://127.0.0.1:2369${localUtils.API.getApiQuery('posts/')}${res.body.posts[0].id}/`);
});
});
});
describe('Edit', function () {
it('published_at = null', function () {
return request
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[0].id}/`))
.set('Origin', config.get('url'))
.expect(200)
.then((res) => {
return request
.put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/'))
.set('Origin', config.get('url'))
.send({
posts: [{
published_at: null,
updated_at: res.body.posts[0].updated_at
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
})
.then((res) => {
// @NOTE: if you set published_at to null and the post is published, we set it to NOW in model layer
should.exist(res.headers['x-cache-invalidate']);
should.exist(res.body.posts);
should.exist(res.body.posts[0].published_at);
});
});
it('html to plaintext', function () {
return request
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[0].id}/`))
.set('Origin', config.get('url'))
.expect(200)
.then((res) => {
return request
.put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/?source=html&formats=html,plaintext'))
.set('Origin', config.get('url'))
.send({
posts: [{
html: '<p>HTML Ipsum presents</p>',
updated_at: res.body.posts[0].updated_at
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
})
.then((res) => {
return models.Post.findOne({
id: res.body.posts[0].id
}, testUtils.context.internal);
})
.then((model) => {
model.get('plaintext').should.equal('HTML Ipsum presents');
});
});
it('canonical_url', function () {
return request
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[0].id}/`))
.set('Origin', config.get('url'))
.expect(200)
.then((res) => {
return request
.put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/'))
.set('Origin', config.get('url'))
.send({
posts: [{
canonical_url: `/canonical/url`,
updated_at: res.body.posts[0].updated_at
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
})
.then((res) => {
should.exist(res.body.posts);
should.exist(res.body.posts[0].canonical_url);
res.body.posts[0].canonical_url.should.equal(`${config.get('url')}/canonical/url`);
});
});
it('update dates & x_by', function () {
const post = {
created_by: ObjectId.generate(),
updated_by: ObjectId.generate(),
created_at: moment().add(2, 'days').format(),
updated_at: moment().add(2, 'days').format()
};
return request
.put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/'))
.set('Origin', config.get('url'))
.send({posts: [post]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
// @NOTE: you cannot modify these fields above manually, that's why the resource won't change.
should.not.exist(res.headers['x-cache-invalidate']);
return models.Post.findOne({
id: res.body.posts[0].id
}, testUtils.context.internal);
})
.then((model) => {
// We expect that the changed properties aren't changed, they are still the same than before.
model.get('created_at').toISOString().should.not.eql(post.created_at);
model.get('updated_by').should.not.eql(post.updated_by);
model.get('created_by').should.not.eql(post.created_by);
// `updated_at` is automatically set, but it's not the date we send to override.
model.get('updated_at').toISOString().should.not.eql(post.updated_at);
});
});
it('Can change scheduled post', function () {
return request
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[7].id}/`))
.set('Origin', config.get('url'))
.expect(200)
.then((res) => {
res.body.posts[0].status.should.eql('scheduled');
return request
.put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[7].id + '/'))
.set('Origin', config.get('url'))
.send({
posts: [{
title: 'change scheduled post',
updated_at: res.body.posts[0].updated_at
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
})
.then((res) => {
should.exist(res.headers['x-cache-invalidate']);
});
});
it('trims title', function () {
const untrimmedTitle = ' test trimmed update title ';
return request
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[0].id}/`))
.set('Origin', config.get('url'))
.expect(200)
.then((res) => {
return request
.put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/'))
.set('Origin', config.get('url'))
.send({
posts: [{
title: untrimmedTitle,
updated_at: res.body.posts[0].updated_at
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
})
.then((res) => {
should.exist(res.body.posts);
should.exist(res.body.posts[0].title);
res.body.posts[0].title.should.equal(untrimmedTitle.trim());
});
});
it('strips invisible unicode from slug', function () {
const slug = 'this-is\u0008-invisible';
return request
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[0].id}/`))
.set('Origin', config.get('url'))
.expect(200)
.then((res) => {
return request
.put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/'))
.set('Origin', config.get('url'))
.send({
posts: [{
slug: slug,
updated_at: res.body.posts[0].updated_at
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
})
.then((res) => {
should.exist(res.body.posts);
should.exist(res.body.posts[0].slug);
res.body.posts[0].slug.should.equal('this-is-invisible');
});
});
it('accepts visibility parameter', function () {
return request
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[0].id}/`))
.set('Origin', config.get('url'))
.expect(200)
.then((res) => {
return request
.put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/'))
.set('Origin', config.get('url'))
.send({
posts: [{
visibility: 'members',
updated_at: res.body.posts[0].updated_at
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
})
.then((res) => {
should.exist(res.body.posts);
should.exist(res.body.posts[0].visibility);
res.body.posts[0].visibility.should.equal('members');
});
});
it('changes to post_meta fields triggers a cache invalidation', function () {
return request
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[0].id}/`))
.set('Origin', config.get('url'))
.expect(200)
.then((res) => {
return request
.put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/'))
.set('Origin', config.get('url'))
.send({
posts: [{
meta_title: 'changed meta title',
updated_at: res.body.posts[0].updated_at
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
})
.then((res) => {
should.exist(res.headers['x-cache-invalidate']);
should.exist(res.body.posts);
should.equal(res.body.posts[0].meta_title, 'changed meta title');
});
});
it('saving post with no modbiledoc content doesn\t trigger cache invalidation', function () {
return request
.post(localUtils.API.getApiQuery('posts/'))
.set('Origin', config.get('url'))
.send({
posts: [{
title: 'Has a title by no other content',
status: 'published'
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.then((res) => {
should.exist(res.body.posts);
should.exist(res.body.posts[0].title);
res.body.posts[0].title.should.equal('Has a title by no other content');
should.equal(res.body.posts[0].html, undefined);
should.equal(res.body.posts[0].plaintext, undefined);
return request
.put(localUtils.API.getApiQuery(`posts/${res.body.posts[0].id}/`))
.set('Origin', config.get('url'))
.send({
posts: [{
title: res.body.posts[0].title,
mobilecdoc: res.body.posts[0].mobilecdoc,
updated_at: res.body.posts[0].updated_at
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
})
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
should.exist(res.body.posts);
res.body.posts[0].title.should.equal('Has a title by no other content');
should.equal(res.body.posts[0].html, undefined);
should.equal(res.body.posts[0].plaintext, undefined);
});
});
});
describe('Destroy', function () {
it('non existent post', function () {
return request
.del(localUtils.API.getApiQuery('posts/' + ObjectId.generate() + '/'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
should.exist(res.body);
should.exist(res.body.errors);
testUtils.API.checkResponseValue(res.body.errors[0], [
'message',
'context',
'type',
'details',
'property',
'help',
'code',
'id'
]);
});
});
});
});