Skip to content

Instantly share code, notes, and snippets.

@ifraixedes
Last active May 6, 2018 06:44
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save ifraixedes/3330ce0edf9286234b04 to your computer and use it in GitHub Desktop.
Save ifraixedes/3330ce0edf9286234b04 to your computer and use it in GitHub Desktop.
AWS lambda handler which download images from S3, resizes them and upload the new ones to S3; tested with mocha, chai, sinon and proxyquire
require('blanket')({
pattern: function (filename) {
return !/node_modules/.test(filename);
}
});
var proxyquire = require('proxyquire');
var chai = require('chai');
var sinonChai = require("sinon-chai");
var extend = require('lodash').extend;
var sinon = require('sinon');
chai.use(sinonChai);
describe('AwsHandler', function () {
var expect = chai.expect;
var sizesConfigs = [
{ width: 800, destinationPath: 'large' },
{ width: 500, destinationPath: 'medium' },
{ width: 200, destinationPath: 'small' },
{ width: 45, destinationPath: 'thumbnail'}
];
var baseEvent = {
"Records": [
{ "s3": {
"bucket": { "name": "testbucket" },
"object": { "key": null }
}
}
]
};
describe('reject to process non recognised image file extensions', function () {
var event, gmSpy, getObjectSpy, putObjectSpy, contextDoneSpy, testedModule;
before(function () {
event = extend({}, baseEvent);
event.Records[0].s3.object.key = "no-supported.gif";
gmSpy = sinon.spy();
getObjectSpy = sinon.spy();
putObjectSpy = sinon.spy();
contextDoneSpy = sinon.spy();
testedModule = getTestedModule(gmSpy, getObjectSpy, putObjectSpy);
testedModule.AwsHandler(event, { done: contextDoneSpy });
});
it('never call s3 getObject', function () {
expect(getObjectSpy).has.not.been.called;
});
it('never call graphics magick', function () {
expect(gmSpy).has.not.been.called;
});
it('never call s3 putObject', function () {
expect(putObjectSpy).has.not.been.called;
});
it('call context done with error', function () {
expect(contextDoneSpy).has.been.calledOnce.and.calledWith(new Error());
});
});
describe('process the image when the it has jpg extension', function () {
var event, gmStubs, getObjectStub, putObjectStub, contextDoneSpy, testedModule, fakeResponse;
before(function (done) {
fakeResponse = { Body: 'image content' };
event = extend({}, baseEvent);
event.Records[0].s3.object.key = "image.jpg";
gmStubs = getGmStubs();
getObjectStub = sinon.stub().callsArgWith(1, null, fakeResponse);
putObjectStub = sinon.stub().callsArgWith(1, null);
contextDoneSpy = sinon.spy();
testedModule = getTestedModule(gmStubs.gm, getObjectStub, putObjectStub);
testedModule.AwsHandler(event, { done: function () {
contextDoneSpy.apply(null, arguments);
done();
}});
});
it('call s3 getObject', function () {
expect(getObjectStub).has.been.calledOnce;
expect(getObjectStub).has.been.calledWith({
Bucket: event.Records[0].s3.bucket.name,
Key: event.Records[0].s3.object.key
});
});
it('call graphics magick', function () {
expect(gmStubs.gm).has.been.callCount(sizesConfigs.length);
expect(gmStubs.resize).has.been.callCount(sizesConfigs.length);
expect(gmStubs.toBuffer).has.been.callCount(sizesConfigs.length);
expect(gmStubs.gm).always.has.been.calledWith(fakeResponse.Body, event.Records[0].s3.object.key);
sizesConfigs.forEach(function(s) {
expect(gmStubs.resize).has.been.calledWith(s.width);
expect(gmStubs.toBuffer).has.been.calledWith('jpg');
});
});
it('call s3 putObject', function () {
expect(putObjectStub).has.been.callCount(sizesConfigs.length);
sizesConfigs.forEach(function(s) {
expect(putObjectStub).has.been.calledWith({
Bucket: event.Records[0].s3.bucket.name,
Key: 'dst/' + s.destinationPath + '/' + event.Records[0].s3.object.key,
Body: 'data',
ContentType: 'image/jpg'
});
});
});
it('call context done with no error', function () {
expect(contextDoneSpy).has.been.calledOnce.and.calledWith(null);
});
});
describe('process the image when the it has jpge extension', function () {
var event, gmStubs, getObjectStub, putObjectStub, contextDoneSpy, testedModule, fakeResponse;
before(function (done) {
fakeResponse = { Body: 'image content' };
event = extend({}, baseEvent);
event.Records[0].s3.object.key = "image.jpge";
gmStubs = getGmStubs();
getObjectStub = sinon.stub().callsArgWith(1, null, fakeResponse);
putObjectStub = sinon.stub().callsArgWith(1, null);
contextDoneSpy = sinon.spy();
testedModule = getTestedModule(gmStubs.gm, getObjectStub, putObjectStub);
testedModule.AwsHandler(event, { done: function () {
contextDoneSpy.apply(null, arguments);
done();
}});
});
it('call s3 getObject', function () {
expect(getObjectStub).has.been.calledOnce;
expect(getObjectStub).has.been.calledWith({
Bucket: event.Records[0].s3.bucket.name,
Key: event.Records[0].s3.object.key
});
});
it('call graphics magick', function () {
expect(gmStubs.gm).has.been.callCount(sizesConfigs.length);
expect(gmStubs.resize).has.been.callCount(sizesConfigs.length);
expect(gmStubs.toBuffer).has.been.callCount(sizesConfigs.length);
expect(gmStubs.gm).always.has.been.calledWith(fakeResponse.Body, event.Records[0].s3.object.key);
sizesConfigs.forEach(function(s) {
expect(gmStubs.resize).has.been.calledWith(s.width);
expect(gmStubs.toBuffer).has.been.calledWith('jpg');
});
});
it('call s3 putObject', function () {
expect(putObjectStub).has.been.callCount(sizesConfigs.length);
sizesConfigs.forEach(function(s) {
expect(putObjectStub).has.been.calledWith({
Bucket: event.Records[0].s3.bucket.name,
Key: 'dst/' + s.destinationPath + '/' + event.Records[0].s3.object.key,
Body: 'data',
ContentType: 'image/jpg'
});
});
});
it('call context done with no error', function () {
expect(contextDoneSpy).has.been.calledOnce.and.calledWith(null);
});
});
describe('process the image when the it has png extension', function () {
var event, gmStubs, getObjectStub, putObjectStub, contextDoneSpy, testedModule, fakeResponse;
before(function (done) {
fakeResponse = { Body: 'image content' };
event = extend({}, baseEvent);
event.Records[0].s3.object.key = "image.png";
gmStubs = getGmStubs();
getObjectStub = sinon.stub().callsArgWith(1, null, fakeResponse);
putObjectStub = sinon.stub().callsArgWith(1, null);
contextDoneSpy = sinon.spy();
testedModule = getTestedModule(gmStubs.gm, getObjectStub, putObjectStub);
testedModule.AwsHandler(event, { done: function () {
contextDoneSpy.apply(null, arguments);
done();
}});
});
it('call s3 getObject', function () {
expect(getObjectStub).has.been.calledOnce;
expect(getObjectStub).has.been.calledWith({
Bucket: event.Records[0].s3.bucket.name,
Key: event.Records[0].s3.object.key
});
});
it('call graphics magick', function () {
expect(gmStubs.gm).has.been.callCount(sizesConfigs.length);
expect(gmStubs.resize).has.been.callCount(sizesConfigs.length);
expect(gmStubs.toBuffer).has.been.callCount(sizesConfigs.length);
expect(gmStubs.gm).always.has.been.calledWith(fakeResponse.Body, event.Records[0].s3.object.key);
sizesConfigs.forEach(function(s) {
expect(gmStubs.resize).has.been.calledWith(s.width);
expect(gmStubs.toBuffer).has.been.calledWith('png');
});
});
it('call s3 putObject', function () {
expect(putObjectStub).has.been.callCount(sizesConfigs.length);
sizesConfigs.forEach(function(s) {
expect(putObjectStub).has.been.calledWith({
Bucket: event.Records[0].s3.bucket.name,
Key: 'dst/' + s.destinationPath + '/' + event.Records[0].s3.object.key,
Body: 'data',
ContentType: 'image/png'
});
});
});
it('call context done with no error', function () {
expect(contextDoneSpy).has.been.calledOnce.and.calledWith(null);
});
});
describe('process the image but image magick fails', function () {
var event, gmStubs, getObjectStub, putObjectSpy, contextDoneSpy, testedModule, fakeResponse;
before(function (done) {
fakeResponse = { Body: 'image content' };
event = extend({}, baseEvent);
event.Records[0].s3.object.key = "image.png";
var toBufferStub = sinon.stub().callsArgWith(1, new Error('Image resize failed'));
gmStubs = getGmStubs(toBufferStub);
getObjectStub = sinon.stub().callsArgWith(1, null, fakeResponse);
putObjectSpy = sinon.spy();
contextDoneSpy = sinon.spy();
testedModule = getTestedModule(gmStubs.gm, getObjectStub, putObjectSpy);
testedModule.AwsHandler(event, { done: function () {
contextDoneSpy.apply(null, arguments);
done();
}});
});
it('call s3 getObject', function () {
expect(getObjectStub).has.been.calledOnce;
expect(getObjectStub).has.been.calledWith({
Bucket: event.Records[0].s3.bucket.name,
Key: event.Records[0].s3.object.key
});
});
it('call graphics magick', function () {
expect(gmStubs.gm).has.been.callCount(sizesConfigs.length);
expect(gmStubs.resize).has.been.callCount(sizesConfigs.length);
expect(gmStubs.toBuffer).has.been.callCount(sizesConfigs.length);
expect(gmStubs.gm).always.has.been.calledWith(fakeResponse.Body, event.Records[0].s3.object.key);
sizesConfigs.forEach(function(s) {
expect(gmStubs.resize).has.been.calledWith(s.width);
expect(gmStubs.toBuffer).has.been.calledWith('png');
});
});
it('never call s3 putObject', function () {
expect(putObjectSpy).has.been.not.called;
});
it('call context done with no error', function () {
expect(contextDoneSpy).has.been.calledOnce.and.calledWith(new Error('Image resize failed'));
});
});
});
function getGmStubs(toBuffer) {
var toBuffer = toBuffer || sinon.stub().callsArgWith(1, null, 'data')
var resize = sinon.stub().returns({ toBuffer: toBuffer });
var gm = sinon.stub().returns({ resize: resize });
return {
gm: gm,
resize: resize,
toBuffer: toBuffer
};
}
function getTestedModule(gm, getObject, putObject) {
return proxyquire('../aws-handler.js', {
'gm': { subClass: function() { return gm; } },
'aws-sdk': {
"S3": function () {
return {
getObject: getObject,
putObject: putObject
};
}
}
});
}
'use strict';
var s3 = new (require('aws-sdk')).S3();
var gm = require('gm').subClass({ imageMagick: true });
var async = require('async');
var imageTypeRegExp = /(?:(jpg)e?|(png))$/;
var sizesConfigs = [
{ width: 800, destinationPath: 'large' },
{ width: 500, destinationPath: 'medium' },
{ width: 200, destinationPath: 'small' },
{ width: 45, destinationPath: 'thumbnail'}
];
exports.AwsHandler = function (event, context) {
var s3Bucket = event.Records[0].s3.bucket.name;
var s3Key = event.Records[0].s3.object.key;
// Check if file has a supported image extension
var imgExt = imageTypeRegExp.exec(s3Key);
if (imgExt === null) {
console.error('unable to infer the image type for key %s', s3Key);
context.done(new Error('unable to infer the image type for key %s' + s3Key));
return;
}
var imageType = imgExt[1] || imgExt[2];
async.waterfall([
function download(next) {
s3.getObject({
Bucket: s3Bucket,
Key: s3Key
}, next);
},
function transform(response, next) {
async.map(sizesConfigs, function (sizeConfig, mapNext) {
gm(response.Body, s3Key)
.resize(sizeConfig.width)
.toBuffer(imageType, function (err, buffer) {
if (err) {
mapNext(err);
return;
}
s3.putObject({
Bucket: s3Bucket,
Key: 'dst/' + sizeConfig.destinationPath + '/' + s3Key,
Body: buffer,
ContentType: 'image/' + imageType
}, mapNext)
});
}, next);
}
], function (err) {
if (err) {
console.error('Error processing image, details %s', err.message);
context.done(err);
} else {
context.done(null, 'Successfully resized images bucket: ' + s3Bucket + ' / key: ' + s3Key);
}
});
};
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004
(http://www.wtfpl.net/about/)
Copyright (C) 2015 Mario Mendes (@hyprstack)
Copyright (C) 2015 Ivan Fraixedes (https://ivan.fraixed.es)
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
{
"name": "aws-handler-image-resizer",
"private": true,
"version": "0.0.0",
"description": "",
"scripts": {
"test": "mocha -R html-cov > coverage.html"
},
"author": "Mario Mendes (@hyprstack)",
"contributors": [{
"name": "Ivan Fraixedes",
"email": "ivan@fraixed.es",
"url": "http://ivan.fraixed.es"
}],
"license": "wtfpl",
"dependencies": {
"async": "^0.9.0",
"aws-sdk": "^2.1.18",
"gm": "^1.17.0"
},
"devDependencies": {
"lodash": "^3.6.0",
"blanket": "^1.1.6",
"mocha": "^2.2.1",
"proxyquire": "^1.4.0",
"sinon": "^1.14.1",
"sinon-chai": "^2.7.0",
"chai": "^2.1.2"
}
}
@hellboy81
Copy link

Why proxyquire, but not rewire ?

@ifraixedes
Copy link
Author

I didn't choose the libraries/modules, a friend of mine had the implementation and the test but he wasn't able to finalize the test because he got some test timeouts and errors that he couldn't understand; he asked me where the problems could be and I fixed the issues on its solution, not building a solution to achieve the functionality.

It was more a learning purpose for him in where the issues were and having a better understanding mainly in how to use proxyquire and sinon.

I never had to use any kind of library similar to proxyquire before this case, so your assessment on rewire should be good based on your experience and use case than my one.

This gist was posted to support my answer to a stackoverflow question (I don't like to post snippets of more than 10 lines in stack overflow) and I wrote a small post about it, which also links to this gist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment