From b7bab56d4d8144cfcd764bd052353094f2410627 Mon Sep 17 00:00:00 2001 From: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com> Date: Sat, 19 Sep 2026 12:52:28 -0700 Subject: [PATCH] test(e2e): report safe OAuth failure locations --- .github/e2e-stack/assert_tests_ran.py | 8 ++++++ .../test_e2e_changed_gate.py | 28 +++++++++++++++++++ tests/e2e/conftest.py | 8 ++++++ 3 files changed, 44 insertions(+) diff --git a/.github/e2e-stack/assert_tests_ran.py b/.github/e2e-stack/assert_tests_ran.py index bc299b14af8..1b051f860cc 100644 --- a/.github/e2e-stack/assert_tests_ran.py +++ b/.github/e2e-stack/assert_tests_ran.py @@ -1,4 +1,5 @@ import os +import re import sys import xml.etree.ElementTree as ET from pathlib import Path @@ -45,6 +46,13 @@ def main() -> int: if case.get("file") != path or all(case.find(tag) is None for tag in ("failure", "error")): continue _ = sys.stdout.write(f" failed: {case.get('classname', '')}::{case.get('name', '')}\n") + for prop in case.findall("./properties/property"): + name = prop.get("name", "") + value = prop.get("value", "") + if name in ("oauth_failure_phase", "oauth_exception_type", "oauth_frame") and re.fullmatch( + r"[A-Za-z0-9_.:<>-]{1,240}", value + ): + _ = sys.stdout.write(f" {name}: {value}\n") if ( selected and not missing diff --git a/tests/code_coverage_tests/test_e2e_changed_gate.py b/tests/code_coverage_tests/test_e2e_changed_gate.py index 5ae0863baf0..d14f007403b 100644 --- a/tests/code_coverage_tests/test_e2e_changed_gate.py +++ b/tests/code_coverage_tests/test_e2e_changed_gate.py @@ -226,3 +226,31 @@ def test_an_unusable_secret_is_named_without_printing_its_value( assert unprintable not in result.stderr assert result.stdout == "" assert not env_path.exists() + + +@pytest.mark.parametrize("phase", ("setup", "call", "teardown")) +def test_oauth_failure_diagnostics_do_not_publish_private_payloads(tmp_path: Path, phase: str) -> None: + suite: Final = ET.Element("testsuite") + case: Final = ET.SubElement(suite, "testcase", file=SELECTED[0]) + private: Final = "private-token-in-exception-message" + failure: Final = ET.SubElement(case, "failure", message=private) + failure.text = private + properties: Final = ET.SubElement(case, "properties") + for name, value in ( + ("oauth_failure_phase", phase), + ("oauth_exception_type", "AssertionError"), + ("oauth_frame", "oauth_gateway.py:120:start"), + ("oauth_frame", f"injected\\n{private}"), + ("unrelated_property", private), + ): + _ = ET.SubElement(properties, "property", name=name, value=value) + report: Final = tmp_path / "report.xml" + ET.ElementTree(suite).write(report) + result: Final = subprocess.run( + [sys.executable, str(GATE), str(report), SELECTED[0]], capture_output=True, text=True + ) + assert result.returncode == 1 + assert f"oauth_failure_phase: {phase}" in result.stdout + assert "oauth_exception_type: AssertionError" in result.stdout + assert "oauth_frame: oauth_gateway.py:120:start" in result.stdout + assert private not in result.stdout + result.stderr diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 268d517a7fe..b0904e39a1f 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -17,6 +17,7 @@ import functools import os from collections.abc import Generator, Iterator from datetime import datetime, timezone +from pathlib import Path from types import MappingProxyType from typing import Final @@ -245,6 +246,13 @@ def pytest_runtest_makereport( """Stash the call-phase outcome so teardown can tell a passed test from a failed one without re-deriving it.""" report = yield + if item.get_closest_marker("mcp_oauth_live") is not None and call.excinfo is not None: + # Publish code locations only, never exception messages, source text or locals. + item.user_properties.append(("oauth_failure_phase", report.when)) + item.user_properties.append(("oauth_exception_type", call.excinfo.type.__name__)) + for entry in call.excinfo.traceback: + item.user_properties.append(("oauth_frame", f"{Path(entry.path).name}:{entry.lineno + 1}:{entry.name}")) + report.user_properties = list(item.user_properties) if report.when == "call": item.stash[_CALL_PASSED] = report.passed return report