"""Lambda test module.""" import base64 import config import json import logging from httpx import HTTPStatusError, Response from unittest.mock import patch import pytest from src import app def events(records: list[tuple[int, int]]) -> list[dict]: """Fixture for a sample custom event.""" return [ { "ASSET_FINAL_ID": record[0], "DURATION_MS": record[1], } for record in records ] def kafka_event(records: list[tuple[int, int]]) -> dict: """Fixture for a sample Kafka event.""" return { "eventSource": "aws:kafka", "records": { "topic-name-1": [ { "topic": "topic-name", "value": base64.b64encode( json.dumps(record).encode("utf-8") ).decode("utf-8"), } for record in events(records) ], "topic-name-2": [], }, } def sqs_event(records: list[tuple[int, int]]) -> dict: return { "Records": [ { "eventSource": "aws:sqs", "body": json.dumps(record), } for record in events(records) ], } def test_httpx_request_logging_is_silenced(): # guards the module-level side effect in src/app.py that quiets httpx's # per-request INFO logging assert logging.getLogger("httpx").level == logging.WARNING def test_decode_records_kafka_event(): """Test decode_records with a valid Kafka event.""" event = kafka_event([(123, 300), (456, 600)]) assert app.decode_records(event) == [(123, 300, False), (456, 600, False)] def test_decode_records_sqs_event(): event = sqs_event([(123, 300), (456, 600)]) assert app.decode_records(event) == [(123, 300, False), (456, 600, False)] def test_decode_records_sqs_overwrite(): """An OVERWRITE flag in the SQS body is decoded onto the record.""" event = { "Records": [ { "eventSource": "aws:sqs", "body": json.dumps( {"ASSET_FINAL_ID": 123, "DURATION_MS": 300, "OVERWRITE": True} ), } ] } assert app.decode_records(event) == [(123, 300, True)] @pytest.mark.parametrize( "overwrite_value", [ pytest.param("false", id="string-false"), pytest.param("true", id="string-true"), pytest.param(1, id="int-one"), ], ) def test_decode_records_overwrite_only_exact_bool(overwrite_value): """Only a real boolean True requests overwrite; truthy non-bools do not.""" event = { "Records": [ { "eventSource": "aws:sqs", "body": json.dumps( { "ASSET_FINAL_ID": 123, "DURATION_MS": 300, "OVERWRITE": overwrite_value, } ), } ] } assert app.decode_records(event) == [(123, 300, False)] def test_decode_custom_event(): """Test decode_records with a custom event format.""" event = {"eventSource": "custom", "assets": events([(123, 300)])} assert app.decode_records(event) == [(123, 300, False)] @pytest.mark.parametrize( "event", [ pytest.param({"eventSource": "aws:s3"}, id="unknown-top-level-event-source"), pytest.param( {"Records": [{"eventSource": "aws:s3", "s3": {"object": {}}}]}, id="records-from-non-sqs-source", ), ], ) def test_decode_records_non_unexpected_event(event): """Test decode_records with a unexpected event.""" with pytest.raises(Exception, match="Unrecognized event format!"): app.decode_records(event) @patch("src.app.ows_assets") @patch("src.app.hive") @patch("src.app.decode_records") @patch("config.secrets_manager_client.get_cred", return_value=None) def test_handler_success( mock_get_cred, mock_decode_records, mock_hive, mock_ows_assets ): """Test the handler function for a successful execution.""" asset_final_id = 12345 duration = 300 signed_url = "http://s3.url/for/asset" event = {"eventSource": "aws:kafka", "records": {}} hive_data = {"hive_result": "some_data"} mock_decode_records.return_value = [(asset_final_id, duration, False)] mock_ows_assets.get_presigned_url.return_value = signed_url mock_hive.run_task.return_value = hive_data result = app.handler(event, None) mock_decode_records.assert_called_once_with(event) mock_ows_assets.get_presigned_url.assert_called_once_with(asset_final_id) mock_hive.run_task.assert_called_once_with(signed_url) mock_ows_assets.post_results.assert_called_once_with( asset_final_id, hive_data, overwrite=False ) assert result == {"status": "OK"} @patch("src.app.ows_assets") @patch("src.app.hive") @patch("src.app.decode_records") @patch("config.secrets_manager_client.get_cred", return_value=None) def test_asset_too_long(mock_get_cred, mock_decode_records, mock_hive, mock_ows_assets): asset_final_id = 12345 duration = 600 * 1000 mock_decode_records.return_value = [(asset_final_id, duration, False)] result = app.handler({}, None) mock_ows_assets.get_presigned_url.assert_not_called() mock_hive.run_task.assert_not_called() mock_ows_assets.post_results.assert_not_called() assert result == {"status": "OK"} @patch("src.app.logger") @patch("src.app.ows_assets") @patch("src.app.hive") @patch("src.app.decode_records") @patch("config.secrets_manager_client.get_cred", return_value=None) def test_asset_not_found( mock_get_cred, mock_decode_records, mock_hive, mock_ows_assets, mock_logger ): event = {"eventSource": "aws:kafka", "records": {}} asset_final_id = 12345 duration = 30 * 1000 mock_decode_records.return_value = [(asset_final_id, duration, False)] mock_ows_assets.get_presigned_url.side_effect = HTTPStatusError( "Not Found", request=None, response=Response(404) ) result = app.handler(event, None) mock_decode_records.assert_called_once_with(event) mock_ows_assets.get_presigned_url.assert_called_once_with(asset_final_id) mock_hive.run_task.assert_not_called() mock_ows_assets.post_results.assert_not_called() assert result == {"status": "OK"} mock_logger.info.assert_called_once_with( f"Skipping asset_final_id {asset_final_id}, asset deleted before Hive scan started" ) mock_logger.error.assert_not_called() @patch("config.ENVIRONMENT", config.PROD_ENVIRONMENT) @patch("src.app.logger") @patch("src.app.ows_assets") @patch("src.app.hive") @patch("src.app.decode_records") @patch("config.secrets_manager_client.get_cred", return_value=None) def test_asset_not_found_in_prod_is_not_an_error( mock_get_cred, mock_decode_records, mock_hive, mock_ows_assets, mock_logger ): event = {"eventSource": "aws:kafka", "records": {}} asset_final_id = 12345 duration = 30 * 1000 mock_decode_records.return_value = [(asset_final_id, duration, False)] mock_ows_assets.get_presigned_url.side_effect = HTTPStatusError( "Not Found", request=None, response=Response(404) ) result = app.handler(event, None) assert result == {"status": "OK"} mock_hive.run_task.assert_not_called() mock_ows_assets.post_results.assert_not_called() mock_logger.info.assert_called_once_with( f"Skipping asset_final_id {asset_final_id}, asset deleted before Hive scan started" ) mock_logger.error.assert_not_called() @patch("src.app.logger") @patch("src.app.ows_assets") @patch("src.app.hive") @patch("src.app.decode_records") @patch("config.secrets_manager_client.get_cred", return_value=None) def test_asset_deleted_before_results_saved( mock_get_cred, mock_decode_records, mock_hive, mock_ows_assets, mock_logger ): asset_final_id = 12345 duration = 300 signed_url = "http://s3.url/for/asset" event = {"eventSource": "aws:kafka", "records": {}} hive_data = {"hive_result": "some_data"} mock_decode_records.return_value = [(asset_final_id, duration, False)] mock_ows_assets.get_presigned_url.return_value = signed_url mock_hive.run_task.return_value = hive_data mock_ows_assets.post_results.side_effect = HTTPStatusError( "Not Found", request=None, response=Response(404) ) result = app.handler(event, None) mock_ows_assets.post_results.assert_called_once_with( asset_final_id, hive_data, overwrite=False ) assert result == {"status": "OK"} mock_logger.info.assert_called_once_with( f"Skipping asset_final_id {asset_final_id}, asset deleted, Hive results discarded" ) mock_logger.error.assert_not_called() @patch("src.app.ows_assets") @patch("src.app.hive") @patch("src.app.decode_records") @patch("config.secrets_manager_client.get_cred", return_value=None) def test_asset_already_processed( mock_get_cred, mock_decode_records, mock_hive, mock_ows_assets ): """Test the handler function for a successful execution.""" asset_final_id = 12345 duration = 300 signed_url = "http://s3.url/for/asset" event = {"eventSource": "aws:kafka", "records": {}} hive_data = {"hive_result": "some_data"} mock_decode_records.return_value = [(asset_final_id, duration, False)] mock_ows_assets.get_presigned_url.return_value = signed_url mock_hive.run_task.return_value = hive_data mock_ows_assets.post_results.side_effect = HTTPStatusError( "Conflict", request=None, response=Response(409) ) result = app.handler(event, None) mock_decode_records.assert_called_once_with(event) mock_ows_assets.get_presigned_url.assert_called_once_with(asset_final_id) mock_hive.run_task.assert_called_once_with(signed_url) mock_ows_assets.post_results.assert_called_once_with( asset_final_id, hive_data, overwrite=False ) assert result == {"status": "OK"} @patch("src.app.ows_assets") @patch("src.app.hive") @patch("src.app.decode_records") @patch("config.secrets_manager_client.get_cred", return_value=None) def test_asset_post_bad_request( mock_get_cred, mock_decode_records, mock_hive, mock_ows_assets ): """Test the handler function for a successful execution.""" asset_final_id = 12345 duration = 300 signed_url = "http://s3.url/for/asset" event = {"eventSource": "aws:kafka", "records": {}} hive_data = {"hive_result": "some_data"} mock_decode_records.return_value = [(asset_final_id, duration, False)] mock_ows_assets.get_presigned_url.return_value = signed_url mock_hive.run_task.return_value = hive_data mock_ows_assets.post_results.side_effect = HTTPStatusError( "Bad Request", request=None, response=Response(400, content=b"{}") ) with pytest.raises(HTTPStatusError, match="Bad Request"): app.handler(event, None) mock_decode_records.assert_called_once_with(event) mock_ows_assets.get_presigned_url.assert_called_once_with(asset_final_id) mock_hive.run_task.assert_called_once_with(signed_url) mock_ows_assets.post_results.assert_called_once_with( asset_final_id, hive_data, overwrite=False ) @patch("src.app.ows_assets") @patch("src.app.hive") @patch("src.app.decode_records") @patch("config.secrets_manager_client.get_cred", return_value=None) def test_hive_bad_request( mock_get_cred, mock_decode_records, mock_hive, mock_ows_assets ): """Test the handler function for a successful execution.""" asset_final_id = 12345 duration = 300 signed_url = "http://s3.url/for/asset" event = {"eventSource": "aws:kafka", "records": {}} mock_decode_records.return_value = [(asset_final_id, duration, False)] mock_ows_assets.get_presigned_url.return_value = signed_url mock_hive.run_task.side_effect = HTTPStatusError( "Bad Request", request=None, response=Response(400, content=b"{}") ) with pytest.raises(HTTPStatusError, match="Bad Request"): app.handler(event, None) mock_decode_records.assert_called_once_with(event) mock_ows_assets.get_presigned_url.assert_called_once_with(asset_final_id) mock_hive.run_task.assert_called_once_with(signed_url) mock_ows_assets.post_results.assert_not_called() @patch("src.app.ows_assets") @patch("src.app.hive") @patch("src.app.decode_records") @patch("config.secrets_manager_client.get_cred", return_value=None) def test_asset_get_bad_request( mock_get_cred, mock_decode_records, mock_hive, mock_ows_assets ): event = {"eventSource": "aws:kafka", "records": {}} asset_final_id = 12345 duration = 30 * 1000 mock_decode_records.return_value = [(asset_final_id, duration, False)] mock_ows_assets.get_presigned_url.side_effect = HTTPStatusError( "Bad Request", request=None, response=Response(400, content=b"{}") ) with pytest.raises(HTTPStatusError, match="Bad Request"): app.handler(event, None) mock_decode_records.assert_called_once_with(event) mock_ows_assets.get_presigned_url.assert_called_once_with(asset_final_id) mock_hive.run_task.assert_not_called() mock_ows_assets.post_results.assert_not_called() @patch("src.app.logger") @patch("src.app.decode_records", side_effect=Exception("Test exception")) @patch("config.secrets_manager_client.get_cred", return_value=None) def test_handler_exception(mock_get_cred, mock_decode_records, mock_logger): """Test the handler function when an exception occurs.""" with pytest.raises(Exception, match="Test exception"): app.handler({}, None) mock_logger.exception.assert_called_once_with("Test exception") @patch("src.app.ows_assets") @patch("src.app.hive") @patch("src.app.decode_records") @patch("config.secrets_manager_client.get_cred", return_value=None) def test_handler_forwards_overwrite( mock_get_cred, mock_decode_records, mock_hive, mock_ows_assets ): """A record decoded with overwrite=True forwards it to post_results.""" asset_final_id = 12345 duration = 300 signed_url = "http://s3.url/for/asset" hive_data = {"hive_result": "some_data"} mock_decode_records.return_value = [(asset_final_id, duration, True)] mock_ows_assets.get_presigned_url.return_value = signed_url mock_hive.run_task.return_value = hive_data result = app.handler({}, None) mock_ows_assets.post_results.assert_called_once_with( asset_final_id, hive_data, overwrite=True ) assert result == {"status": "OK"} @patch("src.app.ows_assets") @patch("src.app.hive") @patch("src.app.decode_records") @patch("config.secrets_manager_client.get_cred", return_value=None) def test_handler_overwrite_409_raises( mock_get_cred, mock_decode_records, mock_hive, mock_ows_assets ): """Under overwrite a 409 means the endpoint ignored the flag: raise, not skip.""" asset_final_id = 12345 duration = 300 mock_decode_records.return_value = [(asset_final_id, duration, True)] mock_ows_assets.get_presigned_url.return_value = "http://s3.url/for/asset" mock_hive.run_task.return_value = {"hive_result": "some_data"} mock_ows_assets.post_results.side_effect = HTTPStatusError( "Conflict", request=None, response=Response(409) ) with pytest.raises(HTTPStatusError, match="Conflict"): app.handler({}, None)