from __future__ import annotations import io import logging import pytest from beaver_gateway.core import redact as redact_mod from beaver_gateway.core.redact import ( RedactFilter, RedactingFormatter, env_secrets, redact, ) # The two lines the gateway actually printed (httpx at INFO), verbatim # apart from the token bodies: the calendar MCP takes the private ical # feed as a query parameter, so the feed's secret is in the URL. GOOGLE = ( "HTTP Request: POST https://calendar-mcp.com/api/mcp" "?email=someone%40gmail.com&icsUrl=https%3A%2F%2Fcalendar.google.com" "%2Fcalendar%2Fical%2Fsomeone%2540gmail.com" "%2Fprivate-1f4a9c0de1f4a9c0de1f4a9c0de%2Fbasic.ics" ' "HTTP/1.1 200 OK"' ) USOS = ( "HTTP Request: POST https://calendar-mcp.com/api/mcp" "?email=someone%40gmail.com&icsUrl=https%3A%2F%2Fusosapps.put.poznan.pl" "%2Fservices%2Ftt%2Fupcoming_ical%3Flang%3Dpl%26user_id%3D145744" "%26key%3DABCDEF0123456789abcdef" ' "HTTP/1.1 202 Accepted"' ) @pytest.fixture(autouse=True) def _no_env_secrets() -> None: redact_mod._ENV_SECRETS[:] = [] # noqa: SLF001 def test_google_private_ical_token_is_masked() -> None: out = redact(GOOGLE) assert "1f4a9c0de1f4a9c0de1f4a9c0de" not in out assert "private-<…>" in out # The harmless part of the line survives, so the log stays useful. assert out.startswith("HTTP Request: POST https://calendar-mcp.com/api/mcp") assert out.endswith('"HTTP/1.1 200 OK"') def test_usos_key_is_masked_through_percent_encoding() -> None: out = redact(USOS) assert "ABCDEF0123456789abcdef" not in out assert "key%3D<…>" in out assert out.endswith('"HTTP/1.1 202 Accepted"') @pytest.mark.parametrize( ("line", "secret"), [ ("GET /hooks/komodo?token=s3cret-value&x=1", "s3cret-value"), ("GET /x?api_key=s3cret-value", "s3cret-value"), ("GET /x?session_key=s3cret-value", "s3cret-value"), ("headers={'Authorization': 'Bearer s3cret-value'}", "s3cret-value"), ("authorization: token s3cret-value", "s3cret-value"), ("retrying with Bearer s3cret-value", "s3cret-value"), ], ) def test_credentials_in_urls_and_headers(line: str, secret: str) -> None: assert secret not in redact(line) def test_known_env_values_are_masked() -> None: redact_mod._ENV_SECRETS[:] = env_secrets( # noqa: SLF001 {"FIREFLY_PAT": "pat-0123456789", "POSTGRES_USER": "beaver"} ) out = redact("firefly said no to pat-0123456789 (user beaver)") assert "pat-0123456789" not in out # Only credential-shaped names contribute; ordinary config survives. assert "beaver" in out def test_env_values_are_masked_percent_encoded_too() -> None: raw = "https://cal/private-xyz/basic.ics" redact_mod._ENV_SECRETS[:] = env_secrets({"CALENDAR_MCPS": raw}) # noqa: SLF001 assert raw not in redact(f"fetching {raw}") assert "%2Fprivate-xyz%2F" not in redact( "fetching https%3A%2F%2Fcal%2Fprivate-xyz%2Fbasic.ics" ) def test_formatter_redacts_the_traceback_too() -> None: stream = io.StringIO() handler = logging.StreamHandler(stream) handler.setFormatter(RedactingFormatter(logging.Formatter("%(message)s"))) log = logging.getLogger("test_redact.traceback") log.propagate = False log.handlers = [handler] try: msg = f"connect failed: {GOOGLE}" raise ConnectionError(msg) # noqa: TRY301 except ConnectionError: log.exception("failed to list tools for MCP %r", "calendar-personal") out = stream.getvalue() assert "calendar-personal" in out assert "1f4a9c0de1f4a9c0de1f4a9c0de" not in out def test_filter_masks_a_secret_split_across_template_and_args() -> None: record = logging.LogRecord( "uvicorn.access", logging.INFO, __file__, 1, "key=%s", ("s3cret-value",), None ) assert RedactFilter().filter(record) assert record.getMessage() == "key=<…>" def test_filter_leaves_a_clean_record_structured() -> None: record = logging.LogRecord( "uvicorn.access", logging.INFO, __file__, 1, "closing %s", ("conv-1",), None ) assert RedactFilter().filter(record) assert record.args == ("conv-1",)