discord-bot-cdk-construct
Version:
A quick CDK Construct for creating a serverless Discord bot in AWS!
349 lines (317 loc) • 10.3 kB
JavaScript
;
/**
* Helpers to mock the AWS SDK Services using sinon.js under the hood
* Export two functions:
* - mock
* - restore
*
* Mocking is done in two steps:
* - mock of the constructor for the service on AWS
* - mock of the method on the service
**/
const sinon = require('sinon');
const traverse = require('traverse');
let _AWS = require('aws-sdk');
const Readable = require('stream').Readable;
const AWS = {};
const services = {};
/**
* Sets the aws-sdk to be mocked.
*/
AWS.setSDK = function(path) {
_AWS = require(path);
};
AWS.setSDKInstance = function(sdk) {
_AWS = sdk;
};
/**
* Stubs the service and registers the method that needs to be mocked.
*/
AWS.mock = function(service, method, replace) {
// If the service does not exist yet, we need to create and stub it.
if (!services[service]) {
services[service] = {};
/**
* Save the real constructor so we can invoke it later on.
* Uses traverse for easy access to nested services (dot-separated)
*/
services[service].Constructor = traverse(_AWS).get(service.split('.'));
services[service].methodMocks = {};
services[service].invoked = false;
mockService(service);
}
// Register the method to be mocked out.
if (!services[service].methodMocks[method]) {
services[service].methodMocks[method] = { replace: replace };
// If the constructor was already invoked, we need to mock the method here.
if (services[service].invoked) {
services[service].clients.forEach(client => {
mockServiceMethod(service, client, method, replace);
})
}
}
return services[service].methodMocks[method];
};
/**
* Stubs the service and registers the method that needs to be re-mocked.
*/
AWS.remock = function(service, method, replace) {
if (services[service].methodMocks[method]) {
restoreMethod(service, method);
services[service].methodMocks[method] = {
replace: replace
};
}
if (services[service].invoked) {
services[service].clients.forEach(client => {
mockServiceMethod(service, client, method, replace);
})
}
return services[service].methodMocks[method];
}
/**
* Stub the constructor for the service on AWS.
* E.g. calls of new AWS.SNS() are replaced.
*/
function mockService(service) {
const nestedServices = service.split('.');
const method = nestedServices.pop();
const object = traverse(_AWS).get(nestedServices);
const serviceStub = sinon.stub(object, method).callsFake(function(...args) {
services[service].invoked = true;
/**
* Create an instance of the service by calling the real constructor
* we stored before. E.g. const client = new AWS.SNS()
* This is necessary in order to mock methods on the service.
*/
const client = new services[service].Constructor(...args);
services[service].clients = services[service].clients || [];
services[service].clients.push(client);
// Once this has been triggered we can mock out all the registered methods.
for (const key in services[service].methodMocks) {
mockServiceMethod(service, client, key, services[service].methodMocks[key].replace);
};
return client;
});
services[service].stub = serviceStub;
};
/**
* Wraps a sinon stub or jest mock function as a fully functional replacement function
*/
function wrapTestStubReplaceFn(replace) {
if (typeof replace !== 'function' || !(replace._isMockFunction || replace.isSinonProxy)) {
return replace;
}
return (params, cb) => {
// If only one argument is provided, it is the callback
if (!cb) {
cb = params;
params = {};
}
// Spy on the users callback so we can later on determine if it has been called in their replace
const cbSpy = sinon.spy(cb);
try {
// Call the users replace, check how many parameters it expects to determine if we should pass in callback only, or also parameters
const result = replace.length === 1 ? replace(cbSpy) : replace(params, cbSpy);
// If the users replace already called the callback, there's no more need for us do it.
if (cbSpy.called) {
return;
}
if (typeof result.then === 'function') {
result.then(val => cb(undefined, val), err => cb(err));
} else {
cb(undefined, result);
}
} catch (err) {
cb(err);
}
};
}
/**
* Stubs the method on a service.
*
* All AWS service methods take two argument:
* - params: an object.
* - callback: of the form 'function(err, data) {}'.
*/
function mockServiceMethod(service, client, method, replace) {
replace = wrapTestStubReplaceFn(replace);
services[service].methodMocks[method].stub = sinon.stub(client, method).callsFake(function() {
const args = Array.prototype.slice.call(arguments);
let userArgs, userCallback;
if (typeof args[(args.length || 1) - 1] === 'function') {
userArgs = args.slice(0, -1);
userCallback = args[(args.length || 1) - 1];
} else {
userArgs = args;
}
const havePromises = typeof AWS.Promise === 'function';
let promise, resolve, reject, storedResult;
const tryResolveFromStored = function() {
if (storedResult && promise) {
if (typeof storedResult.then === 'function') {
storedResult.then(resolve, reject)
} else if (storedResult.reject) {
reject(storedResult.reject);
} else {
resolve(storedResult.resolve);
}
}
};
const callback = function(err, data) {
if (!storedResult) {
if (err) {
storedResult = {reject: err};
} else {
storedResult = {resolve: data};
}
}
if (userCallback) {
userCallback(err, data);
}
tryResolveFromStored();
};
const request = {
promise: havePromises ? function() {
if (!promise) {
promise = new AWS.Promise(function (resolve_, reject_) {
resolve = resolve_;
reject = reject_;
});
}
tryResolveFromStored();
return promise;
} : undefined,
createReadStream: function() {
if (storedResult instanceof Readable) {
return storedResult;
}
if (replace instanceof Readable) {
return replace;
} else {
const stream = new Readable();
stream._read = function(size) {
if (typeof replace === 'string' || Buffer.isBuffer(replace)) {
this.push(replace);
}
this.push(null);
};
return stream;
}
},
on: function(eventName, callback) {
return this;
},
send: function(callback) {
callback(storedResult.reject, storedResult.resolve);
}
};
// different locations for the paramValidation property
const config = (client.config || client.options || _AWS.config);
if (config.paramValidation) {
try {
// different strategies to find method, depending on wether the service is nested/unnested
const inputRules =
((client.api && client.api.operations[method]) || client[method] || {}).input;
if (inputRules) {
const params = userArgs[(userArgs.length || 1) - 1];
new _AWS.ParamValidator((client.config || _AWS.config).paramValidation).validate(inputRules, params);
}
} catch (e) {
callback(e, null);
return request;
}
}
// If the value of 'replace' is a function we call it with the arguments.
if (typeof replace === 'function') {
const result = replace.apply(replace, userArgs.concat([callback]));
if (storedResult === undefined && result != null &&
(typeof result.then === 'function' || result instanceof Readable)) {
storedResult = result
}
}
// Else we call the callback with the value of 'replace'.
else {
callback(null, replace);
}
return request;
});
}
/**
* Restores the mocks for just one method on a service, the entire service, or all mocks.
*
* When no parameters are passed, everything will be reset.
* When only the service is passed, that specific service will be reset.
* When a service and method are passed, only that method will be reset.
*/
AWS.restore = function(service, method) {
if (!service) {
restoreAllServices();
} else {
if (method) {
restoreMethod(service, method);
} else {
restoreService(service);
}
};
};
/**
* Restores all mocked service and their corresponding methods.
*/
function restoreAllServices() {
for (const service in services) {
restoreService(service);
}
}
/**
* Restores a single mocked service and its corresponding methods.
*/
function restoreService(service) {
if (services[service]) {
restoreAllMethods(service);
if (services[service].stub)
services[service].stub.restore();
delete services[service];
} else {
console.log('Service ' + service + ' was never instantiated yet you try to restore it.');
}
}
/**
* Restores all mocked methods on a service.
*/
function restoreAllMethods(service) {
for (const method in services[service].methodMocks) {
restoreMethod(service, method);
}
}
/**
* Restores a single mocked method on a service.
*/
function restoreMethod(service, method) {
if (services[service] && services[service].methodMocks[method]) {
if (services[service].methodMocks[method].stub) {
// restore this method on all clients
services[service].clients.forEach(client => {
if (client[method] && typeof client[method].restore === 'function') {
client[method].restore();
}
})
}
delete services[service].methodMocks[method];
} else {
console.log('Method ' + service + ' was never instantiated yet you try to restore it.');
}
}
(function() {
const setPromisesDependency = _AWS.config.setPromisesDependency;
/* istanbul ignore next */
/* only to support for older versions of aws-sdk */
if (typeof setPromisesDependency === 'function') {
AWS.Promise = global.Promise;
_AWS.config.setPromisesDependency = function(p) {
AWS.Promise = p;
return setPromisesDependency(p);
};
}
})();
module.exports = AWS;