-
-
Save dng-dev/b056b9e6795bf13db1ef 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}; | |
} | |
} | |
}); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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); | |
} | |
}); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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": "[email protected]", | |
"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" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment