"""Tests for pdf-generate Lambda handler. QA-C6 (partial): Verifies SQS batch processing, error handling, and batch failure reporting for the PDF generation pipeline. """ import importlib import sys import os from unittest.mock import MagicMock, patch from helpers import make_sqs_event # Import pdf-generate app under a unique module name to avoid collision with # suggestions/app.py (both are named 'app'). _pdf_gen_dir = os.path.join(os.path.dirname(__file__), "..", "pdf-generate") _spec = importlib.util.spec_from_file_location( "pdf_generate_app", os.path.join(_pdf_gen_dir, "app.py") ) pdf_generate_app = importlib.util.module_from_spec(_spec) sys.modules["pdf_generate_app"] = pdf_generate_app _spec.loader.exec_module(pdf_generate_app) class TestPdfGenerateHandler: """Test the SQS handler entry point for pdf-generate Lambda.""" @patch.object(pdf_generate_app, "generate_pdf") def test_handler_processes_sqs_record_successfully(self, mock_generate_pdf): """QA-C6: Handler extracts proposalId from SQS body and calls generate_pdf.""" mock_generate_pdf.return_value = None event = make_sqs_event({"payload": {"proposalId": "abc-123"}}) result = pdf_generate_app.handler(event, None) mock_generate_pdf.assert_called_once_with("abc-123") assert result["batchItemFailures"] == [] @patch.object(pdf_generate_app, "generate_pdf") def test_handler_processes_multiple_records(self, mock_generate_pdf): """QA-C6: Handler processes all records in an SQS batch.""" mock_generate_pdf.return_value = None event = make_sqs_event( {"payload": {"proposalId": "id-1"}}, {"payload": {"proposalId": "id-2"}}, {"payload": {"proposalId": "id-3"}}, ) result = pdf_generate_app.handler(event, None) assert mock_generate_pdf.call_count == 3 assert result["batchItemFailures"] == [] @patch.object(pdf_generate_app, "generate_pdf") def test_handler_returns_batch_failures_on_error(self, mock_generate_pdf): """QA-C6: Handler returns failed message IDs for partial batch failure.""" mock_generate_pdf.side_effect = [None, Exception("PDF generation failed"), None] event = make_sqs_event( {"payload": {"proposalId": "id-1"}}, {"payload": {"proposalId": "id-2"}}, {"payload": {"proposalId": "id-3"}}, ) result = pdf_generate_app.handler(event, None) assert len(result["batchItemFailures"]) == 1 assert result["batchItemFailures"][0]["itemIdentifier"] == "msg-1" @patch.object(pdf_generate_app, "generate_pdf") def test_handler_handles_malformed_body(self, mock_generate_pdf): """QA-C6: Handler reports failure for records with invalid JSON body.""" event = { "Records": [ { "messageId": "msg-bad", "body": "not-valid-json", "receiptHandle": "handle-0", } ] } result = pdf_generate_app.handler(event, None) assert len(result["batchItemFailures"]) == 1 assert result["batchItemFailures"][0]["itemIdentifier"] == "msg-bad" mock_generate_pdf.assert_not_called() @patch.object(pdf_generate_app, "generate_pdf") def test_handler_handles_missing_proposal_id(self, mock_generate_pdf): """QA-C6: Handler reports failure when proposalId is missing from payload.""" event = make_sqs_event({"payload": {}}) result = pdf_generate_app.handler(event, None) assert len(result["batchItemFailures"]) == 1 mock_generate_pdf.assert_not_called() @patch.object(pdf_generate_app, "generate_pdf") def test_handler_handles_empty_records(self, mock_generate_pdf): """QA-C6: Handler handles empty Records array gracefully.""" result = pdf_generate_app.handler({"Records": []}, None) assert result["batchItemFailures"] == [] mock_generate_pdf.assert_not_called() @patch.object(pdf_generate_app, "generate_pdf") def test_handler_handles_body_without_payload_wrapper(self, mock_generate_pdf): """QA-C6: Handler supports body with proposalId at top level (no payload wrapper).""" mock_generate_pdf.return_value = None event = make_sqs_event({"proposalId": "direct-id"}) result = pdf_generate_app.handler(event, None) mock_generate_pdf.assert_called_once_with("direct-id") assert result["batchItemFailures"] == [] class TestPdfGenerateHelpers: """Test helper functions in the pdf-generate Lambda.""" @patch.object(pdf_generate_app, "_retry_request") def test_fetch_proposal_returns_dict_on_200(self, mock_retry): """QA-C6: fetch_proposal returns proposal dict on 200 response.""" mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"id": "abc", "proposalNumber": "P-001"} mock_retry.return_value = mock_response result = pdf_generate_app.fetch_proposal("abc") assert result is not None assert result["proposalNumber"] == "P-001" @patch.object(pdf_generate_app, "_retry_request") def test_fetch_proposal_returns_none_on_404(self, mock_retry): """QA-C6: fetch_proposal returns None when API returns 404.""" mock_response = MagicMock() mock_response.status_code = 404 mock_retry.return_value = mock_response result = pdf_generate_app.fetch_proposal("nonexistent") assert result is None @patch.object(pdf_generate_app, "_retry_request") def test_fetch_proposal_returns_none_on_exception(self, mock_retry): """QA-C6: fetch_proposal returns None on network error.""" mock_retry.side_effect = Exception("Connection refused") result = pdf_generate_app.fetch_proposal("abc") assert result is None