From 3214186f9871fba310e8dba9f459b57834e49d35 Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Thu, 12 May 2022 14:28:45 +0200 Subject: [PATCH] Improved newsletter limit checking (#14780) refs https://github.com/TryGhost/Team/issues/1583 - Check limits when unarchiving newsletters - Added tests for more scenarios - When editing/adding newsletters, the limit check happens in the same transaction. - `limit-service` was bumped to add transactions support - Added transaction support for edit in newsletter service --- core/server/services/newsletters/service.js | 18 +- package.json | 2 +- .../__snapshots__/newsletters.test.js.snap | 354 ++++++++++++++++++ test/e2e-api/admin/newsletters.test.js | 186 ++++++++- yarn.lock | 8 +- 5 files changed, 557 insertions(+), 11 deletions(-) diff --git a/core/server/services/newsletters/service.js b/core/server/services/newsletters/service.js index 717308a5da..06b5fc9844 100644 --- a/core/server/services/newsletters/service.js +++ b/core/server/services/newsletters/service.js @@ -112,7 +112,8 @@ class NewslettersService { * @public * @param {object} attrs model properties * @param {Object} [options] options - * @param {Object} [options] options.transacting + * @param {boolean} [options.opt_in_existing] Opt in existing members + * @param {Object} [options.transacting] * @returns {Promise<{object}>} Newsetter Model with verification metadata */ async add(attrs, options = {}) { @@ -124,7 +125,9 @@ class NewslettersService { }); } - await this.limitService.errorIfWouldGoOverLimit('newsletters'); + if (!attrs.status || attrs.status === 'active') { + await this.limitService.errorIfWouldGoOverLimit('newsletters', options.transacting ? {transacting: options.transacting} : {}); + } // remove any email properties that are not allowed to be set without verification const {cleanedAttrs, emailsToVerify} = await this.prepAttrsForEmailVerification(attrs); @@ -177,14 +180,21 @@ class NewslettersService { * @param {object} attrs model properties * @param {Object} options options * @param {string} options.id Newsletter id to edit + * @param {Object} [options.transacting] * @returns {Promise<{object}>} Newsetter Model with verification metadata */ async edit(attrs, options) { + const sharedOptions = _.pick(options, 'transacting'); + // fetch newsletter first so we can compare changed emails - const originalNewsletter = await this.NewsletterModel.findOne({id: options.id}, {require: true}); + const originalNewsletter = await this.NewsletterModel.findOne({id: options.id}, {...sharedOptions, require: true}); const {cleanedAttrs, emailsToVerify} = await this.prepAttrsForEmailVerification(attrs, originalNewsletter); + if (originalNewsletter.status !== 'active' && cleanedAttrs.status === 'active') { + await this.limitService.errorIfWouldGoOverLimit('newsletters', sharedOptions); + } + let updatedNewsletter; try { @@ -307,7 +317,7 @@ class NewslettersService { /** * @typedef {object} ILimitService - * @prop {(name: string) => Promise} errorIfWouldGoOverLimit + * @prop {(name: string, options?: {transacting?: Object}) => Promise} errorIfWouldGoOverLimit **/ module.exports = NewslettersService; diff --git a/package.json b/package.json index 17d1244cde..69abfefde2 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@tryghost/kg-default-cards": "5.16.2", "@tryghost/kg-markdown-html-renderer": "5.1.5", "@tryghost/kg-mobiledoc-html-renderer": "5.3.5", - "@tryghost/limit-service": "1.1.2", + "@tryghost/limit-service": "1.2.0", "@tryghost/logging": "2.1.8", "@tryghost/magic-link": "1.0.25", "@tryghost/member-events": "0.4.5", diff --git a/test/e2e-api/admin/__snapshots__/newsletters.test.js.snap b/test/e2e-api/admin/__snapshots__/newsletters.test.js.snap index 1f37fc7f5b..98e625f892 100644 --- a/test/e2e-api/admin/__snapshots__/newsletters.test.js.snap +++ b/test/e2e-api/admin/__snapshots__/newsletters.test.js.snap @@ -1062,6 +1062,360 @@ Object { } `; +exports[`Newsletters API Host Settings: newsletter limits Max limit Adding a newsletter now doesn't fail 1: [body] 1`] = ` +Object { + "meta": Object { + "opted_in_member_count": 6, + }, + "newsletters": Array [ + Object { + "body_font_category": "sans_serif", + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "description": null, + "footer_content": null, + "header_image": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Naughty newsletter", + "sender_email": null, + "sender_name": null, + "sender_reply_to": "newsletter", + "show_badge": true, + "show_feature_image": true, + "show_header_icon": true, + "show_header_name": true, + "show_header_title": true, + "slug": "naughty-newsletter", + "sort_order": 3, + "status": "active", + "subscribe_on_signup": true, + "title_alignment": "center", + "title_font_category": "sans_serif", + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "visibility": "members", + }, + ], +} +`; + +exports[`Newsletters API Host Settings: newsletter limits Max limit Adding a newsletter now doesn't fail 2: [headers] 1`] = ` +Object { + "access-control-allow-origin": "http://127.0.0.1:2369", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "695", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/newsletters\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, + "vary": "Origin, Accept-Encoding", + "x-cache-invalidate": "/*", + "x-powered-by": "Express", +} +`; + +exports[`Newsletters API Host Settings: newsletter limits Max limit Adding an archived newsletter doesn't fail 1: [body] 1`] = ` +Object { + "meta": Object { + "opted_in_member_count": 6, + }, + "newsletters": Array [ + Object { + "body_font_category": "sans_serif", + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "description": null, + "footer_content": null, + "header_image": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Archived newsletter", + "sender_email": null, + "sender_name": null, + "sender_reply_to": "newsletter", + "show_badge": true, + "show_feature_image": true, + "show_header_icon": true, + "show_header_name": true, + "show_header_title": true, + "slug": "archived-newsletter", + "sort_order": 3, + "status": "archived", + "subscribe_on_signup": true, + "title_alignment": "center", + "title_font_category": "sans_serif", + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "visibility": "members", + }, + ], +} +`; + +exports[`Newsletters API Host Settings: newsletter limits Max limit Adding an archived newsletter doesn't fail 2: [body] 1`] = ` +Object { + "meta": Object { + "opted_in_member_count": 6, + }, + "newsletters": Array [ + Object { + "body_font_category": "sans_serif", + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "description": null, + "footer_content": null, + "header_image": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Archived newsletter", + "sender_email": null, + "sender_name": null, + "sender_reply_to": "newsletter", + "show_badge": true, + "show_feature_image": true, + "show_header_icon": true, + "show_header_name": true, + "show_header_title": true, + "slug": "archived-newsletter", + "sort_order": 3, + "status": "archived", + "subscribe_on_signup": true, + "title_alignment": "center", + "title_font_category": "sans_serif", + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "visibility": "members", + }, + ], +} +`; + +exports[`Newsletters API Host Settings: newsletter limits Max limit Adding an archived newsletter doesn't fail 2: [headers] 1`] = ` +Object { + "access-control-allow-origin": "http://127.0.0.1:2369", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "699", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/newsletters\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, + "vary": "Origin, Accept-Encoding", + "x-cache-invalidate": "/*", + "x-powered-by": "Express", +} +`; + +exports[`Newsletters API Host Settings: newsletter limits Max limit Adding an archived newsletter doesn't fail 3: [headers] 1`] = ` +Object { + "access-control-allow-origin": "http://127.0.0.1:2369", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "699", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/newsletters\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, + "vary": "Origin, Accept-Encoding", + "x-cache-invalidate": "/*", + "x-powered-by": "Express", +} +`; + +exports[`Newsletters API Host Settings: newsletter limits Max limit Adding newsletter fails 1: [body] 1`] = ` +Object { + "errors": Array [ + Object { + "code": null, + "context": "Your plan supports up to 3 newsletters. Please upgrade to add more.", + "details": Object { + "limit": 3, + "name": "newsletters", + "total": 3, + }, + "ghostErrorCode": null, + "help": "https://ghost.org/help/", + "id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "message": "Host Limit error, cannot save newsletter.", + "property": null, + "type": "HostLimitError", + }, + ], +} +`; + +exports[`Newsletters API Host Settings: newsletter limits Max limit Adding newsletter fails without transaction 1: [body] 1`] = ` +Object { + "errors": Array [ + Object { + "code": null, + "context": "Your plan supports up to 3 newsletters. Please upgrade to add more.", + "details": Object { + "limit": 3, + "name": "newsletters", + "total": 3, + }, + "ghostErrorCode": null, + "help": "https://ghost.org/help/", + "id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "message": "Host Limit error, cannot save newsletter.", + "property": null, + "type": "HostLimitError", + }, + ], +} +`; + +exports[`Newsletters API Host Settings: newsletter limits Max limit Archiving a newsletter doesn't fail 1: [body] 1`] = ` +Object { + "newsletters": Array [ + Object { + "body_font_category": "sans_serif", + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "description": null, + "footer_content": null, + "header_image": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Default Newsletter", + "sender_email": null, + "sender_name": null, + "sender_reply_to": "newsletter", + "show_badge": true, + "show_feature_image": true, + "show_header_icon": true, + "show_header_name": true, + "show_header_title": true, + "slug": "default-newsletter", + "sort_order": 0, + "status": "archived", + "subscribe_on_signup": true, + "title_alignment": "center", + "title_font_category": "sans_serif", + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "visibility": "members", + }, + ], +} +`; + +exports[`Newsletters API Host Settings: newsletter limits Max limit Archiving a newsletter doesn't fail 2: [headers] 1`] = ` +Object { + "access-control-allow-origin": "http://127.0.0.1:2369", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "662", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Origin, Accept-Encoding", + "x-cache-invalidate": "/*", + "x-powered-by": "Express", +} +`; + +exports[`Newsletters API Host Settings: newsletter limits Max limit Editing an archived newsletter doesn't fail 1: [body] 1`] = ` +Object { + "newsletters": Array [ + Object { + "body_font_category": "serif", + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "description": null, + "footer_content": null, + "header_image": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Updated archived newsletter name", + "sender_email": "jamie@example.com", + "sender_name": "Jamie", + "sender_reply_to": "newsletter", + "show_badge": true, + "show_feature_image": true, + "show_header_icon": true, + "show_header_name": true, + "show_header_title": true, + "slug": "old-newsletter", + "sort_order": 2, + "status": "archived", + "subscribe_on_signup": true, + "title_alignment": "center", + "title_font_category": "serif", + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "visibility": "members", + }, + ], +} +`; + +exports[`Newsletters API Host Settings: newsletter limits Max limit Editing an archived newsletter doesn't fail 2: [headers] 1`] = ` +Object { + "access-control-allow-origin": "http://127.0.0.1:2369", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "680", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Origin, Accept-Encoding", + "x-cache-invalidate": "/*", + "x-powered-by": "Express", +} +`; + +exports[`Newsletters API Host Settings: newsletter limits Max limit Editing an archived newsletter doesn't fails 1: [body] 1`] = ` +Object { + "newsletters": Array [ + Object { + "body_font_category": "serif", + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "description": null, + "footer_content": null, + "header_image": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Updated archived newsletter name", + "sender_email": "jamie@example.com", + "sender_name": "Jamie", + "sender_reply_to": "newsletter", + "show_badge": true, + "show_feature_image": true, + "show_header_icon": true, + "show_header_name": true, + "show_header_title": true, + "slug": "old-newsletter", + "sort_order": 2, + "status": "archived", + "subscribe_on_signup": true, + "title_alignment": "center", + "title_font_category": "serif", + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "visibility": "members", + }, + ], +} +`; + +exports[`Newsletters API Host Settings: newsletter limits Max limit Editing an archived newsletter doesn't fails 2: [headers] 1`] = ` +Object { + "access-control-allow-origin": "http://127.0.0.1:2369", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "680", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Origin, Accept-Encoding", + "x-cache-invalidate": "/*", + "x-powered-by": "Express", +} +`; + +exports[`Newsletters API Host Settings: newsletter limits Max limit Unarchiving a newsletter fails 1: [body] 1`] = ` +Object { + "errors": Array [ + Object { + "code": null, + "context": "Your plan supports up to 3 newsletters. Please upgrade to add more.", + "details": Object { + "limit": 3, + "name": "newsletters", + "total": 3, + }, + "ghostErrorCode": null, + "help": "https://ghost.org/help/", + "id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "message": "Host Limit error, cannot edit newsletter.", + "property": null, + "type": "HostLimitError", + }, + ], +} +`; + exports[`Newsletters API Host Settings: newsletter limits Request fails when newsletter limit is in place 1: [body] 1`] = ` Object { "errors": Array [ diff --git a/test/e2e-api/admin/newsletters.test.js b/test/e2e-api/admin/newsletters.test.js index 3cadc5257c..69cd0dbda3 100644 --- a/test/e2e-api/admin/newsletters.test.js +++ b/test/e2e-api/admin/newsletters.test.js @@ -1,6 +1,7 @@ const assert = require('assert'); const {agentProvider, mockManager, fixtureManager, configUtils, dbUtils, matchers} = require('../../utils/e2e-framework'); const {anyEtag, anyObjectId, anyUuid, anyISODateTime, anyLocationFor, anyNumber} = matchers; +const models = require('../../../core/server/models'); const assertMemberRelationCount = async (newsletterId, expectedCount) => { const relations = await dbUtils.knex('members_newsletters').where({newsletter_id: newsletterId}).pluck('id'); @@ -358,10 +359,10 @@ describe('Newsletters API', function () { }); describe('Host Settings: newsletter limits', function () { - afterEach(function () { + after(function () { configUtils.set('hostSettings:limits', undefined); }); - + it('Request fails when newsletter limit is in place', async function () { configUtils.set('hostSettings:limits', { newsletters: { @@ -388,6 +389,187 @@ describe('Newsletters API', function () { }] }); }); + + describe('Max limit', function () { + before(async function () { + configUtils.set('hostSettings:limits', { + newsletters: { + max: 3, + error: 'Your plan supports up to {{max}} newsletters. Please upgrade to add more.' + } + }); + + agent = await agentProvider.getAdminAPIAgent(); + await fixtureManager.init('newsletters', 'members:newsletters'); + await agent.loginAsOwner(); + }); + + it('Adding newsletter fails', async function () { + const allNewsletters = await models.Newsletter.findAll(); + const newsletterCount = allNewsletters.filter(n => n.get('status') === 'active').length; + assert.equal(newsletterCount, 3, 'This test expects to have 3 current active newsletters'); + + const newsletter = { + name: 'Naughty newsletter' + }; + + await agent + .post(`newsletters/?opt_in_existing=true`) + .body({newsletters: [newsletter]}) + .expectStatus(403) + .matchBodySnapshot({ + errors: [{ + id: anyUuid + }] + }) + .expect(({body}) => { + assert.equal(body.errors[0].context, 'Your plan supports up to 3 newsletters. Please upgrade to add more.'); + }); + }); + + it('Adding newsletter fails without transaction', async function () { + const allNewsletters = await models.Newsletter.findAll(); + const newsletterCount = allNewsletters.filter(n => n.get('status') === 'active').length; + assert.equal(newsletterCount, 3, 'This test expects to have 3 current active newsletters'); + + const newsletter = { + name: 'Naughty newsletter' + }; + + // Note that ?opt_in_existing=true will trigger a transaction, so we explicitly test here without a + // transaction + await agent + .post(`newsletters/`) + .body({newsletters: [newsletter]}) + .expectStatus(403) + .matchBodySnapshot({ + errors: [{ + id: anyUuid + }] + }) + .expect(({body}) => { + assert.equal(body.errors[0].context, 'Your plan supports up to 3 newsletters. Please upgrade to add more.'); + }); + }); + + it('Adding an archived newsletter doesn\'t fail', async function () { + const allNewsletters = await models.Newsletter.findAll(); + const newsletterCount = allNewsletters.filter(n => n.get('status') === 'active').length; + assert.equal(newsletterCount, 3, 'This test expects to have 3 current active newsletters'); + + const newsletter = { + name: 'Archived newsletter', + status: 'archived' + }; + + await agent + .post(`newsletters/?opt_in_existing=true`) + .body({newsletters: [newsletter]}) + .expectStatus(201) + .matchBodySnapshot({ + newsletters: [newsletterSnapshot] + }) + .matchHeaderSnapshot({ + etag: anyEtag, + location: anyLocationFor('newsletters') + }); + }); + + it('Editing an archived newsletter doesn\'t fail', async function () { + const allNewsletters = await models.Newsletter.findAll(); + const newsletterCount = allNewsletters.filter(n => n.get('status') === 'active').length; + assert.equal(newsletterCount, 3, 'This test expects to have 3 current active newsletters'); + + const archivedNewsletter = allNewsletters.find(n => n.get('status') !== 'active'); + assert.ok(archivedNewsletter, 'This test expects to have an archived newsletter in the test fixtures'); + + const id = archivedNewsletter.id; + await agent.put(`newsletters/${id}`) + .body({ + newsletters: [{ + name: 'Updated archived newsletter name' + }] + }) + .expectStatus(200) + .matchBodySnapshot({ + newsletters: [newsletterSnapshot] + }) + .matchHeaderSnapshot({ + etag: anyEtag + }); + }); + + it('Unarchiving a newsletter fails', async function () { + const allNewsletters = await models.Newsletter.findAll(); + const newsletterCount = allNewsletters.filter(n => n.get('status') === 'active').length; + assert.equal(newsletterCount, 3, 'This test expects to have 3 current active newsletters'); + + const archivedNewsletter = allNewsletters.find(n => n.get('status') !== 'active'); + assert.ok(archivedNewsletter, 'This test expects to have an archived newsletter in the test fixtures'); + + const id = archivedNewsletter.id; + await agent.put(`newsletters/${id}`) + .body({ + newsletters: [{ + status: 'active' + }] + }) + .expectStatus(403) + .matchBodySnapshot({ + errors: [{ + id: anyUuid + }] + }) + .expect(({body}) => { + assert.equal(body.errors[0].context, 'Your plan supports up to 3 newsletters. Please upgrade to add more.'); + }); + }); + + it('Archiving a newsletter doesn\'t fail', async function () { + const allNewsletters = await models.Newsletter.findAll(); + const newsletterCount = allNewsletters.filter(n => n.get('status') === 'active').length; + assert.equal(newsletterCount, 3, 'This test expects to have 3 current active newsletters'); + + const activeNewsletter = allNewsletters.find(n => n.get('status') === 'active'); + + const id = activeNewsletter.id; + await agent.put(`newsletters/${id}`) + .body({ + newsletters: [{ + status: 'archived' + }] + }) + .expectStatus(200) + .matchBodySnapshot({ + newsletters: [newsletterSnapshot] + }) + .matchHeaderSnapshot({ + etag: anyEtag + }); + }); + + it('Adding a newsletter now doesn\'t fail', async function () { + const allNewsletters = await models.Newsletter.findAll(); + const newsletterCount = allNewsletters.filter(n => n.get('status') === 'active').length; + assert.equal(newsletterCount, 2, 'This test expects to have 2 current active newsletters'); + + const newsletter = { + name: 'Naughty newsletter' + }; + + await agent + .post(`newsletters/?opt_in_existing=true`) + .body({newsletters: [newsletter]}) + .expectStatus(201) + .matchBodySnapshot({ + newsletters: [newsletterSnapshot] + }) + .matchHeaderSnapshot({ + etag: anyEtag, + location: anyLocationFor('newsletters') + }); + }); + }); }); it(`Can't add multiple newsletters with same name`, async function () { diff --git a/yarn.lock b/yarn.lock index 471060e340..97afdc5617 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2106,10 +2106,10 @@ dependencies: semver "^7.3.5" -"@tryghost/limit-service@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@tryghost/limit-service/-/limit-service-1.1.2.tgz#248f5b1c5383d302a6e29e8bcd2948ff9eb9ab4f" - integrity sha512-aOUVnNvhYDANqtx+2pS0c0RADG39QDQ5tB7CLPjpcz2B6Nsc1fg4dEJeXU8fI1JT0YpMDRSliiNRh0/MMkozbA== +"@tryghost/limit-service@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@tryghost/limit-service/-/limit-service-1.2.0.tgz#fea838bcb77e30b043f670c73237546bebfa4686" + integrity sha512-awO9ZANst25v5+udlkwAcVV3OivisiWEX4OcD4i1YlSqPqcc5ncYr0ozlNjMR3djY0gX+rfN+WUc20ybwWL0Xg== dependencies: "@tryghost/errors" "^1.2.1" lodash "^4.17.21"