Ghost/ghost/core/test/e2e-api/admin/newsletters.test.js
Simon Backx 17ec1e8937
Added email address alignment protections (#19094)
ref GRO-54
fixes GRO-63
fixes GRO-62
fixes GRO-69

When the config `hostSettings:managedEmail:enabled` is enabled, or the
new flag (`newEmailAddresses`) is enabled for self-hosters, we'll start
to check the from addresses of all outgoing emails more strictly.

- Current flow: nothing changes if the managedEmail config is not set or
the `newEmailAddresses` feature flag is not set
- When managedEmail is enabled: never allow to send an email from any
chosen email. We always use `mail.from` for all outgoing emails. Custom
addresses should be set as replyTo instead. Changing the newsletter
sender_email is not allowed anymore (and ignored if it is set).
- When managedEmail is enabled with a custom sending domain: if a from
address doesn't match the sending domain, we'll default to mail.from and
use the original as a replyTo if appropriate and only when no other
replyTo was set. A newsletter sender email addresss can only be set to
an email address on this domain.
- When `newEmailAddresses` is enabled: self hosters are free to set all
email addresses to whatever they want, without verification. In addition
to that, we stop making up our own email addresses and send from
`mail.from` by default instead of generating a `noreply`+ `@` +
`sitedomain.com` address

A more in depth example of all cases can be seen in
`ghost/core/test/integration/services/email-addresses.test.js`

Includes lots of new E2E tests for most new situations. Apart from that,
all email snapshots are changed because the from and replyTo addresses
are now included in snapshots (so we can see unexpected changes in the
future).

Dropped test coverage requirement, because tests were failing coverage
locally, but not in CI

Fixed settings test that set the site title to an array - bug tracked in
GRO-68
2023-11-23 10:25:30 +01:00

1550 lines
55 KiB
JavaScript

const assert = require('assert/strict');
const sinon = require('sinon');
const {agentProvider, mockManager, fixtureManager, configUtils, dbUtils, matchers, regexes} = require('../../utils/e2e-framework');
const {anyContentVersion, anyEtag, anyObjectId, anyUuid, anyErrorId, anyISODateTime, anyLocationFor, anyNumber} = matchers;
const {queryStringToken} = regexes;
const models = require('../../../core/server/models');
const logging = require('@tryghost/logging');
const {mockLabsDisabled, mockLabsEnabled} = require('../../utils/e2e-framework-mock-manager');
const settingsHelpers = require('../../../core/server/services/settings-helpers');
const assertMemberRelationCount = async (newsletterId, expectedCount) => {
const relations = await dbUtils.knex('members_newsletters').where({newsletter_id: newsletterId}).pluck('id');
assert.equal(relations.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: anyNumber
};
describe('Newsletters API', function () {
let agent;
let emailMockReceiver;
before(async function () {
agent = await agentProvider.getAdminAPIAgent();
await fixtureManager.init('newsletters', 'members:newsletters');
await agent.loginAsOwner();
});
beforeEach(function () {
emailMockReceiver = mockManager.mockMail();
mockLabsDisabled('newEmailAddresses');
});
afterEach(function () {
mockManager.restore();
sinon.restore();
});
it('Can browse newsletters', async function () {
await agent.get('newsletters/')
.expectStatus(200)
.matchBodySnapshot({
newsletters: new Array(4).fill(newsletterSnapshot)
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can read a newsletter', async function () {
await agent
.get(`newsletters/${fixtureManager.get('newsletters', 0).id}/`)
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can include members, active members & posts counts when browsing newsletters', async function () {
await agent
.get(`newsletters/?include=count.members,count.active_members,count.posts`)
.expectStatus(200)
.matchBodySnapshot({
newsletters: new Array(4).fill(newsletterSnapshot)
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can include members, active members & posts counts when reading a newsletter', async function () {
await agent
.get(`newsletters/${fixtureManager.get('newsletters', 0).id}/?include=count.members,count.active_members,count.posts`)
.expectStatus(200)
.matchBodySnapshot({
newsletters: new Array(1).fill(newsletterSnapshot)
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can add a newsletter', async function () {
const siteUrl = configUtils.config.getSiteUrl();
const relativePath = 'content/images/2022/05/cover-image.jpg';
const absolutePath = siteUrl + relativePath;
const transformReadyPath = '__GHOST_URL__/' + relativePath;
const newsletter = {
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
assert.equal(body.newsletters[0].header_image, absolutePath);
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
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 dbUtils.knex('newsletters').where('id', id).pluck('header_image');
assert.equal(header_image, transformReadyPath);
});
it('Can include members, active members & posts counts when adding a newsletter', async function () {
const newsletter = {
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.active_members,count.posts`)
.body({newsletters: [newsletter]})
.expectStatus(201)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
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({
'content-version': anyContentVersion,
etag: anyEtag,
location: anyLocationFor('newsletters')
});
await agent
.post(`newsletters/`)
.body({newsletters: [secondNewsletter]})
.expectStatus(201)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag,
location: anyLocationFor('newsletters')
});
});
it('Can add a newsletter - with custom sender_email', async function () {
const newsletter = {
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({
'content-version': anyContentVersion,
etag: anyEtag,
location: anyLocationFor('newsletters')
});
emailMockReceiver
.assertSentEmailCount(1)
.matchMetadataSnapshot()
.matchHTMLSnapshot([{
pattern: queryStringToken('verifyEmail'),
replacement: 'verifyEmail=REPLACED_TOKEN'
}])
.matchPlaintextSnapshot([{
pattern: queryStringToken('verifyEmail'),
replacement: 'verifyEmail=REPLACED_TOKEN'
}]);
});
it('Can add a newsletter - and subscribe existing members', async function () {
const newsletter = {
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({
'content-version': anyContentVersion,
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({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can include members, active members & posts counts when editing newsletters', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}/?include=count.members,count.active_members,count.posts`)
.body({
newsletters: [{
name: 'Updated newsletter name 2'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
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({
'content-version': anyContentVersion,
etag: anyEtag
});
emailMockReceiver
.assertSentEmailCount(1)
.matchMetadataSnapshot()
.matchHTMLSnapshot([{
pattern: queryStringToken('verifyEmail'),
replacement: 'verifyEmail=REPLACED_TOKEN'
}])
.matchPlaintextSnapshot([{
pattern: queryStringToken('verifyEmail'),
replacement: 'verifyEmail=REPLACED_TOKEN'
}]);
});
it('[Legacy] Can only set newsletter reply to to newsletter or support value', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'support'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'newsletter'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('[Legacy] Cannot set newsletter clear sender_reply_to', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: ''
}]
})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('[Legacy] Cannot set newsletter reply-to to any email address', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'hello@acme.com'
}]
})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('[Legacy] Cannot set newsletter sender_email to invalid email address', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: 'notvalid'
}]
})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
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);
// @NOTE: need a way to return snapshot of sent email from email mock receiver
const mail = mockManager.assert.sentEmail([]);
emailMockReceiver
.assertSentEmailCount(1)
.matchMetadataSnapshot()
.matchHTMLSnapshot([{
pattern: queryStringToken('verifyEmail'),
replacement: 'verifyEmail=REPLACED_TOKEN'
}])
.matchPlaintextSnapshot([{
pattern: queryStringToken('verifyEmail'),
replacement: 'verifyEmail=REPLACED_TOKEN'
}]);
const $mailHtml = cheerio.load(mail.html);
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 () {
after(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'
};
sinon.stub(logging, 'error');
await agent
.post(`newsletters/?opt_in_existing=true`)
.body({newsletters: [newsletter]})
.expectStatus(403)
.matchBodySnapshot({
errors: [{
id: anyUuid
}]
});
});
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'
};
sinon.stub(logging, 'error');
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'
};
sinon.stub(logging, 'error');
// 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({
'content-version': anyContentVersion,
etag: anyEtag,
location: anyLocationFor('newsletters')
});
});
it('Editing an active 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');
assert.ok(activeNewsletter, 'This test expects to have an active newsletter in the test fixtures');
const id = activeNewsletter.id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
name: 'Updated active newsletter name'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
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({
'content-version': anyContentVersion,
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');
sinon.stub(logging, 'error');
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({
'content-version': anyContentVersion,
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({
'content-version': anyContentVersion,
etag: anyEtag,
location: anyLocationFor('newsletters')
});
});
});
});
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({
'content-version': anyContentVersion,
etag: anyEtag,
location: anyLocationFor('newsletters')
});
sinon.stub(logging, 'error');
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({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can add a newsletter - with custom sender_email and subscribe existing members', async function () {
if (dbUtils.isSQLite()) {
// 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({
'content-version': anyContentVersion,
etag: anyEtag,
location: anyLocationFor('newsletters')
});
emailMockReceiver
.assertSentEmailCount(1)
.matchMetadataSnapshot()
.matchHTMLSnapshot([{
pattern: queryStringToken('verifyEmail'),
replacement: 'verifyEmail=REPLACED_TOKEN'
}])
.matchPlaintextSnapshot([{
pattern: queryStringToken('verifyEmail'),
replacement: 'verifyEmail=REPLACED_TOKEN'
}]);
});
it(`Can't edit multiple newsletters to existing name`, async function () {
const id = fixtureManager.get('newsletters', 0).id;
sinon.stub(logging, 'error');
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({
'content-version': anyContentVersion,
etag: anyEtag
});
});
describe('Managed email without custom sending domain', function () {
this.beforeEach(function () {
configUtils.set('hostSettings:managedEmail:enabled', true);
configUtils.set('hostSettings:managedEmail:sendingDomain', null);
});
it('Can set newsletter reply-to to newsletter or support', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'support'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'newsletter'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Cannot clear newsletter reply-to', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: ''
}]
})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Cannot set newsletter reply-to to invalid email address', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'notvalid'
}]
})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can set newsletter reply-to to any email address with required verification', async function () {
const id = fixtureManager.get('newsletters', 0).id;
const before = await models.Newsletter.findOne({id});
const beforeSenderReplyTo = before.get('sender_reply_to');
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'hello@acme.com'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot],
meta: {
sent_email_verification: ['sender_reply_to']
}
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
await before.refresh();
assert.equal(before.get('sender_reply_to'), beforeSenderReplyTo, 'sender_reply_to should not have changed because it first requires verification');
emailMockReceiver
.assertSentEmailCount(1)
.matchMetadataSnapshot()
.matchHTMLSnapshot([{
pattern: queryStringToken('verifyEmail'),
replacement: 'verifyEmail=REPLACED_TOKEN'
}])
.matchPlaintextSnapshot([{
pattern: queryStringToken('verifyEmail'),
replacement: 'verifyEmail=REPLACED_TOKEN'
}]);
});
it('Cannot change sender_email', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: 'hello@acme.com'
}]
})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Cannot set newsletter sender_email to invalid email address', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: 'notvalid'
}]
})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can keep sender_email', async function () {
const id = fixtureManager.get('newsletters', 0).id;
const before = await models.Newsletter.findOne({id});
assert(before.get('sender_email'), 'This test requires a non empty sender_email');
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: before.get('sender_email')
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can set sender_email to default address', async function () {
const id = fixtureManager.get('newsletters', 0).id;
const before = await models.Newsletter.findOne({id});
assert(before.get('sender_email'), 'This test requires a non empty sender_email');
const defaultAddress = settingsHelpers.getDefaultEmail().address;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: defaultAddress
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can clear sender_email', async function () {
const id = fixtureManager.get('newsletters', 0).id;
const before = await models.Newsletter.findOne({id});
const beforeEmail = before.get('sender_email');
assert(before.get('sender_email'), 'This test requires a non empty sender_email');
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: ''
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
// Revert back
await before.refresh();
before.set('sender_email', beforeEmail);
await before.save();
});
});
describe('Managed email with custom sending domain', function () {
this.beforeEach(function () {
configUtils.set('hostSettings:managedEmail:enabled', true);
configUtils.set('hostSettings:managedEmail:sendingDomain', 'sendingdomain.com');
});
it('Can set newsletter reply-to to newsletter or support', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'support'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'newsletter'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Cannot clear newsletter reply-to', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: ''
}]
})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Cannot set newsletter reply-to to invalid email address', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'notvalid'
}]
})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can set newsletter reply-to to any email address with required verification', async function () {
const id = fixtureManager.get('newsletters', 0).id;
const before = await models.Newsletter.findOne({id});
const beforeSenderReplyTo = before.get('sender_reply_to');
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'hello@acme.com'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot],
meta: {
sent_email_verification: ['sender_reply_to']
}
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
await before.refresh();
assert.equal(before.get('sender_reply_to'), beforeSenderReplyTo, 'sender_reply_to should not have changed because it first requires verification');
emailMockReceiver
.assertSentEmailCount(1)
.matchMetadataSnapshot()
.matchHTMLSnapshot([{
pattern: queryStringToken('verifyEmail'),
replacement: 'verifyEmail=REPLACED_TOKEN'
}])
.matchPlaintextSnapshot([{
pattern: queryStringToken('verifyEmail'),
replacement: 'verifyEmail=REPLACED_TOKEN'
}]);
});
it('Can set newsletter reply-to to matching sending domain without required verification', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'anything@sendingdomain.com'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
const before = await models.Newsletter.findOne({id});
assert.equal(before.get('sender_reply_to'), 'anything@sendingdomain.com');
emailMockReceiver
.assertSentEmailCount(0);
});
it('Cannot change sender_email to non matching domain', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: 'hello@acme.com'
}]
})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Cannot set newsletter sender_email to invalid email address', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: 'notvalid'
}]
})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can keep sender_email', async function () {
const id = fixtureManager.get('newsletters', 0).id;
const before = await models.Newsletter.findOne({id});
assert(before.get('sender_email'), 'This test requires a non empty sender_email');
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: before.get('sender_email')
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can set sender_email to address matching sending domain, without verification', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: 'anything@sendingdomain.com'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
const before = await models.Newsletter.findOne({id});
assert.equal(before.get('sender_email'), 'anything@sendingdomain.com');
emailMockReceiver
.assertSentEmailCount(0);
});
it('Can clear sender_email', async function () {
const id = fixtureManager.get('newsletters', 0).id;
const before = await models.Newsletter.findOne({id});
const beforeEmail = before.get('sender_email');
assert(before.get('sender_email'), 'This test requires a non empty sender_email');
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: ''
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
// Revert back
await before.refresh();
before.set('sender_email', beforeEmail);
await before.save();
});
});
describe('Self hoster without managed email', function () {
this.beforeEach(function () {
configUtils.set('hostSettings:managedEmail:enabled', false);
configUtils.set('hostSettings:managedEmail:sendingDomain', '');
mockLabsEnabled('newEmailAddresses');
});
it('Can set newsletter reply-to to newsletter or support', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'support'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'newsletter'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Cannot clear newsletter reply-to', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: ''
}]
})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Cannot set newsletter reply-to to invalid email address', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'notvalid'
}]
})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can set newsletter reply-to to any email address without required verification', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'hello@acme.com'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
const before = await models.Newsletter.findOne({id});
assert.equal(before.get('sender_reply_to'), 'hello@acme.com');
emailMockReceiver
.assertSentEmailCount(0);
});
it('Can change sender_email to any address without verification', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: 'hello@acme.com'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
const before = await models.Newsletter.findOne({id});
assert.equal(before.get('sender_email'), 'hello@acme.com');
emailMockReceiver
.assertSentEmailCount(0);
});
it('Cannot set newsletter sender_email to invalid email address', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: 'notvalid'
}]
})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can clear sender_email', async function () {
const id = fixtureManager.get('newsletters', 0).id;
const before = await models.Newsletter.findOne({id});
const beforeEmail = before.get('sender_email');
assert(before.get('sender_email'), 'This test requires a non empty sender_email');
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: ''
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
// Revert back
await before.refresh();
before.set('sender_email', beforeEmail);
await before.save();
});
});
});