Ghost/ghost/core/test/utils/e2e-framework-mock-manager.js
Hannah Wolfe 6161f94910
Updated to use assert/strict everywhere (#17047)
refs: https://github.com/TryGhost/Toolbox/issues/595

We're rolling out new rules around the node assert library, the first of which is enforcing the use of assert/strict. This means we don't need to use the strict version of methods, as the standard version will work that way by default.

This caught some gotchas in our existing usage of assert where the lack of strict mode had unexpected results:
- Url matching needs to be done on `url.href` see aa58b354a4
- Null and undefined are not the same thing,  there were a few cases of this being confused
- Particularly questionable changes in [PostExporter tests](c1a468744b) tracked [here](https://github.com/TryGhost/Team/issues/3505).
- A typo see eaac9c293a

Moving forward, using assert strict should help us to catch unexpected behaviour, particularly around nulls and undefineds during implementation.
2023-06-21 09:56:59 +01:00

305 lines
8.3 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 originalMailServiceSend = mailService.GhostMailer.prototype.send;
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.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.send = mockMailReceiver.send.bind(mockMailReceiver);
mocks.mail = sinon.spy(mailService.GhostMailer.prototype, 'send');
mocks.mockMailReceiver = mockMailReceiver;
return mockMailReceiver;
};
const mockMailgun = (customStubbedSend) => {
mockSetting('mailgun_api_key', 'test');
mockSetting('mailgun_domain', 'example.com');
mockSetting('mailgun_base_url', 'test');
const stubbedSend = 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 stubbedSend.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.send = originalMailServiceSend;
// 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
}
};