diff --git a/source/image-handler/image-request.js b/source/image-handler/image-request.js index f26173da9..c08faf261 100644 --- a/source/image-handler/image-request.js +++ b/source/image-handler/image-request.js @@ -64,7 +64,7 @@ class ImageRequest { * @param {String} requestType - Image handler request type. */ parseImageBucket(event, requestType) { - if (requestType === "Default") { + if (requestType === "Default" || requestType === "Token") { // Decode the image request const decoded = this.decodeRequest(event); if (decoded.bucket !== undefined) { @@ -103,7 +103,7 @@ class ImageRequest { * @param {String} requestType - Image handler request type. */ parseImageEdits(event, requestType) { - if (requestType === "Default") { + if (requestType === "Default" || requestType === "Token") { const decoded = this.decodeRequest(event); return decoded.edits; } else if (requestType === "Thumbor") { @@ -128,10 +128,10 @@ class ImageRequest { * Parses the name of the appropriate Amazon S3 key corresponding to the * original image. * @param {String} event - Lambda request body. - * @param {String} requestType - Type, either "Default", "Thumbor", or "Custom". + * @param {String} requestType - Type, either "Default", "Token", "Thumbor", or "Custom". */ parseImageKey(event, requestType) { - if (requestType === "Default") { + if (requestType === "Default" || requestType === "Token") { // Decode the image request and return the image key const decoded = this.decodeRequest(event); return decoded.key; @@ -149,15 +149,38 @@ class ImageRequest { } } + /** + * Returns the base64-encoded image request carried in a validated JWT, or + * undefined when the request did not arrive through an API Gateway route + * with a JWT authorizer. + * + * API Gateway populates requestContext.authorizer.jwt only after it has + * verified the token's signature, issuer, audience and expiry, so the mere + * presence of the claim is proof of validation - the token itself is never + * parsed here. Lambda Function URL invocations (the CloudFront path) carry + * requestContext.authorizer.iam instead, never .jwt. + * @param {Object} event - Lambda request body. + */ + getRequestClaim(event) { + return event.requestContext?.authorizer?.jwt?.claims?.request; + } + /** * Determines how to handle the request being made based on the URL path - * prefix to the image request. Categorizes a request as either "image" - * (uses the Sharp library), "thumbor" (uses Thumbor mapping), or "custom" - * (uses the rewrite function). + * prefix to the image request. Categorizes a request as either "token" + * (image request supplied as a JWT claim), "image" (uses the Sharp + * library), "thumbor" (uses Thumbor mapping), or "custom" (uses the + * rewrite function). * @param {Object} event - Lambda request body. */ parseRequestType(event) { - let path = event["path"]; + // A validated JWT carrying the request claim short-circuits path + // matching entirely: these requests have no image path to inspect. + if (this.getRequestClaim(event) !== undefined) { + return 'Token'; + } + + let path = event.rawPath ?? event["path"]; if (process.env.TRUNCATE_PATH_PREFIX !== undefined) { // Allows cloudfront to be shared by adding a prefix/* to behaviour @@ -190,30 +213,36 @@ class ImageRequest { } /** - * Decodes the base64-encoded image request path associated with default - * image requests. Provides error handling for invalid or undefined path values. + * Decodes the base64-encoded image request associated with default and + * token image requests. The encoded value is taken from the JWT request + * claim when present, otherwise from the last segment of the URL path. + * Provides error handling for invalid or undefined values. * @param {Object} event - The proxied request object. */ decodeRequest(event) { - const path = event["path"]; - if (path !== undefined) { - const splitPath = path.split("/"); - const encoded = splitPath[splitPath.length - 1]; - const toBuffer = Buffer.from(encoded, 'base64'); - try { - return JSON.parse(toBuffer.toString('ascii')); - } catch (e) { + let encoded = this.getRequestClaim(event); + + if (encoded === undefined) { + const path = event.rawPath ?? event["path"]; + if (path === undefined) { throw ({ status: 400, - code: 'DecodeRequest::CannotDecodeRequest', - message: 'The image request you provided could not be decoded. Please check that your request is base64 encoded properly and refer to the documentation for additional guidance.' + code: 'DecodeRequest::CannotReadPath', + message: 'The URL path you provided could not be read. Please ensure that it is properly formed according to the solution documentation.' }); } - } else { + const splitPath = path.split("/"); + encoded = splitPath[splitPath.length - 1]; + } + + const toBuffer = Buffer.from(encoded, 'base64'); + try { + return JSON.parse(toBuffer.toString('utf8')); + } catch (e) { throw ({ status: 400, - code: 'DecodeRequest::CannotReadPath', - message: 'The URL path you provided could not be read. Please ensure that it is properly formed according to the solution documentation.' + code: 'DecodeRequest::CannotDecodeRequest', + message: 'The image request you provided could not be decoded. Please check that your request is base64 encoded properly and refer to the documentation for additional guidance.' }); } } @@ -240,4 +269,4 @@ class ImageRequest { } // Exports -module.exports = ImageRequest; +module.exports = ImageRequest; \ No newline at end of file diff --git a/source/image-handler/package.json b/source/image-handler/package.json index 435f1b62c..6aa0366fd 100644 --- a/source/image-handler/package.json +++ b/source/image-handler/package.json @@ -14,12 +14,16 @@ }, "devDependencies": { "aws-sdk-mock": "^4.4.0", + "jest": "^30.4.2", "mocha": "^9.1.3", "nyc": "^15.1.0" }, + "jest": { + "testEnvironment": "node" + }, "scripts": { "pretest": "npm install", - "test": "nyc --reporter=html --reporter=text mocha", + "test": "jest", "build:init": "rm -rf package-lock.json && rm -rf dist && rm -rf node_modules", "build:zip": "zip -rq image-handler.zip . -x template.yml", "build:dist": "mkdir dist && mv image-handler.zip dist/", @@ -30,4 +34,4 @@ "dev": "npm run build:sam; sam local start-api -p 8181" }, "license": "Apache-2.0" -} +} \ No newline at end of file diff --git a/source/image-handler/test/image-handler.spec.js b/source/image-handler/test/image-handler.spec.js deleted file mode 100644 index 45d73fbe4..000000000 --- a/source/image-handler/test/image-handler.spec.js +++ /dev/null @@ -1,1009 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -const fs = require('fs'); - -const mockAws = { - getObject: jest.fn(), - detectFaces: jest.fn(), - detectModerationLabels: jest.fn() -}; -jest.mock('aws-sdk', () => { - return { - S3: jest.fn(() => ({ - getObject: mockAws.getObject - })), - Rekognition: jest.fn(() => ({ - detectFaces: mockAws.detectFaces, - detectModerationLabels: mockAws.detectModerationLabels - })) - }; -}); - -const AWS = require('aws-sdk'); -const s3 = new AWS.S3(); -const rekognition = new AWS.Rekognition(); -const ImageHandler = require('../image-handler'); -const sharp = require('sharp'); - -// ---------------------------------------------------------------------------- -// [async] process() -// ---------------------------------------------------------------------------- -describe('process()', function() { - describe('001/default', function() { - it('Should pass if the output image is different from the input image with edits applied', async function() { - // Arrange - const request = { - requestType: "default", - bucket: "sample-bucket", - key: "sample-image-001.jpg", - edits: { - grayscale: true, - flip: true - }, - originalImage: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') - } - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.process(request); - // Assert - expect(result).not.toEqual(request.originalImage); - }); - }); - describe('002/withToFormat', function() { - it('Should pass if the output image is in a different format than the original image', async function() { - // Arrange - const request = { - requestType: "default", - bucket: "sample-bucket", - key: "sample-image-001.jpg", - outputFormat: "png", - edits: { - grayscale: true, - flip: true - }, - originalImage: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') - } - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.process(request); - // Assert - expect(result).not.toEqual(request.originalImage); - }); - }); - describe('003/noEditsSpecified', function() { - it('Should pass if no edits are specified and the original image is returned', async function() { - // Arrange - const request = { - requestType: "default", - bucket: "sample-bucket", - key: "sample-image-001.jpg", - originalImage: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') - } - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.process(request); - // Assert - expect(result).toEqual(request.originalImage.toString('base64')); - }); - }); - describe('004/ExceedsLambdaPayloadLimit', function() { - it('Should fail the return payload is larger than 6MB', async function() { - // Arrange - const request = { - requestType: "default", - bucket: "sample-bucket", - key: "sample-image-001.jpg", - originalImage: Buffer.alloc(6 * 1024 * 1024) - }; - // Act - const imageHandler = new ImageHandler(s3, rekognition); - try { - await imageHandler.process(request); - } catch (error) { - // Assert - expect(error).toEqual({ - status: '413', - code: 'TooLargeImageException', - message: 'The converted image is too large to return.' - }); - } - }); - }); - describe('005/RotateNull', function() { - it('Should pass if rotate is null and return image without EXIF and ICC', async function() { - // Arrange - const originalImage = fs.readFileSync('./test/image/test.jpg'); - const request = { - requestType: "default", - bucket: "sample-bucket", - key: "test.jpg", - edits: { - rotate: null - }, - originalImage: originalImage - }; - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.process(request); - // Assert - const metadata = await sharp(Buffer.from(result, 'base64')).metadata(); - expect(metadata).not.toHaveProperty('exif'); - expect(metadata).not.toHaveProperty('icc'); - expect(metadata).not.toHaveProperty('orientation'); - }); - }); - describe('006/ImageOrientation', function() { - it('Should pass if the original image has orientation', async function() { - // Arrange - const originalImage = fs.readFileSync('./test/image/test.jpg'); - const request = { - requestType: "default", - bucket: "sample-bucket", - key: "test.jpg", - edits: { - resize: { - width: 100, - height: 100 - } - }, - originalImage: originalImage - }; - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.process(request); - // Assert - const metadata = await sharp(Buffer.from(result, 'base64')).metadata(); - expect(metadata).toHaveProperty('icc'); - expect(metadata).toHaveProperty('exif'); - expect(metadata.orientation).toEqual(3); - }); - }); - describe('007/ImageWithoutOrientation', function() { - it('Should pass if the original image does not have orientation', async function() { - // Arrange - const request = { - requestType: "default", - bucket: "sample-bucket", - key: "test.jpg", - edits: { - resize: { - width: 100, - height: 100 - } - }, - originalImage: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') - }; - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.process(request); - // Assert - const metadata = await sharp(Buffer.from(result, 'base64')).metadata(); - expect(metadata).not.toHaveProperty('orientation'); - }); - }); -}); - -// ---------------------------------------------------------------------------- -// [async] applyEdits() -// ---------------------------------------------------------------------------- -describe('applyEdits()', function() { - describe('001/standardEdits', function() { - it('Should pass if a series of standard edits are provided to the function', async function() { - // Arrange - const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const edits = { - grayscale: true, - flip: true - } - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.applyEdits(image, edits); - // Assert - const expectedResult1 = result.options.greyscale; - const expectedResult2 = result.options.flip; - const combinedResults = expectedResult1 && expectedResult2; - expect(combinedResults).toEqual(true); - }); - }); - describe('002/overlay', function() { - it('Should pass if an edit with the overlayWith keyname is passed to the function', async function() { - // Arrange - const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const edits = { - overlayWith: { - bucket: 'aaa', - key: 'bbb' - } - } - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ Body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') }); - } - }; - }); - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.applyEdits(image, edits); - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'aaa', Key: 'bbb' }); - expect(result.options.input.buffer).toEqual(originalImage); - }); - }); - describe('003/overlay/options/smallerThanZero', function() { - it('Should pass if an edit with the overlayWith keyname is passed to the function', async function() { - // Arrange - const originalImage = Buffer.from('/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAAEAAQDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACv/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AfwD/2Q==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const edits = { - overlayWith: { - bucket: 'aaa', - key: 'bbb', - options: { - left: '-1', - top: '-1' - } - } - } - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ Body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') }); - } - }; - }); - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.applyEdits(image, edits); - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'aaa', Key: 'bbb' }); - expect(result.options.input.buffer).toEqual(originalImage); - }); - }); - describe('004/overlay/options/greaterThanZero', function() { - it('Should pass if an edit with the overlayWith keyname is passed to the function', async function() { - // Arrange - const originalImage = Buffer.from('/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAAEAAQDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACv/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AfwD/2Q==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const edits = { - overlayWith: { - bucket: 'aaa', - key: 'bbb', - options: { - left: '1', - top: '1' - } - } - } - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ Body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') }); - } - }; - }); - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.applyEdits(image, edits); - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'aaa', Key: 'bbb' }); - expect(result.options.input.buffer).toEqual(originalImage); - }); - }); - describe('005/overlay/options/percentage/greaterThanZero', function() { - it('Should pass if an edit with the overlayWith keyname is passed to the function', async function() { - // Arrange - const originalImage = Buffer.from('/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAAEAAQDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACv/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AfwD/2Q==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const edits = { - overlayWith: { - bucket: 'aaa', - key: 'bbb', - options: { - left: '50p', - top: '50p' - } - } - } - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ Body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') }); - } - }; - }); - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.applyEdits(image, edits); - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'aaa', Key: 'bbb' }); - expect(result.options.input.buffer).toEqual(originalImage); - }); - }); - describe('006/overlay/options/percentage/smallerThanZero', function() { - it('Should pass if an edit with the overlayWith keyname is passed to the function', async function() { - // Arrange - const originalImage = Buffer.from('/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAAEAAQDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACv/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AfwD/2Q==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const edits = { - overlayWith: { - bucket: 'aaa', - key: 'bbb', - options: { - left: '-50p', - top: '-50p' - } - } - } - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ Body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') }); - } - }; - }); - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.applyEdits(image, edits); - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'aaa', Key: 'bbb' }); - expect(result.options.input.buffer).toEqual(originalImage); - }); - }); - describe('007/smartCrop', function() { - it('Should pass if an edit with the smartCrop keyname is passed to the function', async function() { - // Arrange - const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const buffer = await image.toBuffer(); - const edits = { - smartCrop: { - faceIndex: 0, - padding: 0 - } - } - // Mock - mockAws.detectFaces.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - FaceDetails: [{ - BoundingBox: { - Height: 0.18, - Left: 0.55, - Top: 0.33, - Width: 0.23 - } - }] - }); - } - }; - }); - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.applyEdits(image, edits); - // Assert - expect(mockAws.detectFaces).toHaveBeenCalledWith({ Image: { Bytes: buffer }}); - expect(result.options.input).not.toEqual(originalImage); - }); - }); - describe('008/smartCrop/paddingOutOfBoundsError', function() { - it('Should pass if an excessive padding value is passed to the smartCrop filter', async function() { - // Arrange - const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const buffer = await image.toBuffer(); - const edits = { - smartCrop: { - faceIndex: 0, - padding: 80 - } - } - // Mock - mockAws.detectFaces.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - FaceDetails: [{ - BoundingBox: { - Height: 0.18, - Left: 0.55, - Top: 0.33, - Width: 0.23 - } - }] - }); - } - }; - }); - // Act - try { - const imageHandler = new ImageHandler(s3, rekognition); - await imageHandler.applyEdits(image, edits); - } catch (error) { - // Assert - expect(mockAws.detectFaces).toHaveBeenCalledWith({ Image: { Bytes: buffer }}); - expect(error).toEqual({ - status: 400, - code: 'SmartCrop::PaddingOutOfBounds', - message: 'The padding value you provided exceeds the boundaries of the original image. Please try choosing a smaller value or applying padding via Sharp for greater specificity.' - }); - } - }); - }); - describe('009/smartCrop/boundingBoxError', function() { - it('Should pass if an excessive faceIndex value is passed to the smartCrop filter', async function() { - // Arrange - const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const buffer = await image.toBuffer(); - const edits = { - smartCrop: { - faceIndex: 10, - padding: 0 - } - } - // Mock - mockAws.detectFaces.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - FaceDetails: [{ - BoundingBox: { - Height: 0.18, - Left: 0.55, - Top: 0.33, - Width: 0.23 - } - }] - }); - } - }; - }); - // Act - try { - const imageHandler = new ImageHandler(s3, rekognition); - await imageHandler.applyEdits(image, edits); - } catch (error) { - // Assert - expect(mockAws.detectFaces).toHaveBeenCalledWith({ Image: { Bytes: buffer }}); - expect(error).toEqual({ - status: 400, - code: 'SmartCrop::FaceIndexOutOfRange', - message: 'You have provided a FaceIndex value that exceeds the length of the zero-based detectedFaces array. Please specify a value that is in-range.' - }); - } - }); - }); - describe('010/smartCrop/faceIndexUndefined', function() { - it('Should pass if a faceIndex value of undefined is passed to the smartCrop filter', async function() { - // Arrange - const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const buffer = await image.toBuffer(); - const edits = { - smartCrop: true - } - // Mock - mockAws.detectFaces.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - FaceDetails: [{ - BoundingBox: { - Height: 0.18, - Left: 0.55, - Top: 0.33, - Width: 0.23 - } - }] - }); - } - }; - }); - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.applyEdits(image, edits); - // Assert - expect(mockAws.detectFaces).toHaveBeenCalledWith({ Image: { Bytes: buffer }}); - expect(result.options.input).not.toEqual(originalImage); - }); - }); - describe('011/resizeStringTypeNumber', function() { - it('Should pass if resize width and height are provided as string number to the function', async function() { - // Arrange - const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const edits = { - resize: { - width: '99.1', - height: '99.9' - } - } - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.applyEdits(image, edits); - // Assert - const resultBuffer = await result.toBuffer(); - const convertedImage = await sharp(originalImage, { failOnError: false }).withMetadata().resize({ width: 99, height: 100 }).toBuffer(); - expect(resultBuffer).toEqual(convertedImage); - }); - }); - describe('012/roundCrop/noOptions', function() { - it('Should pass if roundCrop keyName is passed with no additional options', async function() { - // Arrange - const originalImage = Buffer.from('/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAAEAAQDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACv/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AfwD/2Q==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const metadata = image.metadata(); - - const edits = { - roundCrop: true, - - } - - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.applyEdits(image, edits); - - // Assert - const expectedResult = {width: metadata.width / 2, height: metadata.height / 2} - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'aaa', Key: 'bbb' }); - expect(result.options.input).not.toEqual(expectedResult); - }); - }); - describe('013/roundCrop/withOptions', function() { - it('Should pass if roundCrop keyName is passed with additional options', async function() { - // Arrange - const originalImage = Buffer.from('/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAAEAAQDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACv/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AfwD/2Q==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const metadata = image.metadata(); - - const edits = { - roundCrop: { - top: 100, - left: 100, - rx: 100, - ry: 100, - }, - - } - - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.applyEdits(image, edits); - - // Assert - const expectedResult = {width: metadata.width / 2, height: metadata.height / 2} - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'aaa', Key: 'bbb' }); - expect(result.options.input).not.toEqual(expectedResult); - }); - }); - describe('014/contentModeration', function() { - it('Should pass and blur image with minConfidence provided', async function() { - // Arrange - const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const buffer = await image.toBuffer(); - const edits = { - contentModeration: { - minConfidence: 75 - } - } - // Mock - mockAws.detectModerationLabels.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - ModerationLabels: [ - { - Confidence: 99.76720428466, - Name: 'Smoking', - ParentName: 'Tobacco' - }, - { Confidence: 99.76720428466, Name: 'Tobacco', ParentName: '' } - ], - ModerationModelVersion: '4.0' - }); - } - }; - }); - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.applyEdits(image, edits); - const expected = image.blur(50); - // Assert - expect(mockAws.detectFaces).toHaveBeenCalledWith({ Image: { Bytes: buffer }}); - expect(result.options.input).not.toEqual(originalImage); - expect(result).toEqual(expected); - }); - it("should pass and blur to specified amount if blur option is provided", async function() { - // Arrange - const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const buffer = await image.toBuffer(); - const edits = { - contentModeration: { - minConfidence: 75, - blur: 100 - } - } - // Mock - mockAws.detectModerationLabels.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - ModerationLabels: [ - { - Confidence: 99.76720428466, - Name: 'Smoking', - ParentName: 'Tobacco' - }, - { Confidence: 99.76720428466, Name: 'Tobacco', ParentName: '' } - ], - ModerationModelVersion: '4.0' - }); - } - }; - }); - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.applyEdits(image, edits); - const expected = image.blur(100); - // Assert - expect(mockAws.detectFaces).toHaveBeenCalledWith({ Image: { Bytes: buffer }}); - expect(result.options.input).not.toEqual(originalImage); - expect(result).toEqual(expected); - }); - it("should pass and blur if content moderation label matches specied moderartion label", async function() { - // Arrange - const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const buffer = await image.toBuffer(); - const edits = { - contentModeration: { - moderationLabels: ["Smoking"] - } - } - // Mock - mockAws.detectModerationLabels.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - ModerationLabels: [ - { - Confidence: 99.76720428466, - Name: 'Smoking', - ParentName: 'Tobacco' - }, - { Confidence: 99.76720428466, Name: 'Tobacco', ParentName: '' } - ], - ModerationModelVersion: '4.0' - }); - } - }; - }); - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.applyEdits(image, edits); - const expected = image.blur(50); - // Assert - expect(mockAws.detectFaces).toHaveBeenCalledWith({ Image: { Bytes: buffer }}); - expect(result.options.input).not.toEqual(originalImage); - expect(result).toEqual(expected); - }); - it("should not blur if provided moderationLabels not found", async function() { - // Arrange - const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const buffer = await image.toBuffer(); - const edits = { - contentModeration: { - minConfidence: 75, - blur: 100, - moderationLabels: ['Alcohol'] - } - } - // Mock - mockAws.detectModerationLabels.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - ModerationLabels: [ - { - Confidence: 99.76720428466, - Name: 'Smoking', - ParentName: 'Tobacco' - }, - { Confidence: 99.76720428466, Name: 'Tobacco', ParentName: '' } - ], - ModerationModelVersion: '4.0' - }); - } - }; - }); - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.applyEdits(image, edits); - // Assert - expect(mockAws.detectFaces).toHaveBeenCalledWith({ Image: { Bytes: buffer }}); - expect(result).toEqual(image); - }); - it("should fail if rekognition returns an error", async function() { - // Arrange - const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); - const image = sharp(originalImage, { failOnError: false }).withMetadata(); - const buffer = await image.toBuffer(); - const edits = { - contentModeration: { - minConfidence: 75, - blur: 100 - } - } - // Mock - mockAws.detectModerationLabels.mockImplementationOnce(() => { - return { - promise() { - return Promise.reject({ - status: 500, - code: 'InternalServerError', - message: 'Amazon Rekognition experienced a service issue. Try your call again.' - }); - } - }; - }); - // Act - const imageHandler = new ImageHandler(s3, rekognition); - try { - const result = await imageHandler.applyEdits(image, edits); - } catch(error) { - // Assert - expect(mockAws.detectFaces).toHaveBeenCalledWith({ Image: { Bytes: buffer }}); - expect(error).toEqual({ - status: 500, - code: 'InternalServerError', - message: 'Amazon Rekognition experienced a service issue. Try your call again.' - }); - } - }); - }); -}); - -// ---------------------------------------------------------------------------- -// [async] getOverlayImage() -// ---------------------------------------------------------------------------- -describe('getOverlayImage()', function() { - describe('001/validParameters', function() { - it('Should pass if the proper bucket name and key are supplied, simulating an image file that can be retrieved', async function() { - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ Body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') }); - } - }; - }); - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const metadata = await sharp(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64')).metadata(); - const result = await imageHandler.getOverlayImage('validBucket', 'validKey', '100', '100', '20', metadata); - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'validBucket', Key: 'validKey' }); - expect(result).toEqual(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAsTAAALEwEAmpwYAAAADUlEQVQI12P4z8CQCgAEZgFlTg0nBwAAAABJRU5ErkJggg==', 'base64')); - }); - }); - describe('002/imageDoesNotExist', function() { - it('Should throw an error if an invalid bucket or key name is provided, simulating a non-existant overlay image', async function() { - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.reject({ - code: 'InternalServerError', - message: 'SimulatedInvalidParameterException' - }); - } - }; - }); - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const metadata = await sharp(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64')).metadata(); - try { - await imageHandler.getOverlayImage('invalidBucket', 'invalidKey', '100', '100', '20', metadata); - } catch (error) { - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'invalidBucket', Key: 'invalidKey' }); - expect(error).toEqual({ - status: 500, - code: 'InternalServerError', - message: 'SimulatedInvalidParameterException' - }); - } - }); - }); -}); - -// ---------------------------------------------------------------------------- -// [async] getCropArea() -// ---------------------------------------------------------------------------- -describe('getCropArea()', function() { - describe('001/validParameters', function() { - it('Should pass if the crop area can be calculated using a series of valid inputs/parameters', function() { - // Arrange - const boundingBox = { - Height: 0.18, - Left: 0.55, - Top: 0.33, - Width: 0.23 - }; - const options = { padding: 20 }; - const metadata = { - width: 200, - height: 400 - }; - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = imageHandler.getCropArea(boundingBox, options, metadata); - // Assert - const expectedResult = { - left: 90, - top: 112, - width: 86, - height: 112 - }; - expect(result).toEqual(expectedResult); - }); - }); -}); - - -// ---------------------------------------------------------------------------- -// [async] getBoundingBox() -// ---------------------------------------------------------------------------- -describe('getBoundingBox()', function() { - describe('001/validParameters', function() { - it('Should pass if the proper parameters are passed to the function', async function() { - // Arrange - const currentImage = Buffer.from('TestImageData'); - const faceIndex = 0; - // Mock - mockAws.detectFaces.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - FaceDetails: [{ - BoundingBox: { - Height: 0.18, - Left: 0.55, - Top: 0.33, - Width: 0.23 - } - }] - }); - } - }; - }); - // Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.getBoundingBox(currentImage, faceIndex); - // Assert - const expectedResult = { - Height: 0.18, - Left: 0.55, - Top: 0.33, - Width: 0.23 - }; - expect(mockAws.detectFaces).toHaveBeenCalledWith({ Image: { Bytes: currentImage }}); - expect(result).toEqual(expectedResult); - }); - }); - describe('002/errorHandling', function() { - it('Should simulate an error condition returned by Rekognition', async function() { - // Arrange - const currentImage = Buffer.from('NotTestImageData'); - const faceIndex = 0; - // Mock - mockAws.detectFaces.mockImplementationOnce(() => { - return { - promise() { - return Promise.reject({ - code: 'InternalServerError', - message: 'SimulatedError' - }); - } - }; - }); - // Act - const imageHandler = new ImageHandler(s3, rekognition); - try { - await imageHandler.getBoundingBox(currentImage, faceIndex); - } catch (error) { - // Assert - expect(mockAws.detectFaces).toHaveBeenCalledWith({ Image: { Bytes: currentImage }}); - expect(error).toEqual({ - status: 500, - code: 'InternalServerError', - message: 'SimulatedError' - }); - } - }); - }); - describe('003/noDetectedFaces', function () { - it('Should pass if no faces are detected', async function () { - //Arrange - const currentImage = Buffer.from('TestImageData'); - const faceIndex = 0; - - // Mock - mockAws.detectFaces.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - FaceDetails: [] - }); - } - }; - }); - - //Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.getBoundingBox(currentImage, faceIndex); - - // Assert - const expectedResult = { - Height: 1, - Left: 0, - Top: 0, - Width: 1 - }; - expect(mockAws.detectFaces).toHaveBeenCalledWith({ Image: { Bytes: currentImage }}); - expect(result).toEqual(expectedResult); - }); - }); - describe('004/boundsGreaterThanImageDimensions', function () { - it('Should pass if bounds detected go beyond the image dimensions', async function () { - //Arrange - const currentImage = Buffer.from('TestImageData'); - const faceIndex = 0; - - // Mock - mockAws.detectFaces.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - FaceDetails: [{ - BoundingBox: { - Height: 1, - Left: 0.50, - Top: 0.30, - Width: 0.65 - } - }] - }); - } - }; - }); - - //Act - const imageHandler = new ImageHandler(s3, rekognition); - const result = await imageHandler.getBoundingBox(currentImage, faceIndex); - - // Assert - const expectedResult = { - Height: 0.70, - Left: 0.50, - Top: 0.30, - Width: 0.50 - }; - expect(mockAws.detectFaces).toHaveBeenCalledWith({ Image: { Bytes: currentImage }}); - expect(result).toEqual(expectedResult); - }); - }); -}); \ No newline at end of file diff --git a/source/image-handler/test/image-request.spec.js b/source/image-handler/test/image-request.spec.js deleted file mode 100644 index 80a69d87f..000000000 --- a/source/image-handler/test/image-request.spec.js +++ /dev/null @@ -1,1271 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -const mockAws = { - getObject: jest.fn(), - getSecretValue: jest.fn() -}; -jest.mock('aws-sdk', () => { - return { - S3: jest.fn(() => ({ - getObject: mockAws.getObject - })), - SecretsManager: jest.fn(() => ({ - getSecretValue: mockAws.getSecretValue - })) - }; -}); - -const AWS = require('aws-sdk'); -const s3 = new AWS.S3(); -const secretsManager = new AWS.SecretsManager(); -const ImageRequest = require('../image-request'); - -// ---------------------------------------------------------------------------- -// [async] setup() -// ---------------------------------------------------------------------------- -describe('setup()', function() { - beforeEach(() => { - mockAws.getObject.mockReset(); - }); - - describe('001/defaultImageRequest', function() { - it('Should pass when a default image request is provided and populate the ImageRequest object with the proper values', async function() { - // Arrange - const event = { - path : '/eyJidWNrZXQiOiJ2YWxpZEJ1Y2tldCIsImtleSI6InZhbGlkS2V5IiwiZWRpdHMiOnsiZ3JheXNjYWxlIjp0cnVlfSwib3V0cHV0Rm9ybWF0IjoianBlZyJ9' - } - process.env = { - SOURCE_BUCKETS : "validBucket, validBucket2" - } - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ Body: Buffer.from('SampleImageContent\n') }); - } - }; - }); - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - await imageRequest.setup(event); - const expectedResult = { - requestType: 'Default', - bucket: 'validBucket', - key: 'validKey', - edits: { grayscale: true }, - outputFormat: 'jpeg', - originalImage: Buffer.from('SampleImageContent\n'), - CacheControl: 'max-age=31536000,public', - ContentType: 'image/jpeg' - }; - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'validBucket', Key: 'validKey' }); - expect(imageRequest).toEqual(expectedResult); - }); - }); - describe('002/defaultImageRequest/toFormat', function() { - it('Should pass when a default image request is provided and populate the ImageRequest object with the proper values', async function() { - // Arrange - const event = { - path : '/eyJidWNrZXQiOiJ2YWxpZEJ1Y2tldCIsImtleSI6InZhbGlkS2V5IiwiZWRpdHMiOnsidG9Gb3JtYXQiOiJwbmcifX0=', - } - process.env = { - SOURCE_BUCKETS : "validBucket, validBucket2" - } - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ Body: Buffer.from('SampleImageContent\n') }); - } - }; - }); - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - await imageRequest.setup(event); - const expectedResult = { - requestType: 'Default', - bucket: 'validBucket', - key: 'validKey', - edits: { toFormat: 'png' }, - outputFormat: 'png', - originalImage: Buffer.from('SampleImageContent\n'), - CacheControl: 'max-age=31536000,public', - ContentType: 'image/png' - } - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'validBucket', Key: 'validKey' }); - expect(imageRequest).toEqual(expectedResult); - }); - }); - describe('003/thumborImageRequest', function() { - it('Should pass when a thumbor image request is provided and populate the ImageRequest object with the proper values', async function() { - // Arrange - const event = { - path : "/filters:grayscale()/test-image-001.jpg" - } - process.env = { - SOURCE_BUCKETS : "allowedBucket001, allowedBucket002" - } - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ Body: Buffer.from('SampleImageContent\n') }); - } - }; - }); - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - await imageRequest.setup(event); - const expectedResult = { - requestType: 'Thumbor', - bucket: 'allowedBucket001', - key: 'test-image-001.jpg', - edits: { grayscale: true }, - originalImage: Buffer.from('SampleImageContent\n'), - CacheControl: 'max-age=31536000,public', - ContentType: 'image' - } - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'allowedBucket001', Key: 'test-image-001.jpg' }); - expect(imageRequest).toEqual(expectedResult); - }); - }); - describe('004/thumborImageRequest/quality', function() { - it('Should pass when a thumbor image request is provided and populate the ImageRequest object with the proper values', async function() { - // Arrange - const event = { - path : "/filters:format(png)/filters:quality(50)/test-image-001.jpg" - } - process.env = { - SOURCE_BUCKETS : "allowedBucket001, allowedBucket002" - } - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ Body: Buffer.from('SampleImageContent\n') }); - } - }; - }); - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - await imageRequest.setup(event); - const expectedResult = { - requestType: 'Thumbor', - bucket: 'allowedBucket001', - key: 'test-image-001.jpg', - edits: { - toFormat: 'png', - png: { quality: 50 } - }, - originalImage: Buffer.from('SampleImageContent\n'), - CacheControl: 'max-age=31536000,public', - outputFormat: 'png', - ContentType: 'image/png' - } - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'allowedBucket001', Key: 'test-image-001.jpg' }); - expect(imageRequest).toEqual(expectedResult); - }); - }); - describe('005/customImageRequest', function() { - it('Should pass when a custom image request is provided and populate the ImageRequest object with the proper values', async function() { - // Arrange - const event = { - path : '/filters-rotate(90)/filters-grayscale()/custom-image.jpg' - } - process.env = { - SOURCE_BUCKETS : "allowedBucket001, allowedBucket002", - REWRITE_MATCH_PATTERN: /(filters-)/gm, - REWRITE_SUBSTITUTION: 'filters:' - } - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - CacheControl: 'max-age=300,public', - ContentType: 'custom-type', - Expires: 'Tue, 24 Dec 2019 13:46:28 GMT', - LastModified: 'Sat, 19 Dec 2009 16:30:47 GMT', - Body: Buffer.from('SampleImageContent\n') - }); - } - }; - }); - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - await imageRequest.setup(event); - const expectedResult = { - requestType: 'Custom', - bucket: 'allowedBucket001', - key: 'custom-image.jpg', - edits: { - grayscale: true, - rotate: 90 - }, - originalImage: Buffer.from('SampleImageContent\n'), - CacheControl: 'max-age=300,public', - ContentType: 'custom-type', - Expires: 'Tue, 24 Dec 2019 13:46:28 GMT', - LastModified: 'Sat, 19 Dec 2009 16:30:47 GMT', - } - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'allowedBucket001', Key: 'custom-image.jpg' }); - expect(imageRequest).toEqual(expectedResult); - }); - }); - describe('006/errorCase', function() { - it('Should pass when an error is caught', async function() { - // Arrange - const event = { - path : '/eyJidWNrZXQiOiJ2YWxpZEJ1Y2tldCIsImtleSI6InZhbGlkS2V5IiwiZWRpdHMiOnsiZ3JheXNjYWxlIjp0cnVlfX0=' - } - process.env = { - SOURCE_BUCKETS : "allowedBucket001, allowedBucket002" - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - // Assert - try { - await imageRequest.setup(event); - } catch (error) { - expect(error.code).toEqual('ImageBucket::CannotAccessBucket'); - } - }); - }); - describe('007/enableSignature', function() { - beforeAll(() => { - process.env.ENABLE_SIGNATURE = 'Yes'; - process.env.SECRETS_MANAGER = 'serverless-image-hander'; - process.env.SECRET_KEY = 'signatureKey'; - process.env.SOURCE_BUCKETS = 'validBucket'; - }); - it('Should pass when the image signature is correct', async function() { - // Arrange - const event = { - path : '/eyJidWNrZXQiOiJ2YWxpZEJ1Y2tldCIsImtleSI6InZhbGlkS2V5IiwiZWRpdHMiOnsidG9Gb3JtYXQiOiJwbmcifX0=', - queryStringParameters: { - signature: '4d41311006641a56de7bca8abdbda91af254506107a2c7b338a13ca2fa95eac3' - } - }; - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ Body: Buffer.from('SampleImageContent\n') }); - } - }; - }); - mockAws.getSecretValue.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - SecretString: JSON.stringify({ - [process.env.SECRET_KEY]: 'secret' - }) - }); - } - }; - }); - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - await imageRequest.setup(event); - const expectedResult = { - requestType: 'Default', - bucket: 'validBucket', - key: 'validKey', - edits: { toFormat: 'png' }, - outputFormat: 'png', - originalImage: Buffer.from('SampleImageContent\n'), - CacheControl: 'max-age=31536000,public', - ContentType: 'image/png' - } - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'validBucket', Key: 'validKey' }); - expect(mockAws.getSecretValue).toHaveBeenCalledWith({ SecretId: process.env.SECRETS_MANAGER }); - expect(imageRequest).toEqual(expectedResult); - }); - it('Should throw an error when queryStringParameters are missing', async function() { - // Arrange - const event = { - path : '/eyJidWNrZXQiOiJ2YWxpZEJ1Y2tldCIsImtleSI6InZhbGlkS2V5IiwiZWRpdHMiOnsidG9Gb3JtYXQiOiJwbmcifX0=', - }; - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - try { - await imageRequest.setup(event); - } catch (error) { - // Assert - expect(error).toEqual({ - status: 400, - message: 'Query-string requires the signature parameter.', - code: 'AuthorizationQueryParametersError' - }); - } - }); - it('Should throw an error when the image signature query parameter is missing', async function() { - // Arrange - const event = { - path : '/eyJidWNrZXQiOiJ2YWxpZEJ1Y2tldCIsImtleSI6InZhbGlkS2V5IiwiZWRpdHMiOnsidG9Gb3JtYXQiOiJwbmcifX0=', - queryStringParameters: { - sign: '4d41311006641a56de7bca8abdbda91af254506107a2c7b338a13ca2fa95eac3' - } - }; - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - try { - await imageRequest.setup(event); - } catch (error) { - // Assert - expect(error).toEqual({ - status: 400, - message: 'Query-string requires the signature parameter.', - code: 'AuthorizationQueryParametersError' - }); - } - }); - it('Should throw an error when signature does not match', async function() { - // Arrange - const event = { - path : '/eyJidWNrZXQiOiJ2YWxpZEJ1Y2tldCIsImtleSI6InZhbGlkS2V5IiwiZWRpdHMiOnsidG9Gb3JtYXQiOiJwbmcifX0=', - queryStringParameters: { - signature: 'invalid' - } - }; - // Mock - mockAws.getSecretValue.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - SecretString: JSON.stringify({ - [process.env.SECRET_KEY]: 'secret' - }) - }); - } - }; - }); - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - try { - await imageRequest.setup(event); - } catch (error) { - // Assert - expect(mockAws.getSecretValue).toHaveBeenCalledWith({ SecretId: process.env.SECRETS_MANAGER }); - expect(error).toEqual({ - status: 403, - message: 'Signature does not match.', - code: 'SignatureDoesNotMatch' - }); - } - }); - it('Should throw an error when any other error occurs', async function() { - // Arrange - const event = { - path : '/eyJidWNrZXQiOiJ2YWxpZEJ1Y2tldCIsImtleSI6InZhbGlkS2V5IiwiZWRpdHMiOnsidG9Gb3JtYXQiOiJwbmcifX0=', - queryStringParameters: { - signature: '4d41311006641a56de7bca8abdbda91af254506107a2c7b338a13ca2fa95eac3' - } - }; - // Mock - mockAws.getSecretValue.mockImplementationOnce(() => { - return { - promise() { - return Promise.reject({ - message: 'SimulatedError', - code: 'InternalServerError' - }); - } - }; - }); - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - try { - await imageRequest.setup(event); - } catch (error) { - // Assert - expect(mockAws.getSecretValue).toHaveBeenCalledWith({ SecretId: process.env.SECRETS_MANAGER }); - expect(error).toEqual({ - status: 500, - message: 'Signature validation failed.', - code: 'SignatureValidationFailure' - }); - } - }); - }); - describe('008/SVGSupport', function() { - beforeAll(() => { - process.env.ENABLE_SIGNATURE = 'No'; - process.env.SOURCE_BUCKETS = 'validBucket'; - }); - it('Should return SVG image when no edit is provided for the SVG image', async function() { - // Arrange - const event = { - path : '/image.svg' - }; - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - ContentType: 'image/svg+xml', - Body: Buffer.from('SampleImageContent\n') - }); - } - }; - }); - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - await imageRequest.setup(event); - const expectedResult = { - requestType: 'Thumbor', - bucket: 'validBucket', - key: 'image.svg', - edits: {}, - originalImage: Buffer.from('SampleImageContent\n'), - CacheControl: 'max-age=31536000,public', - ContentType: 'image/svg+xml' - }; - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'validBucket', Key: 'image.svg' }); - expect(imageRequest).toEqual(expectedResult); - }); - it('Should return WebP image when there are any edits and no output is specified for the SVG image', async function() { - // Arrange - const event = { - path : '/100x100/image.svg', - }; - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - ContentType: 'image/svg+xml', - Body: Buffer.from('SampleImageContent\n') - }); - } - }; - }); - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - await imageRequest.setup(event); - const expectedResult = { - requestType: 'Thumbor', - bucket: 'validBucket', - key: 'image.svg', - edits: { resize: { width: 100, height: 100 } }, - outputFormat: 'png', - originalImage: Buffer.from('SampleImageContent\n'), - CacheControl: 'max-age=31536000,public', - ContentType: 'image/png' - }; - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'validBucket', Key: 'image.svg' }); - expect(imageRequest).toEqual(expectedResult); - }); - it('Should return JPG image when output is specified to JPG for the SVG image', async function() { - // Arrange - const event = { - path : '/filters:format(jpg)/image.svg', - }; - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - ContentType: 'image/svg+xml', - Body: Buffer.from('SampleImageContent\n') - }); - } - }; - }); - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - await imageRequest.setup(event); - const expectedResult = { - requestType: 'Thumbor', - bucket: 'validBucket', - key: 'image.svg', - edits: { toFormat: 'jpeg' }, - outputFormat: 'jpeg', - originalImage: Buffer.from('SampleImageContent\n'), - CacheControl: 'max-age=31536000,public', - ContentType: 'image/jpeg' - }; - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'validBucket', Key: 'image.svg' }); - expect(imageRequest).toEqual(expectedResult); - }); - }); - describe('009/customHeaders', function() { - it('Should pass and return the customer headers if custom headers are provided', async function() { - // Arrange - const event = { - path : '/eyJidWNrZXQiOiJ2YWxpZEJ1Y2tldCIsImtleSI6InZhbGlkS2V5IiwiaGVhZGVycyI6eyJDYWNoZS1Db250cm9sIjoibWF4LWFnZT0zMTUzNjAwMCxwdWJsaWMifSwib3V0cHV0Rm9ybWF0IjoianBlZyJ9' - } - process.env.SOURCE_BUCKETS = 'validBucket, validBucket2'; - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ Body: Buffer.from('SampleImageContent\n') }); - } - }; - }); - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - await imageRequest.setup(event); - const expectedResult = { - requestType: 'Default', - bucket: 'validBucket', - key: 'validKey', - headers: { 'Cache-Control': 'max-age=31536000,public' }, - outputFormat: 'jpeg', - originalImage: Buffer.from('SampleImageContent\n'), - CacheControl: 'max-age=31536000,public', - ContentType: 'image/jpeg' - }; - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'validBucket', Key: 'validKey' }); - expect(imageRequest).toEqual(expectedResult); - }); - }); -}); -// ---------------------------------------------------------------------------- -// getOriginalImage() -// ---------------------------------------------------------------------------- -describe('getOriginalImage()', function() { - beforeEach(() => { - mockAws.getObject.mockReset(); - }); - - describe('001/imageExists', function() { - it('Should pass if the proper bucket name and key are supplied, simulating an image file that can be retrieved', async function() { - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ Body: Buffer.from('SampleImageContent\n') }); - } - }; - }); - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = await imageRequest.getOriginalImage('validBucket', 'validKey'); - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'validBucket', Key: 'validKey' }); - expect(result).toEqual(Buffer.from('SampleImageContent\n')); - }); - }); - describe('002/imageDoesNotExist', function() { - it('Should throw an error if an invalid bucket or key name is provided, simulating a non-existant original image', async function() { - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.reject({ - code: 'NoSuchKey', - message: 'SimulatedException' - }); - } - }; - }); - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - // Assert - try { - await imageRequest.getOriginalImage('invalidBucket', 'invalidKey'); - } catch (error) { - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'invalidBucket', Key: 'invalidKey' }); - expect(error.status).toEqual(404); - } - }); - }); - describe('003/unknownError', function() { - it('Should throw an error if an unkown problem happens when getting an object', async function() { - // Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.reject({ - code: 'InternalServerError', - message: 'SimulatedException' - }); - } - }; - }); - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - // Assert - try { - await imageRequest.getOriginalImage('invalidBucket', 'invalidKey'); - } catch (error) { - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'invalidBucket', Key: 'invalidKey' }); - expect(error.status).toEqual(500); - } - }); - }); - describe('004/noExtension', function() { - const testFiles = [[0x89,0x50,0x4E,0x47],[0xFF,0xD8,0xFF,0xDB],[0xFF,0xD8,0xFF,0xE0],[0xFF,0xD8,0xFF,0xEE],[0xFF,0xD8,0xFF,0xE1],[0x52,0x49,0x46,0x46],[0x49,0x49,0x2A,0x00],[0x4D,0x4D,0x00,0x2A]]; - const expectFileType = ["image/png", "image/jpeg", "image/jpeg", "image/jpeg", "image/jpeg", "image/webp", "image/tiff", "image/tiff"]; - testFiles.forEach(function (test, index) {it('Should pass and infer content type if there is no extension, had default s3 content type and it has a vlid key and a valid bucket', async function() { - //Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - ContentType: 'binary/octet-stream', - Body: Buffer.from(new Uint8Array(test)) - }); - } - }; - }) - - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = await imageRequest.getOriginalImage('validBucket', 'validKey'); - // Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'validBucket', Key: 'validKey' }); - expect(result).toEqual(Buffer.from(new Uint8Array(test))); - expect(imageRequest.ContentType).toEqual(expectFileType[index]); - });}) - it('Should fail to infer content type if there is no extension and file header is not recognized', async function() { - //Mock - mockAws.getObject.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - ContentType: 'binary/octet-stream', - Body: Buffer.from(new Uint8Array(test)) - }); - } - }; - }) - - //Act - const imageRequest = new ImageRequest(s3, secretsManager); - try { - const result = await imageRequest.getOriginalImage('validBucket', 'validKey'); - } catch(error){ - //Assert - expect(mockAws.getObject).toHaveBeenCalledWith({ Bucket: 'validBucket', Key: 'validKey' }); - expect(error.status).toEqual(500); - } - }); - }); -}); - -// ---------------------------------------------------------------------------- -// parseImageBucket() -// ---------------------------------------------------------------------------- -describe('parseImageBucket()', function() { - describe('001/defaultRequestType/bucketSpecifiedInRequest/allowed', function() { - it('Should pass if the bucket name is provided in the image request and has been whitelisted in SOURCE_BUCKETS', function() { - // Arrange - const event = { - path : '/eyJidWNrZXQiOiJhbGxvd2VkQnVja2V0MDAxIiwia2V5Ijoic2FtcGxlSW1hZ2VLZXkwMDEuanBnIiwiZWRpdHMiOnsiZ3JheXNjYWxlIjoidHJ1ZSJ9fQ==' - } - process.env = { - SOURCE_BUCKETS : "allowedBucket001, allowedBucket002" - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseImageBucket(event, 'Default'); - // Assert - const expectedResult = 'allowedBucket001'; - expect(result).toEqual(expectedResult); - }); - }); - describe('002/defaultRequestType/bucketSpecifiedInRequest/notAllowed', function() { - it('Should throw an error if the bucket name is provided in the image request but has not been whitelisted in SOURCE_BUCKETS', function() { - // Arrange - const event = { - path : '/eyJidWNrZXQiOiJhbGxvd2VkQnVja2V0MDAxIiwia2V5Ijoic2FtcGxlSW1hZ2VLZXkwMDEuanBnIiwiZWRpdHMiOnsiZ3JheXNjYWxlIjoidHJ1ZSJ9fQ==' - } - process.env = { - SOURCE_BUCKETS : "allowedBucket003, allowedBucket004" - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - // Assert - try { - imageRequest.parseImageBucket(event, 'Default'); - } catch (error) { - expect(error).toEqual({ - status: 403, - code: 'ImageBucket::CannotAccessBucket', - message: 'The bucket you specified could not be accessed. Please check that the bucket is specified in your SOURCE_BUCKETS.' - }); - } - }); - }); - describe('003/defaultRequestType/bucketNotSpecifiedInRequest', function() { - it('Should pass if the image request does not contain a source bucket but SOURCE_BUCKETS contains at least one bucket that can be used as a default', function() { - // Arrange - const event = { - path : '/eyJrZXkiOiJzYW1wbGVJbWFnZUtleTAwMS5qcGciLCJlZGl0cyI6eyJncmF5c2NhbGUiOiJ0cnVlIn19==' - } - process.env = { - SOURCE_BUCKETS : "allowedBucket001, allowedBucket002" - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseImageBucket(event, 'Default'); - // Assert - const expectedResult = 'allowedBucket001'; - expect(result).toEqual(expectedResult); - }); - }); - describe('004/thumborRequestType', function() { - it('Should pass if there is at least one SOURCE_BUCKET specified that can be used as the default for Thumbor requests', function() { - // Arrange - const event = { - path : "/filters:grayscale()/test-image-001.jpg" - } - process.env = { - SOURCE_BUCKETS : "allowedBucket001, allowedBucket002" - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseImageBucket(event, 'Thumbor'); - // Assert - const expectedResult = 'allowedBucket001'; - expect(result).toEqual(expectedResult); - }); - }); - describe('005/customRequestType', function() { - it('Should pass if there is at least one SOURCE_BUCKET specified that can be used as the default for Custom requests', function() { - // Arrange - const event = { - path : "/filters:grayscale()/test-image-001.jpg" - } - process.env = { - SOURCE_BUCKETS : "allowedBucket001, allowedBucket002" - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseImageBucket(event, 'Custom'); - // Assert - const expectedResult = 'allowedBucket001'; - expect(result).toEqual(expectedResult); - }); - }); - describe('006/invalidRequestType', function() { - it('Should pass if there is at least one SOURCE_BUCKET specified that can be used as the default for Custom requests', function() { - // Arrange - const event = { - path : "/filters:grayscale()/test-image-001.jpg" - } - process.env = { - SOURCE_BUCKETS : "allowedBucket001, allowedBucket002" - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - // Assert - try { - imageRequest.parseImageBucket(event, undefined); - } catch (error) { - expect(error).toEqual({ - status: 404, - code: 'ImageBucket::CannotFindBucket', - message: 'The bucket you specified could not be found. Please check the spelling of the bucket name in your request.' - }); - } - }); - }); -}); - -// ---------------------------------------------------------------------------- -// parseImageEdits() -// ---------------------------------------------------------------------------- -describe('parseImageEdits()', function() { - describe('001/defaultRequestType', function() { - it('Should pass if the proper result is returned for a sample base64-encoded image request', function() { - // Arrange - const event = { - path : '/eyJlZGl0cyI6eyJncmF5c2NhbGUiOiJ0cnVlIiwicm90YXRlIjo5MCwiZmxpcCI6InRydWUifX0=' - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseImageEdits(event, 'Default'); - // Assert - const expectedResult = { - grayscale: 'true', - rotate: 90, - flip: 'true' - } - expect(result).toEqual(expectedResult); - }); - }); - describe('002/thumborRequestType', function() { - it('Should pass if the proper result is returned for a sample thumbor-type image request', function() { - // Arrange - const event = { - path : '/filters:rotate(90)/filters:grayscale()/thumbor-image.jpg' - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseImageEdits(event, 'Thumbor'); - // Assert - const expectedResult = { - rotate: 90, - grayscale: true - } - expect(result).toEqual(expectedResult); - }); - }); - describe('003/customRequestType', function() { - it('Should pass if the proper result is returned for a sample custom-type image request', function() { - // Arrange - const event = { - path : '/filters-rotate(90)/filters-grayscale()/thumbor-image.jpg' - } - process.env.REWRITE_MATCH_PATTERN = /(filters-)/gm; - process.env.REWRITE_SUBSTITUTION = 'filters:'; - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseImageEdits(event, 'Custom'); - // Assert - const expectedResult = { - rotate: 90, - grayscale: true - } - expect(result).toEqual(expectedResult); - }); - }); - describe('004/customRequestType', function() { - it('Should throw an error if a requestType is not specified and/or the image edits cannot be parsed', function() { - // Arrange - const event = { - path : '/filters:rotate(90)/filters:grayscale()/other-image.jpg' - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - // Assert - try { - imageRequest.parseImageEdits(event, undefined); - } catch (error) { - expect(error).toEqual({ - status: 400, - code: 'ImageEdits::CannotParseEdits', - message: 'The edits you provided could not be parsed. Please check the syntax of your request and refer to the documentation for additional guidance.' - }); - } - }); - }); -}); - -// ---------------------------------------------------------------------------- -// parseImageKey() -// ---------------------------------------------------------------------------- -describe('parseImageKey()', function() { - describe('001/defaultRequestType', function() { - it('Should pass if an image key value is provided in the default request format', function() { - // Arrange - const event = { - path : '/eyJidWNrZXQiOiJteS1zYW1wbGUtYnVja2V0Iiwia2V5Ijoic2FtcGxlLWltYWdlLTAwMS5qcGcifQ==' - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseImageKey(event, 'Default'); - // Assert - const expectedResult = 'sample-image-001.jpg'; - expect(result).toEqual(expectedResult); - }); - }); - describe('002/defaultRequestType/withSlashRequest', function () { - it('should read image requests with base64 encoding having slash', function () { - const event = { - path : '/eyJidWNrZXQiOiJlbGFzdGljYmVhbnN0YWxrLXVzLWVhc3QtMi0wNjY3ODQ4ODU1MTgiLCJrZXkiOiJlbnYtcHJvZC9nY2MvbGFuZGluZ3BhZ2UvMV81N19TbGltTl9MaWZ0LUNvcnNldC1Gb3ItTWVuLVNOQVAvYXR0YWNobWVudHMvZmZjMWYxNjAtYmQzOC00MWU4LThiYWQtZTNhMTljYzYxZGQzX1/Ys9mE2YrZhSDZhNmK2YHYqiAoMikuanBnIiwiZWRpdHMiOnsicmVzaXplIjp7IndpZHRoIjo0ODAsImZpdCI6ImNvdmVyIn19fQ==' - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseImageKey(event, 'Default'); - // Assert - const expectedResult = 'env-prod/gcc/landingpage/1_57_SlimN_Lift-Corset-For-Men-SNAP/attachments/ffc1f160-bd38-41e8-8bad-e3a19cc61dd3__سليم ليفت (2).jpg'; - expect(result).toEqual(expectedResult); - - }) - }); - describe('003/thumborRequestType', function() { - it('Should pass if an image key value is provided in the thumbor request format', function() { - // Arrange - const event = { - path : '/filters:rotate(90)/filters:grayscale()/thumbor-image.jpg' - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseImageKey(event, 'Thumbor'); - // Assert - const expectedResult = 'thumbor-image.jpg'; - expect(result).toEqual(expectedResult); - }); - }); - describe('004/customRequestType', function() { - it('Should pass if an image key value is provided in the custom request format', function() { - // Arrange - const event = { - path : '/filters-rotate(90)/filters-grayscale()/custom-image.jpg' - }; - process.env.REWRITE_MATCH_PATTERN = /(filters-)/gm; - process.env.REWRITE_SUBSTITUTION = 'filters:'; - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseImageKey(event, 'Custom'); - // Assert - const expectedResult = 'custom-image.jpg'; - expect(result).toEqual(expectedResult); - }); - }); - describe('005/customRequestStringType', function() { - it('Should pass if an image key value is provided in the custom request format', function() { - // Arrange - const event = { - path : '/filters-rotate(90)/filters-grayscale()/custom-image.jpg' - }; - process.env.REWRITE_MATCH_PATTERN = '/(filters-)/gm'; - process.env.REWRITE_SUBSTITUTION = 'filters:'; - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseImageKey(event, 'Custom'); - // Assert - const expectedResult = 'custom-image.jpg'; - expect(result).toEqual(expectedResult); - }); - }); - describe('006/elseCondition', function() { - it('Should throw an error if an unrecognized requestType is passed into the function as a parameter', function() { - // Arrange - const event = { - path : '/filters:rotate(90)/filters:grayscale()/other-image.jpg' - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - // Assert - try { - imageRequest.parseImageKey(event, undefined); - } catch (error) { - expect(error).toEqual({ - status: 404, - code: 'ImageEdits::CannotFindImage', - message: 'The image you specified could not be found. Please check your request syntax as well as the bucket you specified to ensure it exists.' - }); - } - }); - }); -}); - -// ---------------------------------------------------------------------------- -// parseRequestType() -// ---------------------------------------------------------------------------- -describe('parseRequestType()', function() { - describe('001/defaultRequestType', function() { - it('Should pass if the method detects a default request', function() { - // Arrange - const event = { - path: '/eyJidWNrZXQiOiJteS1zYW1wbGUtYnVja2V0Iiwia2V5IjoibXktc2FtcGxlLWtleSIsImVkaXRzIjp7ImdyYXlzY2FsZSI6dHJ1ZX19' - } - process.env = {}; - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseRequestType(event); - // Assert - const expectedResult = 'Default'; - expect(result).toEqual(expectedResult); - }); - }); - describe('002/thumborRequestType', function() { - it('Should pass if the method detects a thumbor request', function() { - // Arrange - const event = { - path: '/unsafe/filters:brightness(10):contrast(30)/https://upload.wikimedia.org/wikipedia/commons/thumb/7/79/Coffee_berries_1.jpg/1200px-Coffee_berries_1.jpg' - } - process.env = {}; - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseRequestType(event); - // Assert - const expectedResult = 'Thumbor'; - expect(result).toEqual(expectedResult); - }); - }); - describe('003/customRequestType', function() { - it('Should pass if the method detects a custom request', function() { - // Arrange - const event = { - path: '/additionalImageRequestParameters/image.jpg' - } - process.env = { - REWRITE_MATCH_PATTERN: 'matchPattern', - REWRITE_SUBSTITUTION: 'substitutionString' - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseRequestType(event); - // Assert - const expectedResult = 'Custom'; - expect(result).toEqual(expectedResult); - }); - }); - describe('004/elseCondition', function() { - it('Should throw an error if the method cannot determine the request type based on the three groups given', function() { - // Arrange - const event = { - path : '12x12e24d234r2ewxsad123d34r.bmp' - } - process.env = {}; - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - // Assert - try { - imageRequest.parseRequestType(event); - } catch (error) { - expect(error).toEqual({ - status: 400, - code: 'RequestTypeError', - message: 'The type of request you are making could not be processed. Please ensure that your original image is of a supported file type (jpg, png, tiff, webp, svg) and that your image request is provided in the correct syntax. Refer to the documentation for additional guidance on forming image requests.' - }); - } - }); - }); -}); - -// ---------------------------------------------------------------------------- -// parseImageHaders() -// ---------------------------------------------------------------------------- -describe('parseImageHaders()', function() { - it('001/Should return headers if headers are provided for a sample base64-encoded image request', function() { - // Arrange - const event = { - path: '/eyJidWNrZXQiOiJ2YWxpZEJ1Y2tldCIsImtleSI6InZhbGlkS2V5IiwiaGVhZGVycyI6eyJDYWNoZS1Db250cm9sIjoibWF4LWFnZT0zMTUzNjAwMCxwdWJsaWMifSwib3V0cHV0Rm9ybWF0IjoianBlZyJ9' - }; - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseImageHeaders(event, 'Default'); - // Assert - const expectedResult = { - 'Cache-Control': 'max-age=31536000,public' - }; - expect(result).toEqual(expectedResult); - }); - it('001/Should retrun undefined if headers are not provided for a base64-encoded image request', function() { - // Arrange - const event = { - path: '/eyJidWNrZXQiOiJ2YWxpZEJ1Y2tldCIsImtleSI6InZhbGlkS2V5In0=' - }; - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseImageHeaders(event, 'Default'); - // Assert - expect(result).toEqual(undefined); - }); - it('001/Should retrun undefined for Thumbor or Custom requests', function() { - // Arrange - const event = { - path: '/test.jpg' - }; - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.parseImageHeaders(event, 'Thumbor'); - // Assert - expect(result).toEqual(undefined); - }); -}); - -// ---------------------------------------------------------------------------- -// decodeRequest() -// ---------------------------------------------------------------------------- -describe('decodeRequest()', function() { - describe('001/validRequestPathSpecified', function() { - it('Should pass if a valid base64-encoded path has been specified', function() { - // Arrange - const event = { - path : '/eyJidWNrZXQiOiJidWNrZXQtbmFtZS1oZXJlIiwia2V5Ijoia2V5LW5hbWUtaGVyZSJ9' - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.decodeRequest(event); - // Assert - const expectedResult = { - bucket: 'bucket-name-here', - key: 'key-name-here' - }; - expect(result).toEqual(expectedResult); - }); - }); - describe('002/invalidRequestPathSpecified', function() { - it('Should throw an error if a valid base64-encoded path has not been specified', function() { - // Arrange - const event = { - path : '/someNonBase64EncodedContentHere' - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - // Assert - try { - imageRequest.decodeRequest(event); - } catch (error) { - expect(error).toEqual({ - status: 400, - code: 'DecodeRequest::CannotDecodeRequest', - message: 'The image request you provided could not be decoded. Please check that your request is base64 encoded properly and refer to the documentation for additional guidance.' - }); - } - }); - }); - describe('003/noPathSpecified', function() { - it('Should throw an error if no path is specified at all', - function() { - // Arrange - const event = {} - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - // Assert - try { - imageRequest.decodeRequest(event); - } catch (error) { - expect(error).toEqual({ - status: 400, - code: 'DecodeRequest::CannotReadPath', - message: 'The URL path you provided could not be read. Please ensure that it is properly formed according to the solution documentation.' - }); - } - }); - }); - describe('004/truncatedPathConfig', function() { - it('Should pass if a valid base64-encoded path has been specified', function() { - process.env = { - TRUNCATE_PATH_PREFIX: 'some-eu-path/' - } - // Arrange - const event = { - path : '/some-eu-path/eyJidWNrZXQiOiJidWNrZXQtbmFtZS1oZXJlIiwia2V5Ijoia2V5LW5hbWUtaGVyZSJ9' - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.decodeRequest(event); - // Assert - const expectedResult = { - bucket: 'bucket-name-here', - key: 'key-name-here' - }; - expect(result).toEqual(expectedResult); - }); - }); -}); - -// ---------------------------------------------------------------------------- -// getAllowedSourceBuckets() -// ---------------------------------------------------------------------------- -describe('getAllowedSourceBuckets()', function() { - describe('001/sourceBucketsSpecified', function() { - it('Should pass if the SOURCE_BUCKETS environment variable is not empty and contains valid inputs', function() { - // Arrange - process.env = { - SOURCE_BUCKETS: 'allowedBucket001, allowedBucket002' - } - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.getAllowedSourceBuckets(); - // Assert - const expectedResult = ['allowedBucket001', 'allowedBucket002']; - expect(result).toEqual(expectedResult); - }); - }); - describe('002/noSourceBucketsSpecified', function() { - it('Should throw an error if the SOURCE_BUCKETS environment variable is empty or does not contain valid values', function() { - // Arrange - process.env = {}; - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - // Assert - try { - imageRequest.getAllowedSourceBuckets(); - } catch (error) { - expect(error).toEqual({ - status: 400, - code: 'GetAllowedSourceBuckets::NoSourceBuckets', - message: 'The SOURCE_BUCKETS variable could not be read. Please check that it is not empty and contains at least one source bucket, or multiple buckets separated by commas. Spaces can be provided between commas and bucket names, these will be automatically parsed out when decoding.' - }); - } - }); - }); -}); - -// ---------------------------------------------------------------------------- -// getOutputFormat() -// ---------------------------------------------------------------------------- -describe('getOutputFormat()', function () { - describe('001/AcceptsHeaderIncludesWebP', function () { - it('Should pass if it returns "webp" for an accepts header which includes webp', function () { - // Arrange - process.env = { - AUTO_WEBP: 'Yes' - }; - const event = { - headers: { - Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3" - } - }; - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.getOutputFormat(event); - // Assert - expect(result).toEqual('webp'); - }); - }); - describe('002/AcceptsHeaderDoesNotIncludeWebP', function () { - it('Should pass if it returns null for an accepts header which does not include webp', function () { - // Arrange - process.env = { - AUTO_WEBP: 'Yes' - }; - const event = { - headers: { - Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/apng,*/*;q=0.8,application/signed-exchange;v=b3" - } - }; - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.getOutputFormat(event); - // Assert - expect(result).toEqual(null); - }); - }); - describe('003/AutoWebPDisabled', function () { - it('Should pass if it returns null when AUTO_WEBP is disabled with accepts header including webp', function () { - // Arrange - process.env = { - AUTO_WEBP: 'No' - }; - const event = { - headers: { - Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3" - } - }; - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.getOutputFormat(event); - // Assert - expect(result).toEqual(null); - }); - }); - describe('004/AutoWebPUnset', function () { - it('Should pass if it returns null when AUTO_WEBP is not set with accepts header including webp', function () { - // Arrange - const event = { - headers: { - Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3" - } - }; - // Act - const imageRequest = new ImageRequest(s3, secretsManager); - const result = imageRequest.getOutputFormat(event); - // Assert - expect(result).toEqual(null); - }); - }); -}); diff --git a/source/image-handler/test/image-request.test.js b/source/image-handler/test/image-request.test.js new file mode 100644 index 000000000..66ed35d29 --- /dev/null +++ b/source/image-handler/test/image-request.test.js @@ -0,0 +1,83 @@ +jest.mock('aws-sdk/clients/s3', () => + jest.fn().mockImplementation(() => ({ + getObject: () => ({ + promise: () => Promise.resolve({ Body: Buffer.from('image-bytes') }) + }) + })) +); + +const ImageRequest = require('../image-request.js'); + +const REQUEST = { + bucket: 'dronebase-staging', + key: 'site/asset/photo.jpg', + edits: { resize: { width: 300 } } +}; + +const encode = (request) => Buffer.from(JSON.stringify(request)).toString('base64'); + +// The image request travels as base64-encoded JSON in the JWT's `request` +// claim, matching the format the Default (path-based) route already uses. +const tokenEvent = (request = REQUEST) => ({ + requestContext: { + authorizer: { + jwt: { + claims: { request: encode(request) } + } + } + } +}); + +// The Default route carries the same base64-encoded JSON as the last segment +// of the URL path. +const pathEvent = (request = REQUEST) => ({ + path: `/${encode(request)}` +}); + +beforeEach(() => { + process.env.SOURCE_BUCKETS = 'dronebase-development, dronebase-staging'; +}); + +describe('Token request route', () => { + it('identifies a validated JWT event as a Token request', () => { + const imageRequest = new ImageRequest(); + expect(imageRequest.parseRequestType(tokenEvent())).toEqual('Token'); + }); + + it('decodes the image request from the JWT claim', () => { + const imageRequest = new ImageRequest(); + expect(imageRequest.decodeRequest(tokenEvent())).toEqual(REQUEST); + }); + + it('resolves bucket, key and edits from the claim', async () => { + const imageRequest = new ImageRequest(); + await imageRequest.setup(tokenEvent()); + + expect(imageRequest.requestType).toEqual('Token'); + expect(imageRequest.bucket).toEqual('dronebase-staging'); + expect(imageRequest.key).toEqual('site/asset/photo.jpg'); + expect(imageRequest.edits).toEqual({ resize: { width: 300 } }); + }); +}); + +describe('Default request route', () => { + it('identifies a base64 path event as a Default request', () => { + const imageRequest = new ImageRequest(); + expect(imageRequest.parseRequestType(pathEvent())).toEqual('Default'); + }); + + it('decodes the image request from the URL path', () => { + const imageRequest = new ImageRequest(); + expect(imageRequest.decodeRequest(pathEvent())).toEqual(REQUEST); + }); + + it('resolves bucket, key and edits from the path', async () => { + const imageRequest = new ImageRequest(); + await imageRequest.setup(pathEvent()); + + expect(imageRequest.requestType).toEqual('Default'); + expect(imageRequest.bucket).toEqual('dronebase-staging'); + expect(imageRequest.key).toEqual('site/asset/photo.jpg'); + expect(imageRequest.edits).toEqual({ resize: { width: 300 } }); + }); +}); \ No newline at end of file diff --git a/source/image-handler/test/image/test.jpg b/source/image-handler/test/image/test.jpg deleted file mode 100644 index 2c20d0a84..000000000 Binary files a/source/image-handler/test/image/test.jpg and /dev/null differ diff --git a/source/image-handler/test/index.spec.js b/source/image-handler/test/index.spec.js deleted file mode 100644 index db0e19c3b..000000000 --- a/source/image-handler/test/index.spec.js +++ /dev/null @@ -1,407 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -const mockS3 = jest.fn(); -jest.mock('aws-sdk', () => { - return { - S3: jest.fn(() => ({ - getObject: mockS3 - })), - Rekognition: jest.fn(), - SecretsManager: jest.fn() - }; -}); - -// Import index.js -const index = require('../index.js'); - -describe('index', function() { - // Arrange - process.env.SOURCE_BUCKETS = 'source-bucket'; - const mockImage = Buffer.from('SampleImageContent\n'); - const mockFallbackImage = Buffer.from('SampleFallbackImageContent\n'); - - describe('TC: Success', function() { - beforeEach(() => { - // Mock - mockS3.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - Body: mockImage, - ContentType: 'image/jpeg' - }); - } - }; - }); - }) - - it('001/should return the image when there is no error', async function() { - // Arrange - const event = { - path: '/test.jpg' - }; - // Act - const result = await index.handler(event); - const expectedResult = { - statusCode: 200, - isBase64Encoded: true, - headers: { - 'Access-Control-Allow-Methods': 'GET', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Allow-Credentials': true, - 'Content-Type': 'image/jpeg', - 'Expires': undefined, - 'Cache-Control': 'max-age=31536000,public', - 'Last-Modified': undefined - }, - body: mockImage.toString('base64') - }; - // Assert - expect(mockS3).toHaveBeenCalledWith({ Bucket: 'source-bucket', Key: 'test.jpg' }); - expect(result).toEqual(expectedResult); - }); - it('002/should return the image with custom headers when custom headers are provided', async function() { - // Arrange - const event = { - path: '/eyJidWNrZXQiOiJzb3VyY2UtYnVja2V0Iiwia2V5IjoidGVzdC5qcGciLCJoZWFkZXJzIjp7IkN1c3RvbS1IZWFkZXIiOiJDdXN0b21WYWx1ZSJ9fQ==' - }; - // Act - const result = await index.handler(event); - const expectedResult = { - statusCode: 200, - headers: { - 'Access-Control-Allow-Methods': 'GET', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Allow-Credentials': true, - 'Content-Type': 'image/jpeg', - 'Expires': undefined, - 'Cache-Control': 'max-age=31536000,public', - 'Last-Modified': undefined, - 'Custom-Header': 'CustomValue' - }, - body: mockImage.toString('base64'), - isBase64Encoded: true - }; - // Assert - expect(mockS3).toHaveBeenCalledWith({ Bucket: 'source-bucket', Key: 'test.jpg' }); - expect(result).toEqual(expectedResult); - }); - it('003/should return the image when the request is from ALB', async function() { - // Arrange - const event = { - path: '/test.jpg', - requestContext: { - elb: {} - } - }; - // Act - const result = await index.handler(event); - const expectedResult = { - statusCode: 200, - isBase64Encoded: true, - headers: { - 'Access-Control-Allow-Methods': 'GET', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Content-Type': 'image/jpeg', - 'Expires': undefined, - 'Cache-Control': 'max-age=31536000,public', - 'Last-Modified': undefined - }, - body: mockImage.toString('base64') - }; - // Assert - expect(mockS3).toHaveBeenCalledWith({ Bucket: 'source-bucket', Key: 'test.jpg' }); - expect(result).toEqual(expectedResult); - }); - }); - - describe('TC: Error', function() { - it('001/should return an error JSON when an error occurs', async function() { - // Arrange - const event = { - path: '/test.jpg' - }; - // Mock - mockS3.mockImplementationOnce(() => { - return { - promise() { - return Promise.reject({ - code: 'NoSuchKey', - status: 404, - message: 'NoSuchKey error happened.' - }); - } - }; - }); - // Act - const result = await index.handler(event); - const expectedResult = { - statusCode: 404, - isBase64Encoded: false, - headers: { - 'Access-Control-Allow-Methods': 'GET', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Allow-Credentials': true, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - status: 404, - code: 'NoSuchKey', - message: 'NoSuchKey error happened.' - }) - }; - // Assert - expect(mockS3).toHaveBeenCalledWith({ Bucket: 'source-bucket', Key: 'test.jpg' }); - expect(result).toEqual(expectedResult); - }); - it('002/should return 500 error when there is no error status in the error', async function() { - // Arrange - const event = { - path: 'eyJidWNrZXQiOiJzb3VyY2UtYnVja2V0Iiwia2V5IjoidGVzdC5qcGciLCJlZGl0cyI6eyJ3cm9uZ0ZpbHRlciI6dHJ1ZX19' - }; - // Mock - mockS3.mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - Body: mockImage, - ContentType: 'image/jpeg' - }); - } - }; - }); - // Act - const result = await index.handler(event); - const expectedResult = { - statusCode: 500, - isBase64Encoded: false, - headers: { - 'Access-Control-Allow-Methods': 'GET', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Allow-Credentials': true, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - message: 'Internal error. Please contact the system administrator.', - code: 'InternalError', - status: 500 - }) - }; - // Assert - expect(mockS3).toHaveBeenCalledWith({ Bucket: 'source-bucket', Key: 'test.jpg' }); - expect(result).toEqual(expectedResult); - }); - it('003/should return the default fallback image when an error occurs if the default fallback image is enabled', async function() { - // Arrange - process.env.ENABLE_DEFAULT_FALLBACK_IMAGE = 'Yes'; - process.env.DEFAULT_FALLBACK_IMAGE_BUCKET = 'fallback-image-bucket'; - process.env.DEFAULT_FALLBACK_IMAGE_KEY = 'fallback-image.png'; - process.env.CORS_ENABLED = 'Yes'; - process.env.CORS_ORIGIN = '*'; - const event = { - path: '/test.jpg' - }; - // Mock - mockS3.mockReset(); - mockS3.mockImplementationOnce(() => { - return { - promise() { - return Promise.reject('UnknownError'); - } - }; - }).mockImplementationOnce(() => { - return { - promise() { - return Promise.resolve({ - Body: mockFallbackImage, - ContentType: 'image/png' - }); - } - }; - }); - // Act - const result = await index.handler(event); - const expectedResult = { - statusCode: 500, - isBase64Encoded: true, - headers: { - 'Access-Control-Allow-Methods': 'GET', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Allow-Credentials': true, - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'image/png', - 'Cache-Control': 'max-age=31536000,public', - 'Last-Modified': undefined - }, - body: mockFallbackImage.toString('base64') - }; - // Assert - expect(mockS3).toHaveBeenNthCalledWith(1, { Bucket: 'source-bucket', Key: 'test.jpg' }); - expect(mockS3).toHaveBeenNthCalledWith(2, { Bucket: 'fallback-image-bucket', Key: 'fallback-image.png' }); - expect(result).toEqual(expectedResult); - }); - it('004/should return an error JSON when getting the default fallback image fails if the default fallback image is enabled', async function() { - // Arrange - const event = { - path: '/test.jpg' - }; - // Mock - mockS3.mockReset(); - mockS3.mockImplementation(() => { - return { - promise() { - return Promise.reject({ - code: 'NoSuchKey', - status: 404, - message: 'NoSuchKey error happened.' - }); - } - }; - }); - // Act - const result = await index.handler(event); - const expectedResult = { - statusCode: 404, - isBase64Encoded: false, - headers: { - 'Access-Control-Allow-Methods': 'GET', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Allow-Credentials': true, - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - status: 404, - code: 'NoSuchKey', - message: 'NoSuchKey error happened.' - }) - }; - // Assert - expect(mockS3).toHaveBeenNthCalledWith(1, { Bucket: 'source-bucket', Key: 'test.jpg' }); - expect(mockS3).toHaveBeenNthCalledWith(2, { Bucket: 'fallback-image-bucket', Key: 'fallback-image.png' }); - expect(result).toEqual(expectedResult); - }); - it('005/should return an error JSON when the default fallback image key is not provided if the default fallback image is enabled', async function() { - // Arrange - process.env.DEFAULT_FALLBACK_IMAGE_KEY = ''; - const event = { - path: '/test.jpg' - }; - // Mock - mockS3.mockImplementationOnce(() => { - return { - promise() { - return Promise.reject({ - code: 'NoSuchKey', - status: 404, - message: 'NoSuchKey error happened.' - }); - } - }; - }); - // Act - const result = await index.handler(event); - const expectedResult = { - statusCode: 404, - isBase64Encoded: false, - headers: { - 'Access-Control-Allow-Methods': 'GET', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Allow-Credentials': true, - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - status: 404, - code: 'NoSuchKey', - message: 'NoSuchKey error happened.' - }) - }; - // Assert - expect(mockS3).toHaveBeenCalledWith({ Bucket: 'source-bucket', Key: 'test.jpg' }); - expect(result).toEqual(expectedResult); - }); - it('006/should return an error JSON when the default fallback image bucket is not provided if the default fallback image is enabled', async function() { - // Arrange - process.env.DEFAULT_FALLBACK_IMAGE_BUCKET = ''; - const event = { - path: '/test.jpg' - }; - // Mock - mockS3.mockImplementationOnce(() => { - return { - promise() { - return Promise.reject({ - code: 'NoSuchKey', - status: 404, - message: 'NoSuchKey error happened.' - }); - } - }; - }); - // Act - const result = await index.handler(event); - const expectedResult = { - statusCode: 404, - isBase64Encoded: false, - headers: { - 'Access-Control-Allow-Methods': 'GET', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Allow-Credentials': true, - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - status: 404, - code: 'NoSuchKey', - message: 'NoSuchKey error happened.' - }) - }; - // Assert - expect(mockS3).toHaveBeenCalledWith({ Bucket: 'source-bucket', Key: 'test.jpg' }); - expect(result).toEqual(expectedResult); - }); - }); - it('007/should return an error JSON when ALB request is failed', async function() { - // Arrange - const event = { - path: '/test.jpg', - requestContext: { - elb: {} - } - }; - // Mock - mockS3.mockImplementationOnce(() => { - return { - promise() { - return Promise.reject({ - code: 'NoSuchKey', - status: 404, - message: 'NoSuchKey error happened.' - }); - } - }; - }); - // Act - const result = await index.handler(event); - const expectedResult = { - statusCode: 404, - isBase64Encoded: false, - headers: { - 'Access-Control-Allow-Methods': 'GET', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - status: 404, - code: 'NoSuchKey', - message: 'NoSuchKey error happened.' - }) - }; - // Assert - expect(mockS3).toHaveBeenCalledWith({ Bucket: 'source-bucket', Key: 'test.jpg' }); - expect(result).toEqual(expectedResult); - }); -}); \ No newline at end of file diff --git a/source/image-handler/test/test-image-handler.js b/source/image-handler/test/test-image-handler.js deleted file mode 100644 index b5f9308db..000000000 --- a/source/image-handler/test/test-image-handler.js +++ /dev/null @@ -1,474 +0,0 @@ -/********************************************************************************************************************* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * - * * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * - * with the License. A copy of the License is located at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * - * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * - * and limitations under the License. * - *********************************************************************************************************************/ - -const ImageHandler = require('../image-handler'); -let assert = require('assert'); - -// ---------------------------------------------------------------------------- -// [async] process() -// ---------------------------------------------------------------------------- -describe('process()', function() { - describe('001/default', function() { - it(`Should pass if the output image is different from the input image with edits applied`, async function() { - // Arrange - const sinon = require('sinon'); - // ---- Amazon S3 stub - const S3 = require('aws-sdk/clients/s3'); - const getObject = S3.prototype.getObject = sinon.stub(); - getObject.returns({ - promise: () => { return { - Body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') - }} - }) - // ---- - const request = { - requestType: "default", - bucket: "sample-bucket", - key: "sample-image-001.jpg", - edits: { - grayscale: true, - flip: true - }, - originalImage: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') - } - // Act - const imageHandler = new ImageHandler(); - const result = await imageHandler.process(request); - // Assert - assert.deepEqual((request.originalImage.Body !== result), true); - }); - }); - describe('002/withToFormat', function() { - it(`Should pass if the output image is in a different format than the original image`, async function() { - // Arrange - const sinon = require('sinon'); - // ---- Amazon S3 stub - const S3 = require('aws-sdk/clients/s3'); - const getObject = S3.prototype.getObject = sinon.stub(); - getObject.returns({ - promise: () => { return { - Body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') - }} - }) - // ---- - const request = { - requestType: "default", - bucket: "sample-bucket", - key: "sample-image-001.jpg", - outputFormat: "png", - edits: { - grayscale: true, - flip: true - }, - originalImage: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') - } - // Act - const imageHandler = new ImageHandler(); - const result = await imageHandler.process(request); - // Assert - assert.deepEqual((request.originalImage.Body !== result), true); - }); - }); - describe('003/noEditsSpecified', function() { - it(`Should pass if no edits are specified and the original image is returned`, async function() { - // Arrange - const sinon = require('sinon'); - // ---- Amazon S3 stub - const S3 = require('aws-sdk/clients/s3'); - const getObject = S3.prototype.getObject = sinon.stub(); - getObject.returns({ - promise: () => { return { - Body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') - }} - }) - // ---- - const request = { - requestType: "default", - bucket: "sample-bucket", - key: "sample-image-001.jpg", - originalImage: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') - } - // Act - const imageHandler = new ImageHandler(); - const result = await imageHandler.process(request); - // Assert - assert.deepEqual(result, 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='); - }); - }); -}); - -// ---------------------------------------------------------------------------- -// [async] applyEdits() -// ---------------------------------------------------------------------------- -describe('applyEdits()', function() { - describe('001/standardEdits', function() { - it(`Should pass if a series of standard edits are provided to the - function`, async function() { - // Arrange - const originalImage = Buffer.from('sampleImageContent'); - const edits = { - grayscale: true, - flip: true - } - // Act - const imageHandler = new ImageHandler(); - const result = await imageHandler.applyEdits(originalImage, edits); - // Assert - const expectedResult1 = (result.options.greyscale); - const expectedResult2 = (result.options.flip); - const combinedResults = (expectedResult1 && expectedResult2); - assert.deepEqual(combinedResults, true); - }); - }); - describe('002/overlay', function() { - it(`Should pass if an edit with the overlayWith keyname is passed to - the function`, async function() { - // Arrange - const sinon = require('sinon'); - // ---- Amazon S3 stub - const S3 = require('aws-sdk/clients/s3'); - const getObject = S3.prototype.getObject = sinon.stub(); - getObject.returns({ - promise: () => { return { - Body: Buffer.from('sampleImageContent') - }} - }) - // Act - const originalImage = Buffer.from('sampleImageContent'); - const edits = { - overlayWith: { - bucket: 'aaa', - key: 'bbb' - } - } - // Assert - const imageHandler = new ImageHandler(); - await imageHandler.applyEdits(originalImage, edits).then((result) => { - assert.deepEqual(result.options.input.buffer, originalImage); - }); - }); - }); - describe('003/smartCrop', function() { - it(`Should pass if an edit with the smartCrop keyname is passed to - the function`, async function() { - // Arrange - const sinon = require('sinon'); - // ---- Amazon Rekognition stub - const rekognition = require('aws-sdk/clients/rekognition'); - const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); - detectFaces.returns({ - promise: () => { return { - FaceDetails: [{ - BoundingBox: { - Height: 0.18, - Left: 0.55, - Top: 0.33, - Width: 0.23 - } - }] - }} - }) - // Act - const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); - const edits = { - smartCrop: { - faceIndex: 0, - padding: 0 - } - } - // Assert - const imageHandler = new ImageHandler(); - await imageHandler.applyEdits(originalImage, edits).then((result) => { - //console.log(result); - const sharp = require('sharp'); - const originalImageData = sharp(originalImage); - assert.deepEqual((originalImageData.options.input !== result.options.input), true) - }).catch((err) => { - console.log(err) - }) - }); - }); - describe('004/smartCrop/paddingOutOfBoundsError', function() { - it(`Should pass if an excessive padding value is passed to the - smartCrop filter`, async function() { - // Arrange - const sinon = require('sinon'); - // ---- Amazon Rekognition stub - const rekognition = require('aws-sdk/clients/rekognition'); - const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); - detectFaces.returns({ - promise: () => { return { - FaceDetails: [{ - BoundingBox: { - Height: 0.18, - Left: 0.55, - Top: 0.33, - Width: 0.23 - } - }] - }} - }) - // Act - const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); - const edits = { - smartCrop: { - faceIndex: 0, - padding: 80 - } - } - // Assert - const imageHandler = new ImageHandler(); - await imageHandler.applyEdits(originalImage, edits).then((result) => { - //console.log(result); - const sharp = require('sharp'); - const originalImageData = sharp(originalImage); - assert.deepEqual((originalImageData.options.input !== result.options.input), true) - }).catch((err) => { - console.log(err) - }) - }); - }); - describe('005/smartCrop/boundingBoxError', function() { - it(`Should pass if an excessive faceIndex value is passed to the - smartCrop filter`, async function() { - // Arrange - const sinon = require('sinon'); - // ---- Amazon Rekognition stub - const rekognition = require('aws-sdk/clients/rekognition'); - const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); - detectFaces.returns({ - promise: () => { return { - FaceDetails: [{ - BoundingBox: { - Height: 0.18, - Left: 0.55, - Top: 0.33, - Width: 0.23 - } - }] - }} - }) - // Act - const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); - const edits = { - smartCrop: { - faceIndex: 10, - padding: 0 - } - } - // Assert - const imageHandler = new ImageHandler(); - await imageHandler.applyEdits(originalImage, edits).then((result) => { - //console.log(result); - const sharp = require('sharp'); - const originalImageData = sharp(originalImage); - assert.deepEqual((originalImageData.options.input !== result.options.input), true) - }).catch((err) => { - console.log(err) - }) - }); - }); - describe('006/smartCrop/faceIndexUndefined', function() { - it(`Should pass if a faceIndex value of undefined is passed to the - smartCrop filter`, async function() { - // Arrange - const sinon = require('sinon'); - // ---- Amazon Rekognition stub - const rekognition = require('aws-sdk/clients/rekognition'); - const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); - detectFaces.returns({ - promise: () => { return { - FaceDetails: [{ - BoundingBox: { - Height: 0.18, - Left: 0.55, - Top: 0.33, - Width: 0.23 - } - }] - }} - }) - // Act - const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); - const edits = { - smartCrop: true - } - // Assert - const imageHandler = new ImageHandler(); - await imageHandler.applyEdits(originalImage, edits).then((result) => { - //console.log(result); - const sharp = require('sharp'); - const originalImageData = sharp(originalImage); - assert.deepEqual((originalImageData.options.input !== result.options.input), true) - }).catch((err) => { - console.log(err) - }) - }); - }); -}); - -// ---------------------------------------------------------------------------- -// [async] getOverlayImage() -// ---------------------------------------------------------------------------- -describe('getOverlayImage()', function() { - describe('001/validParameters', function() { - it(`Should pass if the proper bucket name and key are supplied, - simulating an image file that can be retrieved`, async function() { - // Arrange - const S3 = require('aws-sdk/clients/s3'); - const sinon = require('sinon'); - const getObject = S3.prototype.getObject = sinon.stub(); - getObject.withArgs({Bucket: 'validBucket', Key: 'validKey'}).returns({ - promise: () => { return { - Body: Buffer.from('SampleImageContent\n') - }} - }) - // Act - const imageHandler = new ImageHandler(); - const result = await imageHandler.getOverlayImage('validBucket', 'validKey'); - // Assert - assert.deepEqual(result, Buffer.from('SampleImageContent\n')); - }); - }); - describe('002/imageDoesNotExist', async function() { - it(`Should throw an error if an invalid bucket or key name is provided, - simulating a non-existant overlay image`, async function() { - // Arrange - const S3 = require('aws-sdk/clients/s3'); - const sinon = require('sinon'); - const getObject = S3.prototype.getObject = sinon.stub(); - getObject.withArgs({Bucket: 'invalidBucket', Key: 'invalidKey'}).returns({ - promise: () => { - return Promise.reject({ - code: 500, - message: 'SimulatedInvalidParameterException' - }) - } - }); - // Act - const imageHandler = new ImageHandler(); - // Assert - imageHandler.getOverlayImage('invalidBucket', 'invalidKey').then((result) => { - assert.equal(typeof result, Error); - }).catch((err) => { - console.log(err) - }) - }); - }); -}); - -// ---------------------------------------------------------------------------- -// [async] getCropArea() -// ---------------------------------------------------------------------------- -describe('getCropArea()', function() { - describe('001/validParameters', function() { - it(`Should pass if the crop area can be calculated using a series of - valid inputs/parameters`, function() { - // Arrange - const boundingBox = { - Height: 0.18, - Left: 0.55, - Top: 0.33, - Width: 0.23 - }; - const options = { padding: 20 }; - const metadata = { - width: 200, - height: 400 - }; - // Act - const imageHandler = new ImageHandler(); - const result = imageHandler.getCropArea(boundingBox, options, metadata); - // Assert - const expectedResult = { - left: 90, - top: 112, - width: 86, - height: 112 - } - assert.deepEqual(result, expectedResult); - }); - }); -}); - - -// ---------------------------------------------------------------------------- -// [async] getBoundingBox() -// ---------------------------------------------------------------------------- -describe('getBoundingBox()', function() { - describe('001/validParameters', function() { - it(`Should pass if the proper parameters are passed to the function`, - async function() { - // Arrange - const sinon = require('sinon'); - const rekognition = require('aws-sdk/clients/rekognition'); - const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); - // ---- - const imageBytes = Buffer.from('TestImageData'); - detectFaces.withArgs({Image: {Bytes: imageBytes}}).returns({ - promise: () => { return { - FaceDetails: [{ - BoundingBox: { - Height: 0.18, - Left: 0.55, - Top: 0.33, - Width: 0.23 - } - }] - }} - }) - // ---- - const currentImage = imageBytes; - const faceIndex = 0; - // Act - const imageHandler = new ImageHandler(); - const result = await imageHandler.getBoundingBox(currentImage, faceIndex); - // Assert - const expectedResult = { - Height: 0.18, - Left: 0.55, - Top: 0.33, - Width: 0.23 - }; - assert.deepEqual(result, expectedResult); - }); - }); - describe('002/errorHandling', function() { - it(`Should simulate an error condition returned by Rekognition`, - async function() { - // Arrange - const rekognition = require('aws-sdk/clients/rekognition'); - const sinon = require('sinon'); - const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); - detectFaces.returns({ - promise: () => { - return Promise.reject({ - code: 500, - message: 'SimulatedError' - }) - } - }) - // ---- - const currentImage = Buffer.from('NotTestImageData'); - const faceIndex = 0; - // Act - const imageHandler = new ImageHandler(); - // Assert - imageHandler.getBoundingBox(currentImage, faceIndex).then((result) => { - assert.equal(typeof result, Error); - }).catch((err) => { - console.log(err) - }) - }); - }); -}); diff --git a/source/image-handler/test/thumbor-mapping.spec.js b/source/image-handler/test/thumbor-mapping.spec.js deleted file mode 100644 index 264c7b048..000000000 --- a/source/image-handler/test/thumbor-mapping.spec.js +++ /dev/null @@ -1,913 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -const ThumborMapping = require('../thumbor-mapping'); - -// ---------------------------------------------------------------------------- -// process() -// ---------------------------------------------------------------------------- -describe('process()', function() { - describe('001/thumborRequest', function() { - it('Should pass if the proper edit translations are applied and in the correct order', function() { - // Arrange - const event = { - path : "/fit-in/200x300/filters:grayscale()/test-image-001.jpg" - } - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.process(event); - // Assert - const expectedResult = { - edits: { - resize: { - width: 200, - height: 300, - fit: 'inside' - }, - grayscale: true - } - }; - expect(thumborMapping.edits).toEqual(expectedResult.edits); - }); - }); - describe('002/resize/fit-in', function() { - it('Should pass if the proper edit translations are applied and in the correct order', function() { - // Arrange - const event = { - path : "/fit-in/400x300/test-image-001.jpg" - } - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.process(event); - // Assert - const expectedResult = { - edits: { - resize: { - width: 400, - height: 300, - fit: 'inside' - } - } - }; - expect(thumborMapping.edits).toEqual(expectedResult.edits); - }); - }); - describe('003/resize/fit-in/noResizeValues', function() { - it('Should pass if the proper edit translations are applied and in the correct order', function() { - // Arrange - const event = { - path : "/fit-in/test-image-001.jpg" - } - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.process(event); - // Assert - const expectedResult = { - edits: { - resize: { fit: 'inside' } - } - }; - expect(thumborMapping.edits).toEqual(expectedResult.edits); - }); - }); - describe('004/resize/not-fit-in', function() { - it('Should pass if the proper edit translations are applied and in the correct order', function() { - // Arrange - const event = { - path : "/400x300/test-image-001.jpg" - } - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.process(event); - // Assert - const expectedResult = { - edits: { - resize: { - width: 400, - height: 300 - } - } - }; - expect(thumborMapping.edits).toEqual(expectedResult.edits); - }); - }); - describe('005/resize/widthIsZero', function() { - it('Should pass if the proper edit translations are applied and in the correct order', function() { - // Arrange - const event = { - path : "/0x300/test-image-001.jpg" - } - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.process(event); - // Assert - const expectedResult = { - edits: { - resize: { - width: null, - height: 300, - fit: 'inside' - } - } - }; - expect(thumborMapping.edits).toEqual(expectedResult.edits); - }); - }); - describe('006/resize/heightIsZero', function() { - it('Should pass if the proper edit translations are applied and in the correct order', function() { - // Arrange - const event = { - path : "/400x0/test-image-001.jpg" - } - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.process(event); - // Assert - const expectedResult = { - edits: { - resize: { - width: 400, - height: null, - fit: 'inside' - } - } - }; - expect(thumborMapping.edits).toEqual(expectedResult.edits); - }); - }); - describe('007/resize/widthAndHeightAreZero', function() { - it('Should pass if the proper edit translations are applied and in the correct order', function() { - // Arrange - const event = { - path : "/0x0/test-image-001.jpg" - } - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.process(event); - // Assert - const expectedResult = { - edits: { - resize: { - width: null, - height: null, - fit: 'inside' - } - } - }; - expect(thumborMapping.edits).toEqual(expectedResult.edits); - }); - }); -}); - -// ---------------------------------------------------------------------------- -// parseCustomPath() -// ---------------------------------------------------------------------------- -describe('parseCustomPath()', function() { - describe('001/validPath', function() { - it('Should pass if the proper edit translations are applied and in the correct order', function() { - const event = { - path : '/filters-rotate(90)/filters-grayscale()/thumbor-image.jpg' - } - process.env.REWRITE_MATCH_PATTERN = /(filters-)/gm; - process.env.REWRITE_SUBSTITUTION = 'filters:'; - // Act - const thumborMapping = new ThumborMapping(); - const result = thumborMapping.parseCustomPath(event.path); - // Assert - const expectedResult = '/filters:rotate(90)/filters:grayscale()/thumbor-image.jpg'; - expect(result.path).toEqual(expectedResult); - }); - }); - describe('002/undefinedEnvironmentVariables', function() { - it('Should throw an error if the environment variables are left undefined', function() { - const event = { - path : '/filters-rotate(90)/filters-grayscale()/thumbor-image.jpg' - } - delete process.env.REWRITE_MATCH_PATTERN; - delete process.env.REWRITE_SUBSTITUTION; - // Act - const thumborMapping = new ThumborMapping(); - // Assert - expect(() => { - thumborMapping.parseCustomPath(event.path); - }).toThrowError(new Error('ThumborMapping::ParseCustomPath::ParsingError')); - }); - }); - describe('003/undefinedPath', function() { - it('Should throw an error if the path is not defined', function() { - const event = {}; - process.env.REWRITE_MATCH_PATTERN = /(filters-)/gm; - process.env.REWRITE_SUBSTITUTION = 'filters:'; - // Act - const thumborMapping = new ThumborMapping(); - // Assert - expect(() => { - thumborMapping.parseCustomPath(event.path); - }).toThrowError(new Error('ThumborMapping::ParseCustomPath::ParsingError')); - }); - }); - describe('004/undefinedAll', function() { - it('Should throw an error if the path is not defined', function() { - const event = {}; - delete process.env.REWRITE_MATCH_PATTERN; - delete process.env.REWRITE_SUBSTITUTION; - // Act - const thumborMapping = new ThumborMapping(); - // Assert - expect(() => { - thumborMapping.parseCustomPath(event.path); - }).toThrowError(new Error('ThumborMapping::ParseCustomPath::ParsingError')); - }); - }); -}); - -// ---------------------------------------------------------------------------- -// mapFilter() -// ---------------------------------------------------------------------------- -describe('mapFilter()', function() { - describe('001/autojpg', function() { - it('Should pass if the filter is successfully converted from Thumbor:autojpg()', function() { - // Arrange - const edit = 'filters:autojpg()'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { toFormat: 'jpeg' } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('002/background_color', function() { - it('Should pass if the filter is successfully translated from Thumbor:background_color()', function() { - // Arrange - const edit = 'filters:background_color(ffff)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { flatten: { background: {r: 255, g: 255, b: 255}}} - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('003/blur/singleParameter', function() { - it('Should pass if the filter is successfully translated from Thumbor:blur()', function() { - // Arrange - const edit = 'filters:blur(60)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { blur: 30 } - }; - // assert.deepStrictEqual(thumborMapping, expectedResult); - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('004/blur/doubleParameter', function() { - it('Should pass if the filter is successfully translated from Thumbor:blur()', function() { - // Arrange - const edit = 'filters:blur(60, 2)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { blur: 2 } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('005/convolution', function() { - it('Should pass if the filter is successfully translated from Thumbor:convolution()', function() { - // Arrange - const edit = 'filters:convolution(1;2;1;2;4;2;1;2;1,3,true)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { convolve: { - width: 3, - height: 3, - kernel: [1,2,1,2,4,2,1,2,1] - }} - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('006/equalize', function() { - it('Should pass if the filter is successfully translated from Thumbor:equalize()', function() { - // Arrange - const edit = 'filters:equalize()'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { normalize: 'true' } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('007/fill/resizeUndefined', function() { - it('Should pass if the filter is successfully translated from Thumbor:fill()', function() { - // Arrange - const edit = 'filters:fill(fff)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { resize: { background: { r: 255, g: 255, b: 255 }, fit: 'contain' }} - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - - describe('008/fill/resizeDefined', function() { - it('Should pass if the filter is successfully translated from Thumbor:fill()', function() { - // Arrange - const edit = 'filters:fill(fff)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.edits.resize = {}; - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { resize: { background: { r: 255, g: 255, b: 255 }, fit: 'contain' }} - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('009/format/supportedFileType', function() { - it('Should pass if the filter is successfully translated from Thumbor:format()', function() { - // Arrange - const edit = 'filters:format(png)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { toFormat: 'png' } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('010/format/unsupportedFileType', function() { - it('Should return undefined if an accepted file format is not specified', function() { - // Arrange - const edit = 'filters:format(test)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('011/no_upscale/resizeUndefined', function() { - it('Should pass if the filter is successfully translated from Thumbor:no_upscale()', function() { - // Arrange - const edit = 'filters:no_upscale()'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - resize: { - withoutEnlargement: true - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('012/no_upscale/resizeDefined', function() { - it('Should pass if the filter is successfully translated from Thumbor:no_upscale()', function() { - // Arrange - const edit = 'filters:no_upscale()'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.edits.resize = { - height: 400, - width: 300 - }; - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - resize: { - height: 400, - width: 300, - withoutEnlargement: true - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('013/proportion/resizeDefined', function() { - it('Should pass if the filter is successfully translated from Thumbor:proportion()', function() { - // Arrange - const edit = 'filters:proportion(0.3)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.edits = { - resize: { - width: 200, - height: 200 - } - }; - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - resize: { - height: 60, - width: 60 - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('014/proportion/resizeUndefined', function() { - it('Should pass if the filter is successfully translated from Thumbor:resize()', function() { - // Arrange - const edit = 'filters:proportion(0.3)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const actualResult = thumborMapping.edits.resize !== undefined; - const expectedResult = true; - expect(actualResult).toEqual(expectedResult); - }); - }); - describe('015/quality/jpg', function() { - it('Should pass if the filter is successfully translated from Thumbor:quality()', function() { - // Arrange - const edit = 'filters:quality(50)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - jpeg: { - quality: 50 - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('016/quality/png', function() { - it('Should pass if the filter is successfully translated from Thumbor:quality()', function() { - // Arrange - const edit = 'filters:quality(50)'; - const filetype = 'png'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - png: { - quality: 50 - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('017/quality/webp', function() { - it('Should pass if the filter is successfully translated from Thumbor:quality()', function() { - // Arrange - const edit = 'filters:quality(50)'; - const filetype = 'webp'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - webp: { - quality: 50 - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('018/quality/tiff', function() { - it('Should pass if the filter is successfully translated from Thumbor:quality()', function() { - // Arrange - const edit = 'filters:quality(50)'; - const filetype = 'tiff'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - tiff: { - quality: 50 - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('019/quality/heif', function() { - it('Should pass if the filter is successfully translated from Thumbor:quality()', function() { - // Arrange - const edit = 'filters:quality(50)'; - const filetype = 'heif'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - heif: { - quality: 50 - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('020/quality/other', function() { - it('Should return undefined if an unsupported file type is provided', function() { - // Arrange - const edit = 'filters:quality(50)'; - const filetype = 'xml'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('021/rgb', function() { - it('Should pass if the filter is successfully translated from Thumbor:rgb()', function() { - // Arrange - const edit = 'filters:rgb(10, 10, 10)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - tint: { - r: 25.5, - g: 25.5, - b: 25.5 - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('022/rotate', function() { - it('Should pass if the filter is successfully translated from Thumbor:rotate()', function() { - // Arrange - const edit = 'filters:rotate(75)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - rotate: 75 - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('023/sharpen', function() { - it('Should pass if the filter is successfully translated from Thumbor:sharpen()', function() { - // Arrange - const edit = 'filters:sharpen(75, 5)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - sharpen: 3.5 - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('024/stretch/default', function() { - it('Should pass if the filter is successfully translated from Thumbor:stretch()', function() { - // Arrange - const edit = 'filters:stretch()'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - resize: { fit: 'fill' } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('025/stretch/resizeDefined', function() { - it('Should pass if the filter is successfully translated from Thumbor:stretch()', function() { - // Arrange - const edit = 'filters:stretch()'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.edits.resize = { - width: 300, - height: 400 - }; - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - resize: { - width: 300, - height: 400, - fit: 'fill' - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('026/stretch/fit-in', function() { - it('Should pass if the filter is successfully translated from Thumbor:stretch()', function() { - // Arrange - const edit = 'filters:stretch()'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.edits.resize = { - fit: 'inside' - }; - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - resize: { fit: 'inside' } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('027/stretch/fit-in/resizeDefined', function() { - it('Should pass if the filter is successfully translated from Thumbor:stretch()', function() { - // Arrange - const edit = 'filters:stretch()'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.edits.resize = { - width: 400, - height: 300, - fit: 'inside' - }; - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - resize: { - width: 400, - height: 300, - fit: 'inside' - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('028/strip_exif', function() { - it('Should pass if the filter is successfully translated from Thumbor:strip_exif()', function() { - // Arrange - const edit = 'filters:strip_exif()'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - rotate: null - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('029/strip_icc', function() { - it('Should pass if the filter is successfully translated from Thumbor:strip_icc()', function() { - // Arrange - const edit = 'filters:strip_icc()'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - rotate: null - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('030/upscale', function() { - it('Should pass if the filter is successfully translated from Thumbor:upscale()', function() { - // Arrange - const edit = 'filters:upscale()'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - resize: { - fit: 'inside' - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('031/upscale/resizeNotUndefined', function() { - it('Should pass if the filter is successfully translated from Thumbor:upscale()', function() { - // Arrange - const edit = 'filters:upscale()'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.edits.resize = {}; - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - resize: { - fit: 'inside' - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('032/watermark/positionDefined', function() { - it('Should pass if the filter is successfully translated from Thumbor:watermark()', function() { - // Arrange - const edit = 'filters:watermark(bucket,key,100,100,0)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - overlayWith: { - bucket: 'bucket', - key: 'key', - alpha: '0', - wRatio: undefined, - hRatio: undefined, - options: { - left: '100', - top: '100' - } - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('033/watermark/positionDefinedByPercentile', function() { - it('Should pass if the filter is successfully translated from Thumbor:watermark()', function() { - // Arrange - const edit = 'filters:watermark(bucket,key,50p,30p,0)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - overlayWith: { - bucket: 'bucket', - key: 'key', - alpha: '0', - wRatio: undefined, - hRatio: undefined, - options: { - left: '50p', - top: '30p' - } - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('034/watermark/positionDefinedWrong', function() { - it('Should pass if the filter is successfully translated from Thumbor:watermark()', function() { - // Arrange - const edit = 'filters:watermark(bucket,key,x,x,0)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - overlayWith: { - bucket: 'bucket', - key: 'key', - alpha: '0', - wRatio: undefined, - hRatio: undefined, - options: {} - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('035/watermark/ratioDefined', function() { - it('Should pass if the filter is successfully translated from Thumbor:watermark()', function() { - // Arrange - const edit = 'filters:watermark(bucket,key,100,100,0,10,10)'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { - edits: { - overlayWith: { - bucket: 'bucket', - key: 'key', - alpha: '0', - wRatio: '10', - hRatio: '10', - options: { - left: '100', - top: '100' - } - } - } - }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); - describe('036/elseCondition', function() { - it('Should pass if undefined is returned for an unsupported filter', function() { - // Arrange - const edit = 'filters:notSupportedFilter()'; - const filetype = 'jpg'; - // Act - const thumborMapping = new ThumborMapping(); - thumborMapping.mapFilter(edit, filetype); - // Assert - const expectedResult = { edits: {} }; - expect(thumborMapping).toEqual(expectedResult); - }); - }); -}) \ No newline at end of file