Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 1 addition & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,4 @@ repos:
rev: 22.3.0 # Use the latest stable version of Black
hooks:
- id: black
args: [--line-length=88] # Adjust line length as needed
- repo: https://github.com/pycqa/flake8
rev: 7.2.0 # pick a git hash / tag to point to
hooks:
- id: flake8
args: [--line-length=88] # Adjust line length as needed
File renamed without changes.
91 changes: 91 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Agents SDK for Python Testing

This document serves as a quick guide to the utilities we provide internally to test this SDK. More information will come with work on integration testing.

## Storage Tests

More info soon. For now, there are flags defined in the code that dictate whether the Cosmos DB and the Blob storage tests are run, as these tests rely on local emulators.

## Mocking

When using a class or function that takes a `mocker` argument, it is highly recommended that you pass in the Pytest fixture obtained either in:

a test

```python
def test_thing(self, some_fixture, mocker):
mock_UserTokenClient(mocker)
```

the setup method of a class

```python
class TestThing:

def setup_method(self, mocker):
self.msal_auth = MockMsalAuth(mocker)
```

or in another fixture

```python
@pytest.fixture
def user_token_client(self, mocker):
return mock_OAuthFlow(mocker, get_user_token_return="token")
```

This ensures Pytest will correctly manage the lifetime of your mocking logic to occur within the score of the individual test or class.

## Directory Structure


### Module Names

This directory follows the same structure as the package structure. If there is a test module that tests the functionality of a module in the packages source code, then please use the format `test_{ORIGINAL_MODULE}.py` file naming convention. If a module is an extra module that only exists in testing and has no counterpart in the packages source code, then it is encouraged that you prefix it with an underscore. The only exception is if a parent (or ancestor) directory of the testing Python file already has an underscore prefix. For example:

```
microsoft_agents/hosting/core
__init__.py

_common/
__init__.py
bar.py

app/
__init__.py
_helper.py
test_agent_application.py

_foo.py
test_turn_context.py
test_utils.py

```

### `tests/_common`

This directory contains common utilities for testing.

### `tests/_common/_tests`

Tests for testing utilities.

### `tests/_common/data`

Here, we encourage defining the data within the constructors of a class. I guess this helps clean up imports, but the primary purpose is to prevent tests from mutating these testing constants and interfering with other tests. The `test_defaults.py` module defines defaults for fundamental variables such as `user_id` and `channel_id`. These defaults are used in tests as well as other data constructors in `tests/_common/data`. For instance `test_flow_data.py` builds on those defaults to define FlowState objects for testing, and then `tests/_common/storage_data` builds even further on that.

### `tests/_common/storage`

Storage related functionality. This was the first shared testing utility module, so this can eventually be reorganized into the rest of the `tests/_common` structure.

### `tests/_common/fixtures`

This directory holds common fixtures that might be useful. For instance, `FlowStateFixtures` provides fixtures that are useful for testing both the OAuthFlow and Authorization classes, and it relies on `tests/_common/data/test_flow_data.py` module.

### `tests/_common/testing_objects`

This directory contains implementations and functions to construct objects that implement/extend core Protocols or mock existing functionality. In this repo, `testing_{class_name}.py` modules contain simple implementations with predictable behavior while `mock_{class_name}.py` usually contain `mock_{class_name}` and `mock_class_{class_name}` functions that wrap existing instances/classes with Pytest's mocking capabilities.

### `tests/_integration`

At the moment, there is no actual real integration testing, but there is a foundation that lets you define an environmental setup and then run a "sample" on that environment.
5 changes: 5 additions & 0 deletions tests/_common/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .approx_equal import approx_eq

__all__ = [
"approx_eq",
]
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class TestMockOAuthFlow:
def test_mock_oauth_flow(self):
pass
2 changes: 2 additions & 0 deletions tests/_common/approx_equal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def approx_eq(a: float, b: float, tol: float = 1e-9) -> bool:
return abs(a - b) <= tol
15 changes: 15 additions & 0 deletions tests/_common/data/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from .test_defaults import TEST_DEFAULTS
from .test_auth_data import (
TEST_AUTH_DATA,
create_test_auth_handler,
)
from .test_storage_data import TEST_STORAGE_DATA
from .test_flow_data import TEST_FLOW_DATA

__all__ = [
"TEST_DEFAULTS",
"TEST_AUTH_DATA",
"TEST_STORAGE_DATA",
"TEST_FLOW_DATA",
"create_test_auth_handler",
]
41 changes: 41 additions & 0 deletions tests/_common/data/test_auth_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from microsoft_agents.hosting.core import AuthHandler


def create_test_auth_handler(
name: str, obo: bool = False, title: str = None, text: str = None
) -> AuthHandler:
"""
Creates a test AuthHandler instance with standardized connection names.

This helper function simplifies the creation of AuthHandler objects for testing
by automatically generating connection names based on the provided name and
optionally including On-Behalf-Of (OBO) connection configuration.

Args:
name: Base name for the auth handler, used to generate connection names
obo: Whether to include On-Behalf-Of connection configuration
title: Optional title for the auth handler
text: Optional descriptive text for the auth handler

Returns:
AuthHandler: Configured auth handler instance with test-friendly connection names
"""
return AuthHandler(
name,
abs_oauth_connection_name=f"{name}-abs-connection",
obo_connection_name=f"{name}-obo-connection" if obo else None,
title=title,
text=text,
)


class TEST_AUTH_DATA:
def __init__(self):

self.auth_handler: AuthHandler = create_test_auth_handler("graph")

self.auth_handlers: dict[str, AuthHandler] = {
"graph": create_test_auth_handler("graph"),
"github": create_test_auth_handler("github", obo=True),
"slack": create_test_auth_handler("slack", obo=True),
}
20 changes: 20 additions & 0 deletions tests/_common/data/test_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from microsoft_agents.activity import Activity, ActivityTypes

from microsoft_agents.hosting.core import (
AuthHandler,
TurnContext,
)


class TEST_DEFAULTS:
def __init__(self):

self.token = "__token"
self.channel_id = "__channel_id"
self.user_id = "__user_id"
self.bot_url = "https://botframework.com"
self.ms_app_id = "__ms_app_id"
self.abs_oauth_connection_name = "__connection_name"
self.missing_abs_oauth_connection_name = "__missing_connection_name"

self.auth_handlers = [AuthHandler()]
143 changes: 143 additions & 0 deletions tests/_common/data/test_flow_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
from datetime import datetime

from microsoft_agents.hosting.core.oauth.flow_state import FlowState, FlowStateTag

from tests._common.storage import MockStoreItem
from tests._common.data.test_defaults import TEST_DEFAULTS

DEFAULTS = TEST_DEFAULTS()

DEF_FLOW_ARGS = {
"ms_app_id": DEFAULTS.ms_app_id,
"channel_id": DEFAULTS.channel_id,
"user_id": DEFAULTS.user_id,
"connection": DEFAULTS.abs_oauth_connection_name,
}


class TEST_FLOW_DATA:
def __init__(self):

self.not_started = FlowState(
**DEF_FLOW_ARGS,
tag=FlowStateTag.NOT_STARTED,
attempts_remaining=1,
user_token="____",
expiration=datetime.now().timestamp() + 1000000,
)

self.started = FlowState(
**DEF_FLOW_ARGS,
tag=FlowStateTag.BEGIN,
attempts_remaining=1,
user_token="____",
expiration=datetime.now().timestamp() + 1000000,
)

self.started_one_retry = FlowState(
**DEF_FLOW_ARGS,
tag=FlowStateTag.BEGIN,
attempts_remaining=2,
user_token="____",
expiration=datetime.now().timestamp() + 1000000,
)

self.active = FlowState(
**DEF_FLOW_ARGS,
tag=FlowStateTag.CONTINUE,
attempts_remaining=2,
user_token="__token",
expiration=datetime.now().timestamp() + 1000000,
)

self.active_one_retry = FlowState(
**DEF_FLOW_ARGS,
tag=FlowStateTag.CONTINUE,
attempts_remaining=1,
user_token="__token",
expiration=datetime.now().timestamp() + 1000000,
)
self.active_exp = FlowState(
**DEF_FLOW_ARGS,
tag=FlowStateTag.CONTINUE,
attempts_remaining=2,
user_token="__token",
expiration=datetime.now().timestamp(),
)
self.completed = FlowState(
**DEF_FLOW_ARGS,
tag=FlowStateTag.COMPLETE,
attempts_remaining=2,
user_token="test_token",
expiration=datetime.now().timestamp() + 1000000,
)
self.fail_by_attempts = FlowState(
**DEF_FLOW_ARGS,
tag=FlowStateTag.FAILURE,
attempts_remaining=0,
expiration=datetime.now().timestamp() + 1000000,
)

self.fail_by_exp = FlowState(
**DEF_FLOW_ARGS,
tag=FlowStateTag.FAILURE,
attempts_remaining=2,
expiration=0,
)

@staticmethod
def model_copy_list(lst):
return [flow_state.model_copy() for flow_state in lst]

def all_flows(self):
return self.model_copy_list(
[
self.started,
self.started_one_retry,
self.active,
self.active_one_retry,
self.active_exp,
self.completed,
self.fail_by_attempts,
self.fail_by_exp,
]
)

def failed_flows(self):
return self.model_copy_list(
[
self.active_exp,
self.fail_by_attempts,
self.fail_by_exp,
]
)

def active_flows(self):
return self.model_copy_list(
[
self.started,
self.started_one_retry,
self.active,
self.active_one_retry,
]
)

def inactive_flows(self):
return self.model_copy_list(
[
self.active_exp,
self.completed,
self.fail_by_attempts,
self.fail_by_exp,
]
)


def flow_key(channel_id, user_id, handler_id):
return f"auth/{channel_id}/{user_id}/{handler_id}"


def update_flow_state_handler(flow_state, handler):
flow_state = flow_state.model_copy()
flow_state.auth_handler_id = handler
return flow_state
50 changes: 50 additions & 0 deletions tests/_common/data/test_storage_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from tests._common.storage import MockStoreItem

from .test_flow_data import (
TEST_FLOW_DATA,
FlowState,
update_flow_state_handler,
flow_key,
)

FLOW_DATA = TEST_FLOW_DATA()


class TEST_STORAGE_DATA:
def __init__(self):

self.dict = {
"user_id": MockStoreItem({"id": "123"}),
"session_id": MockStoreItem({"id": "abc"}),
flow_key("webchat", "Alice", "graph"): update_flow_state_handler(
FLOW_DATA.completed, "graph"
),
flow_key("webchat", "Alice", "github"): update_flow_state_handler(
FLOW_DATA.active_one_retry, "github"
),
flow_key("teams", "Alice", "graph"): update_flow_state_handler(
FLOW_DATA.started, "graph"
),
flow_key("webchat", "Bob", "graph"): update_flow_state_handler(
FLOW_DATA.active_exp, "graph"
),
flow_key("teams", "Bob", "slack"): update_flow_state_handler(
FLOW_DATA.started, "slack"
),
flow_key("webchat", "Chuck", "github"): update_flow_state_handler(
FLOW_DATA.fail_by_attempts, "github"
),
}

def get_init_data(self):
data = self.dict.copy()
for key, value in data.items():
data[key] = value.model_copy() if isinstance(value, FlowState) else value
return data


def update_data_with_flow_state(data, channel_id, user_id, auth_handler_id, flow_state):
data = data.copy()
key = f"auth/{channel_id}/{user_id}/{auth_handler_id}"
data[key] = flow_state.model_copy()
return data
5 changes: 5 additions & 0 deletions tests/_common/fixtures/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .flow_state_fixtures import FlowStateFixtures

__all__ = [
"FlowStateFixtures",
]
Loading