Initial commit
Some checks failed
CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
CI/CD Pipeline / Policy Validation (push) Has been cancelled
CI/CD Pipeline / Test Suite (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-firm-connectors) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-forms) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-hmrc) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ingestion) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-normalize-map) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ocr) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-indexer) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-reason) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rpa) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (ui-review) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (ui-review) (push) Has been cancelled
CI/CD Pipeline / Generate SBOM (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Notifications (push) Has been cancelled
Some checks failed
CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
CI/CD Pipeline / Policy Validation (push) Has been cancelled
CI/CD Pipeline / Test Suite (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-firm-connectors) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-forms) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-hmrc) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ingestion) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-normalize-map) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ocr) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-indexer) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-reason) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rpa) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (ui-review) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (ui-review) (push) Has been cancelled
CI/CD Pipeline / Generate SBOM (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Notifications (push) Has been cancelled
This commit is contained in:
472
tests/e2e/test_coverage_to_compute_flow.py
Normal file
472
tests/e2e/test_coverage_to_compute_flow.py
Normal file
@@ -0,0 +1,472 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user