"""Lambda test module.""" from copy import deepcopy import json import subprocess from unittest.mock import Mock from unittest.mock import patch import botocore import config from src import app as index from src.common import lambda_exceptions import pytest test_filename = 'test_filename.wav' test_bucket = 'test_bucket' test_presigned_url = 'https://urltoaudio.com' event_mock = { 'key': test_filename, 'bucket': test_bucket, 'file_type': 'wav', 'config': { 'output_bucket_name': 'output-bucket-name', 'preview_bucket_name': 'preview-bucket-name', 'elastic_transcoder_id': 'transcoder_pipeline_123' } } def mocked_subprocess(stdout, stderr): """Fixture for subprocess.Popen.""" process_mock = Mock() process_mock.communicate.return_value = (stdout, stderr) return process_mock @patch('subprocess.Popen') @patch('src.common.s3.client') @patch('src.common.elastic_transcoder.client') @patch('src.common.status.client') def test_handler_success(sns_client, transcoder_client, s3_client, subprocess_mock): """Test handler success.""" s3_client.generate_presigned_url.return_value = test_presigned_url subprocess_mock.side_effect = [ mocked_subprocess('curl_output', ''), mocked_subprocess('waveform_output', '') ] result = index.handler(event_mock, None) assert result sns_client.publish.assert_any_call( TopicArn='encoding-topic-arn', Message=json.dumps({ 'status': 'processing', 'input': { 'key': 'test_filename.wav', 'bucket': 'test_bucket' } })) put_object_call = s3_client.put_object.mock_calls[0][2] assert put_object_call['Bucket'] == 'output-bucket-name' assert '.dat' in put_object_call['Key'] assert put_object_call['ContentType'] == 'binary/octet-stream' assert put_object_call['Bucket'] == 'output-bucket-name' trancoder_call = transcoder_client.create_job.mock_calls[0][2] assert trancoder_call['PipelineId'] == 'transcoder_pipeline_123' assert trancoder_call['Inputs'] == [{'Key': 'test_filename.wav'}] assert type(trancoder_call['Outputs']) == list assert len(trancoder_call['Outputs']) == 2 assert '.flac' in trancoder_call['Outputs'][0]['Key'] assert trancoder_call['Outputs'][0]['PresetId'] == '1351620000001-300110' assert '.mp3' in trancoder_call['Outputs'][1]['Key'] assert trancoder_call['Outputs'][1]['PresetId'] == '1351620000001-300010' encoding_sns_call = sns_client.publish.mock_calls[1][2] encoding_sns_call['TopicArn'] == 'encoding-topic-arn' encoding_message = json.loads(encoding_sns_call['Message']) assert encoding_message['status'] == 'waveform_completed' assert encoding_message['input'] == { 'key': 'test_filename.wav', 'bucket': 'test_bucket' } assert len(encoding_message['final_assets']) == 1 waveform_asset = encoding_message['final_assets'][0] assert '.dat' in waveform_asset['key'] assert waveform_asset['asset_type'] == 'DAT' assert waveform_asset['asset_subtype'] is None @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.wav', '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.generate_presigned_url.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.wav', 'bucket': 'test_bucket' } })) @patch('src.common.s3.client') @patch('src.common.status.client') def test_handler_binary_missing(sns_client, s3_client): """Test handler when binary is missing.""" s3_client.generate_presigned_url.return_value = test_presigned_url with pytest.raises(FileNotFoundError) as err: index.handler(event_mock, None) assert 'No such file or directory' in str(err.value) sns_client.publish.assert_called_with( TopicArn='encoding-topic-arn', Message=json.dumps({ 'status': 'error', 'errors': { 'audio_waveform_error': 'An error occurred during the audio waveform generation. Error message: {}'.format(str(err.value)) }, 'input': { 'key': 'test_filename.wav', 'bucket': 'test_bucket' } })) @patch('subprocess.Popen') @patch('src.common.s3.client') @patch('src.common.status.client') def test_handler_waveform_error(sns_client, s3_client, subprocess_mock): """Test handler raises WaveformError for a subprocess exception.""" s3_client.generate_presigned_url.return_value = test_presigned_url subprocess_mock.side_effect = [ mocked_subprocess('curl_output', ''), subprocess.CalledProcessError( cmd=[config.BIN_PATH, '--input-format', 'wav', '--output-format', 'dat', '-b', '8', '-z', '256'], returncode=127, stderr='illegal seek') ] with pytest.raises(index.WaveformError) as err: index.handler(event_mock, None) assert str(err.value) == 'An error occurred during the audio waveform generation. Error message: illegal seek' sns_client.publish.assert_called_with( TopicArn='encoding-topic-arn', Message=json.dumps({ 'status': 'error', 'errors': { 'audio_waveform_error': 'An error occurred during the audio waveform generation. Error message: illegal seek' }, 'input': { 'key': 'test_filename.wav', 'bucket': 'test_bucket' } }))