"""End-to-end test for coverage to compute flow integration.""" # FILE: tests/e2e/test_coverage_to_compute_flow.py import os import sys from unittest.mock import patch import pytest from fastapi.testclient import TestClient sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..")) sys.path.append( os.path.join(os.path.dirname(__file__), "..", "..", "apps", "svc-coverage") ) from libs.schemas import OverallStatus, Role, Status def create_test_app(): """Create a test FastAPI app without problematic startup events""" from fastapi import FastAPI from libs.config import BaseAppSettings from libs.security import TrustedProxyMiddleware # Create minimal settings class TestSettings(BaseAppSettings): service_name: str = "test-coverage" internal_cidrs: list[str] = ["127.0.0.1/32"] settings = TestSettings() # Create test app test_app = FastAPI( title="Test Coverage Service", description="Test coverage service", version="1.0.0", ) # Add middleware test_app.add_middleware( TrustedProxyMiddleware, internal_cidrs=settings.internal_cidrs ) # Import and add routes from main app from main import app as main_app test_app.router = main_app.router return test_app class TestCoverageToComputeFlow: """Test end-to-end flow from coverage checking to compute triggering""" @pytest.fixture def client(self): """Create test client""" test_app = create_test_app() return TestClient(test_app) @pytest.fixture def mock_dependencies(self): """Mock all external dependencies""" with ( patch("sys.modules") as mock_modules, patch("libs.policy.loader.PolicyLoader") as mock_loader, patch("libs.neo.client.Neo4jClient") as mock_kg, patch("libs.rag.retriever.RAGRetriever") as mock_rag, patch("sqlalchemy.orm.Session") as mock_db, patch("libs.config.create_neo4j_client") as mock_create_neo4j, patch("libs.config.create_event_bus") as mock_create_event_bus, patch("libs.policy.get_policy_loader") as mock_get_policy_loader, ): # Mock policy loader from unittest.mock import Mock mock_policy = Mock() mock_policy.policy.version = "1.0" mock_policy.policy.jurisdiction = "UK" mock_policy.policy.tax_year = "2024-25" mock_loader.return_value.load_policy.return_value = mock_policy mock_loader.return_value.compile_predicates.return_value = mock_policy # Mock KG client mock_kg_client = Mock() mock_kg.return_value = mock_kg_client # Mock RAG client mock_rag_client = Mock() mock_rag.return_value = mock_rag_client # Mock database session mock_session = Mock() mock_db.return_value = mock_session # Mock the factory functions that are called during startup mock_create_neo4j.return_value = Mock() mock_create_event_bus.return_value = Mock() mock_get_policy_loader.return_value = mock_loader.return_value yield { "policy_loader": mock_loader.return_value, "kg_client": mock_kg_client, "rag_client": mock_rag_client, "db_session": mock_session, "policy": mock_policy, } def test_complete_coverage_flow(self, client, mock_dependencies): """Test complete flow when all evidence is present""" # Mock coverage evaluator to return complete coverage with patch("apps.svc_coverage.main.CoverageEvaluator") as mock_evaluator_class: from unittest.mock import Mock mock_evaluator = Mock() mock_evaluator_class.return_value = mock_evaluator # Mock complete coverage report mock_report = Mock() mock_report.overall_status = OverallStatus.OK mock_report.schedules_required = ["SA102"] mock_report.blocking_items = [] # Mock coverage details mock_evidence = Mock() mock_evidence.id = "P60" mock_evidence.status = Status.PRESENT_VERIFIED mock_evidence.role = Role.REQUIRED mock_evidence.found = [ Mock( doc_id="DOC-P60-001", kind="P60", ocr_confidence=0.95, extract_confidence=0.92, ) ] mock_schedule = Mock() mock_schedule.schedule_id = "SA102" mock_schedule.status = OverallStatus.OK mock_schedule.evidence = [mock_evidence] mock_report.coverage = [mock_schedule] mock_evaluator.check_document_coverage.return_value = mock_report # Call coverage check endpoint response = client.post( "/v1/coverage/check", json={ "taxpayer_id": "T-001", "tax_year": "2024-25", "jurisdiction": "UK", }, ) assert response.status_code == 200 data = response.json() # Verify response structure assert data["overall_status"] == "OK" assert len(data["schedules_required"]) == 1 assert "SA102" in data["schedules_required"] assert len(data["coverage"]) == 1 assert len(data["blocking_items"]) == 0 # Verify coverage details sa102_coverage = data["coverage"][0] assert sa102_coverage["schedule_id"] == "SA102" assert sa102_coverage["status"] == "OK" assert len(sa102_coverage["evidence"]) == 1 p60_evidence = sa102_coverage["evidence"][0] assert p60_evidence["id"] == "P60" assert p60_evidence["status"] == "PRESENT_VERIFIED" assert p60_evidence["role"] == "REQUIRED" def test_incomplete_coverage_flow(self, client, mock_dependencies): """Test flow when evidence is missing""" with patch("apps.svc_coverage.main.CoverageEvaluator") as mock_evaluator_class: from unittest.mock import Mock mock_evaluator = Mock() mock_evaluator_class.return_value = mock_evaluator # Mock incomplete coverage report mock_report = Mock() mock_report.overall_status = OverallStatus.BLOCKING mock_report.schedules_required = ["SA102"] # Mock evidence mock_evidence = Mock() mock_evidence.id = "P60" mock_evidence.status = Status.MISSING mock_evidence.role = Role.REQUIRED mock_evidence.found = [] mock_evidence.acceptable_alternatives = ["P45", "FinalPayslipYTD"] mock_schedule = Mock() mock_schedule.schedule_id = "SA102" mock_schedule.status = OverallStatus.BLOCKING mock_schedule.evidence = [mock_evidence] # Mock blocking item (without acceptable_alternatives field per schema) mock_blocking_item = Mock() mock_blocking_item.schedule_id = "SA102" mock_blocking_item.evidence_id = "P60" mock_report.coverage = [mock_schedule] mock_report.blocking_items = [mock_blocking_item] mock_evaluator.check_document_coverage.return_value = mock_report # Call coverage check endpoint response = client.post( "/v1/coverage/check", json={ "taxpayer_id": "T-001", "tax_year": "2024-25", "jurisdiction": "UK", }, ) assert response.status_code == 200 data = response.json() # Verify incomplete status assert data["overall_status"] == "INCOMPLETE" assert len(data["blocking_items"]) == 1 # Verify blocking item details blocking_item = data["blocking_items"][0] assert blocking_item["evidence_id"] == "P60" assert blocking_item["schedule_id"] == "SA102" # Verify alternatives are in the evidence item, not blocking item sa102_coverage = data["coverage"][0] p60_evidence = sa102_coverage["evidence"][0] assert len(p60_evidence["acceptable_alternatives"]) == 2 def test_clarification_flow(self, client, mock_dependencies): """Test clarification question generation flow""" with patch("apps.svc_coverage.main.CoverageEvaluator") as mock_evaluator_class: mock_evaluator = AsyncMock() mock_evaluator_class.return_value = mock_evaluator # Mock clarification response mock_evaluator.generate_clarifying_question.return_value = AsyncMock( question="To complete the SA102 for 2024-25, we need P60. These documents support boxes SA102_b1, SA102_b2.", why="P60 provides year-end pay and PAYE tax figures required for employment income reporting.", blocking=True, boxes_affected=["SA102_b1", "SA102_b2"], upload_options=[ AsyncMock( label="Upload P60 (PDF/CSV)", accepted_formats=["pdf", "csv"], upload_endpoint="/v1/ingest/upload?tag=P60", ), AsyncMock( label="Upload P45 (PDF/CSV)", accepted_formats=["pdf", "csv"], upload_endpoint="/v1/ingest/upload?tag=P45", ), ], citations=[ AsyncMock( rule_id="UK.SA102.P60.Required", doc_id="SA102-Notes-2025", locator="p.3 ยง1.1", url="https://docs.local/SA102-Notes-2025#p3s1.1", ) ], ) # Call clarification endpoint response = client.post( "/v1/coverage/clarify", json={ "taxpayer_id": "T-001", "tax_year": "2024-25", "jurisdiction": "UK", "schedule_id": "SA102", "evidence_id": "P60", }, ) assert response.status_code == 200 data = response.json() # Verify clarification response assert "question" in data assert "why" in data assert data["blocking"] is True assert len(data["boxes_affected"]) == 2 assert len(data["upload_options"]) == 2 assert len(data["citations"]) == 1 # Verify upload options upload_option = data["upload_options"][0] assert "Upload P60" in upload_option["label"] assert "pdf" in upload_option["accepted_formats"] assert "/v1/ingest/upload" in upload_option["upload_endpoint"] def test_policy_validation_flow(self, client, mock_dependencies): """Test policy validation endpoint""" # Mock policy validation mock_dependencies["policy_loader"].validate_policy.return_value = AsyncMock( ok=True, errors=[], ) # Call validation endpoint response = client.post( "/v1/coverage/validate", json={ "version": "1.0", "jurisdiction": "UK", "tax_year": "2024-25", "tax_year_boundary": {"start": "2024-04-06", "end": "2025-04-05"}, "defaults": {"confidence_thresholds": {"ocr": 0.82, "extract": 0.85}}, "document_kinds": ["P60"], "triggers": { "SA102": {"any_of": ["exists(IncomeItem[type='Employment'])"]} }, "schedules": { "SA102": { "evidence": [ {"id": "P60", "role": "REQUIRED", "boxes": ["SA102_b1"]} ] } }, "status_classifier": { "present_verified": {"min_ocr": 0.82}, "present_unverified": {"min_ocr": 0.60}, "conflicting": {"conflict_rules": []}, "missing": {"default": True}, }, "conflict_resolution": {"precedence": ["P60"]}, "question_templates": {"default": {"text": "test", "why": "test"}}, }, ) assert response.status_code == 200 data = response.json() assert data["valid"] is True assert len(data["errors"]) == 0 def test_policy_reload_flow(self, client, mock_dependencies): """Test policy hot reload flow""" # Mock admin user check with patch("apps.svc_coverage.main.check_admin_permission") as mock_admin: mock_admin.return_value = True # Call reload endpoint response = client.post( "/admin/coverage/reload", headers={"Authorization": "Bearer admin-token"}, ) assert response.status_code == 200 data = response.json() assert data["reloaded"] is True assert "timestamp" in data assert "version" in data def test_policy_info_flow(self, client, mock_dependencies): """Test policy information endpoint""" # Call policy info endpoint response = client.get("/v1/coverage/policy") assert response.status_code == 200 data = response.json() assert data["version"] == "1.0" assert data["jurisdiction"] == "UK" assert data["tax_year"] == "2024-25" def test_health_check_flow(self, client, mock_dependencies): """Test health check endpoint""" response = client.get("/health") assert response.status_code == 200 data = response.json() assert data["status"] == "healthy" assert "timestamp" in data def test_error_handling_flow(self, client, mock_dependencies): """Test error handling in coverage flow""" with patch("apps.svc_coverage.main.CoverageEvaluator") as mock_evaluator_class: mock_evaluator = AsyncMock() mock_evaluator_class.return_value = mock_evaluator # Mock evaluator to raise exception mock_evaluator.check_document_coverage.side_effect = Exception( "KG connection failed" ) # Call coverage check endpoint response = client.post( "/v1/coverage/check", json={ "taxpayer_id": "T-001", "tax_year": "2024-25", "jurisdiction": "UK", }, ) assert response.status_code == 500 data = response.json() assert "error" in data["detail"] def test_invalid_request_flow(self, client, mock_dependencies): """Test validation of invalid requests""" # Missing required fields response = client.post( "/v1/coverage/check", json={ "taxpayer_id": "T-001", # Missing tax_year and jurisdiction }, ) assert response.status_code == 422 # Validation error def test_unauthorized_admin_flow(self, client, mock_dependencies): """Test unauthorized access to admin endpoints""" with patch("apps.svc_coverage.main.check_admin_permission") as mock_admin: mock_admin.return_value = False response = client.post( "/admin/coverage/reload", headers={"Authorization": "Bearer user-token"} ) assert response.status_code == 403 def test_concurrent_requests_flow(self, client, mock_dependencies): """Test handling of concurrent requests""" with patch("apps.svc_coverage.main.CoverageEvaluator") as mock_evaluator_class: mock_evaluator = AsyncMock() mock_evaluator_class.return_value = mock_evaluator # Mock successful response mock_evaluator.check_document_coverage.return_value = AsyncMock( overall_status=OverallStatus.OK, schedules_required=[], coverage=[], blocking_items=[], ) # Make multiple concurrent requests responses = [] for i in range(5): response = client.post( "/v1/coverage/check", json={ "taxpayer_id": f"T-{i:03d}", "tax_year": "2024-25", "jurisdiction": "UK", }, ) responses.append(response) # All should succeed for response in responses: assert response.status_code == 200 data = response.json() assert data["overall_status"] == "OK"