Ghost/ghost/core/test/e2e-api/admin/newsletters.test.js
Simon Backx bac2f4d4d4 Fixed snapshot tests for MySQL only newsletter test
no issue

There was an error when generating the snapshot for this test. It never ran, so the snapshot was never committed. On top of that, the generated snapshot would change every time because the email verification token was not replaced with a static value.
2023-05-03 14:05:53 +02:00

764 lines
28 KiB
JavaScript

const assert = require('assert');
const sinon = require('sinon');
const {agentProvider, mockManager, fixtureManager, configUtils, dbUtils, matchers, regexes} = require('../../utils/e2e-framework');
const {anyContentVersion, anyEtag, anyObjectId, anyUuid, anyISODateTime, anyLocationFor, anyNumber} = matchers;
const {queryStringToken} = regexes;
const models = require('../../../core/server/models');
const logging = require('@tryghost/logging');
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();
});
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 & 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({
'content-version': anyContentVersion,
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({
'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 & 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.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 & 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({
'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('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)
.matchHTMLSnapshot()
.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
});
});
});