Ghost/ghost/core/test/e2e-api/admin/newsletters.test.js
Simon Backx eb063f7a40
Fixed clearing invalid sender_email when changing newsletter sender_reply_to (#19555)
fixes PROD-102

When a newsletter has a sender_email stored in the database that Ghost
is not allowed to send from, we no longer return it as sender_email in
the API. Instead we return it as the sender_reply_to. That way the
expected behaviour is shown correctly in the frontend and the API result
also makes more sense.

In addition to that, when a change is made to a newsletters reply_to
address we'll clear any invalid sender_email values in that newsletter.
That makes sure we can clear the sender_reply_to value instead of
keeping the current fallback to sender_email if that one is stored.

On top of that, this change correclty updates the browse endpoint to use
the newsletter service instead of directly using the model.
2024-01-23 16:10:11 +01:00

1879 lines
69 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);
};
// Change directly in database, to test edge cases
async function editNewsletter(id, changes) {
await dbUtils.knex('newsletters').where({id}).update(changes);
}
// Get directly from the database
async function getNewsletter(id) {
return (await dbUtils.knex('newsletters').where({id}))[0];
}
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);
configUtils.set('mail:from', 'default@email.com');
});
describe('Auto correcting invalid domains', function () {
const id = fixtureManager.get('newsletters', 0).id;
beforeEach(async function () {
// Invalid situation in the database)
await editNewsletter(id, {
sender_email: 'notvalid@acme.com',
sender_reply_to: 'newsletter'
});
});
after(async function () {
// Reset
await editNewsletter(id, {
sender_email: null,
sender_reply_to: 'newsletter'
});
});
it('Read returns sender_email as sender_reply_to in case we cannot send from sender_email and sender_reply_to is set to newsletter', async function () {
const {body} = await agent.get(`newsletters/${id}`)
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
// Do a manual check to make sure we don't accidentally change snapshots
assert.equal(body.newsletters[0].sender_email, null);
assert.equal(body.newsletters[0].sender_reply_to, 'notvalid@acme.com');
});
it('Browse returns sender_email as sender_reply_to in case we cannot send from sender_email and sender_reply_to is set to newsletter', async function () {
const {body} = await agent.get(`newsletters`)
.expectStatus(200)
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
const newsletter = body.newsletters.find(n => n.id === id);
// Do a manual check to make sure we don't accidentally change snapshots
assert.equal(newsletter.sender_email, null);
assert.equal(newsletter.sender_reply_to, 'notvalid@acme.com');
});
it('Resets sender_email when editing the newsletter reply_to address', async function () {
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'support'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
const newsletter = await getNewsletter(id);
// Do a manual check to make sure we don't accidentally change snapshots
assert.equal(newsletter.sender_email, null);
assert.equal(newsletter.sender_reply_to, 'support');
});
it('Resets sender_email when editing the newsletter reply_to address in combination with resetting sender email', async function () {
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: null,
sender_reply_to: 'something@allowed.com'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
const newsletter = await getNewsletter(id);
// Do a manual check to make sure we don't accidentally change snapshots
assert.equal(newsletter.sender_email, null);
assert.equal(newsletter.sender_reply_to, 'newsletter'); // required validation
});
it('Resets sender_email when editing the newsletter reply_to address in combination with keeping sender email', async function () {
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: 'notvalid@acme.com',
sender_reply_to: 'something@allowed.com'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
const newsletter = await getNewsletter(id);
// Do a manual check to make sure we don't accidentally change snapshots
assert.equal(newsletter.sender_email, null);
assert.equal(newsletter.sender_reply_to, 'newsletter'); // required validation
});
it('Can switch sender_email to sender_reply_to without validation', async function () {
// The frontend will try to do this because it gets the mapped values from the API
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_email: null,
sender_reply_to: 'notvalid@acme.com'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
const newsletter = await getNewsletter(id);
// Do a manual check to make sure we don't accidentally change snapshots
assert.equal(newsletter.sender_email, null);
assert.equal(newsletter.sender_reply_to, 'notvalid@acme.com'); // did not require validation
});
it('Does not reset sender_email when editing the newsletter (not the reply-to address)', async function () {
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
name: 'My changed newsletter name'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
const newsletter = await getNewsletter(id);
assert.equal(newsletter.name, 'My changed newsletter name');
assert.equal(newsletter.sender_email, 'notvalid@acme.com');
assert.equal(newsletter.sender_reply_to, 'newsletter');
});
});
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 the default address without requiring verification', async function () {
const id = fixtureManager.get('newsletters', 0).id;
const before = await models.Newsletter.findOne({id});
const beforeEmail = before.get('sender_reply_to');
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'default@email.com'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
// No verification
emailMockReceiver.assertSentEmailCount(0);
await before.refresh();
assert.equal(before.get('sender_reply_to'), 'default@email.com');
// Revert back
before.set('sender_reply_to', beforeEmail);
await before.save();
});
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;
// Invalid situation in the database)
await editNewsletter(id, {
sender_email: 'existing@acme.com',
sender_reply_to: 'newsletter'
});
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
});
// No verification
emailMockReceiver.assertSentEmailCount(0);
});
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
});
// No verification
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
});
// No verification
emailMockReceiver.assertSentEmailCount(0);
// 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');
});
describe('Auto correcting invalid domains', function () {
const id = fixtureManager.get('newsletters', 0).id;
beforeEach(async function () {
// Invalid situation in the database)
await editNewsletter(id, {
sender_email: 'notvalid@acme.com',
sender_reply_to: 'newsletter'
});
});
after(async function () {
// Reset
await editNewsletter(id, {
sender_email: null,
sender_reply_to: 'newsletter'
});
});
it('Read returns sender_email as sender_reply_to in case we cannot send from sender_email and sender_reply_to is set to newsletter', async function () {
const {body} = await agent.get(`newsletters/${id}`)
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
// Do a manual check to make sure we don't accidentally change snapshots
assert.equal(body.newsletters[0].sender_email, null);
assert.equal(body.newsletters[0].sender_reply_to, 'notvalid@acme.com');
});
it('Browse returns sender_email as sender_reply_to in case we cannot send from sender_email and sender_reply_to is set to newsletter', async function () {
const {body} = await agent.get(`newsletters`)
.expectStatus(200)
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
const newsletter = body.newsletters.find(n => n.id === id);
// Do a manual check to make sure we don't accidentally change snapshots
assert.equal(newsletter.sender_email, null);
assert.equal(newsletter.sender_reply_to, 'notvalid@acme.com');
});
it('Resets sender_email when editing the newsletter reply_to address', async function () {
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
sender_reply_to: 'support'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
const newsletter = await getNewsletter(id);
// Do a manual check to make sure we don't accidentally change snapshots
assert.equal(newsletter.sender_email, null);
assert.equal(newsletter.sender_reply_to, 'support');
});
it('Does not reset sender_email when editing the newsletter (not the reply-to address)', async function () {
await agent.put(`newsletters/${id}`)
.body({
newsletters: [{
name: 'My changed newsletter name'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
const newsletter = await getNewsletter(id);
assert.equal(newsletter.name, 'My changed newsletter name');
assert.equal(newsletter.sender_email, 'notvalid@acme.com');
assert.equal(newsletter.sender_reply_to, 'newsletter');
});
});
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;
// Invalid situation in the database)
await editNewsletter(id, {
sender_email: 'existing@acme.com',
sender_reply_to: 'newsletter'
});
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();
});
});
});