const {agentProvider, fixtureManager, matchers, mockManager} = require('../../utils/e2e-framework'); const {nullable, anyContentVersion, anyEtag, anyObjectId, anyUuid, anyISODateTime, anyString} = matchers; const assert = require('assert/strict'); const sinon = require('sinon'); const jobManager = require('../../../core/server/services/jobs/job-service'); const models = require('../../../core/server/models'); const settingsHelpers = require('../../../core/server/services/settings-helpers'); const matchEmail = { id: anyObjectId, uuid: anyUuid, created_at: anyISODateTime, updated_at: anyISODateTime, submitted_at: anyISODateTime }; const matchEmailNewsletter = { ...matchEmail, newsletter_id: anyObjectId }; const matchBatch = { id: anyObjectId, provider_id: anyString, created_at: anyISODateTime, updated_at: anyISODateTime }; const matchFailure = { id: anyObjectId, failed_at: anyISODateTime, event_id: anyString }; describe('Emails API', function () { let agent; before(async function () { agent = await agentProvider.getAdminAPIAgent(); await fixtureManager.init('posts', 'newsletters', 'members', 'members:emails:failed'); await agent.loginAsOwner(); }); beforeEach(function () { mockManager.mockEvents(); mockManager.mockMailgun(); sinon.stub(settingsHelpers, 'getMembersValidationKey').returns('test-validation-key'); }); afterEach(function () { mockManager.restore(); sinon.restore(); }); it('Can browse emails', async function () { await agent .get('emails') .expectStatus(200) .matchBodySnapshot({ emails: new Array(2).fill(matchEmail) }) .matchHeaderSnapshot({ 'content-version': anyContentVersion, etag: anyEtag }); }); it('Can read an email', async function () { await agent .get(`emails/${fixtureManager.get('emails', 0).id}/`) .expectStatus(200) .matchBodySnapshot({ emails: [matchEmail] }) .matchHeaderSnapshot({ 'content-version': anyContentVersion, etag: anyEtag }); }); it('Can retry a failed email', async function () { await agent .put(`emails/${fixtureManager.get('emails', 1).id}/retry`) .expectStatus(200) .matchBodySnapshot({ emails: [matchEmail] }) .matchHeaderSnapshot({ 'content-version': anyContentVersion, etag: anyEtag }); await jobManager.allSettled(); mockManager.assert.emittedEvent('email.edited'); }); it('Can browse email batches', async function () { await agent .get(`emails/${fixtureManager.get('emails', 0).id}/batches/`) .expectStatus(200) .matchBodySnapshot({ batches: [matchBatch] }) .matchHeaderSnapshot({ 'content-version': anyContentVersion, etag: anyEtag }); }); it('Can browse email batches with recipient count', async function () { const {body} = await agent .get(`emails/${fixtureManager.get('emails', 0).id}/batches/?include=count.recipients`) .expectStatus(200) .matchBodySnapshot({ batches: [matchBatch] }) .matchHeaderSnapshot({ 'content-version': anyContentVersion, etag: anyEtag }); assert.equal(body.batches[0].count.recipients, 6); }); it('Can browse all email failures', async function () { await agent .get(`emails/${fixtureManager.get('emails', 0).id}/recipient-failures/?order=failed_at%20DESC`) .expectStatus(200) .matchBodySnapshot({ failures: new Array(5).fill(matchFailure) }) .matchHeaderSnapshot({ 'content-version': anyContentVersion, etag: anyEtag }); }); it('Can browse permanent email failures', async function () { await agent .get(`emails/${fixtureManager.get('emails', 0).id}/recipient-failures/?filter=severity:permanent&order=failed_at%20DESC`) .expectStatus(200) .matchBodySnapshot({ failures: new Array(1).fill(matchFailure) }) .matchHeaderSnapshot({ 'content-version': anyContentVersion, etag: anyEtag }); }); it('Can browse temporary email failures', async function () { await agent .get(`emails/${fixtureManager.get('emails', 0).id}/recipient-failures/?filter=severity:temporary&order=failed_at%20DESC`) .expectStatus(200) .matchBodySnapshot({ failures: new Array(4).fill(matchFailure) }) .matchHeaderSnapshot({ 'content-version': anyContentVersion, etag: anyEtag }); }); it('Can browse email failures with includes', async function () { await agent .get(`emails/${fixtureManager.get('emails', 0).id}/recipient-failures/?order=failed_at%20DESC&include=member,email_recipient`) .expectStatus(200) .matchBodySnapshot({ failures: new Array(5).fill({ ...matchFailure, member: { id: anyObjectId, uuid: anyUuid }, email_recipient: { id: anyObjectId, member_uuid: anyUuid, opened_at: nullable(anyISODateTime), // Can be null or string delivered_at: nullable(anyISODateTime), // Can be null or string failed_at: nullable(anyISODateTime), // Can be null or string processed_at: anyISODateTime, batch_id: anyObjectId } }) }) .matchHeaderSnapshot({ 'content-version': anyContentVersion, etag: anyEtag }); }); // Older Ghost emails still have a html body and plaintext body set. it('Does default replacements on the HTML body of an old email', async function () { const html = '
Hey %%{first_name, "there"}%%, Hey %%{first_name}%%,
Unsubscribe'; const plaintext = 'Hey %%{first_name, "there"}%%, Hey %%{first_name}%%\nUnsubscribe [%%{unsubscribe_url}%%]'; // Create this email model in the database const email = await models.Email.add({ post_id: fixtureManager.get('posts', 2).id, newsletter_id: fixtureManager.get('newsletters', 0).id, status: 'submitted', submitted_at: new Date(), track_opens: false, track_clicks: false, feedback_enabled: false, recipient_filter: 'all', subject: 'Test email', from: 'support@example.com', replyTo: null, email_count: 1, source: '{}', source_type: 'lexical', html, plaintext }); const {body} = await agent .get(`emails/${email.id}/`) .expectStatus(200) .matchBodySnapshot({ emails: [matchEmailNewsletter] }) .matchHeaderSnapshot({ 'content-version': anyContentVersion, etag: anyEtag }); // Simple check that there are not %%{ leftover (in case the snapshots gets updated without noticing what this test is checking) assert.equal(body.emails[0].html.includes('%%{'), false); assert.equal(body.emails[0].plaintext.includes('%%{'), false); }); });