2022-09-16 13:59:35 +03:00
|
|
|
const should = require('should');
|
2023-07-26 11:30:31 +03:00
|
|
|
const assert = require('assert/strict');
|
2023-09-14 08:18:38 +03:00
|
|
|
const DomainEvents = require('@tryghost/domain-events');
|
2022-08-22 19:35:37 +03:00
|
|
|
const {agentProvider, fixtureManager, mockManager, matchers} = require('../../utils/e2e-framework');
|
2023-04-18 13:12:35 +03:00
|
|
|
const {anyArray, anyContentVersion, anyEtag, anyErrorId, anyLocationFor, anyObject, anyObjectId, anyISODateTime, anyString, anyStringNumber, anyUuid, stringMatching} = matchers;
|
2022-09-16 13:59:35 +03:00
|
|
|
const models = require('../../../core/server/models');
|
2023-03-27 12:11:49 +03:00
|
|
|
const escapeRegExp = require('lodash/escapeRegExp');
|
2023-09-21 17:51:08 +03:00
|
|
|
const {mobiledocToLexical} = require('@tryghost/kg-converters');
|
2022-09-13 16:05:03 +03:00
|
|
|
|
2022-10-27 10:54:17 +03:00
|
|
|
const tierSnapshot = {
|
|
|
|
id: anyObjectId,
|
|
|
|
created_at: anyISODateTime,
|
|
|
|
updated_at: anyISODateTime
|
|
|
|
};
|
|
|
|
|
2022-09-13 16:05:03 +03:00
|
|
|
const matchPostShallowIncludes = {
|
|
|
|
id: anyObjectId,
|
|
|
|
uuid: anyUuid,
|
2022-09-13 19:29:37 +03:00
|
|
|
comment_id: anyString,
|
2022-09-13 16:05:03 +03:00
|
|
|
url: anyString,
|
|
|
|
authors: anyArray,
|
|
|
|
primary_author: anyObject,
|
|
|
|
tags: anyArray,
|
|
|
|
primary_tag: anyObject,
|
2022-10-27 10:54:17 +03:00
|
|
|
tiers: Array(2).fill(tierSnapshot),
|
2022-09-13 16:05:03 +03:00
|
|
|
created_at: anyISODateTime,
|
2022-09-13 16:09:45 +03:00
|
|
|
updated_at: anyISODateTime,
|
2023-10-11 20:23:31 +03:00
|
|
|
published_at: anyISODateTime
|
2023-04-17 15:46:27 +03:00
|
|
|
};
|
|
|
|
|
2023-07-21 11:17:32 +03:00
|
|
|
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,
|
2023-10-11 20:23:31 +03:00
|
|
|
published_at: anyISODateTime
|
2023-07-21 11:17:32 +03:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2023-03-27 12:11:49 +03:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2022-09-16 13:59:35 +03:00
|
|
|
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]
|
|
|
|
]]
|
|
|
|
]
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2022-08-22 19:35:37 +03:00
|
|
|
describe('Posts API', function () {
|
|
|
|
let agent;
|
|
|
|
|
|
|
|
before(async function () {
|
2023-06-29 01:25:08 +03:00
|
|
|
mockManager.mockLabsEnabled('collections', true);
|
2023-09-21 17:51:08 +03:00
|
|
|
mockManager.mockLabsEnabled('collectionsCard', true);
|
2022-08-22 19:35:37 +03:00
|
|
|
agent = await agentProvider.getAdminAPIAgent();
|
|
|
|
await fixtureManager.init('posts');
|
|
|
|
await agent.loginAsOwner();
|
2023-09-21 17:51:08 +03:00
|
|
|
|
|
|
|
// 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]);
|
|
|
|
}
|
2022-08-22 19:35:37 +03:00
|
|
|
});
|
|
|
|
|
2023-09-21 17:51:08 +03:00
|
|
|
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\'');
|
|
|
|
|
2022-08-22 19:35:37 +03:00
|
|
|
mockManager.restore();
|
|
|
|
});
|
|
|
|
|
2022-09-13 15:30:17 +03:00
|
|
|
it('Can browse', async function () {
|
2023-03-10 14:53:35 +03:00
|
|
|
await agent.get('posts/?limit=2')
|
2022-09-13 15:30:17 +03:00
|
|
|
.expectStatus(200)
|
|
|
|
.matchHeaderSnapshot({
|
2023-01-17 14:56:29 +03:00
|
|
|
'content-version': anyContentVersion,
|
2022-09-13 15:30:17 +03:00
|
|
|
etag: anyEtag
|
|
|
|
})
|
|
|
|
.matchBodySnapshot({
|
2022-09-13 16:05:03 +03:00
|
|
|
posts: new Array(2).fill(matchPostShallowIncludes)
|
2022-09-13 15:30:17 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('Can browse with formats', async function () {
|
2023-03-10 14:53:35 +03:00
|
|
|
await agent.get('posts/?formats=mobiledoc,lexical,html,plaintext&limit=2')
|
2022-09-13 15:30:17 +03:00
|
|
|
.expectStatus(200)
|
|
|
|
.matchHeaderSnapshot({
|
2023-01-17 14:56:29 +03:00
|
|
|
'content-version': anyContentVersion,
|
2022-09-13 15:30:17 +03:00
|
|
|
etag: anyEtag
|
|
|
|
})
|
|
|
|
.matchBodySnapshot({
|
2022-09-13 16:05:03 +03:00
|
|
|
posts: new Array(2).fill(matchPostShallowIncludes)
|
2022-09-13 15:30:17 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-06-16 13:13:31 +03:00
|
|
|
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)
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-07-21 11:17:32 +03:00
|
|
|
it('Can browse filtering by collection using paging parameters', async function () {
|
|
|
|
await agent
|
2023-07-26 11:30:31 +03:00
|
|
|
.get(`posts/?collection=latest&limit=1&page=6`)
|
2023-07-21 11:17:32 +03:00
|
|
|
.expectStatus(200)
|
|
|
|
.matchHeaderSnapshot({
|
|
|
|
'content-version': anyContentVersion,
|
|
|
|
etag: anyEtag
|
|
|
|
})
|
|
|
|
.matchBodySnapshot({
|
|
|
|
posts: Array(1).fill(buildMatchPostShallowIncludes(2))
|
2023-07-26 11:30:31 +03:00
|
|
|
})
|
|
|
|
.expect((res) => {
|
|
|
|
// the total of posts with any status is 13
|
|
|
|
assert.equal(res.body.meta.pagination.total, 13);
|
2023-07-21 11:17:32 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-03-27 12:11:49 +03:00
|
|
|
describe('Export', function () {
|
|
|
|
it('Can export', async function () {
|
|
|
|
const {text} = await agent.get('posts/export')
|
|
|
|
.expectStatus(200)
|
|
|
|
.matchHeaderSnapshot({
|
|
|
|
'content-version': anyContentVersion,
|
2023-03-28 05:48:17 +03:00
|
|
|
etag: anyEtag,
|
2023-04-03 15:50:43 +03:00
|
|
|
'content-disposition': stringMatching(/^Attachment; filename="post-analytics.\d{4}-\d{2}-\d{2}.csv"$/)
|
2023-03-27 12:11:49 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
// 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 () {
|
2023-03-27 12:22:33 +03:00
|
|
|
const {text} = await agent.get('posts/export?order=published_at%20ASC')
|
2023-03-27 12:11:49 +03:00
|
|
|
.expectStatus(200)
|
|
|
|
.matchHeaderSnapshot({
|
|
|
|
'content-version': anyContentVersion,
|
2023-03-28 05:48:17 +03:00
|
|
|
etag: anyEtag,
|
2023-04-03 15:50:43 +03:00
|
|
|
'content-disposition': stringMatching(/^Attachment; filename="post-analytics.\d{4}-\d{2}-\d{2}.csv"$/)
|
2023-03-27 12:11:49 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
// 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,
|
2023-03-28 05:48:17 +03:00
|
|
|
etag: anyEtag,
|
2023-04-03 15:50:43 +03:00
|
|
|
'content-disposition': stringMatching(/^Attachment; filename="post-analytics.\d{4}-\d{2}-\d{2}.csv"$/)
|
2023-03-27 12:11:49 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
// 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,
|
2023-03-28 05:48:17 +03:00
|
|
|
etag: anyEtag,
|
2023-04-03 15:50:43 +03:00
|
|
|
'content-disposition': stringMatching(/^Attachment; filename="post-analytics.\d{4}-\d{2}-\d{2}.csv"$/)
|
2023-03-27 12:11:49 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
// 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'
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2022-09-13 19:29:37 +03:00
|
|
|
describe('Create', function () {
|
|
|
|
it('Can create a post with mobiledoc', async function () {
|
|
|
|
const post = {
|
|
|
|
title: 'Mobiledoc test',
|
2022-09-16 13:59:35 +03:00
|
|
|
mobiledoc: createMobiledoc('Testing post creation with mobiledoc'),
|
2022-09-13 23:01:53 +03:00
|
|
|
lexical: null
|
2022-09-13 19:29:37 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
await agent
|
🐛 Fixed storing original files for images (#16117)
fixes https://github.com/TryGhost/Team/issues/481
This change fixes an issue when multiple images with the same name are
uploaded in parallel. The current system does not guarantee that the
original filename is stored under NAME+`_o`, because the upload for the
original file and the resized file are happening in parallel.
Solution:
- Wait for the storage of the resized image (= the image without the _o
suffix) before storing the original file.
- When that is stored, use the generated file name of the stored image
to generate the filename with the _o suffix. This way, it will always
match and we don't risk both files to have a different number suffix.
We'll also set the `targetDir` argument when saving the file, to avoid
storing the original file in a different directory (when uploading a
file around midnight both files could be stored in 2023/01 and 2023/02).
Some extra optimisations needed with this fix:
- Previously when uploading image.jpg, while it already exists, it would
store two filenames on e.g., `image-3.jpg` and `image_o-3.jpg`. Note the
weird positioning of `_o`. This probably caused bugs when uploading
files named `image-3.jpg`, which would store the original in
`image-3_o.jpg`, but this original would never be used by the
handle-image-sizes middleware (it would look for `image_o-3.jpg`). This
fix would solve this weird naming issue, and make it more consistent.
But we need to make sure our middlewares (including handle-image-sizes)
will be able to handle both file locations to remain compatible with the
old format. This isn't additional work, because it would fix the old bug
too.
- Prevent uploading files that end with `_o`, e.g. by automatically
stripping that suffix from uploaded files. To prevent collisions.
Advantage(s):
- We keep the original file name, which is better for SEO.
- No changes required to the storage adapters.
Downside(s):
- The storage of both files will nog happen parallel any longer. But I
expect the performance implications to be minimal.
- Changes to the routing: normalize middleware is removed
2023-01-30 18:40:50 +03:00
|
|
|
.post('/posts/?formats=mobiledoc,lexical,html', {
|
|
|
|
headers: {
|
|
|
|
'content-type': 'application/json'
|
|
|
|
}
|
|
|
|
})
|
2022-09-13 19:29:37 +03:00
|
|
|
.body({posts: [post]})
|
|
|
|
.expectStatus(201)
|
|
|
|
.matchBodySnapshot({
|
2023-06-15 16:53:21 +03:00
|
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
2022-09-13 19:29:37 +03:00
|
|
|
})
|
|
|
|
.matchHeaderSnapshot({
|
2023-01-17 14:56:29 +03:00
|
|
|
'content-version': anyContentVersion,
|
2022-09-13 19:29:37 +03:00
|
|
|
etag: anyEtag,
|
|
|
|
location: anyLocationFor('posts')
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('Can create a post with lexical', async function () {
|
2022-09-16 13:59:35 +03:00
|
|
|
const lexical = createLexical('Testing post creation with lexical');
|
|
|
|
|
2022-09-13 19:29:37 +03:00
|
|
|
const post = {
|
|
|
|
title: 'Lexical test',
|
2022-09-13 23:01:53 +03:00
|
|
|
mobiledoc: null,
|
2022-09-16 13:59:35 +03:00
|
|
|
lexical
|
2022-09-13 19:29:37 +03:00
|
|
|
};
|
|
|
|
|
2022-09-16 13:59:35 +03:00
|
|
|
const {body} = await agent
|
2022-09-15 18:49:14 +03:00
|
|
|
.post('/posts/?formats=mobiledoc,lexical,html')
|
2022-09-13 19:29:37 +03:00
|
|
|
.body({posts: [post]})
|
|
|
|
.expectStatus(201)
|
|
|
|
.matchBodySnapshot({
|
2023-06-15 16:53:21 +03:00
|
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
2022-09-13 19:29:37 +03:00
|
|
|
})
|
|
|
|
.matchHeaderSnapshot({
|
2023-01-17 14:56:29 +03:00
|
|
|
'content-version': anyContentVersion,
|
2022-09-13 19:29:37 +03:00
|
|
|
etag: anyEtag,
|
|
|
|
location: anyLocationFor('posts')
|
|
|
|
});
|
2022-09-16 13:59:35 +03:00
|
|
|
|
|
|
|
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);
|
2022-09-13 19:29:37 +03:00
|
|
|
});
|
|
|
|
|
2023-09-07 00:16:40 +03:00
|
|
|
it('Can create a post with html', async function () {
|
|
|
|
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')
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2022-09-13 19:29:37 +03:00
|
|
|
it('Errors if both mobiledoc and lexical are present', async function () {
|
|
|
|
const post = {
|
|
|
|
title: 'Mobiledoc+lexical test',
|
2022-09-16 13:59:35 +03:00
|
|
|
mobiledoc: createMobiledoc('Testing post creation with mobiledoc'),
|
|
|
|
lexical: createLexical('Testing post creation with lexical')
|
2022-09-13 19:29:37 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
await agent
|
|
|
|
.post('/posts/?formats=mobiledoc,lexical')
|
|
|
|
.body({posts: [post]})
|
|
|
|
.expectStatus(422)
|
|
|
|
.matchBodySnapshot({
|
|
|
|
errors: [{
|
|
|
|
id: anyErrorId
|
|
|
|
}]
|
|
|
|
})
|
|
|
|
.matchHeaderSnapshot({
|
2023-01-17 14:56:29 +03:00
|
|
|
'content-version': anyContentVersion,
|
2022-09-13 19:29:37 +03:00
|
|
|
etag: anyEtag
|
|
|
|
});
|
2022-09-15 18:49:14 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
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: [{
|
2022-09-19 18:06:34 +03:00
|
|
|
id: anyErrorId,
|
|
|
|
context: stringMatching(/Invalid lexical structure\..*/)
|
2022-09-15 18:49:14 +03:00
|
|
|
}]
|
|
|
|
})
|
|
|
|
.matchHeaderSnapshot({
|
2022-09-19 18:37:54 +03:00
|
|
|
etag: anyEtag,
|
2023-01-17 14:56:29 +03:00
|
|
|
'content-version': anyContentVersion,
|
2022-09-19 18:37:54 +03:00
|
|
|
'content-length': anyStringNumber
|
2022-09-15 18:49:14 +03:00
|
|
|
});
|
2022-09-13 19:29:37 +03:00
|
|
|
});
|
2023-09-21 17:51:08 +03:00
|
|
|
|
|
|
|
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');
|
|
|
|
});
|
2022-09-13 19:29:37 +03:00
|
|
|
});
|
|
|
|
|
2022-09-16 13:59:35 +03:00
|
|
|
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({
|
2023-06-15 16:53:21 +03:00
|
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
2022-09-16 13:59:35 +03:00
|
|
|
})
|
|
|
|
.matchHeaderSnapshot({
|
2023-01-17 14:56:29 +03:00
|
|
|
'content-version': anyContentVersion,
|
2022-09-16 13:59:35 +03:00
|
|
|
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({
|
2023-06-15 16:53:21 +03:00
|
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
2022-09-16 13:59:35 +03:00
|
|
|
})
|
|
|
|
.matchHeaderSnapshot({
|
2023-01-17 14:56:29 +03:00
|
|
|
'content-version': anyContentVersion,
|
2022-09-16 13:59:35 +03:00
|
|
|
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({
|
2023-06-15 16:53:21 +03:00
|
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
2022-09-16 13:59:35 +03:00
|
|
|
})
|
|
|
|
.matchHeaderSnapshot({
|
2023-01-17 14:56:29 +03:00
|
|
|
'content-version': anyContentVersion,
|
2022-09-16 13:59:35 +03:00
|
|
|
etag: anyEtag,
|
|
|
|
location: anyLocationFor('posts')
|
|
|
|
});
|
|
|
|
|
|
|
|
const [postResponse] = postBody.posts;
|
|
|
|
|
|
|
|
await agent
|
2023-04-21 12:04:05 +03:00
|
|
|
.put(`/posts/${postResponse.id}/?formats=mobiledoc,lexical,html&save_revision=true`)
|
2022-09-16 13:59:35 +03:00
|
|
|
.body({posts: [Object.assign({}, postResponse, {lexical: updatedLexical})]})
|
|
|
|
.expectStatus(200)
|
|
|
|
.matchBodySnapshot({
|
2023-06-15 16:53:21 +03:00
|
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
2022-09-16 13:59:35 +03:00
|
|
|
})
|
|
|
|
.matchHeaderSnapshot({
|
2023-01-17 14:56:29 +03:00
|
|
|
'content-version': anyContentVersion,
|
2022-09-16 13:59:35 +03:00
|
|
|
etag: anyEtag,
|
2022-09-19 18:37:54 +03:00
|
|
|
'x-cache-invalidate': anyString
|
2022-09-16 13:59:35 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
});
|
2023-06-15 16:46:48 +03:00
|
|
|
|
|
|
|
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.'
|
|
|
|
}]
|
|
|
|
});
|
|
|
|
|
2023-07-11 09:39:14 +03:00
|
|
|
const collectionPostMatcher = {
|
|
|
|
id: anyObjectId
|
|
|
|
};
|
2023-06-15 16:46:48 +03:00
|
|
|
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
|
|
|
|
}]
|
|
|
|
};
|
2023-07-11 09:39:14 +03:00
|
|
|
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)
|
|
|
|
};
|
|
|
|
};
|
2023-06-15 16:46:48 +03:00
|
|
|
|
|
|
|
await agent.put(`/posts/${postResponse.id}/`)
|
|
|
|
.body({posts: [Object.assign({}, postResponse, {collections: [collectionToRemove.id]})]})
|
|
|
|
.expectStatus(200)
|
|
|
|
.matchBodySnapshot({
|
2023-07-21 11:17:32 +03:00
|
|
|
posts: [
|
|
|
|
Object.assign({}, matchPostShallowIncludes, {published_at: null}, {collections: [
|
|
|
|
// collectionToRemove
|
|
|
|
collectionMatcher,
|
|
|
|
// automatic "latest" collection which cannot be removed
|
2023-10-23 19:51:34 +03:00
|
|
|
buildCollectionMatcher(21)
|
2023-07-21 11:17:32 +03:00
|
|
|
]})]
|
2023-06-15 16:46:48 +03:00
|
|
|
})
|
|
|
|
.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({
|
2023-07-11 09:39:14 +03:00
|
|
|
posts: [
|
|
|
|
Object.assign({}, matchPostShallowIncludes, {published_at: null}, {collections: [
|
2023-07-21 11:17:32 +03:00
|
|
|
// collectionToAdd
|
2023-07-12 15:14:28 +03:00
|
|
|
collectionMatcher,
|
2023-07-21 11:17:32 +03:00
|
|
|
// automatic "latest" collection which cannot be removed
|
2023-10-23 19:51:34 +03:00
|
|
|
buildCollectionMatcher(21)
|
2023-07-21 11:17:32 +03:00
|
|
|
]})]
|
2023-06-15 16:46:48 +03:00
|
|
|
})
|
|
|
|
.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}/)
|
|
|
|
});
|
|
|
|
});
|
2023-09-21 17:51:08 +03:00
|
|
|
|
|
|
|
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');
|
|
|
|
});
|
2022-09-16 13:59:35 +03:00
|
|
|
});
|
|
|
|
|
2022-08-22 19:35:37 +03:00
|
|
|
describe('Delete', function () {
|
|
|
|
it('Can destroy a post', async function () {
|
|
|
|
await agent
|
|
|
|
.delete(`posts/${fixtureManager.get('posts', 0).id}/`)
|
|
|
|
.expectStatus(204)
|
|
|
|
.expectEmptyBody()
|
|
|
|
.matchHeaderSnapshot({
|
2023-01-17 14:56:29 +03:00
|
|
|
'content-version': anyContentVersion,
|
2022-08-22 19:35:37 +03:00
|
|
|
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({
|
2023-01-17 14:56:29 +03:00
|
|
|
'content-version': anyContentVersion,
|
2022-08-22 19:35:37 +03:00
|
|
|
etag: anyEtag
|
|
|
|
})
|
|
|
|
.matchBodySnapshot({
|
|
|
|
errors: [{
|
|
|
|
id: anyErrorId
|
|
|
|
}]
|
|
|
|
});
|
|
|
|
});
|
2023-09-14 08:18:38 +03:00
|
|
|
|
|
|
|
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();
|
|
|
|
});
|
2023-09-21 17:51:08 +03:00
|
|
|
|
|
|
|
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');
|
|
|
|
});
|
2022-08-22 19:35:37 +03:00
|
|
|
});
|
2023-05-15 11:30:32 +03:00
|
|
|
|
|
|
|
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({
|
2023-06-15 16:53:21 +03:00
|
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
|
2023-05-15 11:30:32 +03:00
|
|
|
})
|
|
|
|
.matchHeaderSnapshot({
|
|
|
|
'content-version': anyContentVersion,
|
|
|
|
etag: anyEtag,
|
|
|
|
location: anyLocationFor('posts')
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2023-08-09 01:44:54 +03:00
|
|
|
|
|
|
|
describe('Convert', function () {
|
2023-09-07 00:16:40 +03:00
|
|
|
it('can convert a mobiledoc post to lexical', async function () {
|
2023-08-09 01:44:54 +03:00
|
|
|
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;
|
|
|
|
|
2023-10-17 23:38:51 +03:00
|
|
|
const conversionResponse = await agent
|
2023-08-09 01:44:54 +03:00
|
|
|
.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
|
|
|
|
});
|
2023-10-17 23:38:51 +03:00
|
|
|
|
|
|
|
const convertedPost = conversionResponse.body.posts[0];
|
|
|
|
const expectedConvertedLexical = convertedPost.lexical;
|
|
|
|
await agent
|
|
|
|
.put(`/posts/${postResponse.id}/?formats=mobiledoc,lexical,html&convert_to_lexical=true`)
|
|
|
|
.body({posts: [Object.assign({}, convertedPost)]})
|
|
|
|
.expectStatus(200)
|
|
|
|
.matchBodySnapshot({
|
|
|
|
posts: [Object.assign({}, matchPostShallowIncludes, {lexical: expectedConvertedLexical, mobiledoc: null})]
|
|
|
|
})
|
|
|
|
.matchHeaderSnapshot({
|
|
|
|
'content-version': anyContentVersion,
|
|
|
|
etag: anyEtag
|
|
|
|
});
|
2023-08-09 01:44:54 +03:00
|
|
|
});
|
|
|
|
});
|
2022-08-22 19:35:37 +03:00
|
|
|
});
|