Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e0b1c4a
First commit for microsoft-agents-testing package
rodrigobr-msft Jul 27, 2026
5e14d0f
Another commit
rodrigobr-msft Jul 27, 2026
3cbdf3e
More updates
rodrigobr-msft Jul 27, 2026
0a99284
Test refactoring
rodrigobr-msft Jul 28, 2026
d47d935
Optimizing tests
rodrigobr-msft Jul 28, 2026
35f11c0
Updating devops
rodrigobr-msft Jul 28, 2026
de767a4
is versus == for exception raising indicator
rodrigobr-msft Jul 28, 2026
4d59fa4
Removing aiohttp dependency
rodrigobr-msft Jul 28, 2026
6d1ce7e
Merge branch 'main' of https://github.com/microsoft/Agents-for-python…
rodrigobr-msft Aug 18, 2026
44b3405
Renaming dev testing library to microsoft-agents-hosting-testing
rodrigobr-msft Aug 18, 2026
88959fb
Merge branch 'main' of https://github.com/microsoft/Agents-for-python…
rodrigobr-msft Aug 18, 2026
a16fe8f
Updating pyproject.toml and adding py.typed file to microsoft-agents-…
rodrigobr-msft Aug 18, 2026
9ae5577
Merge branch 'main' of https://github.com/microsoft/Agents-for-python…
rodrigobr-msft Aug 18, 2026
cb77751
Updating readme and changelog for new package
rodrigobr-msft Aug 18, 2026
91b7dde
Merge branch 'main' into users/robrandao/mocks
rodrigobr-msft Aug 19, 2026
96e4502
Improving coverage and utilizing pick_model
rodrigobr-msft Aug 19, 2026
65858e2
resolving merge conflict
rodrigobr-msft Aug 19, 2026
16a7e75
Merge branch 'users/robrandao/mocks' of https://github.com/microsoft/…
rodrigobr-msft Aug 19, 2026
8b460dc
Fixing malformed assertion that fails in older Python versions
rodrigobr-msft Aug 19, 2026
7278212
Adding testing package sample
rodrigobr-msft Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .azdo/ci-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ steps:
python -m pip install ./dist/microsoft_agents_hosting_fastapi*.whl
python -m pip install ./dist/microsoft_agents_storage_blob*.whl
python -m pip install ./dist/microsoft_agents_storage_cosmos*.whl
python -m pip install ./dist/microsoft_agents_testing*.whl
displayName: 'Install wheels'

- script: |
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ jobs:
python -m pip install ./dist/microsoft_agents_hosting_fastapi*.whl
python -m pip install ./dist/microsoft_agents_storage_blob*.whl
python -m pip install ./dist/microsoft_agents_storage_cosmos*.whl
python -m pip install ./dist/microsoft_agents_testing*.whl
- name: Test with pytest
run: |
pytest -W "ignore:SelectableGroups dict interface is deprecated. Use select.:DeprecationWarning"
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
**Release Date:** Unreleased
**Previous Version:** 1.4.0 (Released 2026-08-18)

## Major Features & Enhancements

- **Testing Package**: Added the new `microsoft-agents-testing` package with an in-memory `TestAdapter`, fluent `TestFlow` conversation assertions, and `MockUserTokenClient` support for testing OAuth and token-exchange flows without external services.

## Developer Experience

- **Authentication Configuration Validation**: Added validation for certificate, federated credential, and workload identity authentication settings
Expand Down
3 changes: 2 additions & 1 deletion dev/integration/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ dependencies = [
"microsoft-agents-hosting-aiohttp",
"microsoft-agents-hosting-dialogs",
"microsoft-agents-hosting-fastapi",
"microsoft-agents-testing @ file:///${PROJECT_ROOT}/../microsoft-agents-testing",
"microsoft-agents-hosting-testing @ file:///${PROJECT_ROOT}/../microsoft-agents-hosting-testing",
"microsoft-agents-testing @ file:///${PROJECT_ROOT}/../../libraries/microsoft-agents-testing",
]
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

"""Scenario definition for ActivityHandler-based dialog integration tests."""

from microsoft_agents.testing import (
from microsoft_agents.hosting.testing import (
ActivityHandlerEnvironment,
ActivityHandlerScenario,
ScenarioConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

import pytest

from microsoft_agents.testing import (
from microsoft_agents.hosting.testing import (
ActivityHandlerEnvironment,
ActivityHandlerScenario,
AgentClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import pytest

from microsoft_agents.activity import Activity, Attachment, ActivityTypes
from microsoft_agents.testing import AgentClient, ScenarioConfig, ClientConfig, ActivityTemplate
from microsoft_agents.hosting.testing import AgentClient, ScenarioConfig, ClientConfig, ActivityTemplate

from .scenario import create_dialog_scenario

Expand Down
261 changes: 67 additions & 194 deletions dev/integration/tests/auth/test_oauth_continuation.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,19 @@
Channels,
ResourceResponse,
SignInConstants,
SignInResource,
TokenExchangeResource,
TokenExchangeRequest,
TokenOrSignInResourceResponse,
TokenPostResource,
TokenResponse,
)
from microsoft_agents.hosting.core import TurnContext, TurnState, UserTokenClientBase
from microsoft_agents.testing import (
from microsoft_agents.hosting.core import TurnContext, TurnState
from microsoft_agents.hosting.testing import (
ActivityTemplate,
AgentClient,
AgentEnvironment,
AiohttpScenario,
ClientConfig,
ScenarioConfig,
)
from microsoft_agents.testing import MockUserTokenClient

_APP_ID = "test-app-id"
_CONVERSATION_ID = "auth-continuation-conversation"
Expand All @@ -40,7 +37,6 @@

class _AuthFlowTestState:
def reset(self) -> None:
self.token_available = False
self.exchange_requests = []
self.get_token_or_sign_in_calls = []
self.replay_started = asyncio.Event()
Expand All @@ -53,219 +49,96 @@ def reset(self) -> None:
_auth_flow.reset()


class _FakeUserToken:
def __init__(self, state: _AuthFlowTestState):
self._state = state
def _create_user_token_client(
state: _AuthFlowTestState,
) -> MockUserTokenClient:
client = MockUserTokenClient()
client.add_exchangeable_token(
connection_name=_OAUTH_CONNECTION_NAME,
channel_id=Channels.ms_teams,
user_id="user-id",
exchangeable_item="sso-token",
token="exchanged-token",
)

async def get_token(
self,
user_id: str,
connection_name: str,
channel_id: Optional[str] = None,
code: Optional[str] = None,
) -> TokenResponse:
if self._state.token_available:
return TokenResponse(
connection_name=connection_name,
token="cached-token",
channel_id=channel_id,
)
return TokenResponse()
get_sign_in_resource = client.get_sign_in_resource

async def _get_token_or_sign_in_resource(
self,
user_id: str,
async def get_sign_in_resource_with_fixed_exchange_id(
connection_name: str,
channel_id: str,
state: str,
code: str = "",
final_redirect: str = "",
fwd_url: str = "",
) -> TokenOrSignInResourceResponse:
self._state.get_token_or_sign_in_calls.append(
{
"user_id": user_id,
"connection_name": connection_name,
"channel_id": channel_id,
}
)
if self._state.token_available:
return TokenOrSignInResourceResponse(
token_response=TokenResponse(
connection_name=connection_name,
token="cached-token",
channel_id=channel_id,
)
)
return TokenOrSignInResourceResponse(
sign_in_resource=SignInResource(
sign_in_link="https://example.test/signin",
token_exchange_resource=TokenExchangeResource(
id=_TOKEN_EXCHANGE_ID,
uri="api://test-token-exchange",
provider_id="test-provider",
),
token_post_resource=TokenPostResource(
sas_url="https://example.test/token-post"
),
)
activity: Activity,
final_redirect: str | None = None,
):
resource = await get_sign_in_resource(
connection_name,
activity,
final_redirect,
)
resource.token_exchange_resource.id = _TOKEN_EXCHANGE_ID
return resource

async def get_aad_tokens(
self,
user_id: str,
connection_name: str,
channel_id: Optional[str] = None,
body: Optional[dict] = None,
) -> dict[str, TokenResponse]:
return {}
client.get_sign_in_resource = get_sign_in_resource_with_fixed_exchange_id

async def sign_out(
self,
user_id: str,
connection_name: Optional[str] = None,
channel_id: Optional[str] = None,
) -> None:
self._state.token_available = False
get_token_or_sign_in_resource = client.get_token_or_sign_in_resource

async def get_token_status(
self,
user_id: str,
channel_id: Optional[str] = None,
include: Optional[str] = None,
) -> list:
return []

async def exchange_token(
self,
user_id: str,
async def get_token_or_sign_in_resource_with_tracking(
connection_name: str,
channel_id: str,
body: Optional[dict] = None,
) -> TokenResponse:
self._state.exchange_requests.append(
activity: Activity,
code: str | None = None,
final_redirect: str | None = None,
fwd_url: str | None = None,
):
state.get_token_or_sign_in_calls.append(
{
"user_id": user_id,
"user_id": activity.from_property.id,
"connection_name": connection_name,
"channel_id": channel_id,
"body": body,
"channel_id": activity.channel_id,
}
)
self._state.token_available = True
return TokenResponse(
connection_name=connection_name,
token="exchanged-token",
channel_id=channel_id,
)


class _FakeUserTokenClient(UserTokenClientBase):
def __init__(self, state: _AuthFlowTestState):
self._user_token = _FakeUserToken(state)

@property
def user_token(self) -> _FakeUserToken:
return self._user_token

@property
def agent_sign_in(self):
return None

async def get_user_token(
self,
user_id: str,
connection_name: str,
channel_id: str,
magic_code: str | None = None,
) -> TokenResponse:
return await self._user_token.get_token(
user_id,
connection_name,
channel_id,
code=magic_code,
)

async def get_sign_in_resource(
self,
connection_name: str,
activity: Activity,
final_redirect: str | None = None,
) -> SignInResource:
response = await self.get_token_or_sign_in_resource(
return await get_token_or_sign_in_resource(
connection_name,
activity,
final_redirect=final_redirect,
code,
final_redirect,
fwd_url,
)
return response.sign_in_resource

async def sign_out_user(
self,
user_id: str,
connection_name: str,
channel_id: str,
) -> None:
await self._user_token.sign_out(user_id, connection_name, channel_id)
client.get_token_or_sign_in_resource = (
get_token_or_sign_in_resource_with_tracking
)

async def get_token_status(
self,
user_id: str,
channel_id: str,
include: str | None = None,
) -> list:
return await self._user_token.get_token_status(
user_id,
channel_id,
include=include,
)
exchange_token = client.exchange_token

async def get_aad_tokens(
self,
user_id: str,
connection_name: str,
resource_urls: list[str],
channel_id: str,
) -> dict[str, TokenResponse]:
return await self._user_token.get_aad_tokens(
user_id,
connection_name,
channel_id,
{"resourceUrls": resource_urls},
)

async def exchange_token(
self,
async def exchange_token_with_tracking(
user_id: str,
connection_name: str,
channel_id: str,
exchange_request: TokenExchangeRequest,
) -> TokenResponse:
return await self._user_token.exchange_token(
state.exchange_requests.append(
{
"user_id": user_id,
"connection_name": connection_name,
"channel_id": channel_id,
"body": exchange_request.model_dump(exclude_none=True),
}
)
response = await exchange_token(
user_id,
connection_name,
channel_id,
exchange_request.model_dump(exclude_none=True),
)

async def get_token_or_sign_in_resource(
self,
connection_name: str,
activity: Activity,
code: str | None = None,
final_redirect: str | None = None,
fwd_url: str | None = None,
) -> TokenOrSignInResourceResponse:
return await self._user_token._get_token_or_sign_in_resource(
user_id=activity.from_property.id,
connection_name=connection_name,
channel_id=activity.channel_id,
state="test-state",
code=code or "",
final_redirect=final_redirect or "",
fwd_url=fwd_url or "",
exchange_request,
)
if response.token:
client.add_user_token(
connection_name=connection_name,
channel_id=channel_id,
user_id=user_id,
token=response.token,
)
return response

async def close(self) -> None:
return None
client.exchange_token = exchange_token_with_tracking
return client


class _FakeConversations:
Expand Down Expand Up @@ -329,7 +202,7 @@ async def close(self) -> None:

class _FakeChannelServiceClientFactory:
def __init__(self, state: _AuthFlowTestState):
self._user_token_client = _FakeUserTokenClient(state)
self._user_token_client = _create_user_token_client(state)

async def create_connector_client(
self,
Expand All @@ -347,7 +220,7 @@ async def create_user_token_client(
context,
claims_identity,
use_anonymous: bool = False,
) -> _FakeUserTokenClient:
) -> MockUserTokenClient:
return self._user_token_client


Expand Down
Loading
Loading