mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-19 00:11:49 +03:00
f98467611c
no issue Collection cards contain dynamic data that can change when there's any change to a published post but in Ghost all post/page content is rendered once on save and stored as a static string meaning we need a new approach for triggering a re-render of pages that plays well with caching. - fixed typo in the relations/authors code that meant we weren't correctly calling the prototype method on the Post model inside the `onFetchedCollection` event handler - updated Post model to clear the `html` field of all pages when saving or deleting a published post - updated Post model to re-render `html` fields when fetching individual posts or a collection of posts - modified `insertExtraPostsTags` fixture util to wrap it's concurrent post edits in a transaction otherwise MySQL errors because it hits a deadlock
853 lines
32 KiB
JavaScript
853 lines
32 KiB
JavaScript
const should = require('should');
|
|
const assert = require('assert/strict');
|
|
const DomainEvents = require('@tryghost/domain-events');
|
|
const {agentProvider, fixtureManager, mockManager, matchers} = require('../../utils/e2e-framework');
|
|
const {anyArray, anyContentVersion, anyEtag, anyErrorId, anyLocationFor, anyObject, anyObjectId, anyISODateTime, anyString, anyStringNumber, anyUuid, stringMatching} = matchers;
|
|
const models = require('../../../core/server/models');
|
|
const escapeRegExp = require('lodash/escapeRegExp');
|
|
const {mobiledocToLexical} = require('@tryghost/kg-converters');
|
|
|
|
const tierSnapshot = {
|
|
id: anyObjectId,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime
|
|
};
|
|
|
|
const matchPostShallowIncludes = {
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
comment_id: anyString,
|
|
url: anyString,
|
|
authors: anyArray,
|
|
primary_author: anyObject,
|
|
tags: anyArray,
|
|
primary_tag: anyObject,
|
|
tiers: Array(2).fill(tierSnapshot),
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
published_at: anyISODateTime,
|
|
post_revisions: anyArray
|
|
};
|
|
|
|
const buildMatchPostShallowIncludes = (tiersCount = 2) => {
|
|
return {
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
comment_id: anyString,
|
|
url: anyString,
|
|
authors: anyArray,
|
|
primary_author: anyObject,
|
|
tags: anyArray,
|
|
primary_tag: anyObject,
|
|
tiers: Array(tiersCount).fill(tierSnapshot),
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
published_at: anyISODateTime,
|
|
post_revisions: anyArray
|
|
};
|
|
};
|
|
|
|
function testCleanedSnapshot(text, ignoreReplacements) {
|
|
for (const {match, replacement} of ignoreReplacements) {
|
|
if (match instanceof RegExp) {
|
|
text = text.replace(match, replacement);
|
|
} else {
|
|
text = text.replace(new RegExp(escapeRegExp(match), 'g'), replacement);
|
|
}
|
|
}
|
|
should({text}).matchSnapshot();
|
|
}
|
|
|
|
const createLexical = (text) => {
|
|
return JSON.stringify({
|
|
root: {
|
|
children: [
|
|
{
|
|
children: [
|
|
{
|
|
detail: 0,
|
|
format: 0,
|
|
mode: 'normal',
|
|
style: '',
|
|
text,
|
|
type: 'text',
|
|
version: 1
|
|
}
|
|
],
|
|
direction: 'ltr',
|
|
format: '',
|
|
indent: 0,
|
|
type: 'paragraph',
|
|
version: 1
|
|
}
|
|
],
|
|
direction: 'ltr',
|
|
format: '',
|
|
indent: 0,
|
|
type: 'root',
|
|
version: 1
|
|
}
|
|
});
|
|
};
|
|
|
|
const createMobiledoc = (text) => {
|
|
return JSON.stringify({
|
|
version: '0.3.1',
|
|
ghostVersion: '4.0',
|
|
markups: [],
|
|
atoms: [],
|
|
cards: [],
|
|
sections: [
|
|
[1, 'p', [
|
|
[0, [], 0, text]
|
|
]]
|
|
]
|
|
});
|
|
};
|
|
|
|
describe('Posts API', function () {
|
|
let agent;
|
|
|
|
before(async function () {
|
|
mockManager.mockLabsEnabled('collections', true);
|
|
mockManager.mockLabsEnabled('collectionsCard', true);
|
|
agent = await agentProvider.getAdminAPIAgent();
|
|
await fixtureManager.init('posts');
|
|
await agent.loginAsOwner();
|
|
|
|
// convert inserted pages to lexical so we can test page.html reset/re-render
|
|
const pages = await models.Post.where('type', 'page').fetchAll();
|
|
for (const page of pages) {
|
|
const lexical = mobiledocToLexical(page.get('mobiledoc'));
|
|
await models.Base.knex.raw('UPDATE posts SET mobiledoc=NULL, lexical=? where id=?', [lexical, page.id]);
|
|
}
|
|
});
|
|
|
|
afterEach(async function () {
|
|
// gives pages some HTML back to alleviate test interdependence when pages are reset on create/update/delete
|
|
await models.Base.knex.raw('update posts set html = "<p>Testing</p>" where type = \'page\'');
|
|
|
|
mockManager.restore();
|
|
});
|
|
|
|
it('Can browse', async function () {
|
|
await agent.get('posts/?limit=2')
|
|
.expectStatus(200)
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.matchBodySnapshot({
|
|
posts: new Array(2).fill(matchPostShallowIncludes)
|
|
});
|
|
});
|
|
|
|
it('Can browse with formats', async function () {
|
|
await agent.get('posts/?formats=mobiledoc,lexical,html,plaintext&limit=2')
|
|
.expectStatus(200)
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.matchBodySnapshot({
|
|
posts: new Array(2).fill(matchPostShallowIncludes)
|
|
});
|
|
});
|
|
|
|
it('Can browse filtering by a collection', async function () {
|
|
await agent.get('posts/?collection=featured')
|
|
.expectStatus(200)
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.matchBodySnapshot({
|
|
posts: new Array(2).fill(matchPostShallowIncludes)
|
|
});
|
|
});
|
|
|
|
it('Can browse filtering by collection using paging parameters', async function () {
|
|
await agent
|
|
.get(`posts/?collection=latest&limit=1&page=6`)
|
|
.expectStatus(200)
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.matchBodySnapshot({
|
|
posts: Array(1).fill(buildMatchPostShallowIncludes(2))
|
|
})
|
|
.expect((res) => {
|
|
// the total of posts with any status is 13
|
|
assert.equal(res.body.meta.pagination.total, 13);
|
|
});
|
|
});
|
|
|
|
describe('Export', function () {
|
|
it('Can export', async function () {
|
|
const {text} = await agent.get('posts/export')
|
|
.expectStatus(200)
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
'content-disposition': stringMatching(/^Attachment; filename="post-analytics.\d{4}-\d{2}-\d{2}.csv"$/)
|
|
});
|
|
|
|
// body snapshot doesn't work with text/csv
|
|
testCleanedSnapshot(text, [
|
|
{
|
|
match: /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z/g,
|
|
replacement: '2050-01-01T00:00:00.000Z'
|
|
}
|
|
]);
|
|
});
|
|
|
|
it('Can export with order', async function () {
|
|
const {text} = await agent.get('posts/export?order=published_at%20ASC')
|
|
.expectStatus(200)
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
'content-disposition': stringMatching(/^Attachment; filename="post-analytics.\d{4}-\d{2}-\d{2}.csv"$/)
|
|
});
|
|
|
|
// body snapshot doesn't work with text/csv
|
|
testCleanedSnapshot(text, [
|
|
{
|
|
match: /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z/g,
|
|
replacement: '2050-01-01T00:00:00.000Z'
|
|
}
|
|
]);
|
|
});
|
|
|
|
it('Can export with limit', async function () {
|
|
const {text} = await agent.get('posts/export?limit=1')
|
|
.expectStatus(200)
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
'content-disposition': stringMatching(/^Attachment; filename="post-analytics.\d{4}-\d{2}-\d{2}.csv"$/)
|
|
});
|
|
|
|
// body snapshot doesn't work with text/csv
|
|
testCleanedSnapshot(text, [
|
|
{
|
|
match: /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z/g,
|
|
replacement: '2050-01-01T00:00:00.000Z'
|
|
}
|
|
]);
|
|
});
|
|
|
|
it('Can export with filter', async function () {
|
|
const {text} = await agent.get('posts/export?filter=featured:true')
|
|
.expectStatus(200)
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
'content-disposition': stringMatching(/^Attachment; filename="post-analytics.\d{4}-\d{2}-\d{2}.csv"$/)
|
|
});
|
|
|
|
// body snapshot doesn't work with text/csv
|
|
testCleanedSnapshot(text, [
|
|
{
|
|
match: /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z/g,
|
|
replacement: '2050-01-01T00:00:00.000Z'
|
|
}
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('Create', function () {
|
|
it('Can create a post with mobiledoc', async function () {
|
|
const post = {
|
|
title: 'Mobiledoc test',
|
|
mobiledoc: createMobiledoc('Testing post creation with mobiledoc'),
|
|
lexical: null
|
|
};
|
|
|
|
await agent
|
|
.post('/posts/?formats=mobiledoc,lexical,html', {
|
|
headers: {
|
|
'content-type': 'application/json'
|
|
}
|
|
})
|
|
.body({posts: [post]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('posts')
|
|
});
|
|
});
|
|
|
|
it('Can create a post with lexical', async function () {
|
|
const lexical = createLexical('Testing post creation with lexical');
|
|
|
|
const post = {
|
|
title: 'Lexical test',
|
|
mobiledoc: null,
|
|
lexical
|
|
};
|
|
|
|
const {body} = await agent
|
|
.post('/posts/?formats=mobiledoc,lexical,html')
|
|
.body({posts: [post]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('posts')
|
|
});
|
|
|
|
const [postResponse] = body.posts;
|
|
|
|
// post revision is created
|
|
const postRevisions = await models.PostRevision
|
|
.where('post_id', postResponse.id)
|
|
.orderBy('created_at_ts', 'desc')
|
|
.fetchAll();
|
|
|
|
postRevisions.length.should.equal(1);
|
|
postRevisions.at(0).get('lexical').should.equal(lexical);
|
|
|
|
// mobiledoc revision is not created
|
|
const mobiledocRevisions = await models.MobiledocRevision
|
|
.where('post_id', postResponse.id)
|
|
.orderBy('created_at_ts', 'desc')
|
|
.fetchAll();
|
|
|
|
mobiledocRevisions.length.should.equal(0);
|
|
});
|
|
|
|
it('Can create a post with html', async function () {
|
|
mockManager.mockLabsDisabled('lexicalEditor');
|
|
|
|
const post = {
|
|
title: 'HTML test',
|
|
html: '<p>Testing post creation with html</p>'
|
|
};
|
|
|
|
await agent
|
|
.post('/posts/?source=html&formats=mobiledoc,lexical,html')
|
|
.body({posts: [post]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('posts')
|
|
});
|
|
});
|
|
|
|
it('Can create a post with html (labs.lexicalEditor)', async function () {
|
|
mockManager.mockLabsEnabled('lexicalEditor');
|
|
|
|
const post = {
|
|
title: 'HTML test',
|
|
html: '<p>Testing post creation with html</p>'
|
|
};
|
|
|
|
await agent
|
|
.post('/posts/?source=html&formats=mobiledoc,lexical,html')
|
|
.body({posts: [post]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('posts')
|
|
});
|
|
});
|
|
|
|
it('Errors if both mobiledoc and lexical are present', async function () {
|
|
const post = {
|
|
title: 'Mobiledoc+lexical test',
|
|
mobiledoc: createMobiledoc('Testing post creation with mobiledoc'),
|
|
lexical: createLexical('Testing post creation with lexical')
|
|
};
|
|
|
|
await agent
|
|
.post('/posts/?formats=mobiledoc,lexical')
|
|
.body({posts: [post]})
|
|
.expectStatus(422)
|
|
.matchBodySnapshot({
|
|
errors: [{
|
|
id: anyErrorId
|
|
}]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Errors with an invalid lexical state object', async function () {
|
|
const post = {
|
|
title: 'Invalid lexical state',
|
|
lexical: JSON.stringify({
|
|
notLexical: true
|
|
})
|
|
};
|
|
|
|
await agent
|
|
.post('/posts/?formats=mobiledoc,lexical,html')
|
|
.body({posts: [post]})
|
|
.expectStatus(422)
|
|
.matchBodySnapshot({
|
|
errors: [{
|
|
id: anyErrorId,
|
|
context: stringMatching(/Invalid lexical structure\..*/)
|
|
}]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
'content-version': anyContentVersion,
|
|
'content-length': anyStringNumber
|
|
});
|
|
});
|
|
|
|
it('Clears all page html fields when creating published post', async function () {
|
|
const totalPageCount = await models.Post.where({type: 'page'}).count();
|
|
should.exist(totalPageCount, 'total page count');
|
|
|
|
// sanity check for pages with no html
|
|
const sanityCheckEmptyPageCount = await models.Post.where({html: 'null', type: 'page'}).count();
|
|
should.exist(sanityCheckEmptyPageCount);
|
|
sanityCheckEmptyPageCount.should.equal(0, 'initial empty page count');
|
|
|
|
const post = {
|
|
title: 'Page reset test',
|
|
lexical: createLexical('Testing page.html reset when creating post'),
|
|
status: 'published'
|
|
};
|
|
|
|
await agent
|
|
.post('/posts/?source=html&formats=mobiledoc,lexical,html')
|
|
.body({posts: [post]})
|
|
.expectStatus(201);
|
|
|
|
// all pages have html cleared
|
|
const emptyPageCount = await models.Post.where({html: null, type: 'page'}).count();
|
|
should.exist(emptyPageCount);
|
|
emptyPageCount.should.equal(totalPageCount, 'post-creation empty page count');
|
|
});
|
|
|
|
it('Does not clear page html fields when creating draft post', async function () {
|
|
const post = {
|
|
title: 'Page reset test',
|
|
lexical: createLexical('Testing page.html reset when creating post'),
|
|
status: 'draft'
|
|
};
|
|
|
|
await agent
|
|
.post('/posts/?source=html&formats=mobiledoc,lexical,html')
|
|
.body({posts: [post]})
|
|
.expectStatus(201);
|
|
|
|
// no pages have html cleared
|
|
const emptyPageCount = await models.Post.where({html: null, type: 'page'}).count();
|
|
should.exist(emptyPageCount);
|
|
emptyPageCount.should.equal(0, 'post-creation empty page count');
|
|
});
|
|
});
|
|
|
|
describe('Update', function () {
|
|
it('Can update a post with mobiledoc', async function () {
|
|
const originalMobiledoc = createMobiledoc('Original text');
|
|
const updatedMobiledoc = createMobiledoc('Updated text');
|
|
|
|
const {body: postBody} = await agent
|
|
.post('/posts/?formats=mobiledoc,lexical,html')
|
|
.body({posts: [{
|
|
title: 'Mobiledoc update test',
|
|
mobiledoc: originalMobiledoc
|
|
}]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('posts')
|
|
});
|
|
|
|
const [postResponse] = postBody.posts;
|
|
|
|
await agent
|
|
.put(`/posts/${postResponse.id}/?formats=mobiledoc,lexical,html`)
|
|
.body({posts: [Object.assign({}, postResponse, {mobiledoc: updatedMobiledoc})]})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
'x-cache-invalidate': anyString
|
|
});
|
|
|
|
// mobiledoc revisions are created
|
|
const mobiledocRevisions = await models.MobiledocRevision
|
|
.where('post_id', postResponse.id)
|
|
.orderBy('created_at_ts', 'desc')
|
|
.fetchAll();
|
|
|
|
mobiledocRevisions.length.should.equal(2);
|
|
mobiledocRevisions.at(0).get('mobiledoc').should.equal(updatedMobiledoc);
|
|
mobiledocRevisions.at(1).get('mobiledoc').should.equal(originalMobiledoc);
|
|
|
|
// post revisions are not created
|
|
const postRevisions = await models.PostRevision
|
|
.where('post_id', postResponse.id)
|
|
.orderBy('created_at_ts', 'desc')
|
|
.fetchAll();
|
|
|
|
postRevisions.length.should.equal(0);
|
|
});
|
|
|
|
it('Can update a post with lexical', async function () {
|
|
const originalLexical = createLexical('Original text');
|
|
const updatedLexical = createLexical('Updated text');
|
|
|
|
const {body: postBody} = await agent
|
|
.post('/posts/?formats=mobiledoc,lexical,html')
|
|
.body({posts: [{
|
|
title: 'Lexical update test',
|
|
lexical: originalLexical
|
|
}]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('posts')
|
|
});
|
|
|
|
const [postResponse] = postBody.posts;
|
|
|
|
await agent
|
|
.put(`/posts/${postResponse.id}/?formats=mobiledoc,lexical,html&save_revision=true`)
|
|
.body({posts: [Object.assign({}, postResponse, {lexical: updatedLexical})]})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
'x-cache-invalidate': anyString
|
|
});
|
|
|
|
// post revisions are created
|
|
const postRevisions = await models.PostRevision
|
|
.where('post_id', postResponse.id)
|
|
.orderBy('created_at_ts', 'desc')
|
|
.fetchAll();
|
|
|
|
postRevisions.length.should.equal(2);
|
|
postRevisions.at(0).get('lexical').should.equal(updatedLexical);
|
|
postRevisions.at(1).get('lexical').should.equal(originalLexical);
|
|
|
|
// mobiledoc revisions are not created
|
|
const mobiledocRevisions = await models.MobiledocRevision
|
|
.where('post_id', postResponse.id)
|
|
.orderBy('created_at_ts', 'desc')
|
|
.fetchAll();
|
|
|
|
mobiledocRevisions.length.should.equal(0);
|
|
});
|
|
|
|
it('Can add and remove collections', async function () {
|
|
const {body: postBody} = await agent
|
|
.post('/posts/')
|
|
.body({
|
|
posts: [{
|
|
title: 'Collection update test'
|
|
}]
|
|
})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('posts')
|
|
});
|
|
|
|
const [postResponse] = postBody.posts;
|
|
|
|
const {body: {
|
|
collections: [collectionToAdd]
|
|
}} = await agent
|
|
.post('/collections/')
|
|
.body({
|
|
collections: [{
|
|
title: 'Collection to add.'
|
|
}]
|
|
});
|
|
|
|
const {body: {
|
|
collections: [collectionToRemove]
|
|
}} = await agent
|
|
.post('/collections/')
|
|
.body({
|
|
collections: [{
|
|
title: 'Collection to remove.'
|
|
}]
|
|
});
|
|
|
|
const collectionPostMatcher = {
|
|
id: anyObjectId
|
|
};
|
|
const collectionMatcher = {
|
|
id: anyObjectId,
|
|
created_at: stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/),
|
|
updated_at: stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/),
|
|
posts: [{
|
|
id: anyObjectId
|
|
}]
|
|
};
|
|
const buildCollectionMatcher = (postsCount) => {
|
|
return {
|
|
id: anyObjectId,
|
|
created_at: stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/),
|
|
updated_at: stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/),
|
|
posts: Array(postsCount).fill(collectionPostMatcher)
|
|
};
|
|
};
|
|
|
|
await agent.put(`/posts/${postResponse.id}/`)
|
|
.body({posts: [Object.assign({}, postResponse, {collections: [collectionToRemove.id]})]})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
posts: [
|
|
Object.assign({}, matchPostShallowIncludes, {published_at: null}, {collections: [
|
|
// collectionToRemove
|
|
collectionMatcher,
|
|
// automatic "latest" collection which cannot be removed
|
|
buildCollectionMatcher(22)
|
|
]})]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
'x-cache-invalidate': stringMatching(/\/p\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/)
|
|
});
|
|
|
|
await agent.put(`/posts/${postResponse.id}/`)
|
|
.body({posts: [Object.assign({}, postResponse, {collections: [collectionToAdd.id]})]})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
posts: [
|
|
Object.assign({}, matchPostShallowIncludes, {published_at: null}, {collections: [
|
|
// collectionToAdd
|
|
collectionMatcher,
|
|
// automatic "latest" collection which cannot be removed
|
|
buildCollectionMatcher(22)
|
|
]})]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
'x-cache-invalidate': stringMatching(/\/p\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/)
|
|
});
|
|
});
|
|
|
|
it('Clears all page html fields when publishing a post', async function () {
|
|
const totalPageCount = await models.Post.where({type: 'page'}).count();
|
|
should.exist(totalPageCount, 'total page count');
|
|
|
|
// sanity check for pages with no html
|
|
const sanityCheckEmptyPageCount = await models.Post.where({html: 'null', type: 'page'}).count();
|
|
should.exist(sanityCheckEmptyPageCount);
|
|
sanityCheckEmptyPageCount.should.equal(0, 'initial empty page count');
|
|
|
|
const {body: postBody} = await agent
|
|
.post('/posts/?source=html&formats=mobiledoc,lexical,html')
|
|
.body({posts: [{
|
|
title: 'Page reset test',
|
|
lexical: createLexical('Testing page.html reset when updating post'),
|
|
status: 'draft'
|
|
}]})
|
|
.expectStatus(201);
|
|
|
|
const [postResponse] = postBody.posts;
|
|
|
|
await agent
|
|
.put(`/posts/${postResponse.id}/?source=html&formats=mobiledoc,lexical,html`)
|
|
.body({posts: [Object.assign({}, postResponse, {status: 'published'})]})
|
|
.expectStatus(200);
|
|
|
|
// all pages have html cleared
|
|
const emptyPageCount = await models.Post.where({html: null, type: 'page'}).count();
|
|
should.exist(emptyPageCount);
|
|
emptyPageCount.should.equal(totalPageCount, 'post-update empty page count');
|
|
});
|
|
});
|
|
|
|
describe('Delete', function () {
|
|
it('Can destroy a post', async function () {
|
|
await agent
|
|
.delete(`posts/${fixtureManager.get('posts', 0).id}/`)
|
|
.expectStatus(204)
|
|
.expectEmptyBody()
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Cannot delete a non-existent posts', async function () {
|
|
// This error message from the API is not really what I would expect
|
|
// Adding this as a guard to demonstrate how future refactoring improves the output
|
|
await agent
|
|
.delete('/posts/abcd1234abcd1234abcd1234/')
|
|
.expectStatus(404)
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.matchBodySnapshot({
|
|
errors: [{
|
|
id: anyErrorId
|
|
}]
|
|
});
|
|
});
|
|
|
|
it('Can delete posts belonging to a collection and returns empty response when filtering by that collection', async function () {
|
|
const res = await agent.get('posts/?collection=featured')
|
|
.expectStatus(200)
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.matchBodySnapshot({
|
|
posts: new Array(2).fill(matchPostShallowIncludes)
|
|
});
|
|
|
|
const posts = res.body.posts;
|
|
|
|
await agent.delete(`posts/${posts[0].id}/`).expectStatus(204);
|
|
await agent.delete(`posts/${posts[1].id}/`).expectStatus(204);
|
|
|
|
await DomainEvents.allSettled();
|
|
|
|
await agent
|
|
.get(`posts/?collection=featured`)
|
|
.expectStatus(200)
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.matchBodySnapshot();
|
|
});
|
|
|
|
it('Clears all page html fields when deleting a published post', async function () {
|
|
const totalPageCount = await models.Post.where({type: 'page'}).count();
|
|
should.exist(totalPageCount, 'total page count');
|
|
|
|
// sanity check for pages with no html
|
|
const sanityCheckEmptyPageCount = await models.Post.where({html: 'null', type: 'page'}).count();
|
|
should.exist(sanityCheckEmptyPageCount);
|
|
sanityCheckEmptyPageCount.should.equal(0, 'initial empty page count');
|
|
|
|
const {body: postBody} = await agent
|
|
.get('/posts/?limit=1&filter=status:published')
|
|
.expectStatus(200);
|
|
|
|
const [postResponse] = postBody.posts;
|
|
|
|
await agent
|
|
.delete(`/posts/${postResponse.id}/`)
|
|
.expectStatus(204);
|
|
|
|
// all pages have html cleared
|
|
const emptyPageCount = await models.Post.where({html: null, type: 'page'}).count();
|
|
should.exist(emptyPageCount);
|
|
emptyPageCount.should.equal(totalPageCount, 'post-deletion empty page count');
|
|
});
|
|
});
|
|
|
|
describe('Copy', function () {
|
|
it('Can copy a post', async function () {
|
|
const post = {
|
|
title: 'Test Post',
|
|
status: 'published'
|
|
};
|
|
|
|
const {body: postBody} = await agent
|
|
.post('/posts/?formats=mobiledoc,lexical,html', {
|
|
headers: {
|
|
'content-type': 'application/json'
|
|
}
|
|
})
|
|
.body({posts: [post]})
|
|
.expectStatus(201);
|
|
|
|
const [postResponse] = postBody.posts;
|
|
|
|
await agent
|
|
.post(`/posts/${postResponse.id}/copy?formats=mobiledoc,lexical`)
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('posts')
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Convert', function () {
|
|
it('can convert a mobiledoc post to lexical', async function () {
|
|
const mobiledoc = createMobiledoc('This is some great content.');
|
|
const expectedLexical = createLexical('This is some great content.');
|
|
const postData = {
|
|
title: 'Test Post',
|
|
status: 'published',
|
|
mobiledoc: mobiledoc,
|
|
lexical: null
|
|
};
|
|
|
|
const {body} = await agent
|
|
.post('/posts/?formats=mobiledoc,lexical,html', {
|
|
headers: {
|
|
'content-type': 'application/json'
|
|
}
|
|
})
|
|
.body({posts: [postData]})
|
|
.expectStatus(201);
|
|
|
|
const [postResponse] = body.posts;
|
|
|
|
await agent
|
|
.put(`/posts/${postResponse.id}/?formats=mobiledoc,lexical,html&convert_to_lexical=true`)
|
|
.body({posts: [Object.assign({}, postResponse)]})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {lexical: expectedLexical, mobiledoc: null})]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
});
|
|
});
|