import io import json from typing import Any from unittest import mock import pytest from aws_testing_utils import config, lambda_handler def _make_response( status: int = 200, body: dict[str, Any] | None = None, function_error: str | None = None, ) -> dict[str, Any]: r: dict[str, Any] = {'StatusCode': status} if body is not None: r['Payload'] = io.BytesIO(json.dumps(body).encode()) if function_error is not None: r['FunctionError'] = function_error return r @mock.patch('boto3.client') def test_invoke(mock_boto3: mock.MagicMock) -> None: mock_boto3.return_value.invoke.return_value = {'StatusCode': 200} handler = lambda_handler.LambdaHandler() response = handler.invoke('test') assert response['StatusCode'] == 200 @mock.patch('boto3.client') def test_invoke_with_payload(mock_boto3: mock.MagicMock) -> None: mock_boto3.return_value.invoke.return_value = {'StatusCode': 200} handler = lambda_handler.LambdaHandler() response = handler.invoke('test', {'test': 1}) assert response['StatusCode'] == 200 @mock.patch('boto3.client') def test_payload_is_readable_after_invoke(mock_boto3: mock.MagicMock) -> None: mock_boto3.return_value.invoke.return_value = _make_response(body={'result': 'ok'}) handler = lambda_handler.LambdaHandler() response = handler.invoke('test') assert json.loads(response['Payload'].read()) == {'result': 'ok'} @mock.patch('boto3.client') def test_status_code_error_caught(mock_boto3: mock.MagicMock) -> None: mock_boto3.return_value.invoke.return_value = {'StatusCode': 500} handler = lambda_handler.LambdaHandler() with pytest.raises(AssertionError): handler.invoke('test') @mock.patch('boto3.client') def test_invoke_default_call_omits_invocation_type(mock_boto3: mock.MagicMock) -> None: mock_boto3.return_value.invoke.return_value = {'StatusCode': 200} handler = lambda_handler.LambdaHandler() handler.invoke('test', {'a': 1}) mock_boto3.return_value.invoke.assert_called_once_with( FunctionName='test', Payload='{"a": 1}' ) @mock.patch('boto3.client') def test_invoke_passes_kwargs_through_to_boto3(mock_boto3: mock.MagicMock) -> None: mock_boto3.return_value.invoke.return_value = {'StatusCode': 202} handler = lambda_handler.LambdaHandler() handler.invoke('test', {'a': 1}, expected_status=202, InvocationType='Event') mock_boto3.return_value.invoke.assert_called_once_with( FunctionName='test', Payload='{"a": 1}', InvocationType='Event' ) @mock.patch('boto3.client') def test_invoke_reserved_keys_win_over_kwargs(mock_boto3: mock.MagicMock) -> None: # A caller can't override the target function or payload via kwargs. mock_boto3.return_value.invoke.return_value = {'StatusCode': 200} handler = lambda_handler.LambdaHandler() handler.invoke('real-fn', {'a': 1}, FunctionName='hacker', Payload='evil') mock_boto3.return_value.invoke.assert_called_once_with( FunctionName='real-fn', Payload='{"a": 1}' ) @mock.patch('boto3.client') def test_invoke_accepts_expected_status(mock_boto3: mock.MagicMock) -> None: mock_boto3.return_value.invoke.return_value = {'StatusCode': 202} handler = lambda_handler.LambdaHandler() response = handler.invoke('test', expected_status=202) assert response['StatusCode'] == 202 @mock.patch('boto3.client') def test_invoke_unexpected_status_caught(mock_boto3: mock.MagicMock) -> None: mock_boto3.return_value.invoke.return_value = {'StatusCode': 202} handler = lambda_handler.LambdaHandler() with pytest.raises(AssertionError): handler.invoke('test') @mock.patch('boto3.client') def test_function_error_caught(mock_boto3: mock.MagicMock) -> None: mock_boto3.return_value.invoke.return_value = _make_response( function_error='Unhandled' ) handler = lambda_handler.LambdaHandler() with pytest.raises(AssertionError): handler.invoke('test') @mock.patch('boto3.client') def test_function_error_synthesized_from_payload_error_type( mock_boto3: mock.MagicMock, ) -> None: # Local RIE omits X-Amz-Function-Error; FunctionError must be inferred from payload. mock_boto3.return_value.invoke.return_value = _make_response( body={'errorType': 'RuntimeError', 'errorMessage': 'oops', 'stackTrace': []} ) handler = lambda_handler.LambdaHandler() response = handler.invoke('test', assertion=False) assert response.get('FunctionError') == 'Unhandled' @mock.patch('boto3.client') def test_function_error_not_synthesized_for_normal_payload( mock_boto3: mock.MagicMock, ) -> None: # A payload that happens to lack errorType should not trigger synthesis. mock_boto3.return_value.invoke.return_value = _make_response(body={'job_id': 1}) handler = lambda_handler.LambdaHandler() response = handler.invoke('test') assert 'FunctionError' not in response @mock.patch('boto3.client') def test_function_error_not_synthesized_for_binary_payload( mock_boto3: mock.MagicMock, ) -> None: # Binary (non-UTF-8) payloads must not raise — synthesis is best-effort. r = {'StatusCode': 200, 'Payload': io.BytesIO(b'\xff\xfe')} mock_boto3.return_value.invoke.return_value = r handler = lambda_handler.LambdaHandler() response = handler.invoke('test') assert 'FunctionError' not in response @mock.patch('boto3.client') def test_function_error_not_overwritten_when_already_set( mock_boto3: mock.MagicMock, ) -> None: # Real Lambda already sets FunctionError via the header; don't clobber it. mock_boto3.return_value.invoke.return_value = _make_response( body={'errorType': 'RuntimeError', 'errorMessage': 'oops', 'stackTrace': []}, function_error='Handled', ) handler = lambda_handler.LambdaHandler() response = handler.invoke('test', assertion=False) assert response['FunctionError'] == 'Handled' @mock.patch('boto3.client') def test_invoke_uses_default_endpoint(mock_boto3: mock.MagicMock) -> None: mock_boto3.return_value.invoke.return_value = {'StatusCode': 200} with mock.patch.object(config, 'LAMBDA_ENDPOINT_URL', None): lambda_handler.LambdaHandler() mock_boto3.assert_called_once_with( 'lambda', region_name=mock.ANY, endpoint_url=None ) @mock.patch('boto3.client') def test_invoke_uses_local_endpoint(mock_boto3: mock.MagicMock) -> None: mock_boto3.return_value.invoke.return_value = {'StatusCode': 200} with mock.patch.object(config, 'LAMBDA_ENDPOINT_URL', 'http://localhost:9000'): lambda_handler.LambdaHandler() mock_boto3.assert_called_once_with( 'lambda', region_name=mock.ANY, endpoint_url='http://localhost:9000' ) @mock.patch('boto3.client') def test_warm_up_single_function(mock_boto3: mock.MagicMock) -> None: mock_boto3.return_value.invoke.return_value = {'StatusCode': 202} handler = lambda_handler.LambdaHandler() handler.warm_up('test-function') mock_boto3.return_value.invoke.assert_called_once_with( FunctionName='test-function', InvocationType='Event', Payload=b'{}', ) @mock.patch('boto3.client') def test_warm_up_multiple_functions(mock_boto3: mock.MagicMock) -> None: mock_boto3.return_value.invoke.return_value = {'StatusCode': 202} handler = lambda_handler.LambdaHandler() handler.warm_up(['fn-a', 'fn-b']) assert mock_boto3.return_value.invoke.call_count == 2 @mock.patch('boto3.client') def test_warm_up_swallows_exceptions(mock_boto3: mock.MagicMock) -> None: mock_boto3.return_value.invoke.side_effect = Exception('connection error') handler = lambda_handler.LambdaHandler() handler.warm_up('test-function') # must not raise