Added 100% test coverage for EmailEventStorage

refs https://github.com/TryGhost/Team/issues/2339
This commit is contained in:
Simon Backx 2023-01-10 16:36:41 +01:00
parent 9ca2e3f183
commit 6a364e7779
5 changed files with 202 additions and 75 deletions

View File

@ -4,7 +4,7 @@ module.exports = class EmailBouncedEvent {
* @type {string} * @type {string}
*/ */
id; id;
/** /**
* @readonly * @readonly
* @type {string} * @type {string}
@ -25,7 +25,7 @@ module.exports = class EmailBouncedEvent {
/** /**
* @readonly * @readonly
* @type {{message: string, code: number, enhancedCode: string | null}} * @type {{message: string, code: number, enhancedCode: string | null}|null}
*/ */
error; error;

View File

@ -25,7 +25,7 @@ module.exports = class EmailTemporaryBouncedEvent {
/** /**
* @readonly * @readonly
* @type {{message: string, code: number, enhancedCode: string | null}} * @type {{message: string, code: number, enhancedCode: string | null}|null}
*/ */
error; error;

View File

@ -18,51 +18,27 @@ class EmailEventStorage {
*/ */
listen(domainEvents) { listen(domainEvents) {
domainEvents.subscribe(EmailDeliveredEvent, async (event) => { domainEvents.subscribe(EmailDeliveredEvent, async (event) => {
try { await this.handleDelivered(event);
await this.handleDelivered(event);
} catch (err) {
logging.error(err);
}
}); });
domainEvents.subscribe(EmailOpenedEvent, async (event) => { domainEvents.subscribe(EmailOpenedEvent, async (event) => {
try { await this.handleOpened(event);
await this.handleOpened(event);
} catch (err) {
logging.error(err);
}
}); });
domainEvents.subscribe(EmailBouncedEvent, async (event) => { domainEvents.subscribe(EmailBouncedEvent, async (event) => {
try { await this.handlePermanentFailed(event);
await this.handlePermanentFailed(event);
} catch (e) {
logging.error(e);
}
}); });
domainEvents.subscribe(EmailTemporaryBouncedEvent, async (event) => { domainEvents.subscribe(EmailTemporaryBouncedEvent, async (event) => {
try { await this.handleTemporaryFailed(event);
await this.handleTemporaryFailed(event);
} catch (e) {
logging.error(e);
}
}); });
domainEvents.subscribe(EmailUnsubscribedEvent, async (event) => { domainEvents.subscribe(EmailUnsubscribedEvent, async (event) => {
try { await this.handleUnsubscribed(event);
await this.handleUnsubscribed(event);
} catch (e) {
logging.error(e);
}
}); });
domainEvents.subscribe(SpamComplaintEvent, async (event) => { domainEvents.subscribe(SpamComplaintEvent, async (event) => {
try { await this.handleComplained(event);
await this.handleComplained(event);
} catch (e) {
logging.error(e);
}
}); });
} }

View File

@ -2,27 +2,20 @@ const EmailEventStorage = require('../lib/email-event-storage');
const {EmailDeliveredEvent, EmailOpenedEvent, EmailBouncedEvent, EmailTemporaryBouncedEvent, EmailUnsubscribedEvent, SpamComplaintEvent} = require('@tryghost/email-events'); const {EmailDeliveredEvent, EmailOpenedEvent, EmailBouncedEvent, EmailTemporaryBouncedEvent, EmailUnsubscribedEvent, SpamComplaintEvent} = require('@tryghost/email-events');
const sinon = require('sinon'); const sinon = require('sinon');
const assert = require('assert'); const assert = require('assert');
const logging = require('@tryghost/logging');
const {createDb} = require('./utils');
function stubDb() { describe('Email Event Storage', function () {
const db = { let logError;
knex: function () {
return this; beforeEach(function () {
}, logError = sinon.stub(logging, 'error');
where: function () { });
return this;
}, afterEach(function () {
whereNull: function () { sinon.restore();
return this; });
},
update: sinon.stub().resolves()
};
db.knex.raw = function () {
return this;
};
return db;
}
describe('Email event storage', function () {
describe('Constructor', function () { describe('Constructor', function () {
it('doesn\'t throw', function () { it('doesn\'t throw', function () {
new EmailEventStorage({}); new EmailEventStorage({});
@ -37,14 +30,15 @@ describe('Email event storage', function () {
email: 'example@example.com', email: 'example@example.com',
memberId: '123', memberId: '123',
emailId: '456', emailId: '456',
emailRecipientId: '789' emailRecipientId: '789',
}, new Date(0))); timestamp: new Date(0)
}));
} }
} }
}; };
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe'); const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
const db = stubDb(); const db = createDb();
const eventHandler = new EmailEventStorage({db}); const eventHandler = new EmailEventStorage({db});
eventHandler.listen(DomainEvents); eventHandler.listen(DomainEvents);
sinon.assert.callCount(subscribeSpy, 6); sinon.assert.callCount(subscribeSpy, 6);
@ -60,14 +54,15 @@ describe('Email event storage', function () {
email: 'example@example.com', email: 'example@example.com',
memberId: '123', memberId: '123',
emailId: '456', emailId: '456',
emailRecipientId: '789' emailRecipientId: '789',
}, new Date(0))); timestamp: new Date(0)
}));
} }
} }
}; };
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe'); const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
const db = stubDb(); const db = createDb();
const eventHandler = new EmailEventStorage({db}); const eventHandler = new EmailEventStorage({db});
eventHandler.listen(DomainEvents); eventHandler.listen(DomainEvents);
sinon.assert.callCount(subscribeSpy, 6); sinon.assert.callCount(subscribeSpy, 6);
@ -90,14 +85,15 @@ describe('Email event storage', function () {
message: 'test', message: 'test',
code: 500, code: 500,
enhancedCode: '5.5.5' enhancedCode: '5.5.5'
} },
}, new Date(0))); timestamp: new Date(0)
}));
} }
} }
}; };
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe'); const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
const db = stubDb(); const db = createDb();
const existing = { const existing = {
id: 1, id: 1,
get: (key) => { get: (key) => {
@ -146,14 +142,15 @@ describe('Email event storage', function () {
message: 'test', message: 'test',
code: 500, code: 500,
enhancedCode: '5.5.5' enhancedCode: '5.5.5'
} },
}, new Date(0))); timestamp: new Date(0)
}));
} }
} }
}; };
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe'); const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
const db = stubDb(); const db = createDb();
const EmailRecipientFailure = { const EmailRecipientFailure = {
transaction: async function (callback) { transaction: async function (callback) {
return await callback(1); return await callback(1);
@ -176,6 +173,37 @@ describe('Email event storage', function () {
assert(EmailRecipientFailure.add.calledOnce); assert(EmailRecipientFailure.add.calledOnce);
}); });
it('Handles email permanent bounce event without error data', async function () {
let waitPromise;
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 eventHandler = new EmailEventStorage({
db,
models: {}
});
eventHandler.listen(DomainEvents);
sinon.assert.callCount(subscribeSpy, 6);
await waitPromise;
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; let waitPromise;
@ -191,14 +219,15 @@ describe('Email event storage', function () {
message: 'test', message: 'test',
code: 500, code: 500,
enhancedCode: '5.5.5' enhancedCode: '5.5.5'
} },
}, new Date(0))); timestamp: new Date(0)
}));
} }
} }
}; };
const subscribeSpy = sinon.spy(DomainEvents, 'subscribe'); const subscribeSpy = sinon.spy(DomainEvents, 'subscribe');
const db = stubDb(); const db = createDb();
const existing = { const existing = {
id: 1, id: 1,
get: (key) => { get: (key) => {
@ -247,9 +276,10 @@ describe('Email event storage', function () {
error: { error: {
message: 'test', message: 'test',
code: 500, code: 500,
enhancedCode: '5.5.5' enhancedCode: null
} },
}, new Date(0))); timestamp: new Date(0)
}));
} }
} }
}; };
@ -285,6 +315,59 @@ describe('Email event storage', function () {
assert(existing.save.calledOnce); assert(existing.save.calledOnce);
}); });
it('Handles email temporary bounce events with skipped update', async function () {
let waitPromise;
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 = {
id: 1,
get: (key) => {
if (key === 'severity') {
return 'temporary';
}
if (key === 'failed_at') {
return new Date(5);
}
},
save: sinon.stub().resolves()
};
const EmailRecipientFailure = {
transaction: async function (callback) {
return await callback(1);
},
findOne: sinon.stub().resolves(existing)
};
const eventHandler = new EmailEventStorage({
models: {
EmailRecipientFailure
}
});
eventHandler.listen(DomainEvents);
sinon.assert.callCount(subscribeSpy, 6);
await waitPromise;
assert(existing.save.notCalled);
});
it('Handles unsubscribe', async function () { it('Handles unsubscribe', async function () {
let waitPromise; let waitPromise;
@ -294,8 +377,9 @@ describe('Email event storage', function () {
waitPromise = handler(EmailUnsubscribedEvent.create({ waitPromise = handler(EmailUnsubscribedEvent.create({
email: 'example@example.com', email: 'example@example.com',
memberId: '123', memberId: '123',
emailId: '456' emailId: '456',
}, new Date(0))); timestamp: new Date(0)
}));
} }
} }
}; };
@ -324,8 +408,9 @@ describe('Email event storage', function () {
waitPromise = handler(SpamComplaintEvent.create({ waitPromise = handler(SpamComplaintEvent.create({
email: 'example@example.com', email: 'example@example.com',
memberId: '123', memberId: '123',
emailId: '456' emailId: '456',
}, new Date(0))); timestamp: new Date(0)
}));
} }
} }
}; };
@ -345,4 +430,70 @@ describe('Email event storage', function () {
await waitPromise; await waitPromise;
assert(EmailSpamComplaintEvent.add.calledOnce); assert(EmailSpamComplaintEvent.add.calledOnce);
}); });
it('Handles duplicate complaints', async function () {
let waitPromise;
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 = {
add: sinon.stub().rejects({code: 'ER_DUP_ENTRY'})
};
const eventHandler = new EmailEventStorage({
models: {
EmailSpamComplaintEvent
}
});
eventHandler.listen(DomainEvents);
sinon.assert.callCount(subscribeSpy, 6);
await waitPromise;
assert(EmailSpamComplaintEvent.add.calledOnce);
assert(!logError.calledOnce);
});
it('Handles logging failed complaint storage', async function () {
let waitPromise;
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 = {
add: sinon.stub().rejects(new Error('Some database error'))
};
const eventHandler = new EmailEventStorage({
models: {
EmailSpamComplaintEvent
}
});
eventHandler.listen(DomainEvents);
sinon.assert.callCount(subscribeSpy, 6);
await waitPromise;
assert(EmailSpamComplaintEvent.add.calledOnce);
assert(logError.calledOnce);
});
}); });

View File

@ -59,7 +59,7 @@ const createModelClass = (options = {}) => {
}; };
}; };
const createDb = ({first, all}) => { const createDb = ({first, all} = {}) => {
let a = all; let a = all;
const db = { const db = {
knex: function () { knex: function () {