17ec1e8937
ref GRO-54 fixes GRO-63 fixes GRO-62 fixes GRO-69 When the config `hostSettings:managedEmail:enabled` is enabled, or the new flag (`newEmailAddresses`) is enabled for self-hosters, we'll start to check the from addresses of all outgoing emails more strictly. - Current flow: nothing changes if the managedEmail config is not set or the `newEmailAddresses` feature flag is not set - When managedEmail is enabled: never allow to send an email from any chosen email. We always use `mail.from` for all outgoing emails. Custom addresses should be set as replyTo instead. Changing the newsletter sender_email is not allowed anymore (and ignored if it is set). - When managedEmail is enabled with a custom sending domain: if a from address doesn't match the sending domain, we'll default to mail.from and use the original as a replyTo if appropriate and only when no other replyTo was set. A newsletter sender email addresss can only be set to an email address on this domain. - When `newEmailAddresses` is enabled: self hosters are free to set all email addresses to whatever they want, without verification. In addition to that, we stop making up our own email addresses and send from `mail.from` by default instead of generating a `noreply`+ `@` + `sitedomain.com` address A more in depth example of all cases can be seen in `ghost/core/test/integration/services/email-addresses.test.js` Includes lots of new E2E tests for most new situations. Apart from that, all email snapshots are changed because the from and replyTo addresses are now included in snapshots (so we can see unexpected changes in the future). Dropped test coverage requirement, because tests were failing coverage locally, but not in CI Fixed settings test that set the site title to an array - bug tracked in GRO-68
312 lines
8.6 KiB
JavaScript
312 lines
8.6 KiB
JavaScript
const errors = require('@tryghost/errors');
|
|
const sinon = require('sinon');
|
|
const assert = require('assert/strict');
|
|
const nock = require('nock');
|
|
const MailgunClient = require('@tryghost/mailgun-client');
|
|
|
|
// Helper services
|
|
const configUtils = require('./configUtils');
|
|
const WebhookMockReceiver = require('@tryghost/webhook-mock-receiver');
|
|
const EmailMockReceiver = require('@tryghost/email-mock-receiver');
|
|
const {snapshotManager} = require('@tryghost/express-test').snapshot;
|
|
|
|
let mocks = {};
|
|
let emailCount = 0;
|
|
|
|
// Mockable services
|
|
const mailService = require('../../core/server/services/mail/index');
|
|
const originalMailServiceSendMail = mailService.GhostMailer.prototype.sendMail;
|
|
const labs = require('../../core/shared/labs');
|
|
const events = require('../../core/server/lib/common/events');
|
|
const settingsCache = require('../../core/shared/settings-cache');
|
|
const dns = require('dns');
|
|
const dnsPromises = dns.promises;
|
|
const StripeMocker = require('./stripe-mocker');
|
|
|
|
let fakedLabsFlags = {};
|
|
let allowedNetworkDomains = [];
|
|
const originalLabsIsSet = labs.isSet;
|
|
const stripeMocker = new StripeMocker();
|
|
|
|
/**
|
|
* Stripe Mocks
|
|
*/
|
|
|
|
const disableStripe = async () => {
|
|
// This must be required _after_ startGhost has been called, because the models will
|
|
// not have been loaded otherwise. Consider moving the dependency injection of models
|
|
// into the init method of the Stripe service.
|
|
const stripeService = require('../../core/server/services/stripe');
|
|
await stripeService.disconnect();
|
|
};
|
|
|
|
const disableNetwork = () => {
|
|
nock.disableNetConnect();
|
|
|
|
// externalRequest does dns lookup; stub to make sure we don't fail with fake domain names
|
|
if (!dnsPromises.lookup.restore) {
|
|
sinon.stub(dnsPromises, 'lookup').callsFake(() => {
|
|
return Promise.resolve({address: '123.123.123.123', family: 4});
|
|
});
|
|
}
|
|
|
|
if (!dns.resolveMx.restore) {
|
|
// without this, Node will try and resolve the domain name but local DNS
|
|
// resolvers can take a while to timeout, which causes the tests to timeout
|
|
// `nodemailer-direct-transport` calls `dns.resolveMx`, so if we stub that
|
|
// function and return an empty array, we can avoid any real DNS lookups
|
|
sinon.stub(dns, 'resolveMx').yields(null, []);
|
|
}
|
|
|
|
// Allow localhost
|
|
// Multiple enableNetConnect with different hosts overwrite each other, so we need to add one and use the allowedNetworkDomains variable
|
|
nock.enableNetConnect((host) => {
|
|
if (host.includes('127.0.0.1')) {
|
|
return true;
|
|
}
|
|
for (const h of allowedNetworkDomains) {
|
|
if (host.includes(h)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
};
|
|
|
|
const allowStripe = () => {
|
|
disableNetwork();
|
|
allowedNetworkDomains.push('stripe.com');
|
|
};
|
|
|
|
const mockStripe = () => {
|
|
disableNetwork();
|
|
stripeMocker.reset();
|
|
stripeMocker.stub();
|
|
};
|
|
|
|
const mockSlack = () => {
|
|
disableNetwork();
|
|
|
|
nock(/hooks.slack.com/)
|
|
.persist()
|
|
.post('/')
|
|
.reply(200, 'ok');
|
|
};
|
|
|
|
/**
|
|
* Email Mocks & Assertions
|
|
*/
|
|
|
|
/**
|
|
* @param {String|Object} response
|
|
*/
|
|
const mockMail = (response = 'Mail is disabled') => {
|
|
const mockMailReceiver = new EmailMockReceiver({
|
|
snapshotManager: snapshotManager,
|
|
sendResponse: response
|
|
});
|
|
|
|
mailService.GhostMailer.prototype.sendMail = mockMailReceiver.send.bind(mockMailReceiver);
|
|
mocks.mail = sinon.spy(mailService.GhostMailer.prototype, 'sendMail');
|
|
mocks.mockMailReceiver = mockMailReceiver;
|
|
|
|
return mockMailReceiver;
|
|
};
|
|
|
|
/**
|
|
* A reference to the send method when MailGun is mocked (required for some tests)
|
|
*/
|
|
let mailgunCreateMessageStub;
|
|
|
|
const mockMailgun = (customStubbedSend) => {
|
|
mockSetting('mailgun_api_key', 'test');
|
|
mockSetting('mailgun_domain', 'example.com');
|
|
mockSetting('mailgun_base_url', 'test');
|
|
|
|
mailgunCreateMessageStub = customStubbedSend ? sinon.stub().callsFake(customStubbedSend) : sinon.fake.resolves({
|
|
id: `<${new Date().getTime()}.${0}.5817@samples.mailgun.org>`
|
|
});
|
|
|
|
// We need to stub the Mailgun client before starting Ghost
|
|
sinon.stub(MailgunClient.prototype, 'getInstance').returns({
|
|
// @ts-ignore
|
|
messages: {
|
|
create: async function () {
|
|
return await mailgunCreateMessageStub.call(this, ...arguments);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
const mockWebhookRequests = () => {
|
|
mocks.webhookMockReceiver = new WebhookMockReceiver({snapshotManager});
|
|
|
|
return mocks.webhookMockReceiver;
|
|
};
|
|
|
|
/**
|
|
* @deprecated use emailMockReceiver.assertSentEmailCount(count) instead
|
|
* @param {Number} count number of emails sent
|
|
*/
|
|
const sentEmailCount = (count) => {
|
|
if (!mocks.mail) {
|
|
throw new errors.IncorrectUsageError({
|
|
message: 'Cannot assert on mail when mail has not been mocked'
|
|
});
|
|
}
|
|
|
|
mocks.mockMailReceiver.assertSentEmailCount(count);
|
|
};
|
|
|
|
const sentEmail = (matchers) => {
|
|
if (!mocks.mail) {
|
|
throw new errors.IncorrectUsageError({
|
|
message: 'Cannot assert on mail when mail has not been mocked'
|
|
});
|
|
}
|
|
|
|
let spyCall = mocks.mail.getCall(emailCount);
|
|
|
|
assert.notEqual(spyCall, null, 'Expected at least ' + (emailCount + 1) + ' emails sent.');
|
|
|
|
// We increment here so that the messaging has an index of 1, whilst getting the call has an index of 0
|
|
emailCount += 1;
|
|
|
|
sinon.assert.called(mocks.mail);
|
|
|
|
Object.keys(matchers).forEach((key) => {
|
|
let value = matchers[key];
|
|
|
|
// We use assert, rather than sinon.assert.calledWith, as we end up with much better error messaging
|
|
assert.notEqual(spyCall.args[0][key], undefined, `Expected email to have property ${key}`);
|
|
|
|
if (value instanceof RegExp) {
|
|
assert.match(spyCall.args[0][key], value, `Expected Email ${emailCount} to have ${key} that matches ${value}, got ${spyCall.args[0][key]}`);
|
|
return;
|
|
}
|
|
|
|
assert.equal(spyCall.args[0][key], value, `Expected Email ${emailCount} to have ${key} of ${value}`);
|
|
});
|
|
|
|
return spyCall.args[0];
|
|
};
|
|
|
|
/**
|
|
* Events Mocks & Assertions
|
|
*/
|
|
|
|
const mockEvents = () => {
|
|
mocks.events = sinon.stub(events, 'emit');
|
|
};
|
|
|
|
const emittedEvent = (name) => {
|
|
sinon.assert.calledWith(mocks.events, name);
|
|
};
|
|
|
|
/**
|
|
* Settings Mocks
|
|
*/
|
|
|
|
let fakedSettings = {};
|
|
const originalSettingsGetter = settingsCache.get;
|
|
|
|
const fakeSettingsGetter = (setting) => {
|
|
if (fakedSettings.hasOwnProperty(setting)) {
|
|
return fakedSettings[setting];
|
|
}
|
|
|
|
return originalSettingsGetter(setting);
|
|
};
|
|
|
|
const mockSetting = (key, value) => {
|
|
if (!mocks.settings) {
|
|
mocks.settings = sinon.stub(settingsCache, 'get').callsFake(fakeSettingsGetter);
|
|
}
|
|
|
|
fakedSettings[key] = value;
|
|
};
|
|
|
|
/**
|
|
* Labs Mocks
|
|
*/
|
|
|
|
const fakeLabsIsSet = (flag) => {
|
|
if (fakedLabsFlags.hasOwnProperty(flag)) {
|
|
return fakedLabsFlags[flag];
|
|
}
|
|
|
|
return originalLabsIsSet(flag);
|
|
};
|
|
|
|
const mockLabsEnabled = (flag, alpha = true) => {
|
|
// We assume we should enable alpha experiments unless explicitly told not to!
|
|
if (!alpha) {
|
|
configUtils.set('enableDeveloperExperiments', true);
|
|
}
|
|
|
|
if (!mocks.labs) {
|
|
mocks.labs = sinon.stub(labs, 'isSet').callsFake(fakeLabsIsSet);
|
|
}
|
|
|
|
fakedLabsFlags[flag] = true;
|
|
};
|
|
|
|
const mockLabsDisabled = (flag, alpha = true) => {
|
|
// We assume we should enable alpha experiments unless explicitly told not to!
|
|
if (!alpha) {
|
|
configUtils.set('enableDeveloperExperiments', true);
|
|
}
|
|
|
|
if (!mocks.labs) {
|
|
mocks.labs = sinon.stub(labs, 'isSet').callsFake(fakeLabsIsSet);
|
|
}
|
|
|
|
fakedLabsFlags[flag] = false;
|
|
};
|
|
|
|
const restore = () => {
|
|
// eslint-disable-next-line no-console
|
|
configUtils.restore().catch(console.error);
|
|
sinon.restore();
|
|
mocks = {};
|
|
fakedLabsFlags = {};
|
|
fakedSettings = {};
|
|
emailCount = 0;
|
|
allowedNetworkDomains = [];
|
|
nock.cleanAll();
|
|
nock.enableNetConnect();
|
|
stripeMocker.reset();
|
|
|
|
if (mocks.webhookMockReceiver) {
|
|
mocks.webhookMockReceiver.reset();
|
|
}
|
|
|
|
mailService.GhostMailer.prototype.sendMail = originalMailServiceSendMail;
|
|
|
|
// Disable network again after restoring sinon
|
|
disableNetwork();
|
|
};
|
|
|
|
module.exports = {
|
|
mockEvents,
|
|
mockMail,
|
|
disableStripe,
|
|
mockStripe,
|
|
mockSlack,
|
|
allowStripe,
|
|
mockMailgun,
|
|
mockLabsEnabled,
|
|
mockLabsDisabled,
|
|
mockWebhookRequests,
|
|
mockSetting,
|
|
disableNetwork,
|
|
restore,
|
|
stripeMocker,
|
|
assert: {
|
|
sentEmailCount,
|
|
sentEmail,
|
|
emittedEvent
|
|
},
|
|
getMailgunCreateMessageStub: () => mailgunCreateMessageStub
|
|
};
|