Ghost/test/e2e-api/admin/newsletters.test.js
Simon Backx 38b9cf2472
Fixed newsletter includes when adding or editing (#14696)
refs https://github.com/TryGhost/Team/issues/1571
refs https://ghost.slack.com/archives/C02G9E68C/p1650986988322609

- Makes sure the includes are always included
- Moved read to the newsletter service
- Added tests
- Updated unit tests to work with multiple findOne calls
- Fixed reject assertions not correctly awaiting in unit tests
2022-05-05 11:20:15 +02:00

507 lines
16 KiB
JavaScript

const DatabaseInfo = require('@tryghost/database-info');
const {any} = require('@tryghost/express-test').snapshot;
const {agentProvider, mockManager, fixtureManager, matchers} = require('../../utils/e2e-framework');
const {anyEtag, anyObjectId, anyUuid, anyISODateTime, anyLocationFor} = matchers;
const configUtils = require('../../utils/configUtils');
const uuid = require('uuid');
const urlUtils = require('../../../core/shared/url-utils');
const db = require('../../../core/server/data/db');
const knex = db.knex;
require('should');
const assert = require('assert');
const models = require('../../../core/server/models');
const assertMemberRelationCount = async (newsletterId, expectedCount) => {
const newsletter = await models.Newsletter.findOne({id: newsletterId}, {withRelated: 'members'});
assert.equal(newsletter.related('members').length, expectedCount);
};
const newsletterSnapshot = {
id: anyObjectId,
uuid: anyUuid,
created_at: anyISODateTime,
updated_at: anyISODateTime
};
const newsletterSnapshotWithoutSortOrder = {
id: anyObjectId,
uuid: anyUuid,
created_at: anyISODateTime,
updated_at: anyISODateTime,
sort_order: any(Number)
};
let agent;
describe('Newsletters API', function () {
let mailMocks;
before(async function () {
agent = await agentProvider.getAdminAPIAgent();
await fixtureManager.init('newsletters', 'members:newsletters');
await agent.loginAsOwner();
});
beforeEach(function () {
mailMocks = mockManager.mockMail();
});
afterEach(function () {
mockManager.restore();
});
it('Can browse newsletters', async function () {
await agent.get('newsletters/')
.expectStatus(200)
.matchBodySnapshot({
newsletters: new Array(4).fill(newsletterSnapshot)
})
.matchHeaderSnapshot({
etag: anyEtag
});
});
it('Can read a newsletter', async function () {
await agent
.get(`newsletters/${fixtureManager.get('newsletters', 0).id}/`)
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
etag: anyEtag
});
});
it('Can include members & posts counts when browsing newsletters', async function () {
await agent
.get(`newsletters/?include=count.members,count.posts`)
.expectStatus(200)
.matchBodySnapshot({
newsletters: new Array(4).fill(newsletterSnapshot)
})
.matchHeaderSnapshot({
etag: anyEtag
});
});
it('Can include members & posts counts when reading a newsletter', async function () {
await agent
.get(`newsletters/${fixtureManager.get('newsletters', 0).id}/?include=count.members,count.posts`)
.expectStatus(200)
.matchBodySnapshot({
newsletters: new Array(1).fill(newsletterSnapshot)
})
.matchHeaderSnapshot({
etag: anyEtag
});
});
it('Can add a newsletter', async function () {
const siteUrl = urlUtils.getSiteUrl();
const relativePath = 'content/images/2022/05/cover-image.jpg';
const absolutePath = siteUrl + relativePath;
const transformReadyPath = '__GHOST_URL__/' + relativePath;
const newsletter = {
uuid: uuid.v4(),
name: 'My test newsletter',
sender_name: 'Test',
sender_email: null,
sender_reply_to: 'newsletter',
status: 'active',
subscribe_on_signup: true,
title_font_category: 'serif',
body_font_category: 'serif',
show_header_icon: true,
show_header_title: true,
show_badge: true,
sort_order: 0,
header_image: absolutePath
};
const {body: body2} = await agent
.post(`newsletters/`)
.body({newsletters: [newsletter]})
.expectStatus(201)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.expect(({body}) => {
// Should still be absolute
body.newsletters[0].header_image.should.equal(absolutePath);
})
.matchHeaderSnapshot({
etag: anyEtag,
location: anyLocationFor('newsletters')
});
const id = body2.newsletters[0].id;
// Check with a database query if the header_image is saved correctly with a 'transformReady' path
const [header_image] = await knex('newsletters').where('id', id).pluck('header_image');
header_image.should.equal(transformReadyPath);
});
it('Can include members & posts counts when adding a newsletter', async function () {
const newsletter = {
uuid: uuid.v4(),
name: 'My test newsletter 2',
sender_name: 'Test',
sender_email: null,
sender_reply_to: 'newsletter',
status: 'active',
subscribe_on_signup: true,
title_font_category: 'serif',
body_font_category: 'serif',
show_header_icon: true,
show_header_title: true,
show_badge: true,
sort_order: 0
};
await agent
.post(`newsletters/?include=count.members,count.posts`)
.body({newsletters: [newsletter]})
.expectStatus(201)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
etag: anyEtag,
location: anyLocationFor('newsletters')
});
});
it('Can add multiple newsletters', async function () {
const firstNewsletter = {
name: 'My first test newsletter'
};
const secondNewsletter = {
name: 'My second test newsletter'
};
await agent
.post(`newsletters/`)
.body({newsletters: [firstNewsletter]})
.expectStatus(201)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
etag: anyEtag,
location: anyLocationFor('newsletters')
});
await agent
.post(`newsletters/`)
.body({newsletters: [secondNewsletter]})
.expectStatus(201)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
etag: anyEtag,
location: anyLocationFor('newsletters')
});
});
it('Can add a newsletter - with custom sender_email', async function () {
const newsletter = {
uuid: uuid.v4(),
name: 'My test newsletter with custom sender_email',
sender_name: 'Test',
sender_email: 'test@example.com',
sender_reply_to: 'newsletter',
status: 'active',
subscribe_on_signup: true,
title_font_category: 'serif',
body_font_category: 'serif',
show_header_icon: true,
show_header_title: true,
show_badge: true,
sort_order: 0
};
await agent
.post(`newsletters/`)
.body({newsletters: [newsletter]})
.expectStatus(201)
.matchBodySnapshot({
newsletters: [newsletterSnapshot],
meta: {
sent_email_verification: ['sender_email']
}
})
.matchHeaderSnapshot({
etag: anyEtag,
location: anyLocationFor('newsletters')
});
mockManager.assert.sentEmail({
subject: 'Verify email address',
to: 'test@example.com'
});
});
it('Can add a newsletter - and subscribe existing members', async function () {
const newsletter = {
uuid: uuid.v4(),
name: 'New newsletter with existing members subscribed',
sender_name: 'Test',
sender_email: null,
sender_reply_to: 'newsletter',
status: 'active',
subscribe_on_signup: true,
title_font_category: 'serif',
body_font_category: 'serif',
show_header_icon: true,
show_header_title: true,
show_badge: true,
sort_order: 0
};
const {body} = await agent
.post(`newsletters/?opt_in_existing=true`)
.body({newsletters: [newsletter]})
.expectStatus(201)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
etag: anyEtag,
location: anyLocationFor('newsletters')
});
// Assert that the newsletter has 6 related members in the DB
await assertMemberRelationCount(body.newsletters[0].id, 6);
});
it('Can edit newsletters', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
name: 'Updated newsletter name'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
etag: anyEtag
});
});
it('Can include members & posts counts when editing newsletters', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}/?include=count.members,count.posts`)
.body({
newsletters: [{
name: 'Updated newsletter name 2'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
etag: anyEtag
});
});
it('Can edit a newsletters and update the sender_email when already set', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
name: 'Updated newsletter name',
sender_email: 'updated@example.com'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot],
meta: {
sent_email_verification: ['sender_email']
}
})
.matchHeaderSnapshot({
etag: anyEtag
});
mockManager.assert.sentEmail({
subject: 'Verify email address',
to: 'updated@example.com'
});
});
it('Can verify property updates', async function () {
const cheerio = require('cheerio');
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
name: 'Updated newsletter name',
sender_email: 'verify@example.com'
}]
})
.expectStatus(200);
const mailHtml = mailMocks.getCall(0).args[0].html;
const $mailHtml = cheerio.load(mailHtml);
const verifyUrl = new URL($mailHtml('[data-test-verify-link]').attr('href'));
// convert Admin URL hash to native URL for easier token param extraction
const token = (new URL(verifyUrl.hash.replace('#', ''), 'http://example.com')).searchParams.get('verifyEmail');
await agent.put(`newsletters/verifications`)
.body({
token
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
});
});
describe('Host Settings: newsletter limits', function () {
afterEach(function () {
configUtils.set('hostSettings:limits', undefined);
});
it('Request fails when newsletter limit is in place', async function () {
configUtils.set('hostSettings:limits', {
newsletters: {
disabled: true,
error: 'Nuh uh'
}
});
agent = await agentProvider.getAdminAPIAgent();
await fixtureManager.init('newsletters', 'members:newsletters');
await agent.loginAsOwner();
const newsletter = {
name: 'Naughty newsletter'
};
await agent
.post(`newsletters/?opt_in_existing=true`)
.body({newsletters: [newsletter]})
.expectStatus(403)
.matchBodySnapshot({
errors: [{
id: anyUuid
}]
});
});
});
it(`Can't add multiple newsletters with same name`, async function () {
const firstNewsletter = {
name: 'Duplicate newsletter'
};
const secondNewsletter = {...firstNewsletter};
await agent
.post(`newsletters/`)
.body({newsletters: [firstNewsletter]})
.expectStatus(201)
.matchBodySnapshot({
newsletters: [newsletterSnapshotWithoutSortOrder]
})
.matchHeaderSnapshot({
etag: anyEtag,
location: anyLocationFor('newsletters')
});
await agent
.post(`newsletters/`)
.body({newsletters: [secondNewsletter]})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyUuid,
message: 'Validation error, cannot save newsletter.',
context: 'A newsletter with the same name already exists'
}]
})
.matchHeaderSnapshot({
etag: anyEtag
});
});
it('Can add a newsletter - with custom sender_email and subscribe existing members', async function () {
if (DatabaseInfo.isSQLite(db.knex)) {
// This breaks snapshot tests if you don't update snapshot tests on MySQL + make sure this is the last ADD test
return;
}
const newsletter = {
name: 'My test newsletter with custom sender_email and subscribe existing',
sender_name: 'Test',
sender_email: 'test@example.com',
sender_reply_to: 'newsletter',
status: 'active',
subscribe_on_signup: true,
title_font_category: 'serif',
body_font_category: 'serif',
show_header_icon: true,
show_header_title: true,
show_badge: true,
sort_order: 0
};
await agent
.post(`newsletters/?opt_in_existing=true`)
.body({newsletters: [newsletter]})
.expectStatus(201)
.matchBodySnapshot({
newsletters: [newsletterSnapshot],
meta: {
sent_email_verification: ['sender_email']
}
})
.matchHeaderSnapshot({
etag: anyEtag,
location: anyLocationFor('newsletters')
});
mockManager.assert.sentEmail({
subject: 'Verify email address',
to: 'test@example.com'
});
});
it(`Can't edit multiple newsletters to existing name`, async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
name: 'Duplicate newsletter'
}]
})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyUuid,
message: 'Validation error, cannot edit newsletter.',
context: 'A newsletter with the same name already exists'
}]
})
.matchHeaderSnapshot({
etag: anyEtag
});
});
});