const should = require('should'); const DomainEvents = require('@tryghost/domain-events'); const {mobiledocToLexical} = require('@tryghost/kg-converters'); const {agentProvider, fixtureManager, mockManager} = require('../../utils/e2e-framework'); const models = require('../../../core/server/models'); const assert = require('assert/strict'); describe('Posts Bulk API', function () { let agent; before(async function () { mockManager.mockLabsEnabled('collections'); mockManager.mockLabsEnabled('collectionsCard'); agent = await agentProvider.getAdminAPIAgent(); // Note that we generate lots of fixtures here to test the bulk deletion correctly await fixtureManager.init('posts', 'newsletters', 'members:newsletters', 'emails', 'redirects', 'clicks', 'comments', 'feedback', 'links', 'mentions'); 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 () { // give 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 = "

Testing

" WHERE type = \'page\' AND html IS NULL'); mockManager.restore(); }); describe('Edit', function () { it('Can feature multiple posts', async function () { const filter = 'status:[published,draft,scheduled,sent]'; // Check all the posts that should be affected const changedPosts = await models.Post.findPage({filter, limit: 1, status: 'all'}); const amount = changedPosts.meta.pagination.total; assert(amount > 0, 'Expect at least one post to be affected for this test to work'); let featuredCollection = await models.Collection.findPage({filter: 'slug:featured', limit: 1, withRelated: ['collectionPosts']}); let featuredCollectionPostsAmount = featuredCollection.data[0].toJSON().collectionPosts.length; assert(featuredCollectionPostsAmount > 0, 'Expect to have multiple featured collection posts'); const response = await agent .put('/posts/bulk/?filter=' + encodeURIComponent(filter)) .body({ bulk: { action: 'feature' } }) .expectStatus(200) .matchBodySnapshot(); await DomainEvents.allSettled(); assert.equal(response.body.bulk.meta.stats.successful, amount, `Expect all matching posts (${amount}) to be changed`); // Check page HTML was reset to enable re-render of collection cards // Must be done before fetch all posts otherwise they will be re-rendered const totalPageCount = await models.Post.where({type: 'page'}).count(); const emptyPageCount = await models.Post.where({html: null, type: 'page'}).count(); should.exist(emptyPageCount); emptyPageCount.should.equal(totalPageCount, 'no. of render-queued pages after bulk edit'); // Fetch all posts and check if they are featured const posts = await models.Post.findAll({filter, status: 'all'}); assert.equal(posts.length, amount, `Expect all matching posts (${amount}) to be changed`); featuredCollection = await models.Collection.findPage({filter: 'slug:featured', limit: 1, withRelated: ['collectionPosts']}); featuredCollectionPostsAmount = featuredCollection.data[0].toJSON().collectionPosts.length; assert.equal(featuredCollectionPostsAmount, amount, 'Expect to have same amount featured collection posts as changed'); for (const post of posts) { assert(post.get('featured') === true, `Expect post ${post.id} to be featured`); } }); it('Can unfeature multiple posts', async function () { const filter = 'status:[published,draft,scheduled,sent]'; // Check all the posts that should be affected const changedPosts = await models.Post.findPage({filter, limit: 1, status: 'all'}); const amount = changedPosts.meta.pagination.total; assert(amount > 0, 'Expect at least one post to be affected for this test to work'); let featuredCollection = await models.Collection.findPage({filter: 'slug:featured', limit: 1, withRelated: ['collectionPosts']}); let featuredCollectionPostsAmount = featuredCollection.data[0].toJSON().collectionPosts.length; assert(featuredCollectionPostsAmount > 0, 'Expect to have multiple featured collection posts'); const response = await agent .put('/posts/bulk/?filter=' + encodeURIComponent(filter)) .body({ bulk: { action: 'unfeature' } }) .expectStatus(200) .matchBodySnapshot(); await DomainEvents.allSettled(); assert.equal(response.body.bulk.meta.stats.successful, amount, `Expect all matching posts (${amount}) to be changed`); // Fetch all posts and check if they are featured const posts = await models.Post.findAll({filter, status: 'all'}); assert.equal(posts.length, amount, `Expect all matching posts (${amount}) to be changed`); featuredCollection = await models.Collection.findPage({filter: 'slug:featured', limit: 1, withRelated: ['collectionPosts']}); featuredCollectionPostsAmount = featuredCollection.data[0].toJSON().collectionPosts.length; assert.equal(featuredCollectionPostsAmount, 0, 'Expect to have no featured collection posts'); for (const post of posts) { assert(post.get('featured') === false, `Expect post ${post.id} to be unfeatured`); } }); it('Can change access of posts', async function () { const filter = 'status:[published,draft,scheduled,sent]'; // Check all the posts that should be affected const changedPosts = await models.Post.findPage({filter, limit: 1, status: 'all'}); const amount = changedPosts.meta.pagination.total; assert(amount > 0, 'Expect at least one post to be affected for this test to work'); const response = await agent .put('/posts/bulk/?filter=' + encodeURIComponent(filter)) .body({ bulk: { action: 'access', meta: { visibility: 'paid' } } }) .expectStatus(200) .matchBodySnapshot(); assert.equal(response.body.bulk.meta.stats.successful, amount, `Expect all matching posts (${amount}) to be changed`); // Fetch all posts and check if they have the correct access const posts = await models.Post.findAll({filter, status: 'all'}); assert.equal(posts.length, amount, `Expect all matching posts (${amount}) to be changed`); for (const post of posts) { assert(post.get('visibility') === 'paid', `Expect post ${post.id} to have access 'paid'`); } }); it('Can change access of posts to tiers', async function () { const filter = 'status:[published,draft,scheduled,sent]'; const products = await models.Product.findAll(); const tier1 = products.models[0]; const tier2 = products.models[1]; assert(tier1.id && tier2.id); // Check all the posts that should be affected const changedPosts = await models.Post.findPage({filter, limit: 1, status: 'all'}); const amount = changedPosts.meta.pagination.total; assert(amount > 0, 'Expect at least one post to be affected for this test to work'); const response = await agent .put('/posts/bulk/?filter=' + encodeURIComponent(filter)) .body({ bulk: { action: 'access', meta: { visibility: 'tiers', tiers: [ { id: tier1.id }, { id: tier2.id } ] } } }) .expectStatus(200) .matchBodySnapshot(); assert.equal(response.body.bulk.meta.stats.successful, amount, `Expect all matching posts (${amount}) to be changed`); // Fetch all posts and check if they have the correct access const posts = await models.Post.findAll({filter, status: 'all', withRelated: ['tiers']}); assert.equal(posts.length, amount, `Expect all matching posts (${amount}) to be changed`); for (const post of posts) { assert(post.get('visibility') === 'tiers', `Expect post ${post.id} to have access 'tiers'`); assert.equal(post.related('tiers').length, 2); } }); it('Can add a single tag to posts', async function () { const filter = 'status:[published]'; const tag = await models.Tag.findOne({slug: fixtureManager.get('tags', 0).slug}); assert(tag); // Check all the posts that should be affected const changedPosts = await models.Post.findPage({filter, limit: 1, status: 'all'}); const amount = changedPosts.meta.pagination.total; assert(amount > 0, 'Expect at least one post to be affected for this test to work'); const response = await agent .put('/posts/bulk/?filter=' + encodeURIComponent(filter)) .body({ bulk: { action: 'addTag', meta: { tags: [ { id: tag.id } ] } } }) .expectStatus(200) .matchBodySnapshot(); assert.equal(response.body.bulk.meta.stats.successful, amount, `Expect all matching posts (${amount}) to be changed, got ${response.body.bulk.meta.stats.successful} instead`); // Fetch all posts and check if they have the tag const posts = await models.Post.findAll({filter, status: 'all', withRelated: ['tags']}); assert.equal(posts.length, amount, `Expect all matching posts (${amount}) to be changed`); for (const post of posts) { const tags = post.related('tags'); // Check tag is in the list assert(tags.find(t => t.id === tag.id), `Expect post ${post.id} to have tag ${tag.id}`); } }); it('Can add multiple tags to posts and create new tags', async function () { const filter = 'status:[draft]'; const tag = await models.Tag.findOne({id: fixtureManager.get('tags', 1).id}); assert(tag); const newTag = { name: 'Just a random new tag' }; // Check all the posts that should be affected const changedPosts = await models.Post.findPage({filter, limit: 1, status: 'all'}); const amount = changedPosts.meta.pagination.total; assert(amount > 0, 'Expect at least one post to be affected for this test to work'); const response = await agent .put('/posts/bulk/?filter=' + encodeURIComponent(filter)) .body({ bulk: { action: 'addTag', meta: { tags: [ { id: tag.id }, { name: newTag.name } ] } } }) .expectStatus(200) .matchBodySnapshot(); assert.equal(response.body.bulk.meta.stats.successful, amount, `Expect all matching posts (${amount}) to be changed, got ${response.body.bulk.meta.stats.successful} instead`); // Check if the new tag was created const newTags = await models.Tag.findAll({filter: `name:'${newTag.name}'`}); assert.equal(newTags.length, 1, `Expect tag to be created`); const newTagModel = newTags.models[0]; // Fetch all posts and check if they have the tag const posts = await models.Post.findAll({filter, status: 'all', withRelated: ['tags']}); assert.equal(posts.length, amount, `Expect all matching posts (${amount}) to be changed`); for (const post of posts) { const tags = post.related('tags'); // Check tag is in the list assert(tags.find(t => t.id === tag.id), `Expect post ${post.id} to have tag ${tag.id}`); assert(tags.find(t => t.id === newTagModel.id), `Expect post ${post.id} to have new tag ${newTagModel.id}`); } }); it('Can unpublish posts', async function () { const filter = 'status:[published]'; const changedPosts = await models.Post.findPage({filter, status: 'published'}); const amount = changedPosts.meta.pagination.total; assert(amount > 0, 'Expect at least one post to be affected for this test to work'); const response = await agent .put('/posts/bulk/?filter=' + encodeURIComponent(filter)) .body({ bulk: { action: 'unpublish' } }) .expectStatus(200) .matchBodySnapshot(); assert.equal(response.body.bulk.meta.stats.successful, amount, `Expect all matching posts (${amount}) to be unpublished, got ${response.body.bulk.meta.stats.successful} instead`); // Fetch all posts and check if they are unpublished const posts = await models.Post.findAll({filter, status: 'all'}); assert.equal(posts.length, 0, `Expect all matching posts (${amount}) to be unpublished`); // Re-publish the posts so we don't affect later tests const postIds = changedPosts.data.map(post => post.id); await models.Base.knex.raw(`UPDATE posts SET status = \'published\' WHERE id IN (${postIds.map(() => '?').join(',')})`, [...postIds]); }); }); describe('Delete', function () { it('Can delete posts that match a tag', async function () { const tag = await models.Tag.findOne({id: fixtureManager.get('tags', 0).id}); const filter = 'tag:' + tag.get('slug'); // Check all the posts that should be affected const changedPosts = await models.Post.findPage({filter, limit: 1, status: 'all'}); const amount = changedPosts.meta.pagination.total; assert(amount > 0, 'Expect at least one post to be affected for this test to work'); await agent .get('posts/?collection=latest') .expectStatus(200) .expect((res) => { assert(res.body.posts.length > 0, 'Expect latest collection to have some posts'); }); const response = await agent .delete('/posts/?filter=' + encodeURIComponent(filter)) .expectStatus(200) .matchBodySnapshot(); assert.equal(response.body.bulk.meta.stats.successful, amount, `Expect all matching posts (${amount}) to be deleted, got ${response.body.bulk.meta.stats.successful} instead`); // Check if all posts were deleted const posts = await models.Post.findPage({filter, status: 'all'}); assert.equal(posts.meta.pagination.total, 0, `Expect all matching posts (${amount}) to be deleted`); // Check page HTML was reset to enable re-render of collection cards const totalPageCount = await models.Post.where({type: 'page'}).count(); const emptyPageCount = await models.Post.where({html: null, type: 'page'}).count(); should.exist(emptyPageCount); emptyPageCount.should.equal(totalPageCount, 'no. of render-queued pages after bulk delete'); }); it('Can delete all posts', async function () { const filter = 'status:[published,draft,scheduled,sent]'; // Check all the posts that should be affected const changedPosts = await models.Post.findPage({filter, limit: 1, status: 'all'}); const amount = changedPosts.meta.pagination.total; assert(amount > 0, 'Expect at least one post to be affected for this test to work'); const response = await agent .delete('/posts/?filter=' + encodeURIComponent(filter)) .expectStatus(200) .matchBodySnapshot(); assert.equal(response.body.bulk.meta.stats.successful, amount, `Expect all matching posts (${amount}) to be deleted, got ${response.body.bulk.meta.stats.successful} instead`); // Check if all posts were deleted const posts = await models.Post.findPage({filter, status: 'all'}); assert.equal(posts.meta.pagination.total, 0, `Expect all matching posts (${amount}) to be deleted`); let latestCollection = await models.Collection.findPage({filter: 'slug:latest', limit: 1, withRelated: ['collectionPosts']}); latestCollection = latestCollection.data[0].toJSON().collectionPosts.length; assert.equal(latestCollection, 0, 'Expect to have no collection posts'); }); }); });