126 lines
4.3 KiB
Python
126 lines
4.3 KiB
Python
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())
|