mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-27 22:49:56 -05:00
Moved email event fetching to main thread (#16231)
This commit is contained in:
parent
fd79ca3f5a
commit
ea2c69565f
14 changed files with 336 additions and 496 deletions
|
@ -292,6 +292,7 @@ async function initServices({config}) {
|
||||||
const audienceFeedback = require('./server/services/audience-feedback');
|
const audienceFeedback = require('./server/services/audience-feedback');
|
||||||
const emailSuppressionList = require('./server/services/email-suppression-list');
|
const emailSuppressionList = require('./server/services/email-suppression-list');
|
||||||
const emailService = require('./server/services/email-service');
|
const emailService = require('./server/services/email-service');
|
||||||
|
const emailAnalytics = require('./server/services/email-analytics');
|
||||||
const mentionsService = require('./server/services/mentions');
|
const mentionsService = require('./server/services/mentions');
|
||||||
|
|
||||||
const urlUtils = require('./shared/url-utils');
|
const urlUtils = require('./shared/url-utils');
|
||||||
|
@ -316,6 +317,7 @@ async function initServices({config}) {
|
||||||
slack.listen(),
|
slack.listen(),
|
||||||
audienceFeedback.init(),
|
audienceFeedback.init(),
|
||||||
emailService.init(),
|
emailService.init(),
|
||||||
|
emailAnalytics.init(),
|
||||||
mega.listen(),
|
mega.listen(),
|
||||||
webhooks.listen(),
|
webhooks.listen(),
|
||||||
appService.init(),
|
appService.init(),
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* This is an event that is used to circumvent the job manager that currently isn't able to run scheduled jobs on the main thread (not offloaded).
|
||||||
|
* We simply emit this event in the job manager and listen for it on the main thread.
|
||||||
|
*/
|
||||||
|
module.exports = class StartEmailAnalyticsJobEvent {
|
||||||
|
/**
|
||||||
|
* @param {any} data
|
||||||
|
* @param {Date} timestamp
|
||||||
|
*/
|
||||||
|
constructor(data, timestamp) {
|
||||||
|
this.data = data;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} [data]
|
||||||
|
* @param {Date} [timestamp]
|
||||||
|
*/
|
||||||
|
static create(data, timestamp) {
|
||||||
|
return new StartEmailAnalyticsJobEvent(data, timestamp ?? new Date);
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,23 +1,3 @@
|
||||||
const config = require('../../../shared/config');
|
const EmailAnalyticsServiceWrapper = require('./wrapper');
|
||||||
const db = require('../../data/db');
|
|
||||||
const settings = require('../../../shared/settings-cache');
|
|
||||||
const {EmailAnalyticsService} = require('@tryghost/email-analytics-service');
|
|
||||||
const {EmailEventProcessor} = require('@tryghost/email-service');
|
|
||||||
const MailgunProvider = require('@tryghost/email-analytics-provider-mailgun');
|
|
||||||
const queries = require('./lib/queries');
|
|
||||||
const DomainEvents = require('@tryghost/domain-events');
|
|
||||||
|
|
||||||
const eventProcessor = new EmailEventProcessor({
|
module.exports = new EmailAnalyticsServiceWrapper();
|
||||||
domainEvents: DomainEvents,
|
|
||||||
db
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = new EmailAnalyticsService({
|
|
||||||
config,
|
|
||||||
settings,
|
|
||||||
eventProcessor,
|
|
||||||
providers: [
|
|
||||||
new MailgunProvider({config, settings})
|
|
||||||
],
|
|
||||||
queries
|
|
||||||
});
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const {parentPort} = require('worker_threads');
|
const {parentPort} = require('worker_threads');
|
||||||
|
const StartEmailAnalyticsJobEvent = require('../../events/StartEmailAnalyticsJobEvent');
|
||||||
|
|
||||||
// recurring job to fetch analytics since the most recently seen event timestamp
|
// recurring job to fetch analytics since the most recently seen event timestamp
|
||||||
|
|
||||||
|
@ -24,22 +25,15 @@ if (parentPort) {
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const {run} = require('./run');
|
// We send an evnet message, so that it is emitted on the main thread by the job manager
|
||||||
const {eventStats, aggregateEndDate, fetchStartDate} = await run({
|
// This will start the email analytics job on the main thread (the wrapper service is listening for this event)
|
||||||
domainEvents: {
|
parentPort.postMessage({
|
||||||
dispatch(event) {
|
event: {
|
||||||
parentPort.postMessage({
|
type: StartEmailAnalyticsJobEvent.name
|
||||||
event: {
|
|
||||||
type: event.constructor.name,
|
|
||||||
data: event
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (parentPort) {
|
if (parentPort) {
|
||||||
parentPort.postMessage(`Fetched ${eventStats.totalEvents} events and aggregated stats for ${eventStats.emailIds.length} emails in ${aggregateEndDate - fetchStartDate}ms`);
|
|
||||||
parentPort.postMessage('done');
|
parentPort.postMessage('done');
|
||||||
} else {
|
} else {
|
||||||
// give the logging pipes time finish writing before exit
|
// give the logging pipes time finish writing before exit
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
const debug = require('@tryghost/debug')('jobs:email-analytics:fetch-latest');
|
|
||||||
|
|
||||||
async function run({domainEvents}) {
|
|
||||||
const config = require('../../../../../shared/config');
|
|
||||||
const db = require('../../../../data/db');
|
|
||||||
|
|
||||||
const settingsRows = await db.knex('settings')
|
|
||||||
.whereIn('key', ['mailgun_api_key', 'mailgun_domain', 'mailgun_base_url']);
|
|
||||||
|
|
||||||
const settingsCache = {};
|
|
||||||
|
|
||||||
settingsRows.forEach((row) => {
|
|
||||||
settingsCache[row.key] = row.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const settings = {
|
|
||||||
get(key) {
|
|
||||||
return settingsCache[key];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const {EmailAnalyticsService} = require('@tryghost/email-analytics-service');
|
|
||||||
const MailgunProvider = require('@tryghost/email-analytics-provider-mailgun');
|
|
||||||
const queries = require('../../lib/queries');
|
|
||||||
const {EmailEventProcessor} = require('@tryghost/email-service');
|
|
||||||
|
|
||||||
// Since this is running in a worker thread, we cant dispatch directly
|
|
||||||
// So we post the events as a message to the job manager
|
|
||||||
const eventProcessor = new EmailEventProcessor({
|
|
||||||
domainEvents,
|
|
||||||
db
|
|
||||||
});
|
|
||||||
|
|
||||||
const emailAnalyticsService = new EmailAnalyticsService({
|
|
||||||
config,
|
|
||||||
settings,
|
|
||||||
eventProcessor,
|
|
||||||
providers: [
|
|
||||||
new MailgunProvider({config, settings})
|
|
||||||
],
|
|
||||||
queries
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchStartDate = new Date();
|
|
||||||
debug('Starting email analytics fetch of latest events');
|
|
||||||
const eventStats = await emailAnalyticsService.fetchLatest();
|
|
||||||
const fetchEndDate = new Date();
|
|
||||||
debug(`Finished fetching ${eventStats.totalEvents} analytics events in ${fetchEndDate - fetchStartDate}ms`);
|
|
||||||
|
|
||||||
const aggregateStartDate = new Date();
|
|
||||||
debug(`Starting email analytics aggregation for ${eventStats.emailIds.length} emails`);
|
|
||||||
await emailAnalyticsService.aggregateStats(eventStats);
|
|
||||||
const aggregateEndDate = new Date();
|
|
||||||
debug(`Finished aggregating email analytics in ${aggregateEndDate - aggregateStartDate}ms`);
|
|
||||||
return {eventStats, fetchStartDate, fetchEndDate, aggregateStartDate, aggregateEndDate};
|
|
||||||
}
|
|
||||||
module.exports.run = run;
|
|
90
ghost/core/core/server/services/email-analytics/wrapper.js
Normal file
90
ghost/core/core/server/services/email-analytics/wrapper.js
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
const logging = require('@tryghost/logging');
|
||||||
|
const debug = require('@tryghost/debug')('jobs:email-analytics:fetch-latest');
|
||||||
|
|
||||||
|
class EmailAnalyticsServiceWrapper {
|
||||||
|
init() {
|
||||||
|
if (this.service) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {EmailAnalyticsService} = require('@tryghost/email-analytics-service');
|
||||||
|
const {EmailEventStorage, EmailEventProcessor} = require('@tryghost/email-service');
|
||||||
|
const MailgunProvider = require('@tryghost/email-analytics-provider-mailgun');
|
||||||
|
const {EmailRecipientFailure, EmailSpamComplaintEvent} = require('../../models');
|
||||||
|
const StartEmailAnalyticsJobEvent = require('./events/StartEmailAnalyticsJobEvent');
|
||||||
|
|
||||||
|
const domainEvents = require('@tryghost/domain-events');
|
||||||
|
const config = require('../../../shared/config');
|
||||||
|
const settings = require('../../../shared/settings-cache');
|
||||||
|
const db = require('../../data/db');
|
||||||
|
const queries = require('./lib/queries');
|
||||||
|
const membersService = require('../members');
|
||||||
|
const membersRepository = membersService.api.members;
|
||||||
|
|
||||||
|
this.eventStorage = new EmailEventStorage({
|
||||||
|
db,
|
||||||
|
membersRepository,
|
||||||
|
models: {
|
||||||
|
EmailRecipientFailure,
|
||||||
|
EmailSpamComplaintEvent
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Since this is running in a worker thread, we cant dispatch directly
|
||||||
|
// So we post the events as a message to the job manager
|
||||||
|
const eventProcessor = new EmailEventProcessor({
|
||||||
|
domainEvents,
|
||||||
|
db,
|
||||||
|
eventStorage: this.eventStorage
|
||||||
|
});
|
||||||
|
|
||||||
|
this.service = new EmailAnalyticsService({
|
||||||
|
config,
|
||||||
|
settings,
|
||||||
|
eventProcessor,
|
||||||
|
providers: [
|
||||||
|
new MailgunProvider({config, settings})
|
||||||
|
],
|
||||||
|
queries
|
||||||
|
});
|
||||||
|
|
||||||
|
// We currently cannot trigger a non-offloaded job from the job manager
|
||||||
|
// So the email analytics jobs simply emits an event.
|
||||||
|
domainEvents.subscribe(StartEmailAnalyticsJobEvent, async () => {
|
||||||
|
await this.startFetch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async startFetch() {
|
||||||
|
if (this.fetching) {
|
||||||
|
logging.info('Email analytics fetch already running, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.fetching = true;
|
||||||
|
|
||||||
|
logging.info('Email analytics fetch started');
|
||||||
|
try {
|
||||||
|
const fetchStartDate = new Date();
|
||||||
|
debug('Starting email analytics fetch of latest events');
|
||||||
|
const eventStats = await this.service.fetchLatest();
|
||||||
|
const fetchEndDate = new Date();
|
||||||
|
debug(`Finished fetching ${eventStats.totalEvents} analytics events in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms`);
|
||||||
|
|
||||||
|
const aggregateStartDate = new Date();
|
||||||
|
debug(`Starting email analytics aggregation for ${eventStats.emailIds.length} emails`);
|
||||||
|
await this.service.aggregateStats(eventStats);
|
||||||
|
const aggregateEndDate = new Date();
|
||||||
|
debug(`Finished aggregating email analytics in ${aggregateEndDate.getTime() - aggregateStartDate.getTime()}ms`);
|
||||||
|
|
||||||
|
logging.info(`Fetched ${eventStats.totalEvents} events and aggregated stats for ${eventStats.emailIds.length} emails in ${aggregateEndDate.getTime() - fetchStartDate.getTime()}ms`);
|
||||||
|
|
||||||
|
this.fetching = false;
|
||||||
|
return eventStats;
|
||||||
|
} catch (e) {
|
||||||
|
logging.error(e, 'Error while fetching email analytics');
|
||||||
|
}
|
||||||
|
this.fetching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = EmailAnalyticsServiceWrapper;
|
|
@ -13,8 +13,8 @@ class EmailServiceWrapper {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {EmailService, EmailController, EmailRenderer, SendingService, BatchSendingService, EmailSegmenter, EmailEventStorage, MailgunEmailProvider} = require('@tryghost/email-service');
|
const {EmailService, EmailController, EmailRenderer, SendingService, BatchSendingService, EmailSegmenter, MailgunEmailProvider} = require('@tryghost/email-service');
|
||||||
const {Post, Newsletter, Email, EmailBatch, EmailRecipient, Member, EmailRecipientFailure, EmailSpamComplaintEvent} = require('../../models');
|
const {Post, Newsletter, Email, EmailBatch, EmailRecipient, Member} = require('../../models');
|
||||||
const MailgunClient = require('@tryghost/mailgun-client');
|
const MailgunClient = require('@tryghost/mailgun-client');
|
||||||
const configService = require('../../../shared/config');
|
const configService = require('../../../shared/config');
|
||||||
const settingsCache = require('../../../shared/settings-cache');
|
const settingsCache = require('../../../shared/settings-cache');
|
||||||
|
@ -25,7 +25,6 @@ class EmailServiceWrapper {
|
||||||
const sentry = require('../../../shared/sentry');
|
const sentry = require('../../../shared/sentry');
|
||||||
const membersRepository = membersService.api.members;
|
const membersRepository = membersService.api.members;
|
||||||
const limitService = require('../limits');
|
const limitService = require('../limits');
|
||||||
const domainEvents = require('@tryghost/domain-events');
|
|
||||||
|
|
||||||
const mobiledocLib = require('../../lib/mobiledoc');
|
const mobiledocLib = require('../../lib/mobiledoc');
|
||||||
const lexicalLib = require('../../lib/lexical');
|
const lexicalLib = require('../../lib/lexical');
|
||||||
|
@ -116,16 +115,6 @@ class EmailServiceWrapper {
|
||||||
Email
|
Email
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventStorage = new EmailEventStorage({
|
|
||||||
db,
|
|
||||||
membersRepository,
|
|
||||||
models: {
|
|
||||||
EmailRecipientFailure,
|
|
||||||
EmailSpamComplaintEvent
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.eventStorage.listen(domainEvents);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,8 @@ const {agentProvider, fixtureManager} = require('../../../utils/e2e-framework');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const domainEvents = require('@tryghost/domain-events');
|
const domainEvents = require('@tryghost/domain-events');
|
||||||
const MailgunClient = require('@tryghost/mailgun-client');
|
const MailgunClient = require('@tryghost/mailgun-client');
|
||||||
const {EmailDeliveredEvent} = require('@tryghost/email-events');
|
|
||||||
const DomainEvents = require('@tryghost/domain-events');
|
const DomainEvents = require('@tryghost/domain-events');
|
||||||
|
const emailAnalytics = require('../../../../core/server/services/email-analytics');
|
||||||
|
|
||||||
async function resetFailures(models, emailId) {
|
async function resetFailures(models, emailId) {
|
||||||
await models.EmailRecipientFailure.destroy({
|
await models.EmailRecipientFailure.destroy({
|
||||||
|
@ -31,7 +31,6 @@ describe('EmailEventStorage', function () {
|
||||||
|
|
||||||
// Only reference services after Ghost boot
|
// Only reference services after Ghost boot
|
||||||
models = require('../../../../core/server/models');
|
models = require('../../../../core/server/models');
|
||||||
run = require('../../../../core/server/services/email-analytics/jobs/fetch-latest/run.js').run;
|
|
||||||
membersService = require('../../../../core/server/services/members');
|
membersService = require('../../../../core/server/services/members');
|
||||||
jobsService = require('../../../../core/server/services/jobs');
|
jobsService = require('../../../../core/server/services/jobs');
|
||||||
|
|
||||||
|
@ -78,9 +77,7 @@ describe('EmailEventStorage', function () {
|
||||||
|
|
||||||
// Fire event processing
|
// Fire event processing
|
||||||
// We use offloading to have correct coverage and usage of worker thread
|
// We use offloading to have correct coverage and usage of worker thread
|
||||||
const {eventStats: result} = await run({
|
const result = await emailAnalytics.startFetch();
|
||||||
domainEvents
|
|
||||||
});
|
|
||||||
assert.equal(result.delivered, 1);
|
assert.equal(result.delivered, 1);
|
||||||
assert.deepEqual(result.emailIds, [emailId]);
|
assert.deepEqual(result.emailIds, [emailId]);
|
||||||
assert.deepEqual(result.memberIds, [memberId]);
|
assert.deepEqual(result.memberIds, [memberId]);
|
||||||
|
@ -131,10 +128,7 @@ describe('EmailEventStorage', function () {
|
||||||
assert.equal(initialModel.get('delivered_at'), null);
|
assert.equal(initialModel.get('delivered_at'), null);
|
||||||
|
|
||||||
// Fire event processing
|
// Fire event processing
|
||||||
// We use offloading to have correct coverage and usage of worker thread
|
const result = await emailAnalytics.startFetch();
|
||||||
const {eventStats: result} = await run({
|
|
||||||
domainEvents
|
|
||||||
});
|
|
||||||
assert.equal(result.delivered, 1);
|
assert.equal(result.delivered, 1);
|
||||||
assert.deepEqual(result.emailIds, [emailId]);
|
assert.deepEqual(result.emailIds, [emailId]);
|
||||||
assert.deepEqual(result.memberIds, [memberId]);
|
assert.deepEqual(result.memberIds, [memberId]);
|
||||||
|
@ -182,10 +176,7 @@ describe('EmailEventStorage', function () {
|
||||||
assert.equal(initialModel.get('opened_at'), null);
|
assert.equal(initialModel.get('opened_at'), null);
|
||||||
|
|
||||||
// Fire event processing
|
// Fire event processing
|
||||||
// We use offloading to have correct coverage and usage of worker thread
|
const result = await emailAnalytics.startFetch();
|
||||||
const {eventStats: result} = await run({
|
|
||||||
domainEvents
|
|
||||||
});
|
|
||||||
assert.equal(result.opened, 1);
|
assert.equal(result.opened, 1);
|
||||||
assert.deepEqual(result.emailIds, [emailId]);
|
assert.deepEqual(result.emailIds, [emailId]);
|
||||||
assert.deepEqual(result.memberIds, [memberId]);
|
assert.deepEqual(result.memberIds, [memberId]);
|
||||||
|
@ -267,10 +258,7 @@ describe('EmailEventStorage', function () {
|
||||||
assert.notEqual(initialModel.get('delivered_at'), null);
|
assert.notEqual(initialModel.get('delivered_at'), null);
|
||||||
|
|
||||||
// Fire event processing
|
// Fire event processing
|
||||||
// We use offloading to have correct coverage and usage of worker thread
|
const result = await emailAnalytics.startFetch();
|
||||||
const {eventStats: result} = await run({
|
|
||||||
domainEvents
|
|
||||||
});
|
|
||||||
assert.equal(result.permanentFailed, 1);
|
assert.equal(result.permanentFailed, 1);
|
||||||
assert.deepEqual(result.emailIds, [emailId]);
|
assert.deepEqual(result.emailIds, [emailId]);
|
||||||
assert.deepEqual(result.memberIds, [memberId]);
|
assert.deepEqual(result.memberIds, [memberId]);
|
||||||
|
@ -367,10 +355,7 @@ describe('EmailEventStorage', function () {
|
||||||
assert.notEqual(initialModel.get('failed_at'), null, 'This test requires a failed email recipient');
|
assert.notEqual(initialModel.get('failed_at'), null, 'This test requires a failed email recipient');
|
||||||
|
|
||||||
// Fire event processing
|
// Fire event processing
|
||||||
// We use offloading to have correct coverage and usage of worker thread
|
const result = await emailAnalytics.startFetch();
|
||||||
const {eventStats: result} = await run({
|
|
||||||
domainEvents
|
|
||||||
});
|
|
||||||
assert.equal(result.permanentFailed, 1);
|
assert.equal(result.permanentFailed, 1);
|
||||||
assert.deepEqual(result.emailIds, [emailId]);
|
assert.deepEqual(result.emailIds, [emailId]);
|
||||||
assert.deepEqual(result.memberIds, [memberId]);
|
assert.deepEqual(result.memberIds, [memberId]);
|
||||||
|
@ -462,10 +447,7 @@ describe('EmailEventStorage', function () {
|
||||||
assert.equal(initialModel.get('failed_at'), null);
|
assert.equal(initialModel.get('failed_at'), null);
|
||||||
|
|
||||||
// Fire event processing
|
// Fire event processing
|
||||||
// We use offloading to have correct coverage and usage of worker thread
|
const result = await emailAnalytics.startFetch();
|
||||||
const {eventStats: result} = await run({
|
|
||||||
domainEvents
|
|
||||||
});
|
|
||||||
assert.equal(result.permanentFailed, 1);
|
assert.equal(result.permanentFailed, 1);
|
||||||
assert.deepEqual(result.emailIds, [emailId]);
|
assert.deepEqual(result.emailIds, [emailId]);
|
||||||
assert.deepEqual(result.memberIds, [memberId]);
|
assert.deepEqual(result.memberIds, [memberId]);
|
||||||
|
@ -583,10 +565,7 @@ describe('EmailEventStorage', function () {
|
||||||
assert.equal(initialModel.get('failed_at'), null);
|
assert.equal(initialModel.get('failed_at'), null);
|
||||||
|
|
||||||
// Fire event processing
|
// Fire event processing
|
||||||
// We use offloading to have correct coverage and usage of worker thread
|
const result = await emailAnalytics.startFetch();
|
||||||
const {eventStats: result} = await run({
|
|
||||||
domainEvents
|
|
||||||
});
|
|
||||||
assert.equal(result.temporaryFailed, 1);
|
assert.equal(result.temporaryFailed, 1);
|
||||||
assert.deepEqual(result.emailIds, [emailId]);
|
assert.deepEqual(result.emailIds, [emailId]);
|
||||||
assert.deepEqual(result.memberIds, [memberId]);
|
assert.deepEqual(result.memberIds, [memberId]);
|
||||||
|
@ -690,10 +669,7 @@ describe('EmailEventStorage', function () {
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// Fire event processing
|
// Fire event processing
|
||||||
// We use offloading to have correct coverage and usage of worker thread
|
const result = await emailAnalytics.startFetch();
|
||||||
const {eventStats: result} = await run({
|
|
||||||
domainEvents
|
|
||||||
});
|
|
||||||
assert.equal(result.temporaryFailed, 1);
|
assert.equal(result.temporaryFailed, 1);
|
||||||
assert.deepEqual(result.emailIds, [emailId]);
|
assert.deepEqual(result.emailIds, [emailId]);
|
||||||
assert.deepEqual(result.memberIds, [memberId]);
|
assert.deepEqual(result.memberIds, [memberId]);
|
||||||
|
@ -797,10 +773,7 @@ describe('EmailEventStorage', function () {
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// Fire event processing
|
// Fire event processing
|
||||||
// We use offloading to have correct coverage and usage of worker thread
|
const result = await emailAnalytics.startFetch();
|
||||||
const {eventStats: result} = await run({
|
|
||||||
domainEvents
|
|
||||||
});
|
|
||||||
assert.equal(result.permanentFailed, 1);
|
assert.equal(result.permanentFailed, 1);
|
||||||
assert.deepEqual(result.emailIds, [emailId]);
|
assert.deepEqual(result.emailIds, [emailId]);
|
||||||
assert.deepEqual(result.memberIds, [memberId]);
|
assert.deepEqual(result.memberIds, [memberId]);
|
||||||
|
@ -866,10 +839,7 @@ describe('EmailEventStorage', function () {
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// Fire event processing
|
// Fire event processing
|
||||||
// We use offloading to have correct coverage and usage of worker thread
|
const result = await emailAnalytics.startFetch();
|
||||||
const {eventStats: result} = await run({
|
|
||||||
domainEvents
|
|
||||||
});
|
|
||||||
assert.equal(result.complained, 1);
|
assert.equal(result.complained, 1);
|
||||||
assert.deepEqual(result.emailIds, [emailId]);
|
assert.deepEqual(result.emailIds, [emailId]);
|
||||||
assert.deepEqual(result.memberIds, [memberId]);
|
assert.deepEqual(result.memberIds, [memberId]);
|
||||||
|
@ -920,10 +890,7 @@ describe('EmailEventStorage', function () {
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// Fire event processing
|
// Fire event processing
|
||||||
// We use offloading to have correct coverage and usage of worker thread
|
const result = await emailAnalytics.startFetch();
|
||||||
const {eventStats: result} = await run({
|
|
||||||
domainEvents
|
|
||||||
});
|
|
||||||
assert.equal(result.unsubscribed, 1);
|
assert.equal(result.unsubscribed, 1);
|
||||||
assert.deepEqual(result.emailIds, [emailId]);
|
assert.deepEqual(result.emailIds, [emailId]);
|
||||||
assert.deepEqual(result.memberIds, [memberId]);
|
assert.deepEqual(result.memberIds, [memberId]);
|
||||||
|
@ -962,10 +929,7 @@ describe('EmailEventStorage', function () {
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// Fire event processing
|
// Fire event processing
|
||||||
// We use offloading to have correct coverage and usage of worker thread
|
const result = await emailAnalytics.startFetch();
|
||||||
const {eventStats: result} = await run({
|
|
||||||
domainEvents
|
|
||||||
});
|
|
||||||
assert.equal(result.unhandled, 1);
|
assert.equal(result.unhandled, 1);
|
||||||
assert.deepEqual(result.emailIds, []);
|
assert.deepEqual(result.emailIds, []);
|
||||||
assert.deepEqual(result.memberIds, []);
|
assert.deepEqual(result.memberIds, []);
|
||||||
|
@ -981,10 +945,7 @@ describe('EmailEventStorage', function () {
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// Fire event processing
|
// Fire event processing
|
||||||
// We use offloading to have correct coverage and usage of worker thread
|
const result = await emailAnalytics.startFetch();
|
||||||
const {eventStats: result} = await run({
|
|
||||||
domainEvents
|
|
||||||
});
|
|
||||||
assert.equal(result.unhandled, 0);
|
assert.equal(result.unhandled, 0);
|
||||||
assert.deepEqual(result.emailIds, []);
|
assert.deepEqual(result.emailIds, []);
|
||||||
assert.deepEqual(result.memberIds, []);
|
assert.deepEqual(result.memberIds, []);
|
||||||
|
|
|
@ -3,20 +3,17 @@ const {agentProvider, fixtureManager} = require('../../utils/e2e-framework');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const MailgunClient = require('@tryghost/mailgun-client');
|
const MailgunClient = require('@tryghost/mailgun-client');
|
||||||
const DomainEvents = require('@tryghost/domain-events');
|
const DomainEvents = require('@tryghost/domain-events');
|
||||||
|
const emailAnalytics = require('../../../core/server/services/email-analytics');
|
||||||
|
|
||||||
describe('MailgunEmailSuppressionList', function () {
|
describe('MailgunEmailSuppressionList', function () {
|
||||||
let agent;
|
let agent;
|
||||||
let events = [];
|
let events = [];
|
||||||
let run;
|
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
agent = await agentProvider.getAdminAPIAgent();
|
agent = await agentProvider.getAdminAPIAgent();
|
||||||
await fixtureManager.init('newsletters', 'members:newsletters', 'members:emails');
|
await fixtureManager.init('newsletters', 'members:newsletters', 'members:emails');
|
||||||
await agent.loginAsOwner();
|
await agent.loginAsOwner();
|
||||||
|
|
||||||
// Only reference services after Ghost boot
|
|
||||||
run = require('../../../core/server/services/email-analytics/jobs/fetch-latest/run.js').run;
|
|
||||||
|
|
||||||
sinon.stub(MailgunClient.prototype, 'fetchEvents').callsFake(async function (_, batchHandler) {
|
sinon.stub(MailgunClient.prototype, 'fetchEvents').callsFake(async function (_, batchHandler) {
|
||||||
const normalizedEvents = (events.map(this.normalizeEvent) || []).filter(e => !!e);
|
const normalizedEvents = (events.map(this.normalizeEvent) || []).filter(e => !!e);
|
||||||
return [await batchHandler(normalizedEvents)];
|
return [await batchHandler(normalizedEvents)];
|
||||||
|
@ -47,10 +44,7 @@ describe('MailgunEmailSuppressionList', function () {
|
||||||
recipient
|
recipient
|
||||||
})];
|
})];
|
||||||
|
|
||||||
await run({
|
await emailAnalytics.startFetch();
|
||||||
domainEvents: DomainEvents
|
|
||||||
});
|
|
||||||
|
|
||||||
await DomainEvents.allSettled();
|
await DomainEvents.allSettled();
|
||||||
|
|
||||||
const {body: {members: [memberAfter]}} = await agent.get(`/members/${memberId}`);
|
const {body: {members: [memberAfter]}} = await agent.get(`/members/${memberId}`);
|
||||||
|
@ -78,10 +72,7 @@ describe('MailgunEmailSuppressionList', function () {
|
||||||
recipient
|
recipient
|
||||||
})];
|
})];
|
||||||
|
|
||||||
await run({
|
await emailAnalytics.startFetch();
|
||||||
domainEvents: DomainEvents
|
|
||||||
});
|
|
||||||
|
|
||||||
await DomainEvents.allSettled();
|
await DomainEvents.allSettled();
|
||||||
|
|
||||||
const {body: {members: [memberAfter]}} = await agent.get(`/members/${memberId}`);
|
const {body: {members: [memberAfter]}} = await agent.get(`/members/${memberId}`);
|
||||||
|
@ -109,10 +100,7 @@ describe('MailgunEmailSuppressionList', function () {
|
||||||
recipient
|
recipient
|
||||||
})];
|
})];
|
||||||
|
|
||||||
await run({
|
await emailAnalytics.startFetch();
|
||||||
domainEvents: DomainEvents
|
|
||||||
});
|
|
||||||
|
|
||||||
await DomainEvents.allSettled();
|
await DomainEvents.allSettled();
|
||||||
|
|
||||||
const {body: {members: [memberAfter]}} = await agent.get(`/members/${memberId}`);
|
const {body: {members: [memberAfter]}} = await agent.get(`/members/${memberId}`);
|
||||||
|
@ -140,10 +128,7 @@ describe('MailgunEmailSuppressionList', function () {
|
||||||
recipient
|
recipient
|
||||||
})];
|
})];
|
||||||
|
|
||||||
await run({
|
await emailAnalytics.startFetch();
|
||||||
domainEvents: DomainEvents
|
|
||||||
});
|
|
||||||
|
|
||||||
await DomainEvents.allSettled();
|
await DomainEvents.allSettled();
|
||||||
|
|
||||||
const {body: {members: [memberAfter]}} = await agent.get(`/members/${memberId}`);
|
const {body: {members: [memberAfter]}} = await agent.get(`/members/${memberId}`);
|
||||||
|
@ -178,10 +163,7 @@ describe('MailgunEmailSuppressionList', function () {
|
||||||
timestamp: Math.round(timestamp.getTime() / 1000)
|
timestamp: Math.round(timestamp.getTime() / 1000)
|
||||||
}];
|
}];
|
||||||
|
|
||||||
await run({
|
await emailAnalytics.startFetch();
|
||||||
domainEvents: DomainEvents
|
|
||||||
});
|
|
||||||
|
|
||||||
await DomainEvents.allSettled();
|
await DomainEvents.allSettled();
|
||||||
|
|
||||||
const {body: {members: [memberAfter]}} = await agent.get(`/members/${memberId}`);
|
const {body: {members: [memberAfter]}} = await agent.get(`/members/${memberId}`);
|
||||||
|
|
|
@ -642,7 +642,7 @@ const fixtures = {
|
||||||
memberIds: DataGenerator.forKnex.members.map(member => member.id)
|
memberIds: DataGenerator.forKnex.members.map(member => member.id)
|
||||||
};
|
};
|
||||||
|
|
||||||
return emailAnalyticsService.aggregateStats(toAggregate);
|
return emailAnalyticsService.service.aggregateStats(toAggregate);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ const {EmailDeliveredEvent, EmailOpenedEvent, EmailBouncedEvent, SpamComplaintEv
|
||||||
|
|
||||||
async function waitForEvent() {
|
async function waitForEvent() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(resolve, 200);
|
setTimeout(resolve, 70);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,16 +20,28 @@ async function waitForEvent() {
|
||||||
* @property {string} emailId
|
* @property {string} emailId
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef EmailEventStorage
|
||||||
|
* @property {(event: EmailDeliveredEvent) => Promise<void>} handleDelivered
|
||||||
|
* @property {(event: EmailOpenedEvent) => Promise<void>} handleOpened
|
||||||
|
* @property {(event: EmailBouncedEvent) => Promise<void>} handlePermanentFailed
|
||||||
|
* @property {(event: EmailTemporaryBouncedEvent) => Promise<void>} handleTemporaryFailed
|
||||||
|
* @property {(event: EmailUnsubscribedEvent) => Promise<void>} handleUnsubscribed
|
||||||
|
* @property {(event: SpamComplaintEvent) => Promise<void>} handleComplained
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WARNING: this class is used in a separate thread (an offloaded job). Be careful when working with settings and models.
|
* WARNING: this class is used in a separate thread (an offloaded job). Be careful when working with settings and models.
|
||||||
*/
|
*/
|
||||||
class EmailEventProcessor {
|
class EmailEventProcessor {
|
||||||
#domainEvents;
|
#domainEvents;
|
||||||
#db;
|
#db;
|
||||||
|
#eventStorage;
|
||||||
|
|
||||||
constructor({domainEvents, db}) {
|
constructor({domainEvents, db, eventStorage}) {
|
||||||
this.#domainEvents = domainEvents;
|
this.#domainEvents = domainEvents;
|
||||||
this.#db = db;
|
this.#db = db;
|
||||||
|
this.#eventStorage = eventStorage;
|
||||||
|
|
||||||
// Avoid having to query email_batch by provider_id for every event
|
// Avoid having to query email_batch by provider_id for every event
|
||||||
this.providerIdEmailIdMap = {};
|
this.providerIdEmailIdMap = {};
|
||||||
|
@ -42,15 +54,16 @@ class EmailEventProcessor {
|
||||||
async handleDelivered(emailIdentification, timestamp) {
|
async handleDelivered(emailIdentification, timestamp) {
|
||||||
const recipient = await this.getRecipient(emailIdentification);
|
const recipient = await this.getRecipient(emailIdentification);
|
||||||
if (recipient) {
|
if (recipient) {
|
||||||
this.#domainEvents.dispatch(EmailDeliveredEvent.create({
|
const event = EmailDeliveredEvent.create({
|
||||||
email: emailIdentification.email,
|
email: emailIdentification.email,
|
||||||
emailRecipientId: recipient.emailRecipientId,
|
emailRecipientId: recipient.emailRecipientId,
|
||||||
memberId: recipient.memberId,
|
memberId: recipient.memberId,
|
||||||
emailId: recipient.emailId,
|
emailId: recipient.emailId,
|
||||||
timestamp
|
timestamp
|
||||||
}));
|
});
|
||||||
// We cannot await the dispatched domainEvent, but we need to limit the number of events thare are processed at the same time
|
await this.#eventStorage.handleDelivered(event);
|
||||||
await waitForEvent();
|
|
||||||
|
this.#domainEvents.dispatch(event);
|
||||||
}
|
}
|
||||||
return recipient;
|
return recipient;
|
||||||
}
|
}
|
||||||
|
@ -62,15 +75,17 @@ class EmailEventProcessor {
|
||||||
async handleOpened(emailIdentification, timestamp) {
|
async handleOpened(emailIdentification, timestamp) {
|
||||||
const recipient = await this.getRecipient(emailIdentification);
|
const recipient = await this.getRecipient(emailIdentification);
|
||||||
if (recipient) {
|
if (recipient) {
|
||||||
this.#domainEvents.dispatch(EmailOpenedEvent.create({
|
const event = EmailOpenedEvent.create({
|
||||||
email: emailIdentification.email,
|
email: emailIdentification.email,
|
||||||
emailRecipientId: recipient.emailRecipientId,
|
emailRecipientId: recipient.emailRecipientId,
|
||||||
memberId: recipient.memberId,
|
memberId: recipient.memberId,
|
||||||
emailId: recipient.emailId,
|
emailId: recipient.emailId,
|
||||||
timestamp
|
timestamp
|
||||||
}));
|
});
|
||||||
// We cannot await the dispatched domainEvent, but we need to limit the number of events thare are processed at the same time
|
await this.#eventStorage.handleOpened(event);
|
||||||
await waitForEvent();
|
|
||||||
|
this.#domainEvents.dispatch(event);
|
||||||
|
await waitForEvent(); // Avoids knex connection pool to run dry
|
||||||
}
|
}
|
||||||
return recipient;
|
return recipient;
|
||||||
}
|
}
|
||||||
|
@ -82,7 +97,7 @@ class EmailEventProcessor {
|
||||||
async handleTemporaryFailed(emailIdentification, {timestamp, error, id}) {
|
async handleTemporaryFailed(emailIdentification, {timestamp, error, id}) {
|
||||||
const recipient = await this.getRecipient(emailIdentification);
|
const recipient = await this.getRecipient(emailIdentification);
|
||||||
if (recipient) {
|
if (recipient) {
|
||||||
this.#domainEvents.dispatch(EmailTemporaryBouncedEvent.create({
|
const event = EmailTemporaryBouncedEvent.create({
|
||||||
id,
|
id,
|
||||||
error,
|
error,
|
||||||
email: emailIdentification.email,
|
email: emailIdentification.email,
|
||||||
|
@ -90,9 +105,10 @@ class EmailEventProcessor {
|
||||||
emailId: recipient.emailId,
|
emailId: recipient.emailId,
|
||||||
emailRecipientId: recipient.emailRecipientId,
|
emailRecipientId: recipient.emailRecipientId,
|
||||||
timestamp
|
timestamp
|
||||||
}));
|
});
|
||||||
// We cannot await the dispatched domainEvent, but we need to limit the number of events thare are processed at the same time
|
await this.#eventStorage.handleTemporaryFailed(event);
|
||||||
await waitForEvent();
|
|
||||||
|
this.#domainEvents.dispatch(event);
|
||||||
}
|
}
|
||||||
return recipient;
|
return recipient;
|
||||||
}
|
}
|
||||||
|
@ -104,7 +120,7 @@ class EmailEventProcessor {
|
||||||
async handlePermanentFailed(emailIdentification, {timestamp, error, id}) {
|
async handlePermanentFailed(emailIdentification, {timestamp, error, id}) {
|
||||||
const recipient = await this.getRecipient(emailIdentification);
|
const recipient = await this.getRecipient(emailIdentification);
|
||||||
if (recipient) {
|
if (recipient) {
|
||||||
this.#domainEvents.dispatch(EmailBouncedEvent.create({
|
const event = EmailBouncedEvent.create({
|
||||||
id,
|
id,
|
||||||
error,
|
error,
|
||||||
email: emailIdentification.email,
|
email: emailIdentification.email,
|
||||||
|
@ -112,9 +128,11 @@ class EmailEventProcessor {
|
||||||
emailId: recipient.emailId,
|
emailId: recipient.emailId,
|
||||||
emailRecipientId: recipient.emailRecipientId,
|
emailRecipientId: recipient.emailRecipientId,
|
||||||
timestamp
|
timestamp
|
||||||
}));
|
});
|
||||||
// We cannot await the dispatched domainEvent, but we need to limit the number of events thare are processed at the same time
|
await this.#eventStorage.handlePermanentFailed(event);
|
||||||
await waitForEvent();
|
|
||||||
|
this.#domainEvents.dispatch(event);
|
||||||
|
await waitForEvent(); // Avoids knex connection pool to run dry
|
||||||
}
|
}
|
||||||
return recipient;
|
return recipient;
|
||||||
}
|
}
|
||||||
|
@ -126,14 +144,15 @@ class EmailEventProcessor {
|
||||||
async handleUnsubscribed(emailIdentification, timestamp) {
|
async handleUnsubscribed(emailIdentification, timestamp) {
|
||||||
const recipient = await this.getRecipient(emailIdentification);
|
const recipient = await this.getRecipient(emailIdentification);
|
||||||
if (recipient) {
|
if (recipient) {
|
||||||
this.#domainEvents.dispatch(EmailUnsubscribedEvent.create({
|
const event = EmailUnsubscribedEvent.create({
|
||||||
email: emailIdentification.email,
|
email: emailIdentification.email,
|
||||||
memberId: recipient.memberId,
|
memberId: recipient.memberId,
|
||||||
emailId: recipient.emailId,
|
emailId: recipient.emailId,
|
||||||
timestamp
|
timestamp
|
||||||
}));
|
});
|
||||||
// We cannot await the dispatched domainEvent, but we need to limit the number of events thare are processed at the same time
|
await this.#eventStorage.handleUnsubscribed(event);
|
||||||
await waitForEvent();
|
|
||||||
|
this.#domainEvents.dispatch(event);
|
||||||
}
|
}
|
||||||
return recipient;
|
return recipient;
|
||||||
}
|
}
|
||||||
|
@ -145,14 +164,16 @@ class EmailEventProcessor {
|
||||||
async handleComplained(emailIdentification, timestamp) {
|
async handleComplained(emailIdentification, timestamp) {
|
||||||
const recipient = await this.getRecipient(emailIdentification);
|
const recipient = await this.getRecipient(emailIdentification);
|
||||||
if (recipient) {
|
if (recipient) {
|
||||||
this.#domainEvents.dispatch(SpamComplaintEvent.create({
|
const event = SpamComplaintEvent.create({
|
||||||
email: emailIdentification.email,
|
email: emailIdentification.email,
|
||||||
memberId: recipient.memberId,
|
memberId: recipient.memberId,
|
||||||
emailId: recipient.emailId,
|
emailId: recipient.emailId,
|
||||||
timestamp
|
timestamp
|
||||||
}));
|
});
|
||||||
// We cannot await the dispatched domainEvent, but we need to limit the number of events thare are processed at the same time
|
await this.#eventStorage.handleComplained(event);
|
||||||
await waitForEvent();
|
|
||||||
|
this.#domainEvents.dispatch(event);
|
||||||
|
await waitForEvent(); // Avoids knex connection pool to run dry
|
||||||
}
|
}
|
||||||
return recipient;
|
return recipient;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
const {EmailDeliveredEvent, EmailOpenedEvent, EmailBouncedEvent, EmailTemporaryBouncedEvent, EmailUnsubscribedEvent, SpamComplaintEvent} = require('@tryghost/email-events');
|
|
||||||
const moment = require('moment-timezone');
|
const moment = require('moment-timezone');
|
||||||
const logging = require('@tryghost/logging');
|
const logging = require('@tryghost/logging');
|
||||||
|
|
||||||
|
@ -13,35 +12,6 @@ class EmailEventStorage {
|
||||||
this.#membersRepository = membersRepository;
|
this.#membersRepository = membersRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@tryghost/domain-events')} domainEvents
|
|
||||||
*/
|
|
||||||
listen(domainEvents) {
|
|
||||||
domainEvents.subscribe(EmailDeliveredEvent, async (event) => {
|
|
||||||
await this.handleDelivered(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
domainEvents.subscribe(EmailOpenedEvent, async (event) => {
|
|
||||||
await this.handleOpened(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
domainEvents.subscribe(EmailBouncedEvent, async (event) => {
|
|
||||||
await this.handlePermanentFailed(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
domainEvents.subscribe(EmailTemporaryBouncedEvent, async (event) => {
|
|
||||||
await this.handleTemporaryFailed(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
domainEvents.subscribe(EmailUnsubscribedEvent, async (event) => {
|
|
||||||
await this.handleUnsubscribed(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
domainEvents.subscribe(SpamComplaintEvent, async (event) => {
|
|
||||||
await this.handleComplained(event);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleDelivered(event) {
|
async handleDelivered(event) {
|
||||||
// To properly handle events that are received out of order (this happens because of polling)
|
// To properly handle events that are received out of order (this happens because of polling)
|
||||||
// only set if delivered_at is null
|
// only set if delivered_at is null
|
||||||
|
|
|
@ -5,6 +5,7 @@ const sinon = require('sinon');
|
||||||
|
|
||||||
describe('Email Event Processor', function () {
|
describe('Email Event Processor', function () {
|
||||||
let eventProcessor;
|
let eventProcessor;
|
||||||
|
let eventStorage;
|
||||||
let db;
|
let db;
|
||||||
let domainEvents;
|
let domainEvents;
|
||||||
|
|
||||||
|
@ -18,9 +19,20 @@ describe('Email Event Processor', function () {
|
||||||
domainEvents = {
|
domainEvents = {
|
||||||
dispatch: sinon.stub()
|
dispatch: sinon.stub()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
eventStorage = {
|
||||||
|
handleDelivered: sinon.stub(),
|
||||||
|
handleOpened: sinon.stub(),
|
||||||
|
handlePermanentFailed: sinon.stub(),
|
||||||
|
handleTemporaryFailed: sinon.stub(),
|
||||||
|
handleComplained: sinon.stub(),
|
||||||
|
handleUnsubscribed: sinon.stub()
|
||||||
|
};
|
||||||
|
|
||||||
eventProcessor = new EmailEventProcessor({
|
eventProcessor = new EmailEventProcessor({
|
||||||
db,
|
db,
|
||||||
domainEvents
|
domainEvents,
|
||||||
|
eventStorage
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -88,8 +100,8 @@ describe('Email Event Processor', function () {
|
||||||
memberId: 'member-id',
|
memberId: 'member-id',
|
||||||
emailId: 'email-id'
|
emailId: 'email-id'
|
||||||
});
|
});
|
||||||
assert.equal(domainEvents.dispatch.callCount, 1);
|
assert.equal(eventStorage.handleDelivered.callCount, 1);
|
||||||
const event = domainEvents.dispatch.firstCall.args[0];
|
const event = eventStorage.handleDelivered.firstCall.args[0];
|
||||||
assert.equal(event.email, 'example@example.com');
|
assert.equal(event.email, 'example@example.com');
|
||||||
assert.equal(event.constructor.name, 'EmailDeliveredEvent');
|
assert.equal(event.constructor.name, 'EmailDeliveredEvent');
|
||||||
});
|
});
|
||||||
|
@ -101,8 +113,8 @@ describe('Email Event Processor', function () {
|
||||||
memberId: 'member-id',
|
memberId: 'member-id',
|
||||||
emailId: 'email-id'
|
emailId: 'email-id'
|
||||||
});
|
});
|
||||||
assert.equal(domainEvents.dispatch.callCount, 1);
|
assert.equal(eventStorage.handleOpened.callCount, 1);
|
||||||
const event = domainEvents.dispatch.firstCall.args[0];
|
const event = eventStorage.handleOpened.firstCall.args[0];
|
||||||
assert.equal(event.email, 'example@example.com');
|
assert.equal(event.email, 'example@example.com');
|
||||||
assert.equal(event.constructor.name, 'EmailOpenedEvent');
|
assert.equal(event.constructor.name, 'EmailOpenedEvent');
|
||||||
});
|
});
|
||||||
|
@ -114,8 +126,8 @@ describe('Email Event Processor', function () {
|
||||||
memberId: 'member-id',
|
memberId: 'member-id',
|
||||||
emailId: 'email-id'
|
emailId: 'email-id'
|
||||||
});
|
});
|
||||||
assert.equal(domainEvents.dispatch.callCount, 1);
|
assert.equal(eventStorage.handleTemporaryFailed.callCount, 1);
|
||||||
const event = domainEvents.dispatch.firstCall.args[0];
|
const event = eventStorage.handleTemporaryFailed.firstCall.args[0];
|
||||||
assert.equal(event.email, 'example@example.com');
|
assert.equal(event.email, 'example@example.com');
|
||||||
assert.equal(event.constructor.name, 'EmailTemporaryBouncedEvent');
|
assert.equal(event.constructor.name, 'EmailTemporaryBouncedEvent');
|
||||||
});
|
});
|
||||||
|
@ -127,8 +139,8 @@ describe('Email Event Processor', function () {
|
||||||
memberId: 'member-id',
|
memberId: 'member-id',
|
||||||
emailId: 'email-id'
|
emailId: 'email-id'
|
||||||
});
|
});
|
||||||
assert.equal(domainEvents.dispatch.callCount, 1);
|
assert.equal(eventStorage.handlePermanentFailed.callCount, 1);
|
||||||
const event = domainEvents.dispatch.firstCall.args[0];
|
const event = eventStorage.handlePermanentFailed.firstCall.args[0];
|
||||||
assert.equal(event.email, 'example@example.com');
|
assert.equal(event.email, 'example@example.com');
|
||||||
assert.equal(event.constructor.name, 'EmailBouncedEvent');
|
assert.equal(event.constructor.name, 'EmailBouncedEvent');
|
||||||
});
|
});
|
||||||
|
@ -140,8 +152,8 @@ describe('Email Event Processor', function () {
|
||||||
memberId: 'member-id',
|
memberId: 'member-id',
|
||||||
emailId: 'email-id'
|
emailId: 'email-id'
|
||||||
});
|
});
|
||||||
assert.equal(domainEvents.dispatch.callCount, 1);
|
assert.equal(eventStorage.handleUnsubscribed.callCount, 1);
|
||||||
const event = domainEvents.dispatch.firstCall.args[0];
|
const event = eventStorage.handleUnsubscribed.firstCall.args[0];
|
||||||
assert.equal(event.email, 'example@example.com');
|
assert.equal(event.email, 'example@example.com');
|
||||||
assert.equal(event.constructor.name, 'EmailUnsubscribedEvent');
|
assert.equal(event.constructor.name, 'EmailUnsubscribedEvent');
|
||||||
});
|
});
|
||||||
|
@ -153,8 +165,8 @@ describe('Email Event Processor', function () {
|
||||||
memberId: 'member-id',
|
memberId: 'member-id',
|
||||||
emailId: 'email-id'
|
emailId: 'email-id'
|
||||||
});
|
});
|
||||||
assert.equal(domainEvents.dispatch.callCount, 1);
|
assert.equal(eventStorage.handleComplained.callCount, 1);
|
||||||
const event = domainEvents.dispatch.firstCall.args[0];
|
const event = eventStorage.handleComplained.firstCall.args[0];
|
||||||
assert.equal(event.email, 'example@example.com');
|
assert.equal(event.email, 'example@example.com');
|
||||||
assert.equal(event.constructor.name, 'SpamComplaintEvent');
|
assert.equal(event.constructor.name, 'SpamComplaintEvent');
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,6 +10,7 @@ describe('Email Event Storage', function () {
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
logError = sinon.stub(logging, 'error');
|
logError = sinon.stub(logging, 'error');
|
||||||
|
sinon.stub(logging, 'info');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
|
@ -23,76 +24,51 @@ describe('Email Event Storage', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Handles email delivered events', async function () {
|
it('Handles email delivered events', async function () {
|
||||||
const DomainEvents = {
|
const event = EmailDeliveredEvent.create({
|
||||||
subscribe: async (type, handler) => {
|
email: 'example@example.com',
|
||||||
if (type === EmailDeliveredEvent) {
|
memberId: '123',
|
||||||
handler(EmailDeliveredEvent.create({
|
emailId: '456',
|
||||||
email: 'example@example.com',
|
emailRecipientId: '789',
|
||||||
memberId: '123',
|
timestamp: new Date(0)
|
||||||
emailId: '456',
|
});
|
||||||
emailRecipientId: '789',
|
|
||||||
timestamp: new Date(0)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
|
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
const eventHandler = new EmailEventStorage({db});
|
const eventHandler = new EmailEventStorage({db});
|
||||||
eventHandler.listen(DomainEvents);
|
await eventHandler.handleDelivered(event);
|
||||||
sinon.assert.callCount(subscribeSpy, 6);
|
|
||||||
sinon.assert.calledOnce(db.update);
|
sinon.assert.calledOnce(db.update);
|
||||||
assert(!!db.update.firstCall.args[0].delivered_at);
|
assert(!!db.update.firstCall.args[0].delivered_at);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Handles email opened events', async function () {
|
it('Handles email opened events', async function () {
|
||||||
const DomainEvents = {
|
const event = EmailOpenedEvent.create({
|
||||||
subscribe: async (type, handler) => {
|
email: 'example@example.com',
|
||||||
if (type === EmailOpenedEvent) {
|
memberId: '123',
|
||||||
handler(EmailOpenedEvent.create({
|
emailId: '456',
|
||||||
email: 'example@example.com',
|
emailRecipientId: '789',
|
||||||
memberId: '123',
|
timestamp: new Date(0)
|
||||||
emailId: '456',
|
});
|
||||||
emailRecipientId: '789',
|
|
||||||
timestamp: new Date(0)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
|
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
const eventHandler = new EmailEventStorage({db});
|
const eventHandler = new EmailEventStorage({db});
|
||||||
eventHandler.listen(DomainEvents);
|
await eventHandler.handleOpened(event);
|
||||||
sinon.assert.callCount(subscribeSpy, 6);
|
|
||||||
sinon.assert.calledOnce(db.update);
|
sinon.assert.calledOnce(db.update);
|
||||||
assert(!!db.update.firstCall.args[0].opened_at);
|
assert(!!db.update.firstCall.args[0].opened_at);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Handles email permanent bounce events with update', async function () {
|
it('Handles email permanent bounce events with update', async function () {
|
||||||
let waitPromise;
|
const event = EmailBouncedEvent.create({
|
||||||
|
email: 'example@example.com',
|
||||||
|
memberId: '123',
|
||||||
|
emailId: '456',
|
||||||
|
emailRecipientId: '789',
|
||||||
|
error: {
|
||||||
|
message: 'test',
|
||||||
|
code: 500,
|
||||||
|
enhancedCode: '5.5.5'
|
||||||
|
},
|
||||||
|
timestamp: new Date(0)
|
||||||
|
});
|
||||||
|
|
||||||
const DomainEvents = {
|
|
||||||
subscribe: async (type, handler) => {
|
|
||||||
if (type === EmailBouncedEvent) {
|
|
||||||
waitPromise = handler(EmailBouncedEvent.create({
|
|
||||||
email: 'example@example.com',
|
|
||||||
memberId: '123',
|
|
||||||
emailId: '456',
|
|
||||||
emailRecipientId: '789',
|
|
||||||
error: {
|
|
||||||
message: 'test',
|
|
||||||
code: 500,
|
|
||||||
enhancedCode: '5.5.5'
|
|
||||||
},
|
|
||||||
timestamp: new Date(0)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
|
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
const existing = {
|
const existing = {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
@ -119,37 +95,26 @@ describe('Email Event Storage', function () {
|
||||||
EmailRecipientFailure
|
EmailRecipientFailure
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
eventHandler.listen(DomainEvents);
|
await eventHandler.handlePermanentFailed(event);
|
||||||
sinon.assert.callCount(subscribeSpy, 6);
|
|
||||||
await waitPromise;
|
|
||||||
sinon.assert.calledOnce(db.update);
|
sinon.assert.calledOnce(db.update);
|
||||||
assert(!!db.update.firstCall.args[0].failed_at);
|
assert(!!db.update.firstCall.args[0].failed_at);
|
||||||
assert(existing.save.calledOnce);
|
assert(existing.save.calledOnce);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Handles email permanent bounce events with insert', async function () {
|
it('Handles email permanent bounce events with insert', async function () {
|
||||||
let waitPromise;
|
const event = EmailBouncedEvent.create({
|
||||||
|
email: 'example@example.com',
|
||||||
|
memberId: '123',
|
||||||
|
emailId: '456',
|
||||||
|
emailRecipientId: '789',
|
||||||
|
error: {
|
||||||
|
message: 'test',
|
||||||
|
code: 500,
|
||||||
|
enhancedCode: '5.5.5'
|
||||||
|
},
|
||||||
|
timestamp: new Date(0)
|
||||||
|
});
|
||||||
|
|
||||||
const DomainEvents = {
|
|
||||||
subscribe: async (type, handler) => {
|
|
||||||
if (type === EmailBouncedEvent) {
|
|
||||||
waitPromise = handler(EmailBouncedEvent.create({
|
|
||||||
email: 'example@example.com',
|
|
||||||
memberId: '123',
|
|
||||||
emailId: '456',
|
|
||||||
emailRecipientId: '789',
|
|
||||||
error: {
|
|
||||||
message: 'test',
|
|
||||||
code: 500,
|
|
||||||
enhancedCode: '5.5.5'
|
|
||||||
},
|
|
||||||
timestamp: new Date(0)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
|
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
const EmailRecipientFailure = {
|
const EmailRecipientFailure = {
|
||||||
transaction: async function (callback) {
|
transaction: async function (callback) {
|
||||||
|
@ -165,68 +130,45 @@ describe('Email Event Storage', function () {
|
||||||
EmailRecipientFailure
|
EmailRecipientFailure
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
eventHandler.listen(DomainEvents);
|
await eventHandler.handlePermanentFailed(event);
|
||||||
sinon.assert.callCount(subscribeSpy, 6);
|
|
||||||
await waitPromise;
|
|
||||||
sinon.assert.calledOnce(db.update);
|
sinon.assert.calledOnce(db.update);
|
||||||
assert(!!db.update.firstCall.args[0].failed_at);
|
assert(!!db.update.firstCall.args[0].failed_at);
|
||||||
assert(EmailRecipientFailure.add.calledOnce);
|
assert(EmailRecipientFailure.add.calledOnce);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Handles email permanent bounce event without error data', async function () {
|
it('Handles email permanent bounce event without error data', async function () {
|
||||||
let waitPromise;
|
const event = EmailBouncedEvent.create({
|
||||||
|
email: 'example@example.com',
|
||||||
|
memberId: '123',
|
||||||
|
emailId: '456',
|
||||||
|
emailRecipientId: '789',
|
||||||
|
error: null,
|
||||||
|
timestamp: new Date(0)
|
||||||
|
});
|
||||||
|
|
||||||
const DomainEvents = {
|
|
||||||
subscribe: async (type, handler) => {
|
|
||||||
if (type === EmailBouncedEvent) {
|
|
||||||
waitPromise = handler(EmailBouncedEvent.create({
|
|
||||||
email: 'example@example.com',
|
|
||||||
memberId: '123',
|
|
||||||
emailId: '456',
|
|
||||||
emailRecipientId: '789',
|
|
||||||
error: null,
|
|
||||||
timestamp: new Date(0)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
|
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
|
|
||||||
const eventHandler = new EmailEventStorage({
|
const eventHandler = new EmailEventStorage({
|
||||||
db,
|
db,
|
||||||
models: {}
|
models: {}
|
||||||
});
|
});
|
||||||
eventHandler.listen(DomainEvents);
|
await eventHandler.handlePermanentFailed(event);
|
||||||
sinon.assert.callCount(subscribeSpy, 6);
|
|
||||||
await waitPromise;
|
|
||||||
sinon.assert.calledOnce(db.update);
|
sinon.assert.calledOnce(db.update);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Handles email permanent bounce events with skipped update', async function () {
|
it('Handles email permanent bounce events with skipped update', async function () {
|
||||||
let waitPromise;
|
const event = EmailBouncedEvent.create({
|
||||||
|
email: 'example@example.com',
|
||||||
|
memberId: '123',
|
||||||
|
emailId: '456',
|
||||||
|
emailRecipientId: '789',
|
||||||
|
error: {
|
||||||
|
message: 'test',
|
||||||
|
code: 500,
|
||||||
|
enhancedCode: '5.5.5'
|
||||||
|
},
|
||||||
|
timestamp: new Date(0)
|
||||||
|
});
|
||||||
|
|
||||||
const DomainEvents = {
|
|
||||||
subscribe: async (type, handler) => {
|
|
||||||
if (type === EmailBouncedEvent) {
|
|
||||||
waitPromise = handler(EmailBouncedEvent.create({
|
|
||||||
email: 'example@example.com',
|
|
||||||
memberId: '123',
|
|
||||||
emailId: '456',
|
|
||||||
emailRecipientId: '789',
|
|
||||||
error: {
|
|
||||||
message: 'test',
|
|
||||||
code: 500,
|
|
||||||
enhancedCode: '5.5.5'
|
|
||||||
},
|
|
||||||
timestamp: new Date(0)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
|
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
const existing = {
|
const existing = {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
@ -253,9 +195,7 @@ describe('Email Event Storage', function () {
|
||||||
EmailRecipientFailure
|
EmailRecipientFailure
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
eventHandler.listen(DomainEvents);
|
await eventHandler.handlePermanentFailed(event);
|
||||||
sinon.assert.callCount(subscribeSpy, 6);
|
|
||||||
await waitPromise;
|
|
||||||
sinon.assert.calledOnce(db.update);
|
sinon.assert.calledOnce(db.update);
|
||||||
assert(!!db.update.firstCall.args[0].failed_at);
|
assert(!!db.update.firstCall.args[0].failed_at);
|
||||||
assert(EmailRecipientFailure.findOne.called);
|
assert(EmailRecipientFailure.findOne.called);
|
||||||
|
@ -263,28 +203,19 @@ describe('Email Event Storage', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Handles email temporary bounce events with update', async function () {
|
it('Handles email temporary bounce events with update', async function () {
|
||||||
let waitPromise;
|
const event = EmailTemporaryBouncedEvent.create({
|
||||||
|
email: 'example@example.com',
|
||||||
|
memberId: '123',
|
||||||
|
emailId: '456',
|
||||||
|
emailRecipientId: '789',
|
||||||
|
error: {
|
||||||
|
message: 'test',
|
||||||
|
code: 500,
|
||||||
|
enhancedCode: null
|
||||||
|
},
|
||||||
|
timestamp: new Date(0)
|
||||||
|
});
|
||||||
|
|
||||||
const DomainEvents = {
|
|
||||||
subscribe: async (type, handler) => {
|
|
||||||
if (type === EmailTemporaryBouncedEvent) {
|
|
||||||
waitPromise = handler(EmailTemporaryBouncedEvent.create({
|
|
||||||
email: 'example@example.com',
|
|
||||||
memberId: '123',
|
|
||||||
emailId: '456',
|
|
||||||
emailRecipientId: '789',
|
|
||||||
error: {
|
|
||||||
message: 'test',
|
|
||||||
code: 500,
|
|
||||||
enhancedCode: null
|
|
||||||
},
|
|
||||||
timestamp: new Date(0)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
|
|
||||||
const existing = {
|
const existing = {
|
||||||
id: 1,
|
id: 1,
|
||||||
get: (key) => {
|
get: (key) => {
|
||||||
|
@ -309,35 +240,24 @@ describe('Email Event Storage', function () {
|
||||||
EmailRecipientFailure
|
EmailRecipientFailure
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
eventHandler.listen(DomainEvents);
|
await eventHandler.handleTemporaryFailed(event);
|
||||||
sinon.assert.callCount(subscribeSpy, 6);
|
|
||||||
await waitPromise;
|
|
||||||
assert(existing.save.calledOnce);
|
assert(existing.save.calledOnce);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Handles email temporary bounce events with skipped update', async function () {
|
it('Handles email temporary bounce events with skipped update', async function () {
|
||||||
let waitPromise;
|
const event = EmailTemporaryBouncedEvent.create({
|
||||||
|
email: 'example@example.com',
|
||||||
|
memberId: '123',
|
||||||
|
emailId: '456',
|
||||||
|
emailRecipientId: '789',
|
||||||
|
error: {
|
||||||
|
message: 'test',
|
||||||
|
code: 500,
|
||||||
|
enhancedCode: '5.5.5'
|
||||||
|
},
|
||||||
|
timestamp: new Date(0)
|
||||||
|
});
|
||||||
|
|
||||||
const DomainEvents = {
|
|
||||||
subscribe: async (type, handler) => {
|
|
||||||
if (type === EmailTemporaryBouncedEvent) {
|
|
||||||
waitPromise = handler(EmailTemporaryBouncedEvent.create({
|
|
||||||
email: 'example@example.com',
|
|
||||||
memberId: '123',
|
|
||||||
emailId: '456',
|
|
||||||
emailRecipientId: '789',
|
|
||||||
error: {
|
|
||||||
message: 'test',
|
|
||||||
code: 500,
|
|
||||||
enhancedCode: '5.5.5'
|
|
||||||
},
|
|
||||||
timestamp: new Date(0)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
|
|
||||||
const existing = {
|
const existing = {
|
||||||
id: 1,
|
id: 1,
|
||||||
get: (key) => {
|
get: (key) => {
|
||||||
|
@ -362,29 +282,18 @@ describe('Email Event Storage', function () {
|
||||||
EmailRecipientFailure
|
EmailRecipientFailure
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
eventHandler.listen(DomainEvents);
|
await eventHandler.handleTemporaryFailed(event);
|
||||||
sinon.assert.callCount(subscribeSpy, 6);
|
|
||||||
await waitPromise;
|
|
||||||
assert(existing.save.notCalled);
|
assert(existing.save.notCalled);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Handles unsubscribe', async function () {
|
it('Handles unsubscribe', async function () {
|
||||||
let waitPromise;
|
const event = EmailUnsubscribedEvent.create({
|
||||||
|
email: 'example@example.com',
|
||||||
|
memberId: '123',
|
||||||
|
emailId: '456',
|
||||||
|
timestamp: new Date(0)
|
||||||
|
});
|
||||||
|
|
||||||
const DomainEvents = {
|
|
||||||
subscribe: async (type, handler) => {
|
|
||||||
if (type === EmailUnsubscribedEvent) {
|
|
||||||
waitPromise = handler(EmailUnsubscribedEvent.create({
|
|
||||||
email: 'example@example.com',
|
|
||||||
memberId: '123',
|
|
||||||
emailId: '456',
|
|
||||||
timestamp: new Date(0)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
|
|
||||||
const update = sinon.stub().resolves();
|
const update = sinon.stub().resolves();
|
||||||
|
|
||||||
const eventHandler = new EmailEventStorage({
|
const eventHandler = new EmailEventStorage({
|
||||||
|
@ -392,30 +301,19 @@ describe('Email Event Storage', function () {
|
||||||
update
|
update
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
eventHandler.listen(DomainEvents);
|
await eventHandler.handleUnsubscribed(event);
|
||||||
sinon.assert.callCount(subscribeSpy, 6);
|
|
||||||
await waitPromise;
|
|
||||||
assert(update.calledOnce);
|
assert(update.calledOnce);
|
||||||
assert(update.firstCall.args[0].newsletters.length === 0);
|
assert(update.firstCall.args[0].newsletters.length === 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Handles complaints', async function () {
|
it('Handles complaints', async function () {
|
||||||
let waitPromise;
|
const event = SpamComplaintEvent.create({
|
||||||
|
email: 'example@example.com',
|
||||||
|
memberId: '123',
|
||||||
|
emailId: '456',
|
||||||
|
timestamp: new Date(0)
|
||||||
|
});
|
||||||
|
|
||||||
const DomainEvents = {
|
|
||||||
subscribe: async (type, handler) => {
|
|
||||||
if (type === SpamComplaintEvent) {
|
|
||||||
waitPromise = handler(SpamComplaintEvent.create({
|
|
||||||
email: 'example@example.com',
|
|
||||||
memberId: '123',
|
|
||||||
emailId: '456',
|
|
||||||
timestamp: new Date(0)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
|
|
||||||
const EmailSpamComplaintEvent = {
|
const EmailSpamComplaintEvent = {
|
||||||
add: sinon.stub().resolves()
|
add: sinon.stub().resolves()
|
||||||
};
|
};
|
||||||
|
@ -425,29 +323,18 @@ describe('Email Event Storage', function () {
|
||||||
EmailSpamComplaintEvent
|
EmailSpamComplaintEvent
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
eventHandler.listen(DomainEvents);
|
await eventHandler.handleComplained(event);
|
||||||
sinon.assert.callCount(subscribeSpy, 6);
|
|
||||||
await waitPromise;
|
|
||||||
assert(EmailSpamComplaintEvent.add.calledOnce);
|
assert(EmailSpamComplaintEvent.add.calledOnce);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Handles duplicate complaints', async function () {
|
it('Handles duplicate complaints', async function () {
|
||||||
let waitPromise;
|
const event = SpamComplaintEvent.create({
|
||||||
|
email: 'example@example.com',
|
||||||
|
memberId: '123',
|
||||||
|
emailId: '456',
|
||||||
|
timestamp: new Date(0)
|
||||||
|
});
|
||||||
|
|
||||||
const DomainEvents = {
|
|
||||||
subscribe: async (type, handler) => {
|
|
||||||
if (type === SpamComplaintEvent) {
|
|
||||||
waitPromise = handler(SpamComplaintEvent.create({
|
|
||||||
email: 'example@example.com',
|
|
||||||
memberId: '123',
|
|
||||||
emailId: '456',
|
|
||||||
timestamp: new Date(0)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
|
|
||||||
const EmailSpamComplaintEvent = {
|
const EmailSpamComplaintEvent = {
|
||||||
add: sinon.stub().rejects({code: 'ER_DUP_ENTRY'})
|
add: sinon.stub().rejects({code: 'ER_DUP_ENTRY'})
|
||||||
};
|
};
|
||||||
|
@ -457,30 +344,19 @@ describe('Email Event Storage', function () {
|
||||||
EmailSpamComplaintEvent
|
EmailSpamComplaintEvent
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
eventHandler.listen(DomainEvents);
|
await eventHandler.handleComplained(event);
|
||||||
sinon.assert.callCount(subscribeSpy, 6);
|
|
||||||
await waitPromise;
|
|
||||||
assert(EmailSpamComplaintEvent.add.calledOnce);
|
assert(EmailSpamComplaintEvent.add.calledOnce);
|
||||||
assert(!logError.calledOnce);
|
assert(!logError.calledOnce);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Handles logging failed complaint storage', async function () {
|
it('Handles logging failed complaint storage', async function () {
|
||||||
let waitPromise;
|
const event = SpamComplaintEvent.create({
|
||||||
|
email: 'example@example.com',
|
||||||
|
memberId: '123',
|
||||||
|
emailId: '456',
|
||||||
|
timestamp: new Date(0)
|
||||||
|
});
|
||||||
|
|
||||||
const DomainEvents = {
|
|
||||||
subscribe: async (type, handler) => {
|
|
||||||
if (type === SpamComplaintEvent) {
|
|
||||||
waitPromise = handler(SpamComplaintEvent.create({
|
|
||||||
email: 'example@example.com',
|
|
||||||
memberId: '123',
|
|
||||||
emailId: '456',
|
|
||||||
timestamp: new Date(0)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
|
|
||||||
const EmailSpamComplaintEvent = {
|
const EmailSpamComplaintEvent = {
|
||||||
add: sinon.stub().rejects(new Error('Some database error'))
|
add: sinon.stub().rejects(new Error('Some database error'))
|
||||||
};
|
};
|
||||||
|
@ -490,9 +366,7 @@ describe('Email Event Storage', function () {
|
||||||
EmailSpamComplaintEvent
|
EmailSpamComplaintEvent
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
eventHandler.listen(DomainEvents);
|
await eventHandler.handleComplained(event);
|
||||||
sinon.assert.callCount(subscribeSpy, 6);
|
|
||||||
await waitPromise;
|
|
||||||
assert(EmailSpamComplaintEvent.add.calledOnce);
|
assert(EmailSpamComplaintEvent.add.calledOnce);
|
||||||
assert(logError.calledOnce);
|
assert(logError.calledOnce);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue