mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-27 21:03:29 +03:00
819d0d884c
fixes https://github.com/TryGhost/Team/issues/2366 refs https://ghost.slack.com/archives/C02G9E68C/p1670232405014209 Probem described in issue. In the old MEGA flow: - The `email_verification_required` check is now repeated inside the job In the new email service flow: - The `email_verification_required` is now checked (didn't happen before) - When generating the email batch recipients, we only include members that were created before the email was created. That way it is impossible to avoid limit checks by inserting new members between creating an email and sending an email. - We don't need to repeat the check inside the job because of the above changes Improved handling of large imports: - When checking `email_verification_required`, we now also check if the import threshold is reached (a new method is introduced in vertificationTrigger specifically for this usage). If it is, we start the verification progress. This is required for long running imports that only check the verification threshold at the very end. - This change increases the concurrency of fastq to 3 (refs https://ghost.slack.com/archives/C02G9E68C/p1670232405014209). So when running a long import, it is now possible to send emails without having to wait for the import. Above change makes sure it is not possible to get around the verification limits. Refactoring: - Removed the need to use `updateVerificationTrigger` by making thresholds getters instead of fixed variables. - Improved awaiting of members import job in regression test
430 lines
15 KiB
JavaScript
430 lines
15 KiB
JavaScript
// Switch these lines once there are useful utils
|
|
// const testUtils = require('./utils');
|
|
const sinon = require('sinon');
|
|
const assert = require('assert');
|
|
require('./utils');
|
|
const VerificationTrigger = require('../index');
|
|
const DomainEvents = require('@tryghost/domain-events');
|
|
const {MemberCreatedEvent} = require('@tryghost/member-events');
|
|
|
|
describe('Import threshold', function () {
|
|
it('Creates a threshold based on config', async function () {
|
|
const trigger = new VerificationTrigger({
|
|
getImportTriggerThreshold: () => 2,
|
|
membersStats: {
|
|
getTotalMembers: async () => 1
|
|
}
|
|
});
|
|
|
|
const result = await trigger.getImportThreshold();
|
|
result.should.eql(2);
|
|
});
|
|
|
|
it('Increases the import threshold to the number of members', async function () {
|
|
const trigger = new VerificationTrigger({
|
|
getImportTriggerThreshold: () => 2,
|
|
membersStats: {
|
|
getTotalMembers: async () => 3
|
|
}
|
|
});
|
|
|
|
const result = await trigger.getImportThreshold();
|
|
result.should.eql(3);
|
|
});
|
|
|
|
it('Does not check members count when config threshold is infinite', async function () {
|
|
const membersStub = sinon.stub().resolves(null);
|
|
const trigger = new VerificationTrigger({
|
|
getImportTriggerThreshold: () => Infinity,
|
|
memberStats: {
|
|
getTotalMembers: membersStub
|
|
}
|
|
});
|
|
|
|
const result = await trigger.getImportThreshold();
|
|
result.should.eql(Infinity);
|
|
membersStub.callCount.should.eql(0);
|
|
});
|
|
});
|
|
|
|
describe('Email verification flow', function () {
|
|
it('Triggers verification process', async function () {
|
|
const emailStub = sinon.stub().resolves(null);
|
|
const settingsStub = sinon.stub().resolves(null);
|
|
const trigger = new VerificationTrigger({
|
|
Settings: {
|
|
edit: settingsStub
|
|
},
|
|
isVerified: () => false,
|
|
isVerificationRequired: () => false,
|
|
sendVerificationEmail: emailStub
|
|
});
|
|
|
|
const result = await trigger._startVerificationProcess({
|
|
amount: 10,
|
|
throwOnTrigger: false
|
|
});
|
|
|
|
result.needsVerification.should.eql(true);
|
|
emailStub.callCount.should.eql(1);
|
|
settingsStub.callCount.should.eql(1);
|
|
});
|
|
|
|
it('Does not trigger verification when already verified', async function () {
|
|
const emailStub = sinon.stub().resolves(null);
|
|
const settingsStub = sinon.stub().resolves(null);
|
|
const trigger = new VerificationTrigger({
|
|
Settings: {
|
|
edit: settingsStub
|
|
},
|
|
isVerified: () => true,
|
|
isVerificationRequired: () => false,
|
|
sendVerificationEmail: emailStub
|
|
});
|
|
|
|
const result = await trigger._startVerificationProcess({
|
|
amount: 10,
|
|
throwOnTrigger: false
|
|
});
|
|
|
|
result.needsVerification.should.eql(false);
|
|
emailStub.callCount.should.eql(0);
|
|
settingsStub.callCount.should.eql(0);
|
|
});
|
|
|
|
it('Does not trigger verification when already in progress', async function () {
|
|
const emailStub = sinon.stub().resolves(null);
|
|
const settingsStub = sinon.stub().resolves(null);
|
|
const trigger = new VerificationTrigger({
|
|
Settings: {
|
|
edit: settingsStub
|
|
},
|
|
isVerified: () => false,
|
|
isVerificationRequired: () => true,
|
|
sendVerificationEmail: emailStub
|
|
});
|
|
|
|
const result = await trigger._startVerificationProcess({
|
|
amount: 10,
|
|
throwOnTrigger: false
|
|
});
|
|
|
|
result.needsVerification.should.eql(false);
|
|
emailStub.callCount.should.eql(0);
|
|
settingsStub.callCount.should.eql(0);
|
|
});
|
|
|
|
it('Throws when `throwsOnTrigger` is true', async function () {
|
|
const emailStub = sinon.stub().resolves(null);
|
|
const settingsStub = sinon.stub().resolves(null);
|
|
const trigger = new VerificationTrigger({
|
|
Settings: {
|
|
edit: settingsStub
|
|
},
|
|
isVerified: () => false,
|
|
isVerificationRequired: () => false,
|
|
sendVerificationEmail: emailStub
|
|
});
|
|
|
|
await trigger._startVerificationProcess({
|
|
amount: 10,
|
|
throwOnTrigger: true
|
|
}).should.be.rejected();
|
|
});
|
|
|
|
it('Sends a message containing the number of members imported', async function () {
|
|
const emailStub = sinon.stub().resolves(null);
|
|
const settingsStub = sinon.stub().resolves(null);
|
|
const trigger = new VerificationTrigger({
|
|
Settings: {
|
|
edit: settingsStub
|
|
},
|
|
isVerified: () => false,
|
|
isVerificationRequired: () => false,
|
|
sendVerificationEmail: emailStub
|
|
});
|
|
|
|
await trigger._startVerificationProcess({
|
|
amount: 10,
|
|
throwOnTrigger: false
|
|
});
|
|
|
|
emailStub.lastCall.firstArg.should.eql({
|
|
subject: 'Email needs verification',
|
|
message: 'Email verification needed for site: {siteUrl}, has imported: {amountTriggered} members in the last 30 days.',
|
|
amountTriggered: 10
|
|
});
|
|
});
|
|
|
|
it('Triggers when a number of API events are dispatched', async function () {
|
|
const emailStub = sinon.stub().resolves(null);
|
|
const settingsStub = sinon.stub().resolves(null);
|
|
const eventStub = sinon.stub().resolves({
|
|
meta: {
|
|
pagination: {
|
|
total: 10
|
|
}
|
|
}
|
|
});
|
|
|
|
new VerificationTrigger({
|
|
getApiTriggerThreshold: () => 2,
|
|
Settings: {
|
|
edit: settingsStub
|
|
},
|
|
isVerified: () => false,
|
|
isVerificationRequired: () => false,
|
|
sendVerificationEmail: emailStub,
|
|
eventRepository: {
|
|
getSignupEvents: eventStub
|
|
}
|
|
});
|
|
|
|
DomainEvents.dispatch(MemberCreatedEvent.create({
|
|
memberId: 'hello!',
|
|
source: 'api'
|
|
}, new Date()));
|
|
|
|
eventStub.callCount.should.eql(1);
|
|
eventStub.lastCall.lastArg.should.have.property('source');
|
|
eventStub.lastCall.lastArg.source.should.eql('api');
|
|
eventStub.lastCall.lastArg.should.have.property('created_at');
|
|
eventStub.lastCall.lastArg.created_at.should.have.property('$gt');
|
|
eventStub.lastCall.lastArg.created_at.$gt.should.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
|
|
});
|
|
|
|
it('Triggers when a number of members are imported', async function () {
|
|
const emailStub = sinon.stub().resolves(null);
|
|
const settingsStub = sinon.stub().resolves(null);
|
|
const eventStub = sinon.stub().resolves({
|
|
meta: {
|
|
pagination: {
|
|
total: 10
|
|
}
|
|
}
|
|
});
|
|
|
|
const trigger = new VerificationTrigger({
|
|
getImportTriggerThreshold: () => 2,
|
|
Settings: {
|
|
edit: settingsStub
|
|
},
|
|
membersStats: {
|
|
getTotalMembers: () => 15
|
|
},
|
|
isVerified: () => false,
|
|
isVerificationRequired: () => false,
|
|
sendVerificationEmail: emailStub,
|
|
eventRepository: {
|
|
getSignupEvents: eventStub
|
|
}
|
|
});
|
|
|
|
await trigger.testImportThreshold();
|
|
|
|
eventStub.callCount.should.eql(1);
|
|
eventStub.lastCall.lastArg.should.have.property('source');
|
|
eventStub.lastCall.lastArg.source.should.eql('import');
|
|
eventStub.lastCall.lastArg.should.have.property('created_at');
|
|
eventStub.lastCall.lastArg.created_at.should.have.property('$gt');
|
|
eventStub.lastCall.lastArg.created_at.$gt.should.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
|
|
|
|
emailStub.callCount.should.eql(1);
|
|
emailStub.lastCall.firstArg.should.eql({
|
|
subject: 'Email needs verification',
|
|
message: 'Email verification needed for site: {siteUrl}, has imported: {amountTriggered} members in the last 30 days.',
|
|
amountTriggered: 10
|
|
});
|
|
});
|
|
|
|
it('checkVerificationRequired also checks import', async function () {
|
|
const emailStub = sinon.stub().resolves(null);
|
|
let isVerificationRequired = false;
|
|
const isVerificationRequiredStub = sinon.stub().callsFake(() => {
|
|
return isVerificationRequired;
|
|
});
|
|
const settingsStub = sinon.stub().callsFake(() => {
|
|
isVerificationRequired = true;
|
|
return Promise.resolve();
|
|
});
|
|
const eventStub = sinon.stub().resolves({
|
|
meta: {
|
|
pagination: {
|
|
total: 10
|
|
}
|
|
}
|
|
});
|
|
|
|
const trigger = new VerificationTrigger({
|
|
getImportTriggerThreshold: () => 2,
|
|
Settings: {
|
|
edit: settingsStub
|
|
},
|
|
membersStats: {
|
|
getTotalMembers: () => 15
|
|
},
|
|
isVerified: () => false,
|
|
isVerificationRequired: isVerificationRequiredStub,
|
|
sendVerificationEmail: emailStub,
|
|
eventRepository: {
|
|
getSignupEvents: eventStub
|
|
}
|
|
});
|
|
|
|
assert.equal(await trigger.checkVerificationRequired(), true);
|
|
sinon.assert.calledOnce(emailStub);
|
|
});
|
|
|
|
it('testImportThreshold does not calculate anything if already verified', async function () {
|
|
const trigger = new VerificationTrigger({
|
|
getImportTriggerThreshold: () => 2,
|
|
isVerified: () => true
|
|
});
|
|
|
|
assert.equal(await trigger.testImportThreshold(), undefined);
|
|
});
|
|
|
|
it('testImportThreshold does not calculate anything if already pending', async function () {
|
|
const trigger = new VerificationTrigger({
|
|
getImportTriggerThreshold: () => 2,
|
|
isVerified: () => false,
|
|
isVerificationRequired: () => true
|
|
});
|
|
|
|
assert.equal(await trigger.testImportThreshold(), undefined);
|
|
});
|
|
|
|
it('Triggers when a number of members are added from Admin', async function () {
|
|
const emailStub = sinon.stub().resolves(null);
|
|
const settingsStub = sinon.stub().resolves(null);
|
|
const eventStub = sinon.stub().resolves({
|
|
meta: {
|
|
pagination: {
|
|
total: 10
|
|
}
|
|
}
|
|
});
|
|
|
|
const trigger = new VerificationTrigger({
|
|
getAdminTriggerThreshold: () => 2,
|
|
Settings: {
|
|
edit: settingsStub
|
|
},
|
|
membersStats: {
|
|
getTotalMembers: () => 15
|
|
},
|
|
isVerified: () => false,
|
|
isVerificationRequired: () => false,
|
|
sendVerificationEmail: emailStub,
|
|
eventRepository: {
|
|
getSignupEvents: eventStub
|
|
}
|
|
});
|
|
|
|
await trigger._handleMemberCreatedEvent({
|
|
data: {
|
|
source: 'admin'
|
|
}
|
|
});
|
|
|
|
eventStub.callCount.should.eql(1);
|
|
eventStub.lastCall.lastArg.should.have.property('source');
|
|
eventStub.lastCall.lastArg.source.should.eql('admin');
|
|
eventStub.lastCall.lastArg.should.have.property('created_at');
|
|
eventStub.lastCall.lastArg.created_at.should.have.property('$gt');
|
|
eventStub.lastCall.lastArg.created_at.$gt.should.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
|
|
|
|
emailStub.callCount.should.eql(1);
|
|
emailStub.lastCall.firstArg.should.eql({
|
|
subject: 'Email needs verification',
|
|
message: 'Email verification needed for site: {siteUrl} has added: {amountTriggered} members through the Admin client in the last 30 days.',
|
|
amountTriggered: 10
|
|
});
|
|
});
|
|
|
|
it('Triggers when a number of members are added from API', async function () {
|
|
const emailStub = sinon.stub().resolves(null);
|
|
const settingsStub = sinon.stub().resolves(null);
|
|
const eventStub = sinon.stub().resolves({
|
|
meta: {
|
|
pagination: {
|
|
total: 10
|
|
}
|
|
}
|
|
});
|
|
|
|
const trigger = new VerificationTrigger({
|
|
getAdminTriggerThreshold: () => 2,
|
|
getApiTriggerThreshold: () => 2,
|
|
Settings: {
|
|
edit: settingsStub
|
|
},
|
|
membersStats: {
|
|
getTotalMembers: () => 15
|
|
},
|
|
isVerified: () => false,
|
|
isVerificationRequired: () => false,
|
|
sendVerificationEmail: emailStub,
|
|
eventRepository: {
|
|
getSignupEvents: eventStub
|
|
}
|
|
});
|
|
|
|
await trigger._handleMemberCreatedEvent({
|
|
data: {
|
|
source: 'api'
|
|
}
|
|
});
|
|
|
|
eventStub.callCount.should.eql(1);
|
|
eventStub.lastCall.lastArg.should.have.property('source');
|
|
eventStub.lastCall.lastArg.source.should.eql('api');
|
|
eventStub.lastCall.lastArg.should.have.property('created_at');
|
|
eventStub.lastCall.lastArg.created_at.should.have.property('$gt');
|
|
eventStub.lastCall.lastArg.created_at.$gt.should.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
|
|
|
|
emailStub.callCount.should.eql(1);
|
|
emailStub.lastCall.firstArg.should.eql({
|
|
subject: 'Email needs verification',
|
|
message: 'Email verification needed for site: {siteUrl} has added: {amountTriggered} members through the API in the last 30 days.',
|
|
amountTriggered: 10
|
|
});
|
|
});
|
|
|
|
it('Does not fetch events and trigger when threshold is Infinity', async function () {
|
|
const emailStub = sinon.stub().resolves(null);
|
|
const settingsStub = sinon.stub().resolves(null);
|
|
const eventStub = sinon.stub().resolves({
|
|
meta: {
|
|
pagination: {
|
|
total: 10
|
|
}
|
|
}
|
|
});
|
|
|
|
const trigger = new VerificationTrigger({
|
|
getImportTriggerThreshold: () => Infinity,
|
|
Settings: {
|
|
edit: settingsStub
|
|
},
|
|
membersStats: {
|
|
getTotalMembers: () => 15
|
|
},
|
|
isVerified: () => false,
|
|
isVerificationRequired: () => false,
|
|
sendVerificationEmail: emailStub,
|
|
eventRepository: {
|
|
getSignupEvents: eventStub
|
|
}
|
|
});
|
|
|
|
await trigger.testImportThreshold();
|
|
|
|
// We shouldn't be fetching the events if the threshold is Infinity
|
|
eventStub.callCount.should.eql(0);
|
|
|
|
// We shouldn't be sending emails if the threshold is Infinity
|
|
emailStub.callCount.should.eql(0);
|
|
});
|
|
});
|