Files
agent-framework/python/packages/foundry/tests/foundry/test_oauth_helpers.py
T
Giles Odigwe 7b70f80036 Python: Surface oauth_consent_request events from Responses API in Foundry clients (#5070)
* Fix Foundry clients not surfacing oauth_consent_request events (#5054)

Override _parse_chunk_from_openai in both RawFoundryChatClient and
RawFoundryAgentChatClient to intercept response.output_item.added
events with item.type == 'oauth_consent_request'. The consent link
is validated (HTTPS required) and converted to
Content.from_oauth_consent_request, which the AG-UI layer already
knows how to emit as a CUSTOM event.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review feedback for #5054 OAuth consent parsing

- Extract shared helper (try_parse_oauth_consent_event) to avoid
  duplicated logic between RawFoundryChatClient and
  RawFoundryAgentChatClient
- Use urllib.parse.urlparse() for HTTPS validation instead of
  case-sensitive startswith check
- Sanitize log messages to avoid leaking consent_link tokens;
  log only item id
- Add model=self.model to ChatResponseUpdate to match parent behavior
- Add assertions on role, raw_representation, and model in happy-path
  tests
- Add test for empty-string consent_link
- Add test verifying non-oauth events delegate to super()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Handle response.oauth_consent_requested top-level event (#5054)

Add support for the top-level response.oauth_consent_requested stream
event in addition to the response.output_item.added variant. The
service may emit either form; handle both so the consent link is
reliably surfaced.

Extract _validate_consent_link helper within _oauth_helpers.py to
reduce nesting.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #5054: Python: [Bug]: `FoundryAgent` (Responses API) Does Not Surface `oauth_consent_request` as a CUSTOM AG-UI Event

* Address review feedback: defensive getattr and dedicated helper tests (#5054)

- Use getattr(event, 'type', None) in try_parse_oauth_consent_event
  for defensive access against malformed events without a type attribute
- Add test_oauth_helpers.py with unit tests for _validate_consent_link
  and try_parse_oauth_consent_event covering edge cases:
  - HTTPS URL with empty netloc (https:///path)
  - Warning log messages for rejected consent links
  - Event objects missing 'type' attribute

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #5054: Python: [Bug]: `FoundryAgent` (Responses API) Does Not Surface `oauth_consent_request` as a CUSTOM AG-UI Event

* Fix mypy: match _parse_chunk_from_openai signature with superclass

Add seen_reasoning_delta_item_ids parameter to _parse_chunk_from_openai
overrides in both RawFoundryChatClient and RawFoundryAgentChatClient to
match the updated superclass signature on main. Update super() calls and
test assertions accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
2026-04-24 09:59:14 +00:00

165 lines
5.9 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import logging
from typing import Any
from unittest.mock import MagicMock
import pytest
from agent_framework_foundry._oauth_helpers import _validate_consent_link, try_parse_oauth_consent_event
# region _validate_consent_link tests
def test_validate_consent_link_accepts_valid_https() -> None:
"""A valid HTTPS URL with a netloc passes validation."""
link = "https://consent.example.com/auth?code=123"
assert _validate_consent_link(link, "item-1") == link
def test_validate_consent_link_rejects_http(caplog: pytest.LogCaptureFixture) -> None:
"""An HTTP link is rejected and a warning is logged."""
with caplog.at_level(logging.WARNING):
result = _validate_consent_link("http://insecure.example.com/login", "item-2")
assert result == ""
assert "non-HTTPS" in caplog.text
assert "item-2" in caplog.text
def test_validate_consent_link_rejects_empty_netloc(caplog: pytest.LogCaptureFixture) -> None:
"""An HTTPS URL with an empty netloc (e.g. https:///path) is rejected."""
with caplog.at_level(logging.WARNING):
result = _validate_consent_link("https:///path", "item-3")
assert result == ""
assert "non-HTTPS" in caplog.text
assert "item-3" in caplog.text
def test_validate_consent_link_rejects_non_url(caplog: pytest.LogCaptureFixture) -> None:
"""A non-URL string is rejected."""
with caplog.at_level(logging.WARNING):
result = _validate_consent_link("not-a-url", "item-4")
assert result == ""
# endregion
# region try_parse_oauth_consent_event tests
def _make_output_item_event(
*,
item_type: str = "oauth_consent_request",
consent_link: Any = "https://consent.example.com/auth",
item_id: str = "oauth-item-1",
) -> MagicMock:
"""Create a mock ``response.output_item.added`` event."""
event = MagicMock()
event.type = "response.output_item.added"
item = MagicMock()
item.type = item_type
item.consent_link = consent_link
item.id = item_id
event.item = item
return event
def _make_top_level_event(
*,
consent_link: Any = "https://consent.example.com/authorize",
event_id: str = "consent-event-1",
) -> MagicMock:
"""Create a mock ``response.oauth_consent_requested`` event."""
event = MagicMock()
event.type = "response.oauth_consent_requested"
event.consent_link = consent_link
event.id = event_id
return event
def test_returns_none_for_unrelated_event() -> None:
"""An event with a non-oauth type returns None."""
event = MagicMock()
event.type = "response.output_text.delta"
assert try_parse_oauth_consent_event(event, "model-x") is None
def test_returns_none_for_event_without_type() -> None:
"""An event object missing a 'type' attribute returns None."""
event = object() # no type attribute
assert try_parse_oauth_consent_event(event, "model-x") is None
def test_parses_output_item_added_with_valid_link() -> None:
"""A response.output_item.added event with a valid HTTPS link produces Content."""
event = _make_output_item_event()
update = try_parse_oauth_consent_event(event, "test-model")
assert update is not None
assert update.role == "assistant"
assert update.model == "test-model"
assert update.raw_representation is event
consent = [c for c in update.contents if c.type == "oauth_consent_request"]
assert len(consent) == 1
assert consent[0].consent_link == "https://consent.example.com/auth"
def test_parses_top_level_consent_requested_event() -> None:
"""A response.oauth_consent_requested event produces Content."""
event = _make_top_level_event()
update = try_parse_oauth_consent_event(event, "test-model")
assert update is not None
consent = [c for c in update.contents if c.type == "oauth_consent_request"]
assert len(consent) == 1
assert consent[0].consent_link == "https://consent.example.com/authorize"
def test_empty_contents_for_non_https_link(caplog: pytest.LogCaptureFixture) -> None:
"""A non-HTTPS consent_link produces an update with empty contents and logs a warning."""
event = _make_output_item_event(consent_link="http://bad.example.com/login", item_id="item-http")
with caplog.at_level(logging.WARNING):
update = try_parse_oauth_consent_event(event, "test-model")
assert update is not None
assert len(update.contents) == 0
assert "non-HTTPS" in caplog.text
def test_empty_contents_for_missing_consent_link(caplog: pytest.LogCaptureFixture) -> None:
"""A None consent_link produces an update with empty contents and logs a warning."""
event = _make_output_item_event(consent_link=None, item_id="item-none")
with caplog.at_level(logging.WARNING):
update = try_parse_oauth_consent_event(event, "test-model")
assert update is not None
assert len(update.contents) == 0
assert "without valid consent_link" in caplog.text
def test_empty_contents_for_empty_string_consent_link(caplog: pytest.LogCaptureFixture) -> None:
"""An empty-string consent_link produces an update with empty contents and logs a warning."""
event = _make_output_item_event(consent_link="", item_id="item-empty")
with caplog.at_level(logging.WARNING):
update = try_parse_oauth_consent_event(event, "test-model")
assert update is not None
assert len(update.contents) == 0
assert "without valid consent_link" in caplog.text
def test_empty_contents_for_https_empty_netloc(caplog: pytest.LogCaptureFixture) -> None:
"""An HTTPS URL with empty netloc (https:///path) is rejected."""
event = _make_output_item_event(consent_link="https:///path", item_id="item-no-netloc")
with caplog.at_level(logging.WARNING):
update = try_parse_oauth_consent_event(event, "test-model")
assert update is not None
assert len(update.contents) == 0
assert "non-HTTPS" in caplog.text
# endregion