├── .env.sample ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── config.json ├── handler.js ├── package-lock.json ├── package.json ├── serverless.sample.yml └── src ├── image-resizer.js ├── image-resizer.test.js ├── s3-image-fetcher.js ├── s3-image-fetcher.test.js ├── types.js └── types.test.js /.env.sample: -------------------------------------------------------------------------------- 1 | BUCKET=your-s3-bucket-name 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | dist 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Dependency directory 21 | node_modules 22 | 23 | # npm cache directory 24 | .npm 25 | 26 | # eslint cache 27 | .eslintcache 28 | 29 | #IDE Stuff 30 | **/.idea 31 | 32 | #OS STUFF 33 | .DS_Store 34 | .tmp 35 | 36 | #SERVERLESS STUFF 37 | admin.env 38 | .env 39 | _meta 40 | .serverless 41 | serverless.yml 42 | /lambda 43 | 44 | # Docker 45 | *.swp 46 | 47 | # dotenv environment variables file 48 | .env 49 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lambci/lambda:build-nodejs10.x 2 | 3 | WORKDIR /var/task 4 | 5 | VOLUME /var/task/ 6 | COPY package.json /var/task/ 7 | 8 | # RUN npm install -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 AndreaSonny (https://github.com/andreasonny83) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build \ 2 | interactive \ 3 | install 4 | 5 | build: 6 | docker build -t lambdaci . 7 | 8 | interactive: 9 | docker run --rm -it -v ${PWD}:/var/task --entrypoint bash lambdaci 10 | 11 | install: 12 | docker run --rm -v ${PWD}:/var/task lambdaci npm install 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Image Rendering 2 | 3 | > Fast image delivering system using Sharp, AWS Lambda and Serverless framework. 4 | 5 | [![serverless][badge-serverless]](http://www.serverless.com) 6 | 7 | Demo Lambda function available [here](https://dght5ywo5j.execute-api.us-east-1.amazonaws.com/dev/resize-image?f=placeholder.jpg&w=600&q=75&t=webp) 8 | 9 | Medium article available [here](https://medium.com/@andreasonny83/serverless-image-optimization-and-delivery-510b6c311fe5) 10 | 11 | - [Serverless Image Rendering](#Serverless-Image-Rendering) 12 | - [Getting Started](#Getting-Started) 13 | - [Prerequisites](#Prerequisites) 14 | - [Optional requisites](#Optional-requisites) 15 | - [Installation](#Installation) 16 | - [Images Bucket](#Images-Bucket) 17 | - [Usage](#Usage) 18 | - [Environment configuration](#Environment-configuration) 19 | - [dotENV](#dotENV) 20 | - [Serving the app](#Serving-the-app) 21 | - [Resizing your images](#Resizing-your-images) 22 | - [Unit testing](#Unit-testing) 23 | - [Docker](#Docker) 24 | - [Deployment](#Deployment) 25 | - [Log](#Log) 26 | - [Contributing](#Contributing) 27 | - [Built With](#Built-With) 28 | - [License](#License) 29 | 30 | ## Getting Started 31 | 32 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. 33 | See deployment for notes on how to deploy the project on a live system. 34 | 35 | ### Prerequisites 36 | 37 | This project requires NodeJS (at least version 6) and NPM. 38 | [Node](http://nodejs.org/) and [NPM](https://npmjs.org/) are really easy to install. 39 | To make sure you have them available on your machine, 40 | try running the following command. 41 | 42 | ```sh 43 | $ node -v && npm -v 44 | v10.15.1 45 | 6.9.0 46 | ``` 47 | 48 | You may also want to install Serverless globally on your local machine for accessing the 49 | CLI functionality and directly interact with your deployed Lambda function. 50 | 51 | To install Serverless globally on your machine, run the following command 52 | 53 | ```sh 54 | $ npm i -g serverless 55 | ``` 56 | 57 | Please, read the [official Serverless documentation](https://serverless.com/framework/docs/getting-started/) 58 | to know more about the installation process. 59 | 60 | If you want to deploy your code to AWS, then you will also need an AWS account and 61 | [set-up your provider credentials](https://serverless.com/framework/docs/providers/aws/guide/credentials/) 62 | 63 | ## Optional requisites 64 | 65 | This project is making use of Serverless and Docker. 66 | They are both optionals however Docker will be required for deploying your local code 67 | to Lambda if you are not running this project from a Linux machine. 68 | Read the [Docker](#docker) section to know more about. 69 | 70 | ### Installation 71 | 72 | Start with cloning this repo on your local machine: 73 | 74 | ```sh 75 | $ git clone https://github.com/andreasonny83/serverless-image-rendering.git 76 | $ cd serverless-image-rendering 77 | ``` 78 | 79 | Then install all the Node dependencies using npm or Yarn 80 | 81 | ```sh 82 | $ npm install 83 | # Or using Yarn for a faster installation 84 | $ yarn 85 | ``` 86 | 87 | ### Images Bucket 88 | 89 | This project is making use of a pre-existing S3 bucket from where fetching 90 | the images to be processed and deliver to the client. 91 | If you don't have an S3 already, create one from your AWS dashboard and 92 | continue reading the [Environment configuration](#environment-configuration) 93 | section. 94 | 95 | ## Usage 96 | 97 | ### Environment configuration 98 | 99 | This project contains a `serverless.sample.yml` file. 100 | You need to manually renamed it to `serverless.yml`. 101 | In order for your application to be correctly deployed to AWS, you will need to 102 | replace the `BUCKET` name under the `environment` section according to your S3 103 | bucket name previously created. 104 | 105 | ### dotENV 106 | 107 | For your local development you will need a `.env` file containing your S3 bucket name. 108 | Rename the `.env.sample` file in this project to be `.env` first, then replace the 109 | `your-s3-bucket-name` placeholder with the correct name of your S3 Bucket. 110 | 111 | ### Serving the app 112 | 113 | ```sh 114 | $ npm run serve 115 | ``` 116 | 117 | This will run the Lambda function locally using `serverless-offline`. 118 | 119 | ### Resizing your images 120 | 121 | While running your local app, open a browser to 122 | [`http://localhost:3000/resize-image?status`](http://localhost:3000/resize-image?status) 123 | 124 | You should be able to see a JSON information to prove that your app is actually 125 | up and running. 126 | 127 | You can then replace the `?status` endpoint with your image information. 128 | 129 | `http://localhost:3000/resize-image?f=FILE-NAME&w=WIDTH&h=HEIGHT&q=QUALITY&t=TYPE` 130 | 131 | | Query string name | Type | Required | Description | 132 | | ------------------ | ------ | -------- | ----------- | 133 | | `f` | String | Yes | The complete image name uploaded to your S3 bucket (eg. placeholder.jpg) 134 | | `w` | Number | No | The image width 135 | | `h` | Number | No | The image height 136 | | `q` | Number | No | The image quality (between 1-100) 137 | | `t` | String | No | The image type (default is webp) Available values are [webp, jpeg and png] 138 | 139 | Note. If the Type is different from your original image type, it will 140 | automatically be converted into the new format. 141 | 142 | **Example** 143 | 144 | `http://localhost:3000/resize-image?f=placeholder.png&w=600&q=75&t=jpeg` 145 | 146 | Assuming that you have an image called `placeholder.png` on your S3 bucket: 147 | 148 | 149 | ### Unit testing 150 | 151 | ```sh 152 | $ npm test 153 | ``` 154 | 155 | This is using Jest framework. 156 | If you want to keep running Jest in the background and watch for file change while developing 157 | use the following npm command instead: 158 | 159 | ```sh 160 | $ npm run test:watch 161 | ``` 162 | 163 | ### Docker 164 | 165 | The `Make install` command will run `npm install` from inside a Docker 166 | container to reproduce the same environment configuration present on AWS Lambda. 167 | Deploying the Lambda function after running `npm install` from your local machine 168 | will probably result on the function returning a server error if your local machine 169 | configuration is different from the one on AWS. 170 | 171 | To make sure the `npm run deploy` succeeds: 172 | 173 | 1. Clean your `node_modules` folder with `rm -rf node_modules package-lock.json` 174 | 1. Create the Docker image with `make build` 175 | 1. Run `npm install` from inside the Docker container with `make install` 176 | 1. Then run serverless deploy` with `npm run deploy` 177 | 178 | ## Deployment 179 | 180 | 1. Install serverless globally 181 | 1. Make sure you have correctly set up your AWS credentials 182 | 1. Install the Node dependencies with Docker by running `make build` to generate 183 | the Docker image, the run `make install` (This will run `npm install` from inside 184 | the Docker container using the same environment setup running on AWS) 185 | 1. run `npm run deploy` to deploy your function to AWS Lambda 186 | 187 | ## Log 188 | 189 | Note. The following commands will require Serverless to be globally 190 | installed on your machine. Please read the [Prerequisites](#prerequisites) 191 | section for more information. 192 | 193 | When your Lambda function has been deployed, you will be able to 194 | easily log all the events directly inside your local terminal. 195 | 196 | ```sh 197 | $ sls logs -f {function-name} -t 198 | ``` 199 | 200 | If you don't remember what's your function name, you can easily retrieve 201 | all your Lambda information running the following command from inside your 202 | project's directory. 203 | 204 | ```sh 205 | $ sls info 206 | ``` 207 | 208 | ## Contributing 209 | 210 | 1. Fork it! 211 | 2. Create your feature branch: `git checkout -b my-new-feature` 212 | 3. Add your changes: `git add .` 213 | 4. Commit your changes: `git commit -am 'Add some feature'` 214 | 5. Push to the branch: `git push origin my-new-feature` 215 | 6. Submit a pull request :sunglasses: 216 | 217 | ## Built With 218 | 219 | * VSCode 220 | * dotENV 221 | * Serverless 222 | * Love 223 | 224 | ## License 225 | 226 | [MIT License](https://andreasonny.mit-license.org/2018-2019) © Andrea SonnY 227 | 228 | [badge-serverless]: http://public.serverless.com/badges/v3.svg 229 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "testRegex": ".*-test\\.js$", 3 | "testEnvironment": "node" 4 | } 5 | -------------------------------------------------------------------------------- /handler.js: -------------------------------------------------------------------------------- 1 | const uuid = require('uuid'); 2 | const Sharp = require('sharp'); 3 | const Types = require('./src/types'); 4 | const ImageFetcher = require('./src/s3-image-fetcher'); 5 | const ImageResizr = require('./src/image-resizer'); 6 | 7 | const displayStatus = () => { 8 | const APP_NAME = process.env.APP_NAME; 9 | const ENV_NAME = process.env.ENV_NAME; 10 | 11 | return JSON.stringify({ 12 | app: APP_NAME, 13 | environment: ENV_NAME, 14 | message: `OK - ${uuid.v4()}`, 15 | }); 16 | }; 17 | 18 | module.exports.fetchImage = (event) => { 19 | const imageFetcher = new ImageFetcher(process.env.BUCKET); 20 | 21 | const fileName = event.queryStringParameters && event.queryStringParameters.f; 22 | const status = event.queryStringParameters && 'status' in event.queryStringParameters; 23 | 24 | if (process.env.DEBUG) { 25 | console.log('bucketName:', imageFetcher._bucketName); 26 | console.log('fileName:', fileName); 27 | } 28 | 29 | if (Boolean(status)) { 30 | return { 31 | statusCode: 200, 32 | body: displayStatus(), 33 | }; 34 | } 35 | 36 | return imageFetcher.fetchImage(fileName) 37 | .then(data => { 38 | const contentType = data.contentType; 39 | const img = new Buffer(data.image.buffer, 'base64'); 40 | 41 | return { 42 | statusCode: 200, 43 | headers: { 'Content-Type': contentType }, 44 | body: img.toString('base64'), 45 | isBase64Encoded: true, 46 | }; 47 | }) 48 | .catch(error => { 49 | console.error('Error:', error); 50 | return { 51 | statusCode: 200, 52 | body: error, 53 | }; 54 | }); 55 | }; 56 | 57 | module.exports.resizeImage = async (event, context, callback) => { 58 | const imageFetcher = new ImageFetcher(process.env.BUCKET); 59 | const imageResizr = new ImageResizr(Types, Sharp); 60 | 61 | const fileName = event.queryStringParameters && event.queryStringParameters.f; 62 | const status = event.queryStringParameters && 'status' in event.queryStringParameters; 63 | const quality = event.queryStringParameters && +event.queryStringParameters.q || 100; 64 | const type = event.queryStringParameters && event.queryStringParameters.t; 65 | const size = { 66 | w: event && +event.queryStringParameters.w || null, 67 | h: event && +event.queryStringParameters.h || null, 68 | }; 69 | 70 | if (process.env.DEBUG) { 71 | console.log('bucketName:', imageFetcher._bucketName); 72 | console.log('fileName:', fileName); 73 | } 74 | 75 | if (Boolean(status)) { 76 | return { 77 | statusCode: 200, 78 | body: displayStatus(), 79 | }; 80 | } 81 | 82 | return imageFetcher.fetchImage(fileName) 83 | .then(data => imageResizr.resize(data.image, size, quality, type)) 84 | .then(data => { 85 | const contentType = data.contentType; 86 | const img = new Buffer(data.image.buffer, 'base64'); 87 | 88 | return { 89 | statusCode: 200, 90 | headers: { 'Content-Type': contentType }, 91 | body: img.toString('base64'), 92 | isBase64Encoded: true, 93 | }; 94 | }) 95 | .catch(error => { 96 | console.error('Error:', error); 97 | return { 98 | statusCode: 404, 99 | body: error, 100 | } 101 | }); 102 | }; 103 | 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-image-rendering", 3 | "version": "0.0.0", 4 | "description": "Server side image rendering using AWS Lamdba with Serverless", 5 | "scripts": { 6 | "serve": "serverless offline start", 7 | "deploy": "serverless deploy", 8 | "test": "jest", 9 | "test:watch": "jest --watchAll" 10 | }, 11 | "author": "andreasonny83@gmail.com", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@types/jest": "^24.0.15", 15 | "aws-sdk-mock": "^4.5.0", 16 | "body-parser": "^1.19.0", 17 | "jest": "^24.8.0", 18 | "nodemon": "^1.19.1", 19 | "serverless-offline": "^5.7.2", 20 | "supertest": "^4.0.2" 21 | }, 22 | "dependencies": { 23 | "aws-sdk": "^2.489.0", 24 | "dotenv": "^8.0.0", 25 | "serverless-apigw-binary": "^0.4.4", 26 | "serverless-apigwy-binary": "^1.0.0", 27 | "sharp": "^0.22.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /serverless.sample.yml: -------------------------------------------------------------------------------- 1 | service: serverless-image-rendering 2 | 3 | custom: 4 | envName: '${self:provider.stage}' 5 | apigwBinary: 6 | types: 7 | - '*/*' 8 | 9 | provider: 10 | name: aws 11 | runtime: nodejs10.x 12 | stage: dev 13 | region: us-east-1 14 | timeout: 5 # optional, in seconds, default is 6 15 | 16 | role: ImageRenderingRole 17 | 18 | environment: 19 | ENV_NAME: ${self:custom.envName} 20 | APP_NAME: serverless-image-rendering 21 | BUCKET: YOUR-S3-BUCKET-NAME 22 | 23 | plugins: 24 | - serverless-offline 25 | - serverless-apigw-binary 26 | - serverless-apigwy-binary 27 | 28 | functions: 29 | resizeImage: 30 | name: ${self:service}-${self:provider.stage} 31 | description: real time image resizing 32 | handler: handler.resizeImage 33 | events: 34 | - http: 35 | path: resize-image 36 | method: get 37 | contentHandling: CONVERT_TO_BINARY 38 | 39 | resources: 40 | Resources: 41 | ImageRenderingRole: 42 | Type: AWS::IAM::Role 43 | Properties: 44 | RoleName: ${self:service}-S3-AND-LOG-ACCESS 45 | AssumeRolePolicyDocument: 46 | Version: "2012-10-17" 47 | Statement: 48 | - Effect: Allow 49 | Principal: 50 | Service: 51 | - lambda.amazonaws.com 52 | Action: sts:AssumeRole 53 | Policies: 54 | - PolicyName: ${self:service}-s3-and-log-access 55 | PolicyDocument: 56 | Version: "2012-10-17" 57 | Statement: 58 | - Effect: Allow 59 | Action: 60 | - logs:CreateLogGroup 61 | - logs:CreateLogStream 62 | - logs:PutLogEvents 63 | Resource: 64 | - 'arn:aws:logs:${self:provider.region}:*:log-group:/aws/lambda/*:*:*' 65 | - Effect: Allow 66 | Action: 67 | - "s3:GetObject" 68 | Resource: 69 | - 'arn:aws:s3:::${self:provider.environment.BUCKET}/*' 70 | -------------------------------------------------------------------------------- /src/image-resizer.js: -------------------------------------------------------------------------------- 1 | class ImageResizr { 2 | constructor(Types, Sharp) { 3 | this.types = Types; 4 | this.sharp = Sharp; 5 | } 6 | 7 | getImageType(type, def = 'webp') { 8 | const found = this.types.find(item => item.sharp === type); 9 | 10 | if (!found && type === def) { 11 | return { sharp: def, contentType: `image/${def}`}; 12 | } 13 | 14 | return found || this.getImageType(def, def); 15 | } 16 | 17 | resize(image, size, quality, type) { 18 | if (!image) throw new Error('An Image must be specified'); 19 | if (!size) throw new Error('Image size must be specified'); 20 | 21 | const sharpType = this.getImageType(type, 'webp'); 22 | 23 | return new Promise((res, rej) => { 24 | this.sharp(new Buffer(image.buffer)) 25 | .resize(size.w, size.h, { fit: 'inside' }) 26 | [sharpType.sharp]({quality: quality}) 27 | .toBuffer() 28 | .then(data => { 29 | return res({ 30 | image: data, 31 | contentType: sharpType.contentType, 32 | }); 33 | }) 34 | .catch(err => rej(err)) 35 | }); 36 | } 37 | } 38 | 39 | module.exports = ImageResizr; 40 | -------------------------------------------------------------------------------- /src/image-resizer.test.js: -------------------------------------------------------------------------------- 1 | const ImageResizr = require('./image-resizer'); 2 | 3 | describe('imageResizr', () => { 4 | let imageResizr; 5 | 6 | const mockedTypes = [{ 7 | sharp: 'bla', 8 | contentType: 'image/bla' 9 | }]; 10 | 11 | describe('getImageType', () => { 12 | beforeEach(() => { 13 | imageResizr = new ImageResizr(mockedTypes); 14 | }); 15 | 16 | it('should return an object', () => { 17 | const t = imageResizr.getImageType('bla'); 18 | 19 | expect(typeof t).toBe('object'); 20 | }); 21 | 22 | it('it should return an object containg a `sharp` key', () => { 23 | const t = imageResizr.getImageType('bla'); 24 | 25 | expect(t.sharp).toBeDefined(); 26 | }); 27 | 28 | it('it should return an object containg a `contentType` key', () => { 29 | const t = imageResizr.getImageType('bla'); 30 | 31 | expect(t.contentType).toBeDefined(); 32 | }); 33 | 34 | it('if type is not found in the passed configuration it should' + 35 | 'return a default type', () => { 36 | const t = imageResizr.getImageType('not-defined'); 37 | 38 | expect(t.sharp).toBe('webp'); 39 | expect(t.contentType).toBe('image/webp'); 40 | }); 41 | 42 | it('the default type should be accepted as an optional attribute', () => { 43 | const t = imageResizr.getImageType('not-defined', 'default'); 44 | 45 | expect(t.sharp).toBe('default'); 46 | expect(t.contentType).toBe('image/default'); 47 | }); 48 | }); 49 | 50 | describe('resize', () => { 51 | const SharpStub = jest.fn().mockImplementation((any) => ({ 52 | resize: function() { return this }, 53 | max: function() { return this }, 54 | webp: function(x) { return this }, 55 | toBuffer: () => Promise.resolve('image') 56 | })); 57 | 58 | it('should return a resized image', async () => { 59 | const mockImage = new Buffer('foo'); 60 | const options = { w: 600, h: 600 }; 61 | imageResizr = new ImageResizr(mockedTypes, SharpStub); 62 | const t = await imageResizr.resize(mockImage, options); 63 | 64 | expect(SharpStub).toHaveBeenCalledWith(new Buffer(mockImage.buffer)); 65 | expect(t).toMatchObject({contentType: 'image/webp', image: 'image'}); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/s3-image-fetcher.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const checkS3 = (s3, bucketName, fileName) => 4 | new Promise((res, rej) => { 5 | s3.headObject({ 6 | Bucket: bucketName, 7 | Key: fileName, 8 | }, 9 | (err, metadata) => { 10 | if (err && ['NotFound', 'Forbidden'].indexOf(err.code) > -1) { 11 | return rej(err.code); 12 | } else if (err) { 13 | return rej(err); 14 | } 15 | 16 | return res(metadata); 17 | }); 18 | }); 19 | 20 | const getS3 = (s3, bucketName, fileName) => 21 | new Promise((res, rej) => { 22 | s3.getObject({ 23 | Bucket: bucketName, 24 | Key: fileName 25 | }, (err, data) => { 26 | if (err) { 27 | return rej(err); 28 | } 29 | 30 | const contentType = data.ContentType; 31 | const image = data.Body; 32 | 33 | return res({ image, contentType }); 34 | }); 35 | }); 36 | 37 | class ImageFetcher { 38 | constructor(bucketName) { 39 | this._S3 = new AWS.S3(); 40 | this._bucketName = bucketName; 41 | } 42 | 43 | fetchImage(fileName) { 44 | if (!fileName) { 45 | return Promise.reject('Filename not specified'); 46 | } 47 | 48 | return Promise 49 | .resolve(checkS3(this._S3, this._bucketName, fileName)) 50 | .then(metadata => { 51 | if (!metadata) { 52 | throw new Error('Invalid File'); 53 | } 54 | }) 55 | .then(() => Promise 56 | .resolve(getS3(this._S3, this._bucketName, fileName))); 57 | } 58 | } 59 | 60 | module.exports = ImageFetcher; 61 | -------------------------------------------------------------------------------- /src/s3-image-fetcher.test.js: -------------------------------------------------------------------------------- 1 | const mockAWS = require('aws-sdk-mock'); 2 | const ImageFetcher = require('./s3-image-fetcher'); 3 | 4 | describe('S3 Image Fetcher', () => { 5 | describe('fetchImage', () => { 6 | afterAll(() => { 7 | mockAWS.restore('S3'); 8 | }); 9 | 10 | test('fetchImage should be defined', () => { 11 | const imageFetcher = new ImageFetcher(); 12 | 13 | expect(imageFetcher.fetchImage).toBeDefined(); 14 | }); 15 | 16 | test('should throw an error if a filename is not passed', 17 | async () => { 18 | const imageFetcher = new ImageFetcher(); 19 | 20 | expect(imageFetcher.fetchImage()) 21 | .rejects.toMatch('Filename not specified') 22 | }); 23 | 24 | test(`should throw an error if the filename doesn't exist`, 25 | async () => { 26 | mockAWS.mock('S3', 'headObject', (params, callback) => 27 | callback('Test error message')); 28 | 29 | const imageFetcher = new ImageFetcher('test_bucket'); 30 | 31 | await expect(imageFetcher.fetchImage('test.jpg')) 32 | .rejects.toMatch('Test error message'); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | const Types = [ 2 | { sharp: 'webp', contentType: 'image/webp'}, 3 | { sharp: 'jpeg', contentType: 'image/jpeg'}, 4 | { sharp: 'png', contentType: 'image/png'}, 5 | ]; 6 | 7 | module.exports = Types; 8 | -------------------------------------------------------------------------------- /src/types.test.js: -------------------------------------------------------------------------------- 1 | const Types = require('./Types'); 2 | 3 | describe('Types', () => { 4 | test('Types should be defined', () => { 5 | expect(Types).toBeDefined(); 6 | }); 7 | 8 | test('Types should be an array of object', () => { 9 | expect(Array.isArray(Types)).toBe(true); 10 | expect(Types.length).not.toBe(0); 11 | }); 12 | 13 | test(`Types objects should contain a 'sharp' key`, () => { 14 | expect(Types[0].sharp).toBeDefined(); 15 | }); 16 | 17 | test(`Types objects should contain a 'contentType' key`, () => { 18 | expect(Types[0].contentType).toBeDefined(); 19 | }); 20 | }); 21 | --------------------------------------------------------------------------------