I have Connection class that is used to connect to AWS Rds Proxy via IAM Authentication. Part of that process is to create a token. I have a function to create the token but now I having a hard time to mock and test it.
Here is the Connection class with setToken
method:
class Connection {
constructor(username, endpoint, database) {
this.username = username;
this.endpoint = endpoint;
this.database = database;
}
setToken () {
let signer = new AWS.RDS.Signer({
region: 'us-east-1', // example: us-east-2
hostname: this.endpoint,
port: 3306,
username: this.username
});
this.token = signer.getAuthToken({
username: this.username
});
}
}
And here I am trying to mock the return value of AWS.RDS.Signer.getAuthToken()
test('Test Connection setToken', async () => {
AWSMock.setSDKInstance(AWS);
AWSMock.mock('RDS.Signer', 'getAuthToken', 'mock-token');
let conn = new connections.Connection(
'testUser',
'testEndpoint',
'testDb');
conn.setToken();
console.log(conn.token);
});
I expected to see "mock-token" as the value for conn.token
, but what I get is this:
{
promise: [Function],
createReadStream: [Function: createReadStream],
on: [Function: on],
send: [Function: send]
}
How can I get AWS.RDS.Signer.getAuthToken()
to return a mock token?
Edit after trying solution from @ggordon
I have tried to get this to work by injecting AWS
into the constructor, but still seem to be having the same issue. I think part of my problem is that AWS.RDS.Signer does not support promises, but I'm not entirely sure.
Here is my new code:
The Token
class which generates the token.
import AWS from 'aws-sdk';
class Token {
constructor(awsInstance) {
this.awsInstance = awsInstance || AWS;
}
getToken () {
const endpoint = 'aurora-proxy.proxy.rds.amazonaws.com';
const signer = new this.awsInstance.RDS.Signer({
region: 'my-region',
hostname: endpoint,
port: 3306,
username: 'myUser'
});
const token = signer.getAuthToken({
username: 'svcLambda'
});
console.log ("IAM Token obtained\n");
return token
}
}
module.exports = { Token };
And the test:
test('Should test getToken from Token', async () => {
AWSMock.setSDKInstance(AWS);
AWSMock.mock('RDS.Signer', 'getAuthToken', 'mock-token');
let tokenObject = new tokens.Token(AWS);
const token = tokenObject.getToken();
console.log(token);
expect(token).toStrictEqual('mock-token');
});
The Token class itself works -- it creates the token and the token can be used to make a successful connection to RDS. However, the unit test fails with the actual token returned (from console.log) being this:
{
promise: [Function],
createReadStream: [Function: createReadStream],
on: [Function: on],
send: [Function: send]
}
Also here is the package.json
as requested by @GSSWain
{
"name": "mylambda",
"version": "0.0.1",
"description": "My description.",
"repository": {
"type": "git",
"url": ""
},
"scripts": {
"lint": "eslint src/**/*.js __tests__/**/*.js",
"prettier": "prettier --write src/**/*.js __tests__/**/*.js",
"prettier:ci": "prettier --list-different src/**/*.js __tests__/**/*.js",
"test": "cross-env NODE_ENV=test jest",
"test:coverage": "cross-env CI=true jest --coverage --watchAll=false -u --reporter=default --reporters=jest-junit",
"build": "npm run build:dev",
"build:dev": "cross-env NODE_ENV=development webpack --config webpack.config.js"
},
"dependencies": {
"mysql2": "^2.2.5"
},
"devDependencies": {
"@babel/core": "^7.6.4",
"@babel/preset-env": "^7.6.3",
"aws-sdk": "^2.552.0",
"aws-sdk-mock": "^5.1.0",
"babel-jest": "^24.9.0",
"babel-loader": "^8.0.6",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"cross-env": "^6.0.3",
"eslint": "^6.5.1",
"eslint-config-prettier": "^6.4.0",
"eslint-plugin-jest": "^22.19.0",
"jest": "^24.9.0",
"jest-junit": "^10.0.0",
"prettier": "^1.18.2",
"sinon": "^9.0.3"
},
"jest": {
"verbose": true,
"transform": {
"^.+\\.js$": "babel-jest"
},
"globals": {
"NODE_ENV": "test"
},
"moduleFileExtensions": [
"js"
],
"moduleDirectories": [
"node_modules",
"src"
],
"coverageThreshold": {
"global": {
"statements": 100,
"branches": 100,
"functions": 100,
"lines": 100
}
}
},
"jest-junit": {
"outputName": "junit_jest.xml"
}
}
The AWS
instance/object in your test scope is different from the AWS
instance/object being used in your setToken
method.
aws-sdk-mock
mocks this instance
Due to transpiling, code written in TypeScript or ES6 may not correctly mock because the aws-sdk object created within aws-sdk-mock will not be equal to the object created within the code to test.
Also require
will return a new instance.
In essence you are mocking an instance in your test while your actual code is using another instance that has not been mocked.
You could modify your code to allow you to optionally inject the desired AWS
instances to use eg
import AWS from 'aws-sdk';
class Connection {
constructor(username, endpoint, database,awsInstance) {
this.username = username;
this.endpoint = endpoint;
this.database = database;
//if the awsInstance is null or not provided use the default
this.awsInstance = awsInstance || AWS;
}
setToken () {
let signer = new this.awsInstance.RDS.Signer({
region: 'us-east-1', // example: us-east-2
hostname: this.endpoint,
port: 3306,
username: this.username
});
this.token = signer.getAuthToken({
username: this.username
});
}
}
your code would not need any modifications, however now you can optionally in your tests
test('Test Connection setToken', async () => {
AWSMock.setSDKInstance(AWS);
AWSMock.mock('RDS.Signer', 'getAuthToken', 'mock-token');
let conn = new connections.Connection(
'testUser',
'testEndpoint',
'testDb',
AWS //pass mock instance
);
conn.setToken();
let actualToken = (await conn.token.promise());
console.log(conn.token);
console.log(actualToken);
});
This is only constructor based injection, you could inject it by doing similar in the setToken
method.
You will also notice that in the examples provided by aws-sdk-mock
and the example above we've extracted the result from the promise
object returned. This is because the mock implementation returns a promise object despite the fact that the aws-sdk
especially for the AWS.RDS.Signer.getAuthToken
supports synchronous operations. This is a constraint based on the library you are using.
You may want to consider another mocking library if you are interested in synchronous calls which based on the examples shared here would better mimic your code/flow. The other alternative is to consider an asynchronous/promise rewrite of your implementations. I leave this decision to you.
A simple alternative could be:
test('Test Connection setToken', async () => {
AWS.RDS.Signer = function MockSigner() {
return {
getAuthToken: function MockGetAuthToken(){ return 'mock-token'; }
};
};
let conn = new connections.Connection(
'testUser',
'testEndpoint',
'testDb',
AWS //pass mock instance
);
conn.setToken();
console.log(conn.token);
});
I've included a snippet of the method used to mock functions for the aws-sdk-mock
retrieved from https://github.com/dwyl/aws-sdk-mock/blob/master/index.js#L118 . You'll see that it creates and returns a request
function mockServiceMethod(service, client, method, 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 (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) {
},
send: function(callback) {
}
};
// 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') {
storedResult = result
}
}
// Else we call the callback with the value of 'replace'.
else {
callback(null, replace);
}
return request;
});
}