import logging import pytest from httpx import ASGITransport, AsyncClient from lead_scoring.config import Settings from lead_scoring.main import create_app from lead_scoring.observability import Observability, redact_payload, redact_text def test_redact_payload_removes_credentials_snapshot_and_pii() -> None: secret = "sentinel-vkey-123" payload = { "authorization": f"Bearer {secret}", "tenant_v_key": secret, "service_secret": "sentinel-service-secret", "snapshot": {"lead_profile": {"email": "person@example.com"}}, "review_note": "private note", "safe_code": "aihub_timeout", } redacted = redact_payload(payload) assert secret not in repr(redacted) assert "sentinel-service-secret" not in repr(redacted) assert redacted["snapshot"] == "[REDACTED]" assert redacted["review_note"] == "[REDACTED]" assert redacted["safe_code"] == "aihub_timeout" technical_metadata = redact_payload({"stage_name": "AIHub", "message_id": "msg-1"}) assert technical_metadata == {"stage_name": "AIHub", "message_id": "msg-1"} def test_observability_records_redacted_exception_and_alert_threshold(caplog) -> None: caplog.set_level(logging.DEBUG) telemetry = Observability( alert_thresholds={"aihub_failures_total": 1}, ) telemetry.record_exception( "aihub_failure", RuntimeError("request contained sentinel-secret"), run_id="run-1", secrets=("sentinel-secret",), ) telemetry.increment( "aihub_failures_total", run_id="run-1", fields={ "snapshot": { "email": "person@example.com", "phone": "+84901234567", } }, ) telemetry.record( "trace.test", fields={"safe_value": "sentinel-service-secret"}, secrets=("sentinel-service-secret",), ) snapshot = telemetry.snapshot() assert snapshot["counters"]["aihub_failures_total"] == 1 assert snapshot["alerts"][0]["metric"] == "aihub_failures_total" assert "sentinel-secret" not in repr(snapshot) assert "sentinel-service-secret" not in repr(snapshot) assert "sentinel-service-secret" not in caplog.text assert "+84901234567" not in repr(snapshot) assert "person@example.com" not in repr(snapshot) def test_settings_expose_kill_switch_and_alert_thresholds() -> None: settings = Settings(kill_switch_enabled=True, stuck_run_alert_threshold=2) assert settings.kill_switch_enabled is True assert settings.stuck_run_alert_threshold == 2 def test_observability_exposes_required_metric_families_and_redacted_text() -> None: telemetry = Observability() for metric in ( "scoring_admission_total", "scoring_run_latency_seconds", "aihub_failures_total", "scoring_degraded_rate", "scoring_hitl_rate", "scoring_override_rate", "aihub_tokens_total", "scoring_stuck_runs_total", "outbox_retries_total", "outbox_dead_letter_total", "retention_failures_total", ): telemetry.increment(metric) telemetry.observe("scoring_run_latency_seconds", 0.25) telemetry.observe("scoring_degraded_rate", 1.0) snapshot = telemetry.snapshot() assert set(snapshot["counters"]).issuperset( { "scoring_admission_total", "aihub_failures_total", "outbox_dead_letter_total", "retention_failures_total", } ) assert snapshot["histograms"]["scoring_degraded_rate"]["average"] == 1.0 assert redact_text("Authorization: Bearer vkey-secret", secrets=("vkey-secret",)) == ( "Authorization: Bearer [REDACTED]" ) assert "person@example.com" not in redact_text("person@example.com") assert "+84901234567" not in redact_text("+84901234567") @pytest.mark.asyncio async def test_request_correlation_does_not_echo_pii() -> None: app = create_app(Settings()) async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: response = await client.get( "/health", headers={"X-Request-ID": "person@example.com"}, ) assert response.status_code == 503 assert response.headers["X-Request-ID"] != "person@example.com" assert "person@example.com" not in repr(app.state.telemetry.snapshot())