"""Lambda test module.""" from copy import deepcopy from io import BytesIO import json from unittest.mock import Mock from unittest.mock import patch import botocore from PIL import Image import pytest from src import app as index from src.common import lambda_exceptions test_filename = 'test_filename.jpg' test_bucket = 'test_bucket' event_mock = { 'key': test_filename, 'bucket': test_bucket, 'file_type': 'jpeg', 'config': { 'output_bucket_name': 'output-bucket-name', 'preview_bucket_name': 'preview-bucket-name', 'elastic_transcoder_id': '123' } } def build_test_image(size, mode, fmt, convert=None): """Utils returns an image.""" file = BytesIO() image = Image.new(mode, size=size, color=(155, 0, 0)) if convert: image = image.convert(convert) image.save(file, fmt) file.name = 'test.{}'.format(fmt) file.seek(0) return file @patch('src.common.s3.download_file_object') @patch('src.common.s3.client') @patch('src.common.status.client') def test_handler_success(sns_client, s3_client, s3_download): """Test handler function.""" test_image = build_test_image((1, 1), 'RGB', 'jpeg') s3_download.return_value = test_image result = index.handler(event_mock, None) assert result == {'status': 'OK', 'message': 'image encoding successful'} assert s3_client.put_object.call_count == 4 tiff_output = s3_client.put_object.mock_calls[0][2] assert tiff_output['Bucket'] == 'output-bucket-name' assert 'images/tif' in tiff_output['Key'] assert 'test_filename.tif' in tiff_output['Key'] assert tiff_output['ContentType'] == 'image/tiff' assert type(tiff_output['Body']) == BytesIO cover_output = s3_client.put_object.mock_calls[1][2] assert cover_output['Bucket'] == 'preview-bucket-name' assert 'images/cover' in cover_output['Key'] assert 'test_filename.jpeg' in cover_output['Key'] assert cover_output['ContentType'] == 'image/jpeg' assert type(cover_output['Body']) == BytesIO large_cover_output = s3_client.put_object.mock_calls[2][2] assert large_cover_output['Bucket'] == 'preview-bucket-name' assert 'images/large_cover' in large_cover_output['Key'] assert large_cover_output['ContentType'] == 'image/jpeg' assert type(large_cover_output['Body']) == BytesIO xlarge_cover_output = s3_client.put_object.mock_calls[3][2] assert xlarge_cover_output['Bucket'] == 'preview-bucket-name' assert 'images/xlarge_cover' in xlarge_cover_output['Key'] assert xlarge_cover_output['ContentType'] == 'image/jpeg' assert type(xlarge_cover_output['Body']) == BytesIO sns_client.publish.assert_any_call( TopicArn='encoding-topic-arn', Message=json.dumps({ 'status': 'processing', 'input': { 'key': 'test_filename.jpg', 'bucket': 'test_bucket' } })) sns_client.publish.assert_called_with( TopicArn='encoding-topic-arn', Message=json.dumps({ 'status': 'completed', 'final_assets': [ {'key': cover_output['Key'], 'asset_type': 'JPG', 'asset_subtype': 'cover'}, {'key': large_cover_output['Key'], 'asset_type': 'JPG', 'asset_subtype': 'large_cover'}, {'key': xlarge_cover_output['Key'], 'asset_type': 'JPG', 'asset_subtype': 'xlarge_cover'}, {'key': tiff_output['Key'], 'asset_type': 'TIF', 'asset_subtype': None}], 'input': { 'key': 'test_filename.jpg', 'bucket': 'test_bucket' } })) assert test_image.closed @patch('src.common.status.client') def test_handler_lambda_event_error(sns_client): """Test handler function with an invalid lambda event.""" bad_event = deepcopy(event_mock) del bad_event['config']['elastic_transcoder_id'] with pytest.raises(lambda_exceptions.LambdaEventError) as err: index.handler(bad_event, None) assert err.value.error_code == 'lambda_event_error' assert str(err.value) == 'Lambda event from previous step is invalid. {message}'.format( message=json.dumps({'config': {'elastic_transcoder_id': ['Missing data for required field.']}}) ) sns_client.publish.assert_called_with( TopicArn='encoding-topic-arn', Message=json.dumps({ 'status': 'error', 'errors': err.value.errors, 'input': { 'key': 'test_filename.jpg', 'bucket': 'test_bucket' } })) @patch('src.common.s3.client') @patch('src.common.status.client') def test_handler_object_not_found(sns_client, s3_client): """Test handler function with an object that does not exist.""" error = {'Error': {'Code': '404', 'Message': 'Not found'}, 'ResponseMetadata': {'HTTPStatusCode': 404}} s3_client.download_fileobj.side_effect = botocore.exceptions.ClientError(error, 'HeadObject') with pytest.raises(lambda_exceptions.S3Error) as err: index.handler(event_mock, None) sns_client.publish.assert_called_with( TopicArn='encoding-topic-arn', Message=json.dumps({ 'status': 'error', 'errors': err.value.errors, 'input': { 'key': 'test_filename.jpg', 'bucket': 'test_bucket' } })) @patch('src.common.s3.download_file_object') @patch('src.common.status.client') def test_handler_pil_tif_error(sns_client, s3_download): """Test handler function with unexpected PIL error.""" test_image = build_test_image((1, 1), 'RGB', 'jpeg') s3_download.return_value = test_image restore_func = Image.Image.save Image.Image.save = Mock(side_effect=AttributeError("'tuple' object has no attribute 'ljust'")) with pytest.raises(AttributeError) as err: index.handler(event_mock, None) errors = { 'image_encoding_error': 'An error occurred during image encoding. Error message: {message}'.format(message=str(err.value))} sns_client.publish.assert_called_with( TopicArn='encoding-topic-arn', Message=json.dumps({ 'status': 'error', 'errors': errors, 'input': { 'key': 'test_filename.jpg', 'bucket': 'test_bucket' }})) Image.Image.save = restore_func @patch('src.common.s3.download_file_object') @patch('src.common.status.client') def test_handler_pil_tif_large(sns_client, s3_download): """Test handler function with unexpected PIL error.""" test_image = build_test_image((1, 1), 'RGB', 'jpeg') s3_download.return_value = test_image restore_func = Image.open Image.open = Mock(side_effect=Image.DecompressionBombError( 'Image size (434013889 pixels) exceeds limit of 178956970 pixels, could be decompression bomb DOS attack.')) with pytest.raises(Image.DecompressionBombError) as err: index.handler(event_mock, None) errors = { 'image_encoding_error': 'An error occurred during image encoding. Error message: {message}'.format(message=str(err.value))} sns_client.publish.assert_called_with( TopicArn='encoding-topic-arn', Message=json.dumps({ 'status': 'error', 'errors': errors, 'input': { 'key': 'test_filename.jpg', 'bucket': 'test_bucket' }})) Image.open = restore_func