From 8aa55feaf8e500a0e5077a46bc0c2fbee1b10c40 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Fri, 11 Dec 2020 18:45:35 +0000 Subject: [PATCH] Added acceptance test for `/member/:id/?include=email_recipients` (#12477) refs https://github.com/TryGhost/Ghost/commit/c1d66f0b019a0c63efb36fa9605befdf36175ac3 - fixed base model allowing '@@INDEXES@@' as a permitted attribute/order - fixed base model automatically setting `@@INDEXES@@` to null on the model when creating - added `doAuth('members:emails')` - creates an `email_batch` record attached to the first email in the fixtures - creates an `email_recipients` record for each member - runs analytics aggregation so the email and member counts are as expected - added acceptance test for `/member/:id/?include=email_recipients` --- core/server/models/base/index.js | 8 +- test/api-acceptance/admin/members_spec.js | 21 +++- test/api-acceptance/admin/utils.js | 3 + test/utils/fixtures/data-generator.js | 115 +++++++++++++++++++++- test/utils/index.js | 25 +++++ 5 files changed, 163 insertions(+), 9 deletions(-) diff --git a/core/server/models/base/index.js b/core/server/models/base/index.js index 852e1ad097..7949696c52 100644 --- a/core/server/models/base/index.js +++ b/core/server/models/base/index.js @@ -172,13 +172,15 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ // Ghost option handling - get permitted attributes from server/data/schema.js, where the DB schema is defined permittedAttributes: function permittedAttributes() { - return _.keys(schema.tables[this.tableName]); + return _.keys(schema.tables[this.tableName]) + .filter(key => key.indexOf('@@') === -1); }, // Ghost ordering handling, allows to order by permitted attributes by default and can be overriden on specific model level orderAttributes: function orderAttributes() { return Object.keys(schema.tables[this.tableName]) - .map(key => `${this.tableName}.${key}`); + .map(key => `${this.tableName}.${key}`) + .filter(key => key.indexOf('@@') === -1); }, // When loading an instance, subclasses can specify default to fetch @@ -354,7 +356,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ * * Happens after validation to ensure we don't set fields which are not nullable on db level. */ - _.each(Object.keys(schema.tables[this.tableName]), (columnKey) => { + _.each(Object.keys(schema.tables[this.tableName]).filter(key => key.indexOf('@@') === -1), (columnKey) => { if (model.get(columnKey) === undefined) { model.set(columnKey, null); } diff --git a/test/api-acceptance/admin/members_spec.js b/test/api-acceptance/admin/members_spec.js index 14dca51d8f..8e4da9fa06 100644 --- a/test/api-acceptance/admin/members_spec.js +++ b/test/api-acceptance/admin/members_spec.js @@ -20,7 +20,7 @@ describe('Members API', function () { before(async function () { await testUtils.startGhost(); request = supertest.agent(config.get('url')); - await localUtils.doAuth(request, 'members'); + await localUtils.doAuth(request, 'members', 'members:emails'); sinon.stub(labs, 'isSet').withArgs('members').returns(true); }); @@ -123,6 +123,25 @@ describe('Members API', function () { localUtils.API.checkResponse(jsonResponse.members[0], 'member', 'stripe'); }); + it('Can read and include email_recipients', async function () { + const res = await request + .get(localUtils.API.getApiQuery(`members/${testUtils.DataGenerator.Content.members[0].id}/?include=email_recipients`)) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200); + + should.not.exist(res.headers['x-cache-invalidate']); + const jsonResponse = res.body; + should.exist(jsonResponse); + should.exist(jsonResponse.members); + jsonResponse.members.should.have.length(1); + localUtils.API.checkResponse(jsonResponse.members[0], 'member', ['stripe', 'email_recipients']); + jsonResponse.members[0].email_recipients.length.should.equal(1); + localUtils.API.checkResponse(jsonResponse.members[0].email_recipients[0], 'email_recipient', ['email']); + localUtils.API.checkResponse(jsonResponse.members[0].email_recipients[0].email, 'email'); + }); + it('Can add', async function () { const member = { name: 'test', diff --git a/test/api-acceptance/admin/utils.js b/test/api-acceptance/admin/utils.js index ff516397d9..4e16878caa 100644 --- a/test/api-acceptance/admin/utils.js +++ b/test/api-acceptance/admin/utils.js @@ -115,6 +115,9 @@ const expectedProperties = { email: _(schema.emails) .keys(), email_preview: ['html', 'subject', 'plaintext'], + email_recipient: _(schema.email_recipients) + .keys() + .filter(key => key.indexOf('@@') === -1), snippet: _(schema.snippets).keys() }; diff --git a/test/utils/fixtures/data-generator.js b/test/utils/fixtures/data-generator.js index f1e06c8beb..3458d99b59 100644 --- a/test/utils/fixtures/data-generator.js +++ b/test/utils/fixtures/data-generator.js @@ -310,23 +310,27 @@ DataGenerator.Content = { { id: ObjectId.generate(), email: 'member1@test.com', - name: 'Mr Egg' + name: 'Mr Egg', + uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b340' }, { id: ObjectId.generate(), email: 'member2@test.com', - email_open_rate: 50 + email_open_rate: 50, + uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b341' }, { id: ObjectId.generate(), email: 'paid@test.com', name: 'Egon Spengler', - email_open_rate: 80 + email_open_rate: 80, + uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b342' }, { id: ObjectId.generate(), email: 'trialing@test.com', - name: 'Ray Stantz' + name: 'Ray Stantz', + uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b343' } ], @@ -446,9 +450,11 @@ DataGenerator.Content = { uuid: '6b6afda6-4b5e-4893-bff6-f16859e8349a', status: 'submitted', email_count: 2, + recipient_filter: 'all', subject: 'You got mailed!', html: '

Look! I\'m an email

', plaintext: 'Waba-daba-dab-da', + track_opens: false, submitted_at: moment().toDate() }, { @@ -456,15 +462,72 @@ DataGenerator.Content = { uuid: '365daa11-4bf0-4614-ad43-6346387ffa00', status: 'failed', error: 'Everything went south', - stats: '', email_count: 3, subject: 'You got mailed! Again!', html: '

What\'s that? Another email!

', plaintext: 'yes this is an email', + track_opens: false, submitted_at: moment().toDate() } ], + email_batches: [ + { + id: ObjectId.generate(), + email_id: null, // emails[0] relation added later + // TODO: cleanup <> in provider_id + provider_id: '', + status: 'submitted' + } + ], + + email_recipients: [ + { + id: ObjectId.generate(), + email_id: null, // emails[0] relation added later + member_id: null, // members[0] relation added later + batch_id: null, // email_batches[0] relation added later + processed_at: moment().toDate(), + failed_at: null, + member_uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b340', + member_email: 'member1@test.com', + member_name: 'Mr Egg' + }, + { + id: ObjectId.generate(), + email_id: null, // emails[0] relation added later + member_id: null, // members[1] relation added later + batch_id: null, // email_batches[0] relation added later + processed_at: moment().toDate(), + failed_at: null, + member_uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b341', + member_email: 'member2@test.com', + member_name: null + }, + { + id: ObjectId.generate(), + email_id: null, // emails[0] relation added later + member_id: null, // members[2] relation added later + batch_id: null, // email_batches[0] relation added later + processed_at: moment().toDate(), + failed_at: null, + member_uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b342', + member_email: 'member1@test.com', + member_name: 'Mr Egg' + }, + { + id: ObjectId.generate(), + email_id: null, // emails[0] relation added later + member_id: null, // members[3] relation added later + batch_id: null, // email_batches[0] relation added later + processed_at: moment().toDate(), + failed_at: null, + member_uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b343', + member_email: 'member1@test.com', + member_name: 'Mr Egg' + } + ], + snippets: [ { id: ObjectId.generate(), @@ -480,6 +543,19 @@ DataGenerator.Content.api_keys[0].integration_id = DataGenerator.Content.integra DataGenerator.Content.api_keys[1].integration_id = DataGenerator.Content.integrations[0].id; DataGenerator.Content.emails[0].post_id = DataGenerator.Content.posts[0].id; DataGenerator.Content.emails[1].post_id = DataGenerator.Content.posts[1].id; +DataGenerator.Content.email_batches[0].email_id = DataGenerator.Content.emails[0].id; +DataGenerator.Content.email_recipients[0].batch_id = DataGenerator.Content.email_batches[0].id; +DataGenerator.Content.email_recipients[0].email_id = DataGenerator.Content.email_batches[0].email_id; +DataGenerator.Content.email_recipients[0].member_id = DataGenerator.Content.members[0].id; +DataGenerator.Content.email_recipients[1].batch_id = DataGenerator.Content.email_batches[0].id; +DataGenerator.Content.email_recipients[1].email_id = DataGenerator.Content.email_batches[0].email_id; +DataGenerator.Content.email_recipients[1].member_id = DataGenerator.Content.members[1].id; +DataGenerator.Content.email_recipients[2].batch_id = DataGenerator.Content.email_batches[0].id; +DataGenerator.Content.email_recipients[2].email_id = DataGenerator.Content.email_batches[0].email_id; +DataGenerator.Content.email_recipients[2].member_id = DataGenerator.Content.members[2].id; +DataGenerator.Content.email_recipients[3].batch_id = DataGenerator.Content.email_batches[0].id; +DataGenerator.Content.email_recipients[3].email_id = DataGenerator.Content.email_batches[0].email_id; +DataGenerator.Content.email_recipients[3].member_id = DataGenerator.Content.members[3].id; DataGenerator.Content.members_stripe_customers[0].member_id = DataGenerator.Content.members[2].id; DataGenerator.Content.members_stripe_customers[1].member_id = DataGenerator.Content.members[3].id; @@ -758,6 +834,22 @@ DataGenerator.forKnex = (function () { }); } + function createEmailBatch(overrides) { + const newObj = _.cloneDeep(overrides); + return _.defaults(newObj, { + id: ObjectId.generate(), + created_at: new Date(), + updated_at: new Date() + }); + } + + function createEmailRecipient(overrides) { + const newObj = _.cloneDeep(overrides); + return _.defaults(newObj, { + id: ObjectId.generate() + }); + } + const posts = [ createPost(DataGenerator.Content.posts[0]), createPost(DataGenerator.Content.posts[1]), @@ -965,6 +1057,17 @@ DataGenerator.forKnex = (function () { createEmail(DataGenerator.Content.emails[1]) ]; + const email_batches = [ + createEmailBatch(DataGenerator.Content.email_batches[0]) + ]; + + const email_recipients = [ + createEmailRecipient(DataGenerator.Content.email_recipients[0]), + createEmailRecipient(DataGenerator.Content.email_recipients[1]), + createEmailRecipient(DataGenerator.Content.email_recipients[2]), + createEmailRecipient(DataGenerator.Content.email_recipients[3]) + ]; + const members = [ createMember(DataGenerator.Content.members[0]), createMember(DataGenerator.Content.members[1]), @@ -1035,6 +1138,8 @@ DataGenerator.forKnex = (function () { integrations, api_keys, emails, + email_batches, + email_recipients, labels, members, members_labels, diff --git a/test/utils/index.js b/test/utils/index.js index 6d22f691ef..257e174731 100644 --- a/test/utils/index.js +++ b/test/utils/index.js @@ -22,6 +22,7 @@ const routingService = require('../../core/frontend/services/routing'); const settingsService = require('../../core/server/services/settings'); const frontendSettingsService = require('../../core/frontend/services/settings'); const settingsCache = require('../../core/server/services/settings/cache'); +const emailAnalyticsService = require('../../core/server/services/email-analytics'); const imageLib = require('../../core/server/lib/image'); const web = require('../../core/server/web'); const permissions = require('../../core/server/services/permissions'); @@ -501,6 +502,27 @@ fixtures = { }); }, + insertEmailsAndRecipients: function insertEmailsAndRecipients() { + return Promise.each(_.cloneDeep(DataGenerator.forKnex.emails), function (email) { + return models.Email.add(email, module.exports.context.internal); + }).then(function () { + return Promise.each(_.cloneDeep(DataGenerator.forKnex.email_batches), function (emailBatch) { + return models.EmailBatch.add(emailBatch, module.exports.context.internal); + }); + }).then(function () { + return Promise.each(_.cloneDeep(DataGenerator.forKnex.email_recipients), (emailRecipient) => { + return models.EmailRecipient.add(emailRecipient, module.exports.context.internal); + }); + }).then(function () { + const toAggregate = { + emailIds: DataGenerator.forKnex.emails.map(email => email.id), + memberIds: DataGenerator.forKnex.members.map(member => member.id) + }; + + return emailAnalyticsService.aggregateStats(toAggregate); + }); + }, + insertSnippets: function insertSnippets() { return Promise.map(DataGenerator.forKnex.snippets, function (snippet) { return models.Snippet.add(snippet, module.exports.context.internal); @@ -576,6 +598,9 @@ toDoList = { members: function insertMembersAndLabels() { return fixtures.insertMembersAndLabels(); }, + 'members:emails': function insertEmailsAndRecipients() { + return fixtures.insertEmailsAndRecipients(); + }, posts: function insertPostsAndTags() { return fixtures.insertPostsAndTags(); },